// Package regionalapi add by blacknc <blacknc@163.com> at 2024/09/02 15:59:38
package regionalapi

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"time"

	"github.com/pingcap/errors"
	"github.com/tidbcloud/dataflow-service/internal/metrics"
	"github.com/tidbcloud/pkgv2/log"
	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	"go.uber.org/zap"
)

type HTTPRequester interface {
	DELETE(ctx context.Context, url string, opts *HTTPRequesterOption) ([]byte, error)
	GET(ctx context.Context, url string, opts *HTTPRequesterOption) ([]byte, error)
	POST(ctx context.Context, url string, body io.Reader, opts *HTTPRequesterOption) ([]byte, error)
	PUT(ctx context.Context, url string, body io.Reader, opts *HTTPRequesterOption) ([]byte, error)
}

func NewHttpRequester(cli *http.Client, serviceName string) HTTPRequester {
	if cli == nil {
		cli = http.DefaultClient
	}

	// use otelhttp to wrap http client to propagate trace id
	tracedClient := &http.Client{
		Transport: otelhttp.NewTransport(
			cli.Transport,
			otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
				return fmt.Sprintf("%s %s %s", serviceName, r.Method, r.URL.Path)
			}),
		),
		Timeout:       cli.Timeout,
		CheckRedirect: cli.CheckRedirect,
		Jar:           cli.Jar,
	}

	return &httpRequesterImpl{
		cli:         tracedClient,
		serviceName: serviceName,
	}
}

type httpRequesterImpl struct {
	cli         *http.Client
	serviceName string
}

type RequestHandler func(resp *http.Request) error
type ResponseHandler func(resp *http.Response) ([]byte, error)

type HTTPRequesterOption struct {
	Before []RequestHandler
	After  ResponseHandler
}

func (c *httpRequesterImpl) DELETE(ctx context.Context, url string, opts *HTTPRequesterOption) ([]byte, error) {
	return c.DO(ctx, http.MethodDelete, url, nil, opts)
}

func (c *httpRequesterImpl) GET(ctx context.Context, url string, opts *HTTPRequesterOption) ([]byte, error) {
	return c.DO(ctx, http.MethodGet, url, nil, opts)
}

func (c *httpRequesterImpl) POST(ctx context.Context, url string, body io.Reader, opts *HTTPRequesterOption) ([]byte, error) {
	return c.DO(ctx, http.MethodPost, url, body, opts)
}

func (c *httpRequesterImpl) PUT(ctx context.Context, url string, body io.Reader, opts *HTTPRequesterOption) ([]byte, error) {
	return c.DO(ctx, http.MethodPut, url, body, opts)
}

func (c *httpRequesterImpl) DO(ctx context.Context, method, reqUrl string, body io.Reader, opts *HTTPRequesterOption) (data []byte, err error) {
	logger := log.WithContextL(ctx).With(
		zap.String("method", method),
		zap.String("url", reqUrl),
	)

	// add request payload to log
	var reqPayload string
	if body != nil {
		reqData, err := io.ReadAll(body)
		if err != nil {
			return nil, errors.Annotate(err, "httpRequesterImpl failed to read request body")
		}
		if len(reqData) > 1024 {
			reqPayload = string(reqData[:1024]) + "..."
		} else {
			reqPayload = string(reqData)
		}
		body = io.NopCloser(bytes.NewReader(reqData))
	}

	logger.Info("http request begin", zap.String("request", reqPayload))
	startTime := time.Now()
	defer func() {
		level := zap.InfoLevel
		if err != nil {
			level = zap.WarnLevel
		}
		var logData string
		if len(data) > 1024 {
			logData = string(data[:1024]) + "..."
		} else {
			logData = string(data)
		}
		cost := time.Since(startTime)
		logger.Log(level, "http request completed", zap.Duration("cost", cost), zap.String("response", logData), zap.Error(err))
		u, _ := url.Parse(reqUrl)
		ok := strconv.FormatBool(err == nil)
		if metrics.DownstreamRequestDuration != nil {
			metrics.DownstreamRequestDuration.WithLabelValues(c.serviceName, u.Path, ok).Observe(float64(cost.Milliseconds()))
		} else {
			logger.Warn("maybe you have forgot init DownstreamRequestDuration metrics")
		}
	}()

	req, err := http.NewRequestWithContext(ctx, method, reqUrl, body)
	if err != nil {
		return nil, err
	}

	for _, opt := range opts.Before {
		if err := opt(req); err != nil {
			return nil, err
		}
	}

	resp, err := c.cli.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if opts.After != nil {
		return opts.After(resp)
	} else {
		return defaultResponseHandler(resp)
	}
}

func defaultResponseHandler(resp *http.Response) ([]byte, error) {
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("http request failed, method:%s, url:%s, "+
			"response_status:%s response_body:%s",
			resp.Request.Method, resp.Request.URL.String(), resp.Status, data)
	}
	return data, nil
}
