package server

import (
	"context"
	"fmt"
	"strings"

	"github.com/pingcap/log"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"github.com/tidbcloud/cluster-controller/server/internal/util"
	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"
)

const (
	DeployPositionTarget     = "target"
	DeployPositionSource     = "source"
	DeployPositionIndividual = "individual"
)

// ReplayStateTransitionMap defines playback task state transition rules
var ReplayStateTransitionMap = map[cloudv1.ReplayState][]cloudv1.ReplayState{
	cloudv1.ReplayStateDeploy: {cloudv1.ReplayStateActive},
	cloudv1.ReplayStateActive: {cloudv1.ReplayStatePause},
	cloudv1.ReplayStatePause:  {cloudv1.ReplayStateActive},
}

// ReplayServer implements the replay service
type ReplayServer struct {
	apiv1.UnimplementedReplayServiceServer

	KubeClient   client.Client
	replayReader util.ReplayReader
}

type ReplayServerOption func(*ReplayServer)

func WithLabelSelectorReplayReader() ReplayServerOption {
	return func(s *ReplayServer) {
		s.replayReader = &util.LabelSelectorReplayReader{}
	}
}

func NewReplayServer(kubeClient client.Client, opts ...ReplayServerOption) *ReplayServer {
	cs := &ReplayServer{
		KubeClient: kubeClient,
	}

	for _, opt := range opts {
		opt(cs)
	}

	return cs
}

// ========== Create Replay Task ==========

func (s *ReplayServer) CreateReplay(ctx context.Context, req *apiv1.CreateReplayRequest) (*apiv1.Replay, error) {

	if req == nil || req.Replay == nil {
		return nil, status.Errorf(codes.InvalidArgument, "request is nil or replay is nil")
	}
	replayReq := req.Replay
	logger := tilog.WithContextL(ctx).With(zap.String("replay", replayReq.ReplayId))

	// build replay task CR
	builder := newReplayBuilder(s, replayReq)
	if err := s.ensureReplayNotExists(ctx, builder.namespace, replayReq.ReplayId); err != nil {
		return nil, err
	}

	replay, err := builder.Build(ctx)
	if err != nil {
		logger.Error("Failed to build replay", zap.Error(err))
		return nil, replayBuildError("Failed to build replay", err)
	}

	// create Kubernetes resources
	if err := s.ensureReplayResources(ctx, replay, replayReq); err != nil {
		if cleanupErr := s.cleanupGeneratedKubeConfigSecretIfReplayAbsent(ctx, replay); cleanupErr != nil {
			log.Error("Failed to cleanup generated kubeconfig secret after resource preparation failure", zap.Error(cleanupErr))
		}
		return nil, err
	}

	if err := s.KubeClient.Create(ctx, replay); err != nil {
		if cleanupErr := s.cleanupGeneratedKubeConfigSecretIfReplayAbsent(ctx, replay); cleanupErr != nil {
			log.Error("Failed to cleanup generated kubeconfig secret after replay create failure", zap.Error(cleanupErr))
		}
		log.Error("Failed to create replay", zap.Error(err))
		if apierrors.IsAlreadyExists(err) {
			return nil, status.Errorf(codes.AlreadyExists, "Replay %s already exists", replayReq.ReplayId)
		}
		return nil, err
	}

	return cvt.Replay2PB(replay), nil
}

// ========== Update Replay Task ==========

func (s *ReplayServer) UpdateReplay(ctx context.Context, req *apiv1.UpdateReplayRequest) (*apiv1.Replay, error) {
	if req == nil || req.Replay == nil {
		return nil, status.Errorf(codes.InvalidArgument, "request is nil or replay is nil")
	}
	replayReq := req.Replay

	cfg := config2.GetConfig()
	nameSpace := genNamespace(cfg.NamespacePrefix, replayReq.ProjectId)

	replay := &cloudv1.Replay{}
	if err := s.KubeClient.Get(ctx, client.ObjectKey{Name: replayReq.ReplayId, Namespace: nameSpace}, replay); err != nil {
		log.Error("Failed to get replay", zap.Error(err))
		if apierrors.IsNotFound(err) {
			return nil, status.Errorf(codes.NotFound, "Replay %s not found", replayReq.ReplayId)
		}
		return nil, status.Errorf(codes.Internal, "Failed to get replay %s: %s", replayReq.ReplayId, err)
	}

	if canUpdateConfig(replay.Spec.State) {
		// build replay task CR
		builder := newReplayBuilder(s, replayReq)
		err := builder.BuildForUpdate(ctx, replay)
		if err != nil {
			log.Error("Failed to build replay for update", zap.Error(err))
			return nil, replayBuildError("Failed to build replay for update", err)
		}

		// create Kubernetes resources
		if err := s.ensureReplayResources(ctx, replay, replayReq); err != nil {
			return nil, err
		}
	}

	// validate and update state
	if err := s.updateReplayState(replay, replayReq); err != nil {
		return nil, err
	}

	// update pause state
	if replayReq.ReconcilePaused != nil {
		replay.Spec.Paused = replayReq.ReconcilePaused
	}

	if err := s.KubeClient.Update(ctx, replay); err != nil {
		log.Error("Failed to update replay", zap.Error(err))
		if apierrors.IsNotFound(err) {
			return nil, status.Errorf(codes.NotFound, "Replay %s not found", replayReq.ReplayId)
		}
		return nil, err
	}

	return cvt.Replay2PB(replay), nil
}

// ========== Get Replay Task ==========

func (s *ReplayServer) GetReplay(ctx context.Context, req *apiv1.GetReplayRequest) (*apiv1.GetReplayResponse, error) {
	logger := tilog.WithContextL(ctx).With(zap.String("replay", req.ReplayId))

	replay, err := s.replayReader.Get(ctx, s.KubeClient, req.ReplayId)
	if err != nil {
		logger.Error("Failed to get replay", zap.Error(err))
		if apierrors.IsNotFound(err) {
			return nil, status.Errorf(codes.NotFound, "Replay %s not found", req.ReplayId)
		}
		return nil, status.Errorf(codes.Internal, "Failed to get replay %s: %s", req.ReplayId, err)
	}

	return &apiv1.GetReplayResponse{
		Replay: cvt.Replay2PB(replay),
		Status: cvt.ReplayStatus2PB(&replay.Status),
	}, nil
}

// ========== Start/Stop Replay Task ==========

func (s *ReplayServer) StartReplay(ctx context.Context, req *apiv1.StartReplayRequest) (*apiv1.Replay, error) {
	logger := tilog.WithContextL(ctx).With(zap.String("replay", req.ReplayId))

	replay, err := s.replayReader.Get(ctx, s.KubeClient, req.ReplayId)
	if err != nil {
		logger.Error("Failed to get replay", zap.Error(err))
		if apierrors.IsNotFound(err) {
			return nil, status.Errorf(codes.NotFound, "Replay %s not found", req.ReplayId)
		}
		return nil, status.Errorf(codes.Internal, "Failed to get replay %s: %s", req.ReplayId, err)
	}

	if err := s.transReplayState(ctx, replay, cloudv1.ReplayStateActive); err != nil {
		return nil, err
	}

	return cvt.Replay2PB(replay), nil
}

func (s *ReplayServer) StopReplay(ctx context.Context, req *apiv1.StopReplayRequest) (*apiv1.Replay, error) {
	logger := tilog.WithContextL(ctx).With(zap.String("replay", req.ReplayId))

	replay, err := s.replayReader.Get(ctx, s.KubeClient, req.ReplayId)
	if err != nil {
		logger.Error("Failed to get replay", zap.Error(err))
		if apierrors.IsNotFound(err) {
			return nil, status.Errorf(codes.NotFound, "Replay %s not found", req.ReplayId)
		}
		return nil, status.Errorf(codes.Internal, "Failed to get replay %s: %s", req.ReplayId, err)
	}

	if err := s.transReplayState(ctx, replay, cloudv1.ReplayStatePause); err != nil {
		return nil, err
	}

	return cvt.Replay2PB(replay), nil
}

// ========== Delete Replay Task ==========

func (s *ReplayServer) DeleteReplay(ctx context.Context, req *apiv1.DeleteReplayRequest) (*apiv1.Replay, error) {
	logger := tilog.WithContextL(ctx).With(zap.String("replay", req.ReplayId))

	replay, err := s.replayReader.Get(ctx, s.KubeClient, req.ReplayId)
	if err != nil {
		logger.Error("Failed to get replay", zap.Error(err))
		if apierrors.IsNotFound(err) {
			return nil, status.Errorf(codes.NotFound, "Replay %s not found", req.ReplayId)
		}
		return nil, status.Errorf(codes.Internal, "Failed to get replay %s: %s", req.ReplayId, err)
	} else if !replay.DeletionTimestamp.IsZero() {
		return nil, status.Errorf(codes.FailedPrecondition, "Replay %s is being deleted", req.ReplayId)
	}

	if err := s.KubeClient.Delete(ctx, replay); err != nil {
		logger.Error("Failed to delete replay", zap.Error(err))
		return nil, status.Errorf(codes.Internal, "Failed to delete replay %s: %s", req.ReplayId, err)
	}

	return cvt.Replay2PB(replay), nil
}

// ========== ReplayBuilder Replay Task Builder ==========

type replayBuilder struct {
	server    *ReplayServer
	req       *apiv1.Replay
	cfg       *config2.Config
	namespace string
}

func newReplayBuilder(server *ReplayServer, req *apiv1.Replay) *replayBuilder {
	cfg := config2.GetConfig()
	return &replayBuilder{
		server:    server,
		req:       req,
		cfg:       cfg,
		namespace: genNamespace(cfg.NamespacePrefix, req.ProjectId),
	}
}

func (b *replayBuilder) Build(ctx context.Context) (*cloudv1.Replay, error) {
	if err := ensureNamespace(ctx, b.server.KubeClient, b.namespace, b.req.ProjectId, b.req.TenantId); err != nil {
		return nil, err
	}

	rctx, err := newReplayContext(b.req, b.cfg, b.server)
	if err != nil {
		return nil, err
	}

	replay := &cloudv1.Replay{
		ObjectMeta: metav1.ObjectMeta{
			Name:        b.req.ReplayId,
			Namespace:   b.namespace,
			Annotations: b.buildAnnotations(),
			Labels:      b.buildLabels(),
		},
		Spec: cloudv1.ReplaySpec{
			State: cvt.PB2ReplayState(b.req.State),
		},
	}

	b.setTargetClusterInfo(replay, rctx)
	b.setSourceClusterInfo(replay, rctx)

	b.setReplayConfig(replay)

	if err := b.buildReplayTasks(ctx, replay, rctx); err != nil {
		return nil, err
	}
	if err := b.setKubernetesInfo(ctx, replay, rctx); err != nil {
		return nil, err
	}

	return replay, nil
}

func (b *replayBuilder) BuildForUpdate(ctx context.Context, replay *cloudv1.Replay) error {
	if replay == nil {
		return fmt.Errorf("existing replay is not set")
	}

	rctx, err := newReplayContext(b.req, b.cfg, b.server)
	if err != nil {
		return err
	}

	b.setReplayConfig(replay)
	if err := b.buildReplayTasks(ctx, replay, rctx); err != nil {
		return err
	}
	return b.setKubernetesInfo(ctx, replay, rctx)
}

// ========== Helper Methods: State Transition and Update ==========

func (s *ReplayServer) transReplayState(ctx context.Context, replay *cloudv1.Replay, newState cloudv1.ReplayState) error {
	logger := tilog.WithContextL(ctx).With(zap.String("replay", replay.Name))

	if !isValidStateTransition(replay.Spec.State, newState) {
		return status.Errorf(codes.InvalidArgument, "invalid state transition from %s to %s", replay.Spec.State, newState)
	}

	replay.Spec.State = newState
	if err := s.KubeClient.Update(ctx, replay); err != nil {
		logger.Error("Failed to trans replay state", zap.String("curState", string(replay.Spec.State)), zap.String("newState", string(newState)), zap.Error(err))
		return err
	}

	return nil
}

func (s *ReplayServer) updateReplayState(replay *cloudv1.Replay, req *apiv1.Replay) error {
	newState := cvt.PB2ReplayState(req.State)
	if newState == replay.Spec.State {
		return nil
	}

	if !isValidStateTransition(replay.Spec.State, newState) {
		return status.Errorf(codes.InvalidArgument, "invalid state transition from %s to %s", replay.Spec.State, newState)
	}

	replay.Spec.State = newState
	return nil
}

// ========== Helper Methods: Resource Management ==========

func (s *ReplayServer) ensureReplayResources(
	ctx context.Context,
	replay *cloudv1.Replay,
	req *apiv1.Replay,
) error {
	// create password secret
	if err := s.createPasswordSecret(ctx, replay, req); err != nil {
		return err
	}

	return nil
}

func (s *ReplayServer) ensureReplayNotExists(ctx context.Context, namespace, replayID string) error {
	replay := &cloudv1.Replay{}
	if err := s.KubeClient.Get(ctx, client.ObjectKey{Name: replayID, Namespace: namespace}, replay); err != nil {
		if apierrors.IsNotFound(err) {
			return nil
		}
		return status.Errorf(codes.Internal, "Failed to get replay %s: %s", replayID, err)
	}
	return status.Errorf(codes.AlreadyExists, "Replay %s already exists", replayID)
}

func (s *ReplayServer) createKubeConfigSecretFromData(
	ctx context.Context,
	replay *cloudv1.Replay,
	kubeconfigData []byte,
) error {
	// Secret.Data expects raw bytes. Kubernetes will serialize them as base64 in the API response, so
	// callers should decode any transport/base64 wrapping before reaching this helper instead of trying
	// to double-encode or "encrypt" the payload here.
	secret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      fmt.Sprintf("%s-kubeconfig", replay.Name),
			Namespace: replay.Namespace,
		},
		Data: map[string][]byte{
			"kubeconfig": kubeconfigData,
		},
	}

	if _, err := controllerutil.CreateOrUpdate(ctx, s.KubeClient, secret, func() error {
		secret.Data = map[string][]byte{
			"kubeconfig": kubeconfigData,
		}
		return nil
	}); err != nil {
		return fmt.Errorf("create kubeconfig secret: %w", err)
	}

	replay.Spec.Kubernetes.KubeConfigSecretRef = &corev1.SecretReference{
		Name:      secret.Name,
		Namespace: secret.Namespace,
	}
	return nil
}

func (s *ReplayServer) cleanupGeneratedKubeConfigSecretIfReplayAbsent(
	ctx context.Context,
	replay *cloudv1.Replay,
) error {
	ref := generatedReplayKubeConfigSecretRef(replay)
	if ref == nil {
		return nil
	}

	existingReplay := &cloudv1.Replay{}
	err := s.KubeClient.Get(ctx, client.ObjectKey{
		Name:      replay.Name,
		Namespace: replay.Namespace,
	}, existingReplay)
	if err == nil {
		return nil
	}
	if !apierrors.IsNotFound(err) {
		return fmt.Errorf("check replay before generated kubeconfig secret cleanup: %w", err)
	}

	secret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      ref.Name,
			Namespace: ref.Namespace,
		},
	}
	if err := s.KubeClient.Delete(ctx, secret); err != nil && !apierrors.IsNotFound(err) {
		return fmt.Errorf("cleanup generated kubeconfig secret %s/%s: %w", ref.Namespace, ref.Name, err)
	}
	return nil
}

func generatedReplayKubeConfigSecretRef(replay *cloudv1.Replay) *corev1.SecretReference {
	if replay == nil || replay.Spec.Kubernetes.KubeConfigSecretRef == nil {
		return nil
	}

	ref := replay.Spec.Kubernetes.KubeConfigSecretRef
	if ref.Namespace != replay.Namespace || ref.Name != fmt.Sprintf("%s-kubeconfig", replay.Name) {
		return nil
	}
	return ref
}

func (s *ReplayServer) createPasswordSecret(
	ctx context.Context,
	replay *cloudv1.Replay,
	replayReq *apiv1.Replay,
) error {
	secretData := make(map[string][]byte)
	if username, password, ok, err := resolveDefaultCredentialSecretEntry(replayReq); err != nil {
		return err
	} else if ok {
		secretData[username] = password
	}

	for _, task := range replayReq.Tasks {
		if task.Target.Credential != nil && task.Target.Credential.Username != "" && task.Target.Credential.Password != "" {
			secretData[task.Target.Credential.Username] = []byte(task.Target.Credential.Password)
		}
	}

	if len(secretData) == 0 {
		return nil
	}

	secret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      fmt.Sprintf("replay-%s-target-db-password", replay.Name),
			Namespace: replay.Namespace,
		},
		Data: secretData,
	}

	if _, err := controllerutil.CreateOrUpdate(ctx, s.KubeClient, secret, func() error {
		secret.Data = secretData
		return nil
	}); err != nil {
		return fmt.Errorf("create db password secret failed: %w", err)
	}

	replay.Spec.PasswordSecretRef = corev1.SecretReference{
		Name:      secret.Name,
		Namespace: secret.Namespace,
	}
	return nil
}

func resolveDefaultCredentialSecretEntry(replayReq *apiv1.Replay) (string, []byte, bool, error) {
	if replayReq == nil || replayReq.Defaults == nil || replayReq.Defaults.Credential == nil {
		return "", nil, false, nil
	}

	cred := replayReq.Defaults.Credential
	if cred.Username == "" && cred.Password == "" {
		return "", nil, false, nil
	}

	if cred.Username == "" || cred.Password == "" {
		return "", nil, false, status.Error(codes.InvalidArgument, "defaults.credential.username and defaults.credential.password must be updated together")
	}

	return cred.Username, []byte(cred.Password), true, nil
}

// ========== Helper Functions ==========

func isValidStateTransition(currentState, newState cloudv1.ReplayState) bool {
	validTransitions, ok := ReplayStateTransitionMap[currentState]
	if !ok {
		return false
	}
	for _, state := range validTransitions {
		if state == newState {
			return true
		}
	}
	return false
}

func canUpdateConfig(state cloudv1.ReplayState) bool {
	return state == cloudv1.ReplayStateDeploy || state == cloudv1.ReplayStatePause
}

func extractPrefix(str string) string {
	if idx := strings.Index(str, "-tidb-"); idx != -1 {
		return str[:idx]
	}
	return str
}
