package utils

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

	"github.com/gin-gonic/gin"

	"go.uber.org/zap"

	apierr "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/log"
	"github.com/pingcap-inc/clinic/pkg/o11y"
	o11yurls "github.com/pingcap-inc/clinic/pkg/o11y/urls"
)

var httpClient = &http.Client{
	Timeout: 1 * time.Minute,
}

// ResponseModifier modifies proxy response body. Return modified body or error.
// Used when path-specific post-processing is needed (e.g. add binary_plan_text).
type ResponseModifier func(body []byte) ([]byte, error)

func ResolveO11yInternalReadTarget(ctx context.Context, cli *o11y.Client,
	deployType, vendor, region, tenantID, projectID, clusterID string,
) (*o11yurls.InternalReadTarget, error) {
	return o11yurls.NewClusterURLsResolver(cli).ResolveInternalReadTarget(
		ctx,
		clusterFromQueryParams(deployType, vendor, region, tenantID, projectID, clusterID),
		o11yurls.MetricsScopeChild,
		"",
	)
}

func ProxyO11yByQueryCluster(ctx context.Context, c *gin.Context, cli *o11y.Client,
	deployType, env, vendor, region, tenantID, projectID, clusterID, path string,
) error {
	return ProxyO11yByQueryClusterWithModifier(ctx, c, cli,
		deployType, env, vendor, region, tenantID, projectID, clusterID, path, nil)
}

func ProxyO11yByQueryClusterWithModifier(ctx context.Context, c *gin.Context, cli *o11y.Client,
	deployType, env, vendor, region, tenantID, projectID, clusterID, path string, modifier ResponseModifier,
) error {
	bizType := o11y.InternalReadBizType(env, deployType)

	target, err := ResolveO11yInternalReadTarget(ctx, cli, deployType, vendor, region, tenantID, projectID, clusterID)
	if err != nil {
		log.Error(ctx, "failed to resolve o11y internal read target", zap.Error(err))
		return fmt.Errorf("failed to resolve o11y internal read target: %w", err)
	}

	o11yURL, err := url.Parse(target.InternalReadURL)
	if err != nil {
		return fmt.Errorf("failed to parse o11y internal read uri: %w", err)
	}
	log.Info(ctx, "original  o11yURL ", zap.String("o11yURL", o11yURL.String()))
	o11yURL.Path = path
	// @todo update byoc url
	if deployType == o11y.ByocType || deployType == o11y.ByocHostType {
		log.Info(ctx, "will update byoc proxy o11y url", zap.String("deployType", deployType), zap.Any("clusterID", clusterID))
		if byocDSEndpoint := cli.GetByocDSEndpoint(); byocDSEndpoint != "" {
			o11yURL.Host = byocDSEndpoint
		}
		if target.O11yCluster == nil {
			return fmt.Errorf("byoc o11y cluster is empty")
		}
		o11yURL.Path = buildPath(o11yURL.Path, target.O11yCluster.ID)
		log.Info(ctx, "update byoc  proxy o11y url", zap.String("url", o11yURL.String()))
	}
	log.Info(ctx, "final proxy o11y url", zap.String("url", o11yURL.String()), zap.Any("clusterID", clusterID))

	ProxyO11yURL(ctx, c, target.O11yCluster, o11yURL.String(), env, bizType, modifier)
	return nil
}

func clusterFromQueryParams(deployType, vendor, region, tenantID, projectID, clusterID string) *db.Cluster {
	tenantIDUint, _ := strconv.ParseUint(tenantID, 10, 64)
	return &db.Cluster{
		DeployType: deployType,
		Provider:   vendor,
		Region:     region,
		TenantID:   tenantIDUint,
		ProjectID:  projectID,
		ClusterID:  clusterID,
		Organization: db.Organization{
			DbaasOrgID: uint(tenantIDUint),
		},
	}
}

// Insert o11yUuid after the first path segment: /internal/{o11yUuid}/...
func buildPath(path, o11yUuid string) string {
	const prefix = "/internal"

	if strings.HasPrefix(path, prefix) {
		return prefix + "/" + o11yUuid + strings.TrimPrefix(path, prefix)
	}

	return path
}

func ProxyO11yURL(ctx context.Context, c *gin.Context, o11yCluster *o11y.ClusterInfo, url, env, bizType string, modifier ResponseModifier) {
	if o11yCluster == nil && bizType == "byoc" {
		log.Error(ctx, "o11y cluster is nil")
		_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(fmt.Errorf("o11y cluster is nil")))
		return
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		log.Warn(ctx, "failed to create proxy request", zap.Error(err))
		_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(err))
		return
	}
	// Copy headers
	req.Header["x-env"] = []string{env}
	for k, v := range c.Request.Header {
		if strings.EqualFold(k, "Accept-Encoding") {
			continue
		}
		req.Header[k] = v
	}
	if bizType != "" {
		req.Header["x-biz-type"] = []string{bizType}
	}

	if bizType == "byoc" && o11yCluster != nil {
		req.Header["X-Org-Id"] = []string{o11yCluster.TenantID}
		XRegionId := fmt.Sprintf("%s-%s", o11yCluster.Vendor, o11yCluster.Region)
		req.Header["X-Region-Id"] = []string{XRegionId}
		req.Header["X-Application-Id"] = []string{o11yCluster.ApplicationID}
		log.Info(ctx, "debug proxy o11y request header", zap.String("X-Org-Id", o11yCluster.TenantID), zap.String("X-Region-Id", XRegionId), zap.String("X-Application-Id", o11yCluster.ApplicationID))
	}
	// Copy query parameters
	q := req.URL.Query()
	for k, v := range c.Request.URL.Query() {
		q[k] = v
	}
	req.URL.RawQuery = q.Encode()
	log.Info(ctx, "debug proxy o11y request headers", log.RedactedHeaderFields(req.Header)...)
	log.Info(ctx, "debug proxy o11y request url:"+req.URL.String())
	resp, err := httpClient.Do(req)
	if err != nil {
		log.Warn(ctx, "failed to forward request to o11y", zap.Error(err))
		_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(err))
		return
	}
	log.Info(ctx, "debug proxy o11y response", zap.String("status", resp.Status))
	defer resp.Body.Close()
	for k, v := range resp.Header {
		c.Header(k, v[0])
	}
	c.Status(resp.StatusCode)

	if modifier != nil && resp.StatusCode == http.StatusOK {
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			log.Warn(ctx, "failed to read response body", zap.Error(err))
			_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(err))
			return
		}
		body, err = modifier(body)
		if err != nil {
			log.Warn(ctx, "failed to modify response body", zap.Error(err))
			_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(err))
			return
		}
		c.Header("Content-Length", strconv.Itoa(len(body)))
		if _, err := c.Writer.Write(body); err != nil {
			log.Warn(ctx, "failed to write response body", zap.Error(err))
			_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(err))
			return
		}
	} else if _, err := io.Copy(c.Writer, resp.Body); err != nil {
		log.Warn(ctx, "failed to write response body", zap.Error(err))
		_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(err))
		return
	}
}

func ProxyO11yURLWithDebug(ctx context.Context, c *gin.Context, url, env, bizType string) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		log.Warn(ctx, "failed to create proxy request", zap.Error(err))
		_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(err))
		return
	}
	// Copy headers
	req.Header["x-env"] = []string{env}
	for k, v := range c.Request.Header {
		if strings.EqualFold(k, "Accept-Encoding") {
			continue
		}
		req.Header[k] = v
	}
	if bizType != "" {
		req.Header["x-biz-type"] = []string{bizType}
	}
	// Copy query parameters
	q := req.URL.Query()
	for k, v := range c.Request.URL.Query() {
		q[k] = v
	}
	req.URL.RawQuery = q.Encode()
	resp, err := httpClient.Do(req)
	if err != nil {
		log.Warn(ctx, "failed to forward request to o11y", zap.Error(err))
		_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(err))
		return
	}
	defer resp.Body.Close()
	for k, v := range resp.Header {
		c.Header(k, v[0])
		log.Warn(ctx, "debug header", zap.String(k, v[0]))
	}
	c.Status(resp.StatusCode)
	log.Warn(ctx, "debug status", zap.Int("status", resp.StatusCode))
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Warn(ctx, "failed to read response body", zap.Error(err))
		_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(err))
		return
	}
	log.Warn(ctx, "debug body", zap.String("body", string(body)))
	if _, err := c.Writer.Write(body); err != nil {
		log.Warn(ctx, "failed to write response body", zap.Error(err))
		_ = c.Error(apierr.ErrInternal.WrapWithNoMessage(err))
		return
	}
}
