package maintenancewindow

import (
	"context"
	"time"

	"google.golang.org/protobuf/types/known/timestamppb"

	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/tidb-mgmt-service/domain/repository"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/clusterevent"
)

func (svc *service) CreateMaintenanceTask(
	ctx context.Context,
	req *tidbmgmtservicev1.CreateMaintenanceTaskRequest,
) (*tidbmgmtservicev1.MaintenanceTask, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.CreateMaintenanceTaskSvc")
	defer span.End()

	task := &entity.MaintenanceTask{}

	err := svc.transaction.Transaction(ctx, func(ctx context.Context) error {
		if err := svc.checkCreateMaintenanceTask(ctx, req); err != nil {
			return err
		}

		requestTask := req.GetMaintenanceTask()
		maintenanceWindow, err := svc.mwRepo.LoadByProjectID(ctx, requestTask.GetProjectId())
		if err != nil {
			if err.RawErrCode() != bizerr.ErrCodeRecordNotFound {
				return err
			}

			utcOffset, err := svc.accountCli.GetTimeZoneByProjectID(ctx, requestTask.GetProjectId())
			if err != nil {
				return bizerr.WrapError(err, "get time zone by project id")
			}
			maintenanceWindowWithTimeZone := buildDefaultMaintenanceWindowWithTimeZone(
				requestTask.GetProjectId(),
				utcOffset,
			)
			maintenanceWindow = buildMaintenanceWindow(maintenanceWindowWithTimeZone)
		}

		task, err = svc.buildMaintenanceTask(req, maintenanceWindow)
		if err != nil {
			return err
		}

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

		executions := buildMaintenanceTaskExecutions(task.GetMaintenanceTaskId(), requestTask.GetClusterIds())
		for _, execution := range executions {
			if err := svc.mteRepo.Save(ctx, execution); err != nil {
				return err
			}
		}

		ev := newMaintenanceTaskEvent(ctx, clusterevent.MaintenanceWindowEventTypeTaskCreated, task)
		return svc.eventCommitter.Write(ctx, clusterevent.TopicMaintenanceWindowEvent, ev)
	})
	if err != nil {
		return nil, bizerr.WrapError(err, "create maintenance task")
	}

	return &task.MaintenanceTask, nil
}

// calculateScheduledApplyTime calculates the scheduled apply time based on maintenance window and deadline.
// Due to email notification policy, we need to notify users 14 days before scheduled apply time, if  14 days
// is not enough, fallback to 7 days.
// So preferred scheduled apply time is maintenance window's current apply time + 14 days.
// if deadline is not nil, scheduled apply time should be before deadline.
// if preferred scheduled apply time is after deadline, it should be maintenance window's current apply time + 7 days.
func (svc *service) calculateScheduledApplyTime(
	maintenanceWindow *entity.MaintenanceWindow,
	deadline *int64,
) (time.Time, bizerr.BizError) {
	now := time.Now()
	var currentApplyTime time.Time
	// if currentApplyTime is not nil(UT scenario), use it directly, otherwise calculate it from maintenance window
	if svc.currentApplyTime != nil {
		currentApplyTime = *svc.currentApplyTime
	} else {
		currentApplyTime = getCurrentApplyTime(now, maintenanceWindow)
	}

	scheduledApplyTime := currentApplyTime.AddDate(0, 0, 14)

	if deadline == nil {
		return scheduledApplyTime, nil
	}

	deadlineTime := time.Unix(*deadline, 0)

	if !scheduledApplyTime.After(deadlineTime) {
		return scheduledApplyTime, nil
	}

	scheduledApplyTime = currentApplyTime.AddDate(0, 0, 7)
	if scheduledApplyTime.After(deadlineTime) {
		return scheduledApplyTime, bizerr.New(bizerr.ErrCodeParams, "scheduled apply time is after deadline")
	}

	return scheduledApplyTime, nil
}

func (svc *service) buildMaintenanceTask(
	req *tidbmgmtservicev1.CreateMaintenanceTaskRequest,
	maintenanceWindow *entity.MaintenanceWindow,
) (*entity.MaintenanceTask, bizerr.BizError) {
	reqTask := req.GetMaintenanceTask()
	task := &entity.MaintenanceTask{
		MaintenanceTask: tidbmgmtservicev1.MaintenanceTask{
			ProjectId:          reqTask.GetProjectId(),
			ClusterIds:         reqTask.GetClusterIds(),
			Description:        reqTask.GetDescription(),
			State:              tidbmgmtservicev1.MaintenanceTask_PENDING,
			Deadline:           reqTask.GetDeadline(),
			ScheduledApplyTime: reqTask.GetScheduledApplyTime(),
		},
		NotificationPhase: entity.NSMaintenanceWindowNone,
	}

	if reqTask.GetScheduledApplyTime() == nil {
		scheduledApplyTime, err := svc.calculateScheduledApplyTime(maintenanceWindow, &reqTask.GetDeadline().Seconds)
		if err != nil {
			return nil, err
		}
		task.ScheduledApplyTime = timestamppb.New(scheduledApplyTime)
	}

	return task, nil
}

func buildMaintenanceTaskExecutions(taskID uint64, clusterIDs []uint64) []*entity.MaintenanceTaskExecution {
	var executions []*entity.MaintenanceTaskExecution
	for _, clusterID := range clusterIDs {
		execution := &entity.MaintenanceTaskExecution{
			MaintenanceTaskExecutionId: 0, // leave it empty
			MaintenanceTaskId:          taskID,
			ClusterId:                  clusterID,
			State:                      tidbmgmtservicev1.MaintenanceTaskExecution_PENDING,
			CancellationReason:         tidbmgmtservicev1.MaintenanceTaskExecution_REASON_UNSPECIFIED,
		}
		executions = append(executions, execution)
	}
	return executions
}

func (svc *service) checkCreateMaintenanceTask(
	ctx context.Context,
	req *tidbmgmtservicev1.CreateMaintenanceTaskRequest,
) bizerr.BizError {
	if req.GetMaintenanceTask() == nil {
		return bizerr.New(bizerr.ErrCodeParams, "maintenance task is required")
	}
	maintenanceTask := req.GetMaintenanceTask()
	if maintenanceTask.GetDescription() == "" {
		return bizerr.New(bizerr.ErrCodeParams, "description is required")
	}

	// Currently, there is no limit to the number of deferrals, and the deadline cannot be empty.
	if maintenanceTask.GetDeadline() == nil {
		return bizerr.New(bizerr.ErrCodeParams, "deadline is required")
	}

	if len(maintenanceTask.GetClusterIds()) == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "cluster ids is required")
	}
	if maintenanceTask.GetScheduledApplyTime() != nil {
		if err := svc.checkScheduledApplyTime(maintenanceTask); err != nil {
			return err
		}
	}

	filter := &repository.ClusterFilter{
		ClusterIds:   maintenanceTask.GetClusterIds(),
		QueryDeleted: false,
		DeployType:   entity.DeployTypeDedicated,
	}
	clusters, err := svc.clusterRepo.QueryClusterByFilterAndPage(ctx, filter, 0, 0)
	if err != nil {
		return bizerr.WrapGormError(err, "query active dedicated clusters")
	}
	clustersMap := make(map[uint64]struct{})
	for _, cluster := range clusters {
		clustersMap[cluster.ClusterId] = struct{}{}
	}

	for _, clusterID := range maintenanceTask.GetClusterIds() {
		if _, ok := clustersMap[clusterID]; !ok {
			return bizerr.New(bizerr.ErrCodeParams, "active dedicated cluster %d is not found", clusterID)
		}
	}

	return nil
}

func (svc *service) checkScheduledApplyTime(task *tidbmgmtservicev1.MaintenanceTask) bizerr.BizError {
	if task.GetScheduledApplyTime() == nil || task.GetDeadline() == nil {
		return bizerr.New(bizerr.ErrCodeParams, "scheduled apply time and deadline are required")
	}
	if task.GetScheduledApplyTime().AsTime().Before(time.Now()) {
		return bizerr.New(bizerr.ErrCodeParams, "scheduled apply time is before now")
	}
	if task.GetScheduledApplyTime().AsTime().After(task.GetDeadline().AsTime()) {
		return bizerr.New(bizerr.ErrCodeParams, "scheduled apply time is after deadline")
	}
	return nil
}

func getCurrentApplyTime(now time.Time, window *entity.MaintenanceWindow) time.Time {
	now = now.UTC()
	currentWeekday := int(now.Weekday())
	weekOffset := (int(window.WeekDay) - currentWeekday + 7) % 7

	nextWindow := now.Add(time.Hour * time.Duration(24*weekOffset))
	nextWindow = time.Date(
		nextWindow.Year(),
		nextWindow.Month(),
		nextWindow.Day(),
		int(window.DayHour),
		int(window.HourMinute),
		0,
		0,
		time.UTC,
	)

	if nextWindow.Before(now) {
		nextWindow = nextWindow.Add(time.Hour * time.Duration(24*7))
	}

	return nextWindow.UTC()
}
