package proxy

import (
	"context"
	"errors"
	"net/http"
	"net/url"
	"path"
	"strings"

	"github.com/gin-gonic/gin"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/api/common"
	error2 "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/datasource"
	"github.com/pingcap-inc/clinic/pkg/db"
	dbmodel "github.com/pingcap-inc/clinic/pkg/db/model"
	"github.com/pingcap-inc/clinic/pkg/log"
	"github.com/pingcap-inc/clinic/pkg/o11y"
	o11yurls "github.com/pingcap-inc/clinic/pkg/o11y/urls"
)

var getAnyClusterWithOrg = dbmodel.GetAnyClusterWithOrg

func (handler *TiDBClusterMetricHandler) GinHandleLogs(
	c *gin.Context,
	getOrgType func(c *gin.Context) string,
	getOrgID func(c *gin.Context) uint,
	getClusterID func(c *gin.Context) string,
	routerPathFunc func(string) string,
) {
	user := session.GetUser(c)
	if user == nil {
		_ = c.Error(error2.ErrNotFound.New("the user is not found"))
		return
	}

	ctx := c.Request.Context()
	if log.GeTraceId(ctx) == "" {
		ctx = log.StartTrace("log-proxy", "GinHandleLogs")
		c.Request = c.Request.WithContext(ctx)
	}

	clusterID := getClusterID(c)
	if clusterID == "" {
		_ = c.Error(error2.ErrNotFound.New("cluster is not found"))
		return
	}
	clusterInfo := strings.Split(clusterID, "/")
	if len(clusterInfo) == 2 {
		clusterID = clusterInfo[0]
	}
	ctx = contextWithClusterID(ctx, clusterID)
	c.Request = c.Request.WithContext(ctx)

	orgType := getOrgType(c)
	oid := getOrgID(c)
	if oid <= 0 {
		resolvedOrgType, resolvedOID, err := resolveLogQueryOrgFromCluster(ctx, clusterID)
		if err != nil {
			log.Error(ctx, "resolve log query organization from cluster failed",
				zap.String("clusterID", clusterID),
				zap.Error(err))
			_ = c.Error(error2.ErrNotFound.New("not found the organization"))
			return
		}
		orgType = resolvedOrgType
		oid = resolvedOID
	}

	handler.proxyLogs(ctx, c, orgType, oid, clusterID, user, routerPathFunc)
}

func resolveLogQueryOrgFromCluster(ctx context.Context, clusterID string) (string, uint, error) {
	cluster, err := getAnyClusterWithOrg(clusterID)
	if err != nil {
		return "", 0, err
	}
	oid := cluster.OrgID
	if oid == 0 {
		oid = cluster.Organization.ID
	}
	if oid == 0 {
		return "", 0, errors.New("cluster organization id is empty")
	}
	orgType := cluster.Organization.OrgType
	if orgType == "" && cluster.ClusterType == "cloud" {
		orgType = "cloud"
	}
	log.Info(ctx, "resolved log query organization from cluster",
		zap.String("clusterID", clusterID),
		zap.Uint("orgID", oid),
		zap.String("orgType", orgType))
	return orgType, oid, nil
}

func (handler *TiDBClusterMetricHandler) proxyLogs(
	ctx context.Context,
	c *gin.Context,
	orgType string,
	oid uint,
	clusterID string,
	user *session.CtxUser,
	routerPathFunc func(string) string,
) {
	targetScheme, targetHost, requestModifyFunc, respModifyFunc, err := handler.handleLogs(ctx, orgType, oid, clusterID, user, routerPathFunc) //nolint:bodyclose
	if err != nil {
		_ = c.Error(err)
		return
	}
	proxyModifyResponse(ctx, c, targetScheme, targetHost, requestModifyFunc, respModifyFunc)
}

func (handler *TiDBClusterMetricHandler) handleLogs(
	ctx context.Context,
	orgType string,
	oid uint,
	clusterID string,
	user *session.CtxUser,
	routerPathFunc func(string) string,
) (
	string,
	string,
	func(r *http.Request) *http.Request,
	func(resp *http.Response) error,
	error,
) {
	if orgType != "cloud" {
		return "", "", nil, nil, error2.ErrBadRequest.New("log query proxy only supports cloud clusters")
	}

	if !user.Internal {
		ok, err := common.CheckExternalUserPermission(user, oid, clusterID)
		if err != nil || !ok {
			return "", "", nil, nil, error2.ErrForbidden.New("permission deny")
		}
	}
	if !handler.o11yEnable {
		return "", "", nil, nil, errors.New("o11y is not enabled")
	}
	org, err := datasource.GetOrganization(user, oid)
	if err != nil {
		log.Error(ctx, "get org from datasource failed", zap.Error(err))
		return "", "", nil, nil, err
	}
	cluster, err := datasource.GetCloudCluster(user, clusterID, oid)
	if err != nil {
		log.Error(ctx, "get cluster from datasource failed", zap.Error(err))
		return "", "", nil, nil, errors.New("get cluster from datasource failed")
	}
	cluster.Organization = *org

	target, err := o11yurls.NewClusterURLsResolver(handler.o11yCli).ResolveLogsTarget(
		ctx,
		cluster,
	)
	if err != nil {
		log.Error(ctx, "resolve logs target failed",
			zap.String("clusterID", cluster.ClusterID),
			zap.String("deployType", cluster.DeployType),
			zap.String("deployTypeV2", cluster.DeployTypeV2),
			zap.String("provider", cluster.Provider),
			zap.String("region", cluster.Region),
			zap.Error(err))
		if errors.Is(err, o11y.ErrNotFound) {
			return "", "", nil, nil, error2.ErrNotFound.New("o11y logs target resolve failed: no o11y cluster found")
		}
		return "", "", nil, nil, error2.ErrInternal.New("resolve logs target failed: %s", err.Error())
	}
	return buildProxyForLogsTarget(ctx, target, cluster, routerPathFunc) //nolint:bodyclose
}

func buildProxyForLogsTarget(
	ctx context.Context,
	target *o11yurls.LogsTarget,
	cluster *db.Cluster,
	routerPathFunc func(string) string,
) (
	string,
	string,
	func(r *http.Request) *http.Request,
	func(resp *http.Response) error,
	error,
) {
	if target == nil || target.LogQueryURL == "" {
		return "", "", nil, nil, error2.ErrInternal.New("logs target is empty")
	}
	logQueryURL, err := url.Parse(target.LogQueryURL)
	if err != nil {
		log.Error(ctx, "parse logs target URL failed", zap.Error(err), zap.String("url", target.LogQueryURL))
		return "", "", nil, nil, error2.ErrInternal.New("parse logs target URL failed")
	}
	targetScheme := logQueryURL.Scheme
	targetHost := logQueryURL.Host
	requestModifyFunc := func(r *http.Request) *http.Request {
		stripLogsRoutingParams(r)
		stripLogsRoutingHeaders(r)
		r.URL.Path = joinO11yLogQueryPath(logQueryURL.Path, routerPathFunc(r.URL.Path))
		if r.URL.RawPath != "" {
			r.URL.RawPath = joinO11yLogQueryPath(logQueryURL.Path, routerPathFunc(r.URL.RawPath))
		}
		log.Info(r.Context(), "forward to resolved logs target",
			zap.String("targetHost", targetHost),
			zap.String("path", r.URL.Path),
			zap.String("url", r.URL.String()))
		r.Host = targetHost
		return r
	}
	if cluster != nil && target.O11yCluster != nil && isByocDeployType(cluster.DeployType) {
		requestModifyFunc = withByocRequestHeaders(requestModifyFunc, cluster, target.O11yCluster)
	} else if target.O11yCluster != nil {
		requestModifyFunc = withO11yClusterRequestHeaders(requestModifyFunc, target.O11yCluster)
	}
	respModifyFunc := func(resp *http.Response) error {
		resp.Header.Set("X-Logs-From", "o11y")
		if resp.StatusCode != http.StatusOK {
			log.Error(resp.Request.Context(), "forward to resolved logs target failed",
				zap.String("url", resp.Request.URL.String()),
				zap.Int("status", resp.StatusCode))
		}
		return nil
	}
	return targetScheme, targetHost, requestModifyFunc, respModifyFunc, nil
}

func joinO11yLogQueryPath(logQueryPath, lokiAPIPath string) string {
	return path.Join("/", logQueryPath, strings.TrimPrefix(lokiAPIPath, "/"))
}

func stripLogsRoutingParams(r *http.Request) {
	q := r.URL.Query()
	for _, key := range []string{
		"cluster_id",
		"x-clinic-cluster-id",
		"var-cluster_id",
		VarTiDBClusterVariable,
		"tidb_cluster",
		ControlPlaneInfoVariable,
		VarControlPlaneInfoVariable,
		VarProjectIDVariable,
	} {
		q.Del(key)
	}
	r.URL.RawQuery = q.Encode()
}

func stripLogsRoutingHeaders(r *http.Request) {
	for _, key := range []string{
		"x-clinic-cluster-id",
		ControlPlaneInfoVariable,
	} {
		r.Header.Del(key)
	}
}
