package maintenancewindow

import (
	"context"
	"time"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/emptypb"

	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) SyncMaintenanceWindow(
	ctx context.Context,
	req *tidbmgmtservicev1.SyncMaintenanceWindowRequest,
) (*emptypb.Empty, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "SyncMaintenanceWindowSvc")
	defer func() {
		span.End()
	}()

	logger := tilog.WithContextS(ctx)

	if err := svc.applyTasksToExecute(ctx); err != nil {
		logger.Error("failed to apply tasks to execute", zap.Error(err))
	}

	return &emptypb.Empty{}, nil
}

// tasksToNotifyInSpan returns pending tasks that will be notified in the span of [startTime, endTime)
func (svc *service) tasksInSpan(
	ctx context.Context,
	startTime, endTime *time.Time,
) (map[uint64][]*entity.MaintenanceTask, bizerr.BizError) {
	maintenanceTaskFilter := &tidbmgmtservicev1.MaintenanceTaskFilter{
		States: []tidbmgmtservicev1.MaintenanceTask_State{tidbmgmtservicev1.MaintenanceTask_PENDING},
	}
	tasks, _, err := svc.mtRepo.Query(
		ctx,
		maintenanceTaskFilter,
		startTime,
		endTime,
		nil,
		nil,
		nil,
		nil,
	)
	if err != nil {
		return nil, err
	}

	// group tasks by project id
	projectID2TasksMap := map[uint64][]*entity.MaintenanceTask{}
	for _, task := range tasks {
		projectID2TasksMap[task.GetProjectId()] = append(projectID2TasksMap[task.GetProjectId()], task)
	}

	return projectID2TasksMap, nil
}

func (svc *service) applyTasksToExecute(ctx context.Context) bizerr.BizError {
	logger := tilog.WithContextS(ctx)
	now := time.Now()

	// query tasks that are pending and scheduled to be applied immediately
	projectID2TasksMap, err := svc.tasksInSpan(ctx, nil, &now)
	if err != nil {
		return err
	}

	for projectID, tasks := range projectID2TasksMap {
		if err := svc.transaction.Transaction(ctx, func(ctx context.Context) error {
			return svc.startMaintenanceWindow(ctx, tasks)
		}); err != nil {
			logger.Error(
				"failed to notify maintenance window started",
				zap.Uint64("projectID", projectID),
				zap.Error(err),
			)
			continue
		}
	}
	return nil
}

func (svc *service) startMaintenanceWindow(ctx context.Context, tasks []*entity.MaintenanceTask) bizerr.BizError {
	ctx, span := tracer.Start(ctx, "Domain.StartMaintenanceWindow")
	defer span.End()

	tasks, err := svc.filterCanceledTasks(ctx, tasks)
	if err != nil {
		return err
	}

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

	// update notification phase and status
	for _, task := range tasks {
		ev := newMaintenanceTaskEvent(ctx, clusterevent.MaintenanceWindowEventTypeTaskRunning, task)
		if err := svc.eventCommitter.Write(ctx, clusterevent.TopicMaintenanceWindowEvent, ev); err != nil {
			return err
		}

		task.State = tidbmgmtservicev1.MaintenanceTask_RUNNING
		task.NotificationPhase = entity.NSMaintenanceWindowStarted
		if err := svc.mtRepo.Save(ctx, task); err != nil {
			return err
		}
	}

	return nil
}

func (svc *service) filterCanceledTasks(
	ctx context.Context,
	tasks []*entity.MaintenanceTask,
) ([]*entity.MaintenanceTask, bizerr.BizError) {
	var out []*entity.MaintenanceTask
	for _, task := range tasks {
		filter := &tidbmgmtservicev1.QueryMaintenanceTaskExecutionsRequest_Filter{
			MaintenanceTaskIds: []uint64{task.GetMaintenanceTaskId()},
			States: []tidbmgmtservicev1.MaintenanceTaskExecution_State{
				tidbmgmtservicev1.MaintenanceTaskExecution_PENDING,
			},
		}
		records, err := svc.mteRepo.Query(ctx, filter, nil)
		if err != nil {
			return nil, err
		}
		if len(records) > 0 {
			out = append(out, task)
			continue
		}

		task.State = tidbmgmtservicev1.MaintenanceTask_DONE
		if err := svc.mtRepo.Save(ctx, task); err != nil {
			return nil, err
		}
	}
	tilog.Info(ctx, "filterCanceledTasks result", zap.Int("len", len(out)))
	return out, nil
}

func newMaintenanceTaskEvent(
	ctx context.Context,
	eventType clusterevent.MaintenanceWindowEventType,
	task *entity.MaintenanceTask,
) *clusterevent.MaintenanceWindowEvent {
	return &clusterevent.MaintenanceWindowEvent{
		ProjectID:        task.GetProjectId(),
		TraceID:          clusterevent.TraceIDFromContext(ctx),
		EventType:        eventType,
		EventCreatedAtMs: 0, // fill by pub sub implementation
		MaintenanceTask: &clusterevent.MaintenanceTask{
			ID:                 task.GetMaintenanceTaskId(),
			Status:             task.GetState().String(),
			ScheduledApplyTime: task.ScheduledApplyTime.AsTime().Unix(),
			ClusterIDs:         task.GetClusterIds(),
		},
	}
}
