package tcaction

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

	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	opent "github.com/tidbcloud/devops/entity/operation"
	"github.com/tidbcloud/devops/pkg/service/operation/actions"
	"github.com/tidbcloud/devops/pkg/service/operation/repo"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/types/known/durationpb"

	apiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
)

var (
	ActionCreateOperation opent.ActionMetadata = opent.ActionMetadata{
		Name: "CreateOperation",
		Type: opent.ActionTypeAuto,
	}
	ActionStartZonalShift opent.ActionMetadata = opent.ActionMetadata{
		Name: "StartZonalShift",
		Type: opent.ActionTypeAuto,
	}
	ActionOfflineAZ opent.ActionMetadata = opent.ActionMetadata{
		Name: "OfflineAZ",
		Type: opent.ActionTypeAuto,
	}
	ActionRecoverAZ opent.ActionMetadata = opent.ActionMetadata{
		Name: "RecoverAZ",
		Type: opent.ActionTypeAuto,
	}
	ActionStopZonalShift opent.ActionMetadata = opent.ActionMetadata{
		Name: "StopZonalShift",
		Type: opent.ActionTypeAuto,
	}
)

type (
	OperationType string
)

const (
	OperationTypeRecreateTcMember     OperationType = "RecreateTCMember"
	OperationTypeReplaceNode          OperationType = "ReplaceNode"
	OperationTypeShutDownUpgradeNodes OperationType = "ShutDownUpgradeNodes"
	OperationTypeRestartOtherPods     OperationType = "RestartOtherPods"
	OperationTypeFixIllegalBoundary   OperationType = "FixTiKVIllegalBoundary"
	OperationTypeEnableProxyProtocol  OperationType = "EnableProxyProtocolForCluster"
	OperationTypeModifyLBLogs         OperationType = "ModifyLoadBalanceLogs"
	OperationTypeModifyVPCLogs        OperationType = "ModifyVPCFlowLogs"
	OperationTypeOfflineAZ            OperationType = "OfflineAZ"
	OperationTypeRecoverAZ            OperationType = "RecoverAZ"
	OperationTypeStartZonalShift      OperationType = "StartZonalShift"
	OperationTypeUpdateZonalShift     OperationType = "UpdateZonalShift"
	OperationTypeStopZonalShift       OperationType = "StopZonalShift"
)

type createOperation struct {
	actions.BaseAction
	comps *actions.Components
	meta  opent.ActionMetadata
	// fixedOp, when set, ignores ParamOperationType and uses this type.
	fixedOp OperationType
	// opIDOut is the instance output key used to store/poll cluster-server operation id.
	opIDOut opent.OutputName
}

type createOperationParam struct {
	req   *apiv1.CreateOperationRequest
	force bool
}

func NewCreateOperation(comps *actions.Components) actions.AutoAction {
	return &createOperation{
		comps:   comps,
		meta:    ActionCreateOperation,
		opIDOut: opent.OutputOperationID,
	}
}

func NewStartZonalShift(comps *actions.Components) actions.AutoAction {
	return &createOperation{
		comps:   comps,
		meta:    ActionStartZonalShift,
		fixedOp: OperationTypeStartZonalShift,
		opIDOut: opent.OutputOperationIDStartZonalShift,
	}
}

func NewOfflineAZ(comps *actions.Components) actions.AutoAction {
	return &createOperation{
		comps:   comps,
		meta:    ActionOfflineAZ,
		fixedOp: OperationTypeOfflineAZ,
		opIDOut: opent.OutputOperationIDOfflineAZ,
	}
}

func NewRecoverAZ(comps *actions.Components) actions.AutoAction {
	return &createOperation{
		comps:   comps,
		meta:    ActionRecoverAZ,
		fixedOp: OperationTypeRecoverAZ,
		opIDOut: opent.OutputOperationIDRecoverAZ,
	}
}

func NewStopZonalShift(comps *actions.Components) actions.AutoAction {
	return &createOperation{
		comps:   comps,
		meta:    ActionStopZonalShift,
		fixedOp: OperationTypeStopZonalShift,
		opIDOut: opent.OutputOperationIDStopZonalShift,
	}
}

func (a *createOperation) Metadata() opent.ActionMetadata {
	return a.meta
}

func (a *createOperation) PreCheck(ctx context.Context, instance *opent.Instance) error {
	param, err := a.getParam(ctx, instance)
	if err != nil {
		return err
	}

	cid, err := actions.GetClusterID(instance)
	if err != nil {
		return err
	}
	cidStr := strconv.FormatUint(cid, 10)

	// just check the cluster existence
	cluster, err := a.comps.CoreSvc.GetCluster(ctx, cid, false)
	if err != nil {
		return err
	}
	// Ignore status check if force is true
	if !param.force && cluster.Status != core.ClusterStatus_CLUSTER_STATUS_NORMAL {
		return fmt.Errorf("cluster %d is not normal", cid)
	}

	// check if there are other operations running
	cnt, err := a.comps.OpRepo.CountInstance(ctx,
		repo.InstanceOpts.IDNotIn([]uint64{instance.ID}),
		repo.InstanceOpts.TypeIs(opent.OperationTargetCluster),
		repo.InstanceOpts.TargetIDIn(opent.OperationTargetCluster, []string{cidStr}),
		repo.InstanceOpts.PhaseIn([]opent.InstancePhase{opent.InstancePhaseRunning}),
		repo.InstanceOpts.OperationIn(allShutdownOperations),
	)
	if err != nil {
		return err
	}
	if cnt != 0 {
		return fmt.Errorf("there are %d running instances in this cluster, cid=%s", cnt, cidStr)
	}

	return nil
}

func (a *createOperation) Start(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	param, err := a.getParam(ctx, instance)
	if err != nil {
		return actions.FailedActionResult(err), nil
	}

	resp, err := a.comps.MgmtHTTP.ClusterServer(param.req.ClusterId).CreateOperation(ctx, param.req)
	if err != nil {
		if body, mErr := protojson.Marshal(param.req); mErr == nil {
			err = fmt.Errorf("%w; request=%s", err, string(body))
		}
		return actions.FailedActionResult(err), nil
	}

	output := map[opent.OutputName]string{
		opent.OutputOperateClusterID: param.req.ClusterId,
		a.opIDOut:                    resp.Id,
	}

	return actions.RunningActionResult("sent request to create operation ", output), nil
}

func (a *createOperation) Result(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	param, err := a.getParam(ctx, instance)
	if err != nil {
		return nil, err
	}

	opid, _ := instance.GetOutput(a.opIDOut)
	if opid == "" {
		return nil, fmt.Errorf("%s is not exist in output", a.opIDOut)
	}

	op, err := a.comps.MgmtHTTP.ClusterServer(param.req.ClusterId).GetOperation(ctx, &apiv1.GetOperationRequest{
		ClusterId:   param.req.ClusterId,
		OperationId: opid,
	})
	if err != nil {
		return actions.NewActionResult(opent.ActionStatusUnknown, err.Error(), nil), nil
	}

	switch op.Phase {
	case apiv1.Operation_PENDING:
		return actions.RunningActionResult("operation is pending", nil), nil
	case apiv1.Operation_RUNNING:
		return actions.RunningActionResult(fmt.Sprintf("operation is running: %s", op.Reason), nil), nil
	case apiv1.Operation_COMPLETED:
		return actions.SucceededActionResult(nil), nil
	case apiv1.Operation_FAILED:
		return actions.FailedActionResult(fmt.Errorf("operation %v is failed: %s", opid, op.Reason)), nil
	default:
		return actions.NewActionResult(opent.ActionStatusUnknown, fmt.Sprintf("unsupported operation phase '%s'", op.Phase), nil), nil
	}
}

func (a *createOperation) getParam(ctx context.Context, instance *opent.Instance) (_ *createOperationParam, _ error) {
	cidStr, _ := instance.GetParam(opent.ParamClusterID)
	if len(cidStr) == 0 {
		return nil, fmt.Errorf("cluster_id is required")
	}
	cid, err := strconv.ParseUint(cidStr, 10, 64)
	if err != nil {
		return nil, err
	}

	op := a.fixedOp
	if op == "" {
		opStr, _ := instance.GetParam(opent.ParamOperationType)
		if opStr == "" {
			return nil, fmt.Errorf("operation_type is required")
		}
		op = OperationType(opStr)
	}
	opType := getOperationType(string(op))

	force, _ := instance.GetParam(opent.ParamForce)

	p := &apiv1.CreateOperationRequest{}
	key := fmt.Sprintf("devops%v", instance.ID)
	if a.fixedOp != "" {
		// Must be ^[a-z0-9A-Z]{1,48}$ for cluster-server, and lowercase-safe for
		// K8s Workflow name "{type}-{idempotencyKey}" (RFC 1123 subdomain).
		key = fmt.Sprintf("devops%v%s", instance.ID, strings.ToLower(string(a.meta.Name)))
	}

	switch opType {
	case apiv1.Operation_ENABLE_VPC_FLOW_LOGS:
		enabled, _ := instance.GetParam(opent.ParamEnabled)
		sampleRate, _ := instance.GetParam(opent.ParamSampleRate)
		metadataMode, _ := instance.GetParam(opent.ParamMetadataMode)
		metadataField, _ := instance.GetParam(opent.ParamMetadataFields)
		aggregationInterval, _ := instance.GetParam(opent.ParamAggregationInterval)

		if enabled == "" {
			return nil, fmt.Errorf("%s is required", opent.ParamEnabled)
		}

		var metadataFields []string
		if metadataField != "" {
			metadataField = strings.Replace(metadataField, ", ", ",", -1)
			metadataFields = strings.Split(metadataField, ",")
		}

		p = &apiv1.CreateOperationRequest{
			ClusterId: strconv.FormatUint(cid, 10),
			Operation: &apiv1.Operation{
				IdempotencyKey: key,
				Type:           opType,
				Args: &apiv1.Operation_EnableVpcFlowLogs{
					EnableVpcFlowLogs: &apiv1.Operation_EnableVPCFlowLogs{
						Enabled:             enabled == "true",
						SampleRate:          sampleRate,
						MetadataMode:        metadataMode,
						MetadataFields:      metadataFields,
						AggregationInterval: aggregationInterval,
					},
				},
			},
		}

	case apiv1.Operation_ENABLE_LOAD_BALANCE_LOGS:
		enabled, _ := instance.GetParam(opent.ParamEnabled)
		sampleRate, _ := instance.GetParam(opent.ParamSampleRate)
		optionalMode, _ := instance.GetParam(opent.ParamOptionalMode)
		optionalField, _ := instance.GetParam(opent.ParamOptionalFields)

		if enabled == "" {
			return nil, fmt.Errorf("%s is required", opent.ParamEnabled)
		}

		var optionalFields []string
		if optionalField != "" {
			optionalField = strings.Replace(optionalField, ", ", ",", -1)
			optionalFields = strings.Split(optionalField, ",")
		}

		p = &apiv1.CreateOperationRequest{
			ClusterId: strconv.FormatUint(cid, 10),
			Operation: &apiv1.Operation{
				IdempotencyKey: key,
				Type:           opType,
				Args: &apiv1.Operation_EnableLoadBalanceLogs_{
					EnableLoadBalanceLogs: &apiv1.Operation_EnableLoadBalanceLogs{
						Enabled:        enabled == "true",
						SampleRate:     sampleRate,
						OptionalMode:   optionalMode,
						OptionalFields: optionalFields,
					},
				},
			},
		}

	case apiv1.Operation_ENABLE_PROXY_PROTOCOL_FOR_CLUSTER:
		p = &apiv1.CreateOperationRequest{
			ClusterId: strconv.FormatUint(cid, 10),
			Operation: &apiv1.Operation{
				IdempotencyKey: key,
				Type:           opType,
				Args: &apiv1.Operation_EnableProxyProtocolForCluster_{
					EnableProxyProtocolForCluster: &apiv1.Operation_EnableProxyProtocolForCluster{},
				},
			},
		}

	case apiv1.Operation_FIX_TIKV_ILLEGAL_BOUNDARY:
		dryRun, _ := instance.GetParam(opent.ParamDryRun)
		if dryRun == "" {
			return nil, fmt.Errorf("%s is required", opent.ParamDryRun)
		}

		p = &apiv1.CreateOperationRequest{
			ClusterId: strconv.FormatUint(cid, 10),
			Operation: &apiv1.Operation{
				IdempotencyKey: key,
				Type:           opType,
				Args: &apiv1.Operation_FixTikvIllegalBoundary{
					FixTikvIllegalBoundary: &apiv1.Operation_FixTiKVIllegalBoundary{
						DryRun: dryRun == "true",
					},
				},
			},
		}

	case apiv1.Operation_RECREATE_TC_MEMBER:
		memberName, _ := instance.GetParam(opent.ParamMemberName)
		if memberName == "" {
			return nil, fmt.Errorf("member_name is required")
		}

		p = &apiv1.CreateOperationRequest{
			ClusterId: strconv.FormatUint(cid, 10),
			Operation: &apiv1.Operation{
				IdempotencyKey: key,
				Type:           opType,
				Args: &apiv1.Operation_RecreateTcMember{
					RecreateTcMember: &apiv1.Operation_RecreateTCMember{
						MemberName: memberName,
					},
				},
			},
		}

	case apiv1.Operation_RETIRE_NODE:
		podName, _ := instance.GetParam(opent.ParamPodName)
		if podName == "" {
			return nil, fmt.Errorf("pod_name is required")
		}

		p = &apiv1.CreateOperationRequest{
			ClusterId: strconv.FormatUint(cid, 10),
			Operation: &apiv1.Operation{
				IdempotencyKey: key,
				Type:           opType,
				Args: &apiv1.Operation_RetireNode_{
					RetireNode: &apiv1.Operation_RetireNode{
						PodName: podName,
					},
				},
			},
		}

	case apiv1.Operation_SHUTDOWN_UPGRADE_NODES:
		desiredVersion, _ := instance.GetParam(opent.ParamDesiredVersion)
		if desiredVersion == "" {
			return nil, fmt.Errorf("desired_version is required")
		}
		p = &apiv1.CreateOperationRequest{
			ClusterId: strconv.FormatUint(cid, 10),
			Operation: &apiv1.Operation{
				IdempotencyKey: key,
				Type:           opType,
				Args: &apiv1.Operation_ShutdownUpgradeNodes{
					ShutdownUpgradeNodes: &apiv1.Operation_ShutDownUpgradeNodes{
						DesiredVersion: desiredVersion,
					},
				},
			},
		}

	case apiv1.Operation_RESTART_OTHER_PODS:
		// check whether the cluster id is in the white list
		info, ok := a.comps.Conf.OperationClusterWhiteList[cidStr]
		if !ok {
			return nil, fmt.Errorf("cluster %s is not allowed to execute restart DM", cidStr)
		}

		podName, _ := instance.GetParam(opent.ParamPodName)
		if podName == "" {
			return nil, fmt.Errorf("pod_name is required")
		}
		namespace, _ := instance.GetParam(opent.ParamNamespace)
		if namespace == "" {
			return nil, fmt.Errorf("namespace is required")
		}

		var pods []string
		if podName == "ALL" {
			dm, err := a.comps.DataflowCli.GetClusterDMConfig(ctx, info.ClusterAgentURL, cidStr)
			if err != nil {
				return nil, err
			}
			pods = append(pods, dm.ExecutorPodNames...)
		} else {
			pods = append(pods, podName)
		}
		p = &apiv1.CreateOperationRequest{
			ClusterId: strconv.FormatUint(cid, 10),
			Operation: &apiv1.Operation{
				IdempotencyKey: key,
				Type:           opType,
				Args: &apiv1.Operation_RestartOtherPods_{
					RestartOtherPods: &apiv1.Operation_RestartOtherPods{
						Namespace: namespace,
						PodNames:  pods,
						Force:     true,
					},
				},
			},
		}

	case apiv1.Operation_OFFLINE_AZ:
		az, _ := instance.GetParam(opent.ParamAvailabilityZone)
		if az == "" {
			return nil, fmt.Errorf("%s is required", opent.ParamAvailabilityZone)
		}
		offlineArgs := &apiv1.Operation_OfflineAZ{
			AvailabilityZone: az,
			GroupIds:         parseCommaSeparated(instance, opent.ParamGroupIDs),
		}
		if timeoutStr, _ := instance.GetParam(opent.ParamGracefulTimeout); timeoutStr != "" {
			d, err := time.ParseDuration(timeoutStr)
			if err != nil {
				return nil, fmt.Errorf("invalid %s %q: %w", opent.ParamGracefulTimeout, timeoutStr, err)
			}
			offlineArgs.GracefulTimeout = durationpb.New(d)
		}
		p = &apiv1.CreateOperationRequest{
			ClusterId: strconv.FormatUint(cid, 10),
			Operation: &apiv1.Operation{
				IdempotencyKey: key,
				Type:           opType,
				Args: &apiv1.Operation_OfflineAz{
					OfflineAz: offlineArgs,
				},
			},
		}

	case apiv1.Operation_RECOVER_AZ:
		az, _ := instance.GetParam(opent.ParamAvailabilityZone)
		if az == "" {
			return nil, fmt.Errorf("%s is required", opent.ParamAvailabilityZone)
		}
		p = &apiv1.CreateOperationRequest{
			ClusterId: strconv.FormatUint(cid, 10),
			Operation: &apiv1.Operation{
				IdempotencyKey: key,
				Type:           opType,
				Args: &apiv1.Operation_RecoverAz{
					RecoverAz: &apiv1.Operation_RecoverAZ{
						AvailabilityZone: az,
						GroupIds:         parseCommaSeparated(instance, opent.ParamGroupIDs),
					},
				},
			},
		}

	case apiv1.Operation_START_ZONAL_SHIFT:
		az, _ := instance.GetParam(opent.ParamAvailabilityZone)
		if az == "" {
			return nil, fmt.Errorf("%s is required", opent.ParamAvailabilityZone)
		}
		expiresIn, _ := instance.GetParam(opent.ParamExpiresIn)
		if expiresIn == "" {
			return nil, fmt.Errorf("%s is required", opent.ParamExpiresIn)
		}
		p = &apiv1.CreateOperationRequest{
			ClusterId: strconv.FormatUint(cid, 10),
			Operation: &apiv1.Operation{
				IdempotencyKey: key,
				Type:           opType,
				Args: &apiv1.Operation_StartZonalShift_{
					StartZonalShift: &apiv1.Operation_StartZonalShift{
						AvailabilityZone: az,
						ExpiresIn:        expiresIn,
						GroupIds:         parseCommaSeparated(instance, opent.ParamGroupIDs),
					},
				},
			},
		}

	case apiv1.Operation_UPDATE_ZONAL_SHIFT:
		expiresIn, _ := instance.GetParam(opent.ParamExpiresIn)
		if expiresIn == "" {
			return nil, fmt.Errorf("%s is required", opent.ParamExpiresIn)
		}
		p = &apiv1.CreateOperationRequest{
			ClusterId: strconv.FormatUint(cid, 10),
			Operation: &apiv1.Operation{
				IdempotencyKey: key,
				Type:           opType,
				Args: &apiv1.Operation_UpdateZonalShift_{
					UpdateZonalShift: &apiv1.Operation_UpdateZonalShift{
						ExpiresIn: expiresIn,
						GroupIds:  parseCommaSeparated(instance, opent.ParamGroupIDs),
					},
				},
			},
		}

	case apiv1.Operation_STOP_ZONAL_SHIFT:
		waitDuration, _ := instance.GetParam(opent.ParamWaitDuration)
		p = &apiv1.CreateOperationRequest{
			ClusterId: strconv.FormatUint(cid, 10),
			Operation: &apiv1.Operation{
				IdempotencyKey: key,
				Type:           opType,
				Args: &apiv1.Operation_StopZonalShift_{
					StopZonalShift: &apiv1.Operation_StopZonalShift{
						WaitDuration: waitDuration,
						GroupIds:     parseCommaSeparated(instance, opent.ParamGroupIDs),
					},
				},
			},
		}
	}

	if p.Operation == nil || p.Operation.Type == apiv1.Operation_TYPE_UNSPECIFIED || p.Operation.GetArgs() == nil {
		return nil, fmt.Errorf("unsupported or incomplete operation_type %q (resolved=%v, action=%s)", op, opType, a.meta.Name)
	}

	param := &createOperationParam{
		req:   p,
		force: force == "true",
	}
	return param, nil
}

func getOperationType(opType string) apiv1.Operation_Type {
	switch OperationType(opType) {
	case OperationTypeRecreateTcMember:
		return apiv1.Operation_RECREATE_TC_MEMBER
	case OperationTypeReplaceNode:
		return apiv1.Operation_RETIRE_NODE
	case OperationTypeShutDownUpgradeNodes:
		return apiv1.Operation_SHUTDOWN_UPGRADE_NODES
	case OperationTypeRestartOtherPods:
		return apiv1.Operation_RESTART_OTHER_PODS
	case OperationTypeFixIllegalBoundary:
		return apiv1.Operation_FIX_TIKV_ILLEGAL_BOUNDARY
	case OperationTypeEnableProxyProtocol:
		return apiv1.Operation_ENABLE_PROXY_PROTOCOL_FOR_CLUSTER
	case OperationTypeModifyLBLogs:
		return apiv1.Operation_ENABLE_LOAD_BALANCE_LOGS
	case OperationTypeModifyVPCLogs:
		return apiv1.Operation_ENABLE_VPC_FLOW_LOGS
	case OperationTypeOfflineAZ:
		return apiv1.Operation_OFFLINE_AZ
	case OperationTypeRecoverAZ:
		return apiv1.Operation_RECOVER_AZ
	case OperationTypeStartZonalShift:
		return apiv1.Operation_START_ZONAL_SHIFT
	case OperationTypeUpdateZonalShift:
		return apiv1.Operation_UPDATE_ZONAL_SHIFT
	case OperationTypeStopZonalShift:
		return apiv1.Operation_STOP_ZONAL_SHIFT
	default:
		return apiv1.Operation_TYPE_UNSPECIFIED
	}
}

func parseCommaSeparated(instance *opent.Instance, name opent.ParamName) []string {
	raw, _ := instance.GetParam(name)
	if raw == "" {
		return nil
	}
	raw = strings.ReplaceAll(raw, ", ", ",")
	parts := strings.Split(raw, ",")
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p != "" {
			out = append(out, p)
		}
	}
	return out
}
