package interceptor

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"sync"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	commonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	tidbmgmt "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	tigrpc "github.com/tidbcloud/tiinf/transport/tigrpc/client"
	"go.uber.org/zap"

	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/errors"
	"github.com/tidbcloud/observability/pkg/common/message"
	"github.com/tidbcloud/observability/pkg/common/utils"
	"github.com/tidbcloud/observability/pkg/globalservice"
)

var (
	oapiClients sync.Map
)

const (
	authMetaOrgId     = "auth_meta_org_id"
	authMetaProjectId = "auth_meta_project_id"
)

func OpenAPIBizType(c *gin.Context) {
	bizType := constants.DefaultByocBizType
	c.Request.Header.Set(string(constants.OpenAPIHeaderBizType), bizType)
	c.Header(string(constants.OpenAPIHeaderBizType), bizType)
	c.Set(string(constants.OpenAPIHeaderBizType), bizType)

	c.Next()
}

func OAPIBasicInfo(c *gin.Context) {
	bizType, clusterId := c.Param("bizType"), c.Param("applicationID")
	if len(clusterId) == 0 {
		c.Next()
		return
	}
	cli, err := getTiDBCloudOAPISvcClient(bizType)
	if err != nil {
		log.Error("failed to get oapi client", zap.Error(err))
		Fail(c, errors.New(errors.InternalServerError, "failed to get cluster"))
		return
	}
	id, err := strconv.ParseUint(clusterId, 10, 64)
	if err != nil {
		log.Error("failed to parse clusterId", zap.Error(err))
		Fail(c, errors.New(errors.ParameterInvalid, "clusterId should be type uint64"))
		return
	}

	resp, err := cli.ListClusters(context.TODO(), &tidbmgmt.ListClustersRequest{
		Filter: &tidbmgmt.ClusterFilter{
			ClusterIds: []uint64{id},
		},
	})
	log.Info("resp received from openapi", zap.Any("response", resp))
	if err != nil {
		log.Error("failed to get cluster", zap.Error(err))
		Fail(c, errors.New(errors.InternalServerError, "failed to get cluster"))
		return
	}
	if resp == nil || len(resp.Clusters) == 0 {
		log.Error("cluster not found", zap.Error(err))
		Fail(c, errors.New(errors.ClusterNotFound, fmt.Sprintf("cluster %d not found", id)))
		return
	}

	clusterInfo := resp.Clusters[0]
	// set region vendor tenant info for o11y logical region routing
	region, vendor, tenant, projectId := clusterInfo.Region, clusterInfo.CloudProvider, fmt.Sprintf("%d", clusterInfo.OrganizationId), fmt.Sprintf("%d", clusterInfo.ProjectId)

	if !isSupportedOpenAPIVendor(vendor) {
		Fail(c, errors.New(errors.UnsupportedRegion, "only aws and alicloud are supported"))
		return
	}
	if !isOpenAPIWhitelistedTenant(tenant) {
		log.Warn("tenant is not in OpenAPI whitelist", zap.String("tenantID", tenant))
		Fail(c, errors.New(errors.UnsupportedRegion, "tenant is not allowed to use OpenAPI"))
		return
	}
	// OpenAPI integration tasks support AWS and AliCloud across serverless,
	// dedicated, premium(nextgen), and BYOC. Resolve the exact bizType from cluster
	// metadata instead of relying on the public host/path tier.
	env := envFromBizType(bizType)
	resolvedBizType, resolveErr := bizTypeFromCluster(clusterInfo.DeployType, clusterInfo.GetNextGenCluster().GetServicePlan(), env)
	if resolveErr != nil {
		log.Error("failed to resolve bizType from cluster",
			zap.String("bizType", bizType),
			zap.Any("deployType", clusterInfo.DeployType),
			zap.Error(resolveErr))
		Fail(c, errors.New(errors.InternalServerError, "failed to resolve cluster tier"))
		return
	}
	// Override URL-path bizType with the cluster-metadata-derived one.
	// extractParam uses header > path-param priority, so this wins.
	c.Request.Header.Set(string(constants.OpenAPIHeaderBizType), resolvedBizType)

	c.Request.Header.Set(string(constants.OpenAPIHeaderRegion), region)
	c.Request.Header.Set(string(constants.OpenAPIHeaderVendor), vendor)
	c.Request.Header.Set(string(constants.OpenAPIHeaderOrgId), tenant)
	c.Set(authMetaOrgId, tenant)
	c.Set(authMetaProjectId, projectId)

	c.Next()
}

func isSupportedOpenAPIVendor(vendor string) bool {
	return vendor == string(constants.AWS) || vendor == string(constants.AliCloud)
}

func isOpenAPIWhitelistedTenant(tenant string) bool {
	if metaCacheGetter == nil {
		metaCacheGetter = globalservice.GetGlobalManager()
	}
	meta := metaCacheGetter.GetGlobalMetaCache()
	if meta == nil {
		return false
	}
	return utils.IsIncludeString(meta.ServerConfig.OpenAPIWhitelistTenants, tenant)
}

// envFromBizType extracts the environment ("prod", "staging", "dev") from a bizType string.
// bizTypes without a known prefix default to "prod" (e.g. bare "tidbcloud" is prod dedicated).
func envFromBizType(bizType string) string {
	for _, env := range []string{constants.EnvStaging, constants.EnvDev} {
		if strings.HasPrefix(bizType, env+"-") {
			return env
		}
	}
	return constants.EnvProd
}

// bizTypeFromCluster resolves the o11y routing bizType from cluster metadata.
// It mirrors globalapi.resolveBizType so all OpenAPI tiers route to their
// correct regional servers.
func bizTypeFromCluster(deployType tidbmgmt.DeployType, servicePlan commonv1.ServicePlan, env string) (string, error) {
	if servicePlan == commonv1.ServicePlan_BYOC {
		return "byoc", nil
	}
	var base string
	switch deployType {
	case tidbmgmt.DeployType_DEPLOY_TYPE_DEDICATED:
		base = "tidbcloud"
	case tidbmgmt.DeployType_DEPLOY_TYPE_NEXTGEN:
		base = "tidbcloud-nextgen"
	case tidbmgmt.DeployType_DEPLOY_TYPE_SERVERLESS:
		base = "devtier"
	default:
		return "", fmt.Errorf("unsupported deploy type: %v", deployType)
	}
	if env == constants.EnvProd && base == "tidbcloud" {
		return base, nil
	}
	return fmt.Sprintf("%s-%s", env, base), nil
}

// getTiDBCloudOAPISvcClient lazy initialize the cluster service client for corresponding bizType
func getTiDBCloudOAPISvcClient(bizType string) (tidbmgmt.ClusterServiceClient, error) {
	if cli, ok := oapiClients.Load(bizType); ok {
		return cli.(tidbmgmt.ClusterServiceClient), nil
	}

	addr, err := getOpenAPISvcAddrByBizType(bizType)
	if err != nil || addr == "" {
		return nil, err
	}

	conn, err := tigrpc.NewClientConn(addr, tigrpc.WithDefault())
	if err != nil {
		return nil, err
	}
	cli := tidbmgmt.NewClusterServiceClient(conn)
	oapiClients.Store(bizType, cli)
	return cli, nil
}

func getOpenAPISvcAddrByBizType(bizType string) (string, error) {
	if metaCacheGetter == nil {
		metaCacheGetter = globalservice.GetGlobalManager()
	}
	config := metaCacheGetter.GetGlobalMetaCache().ServerConfig

	addr, ok := config.BizToOpenAPISvcAddr[bizType]
	if !ok {
		return "", fmt.Errorf("unsupported bizType %s", bizType)
	}
	return addr, nil
}

func Fail(c *gin.Context, err error) {
	var (
		status int
		result message.Result
	)

	if e, ok := err.(errors.Error); ok {
		status = e.Code().GetHttpCode()
		result = message.Result{
			Code:    int(e.Code()),
			Message: err.Error(),
		}
	} else {
		status = errors.UnrecognizedError.GetHttpCode()
		result = message.Result{
			Code:    int(errors.UnrecognizedError),
			Message: err.Error(),
		}
	}

	c.JSON(status, result)
	c.Abort()
}
