package operation

import (
	"context"
	"encoding/json"
	"fmt"
	"slices"
	"strconv"

	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	opent "github.com/tidbcloud/devops/entity/operation"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/grpcutil"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	commonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	dediv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	nextgenv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
)

func (s *svc) ListOperations(ctx context.Context, param ListOperationParam) ([]*opent.Operation, error) {
	result := make([]*opent.Operation, 0)

	pred := func(op *opent.Operation) bool {
		if param.Target != nil && *param.Target != op.Target {
			return false
		}
		for _, category := range param.Categories {
			if !slices.Contains(op.Categories, category) {
				return false
			}
		}

		return true
	}

	for _, op := range s.opRegistry.List() {
		if pred(op) {
			result = append(result, op)
		}
	}

	return result, nil
}

func (s *svc) GetOperation(ctx context.Context, name string, param GetOperationParam) (*opent.Operation, error) {
	op := s.opRegistry.Get(name)
	if op == nil {
		return nil, fmt.Errorf("unknown operation '%s'", name)
	}

	// inject params
	if param.InjectParam {
		err := s.injectParam(ctx, op, param)
		if err != nil {
			return nil, fmt.Errorf("inject param failed: %w", err)
		}
	}

	return op, nil
}

func (s *svc) ListOperationCategories(ctx context.Context) ([]string, error) {
	set := container.NewSet[string]()
	for _, op := range s.opRegistry.List() {
		for _, category := range op.Categories {
			set.Insert(category)
		}
	}
	return set.Slice(), nil
}

func (s *svc) GetOperationClusterWhiteListInfo(ctx context.Context, clusterID string) (string, error) {
	info, ok := s.conf.OperationClusterWhiteList[clusterID]
	if !ok {
		return "", fmt.Errorf("cluster %s is not allowed", clusterID)
	}
	return info.ClusterAgentURL, nil
}

func (s *svc) injectParam(ctx context.Context, op *opent.Operation, param GetOperationParam) error {
	if param.TargetID == nil || param.TargetType == nil {
		return nil
	}

	targetType := *param.TargetType
	targetID := *param.TargetID

	switch targetType {
	case opent.OperationTargetCluster:
		cid, err := strconv.ParseUint(targetID, 10, 64)
		if err != nil {
			return fmt.Errorf("parse cluster id '%s' failed: %w", targetID, err)
		}
		cluster, err := s.coresvcCli.Mgmt.GetCluster(ctx, cid)
		if err != nil {

			return fmt.Errorf("query cluster '%d' failed: %w", cid, err)
		}

		setParamDefIfExist(op, opent.ParamClusterID, func(def *opent.ParamDefine) {
			def.Const = pointer.Pointer(strconv.FormatUint(cluster.GetClusterId(), 10))
		})
		setParamDefIfExist(op, opent.ParamProjectID, func(def *opent.ParamDefine) {
			def.Const = pointer.Pointer(strconv.FormatUint(cluster.GetProjectId(), 10))
		})
		setParamDefIfExist(op, opent.ParamRegion, func(def *opent.ParamDefine) {
			def.Const = pointer.Pointer(cluster.GetRegion())
		})
		if cluster.GetDeployType() == mgmtv1.DeployType_DEPLOY_TYPE_NEXTGEN {
			setParamDefIfExist(op, opent.ParamResourcePoolID, func(def *opent.ParamDefine) {
				def.Const = pointer.Pointer(cluster.GetNextGenCluster().GetSharedPoolId())
			})
		}
		setParamDefIfExist(op, opent.ParamReplicas, func(def *opent.ParamDefine) {
			def.Default = pointer.Pointer(strconv.FormatUint(0, 10))
		})
		// inject tiproxy component count
		if op.Name == "ScaleClusterTiProxy" { // name must match the operation definition
			comp := getClusterTiProxy(cluster.GetDedicatedCluster())
			if len(comp) > 0 {
				setParamDefIfExist(op, opent.ParamReplicas, func(def *opent.ParamDefine) {
					def.Default = pointer.Pointer(strconv.FormatInt(int64(pointer.Deref(comp[0].Count, 0)), 10))
				})
			}
		}
		// inject tenant params recursively
		rParam := GetOperationParam{
			InjectParam: true,
			TargetType:  pointer.Pointer(opent.OperationTargetTenant),
			TargetID:    pointer.Pointer(strconv.FormatUint(cluster.GetOrganizationId(), 10)),
		}
		err = s.injectParam(ctx, op, rParam)
		if err != nil {
			return fmt.Errorf("inject tenant params failed: %w", err)
		}

	case opent.OperationTargetResourcePool:
		rpID, err := strconv.ParseUint(targetID, 10, 64)
		if err != nil {
			return fmt.Errorf("parse resource pool id '%s' failed: %w", targetID, err)
		}
		rp, err := s.coresvcCli.Mgmt.GetResourcePool(ctx, rpID)
		if err != nil {
			return fmt.Errorf("query resource pool '%s' failed: %w", targetID, err)
		}

		setParamDefIfExist(op, opent.ParamResourcePoolID, func(def *opent.ParamDefine) {
			def.Const = pointer.Pointer(targetID)
		})
		setParamDefIfExist(op, opent.ParamProvider, func(def *opent.ParamDefine) {
			def.Const = pointer.Pointer(rp.GetRegion().GetCloudProviderId().String())
		})
		setParamDefIfExist(op, opent.ParamRegion, func(def *opent.ParamDefine) {
			def.Const = pointer.Pointer(rp.GetRegion().GetId().String())
		})
		setParamDefIfExist(op, opent.ParamRegionName, func(def *opent.ParamDefine) {
			def.Const = pointer.Pointer(rp.GetRegion().GetId().String())
		})

		// inject tenant params recursively
		if rp.GetResourcePool().GetOwnerType() == nextgenv1.ResourcePool_ORGANIZATION {
			rParam := GetOperationParam{
				InjectParam: true,
				TargetType:  pointer.Pointer(opent.OperationTargetTenant),
				TargetID:    pointer.Pointer(strconv.FormatUint(rp.GetResourcePool().GetOwnerId(), 10)),
			}
			err = s.injectParam(ctx, op, rParam)
			if err != nil {
				return fmt.Errorf("inject tenant params failed: %w", err)
			}
		} else if rp.GetResourcePool().GetOwnerType() == nextgenv1.ResourcePool_SHARED {
			rParam := GetOperationParam{
				InjectParam: true,
				TargetType:  pointer.Pointer(opent.OperationTargetTenant),
				TargetID:    pointer.Pointer("0"),
			}
			err = s.injectParam(ctx, op, rParam)
			if err != nil {
				return fmt.Errorf("inject shared tenant params failed: %w", err)
			}
			if op.Name == "SetDefaultResourcePool" {
				setParamDefIfExist(op, opent.ParamTenantID, func(def *opent.ParamDefine) {
					def.Const = nil
					def.Default = pointer.Pointer("0")
				})
			}
		}

	case opent.OperationTargetTenant:
		tid, err := strconv.ParseUint(targetID, 10, 64)
		if err != nil {
			return fmt.Errorf("parse tenant id '%s' failed: %w", targetID, err)
		}
		if tid == 0 {
			setParamDefIfExist(op, opent.ParamTenantID, func(def *opent.ParamDefine) {
				def.Const = pointer.Pointer("0")
			})
			setParamDefIfExist(op, opent.ParamTenantRegion, func(def *opent.ParamDefine) {
				def.Const = pointer.Pointer(opent.ParamTenantRegionSharedPool)
			})
			return nil
		}
		tenant, err := s.tenantSvc.GetTenant(ctx, tid)
		if err != nil {
			return fmt.Errorf("query tenant '%d' failed: %w", tid, err)
		}

		setParamDefIfExist(op, opent.ParamTenantID, func(def *opent.ParamDefine) {
			def.Const = pointer.Pointer(strconv.FormatUint(tid, 10))
		})

		setParamDefIfExist(op, opent.ParamTenantRegion, func(def *opent.ParamDefine) {
			tenantRegion := tenant.GetTenantRegion()
			def.Const = pointer.Pointer(string(tenantRegion))
		})
		// inject sales_owner
		setParamDefIfExist(op, opent.ParamSalesOwner, func(def *opent.ParamDefine) {
			salesOwner := tenant.SalesOwnerEmail
			if len(salesOwner) > 0 {
				def.Const = pointer.Pointer(salesOwner)
			}
		})

		if op.Name == "ModifyTenantQuota" { // name must match the operation definition
			resp, err := s.coresvcCli.Core.ListTenantQuota(ctx, &core.ListTenantQuotaReq{TenantId: tid})
			if err == nil && grpcutil.RespToErr(resp.BaseResp) == nil {
				paramToComp := map[opent.ParamName]core.ComponentType{
					opent.ParamTiDBNodeQuota:    core.ComponentType_COMPONENT_TYPE_TIDB,
					opent.ParamTiKVNodeQuota:    core.ComponentType_COMPONENT_TYPE_TIKV,
					opent.ParamTiFlashNodeQuota: core.ComponentType_COMPONENT_TYPE_TIFLASH,
					opent.ParamTiProxyNodeQuota: core.ComponentType_COMPONENT_TYPE_TIPROXY,
				}
				// in some cases, tiproxy quota in the `resp.TenantQuotas` is empty, so we need use `0` to fill it because `0` means unchanged.
				setParamDefIfExist(op, opent.ParamTiProxyNodeQuota, func(def *opent.ParamDefine) {
					def.Default = pointer.Pointer(strconv.FormatUint(0, 10))
				})
				for pname, comp := range paramToComp {
					setParamDefIfExist(op, pname, func(def *opent.ParamDefine) {
						for _, quota := range resp.TenantQuotas {
							if quota.Component == comp {
								def.Default = pointer.Pointer(strconv.FormatUint(uint64(quota.Quota), 10))
							}
						}
					})
				}
			}
		}

		if op.Name == "ModifyTenantPremiumQuota" { // name must match the operation definition
			if op.RuntimeData == nil {
				op.RuntimeData = map[string]string{}
			}

			setParamDefIfExist(op, opent.ParamServicePlan, func(def *opent.ParamDefine) {
				def.Default = pointer.Pointer(commonv1.ServicePlan_Premium.String())
			})

			listResp, err := s.coresvcCli.Mgmt.RcuQuota.ListOrgRcuQuotas(ctx, &nextgenv1.ListOrgRcuQuotasRequest{OrganizationId: tid})
			if err != nil {
				break
			}

			planDefaults := make(map[string]map[string]string)
			for _, quota := range listResp.GetRcuQuotas() {
				plan := quota.GetServicePlan().String()
				if plan == commonv1.ServicePlan_SERVICE_PLAN_UNSPECIFIED.String() {
					continue
				}

				planDefaults[plan] = map[string]string{
					string(opent.ParamInstanceMaxRCU):            strconv.FormatUint(quota.GetInstanceMaxRcu(), 10),
					string(opent.ParamTotalMaxRCU):               strconv.FormatUint(quota.GetTotalMaxRcu(), 10),
					string(opent.ParamBurstyRatio):               strconv.FormatFloat(float64(quota.GetBurstyRatio()), 'f', -1, 32),
					string(opent.ParamIdleResourcePoolTTLInHour): strconv.FormatUint(quota.GetIdleResourcePoolTtlInHours(), 10),
					string(opent.ParamVcpusLimitPerResourcePool): strconv.FormatUint(quota.GetVcpusLimitPerResourcePool(), 10),
					string(opent.ParamMinimalRCUBaseline):        strconv.FormatUint(quota.GetMinimalRcuBaseline(), 10),
					string(opent.ParamMinimalRCUCoefficient):     strconv.FormatFloat(float64(quota.GetMinimalRcuCoefficient()), 'f', -1, 32),
					string(opent.ParamScaleDownCooldownMinutes):  strconv.FormatUint(quota.GetScaleDownCooldownMinutes(), 10),
				}
			}

			if len(planDefaults) == 0 {
				break
			}

			plan := commonv1.ServicePlan_Premium.String()
			if param.ServicePlan != nil {
				if _, ok := commonv1.ServicePlan_value[*param.ServicePlan]; ok {
					plan = *param.ServicePlan
				}
			}

			defaultsByPlanRaw, err := json.Marshal(planDefaults)
			if err == nil {
				op.RuntimeData["rcu_quota_defaults_by_service_plan"] = string(defaultsByPlanRaw)
			}

			selectedDefaults, ok := planDefaults[plan]
			if !ok {
				selectedDefaults = planDefaults[commonv1.ServicePlan_Premium.String()]
			}

			setDefaultIfExists := func(name opent.ParamName) {
				v, ok := selectedDefaults[string(name)]
				if !ok {
					return
				}
				setParamDefIfExist(op, name, func(def *opent.ParamDefine) {
					def.Default = pointer.Pointer(v)
				})
			}

			setDefaultIfExists(opent.ParamInstanceMaxRCU)
			setDefaultIfExists(opent.ParamTotalMaxRCU)
			setDefaultIfExists(opent.ParamBurstyRatio)
			setDefaultIfExists(opent.ParamIdleResourcePoolTTLInHour)
			setDefaultIfExists(opent.ParamVcpusLimitPerResourcePool)
			setDefaultIfExists(opent.ParamMinimalRCUBaseline)
			setDefaultIfExists(opent.ParamMinimalRCUCoefficient)
			setDefaultIfExists(opent.ParamScaleDownCooldownMinutes)
		}
	}

	return nil
}

func setParamDefIfExist(op *opent.Operation, name opent.ParamName, set func(def *opent.ParamDefine)) {
	def, exist := op.Params[name]
	if !exist {
		return
	}
	set(&def)
	op.Params[name] = def
}

func getClusterTiProxy(cluster *dediv1.Cluster) []*dediv1.Cluster_ClusterComponent {
	comp := make([]*dediv1.Cluster_ClusterComponent, 0)
	for _, c := range cluster.ClusterComponents {
		if c.ComponentType == dediv1.ComponentType_TIPROXY {
			comp = append(comp, c)
		}
	}
	return comp
}
