package proxy

import (
	"bytes"
	"context"
	"io"
	"net/http"
	stdurl "net/url"
	"strings"

	"github.com/pingcap-inc/clinic/pkg/auth/session"
)

type O11yPromClient struct {
	targetHost        string
	requestModifyFunc func(r *http.Request) *http.Request
}

func NewO11PromClient(oid uint, clusterID string, user *session.CtxUser, handler *TiDBClusterMetricHandler) (*O11yPromClient, error) {
	_, targetHost, requestModifyFunc, _, err := handler.handle(context.Background(), "cloud", oid, clusterID, user, MetricScopeChild, "", func(string2 string) string { //nolint:bodyclose
		return string2
	})
	if err != nil {
		return nil, err
	}

	return &O11yPromClient{
		targetHost:        targetHost,
		requestModifyFunc: requestModifyFunc,
	}, nil
}

func (cli *O11yPromClient) URL(ep string, args map[string]string) *stdurl.URL {
	u := &stdurl.URL{
		Scheme: "http",
		Host:   cli.targetHost,
	}

	u.Path = ep
	u.RawPath = ep

	return u
}

// maxPrometheusQueryGETBytes caps URL length when converting POST /query(_range) to GET; longer bodies stay POST.
const maxPrometheusQueryGETBytes = 256 * 1024

// prometheusFormPOSTToGET converts Prometheus API client POSTs to GET with query string.
// Some O11y / gateway stacks do not surface form bodies to the metrics backend and return 422 missing `query`;
// upstream client_golang only falls back to GET on 405/501, not 422.
func prometheusFormPOSTToGET(req *http.Request) *http.Request {
	if req.Method != http.MethodPost {
		return req
	}
	ct := req.Header.Get("Content-Type")
	if !strings.HasPrefix(strings.ToLower(strings.TrimSpace(ct)), "application/x-www-form-urlencoded") {
		return req
	}
	path := req.URL.Path
	isQuery := strings.HasSuffix(path, "/query") && !strings.HasSuffix(path, "/query_range")
	isQueryRange := strings.Contains(path, "/query_range")
	if !isQuery && !isQueryRange {
		return req
	}
	if req.Body == nil {
		return req
	}
	body, err := io.ReadAll(req.Body)
	_ = req.Body.Close()
	if err != nil || len(body) == 0 {
		if len(body) > 0 {
			req.Body = io.NopCloser(bytes.NewReader(body))
			req.ContentLength = int64(len(body))
		}
		return req
	}
	values, err := stdurl.ParseQuery(string(body))
	if err != nil {
		req.Body = io.NopCloser(bytes.NewReader(body))
		req.ContentLength = int64(len(body))
		return req
	}
	encoded := values.Encode()
	if len(encoded) > maxPrometheusQueryGETBytes {
		req.Body = io.NopCloser(bytes.NewReader(body))
		req.ContentLength = int64(len(body))
		return req
	}
	u := *req.URL
	u.RawQuery = encoded
	getReq, err := http.NewRequestWithContext(req.Context(), http.MethodGet, u.String(), nil)
	if err != nil {
		req.Body = io.NopCloser(bytes.NewReader(body))
		req.ContentLength = int64(len(body))
		return req
	}
	for k, v := range req.Header {
		if strings.EqualFold(k, "Content-Type") || strings.EqualFold(k, "Content-Length") {
			continue
		}
		getReq.Header[k] = v
	}
	return getReq
}

func (cli *O11yPromClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
	if ctx != nil {
		req = req.WithContext(ctx)
	}

	newReq := cli.requestModifyFunc(req)
	newReq = prometheusFormPOSTToGET(newReq)
	resp, err := http.DefaultClient.Do(newReq)
	defer func() {
		if resp != nil {
			resp.Body.Close()
		}
	}()

	if err != nil {
		return nil, nil, err
	}

	var body []byte
	done := make(chan struct{})
	go func() {
		var buf bytes.Buffer
		_, err = buf.ReadFrom(resp.Body)
		body = buf.Bytes()
		close(done)
	}()

	select {
	case <-ctx.Done():
		<-done
		err = resp.Body.Close()
		if err == nil {
			err = ctx.Err()
		}
	case <-done:
	}

	return resp, body, err
}
