package maintenancewindow

import (
	"context"
	"errors"
	"fmt"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"

	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	globalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/clusterevent"
)

func (svc *service) UpdateMaintenanceTaskExecution(
	ctx context.Context,
	req *tidbmgmtservicev1.UpdateMaintenanceTaskExecutionRequest,
) (*tidbmgmtservicev1.MaintenanceTaskExecution, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.UpdateMaintenanceTaskExecutionSvc")
	defer span.End()
	if err := svc.checkUpdateMaintenanceTaskExecution(req); err != nil {
		return nil, err
	}
	reqExecution := req.GetMaintenanceTaskExecution()
	updated := &tidbmgmtservicev1.MaintenanceTaskExecution{}
	err := svc.transaction.Transaction(ctx, func(ctx context.Context) error {
		var err bizerr.BizError
		updated, err = svc.updateMaintenanceTaskExecution(ctx, reqExecution)
		return err
	})
	if err != nil {
		return nil, bizerr.WrapError(err, "update maintenance task execution")
	}
	return updated, nil
}

func (svc *service) updateMaintenanceTaskExecution(
	ctx context.Context,
	reqExecution *tidbmgmtservicev1.MaintenanceTaskExecution,
) (*tidbmgmtservicev1.MaintenanceTaskExecution, bizerr.BizError) {
	taskExecution, err := svc.mteRepo.Load(ctx, reqExecution.GetMaintenanceTaskExecutionId())
	if err != nil {
		return nil, err
	}

	if reqExecution.State == tidbmgmtservicev1.MaintenanceTaskExecution_STATE_UNSPECIFIED {
		return nil, bizerr.New(bizerr.ErrCodeParams, "missing state")
	}

	if reqExecution.GetState() == taskExecution.GetState() {
		return taskExecution, nil
	}

	task, err := svc.mtRepo.Load(ctx, taskExecution.GetMaintenanceTaskId())
	if err != nil {
		return nil, err
	}

	cluster, err_ := svc.clusterRepo.GetCluster(ctx, taskExecution.GetClusterId(), true)
	if err_ != nil {
		return nil, bizerr.WrapError(err_, "get cluster")
	}

	var ev *clusterevent.MaintenanceWindowEvent
	switch reqExecution.GetState() {
	case tidbmgmtservicev1.MaintenanceTaskExecution_PENDING:
		return nil, bizerr.New(bizerr.ErrCodeParams, "cannot update status to pending")
	case tidbmgmtservicev1.MaintenanceTaskExecution_RUNNING:
		canStart, err := svc.checkWhetherExecutionCanStartByClusterStatus(ctx, cluster.DeployType, cluster.ClusterId)
		if err != nil {
			return nil, err
		}
		if !canStart {
			return nil, bizerr.New(
				bizerr.ErrCodeParams,
				"cannot start a task execution on a cluster that is not in normal or paused status",
			)
		}
		ev, err = buildStartEvent(ctx, taskExecution, task, cluster)
		if err != nil {
			return nil, err
		}
		if err := svc.createCustomStatus(ctx, taskExecution, cluster.ClusterId); err != nil {
			return nil, err
		}
	case tidbmgmtservicev1.MaintenanceTaskExecution_DONE:
		ev, err = buildFinishEvent(ctx, taskExecution, task)
		if err != nil {
			return nil, err
		}
		if err := svc.deleteCustomStatus(ctx, taskExecution, cluster.ClusterId); err != nil {
			return nil, err
		}
	case tidbmgmtservicev1.MaintenanceTaskExecution_CANCELED:
		ev, err = buildCancelEvent(ctx, taskExecution, task, cluster)
		if err != nil {
			return nil, err
		}
		if err := svc.deleteCustomStatus(ctx, taskExecution, cluster.ClusterId); err != nil {
			return nil, err
		}
	}

	tilog.Info(
		ctx,
		"update maintenance task execution status",
		zap.String("reason", taskExecution.CancellationReason.String()),
	)

	if err := svc.eventCommitter.Write(ctx, clusterevent.TopicMaintenanceWindowEvent, ev); err != nil {
		return nil, err
	}

	if err := svc.mteRepo.Save(ctx, taskExecution); err != nil {
		return nil, err
	}

	return taskExecution, nil
}

func (svc *service) checkUpdateMaintenanceTaskExecution(
	req *tidbmgmtservicev1.UpdateMaintenanceTaskExecutionRequest,
) bizerr.BizError {
	if req.GetMaintenanceTaskExecution() == nil {
		return bizerr.New(bizerr.ErrCodeParams, "missing maintenance task execution")
	}
	reqExecution := req.GetMaintenanceTaskExecution()
	if reqExecution.GetMaintenanceTaskExecutionId() == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "missing maintenance task execution id")
	}
	if reqExecution.GetState() == tidbmgmtservicev1.MaintenanceTaskExecution_STATE_UNSPECIFIED {
		return bizerr.New(bizerr.ErrCodeParams, "missing state")
	}
	return nil
}

func buildStartEvent(
	ctx context.Context,
	exe *entity.MaintenanceTaskExecution,
	task *entity.MaintenanceTask,
	cluster *entity.Cluster,
) (*clusterevent.MaintenanceWindowEvent, bizerr.BizError) {
	if cluster.DeletedAt != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cannot start a task execution on a deleted cluster")
	}

	if task.State != tidbmgmtservicev1.MaintenanceTask_RUNNING {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cannot start a task execution on a task that is not running")
	}

	state := exe.State
	switch state {
	case tidbmgmtservicev1.MaintenanceTaskExecution_PENDING:
		// continue
	case tidbmgmtservicev1.MaintenanceTaskExecution_RUNNING:
		return nil, bizerr.New(bizerr.ErrCodeParams, "task execution is already running")
	case tidbmgmtservicev1.MaintenanceTaskExecution_DONE:
		return nil, bizerr.New(bizerr.ErrCodeParams, "cannot start a finished task execution")
	case tidbmgmtservicev1.MaintenanceTaskExecution_CANCELED:
		exe.CancellationReason = tidbmgmtservicev1.MaintenanceTaskExecution_REASON_UNSPECIFIED
	}

	exe.State = tidbmgmtservicev1.MaintenanceTaskExecution_RUNNING
	ev := newMaintenanceWindowEvent(ctx, exe, task.GetProjectId(), clusterevent.MaintenanceWindowEventTypeExecutionRunning)

	return ev, nil
}

func buildFinishEvent(
	ctx context.Context,
	exe *entity.MaintenanceTaskExecution,
	task *entity.MaintenanceTask,
) (*clusterevent.MaintenanceWindowEvent, bizerr.BizError) {
	state := exe.State
	switch state {
	case tidbmgmtservicev1.MaintenanceTaskExecution_PENDING:
		return nil, bizerr.New(bizerr.ErrCodeParams, "only running task execution can be finished")
	case tidbmgmtservicev1.MaintenanceTaskExecution_RUNNING:
		// continue
	case tidbmgmtservicev1.MaintenanceTaskExecution_DONE:
		return nil, bizerr.New(bizerr.ErrCodeParams, "task execution is already done")
	case tidbmgmtservicev1.MaintenanceTaskExecution_CANCELED:
		return nil, bizerr.New(bizerr.ErrCodeParams, "only running task execution can be finished")
	}

	exe.State = tidbmgmtservicev1.MaintenanceTaskExecution_DONE
	ev := newMaintenanceWindowEvent(ctx, exe, task.GetProjectId(), clusterevent.MaintenanceWindowEventTypeExecutionDone)
	return ev, nil
}

func buildCancelEvent(
	ctx context.Context,
	exe *entity.MaintenanceTaskExecution,
	task *entity.MaintenanceTask,
	cluster *entity.Cluster,
) (*clusterevent.MaintenanceWindowEvent, bizerr.BizError) {
	state := exe.State
	switch state {
	case tidbmgmtservicev1.MaintenanceTaskExecution_PENDING:
		// continue
	case tidbmgmtservicev1.MaintenanceTaskExecution_RUNNING:
		// continue
	case tidbmgmtservicev1.MaintenanceTaskExecution_DONE:
		return nil, bizerr.New(bizerr.ErrCodeParams, "cannot cancel a finished task execution")
	case tidbmgmtservicev1.MaintenanceTaskExecution_CANCELED:
		return nil, bizerr.New(bizerr.ErrCodeParams, "task execution is already canceled")
	}

	exe.State = tidbmgmtservicev1.MaintenanceTaskExecution_CANCELED
	if cluster.DeletedAt != nil {
		exe.CancellationReason = tidbmgmtservicev1.MaintenanceTaskExecution_CLUSTER_DELETED
	} else {
		exe.CancellationReason = tidbmgmtservicev1.MaintenanceTaskExecution_OPS_CANCELED
	}
	ev := newMaintenanceWindowEvent(ctx, exe, task.GetProjectId(), clusterevent.MaintenanceWindowEventTypeExecutionCanceled)
	return ev, nil
}

func newMaintenanceWindowEvent(
	ctx context.Context,
	exe *entity.MaintenanceTaskExecution,
	projectID uint64,
	eventType clusterevent.MaintenanceWindowEventType,
) *clusterevent.MaintenanceWindowEvent {
	ev := &clusterevent.MaintenanceWindowEvent{
		ProjectID:        projectID,
		TraceID:          clusterevent.TraceIDFromContext(ctx),
		EventType:        eventType,
		EventCreatedAtMs: 0,   // fill by pub sub implementation
		MaintenanceTask:  nil, // leave it empty
		MaintenanceTaskExecution: &clusterevent.MaintenanceTaskExecution{
			ID:                exe.GetMaintenanceTaskExecutionId(),
			MaintenanceTaskID: exe.GetMaintenanceTaskId(),
			ClusterID:         exe.GetClusterId(),
			Status:            exe.State.String(),
		},
	}

	return ev
}

func (svc *service) checkWhetherExecutionCanStartByClusterStatus(
	ctx context.Context,
	deployType entity.DeployType,
	clusterId uint64,
) (bool, bizerr.BizError) {
	switch deployType {
	case entity.DeployTypeDedicated:
		c, err := svc.dedicatedClusterSvc.GetCluster(ctx, clusterId, entity.ClusterViewClusterViewBasic, true, false)
		if err != nil {
			return false, bizerr.WrapError(err, "failed to get dedicated cluster")
		}
		dc, err := c.GetDedicatedCluster()
		if err != nil {
			return false, bizerr.WrapError(err, "failed to get dedicated cluster")
		}
		return dc.GetState() == globaldedicatedv1.Cluster_AVAILABLE ||
			dc.GetState() == globaldedicatedv1.Cluster_PAUSED, nil
	case entity.DeployTypeServerless:
		c, err := svc.serverlessClusterSvc.GetCluster(ctx, clusterId, entity.ClusterViewClusterViewBasic)
		if err != nil {
			return false, bizerr.WrapError(err, "failed to get serverless cluster")
		}
		sc, err := c.GetServerlessCluster()
		if err != nil {
			return false, bizerr.WrapError(err, "failed to get serverless cluster")
		}
		return sc.GetUserStatus() == globalserverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_NORMAL, nil
	}
	return false, bizerr.New(bizerr.ErrCodeParams, "unsupported deploy type")
}

func (svc *service) createCustomStatus(
	ctx context.Context,
	exe *entity.MaintenanceTaskExecution,
	clusterId uint64,
) bizerr.BizError {
	status := &entity.ClusterCustomStatus{
		StatusId:      entity.GenStatusId(),
		StatusName:    fmt.Sprintf("maintenance_task_execution_%d", exe.GetMaintenanceTaskExecutionId()),
		ClusterId:     clusterId,
		StatusType:    tidbmgmtservicev1.CustomStatusType_CUSTOM_STATUS_TYPE_MAINTENANCE,
		OverrideState: openapicommonv1beta1.ClusterState_MAINTENANCE.Enum(),
		CustomMutex: &tidbmgmtservicev1.ClusterCustomStatus_CustomMutex{
			StatusType: []tidbmgmtservicev1.CustomStatusType{
				tidbmgmtservicev1.CustomStatusType_CUSTOM_STATUS_TYPE_IMPORT,
				tidbmgmtservicev1.CustomStatusType_CUSTOM_STATUS_TYPE_RESTORE,
				tidbmgmtservicev1.CustomStatusType_CUSTOM_STATUS_TYPE_APPLY,
				tidbmgmtservicev1.CustomStatusType_CUSTOM_STATUS_TYPE_MAINTENANCE,
			},
			ClusterAction: []tidbmgmtservicev1.ClusterAction{
				tidbmgmtservicev1.ClusterAction_CLUSTER_ACTION_MODIFY_CLUSTER,
				tidbmgmtservicev1.ClusterAction_CLUSTER_ACTION_PAUSE_CLUSTER,
				tidbmgmtservicev1.ClusterAction_CLUSTER_ACTION_SCALE_CLUSTER,
				tidbmgmtservicev1.ClusterAction_CLUSTER_ACTION_SET_ROOT_PASSWORD,
				tidbmgmtservicev1.ClusterAction_CLUSTER_ACTION_SET_TRAFFIC_FILTER,
				tidbmgmtservicev1.ClusterAction_CLUSTER_ACTION_CREATE_PRIVATE_LINK,
				tidbmgmtservicev1.ClusterAction_CLUSTER_ACTION_CREATE_VPC_PEERING,
				tidbmgmtservicev1.ClusterAction_CLUSTER_ACTION_DELETE_VPC_PEERING,
			},
		},
	}

	// check ClusterAction_CLUSTER_ACTION_DELETE_CLUSTER mutex
	isMutex, mutexedStatus, err := svc.customStatusRepo.IsMutexCustomStatus(ctx, status)
	if err != nil {
		return bizerr.WrapGormError(err, "failed to check mutex custom status")
	}

	if isMutex {
		tilog.Error(
			ctx,
			"mutex custom status is exist",
			zap.Uint64("cluster_id", clusterId),
			zap.Any("custom status", mutexedStatus),
		)
		return bizerr.New(bizerr.ErrCodePermission, "cluster is in %s", mutexedStatus.StatusName)
	}

	// Then set custom status
	if _, err = svc.customStatusRepo.NewClusterCustomStatus(ctx, status); err != nil {
		return bizerr.WrapGormError(err, "failed to insert custom status")
	}

	return nil
}

func (svc *service) deleteCustomStatus(
	ctx context.Context,
	exe *entity.MaintenanceTaskExecution,
	clusterId uint64,
) bizerr.BizError {
	// First get custom status
	status, err := svc.customStatusRepo.GetClusterCustomStatusByName(
		ctx,
		clusterId,
		fmt.Sprintf("maintenance_task_execution_%d", exe.GetMaintenanceTaskExecutionId()),
		tidbmgmtservicev1.CustomStatusType_CUSTOM_STATUS_TYPE_MAINTENANCE,
	)
	if err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return bizerr.WrapGormError(err, "failed to get custom status")
		}
		// no custom status found, return nil
		return nil
	}

	// Then delete custom status
	if err := svc.customStatusRepo.DeleteClusterCustomStatus(ctx, status.StatusId); err != nil {
		return bizerr.WrapGormError(err, "failed to delete custom status")
	}
	return nil
}
