package httputil

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"strconv"

	"github.com/go-resty/resty/v2"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	"go.uber.org/zap"

	"github.com/tidbcloud/tidb-management-service/utilities/grpc"
)

// DoRequest sends a http request.
// It has tracing, metrics and log equipped.
// No status code check is done here. It's up to the caller to check the status code.
// Params:
// - method: string, http method, e.g. `http.MethodGet`
// - baseUrl: string, [scheme]://[host][:port], e.g. "http://localhost:8080"
// - apiPath: string, the path of the api, could be empty, e.g. "/api/v1/health"
func DoRequest(
	ctx context.Context,
	client *resty.Client,
	method, baseUrl, apiPath string,
	setParam func(req *resty.Request),
) (*resty.Response, error) {
	// Set tracing.
	if _, ok := client.GetClient().Transport.(*otelhttp.Transport); !ok {
		client.SetTransport(otelhttp.NewTransport(client.GetClient().Transport))
	}

	// Build the request.
	request := client.R().EnableTrace().SetContext(ctx)
	setParam(request)

	// Build the url.
	if baseUrl[len(baseUrl)-1] == '/' {
		baseUrl = baseUrl[:len(baseUrl)-1]
	}
	if apiPath != "" && apiPath[0] != '/' {
		apiPath = "/" + apiPath
	}
	beforeUrl := baseUrl + apiPath

	// Execute the request.
	resp, err := request.Execute(method, beforeUrl)
	if err != nil {
		var netErr net.Error
		switch {
		case errors.Is(err, context.Canceled):
			mRequestTotalTime.WithLabelValues(beforeUrl, method, apiPath, "context.Canceled").
				Observe(resp.Request.TraceInfo().TotalTime.Seconds())
		case errors.Is(err, context.DeadlineExceeded):
			mRequestTotalTime.WithLabelValues(beforeUrl, method, apiPath, "context.DeadlineExceeded").
				Observe(resp.Request.TraceInfo().TotalTime.Seconds())
		case errors.As(err, &netErr):
			mRequestTotalTime.WithLabelValues(beforeUrl, method, apiPath, "net.Error").
				Observe(resp.Request.TraceInfo().TotalTime.Seconds())
		}
		return nil, fmt.Errorf("http request execute failed: %w", err)
	}
	if resp == nil {
		return nil, errors.New("empty response")
	}

	// Extract host[:port] from afterUrl.
	afterUrl, err := url.Parse(resp.Request.URL)
	if err != nil {
		return nil, fmt.Errorf("parse url failed: %w", err)
	}

	// Record the metrics.
	statusCodeStr := strconv.Itoa(resp.StatusCode())
	mRequestTotalTime.WithLabelValues(afterUrl.Host, method, apiPath, statusCodeStr).
		Observe(resp.Request.TraceInfo().TotalTime.Seconds())
	mRequestResponseTime.WithLabelValues(afterUrl.Host, method, apiPath, statusCodeStr).
		Observe(resp.Request.TraceInfo().ResponseTime.Seconds())
	mRequestTLSHandshakeTime.WithLabelValues(afterUrl.Host, method, apiPath, statusCodeStr).
		Observe(resp.Request.TraceInfo().TLSHandshake.Seconds())
	mRequestTCPConnTime.WithLabelValues(afterUrl.Host, method, apiPath, statusCodeStr).
		Observe(resp.Request.TraceInfo().TCPConnTime.Seconds())
	mRequestConnTime.WithLabelValues(afterUrl.Host, method, apiPath, statusCodeStr).
		Observe(resp.Request.TraceInfo().ConnTime.Seconds())
	mRequestDNSLookupTime.WithLabelValues(afterUrl.Host, method, apiPath, statusCodeStr).
		Observe(resp.Request.TraceInfo().DNSLookup.Seconds())

	var reqBody string
	if request.Body != nil {
		switch b := request.Body.(type) {
		case []byte:
			reqBody = string(b)
		case string:
			reqBody = b
		default:
			bs, err := json.Marshal(request.Body)
			if err != nil {
				tilog.Error(ctx, "marshal request body failed", zap.Error(err), zap.Any("body", request.Body))
			} else {
				reqBody = string(bs)
			}
		}
	}

	redactedReqBody := grpc.RedactSecrets(reqBody)
	redactedRespBody := grpc.RedactSecrets(string(resp.Body()))

	// Log the request and response.
	if resp.StatusCode() < http.StatusOK || resp.StatusCode() >= http.StatusMultipleChoices {
		grpc.GetPayloadLogger(ctx).Error(
			"---> http client (resty)",
			zap.String("method", method),
			zap.Int("status", resp.StatusCode()),
			zap.String("url", resp.Request.URL),
			zap.String("request", redactedReqBody),
			zap.String("response", redactedRespBody),
			zap.Duration("cost", resp.Request.TraceInfo().TotalTime),
		)
	} else {
		grpc.GetPayloadLogger(ctx).Info(
			"---> http client (resty)",
			zap.String("method", method),
			zap.Int("status", resp.StatusCode()),
			zap.String("url", resp.Request.URL),
			zap.String("request", redactedReqBody),
			zap.String("response", redactedRespBody),
			zap.Duration("cost", resp.Request.TraceInfo().TotalTime),
		)
	}

	return resp, nil
}
