package server

import (
	"context"
	"encoding/base64"
	"fmt"
	"strings"

	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	config2 "github.com/tidbcloud/cluster-controller/server/pkg/config"
	"github.com/tidbcloud/cluster-controller/server/server/cvt"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	apiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	gitopsconsts "github.com/tidbcloud/infra-provider/pkg/gitops-sdk/consts"
)

// replayContext holds all fields needed across the builder's Build / BuildForUpdate lifecycle.
// It is constructed once at the entry point and passed through every step, eliminating
// repeated extraction of the same fields and ensuring providers are instantiated only once.
type replayContext struct {
	// request-derived fields
	sourceRef             *apiv1.Replay_ClusterRef
	targetRef             *apiv1.Replay_ClusterRef
	kubeClient            client.Client
	namespacePrefix       string
	requestProjectID      string
	explicitTasks         []cloudv1.ReplayTask
	sourceAuditLog        *apiv1.Replay_AuditLogAddr
	premiumTargetEndpoint string

	// task-spec defaults
	replicas        int32
	resources       *cloudv1.ReplayTaskResources
	username        string
	replayStartTime string
	runStartTime    string

	// computed once at construction time — do not mutate after newReplayContext returns
	deployPosition string
	sourceProvider replayClusterProvider
	targetProvider replayClusterProvider
}

// newReplayContext constructs a replayContext from the incoming request.
// It computes deployPosition and initialises source/target providers exactly once.
func newReplayContext(req *apiv1.Replay, cfg *config2.Config, server *ReplayServer) (*replayContext, error) {
	var kubeClient client.Client
	if server != nil {
		kubeClient = server.KubeClient
	}

	rctx := &replayContext{
		sourceRef:             req.SourceCluster,
		targetRef:             req.TargetCluster,
		kubeClient:            kubeClient,
		namespacePrefix:       cfg.NamespacePrefix,
		requestProjectID:      req.ProjectId,
		premiumTargetEndpoint: req.GetPremiumTargetEndpoint(),
		replicas:              cvt.ReplayDefaultReplicas(req),
		resources:             cvt.ReplayDefaultResources(req),
		username:              cvt.ReplayDefaultUsername(req),
		replayStartTime:       cvt.ReplayDefaultReplayStartTime(req),
		runStartTime:          cvt.ReplayDefaultRunStartTime(req),
	}
	if len(req.Tasks) > 0 {
		rctx.explicitTasks = cvt.PB2ReplayTasks(req)
	}
	if req.Defaults != nil {
		rctx.sourceAuditLog = req.Defaults.SourceAuditLog
	}

	rctx.deployPosition = computeReplayDeployPosition(req)

	if err := rctx.initProviders(); err != nil {
		return nil, err
	}
	return rctx, nil
}

// initProviders constructs source and target providers exactly once.
// Subsequent calls to buildReplayTasks / setKubernetesInfo reuse the same instances.
func (rc *replayContext) initProviders() error {
	if rc.sourceRef != nil && rc.sourceRef.ClusterId != "" {
		p, err := newReplayClusterProviderFn(rc, rc.sourceRef)
		if err != nil {
			return err
		}
		rc.sourceProvider = p
	}
	if rc.targetRef != nil && rc.targetRef.ClusterId != "" {
		p, err := newReplayClusterProviderFn(rc, rc.targetRef)
		if err != nil {
			return err
		}
		rc.targetProvider = p
	}
	return nil
}

// computeReplayDeployPosition determines whether the replay dataplane is co-located with
// the target cluster, the source cluster, or neither (individual placement).
// It is a pure function of the request — no provider calls are needed.
func computeReplayDeployPosition(req *apiv1.Replay) string {
	if matchRequestPlacement(req, req.TargetCluster) {
		return DeployPositionTarget
	}
	if matchRequestPlacement(req, req.SourceCluster) {
		return DeployPositionSource
	}
	return DeployPositionTarget
}

// replayClusterRefMatchesRequestPlacement returns true when the given cluster ref is
// co-located with the replay request's deployment environment:
//   - Dedicated clusters require the same project, tenant, provider, and region.
//   - Premium clusters are tenant-scoped and only require tenant, provider, and region.
func matchRequestPlacement(req *apiv1.Replay, ref *apiv1.Replay_ClusterRef) bool {
	if ref == nil {
		return false
	}
	if req.TenantId != ref.TenantId || req.ProjectId != ref.ProjectId ||
		req.Provider != ref.Provider || req.Region != ref.Region {
		return false
	}
	return true
}

func validateAWSAuditLog(aws *apiv1.Replay_AWSAuditLog) error {
	if aws == nil {
		return nil
	}

	hasS3URL := aws.S3Url != ""
	hasRoleARN := aws.RoleArn != ""
	hasExternalID := aws.ExternalId != nil && *aws.ExternalId != ""
	if hasRoleARN != hasExternalID {
		return errReplayAWSAuditLogCredentialMismatch
	}
	if !hasS3URL && !hasRoleARN {
		return errReplayAWSAuditLogMissingSource
	}
	return nil
}

func (b *replayBuilder) buildAnnotations() map[string]string {
	return map[string]string{
		gitopsconsts.OverrideHelperImageKey: b.cfg.OverrideHelperImage,
		gitopsconsts.OverrideRegistryKey:    b.cfg.OverrideRegistry,
		gitopsconsts.OverrideRepoPrefixKey:  b.cfg.OverrideRepoPrefix,
	}
}

func (b *replayBuilder) buildLabels() map[string]string {
	return map[string]string{
		cloudv1.TenantTagKey:                b.req.TenantId,
		cloudv1.ProjectTagKey:               b.req.ProjectId,
		cloudv1.ReplayTagKey:                b.req.ReplayId,
		cloudv1.ProviderTypeLabel:           b.req.Provider,
		cloudv1.RegionLabel:                 b.req.Region,
		consts.ClusterServerCreatorLabelKey: consts.ClusterServerCreatorLabelValue,
	}
}

func (b *replayBuilder) setTargetClusterInfo(replay *cloudv1.Replay, rctx *replayContext) {
	if rctx.targetRef == nil {
		return
	}
	replay.Spec.TargetClusterType = cvt.PB2ClusterType(rctx.targetRef.ClusterType)
	replay.Spec.TargetClusterID = rctx.targetRef.ClusterId
	replay.Spec.TargetProvider = rctx.targetRef.Provider
	replay.Spec.TargetRegion = rctx.targetRef.Region
}

func (b *replayBuilder) setSourceClusterInfo(replay *cloudv1.Replay, rctx *replayContext) {
	if rctx.sourceRef == nil {
		return
	}
	replay.Spec.SourceClusterType = ptr.To(cvt.PB2ClusterType(rctx.sourceRef.ClusterType))
	replay.Spec.SourceClusterID = ptr.To(rctx.sourceRef.ClusterId)
	replay.Spec.SourceProvider = ptr.To(rctx.sourceRef.Provider)
	replay.Spec.SourceRegion = ptr.To(rctx.sourceRef.Region)
}

func (b *replayBuilder) setReplayConfig(replay *cloudv1.Replay) {
	if replay == nil || b.req.Defaults == nil {
		return
	}
	replay.Spec.ReplayConfig = cvt.NewReplayOptionsBuilder().
		WithTaskDefaults(b.req.Defaults).
		Build()
}

// setKubernetesInfo sets the replay's Kubernetes spec.
// If an explicit dataplane is provided in the request, it is used directly.
// Otherwise, the appropriate provider (source or target based on deploy position) resolves it.
func (b *replayBuilder) setKubernetesInfo(ctx context.Context, replay *cloudv1.Replay, rctx *replayContext) error {
	if b.req.DataplaneKubernetes != nil {
		return b.setExplicitKubernetesInfo(ctx, replay, b.req.DataplaneKubernetes)
	}

	var dataplaneProvider replayClusterProvider
	switch rctx.deployPosition {
	case DeployPositionTarget:
		dataplaneProvider = rctx.targetProvider
	case DeployPositionSource:
		dataplaneProvider = rctx.sourceProvider
	default:
		return errReplayDataplaneUnsupportedPlacement
	}
	if dataplaneProvider == nil {
		return errReplayDataplaneMissingDataNode
	}

	resolution, err := dataplaneProvider.ResolveDataplane(ctx)
	if err != nil {
		return err
	}

	replay.Spec.Kubernetes = resolution.kubernetes
	if len(resolution.kubeconfigData) == 0 {
		return nil
	}
	if b.server == nil {
		return fmt.Errorf("premium kubeconfig persister is not set")
	}
	return b.server.createKubeConfigSecretFromData(ctx, replay, resolution.kubeconfigData)
}

func (b *replayBuilder) setExplicitKubernetesInfo(
	ctx context.Context,
	replay *cloudv1.Replay,
	request *apiv1.Kubernetes,
) error {
	if err := validateExplicitKubernetesRequest(request); err != nil {
		return err
	}

	kubernetes, err := buildExplicitKubernetes(b.req.Provider, request)
	if err != nil {
		return err
	}
	replay.Spec.Kubernetes = kubernetes

	if request.GetKubeconfig() == "" {
		return nil
	}

	kubeconfigData := decodeKubeconfigPayload(request.GetKubeconfig())
	return b.server.createKubeConfigSecretFromData(ctx, replay, kubeconfigData)
}

func decodeKubeconfigPayload(raw string) []byte {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil
	}
	if decoded, err := base64.StdEncoding.DecodeString(raw); err == nil && len(decoded) > 0 {
		return decoded
	}
	return []byte(raw)
}
