package workflow

import (
	"context"
	"errors"
	"fmt"

	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/samber/lo"

	"github.com/tidbcloud/eks-provider/controllers/workflow/action"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
)

// Scheduler defines the interface for scheduling workflow steps.
type Scheduler interface {
	Schedule(ctx context.Context, wf *cloudv1.Workflow) (scheduled bool, _ error)
}

// sequenceScheduler schedules steps sequentially, one at a time.
type sequenceScheduler struct {
	registry *action.Registry
}

// NewSequenceScheduler creates a new sequence scheduler.
func NewSequenceScheduler(registry *action.Registry) *sequenceScheduler {
	return &sequenceScheduler{
		registry: registry,
	}
}

func (s *sequenceScheduler) Schedule(ctx context.Context, wf *cloudv1.Workflow) (scheduled bool, _ error) {
	switch wf.Status.Phase {
	case cloudv1.WorkflowPhaseRunning:
		return s.scheduleOnRunning(ctx, wf)
	case cloudv1.WorkflowPhaseCancelling:
		return s.scheduleOnCancelling(ctx, wf)
	default:
		log.FromContext(ctx).Info("workflow is not in running or cancelling phase, skip scheduling", "phase", wf.Status.Phase)
		return false, nil
	}
}

func (s *sequenceScheduler) scheduleOnRunning(ctx context.Context, wf *cloudv1.Workflow) (scheduled bool, _ error) {
	logger := log.FromContext(ctx).WithValues("scheduler", "sequence")

	if len(wf.Status.RunningSteps) != 0 || len(wf.Status.EndingSteps) != 0 {
		logger.Info("there are running or ending steps, skip scheduling",
			"runningSteps", wf.Status.RunningSteps, "endingSteps", wf.Status.EndingSteps)
		return false, nil
	}

	for _, stepStatus := range wf.Status.Steps {
		if stepStatus.Pending() {
			logger.Info("schedule a step to run", "step", stepStatus.Name)
			return true, wf.ScheduleStepToRun(ctx, stepStatus.Name)
		}
	}

	return false, nil
}

func (s *sequenceScheduler) scheduleOnCancelling(ctx context.Context, wf *cloudv1.Workflow) (scheduled bool, _ error) {
	logger := log.FromContext(ctx).WithValues("scheduler", "sequence")

	if len(wf.Status.CancellingSteps) != 0 || len(wf.Status.CancelEndingSteps) != 0 {
		logger.Info("there are cancelling or cancelEnding steps, skip scheduling",
			"cancellingSteps", wf.Status.CancellingSteps, "cancelEndingSteps", wf.Status.CancelEndingSteps,
		)
		return false, nil
	}

	// schedule a running or ending step to cancel or keep going
	if first, ok := lo.First(lo.Flatten([][]string{wf.Status.RunningSteps, wf.Status.EndingSteps})); ok {
		stepSpec := wf.MustStep(ctx, first)
		if finishedSteps := wf.CompletedStepsInGroup(ctx, stepSpec.StepGroup); len(finishedSteps) > 0 {
			logger.Info("there are finished steps in group, keep running", "step", first, "group", stepSpec.StepGroup, "finishedSteps", finishedSteps)
			return false, nil
		}

		stepStatus := wf.MustStepStatus(ctx, first)
		act, ok := s.registry.Actions[stepSpec.Uses]
		lo.Assertf(ok, "action(%s) not found in registry", stepSpec.Uses)

		actCancel, ok := act.(action.ActionCancel)
		if !ok {
			logger.Info("step's Action doesn't implement ActionCancel, keep going", "step", first, "action", stepSpec.Uses, "phase", stepStatus.Phase)
			return false, nil
		}

		inflated, err := inflateStepInputs(ctx, wf, stepSpec.Inputs.Object)
		if err != nil {
			return false, fmt.Errorf("inflate step(%s) inputs: %w", first, err)
		}
		stepSpec.Inputs.Object = inflated

		if err := actCancel.Cancel(ctx, wf, stepSpec, true); err != nil {
			var stepNotCancelableErr *action.ActionStepNotCancelableError
			if errors.As(err, &stepNotCancelableErr) {
				logger.Info("step's action implements ActionCancel but step not cancelable, keep going", "step", first, "action", stepSpec.Uses, "phase", stepStatus.Phase)
				return false, nil
			}
			return false, err
		}
		logger.Info("schedule to cancel", "step", first)
		return true, wf.ScheduleStepToCancel(ctx, first)
	}

	// schedule a pending step to cancel or run
	if first, ok := lo.First(wf.StepsByPhase(cloudv1.WorkflowStepPhasePending)); ok {
		stepSpec := wf.MustStep(ctx, first)
		if finishedSteps := wf.CompletedStepsInGroup(ctx, stepSpec.StepGroup); len(finishedSteps) > 0 {
			logger.Info("there are finished steps in group, schedule to run", "step", first, "group", stepSpec.StepGroup, "finishedSteps", finishedSteps)
			return true, wf.ScheduleStepToRun(ctx, stepSpec.Name)
		}

		return true, wf.ScheduleStepToCancel(ctx, first)
	}

	return false, nil
}
