package tidbxinstance

import (
	"context"
	"strconv"
	"time"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.uber.org/zap"

	bffv1beta2 "github.com/tidbcloud/tidb-management-service/api/spec/bff/cluster_management/v1beta2"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/adapter/eventbus"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/repository"
	wfdag "github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/service/workflow/dag"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

// GetProjectMigrationTask returns the migration task for the caller's org.
// It enriches each migration item with real-time cluster counts and source
// project names fetched from external services.
func (s *TiDBServer) GetProjectMigrationTask(
	ctx context.Context,
	_ *bffv1beta2.GetProjectMigrationTaskRequest,
) (taskResp *bffv1beta2.ProjectMigrationTask, err error) {
	startTime := time.Now()
	defer func() {
		recordQueryAPIMetrics(apiGetProjectMigrationTask, time.Since(startTime).Seconds(), err)
	}()

	reqMeta, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	task, err := s.projectMigrationTaskRepo.GetByOrgID(ctx, reqMeta.OrgID)
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to get project migration task")
	}
	if task == nil {
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "project migration task not found")
	}

	taskResp = s.cvtProjectMigrationTask(ctx, task)
	return taskResp, nil
}

// ApproveProjectMigrationTask transitions the task from PENDING → RUNNING and
// starts the migration workflow.  Callers must supply target project information
// for every migration entry in the stored task.
func (s *TiDBServer) ApproveProjectMigrationTask(
	ctx context.Context,
	req *bffv1beta2.ApproveProjectMigrationTaskRequest,
) (*bffv1beta2.ApproveProjectMigrationTaskResponse, error) {
	var (
		err     error
		reqMeta *metadata.RequestMeta
		task    *entity.ProjectMigrationTask
	)
	tenantID := unknownTenantID
	startTime := time.Now()
	defer func() {
		recordWriteAPIMetrics(apiApproveProjectMigrationTask, tenantID, time.Since(startTime).Seconds(), err)
	}()

	detailsFunc := func() auditlog.Details {
		details := eventbus.NewAuditLogDetails(eventbus.TierTypeServerless)
		if task != nil {
			migrations := make([]map[string]any, 0, len(task.Migrations))
			for _, m := range task.Migrations {
				migrations = append(migrations, map[string]any{
					eventbus.KeySourceProjectID:   m.SourceProjectID,
					eventbus.KeyTargetProjectID:   m.TargetProjectID,
					eventbus.KeyTargetProjectName: m.TargetProjectName,
				})
			}
			details[eventbus.KeyMigrations] = migrations
		}
		return details
	}
	defer func() {
		s.auditLogSender.SubmitAuditLog(ctx, auditlog.EventApproveProjectMigration, reqMeta, detailsFunc, err)
	}()

	reqMeta, err = metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}
	tenantID = strconv.FormatUint(reqMeta.OrgID, 10)

	task, err = s.projectMigrationTaskRepo.GetByOrgID(ctx, reqMeta.OrgID)
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to get project migration task")
	}
	if task == nil {
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "project migration task not found")
	}
	if task.State != entity.ProjectMigrationTaskStatePending {
		return nil, bizerr.New(bizerr.ErrCodeParams,
			"project migration task is in state %s, only PENDING tasks can be approved", task.State)
	}

	// Build a lookup map from source project ID to request migration entry.
	reqMigrationMap := make(map[uint64]*bffv1beta2.Migration, len(req.GetMigrations()))
	for _, m := range req.GetMigrations() {
		reqMigrationMap[m.GetSourceProjectId()] = m
	}

	// Validate and apply target project info from the request.
	for _, m := range task.Migrations {
		reqM, ok := reqMigrationMap[m.SourceProjectID]
		if !ok {
			return nil, bizerr.New(bizerr.ErrCodeParams,
				"missing migration info for source project %d in request", m.SourceProjectID)
		}
		if reqM.GetTargetProjectId() == 0 && reqM.GetTargetProjectName() == "" {
			return nil, bizerr.New(bizerr.ErrCodeParams,
				"migration for source project %d must specify either target_project_id or target_project_name",
				m.SourceProjectID)
		}
		m.TargetProjectID = reqM.GetTargetProjectId()
		m.TargetProjectName = reqM.GetTargetProjectName()
	}

	// Transition to RUNNING and persist.
	task.State = entity.ProjectMigrationTaskStateRunning
	if err = s.projectMigrationTaskRepo.Save(ctx, task); err != nil {
		return nil, bizerr.WrapError(err, "failed to save project migration task")
	}

	// Launch the workflow (async; errors here should not roll back the state
	// update since the workflow engine will retry).
	if err = wfdag.RunProjectMigrationDag(ctx, s.workflow, &wfdag.ProjectMigrationDagParams{
		OrgID:      task.OrgID,
		TaskID:     task.ID,
		Migrations: task.Migrations,
	}); err != nil {
		tilog.Error(ctx, "failed to start project migration workflow",
			zap.Uint64("orgID", task.OrgID), zap.Error(err))
		return nil, bizerr.WrapError(err, "failed to start project migration workflow")
	}

	resp := s.cvtProjectMigrationTask(ctx, task)
	return &bffv1beta2.ApproveProjectMigrationTaskResponse{Task: resp}, nil
}

// cvtProjectMigrationTask converts a domain entity to the protobuf response,
// enriching each migration entry with real-time data.
func (s *TiDBServer) cvtProjectMigrationTask(
	ctx context.Context,
	task *entity.ProjectMigrationTask,
) *bffv1beta2.ProjectMigrationTask {
	migrations := make([]*bffv1beta2.Migration, 0, len(task.Migrations))
	for _, m := range task.Migrations {
		pbM := s.cvtMigration(ctx, task.OrgID, m)
		migrations = append(migrations, pbM)
	}

	stateVal, ok := bffv1beta2.ProjectMigrationTask_State_value[string(task.State)]
	if !ok {
		stateVal = int32(bffv1beta2.ProjectMigrationTask_STATE_UNSPECIFIED)
	}

	return &bffv1beta2.ProjectMigrationTask{
		OrgId:      task.OrgID,
		State:      bffv1beta2.ProjectMigrationTask_State(stateVal),
		Migrations: migrations,
	}
}

// cvtMigration converts one migration entry, querying real-time cluster counts
// and the source project name.
func (s *TiDBServer) cvtMigration(
	ctx context.Context,
	orgID uint64,
	m *entity.ProjectMigration,
) *bffv1beta2.Migration {
	pbM := &bffv1beta2.Migration{
		SourceProjectId:   m.SourceProjectID,
		TargetProjectId:   m.TargetProjectID,
		TargetProjectName: m.TargetProjectName,
	}

	// Fetch source project name from the account service.
	if m.SourceProjectID != 0 {
		proj, err := s.accountCli.GetProject(ctx, m.SourceProjectID)
		if err != nil {
			tilog.Warn(ctx, "failed to get source project name",
				zap.Uint64("sourceProjectID", m.SourceProjectID), zap.Error(err))
		} else if proj != nil {
			pbM.SourceProjectName = proj.GetName()
		}
	}

	// Count Starter + Essential clusters in the source project.
	srcCount, err := s.clusterRepo.CountClusterByFilter(ctx, &repository.ClusterFilter{
		ProjectIds:      []uint64{m.SourceProjectID},
		OrganizationIds: []uint64{orgID},
		ServicePlans:    []string{"Starter", "Essential"},
	})
	if err != nil {
		tilog.Warn(ctx, "failed to count source project clusters",
			zap.Uint64("sourceProjectID", m.SourceProjectID), zap.Error(err))
	} else {
		pbM.CountInSourceProject = uint64(srcCount)
	}

	// Count clusters in the target project (if known).
	if m.TargetProjectID != 0 {
		dstCount, err := s.clusterRepo.CountClusterByFilter(ctx, &repository.ClusterFilter{
			ProjectIds:      []uint64{m.TargetProjectID},
			OrganizationIds: []uint64{orgID},
			ServicePlans:    []string{"Starter", "Essential"},
		})
		if err != nil {
			tilog.Warn(ctx, "failed to count target project clusters",
				zap.Uint64("targetProjectID", m.TargetProjectID), zap.Error(err))
		} else {
			pbM.CountInTargetProject = uint64(dstCount)
		}
	}

	// Fall back to target project name fetched from account service when not
	// explicitly stored (e.g. after project creation).
	if pbM.TargetProjectName == "" && m.TargetProjectID != 0 {
		tProj, err := s.accountCli.GetProject(ctx, m.TargetProjectID)
		if err != nil {
			tilog.Warn(ctx, "failed to get target project name",
				zap.Uint64("targetProjectID", m.TargetProjectID), zap.Error(err))
		} else if tProj != nil {
			pbM.TargetProjectName = tProj.GetName()
		}
	}

	return pbM
}
