// Copyright 2025 The HuaTuo Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package request

import (
	"bufio"
	"bytes"
	"encoding/json"
	"io"
	"net"
	"net/http"
	"runtime"
	"syscall"
)

const (
	cmsgDataMax = 200 * 4
	dataMax     = 3072
)

// RawHTTP is a structure that describes a Info of raw Http
type RawHTTP struct {
	conn   *net.UnixConn
	method string
}

func parsePerfFds(data []byte) ([]int, error) {
	// parse control msgs
	var msgs []syscall.SocketControlMessage
	msgs, err := syscall.ParseSocketControlMessage(data)
	if err != nil {
		return nil, err
	}

	resFds := []int{}
	for i := range msgs {
		var fds []int
		fds, err := syscall.ParseUnixRights(&msgs[i])
		if err != nil {
			return nil, err
		}

		resFds = append(resFds, fds...)
	}

	return resFds, nil
}

func makeRequestRawBuf(method, url, sid string, body io.Reader) (*bytes.Buffer, error) {
	// send request
	sbuf := new(bytes.Buffer)

	req, err := http.NewRequest(method, url, body)
	if err != nil {
		return nil, err
	}
	req.Header.Add("X-HUATUO-SID", sid)
	req.Header.Set("Content-Type", "application/json")

	if err := req.Write(sbuf); err != nil {
		return nil, err
	}

	return sbuf, err
}

func connectUnix(sock string) (*net.UnixConn, error) {
	c, err := net.Dial("unix", sock)
	if err != nil {
		return nil, err
	}

	// auto clean
	runtime.SetFinalizer(c, (*net.UnixConn).Close)

	return c.(*net.UnixConn), nil
}

// SendRequest Provide a standard interface for sending http to the security agent
func (h *RawHTTP) SendRequest(sock, method, url, sid string, body io.Reader) error {
	conn, err := connectUnix(sock)
	if err != nil {
		return err
	}

	buf, err := makeRequestRawBuf(method, url, sid, body)
	if err != nil {
		return err
	}

	// write raw http data
	_, err = conn.Write(buf.Bytes())
	if err != nil {
		return err
	}

	h.conn = conn
	h.method = method

	return nil
}

func setCmsgtoResp(resp *http.Response, fds []int) (*http.Response, error) {
	// Resp.Body we will re assign the value so close it
	resp.Body.Close()

	// Construct body info, we only transfer fds
	body := make(map[string]any)
	body["fds"] = &fds

	newbody, err := json.Marshal(body)
	if err != nil {
		return nil, err
	}

	resp.Body = io.NopCloser(bytes.NewReader(newbody))
	return resp, nil
}

func noExistCmsg(length int) bool {
	// If length is 0, it means that data is only http response data,
	return length == 0
}

func handlerResponse(method string, data []byte) (*http.Response, error) {
	return http.ReadResponse(bufio.NewReader(bytes.NewReader(data)), &http.Request{Method: method})
}

func handlerCmsgAndResponse(method string, requestFD int, cmsgData []byte) (*http.Response, error) {
	// When the count of CPUs exceeds 200, there can receive multiple responses within cmsgs.
	var allFds []int

	// The first response contains cmsg.
	fds, err := parsePerfFds(cmsgData)
	if err != nil {
		return nil, err
	}
	allFds = append(allFds, fds...)

	for {
		// Read the next response which could contain cmsg.
		httpData, cmsgData, err := readCmsgAndResponse(requestFD)
		if err != nil {
			return nil, err
		}

		// Judge cmsgData to determine whether data is only HTTP response data, and exit
		// the loop.
		if noExistCmsg(len(cmsgData)) {
			resp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(httpData)), &http.Request{Method: method})
			if err != nil {
				return nil, err
			}

			return setCmsgtoResp(resp, allFds)
		}

		// Parse cmsg
		fds, err := parsePerfFds(cmsgData)
		if err != nil {
			return nil, err
		}
		allFds = append(allFds, fds...)
	}
}

// readCmsgAndResponse Provide a standard interface for reading cmsg and http response to
// the security agent.
func readCmsgAndResponse(fd int) ([]byte, []byte, error) {
	// The data is divided into two parts:
	// 1. [:cmsgLen] as cmsg
	// 2. [cmsgLen:] as http response data
	data := make([]byte, dataMax)
	cmsgLen := syscall.CmsgSpace(cmsgDataMax)

	cmsgData := data[:cmsgLen]
	httpData := data[cmsgLen:]

	// Read data: There are two cases here
	// 1. If cLen is 0, only HTTP response data is received.
	// 2. If cLen is not 0, it means that cmsg has been received and next recv HTTP response.
	_, cLen, _, _, err := syscall.Recvmsg(fd, httpData, cmsgData, 0)
	if err != nil {
		return nil, nil, err
	}

	return httpData, cmsgData[:cLen], nil
}

// ReadResponse Provide a standard interface for reading http response to the security agent
func (h *RawHTTP) ReadResponse() (*http.Response, error) {
	connFile, err := h.conn.File()
	if err != nil {
		return nil, err
	}

	defer connFile.Close()
	defer h.conn.Close()

	httpData, cmsgData, err := readCmsgAndResponse(int(connFile.Fd()))
	if err != nil {
		return nil, err
	}

	// Judge cLen to determine whether data is only HTTP response data
	if noExistCmsg(len(cmsgData)) {
		return handlerResponse(h.method, httpData)
	}

	// set cmsg to response body
	return handlerCmsgAndResponse(h.method, int(connFile.Fd()), cmsgData)
}

// EncodeBody encode the body to io.Reader for Post body
func EncodeBody(obj any) (io.Reader, error) {
	if obj == nil {
		return nil, nil
	}
	body, err := encodeData(obj)
	if err != nil {
		return nil, err
	}

	return body, nil
}
