package impl

import (
	"context"
	"time"

	"github.com/AlekSi/pointer"
	"github.com/tidbcloud/dataflow-service/app/internal/status_matrix"
	"github.com/tidbcloud/dataflow-service/app/models/domain"
	"github.com/tidbcloud/dataflow-service/app/models/replay"
	"github.com/tidbcloud/dataflow-service/app/repo"
	"github.com/tidbcloud/dataflow-service/app/repo/entity"
	encryptor_wrapper "github.com/tidbcloud/dataflow-service/pkg/encryptor-wrapper"
	"github.com/tidbcloud/dataflow-service/pkg/log"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/db"
	"go.uber.org/zap"
)

const passwordPlaceholder = "{PASSWORD}"

type replayModelImpl struct {
	transProvider  db.TransProvider
	replayRepo     repo.ReplayRepo
	replayOperator replay.ReplayOperator
	statusMatrix   status_matrix.StatusMatrix
	encryptor      encryptor_wrapper.Encryptor
}

func NewReplayModel(
	transProvider db.TransProvider,
	replayRepo repo.ReplayRepo,
	replayOperator replay.ReplayOperator,
	encryptor encryptor_wrapper.Encryptor,
) replay.ReplayModel {
	return &replayModelImpl{
		transProvider:  transProvider,
		replayRepo:     replayRepo,
		replayOperator: replayOperator,
		statusMatrix:   NewReplayStatusMatrix(),
		encryptor:      encryptor,
	}
}

func (m *replayModelImpl) Create(
	ctx context.Context, req *domain.CreateReplayReq,
) (*domain.CreateReplayResp, bizerr.BizError) {
	logger := log.GetLogger(ctx)

	// encrypt ReplayConfig password
	if req.ReplayConfig != nil && req.ReplayConfig.Password != "" {
		if err := m.encryptor.EncryptBase64Inplace(&req.ReplayConfig.Password); err != nil {
			return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeUnknown, "failed to encrypt password")
		}
	}

	// encrypt ReplayTasks passwords
	if err := m.encryptReplayTasksPasswords(req.ReplayTasks); err != nil {
		return nil, err
	}

	replayEntity := &entity.Replay{
		TenantID:              entity.ID(req.TenantID),
		ProjectID:             entity.ID(req.ProjectID),
		CloudProvider:         req.CloudProvider,
		Region:                req.Region,
		SourceClusterID:       entity.ID(req.SourceClusterID),
		TargetClusterID:       entity.ID(req.TargetClusterID),
		DesiredState:          entity.ReplayDesiredStateDeploy,
		CurrentState:          entity.ReplayCurrentStatePending,
		ReplayTasks:           req.ReplayTasks,
		ReplayConfig:          req.ReplayConfig,
		TaskResources:         req.TaskResources,
		ReplayImage:           req.ReplayImage,
		Replicas:              req.Replicas,
		SourceAuditCredential: req.SourceAuditCredential,
		ReplayStatus: &entity.ReplayStatus{
			Phase: entity.ReplayPhasePending,
		},
	}
	if err := encryptReplayAuditLogSecretFields(m.encryptor, replayEntity); err != nil {
		return nil, err
	}

	if err := m.replayRepo.Create(ctx, replayEntity); err != nil {
		logger.Error("create replay failed", zap.Error(err))
		return nil, err.Chain("create replay")
	}

	logger.Info("replay created", zap.Uint64("replay_id", uint64(replayEntity.ID)))
	return &domain.CreateReplayResp{
		ReplayID: uint64(replayEntity.ID),
	}, nil
}

func (m *replayModelImpl) Get(
	ctx context.Context, req *domain.GetReplayReq,
) (*domain.GetReplayResp, bizerr.BizError) {
	replayEntity, err := m.replayRepo.Load(ctx, req.ReplayID)
	if err != nil {
		return nil, err.Chain("load replay")
	}
	replayDomain, bizErr := m.entityToReplayForRead(replayEntity)
	if bizErr != nil {
		return nil, bizErr
	}

	return &domain.GetReplayResp{
		Replay: replayDomain,
	}, nil
}

func (m *replayModelImpl) List(
	ctx context.Context, req *domain.ListReplayReq,
) (*domain.ListReplayResp, bizerr.BizError) {
	filterOp := func(qs entity.ReplayQuerySet) entity.ReplayQuerySet {
		if req.TenantID > 0 {
			qs = qs.TenantIDEq(entity.ID(req.TenantID))
		}
		if req.ProjectID > 0 {
			qs = qs.ProjectIDEq(entity.ID(req.ProjectID))
		}
		if req.ClusterID > 0 {
			qs = qs.TargetClusterIDEq(entity.ID(req.ClusterID))
		}
		return qs
	}

	total, err := m.replayRepo.Count(ctx, filterOp)
	if err != nil {
		return nil, err.Chain("count replays")
	}

	pagerOp := func(qs entity.ReplayQuerySet) entity.ReplayQuerySet {
		if req.PageSize > 0 {
			qs = qs.Limit(int(req.PageSize))
			if req.Page > 0 {
				qs = qs.Offset(int((req.Page - 1) * req.PageSize))
			}
		}
		return qs.OrderDescByCreatedAt()
	}

	replayEntities, err := m.replayRepo.ListWithPager(ctx, filterOp, pagerOp)
	if err != nil {
		return nil, err.Chain("list replays")
	}

	replays := make([]*domain.Replay, 0, len(replayEntities))
	for i := range replayEntities {
		replayDomain, bizErr := m.entityToReplayForRead(&replayEntities[i])
		if bizErr != nil {
			return nil, bizErr
		}
		replays = append(replays, replayDomain)
	}

	return &domain.ListReplayResp{
		Replays: replays,
		Total:   uint32(total),
	}, nil
}

func (m *replayModelImpl) Update(
	ctx context.Context, req *domain.UpdateReplayReq,
) (*domain.UpdateReplayResp, bizerr.BizError) {
	logger := log.GetLogger(ctx)
	replayEntity, err := m.replayRepo.Load(ctx, req.ReplayID)
	if err != nil {
		return nil, err.Chain("load replay")
	}
	if replayEntity.CurrentState != entity.ReplayCurrentStatePending &&
		replayEntity.CurrentState != entity.ReplayCurrentStatePaused &&
		replayEntity.CurrentState != entity.ReplayCurrentStateDeployed {
		return nil, bizerr.New(bizerr.ErrCodeUnknown,
			"cannot update replay in current state: %s", replayEntity.CurrentState)
	}

	if err := m.applyReplayUpdates(replayEntity, req); err != nil {
		return nil, err
	}

	if err := m.replayRepo.Save(ctx, replayEntity); err != nil {
		return nil, err.Chain("save replay")
	}

	logger.Info("replay updated", zap.Uint64("replay_id", req.ReplayID))
	replayDomain, bizErr := m.entityToReplayForRead(replayEntity)
	if bizErr != nil {
		return nil, bizErr
	}

	return &domain.UpdateReplayResp{
		Replay: replayDomain,
	}, nil
}

func (m *replayModelImpl) applyReplayUpdates(
	replayEntity *entity.Replay, req *domain.UpdateReplayReq,
) bizerr.BizError {
	if req.CloudProvider != nil {
		replayEntity.CloudProvider = *req.CloudProvider
	}
	if req.Region != nil {
		replayEntity.Region = *req.Region
	}
	if req.SourceClusterID != nil {
		replayEntity.SourceClusterID = entity.ID(*req.SourceClusterID)
	}
	if req.TargetClusterID != nil {
		replayEntity.TargetClusterID = entity.ID(*req.TargetClusterID)
	}
	if req.ReplayTasks != nil {
		if err := m.mergeReplayTasks(replayEntity, req.ReplayTasks); err != nil {
			return err
		}
	}
	if req.ReplayConfig != nil {
		if err := m.mergeReplayConfig(replayEntity, req.ReplayConfig); err != nil {
			return err
		}
	}
	if req.TaskResources != nil {
		replayEntity.TaskResources = req.TaskResources
	}
	if req.ReplayImage != nil {
		replayEntity.ReplayImage = *req.ReplayImage
	}
	if req.Replicas != nil {
		replayEntity.Replicas = *req.Replicas
	}
	if req.SourceAuditCredential != nil {
		if err := encryptAuditLogAddrSecretFields(m.encryptor, req.SourceAuditCredential); err != nil {
			return err
		}
		replayEntity.SourceAuditCredential = req.SourceAuditCredential
	}
	return nil
}

func (m *replayModelImpl) mergeReplayConfig(
	replayEntity *entity.Replay, newConfig *entity.ReplayCommonConfig,
) bizerr.BizError {
	// handle password: empty or placeholder means keep original, otherwise encrypt
	if newConfig.Password == "" || isPasswordPlaceholder(newConfig.Password) {
		// keep the original password
		if replayEntity.ReplayConfig != nil {
			newConfig.Password = replayEntity.ReplayConfig.Password
		} else {
			newConfig.Password = ""
		}
	} else {
		// encrypt the new password
		if err := m.encryptor.EncryptBase64Inplace(&newConfig.Password); err != nil {
			return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeUnknown, "failed to encrypt password")
		}
	}

	// keep the original username if not provided
	if newConfig.Username == "" && replayEntity.ReplayConfig != nil {
		newConfig.Username = replayEntity.ReplayConfig.Username
	}

	replayEntity.ReplayConfig = newConfig
	return nil
}

// isPasswordPlaceholder checks if the password is the placeholder "{PASSWORD}"
func isPasswordPlaceholder(password string) bool {
	return password == passwordPlaceholder
}

func (m *replayModelImpl) Delete(ctx context.Context, req *domain.DeleteReplayReq) bizerr.BizError {
	logger := log.GetLogger(ctx)

	replayEntity, err := m.replayRepo.Load(ctx, req.ReplayID)
	if err != nil {
		return err.Chain("load replay")
	}

	// check if the replay is already in the deleting state
	if replayEntity.CurrentState == entity.ReplayCurrentStateDeleted ||
		replayEntity.CurrentState == entity.ReplayCurrentStateDeleting {
		return nil // already in the deleting state or deleted
	}

	// calculate the next status
	nextStatus := m.statusMatrix.CalculateNextStatusForAction(
		status_matrix.CurrentStatus{Status: string(replayEntity.CurrentState)},
		string(entity.ReplayActionDelete),
	)
	if nextStatus == nil {
		return bizerr.New(bizerr.ErrCodeUnknown,
			"cannot delete replay in current state: %s", replayEntity.CurrentState)
	}

	// update the state to delete
	replayEntity.DesiredState = entity.ReplayDesiredStateDelete
	replayEntity.DeletingAt = pointer.ToTime(time.Now().UTC())
	if err := m.replayRepo.Save(ctx, replayEntity); err != nil {
		return err.Chain("save replay")
	}

	logger.Info("replay marked for deletion", zap.Uint64("replay_id", req.ReplayID))
	return nil
}

//nolint:dupl
func (m *replayModelImpl) Pause(ctx context.Context, req *domain.PauseReplayReq) bizerr.BizError {
	logger := log.GetLogger(ctx)

	replayEntity, err := m.replayRepo.Load(ctx, req.ReplayID)
	if err != nil {
		return err.Chain("load replay")
	}

	// check if the replay can be paused
	nextStatus := m.statusMatrix.CalculateNextStatusForAction(
		status_matrix.CurrentStatus{Status: string(replayEntity.CurrentState)},
		string(entity.ReplayActionPause),
	)
	if nextStatus == nil {
		return bizerr.New(bizerr.ErrCodeUnknown,
			"cannot pause replay in current state: %s", replayEntity.CurrentState)
	}

	// update the desired state and current state
	replayEntity.DesiredState = entity.ReplayDesiredStatePause
	now := time.Now().UTC()
	replayEntity.PausingAt = &now

	if err := m.replayRepo.Save(ctx, replayEntity); err != nil {
		return err.Chain("save replay")
	}

	logger.Info("replay pausing", zap.Uint64("replay_id", req.ReplayID))
	return nil
}

//nolint:dupl
func (m *replayModelImpl) Resume(ctx context.Context, req *domain.ResumeReplayReq) bizerr.BizError {
	logger := log.GetLogger(ctx)

	replayEntity, err := m.replayRepo.Load(ctx, req.ReplayID)
	if err != nil {
		return err.Chain("load replay")
	}

	// check if the replay can be resumed
	nextStatus := m.statusMatrix.CalculateNextStatusForAction(
		status_matrix.CurrentStatus{Status: string(replayEntity.CurrentState)},
		string(entity.ReplayActionActivate),
	)
	if nextStatus == nil {
		return bizerr.New(bizerr.ErrCodeUnknown,
			"cannot start replay in current state: %s", replayEntity.CurrentState)
	}

	// update the desired state and current state
	replayEntity.DesiredState = entity.ReplayDesiredStateActive
	now := time.Now().UTC()
	replayEntity.StartingAt = &now

	if err := m.replayRepo.Save(ctx, replayEntity); err != nil {
		return err.Chain("save replay")
	}

	logger.Info("replay resuming", zap.Uint64("replay_id", req.ReplayID))
	return nil
}

func entityToReplay(e *entity.Replay) *domain.Replay {
	return &domain.Replay{
		ID:                    uint64(e.ID),
		TenantID:              uint64(e.TenantID),
		ProjectID:             uint64(e.ProjectID),
		CloudProvider:         e.CloudProvider,
		Region:                e.Region,
		SourceClusterID:       uint64(e.SourceClusterID),
		TargetClusterID:       uint64(e.TargetClusterID),
		DesiredState:          e.DesiredState,
		CurrentState:          e.CurrentState,
		ReplayTasks:           e.ReplayTasks,
		ReplayConfig:          e.ReplayConfig,
		TaskResources:         e.TaskResources,
		ReplayImage:           e.ReplayImage,
		Replicas:              e.Replicas,
		SourceAuditCredential: e.SourceAuditCredential,
		ReplayStatus:          e.ReplayStatus,
		CreatedAt:             e.CreatedAt,
		UpdatedAt:             e.UpdatedAt,
	}
}

func (m *replayModelImpl) entityToReplayForRead(e *entity.Replay) (*domain.Replay, bizerr.BizError) {
	if e == nil {
		return nil, nil
	}
	cloned := cloneReplayForExecution(e)
	if err := decryptReplayAuditLogSecretFields(m.encryptor, cloned, auditLogSecretScopeTransport); err != nil {
		return nil, err.Chain("decrypt replay audit log bucket URLs for read")
	}
	return entityToReplay(cloned), nil
}

// encryptReplayTasksPasswords encrypts passwords in ReplayTasks for Create operation
func (m *replayModelImpl) encryptReplayTasksPasswords(tasks entity.ReplayTaskList) bizerr.BizError {
	for i := range tasks {
		if tasks[i].DataTarget.Password != nil && *tasks[i].DataTarget.Password != "" {
			if err := m.encryptor.EncryptBase64Inplace(tasks[i].DataTarget.Password); err != nil {
				return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeUnknown,
					"failed to encrypt password for task %s", tasks[i].Name)
			}
		}
	}
	return nil
}

// mergeReplayTasks merges ReplayTasks with password handling for Update operation
//
//nolint:nestif
func (m *replayModelImpl) mergeReplayTasks(
	replayEntity *entity.Replay, newTasks entity.ReplayTaskList,
) bizerr.BizError {
	// build a map of existing tasks by name for quick lookup
	existingTaskMap := make(map[string]*entity.ReplayTask)
	for i := range replayEntity.ReplayTasks {
		existingTaskMap[replayEntity.ReplayTasks[i].Name] = &replayEntity.ReplayTasks[i]
	}

	// process each new task
	for i := range newTasks {
		newTask := &newTasks[i]
		existingTask := existingTaskMap[newTask.Name]

		// handle DataTarget password
		if newTask.DataTarget.Password != nil {
			password := *newTask.DataTarget.Password
			if password == "" || isPasswordPlaceholder(password) {
				// keep the original password
				if existingTask != nil && existingTask.DataTarget.Password != nil {
					newTask.DataTarget.Password = existingTask.DataTarget.Password
				} else {
					newTask.DataTarget.Password = nil
				}
			} else {
				// encrypt the new password
				if err := m.encryptor.EncryptBase64Inplace(newTask.DataTarget.Password); err != nil {
					return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeUnknown,
						"failed to encrypt password for task %s", newTask.Name)
				}
			}
		} else if existingTask != nil && existingTask.DataTarget.Password != nil {
			// password not provided, keep the original
			newTask.DataTarget.Password = existingTask.DataTarget.Password
		}

		// handle DataTarget username
		if newTask.DataTarget.Username == nil && existingTask != nil {
			newTask.DataTarget.Username = existingTask.DataTarget.Username
		}

		if err := encryptReplayTaskAuditLogSecretFields(m.encryptor, newTask); err != nil {
			return err
		}
	}

	replayEntity.ReplayTasks = newTasks
	return nil
}
