package api

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	ul "net/url"
	"path/filepath"
	"strings"

	"github.com/google/go-querystring/query"
	"gitlink.org.cn/cloudream/common/pkgs/iterator"
	"gitlink.org.cn/cloudream/common/sdks"
	"gitlink.org.cn/cloudream/common/utils/http2"
	"gitlink.org.cn/cloudream/common/utils/math2"
	"gitlink.org.cn/cloudream/common/utils/serder"
)

func MakeIPFSFilePath(fileHash string) string {
	return filepath.Join("ipfs", fileHash)
}

func ParseJSONResponse[TBody any](resp *http.Response) (TBody, error) {
	var ret TBody
	contType := resp.Header.Get("Content-Type")
	if strings.Contains(contType, http2.ContentTypeJSON) {
		var err error
		if ret, err = serder.JSONToObjectStreamEx[TBody](resp.Body); err != nil {
			return ret, fmt.Errorf("parsing response: %w", err)
		}

		return ret, nil
	}

	cont, err := io.ReadAll(resp.Body)
	if err != nil {
		return ret, fmt.Errorf("unknow response content type: %s, status: %d", contType, resp.StatusCode)
	}
	strCont := string(cont)

	return ret, fmt.Errorf("unknow response content type: %s, status: %d, body(prefix): %s", contType, resp.StatusCode, strCont[:math2.Min(len(strCont), 200)])
}

func JSONAPI[Resp sdks.APIResponse, Req sdks.APIRequest](cfg *Config, cli *http.Client, req Req, resp Resp) (Resp, error) {

	param := req.MakeParam()

	httpReq, err := param.MakeRequest(cfg.URL)
	if err != nil {
		return resp, err
	}

	httpResp, err := cli.Do(httpReq)
	if err != nil {
		return resp, err
	}

	err = resp.ParseResponse(httpResp)
	return resp, err
}

func JSONAPINoData[Req sdks.APIRequest](cfg *Config, cli *http.Client, req Req) error {
	param := req.MakeParam()

	httpReq, err := param.MakeRequest(cfg.URL)
	if err != nil {
		return err
	}

	resp, err := cli.Do(httpReq)
	if err != nil {
		return err
	}

	return sdks.ParseCodeDataJSONResponse(resp, any(nil))
}

func calcSha256(body sdks.RequestBody) string {
	hasher := sha256.New()
	switch body := body.(type) {
	case *sdks.StringBody:
		hasher.Write([]byte(body.Value))
		return hex.EncodeToString(hasher.Sum(nil))

	case *sdks.BytesBody:
		hasher.Write(body.Value)
		return hex.EncodeToString(hasher.Sum(nil))

	case *sdks.StreamBody:
		return ""

	default:
		hash := sha256.Sum256([]byte(""))
		return hex.EncodeToString(hash[:])
	}
}

func PostMultiPart(cfg *Config, url string, info any, files http2.MultiPartFileIterator) (*http.Response, error) {
	req, err := http.NewRequest(http.MethodPost, url, nil)
	if err != nil {
		return nil, err
	}

	pr, pw := io.Pipe()
	muWriter := multipart.NewWriter(pw)

	req.Header.Set("Content-Type", fmt.Sprintf("%s;boundary=%s", http2.ContentTypeMultiPart, muWriter.Boundary()))

	writeResult := make(chan error, 1)
	go func() {
		writeResult <- func() error {
			defer pw.Close()
			defer muWriter.Close()

			if info != nil {
				mp, err := query.Values(info)
				if err != nil {
					return fmt.Errorf("formValues object to map failed, err: %w", err)
				}

				for k, v := range mp {
					err := muWriter.WriteField(k, v[0])
					if err != nil {
						return fmt.Errorf("write form field failed, err: %w", err)
					}
				}
			}

			for {
				file, err := files.MoveNext()
				if err == iterator.ErrNoMoreItem {
					break
				}
				if err != nil {
					return fmt.Errorf("opening file: %w", err)
				}

				err = sendFileOnePart(muWriter, file.FieldName, file.FileName, file.File)
				file.File.Close()
				if err != nil {
					return err
				}
			}

			return nil
		}()
	}()

	req.Body = pr

	cli := http.Client{}
	resp, err := cli.Do(req)
	if err != nil {
		return nil, err
	}

	writeErr := <-writeResult
	if writeErr != nil {
		return nil, writeErr
	}

	return resp, nil
}

func sendFileOnePart(muWriter *multipart.Writer, fieldName, fileName string, file io.ReadCloser) error {
	w, err := muWriter.CreateFormFile(fieldName, ul.PathEscape(fileName))
	if err != nil {
		return fmt.Errorf("create form file failed, err: %w", err)
	}

	_, err = io.Copy(w, file)
	return err
}
