package config

import (
	"fmt"
	"strconv"
	"strings"
	"time"

	dataflowng "github.com/tidbcloud/dataflow-service-ng/idl/pbgen/proto/consoleapi"
	"github.com/tidbcloud/dataflow-service/idl/pbgen/proto/dataflow"
	"github.com/tidbcloud/devops/api"
	apprent "github.com/tidbcloud/devops/entity/approval"
	ccent "github.com/tidbcloud/devops/entity/cloudconfig"
	entcommon "github.com/tidbcloud/devops/entity/common"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	apprapi "github.com/tidbcloud/devops/pkg/service/approval/api"
	ccsvcapi "github.com/tidbcloud/devops/pkg/service/cloudconfig/api"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/cvt"
	nextgenv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
)

const (
	ApprovalNameCluster               = "Modify Cluster Configuration"
	ApprovalNameTenantInit            = "Modify Tenant Init Configuration"
	ApprovalNameTemplate              = "Create Custom Template"
	ApprovalTemplateModifyConfig      = "cloudconfig/modify-config"
	ApprovalTemplateModifyTemplate    = "cloudconfig/modify-template"
	ApprovalTemplateModifyConfigForNG = "cloudconfig/modify-config-ng"

	ApprovalParamChangeID           = "configuration_change_id"
	ApprovalParamClusterID          = "cluster_id"
	ApprovalParamClusterName        = "cluster_name"
	ApprovalParamTenantID           = "tenant_id"
	ApprovalParamTenantName         = "tenant_name"
	ApprovalParamTenantRegion       = "tenant_region"
	ApprovalParamModifyScope        = "modified_scope"
	ApprovalParamHighRiskComponents = "high_risk_components"
	ApprovalParamClusterType        = "cluster_type"
	ApprovalParamChangeType         = "configuration_change_type"
	ApprovalParamTemplateType       = "template_type"
)

type actorCreated struct {
	*baseActor
}

func (a *actorCreated) SendApproval() error {
	ctx := a.Context
	chg := a.Change

	// create an operation
	createApprParam, err := a.buildParamToCreateApproval()
	if err != nil {
		return fmt.Errorf("build param to create operation failed: %w", err)
	}

	// For dev/staging env, we don't need approval for automated test.
	if consts.GetEnv() == consts.EnvironmentDev || consts.GetEnv() == consts.EnvironmentStaging {
		if chg.Reason == consts.AutomatedTestReason {
			createApprParam.FromTemplate = consts.AutomatedTestApprovalTemplate
		}
	}

	var mutates []func(param *apprapi.CreateApprovalParam)
	maintenanceWindowMutate, err := a.buildMaintenanceWindowApprovalMutate()
	if err != nil {
		return fmt.Errorf("build maintenance window approval mutate failed: %w", err)
	}
	if maintenanceWindowMutate != nil {
		mutates = append(mutates, maintenanceWindowMutate)
	}

	apprID, err := a.ApprovalService.CreateApprovalFromTemplate(ctx, createApprParam, mutates...)
	if err != nil {
		return fmt.Errorf("create approval failed: %w", err)
	}

	// transition status to 'Approving'
	err = a.PatchConfigurationChange(ctx,
		func(cur *ccent.ConfigurationChange) error {
			cur.ApprovalID = apprID
			cur.Phase = ccent.ConfigurationChangePhaseApproving
			return nil
		},
	)
	if err != nil {
		return fmt.Errorf("approval sent but transition status to approving failed: %w", err)
	}

	return nil
}

func (a *actorCreated) Cancel() error {
	return a.cancel()
}

func (a *actorCreated) buildMaintenanceWindowApprovalMutate() (func(param *apprapi.CreateApprovalParam), error) {
	chg := a.Change
	if chg.TargetType != ccent.ConfigurationChangeTargetTypeCluster || chg.TargetCluster == nil {
		return nil, nil
	}

	var executeTimes []time.Time
	if chg.ExecutionWindowID != nil {
		executionWindow, err := a.MaintenanceSvc.GetOpsExecutionWindow(a.Context, *chg.ExecutionWindowID)
		if err != nil {
			return nil, err
		}
		executeTimes = append(
			executeTimes,
			time.Unix(executionWindow.StartAt, 0),
			time.Unix(executionWindow.EndAt, 0),
		)
	}

	inWindow, window, err := a.MaintenanceSvc.IsInClusterOpsMaintenanceWindow(a.Context, chg.TargetCluster.ClusterID, executeTimes)
	if err != nil {
		return nil, err
	}
	if inWindow || len(window.Approvers) == 0 {
		return nil, nil
	}

	// not in maintenance window, append extra approvers.
	return func(param *apprapi.CreateApprovalParam) {
		param.Nodes = append(param.Nodes, &apprent.ApproverNode{
			Name:   "Cluster Is Outside Ops Maintenance Window",
			Method: apprent.ApproveMethodAny,
			Approver: &apprent.Approver{
				Emails: window.Approvers,
			},
		})
	}, nil
}

func (a *actorCreated) buildParamToCreateApproval() (apprapi.CreateApprovalFromTemplateParam, error) {
	ctx := a.Context
	chg := a.Change

	param := apprapi.CreateApprovalFromTemplateParam{
		CreateApprovalParamBase: apprapi.CreateApprovalParamBase{
			Name:       "Modify Cluster Configuration",
			Applicant:  chg.Applicant,
			DetailLink: DetailLink(chg.ID),
			Overview:   map[string]string{},
			Parameters: map[string]string{
				ApprovalParamChangeID:           strconv.FormatUint(chg.ID, 10),
				ApprovalParamChangeType:         string(chg.ChangeType),
				ApprovalParamModifyScope:        "[" + chg.ChangedScope().HumanReadable() + "]",
				ApprovalParamHighRiskComponents: "", // tenant and cluster changes share the arrproval template, the empty value is necessary to parse the teamplate
				// others are injected later.
			},
			Reason: chg.Reason,
		},
	}
	operatorType, _, _ := entcommon.ParseOperatorEntity(chg.OperatorEntity)
	if operatorType == consts.CtxOperatorTypeAPIKey {
		param.Configuration.SkipApplicantNotification = true
	}

	switch chg.TargetType {
	case ccent.ConfigurationChangeTargetTypeCluster:
		param.Name = ApprovalNameCluster

		var tenantID uint64
		clusterIDStr := chg.TargetCluster.ClusterID
		clusterID, err := strconv.ParseUint(clusterIDStr, 10, 64)
		if err != nil && chg.ChangeType != string(ccsvcapi.ChangeTypeChangefeedEssential) {
			return param, fmt.Errorf("parse cluster id failed: %w", err)
		}

		// inject cluster related params
		param.Parameters[ApprovalParamClusterType] = string(chg.TargetCluster.ClusterType)
		param.Parameters[ApprovalParamClusterID] = clusterIDStr
		param.Overview[ApprovalParamClusterID] = clusterIDStr
		param.Overview[ApprovalParamClusterType] = string(chg.TargetCluster.ClusterType)

		switch chg.ChangeType {
		case string(ccsvcapi.ChangeTypeChangefeedEssential):
			changefeed, err := a.Serverless.GetChangefeed(ctx, clusterIDStr)
			if err != nil {
				return param, fmt.Errorf("get essential changefeed failed: %w", err)
			}
			param.Overview[ApprovalParamClusterName] = changefeed.DisplayName
		case "changefeed":
			cluster, err := a.Dataflow.Ops.GetInternalChangefeed(ctx, &dataflow.GetInternalChangefeedRequest{ChangefeedId: clusterID})
			if err != nil {
				return param, fmt.Errorf("get changefeed cluster failed: %w", err)
			}

			param.Overview[ApprovalParamClusterName] = cluster.DisplayName
		case "changefeed_ng":
			cluster, err := a.Dataflow.NgOps.GetInternalChangefeed(ctx, &dataflowng.GetInternalChangefeedRequest{ChangefeedId: clusterID})
			if err != nil {
				return param, fmt.Errorf("get changefeed ng cluster failed: %w", err)
			}

			param.Overview[ApprovalParamClusterName] = cluster.DisplayName
		case "ticdc":
			cluster, err := a.Dataflow.Ops.GetInternalTiCDCCluster(ctx, &dataflow.GetInternalTiCDCClusterRequest{TicdcClusterId: clusterID})
			if err != nil {
				return param, fmt.Errorf("get ticdc cluster failed: %w", err)
			}
			tenantID = cluster.OrgId

			param.Overview[ApprovalParamClusterName] = cluster.Name
		case "ticdc_ng":
			cluster, err := a.Dataflow.NgOps.GetInternalTiCDCCluster(ctx, &dataflowng.GetInternalTiCDCClusterRequest{TicdcClusterId: clusterID})
			if err != nil {
				return param, fmt.Errorf("get ticdc ng cluster failed: %w", err)
			}
			param.Overview[ApprovalParamClusterName] = cluster.Name
		default:
			cluster, err := a.CoreSvc.Mgmt.GetCluster(ctx, clusterID)
			if err != nil {
				if apie, ok := errs.AsAPIError(err); ok && apie.ErrCode() == api.ErrCodeNotFound {
					rp, err := a.CoreSvc.Mgmt.GetResourcePool(ctx, clusterID)
					if err != nil {
						return param, fmt.Errorf("both query for cluster and resource pool '%d' failed: %w", clusterID, err)
					}
					if rp.GetResourcePool().GetOwnerType() == nextgenv1.ResourcePool_ORGANIZATION {
						tenantID = rp.GetResourcePool().GetOwnerId()
					}
					param.Overview[ApprovalParamClusterName] = rp.GetResourcePool().GetName()
				} else {
					return param, fmt.Errorf("get cluster from global failed: %w", err)
				}
			} else {
				tenantID = cluster.OrganizationId

				param.Overview[ApprovalParamClusterName] = cluster.ClusterName
			}
		}

		// inject tenant related params, it is used in approval node
		param.Parameters[ApprovalParamTenantID] = ""
		param.Parameters[ApprovalParamTenantName] = ""
		param.Parameters[ApprovalParamTenantRegion] = ""
		if tenantID != 0 {
			tenant, err := a.TenantService.GetTenant(ctx, uint64(tenantID))
			if err != nil {
				return param, fmt.Errorf("get tenant %d failed: %w", tenantID, err)
			}

			param.Parameters[ApprovalParamTenantID] = strconv.FormatUint(tenant.Id, 10)
			param.Parameters[ApprovalParamTenantName] = tenant.Name
			param.Parameters[ApprovalParamTenantRegion] = string(tenant.GetTenantRegion())
			param.Overview[ApprovalParamTenantID] = strconv.FormatUint(tenant.Id, 10)
			param.Overview[ApprovalParamTenantName] = tenant.Name
		}

		// inject high risk components, it is used in approval node
		highRiskComponentSet := container.NewSet[string]()
		for _, file := range chg.Scope.Files {
			for _, item := range file.ModifiedParameters {
				if item.RiskLevel != ccent.RiskLevelLow && (item.FromSource == ccent.ParameterCustom || item.ToSource == ccent.ParameterCustom) {
					component := cvt.To.ConfigCenter.ClusterType(chg.TargetCluster.ClusterType).ConfigType2Component(file.ConfigType)
					if component != "" {
						highRiskComponentSet.Insert(component)
					}
					continue
				}
			}
		}
		param.Parameters[ApprovalParamHighRiskComponents] = strings.Join(highRiskComponentSet.Slice(), ",")

		// determine approval template id
		switch chg.TargetCluster.ClusterType {
		case ccent.ClusterTypeLogicalCluster, ccent.ClusterTypeSharedPool,
			ccent.ClusterTypeTiCDCNG, ccent.ClusterTypeChangefeedNG, ccent.ClusterTypeChangefeedEssential:
			param.FromTemplate = ApprovalTemplateModifyConfigForNG
		default:
			param.FromTemplate = ApprovalTemplateModifyConfig
		}

	case ccent.ConfigurationChangeTargetTypeTenantInit:
		tenant, err := a.TenantService.GetTenant(ctx, chg.TargetTenantInit.TenantID)
		if err != nil {
			return param, fmt.Errorf("get tenant %d failed: %w", chg.TargetTenantInit.TenantID, err)
		}

		param.Name = ApprovalNameTenantInit
		param.Parameters[ApprovalParamTenantID] = strconv.FormatUint(tenant.Id, 10)
		param.Parameters[ApprovalParamTenantName] = tenant.Name
		param.Parameters[ApprovalParamTenantRegion] = string(tenant.GetTenantRegion())
		param.Overview[ApprovalParamTenantID] = strconv.FormatUint(tenant.Id, 10)
		param.Overview[ApprovalParamTenantName] = tenant.Name

		// inject high risk components, it is used in approval node
		highRiskComponentSet := container.NewSet[string]()
		for _, file := range chg.Scope.Files {
			for _, item := range file.ModifiedParameters {
				if item.RiskLevel != ccent.RiskLevelLow {
					component := cvt.To.ConfigCenter.ClusterType(chg.TargetTenantInit.ClusterType).ConfigType2Component(file.ConfigType)
					if component != "" {
						highRiskComponentSet.Insert(component)
					}
					continue
				}
			}
		}
		param.Parameters[ApprovalParamHighRiskComponents] = strings.Join(highRiskComponentSet.Slice(), ",")

		// determine approval template id
		switch chg.TargetTenantInit.ClusterType {
		case ccent.ClusterTypeLogicalCluster, ccent.ClusterTypeSharedPool,
			ccent.ClusterTypeTiCDCNG, ccent.ClusterTypeChangefeedNG:
			param.FromTemplate = ApprovalTemplateModifyConfigForNG
		default:
			param.FromTemplate = ApprovalTemplateModifyConfig
		}
	case ccent.ConfigurationChangeTargetTypeTemplate:
		param.Name = ApprovalNameTemplate
		param.Parameters[ApprovalParamTemplateType] = string(chg.TargetTemplate.TemplateType)

		if chg.TargetTemplate.TemplateType == ccent.TemplateTypeTenantDefault {
			tenantIDInt, err := strconv.ParseUint(*chg.TargetTemplate.TenantID, 10, 64)
			if err != nil {
				return param, fmt.Errorf("parse tenant id failed: %w", err)
			}

			tenant, err := a.TenantService.GetTenant(ctx, tenantIDInt)
			if err != nil {
				return param, fmt.Errorf("get tenant %d failed: %w", tenantIDInt, err)
			}

			param.Parameters[ApprovalParamTenantID] = strconv.FormatUint(tenant.Id, 10)
			param.Parameters[ApprovalParamTenantName] = tenant.Name
			param.Parameters[ApprovalParamTenantRegion] = string(tenant.GetTenantRegion())
			param.Overview[ApprovalParamTenantID] = strconv.FormatUint(tenant.Id, 10)
			param.Overview[ApprovalParamTenantName] = tenant.Name
		}

		// determine approval template id
		param.FromTemplate = ApprovalTemplateModifyTemplate
	default:
		return param, fmt.Errorf("unknown target type: %s", chg.TargetType)
	}

	return param, nil
}
