package ssched

import (
	"strings"
	"time"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/ostate"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"go.uber.org/zap"
)

type svc struct {
	logger       mlogger.Logger
	stateMachine ostate.StateMachine
	now          func() time.Time
}

type preparedInput struct {
	stepsByRef       map[string]xplan.Step
	order            []string
	executionsByRef  map[string]*agg.StepExecution
	maxAttemptsByRef map[string]uint32
}

type gate int

const (
	gateReady gate = iota + 1
	gateWaiting
	gateImpossible
)

type stepOutcome int

const (
	outcomeUnknown stepOutcome = iota + 1
	outcomeSuccess
	outcomeFailure
	outcomeSkipped
)

func (s *svc) Schedule(in Input) (out *Output, err error) {
	logger := s.logger
	logger.Debug("Starting Schedule",
		zap.Int("steps_count", len(in.Steps)),
		zap.Int("step_executions_count", len(in.StepExecutions)),
		zap.Uint32("retry_max_attempts", in.Retry.MaxAttempts),
	)
	defer func(start time.Time) {
		fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
		if out != nil {
			fields = append(fields,
				zap.Int("runnable_count", len(out.Runnable)),
				zap.Int("blocked_count", len(out.Blocked)),
				zap.Int("skipped_count", len(out.Skipped)),
			)
		}
		if err != nil {
			logger.Warn("Failed to schedule", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Schedule", fields...)
	}(time.Now())

	prep, err := s.prepareInput(in)
	if err != nil {
		return nil, err
	}

	skipped := map[string]struct{}{}
	s.reconcileStates(prep, skipped)

	out = &Output{
		StepExecutions: make([]agg.StepExecution, 0, len(prep.order)),
	}
	for _, stepRef := range prep.order {
		step := prep.stepsByRef[stepRef]
		exec := prep.executionsByRef[stepRef]
		if exec == nil {
			return nil, merrors.InvalidArgument("execution is required for step_ref " + stepRef)
		}

		switch exec.State {
		case agg.StepStatePending:
			switch evaluateGate(step, prep.executionsByRef, prep.maxAttemptsByRef) {
			case gateReady:
				out.Runnable = append(out.Runnable, RunnableStep{
					StepRef:  exec.StepRef,
					StepCode: firstNonEmpty(exec.StepCode, step.StepCode),
					Attempt:  max(exec.Attempt, 1),
				})
			case gateWaiting:
				out.Blocked = append(out.Blocked, blockedStep(exec, step, BlockedWaitingDependencies))
			default:
				out.Blocked = append(out.Blocked, blockedStep(exec, step, BlockedDependencyMismatch))
			}

		case agg.StepStateFailed:
			maxAttempts := prep.maxAttemptsByRef[stepRef]
			if exec.Attempt >= maxAttempts {
				out.Blocked = append(out.Blocked, blockedStep(exec, step, BlockedRetryExhausted))
				break
			}
			switch evaluateGate(step, prep.executionsByRef, prep.maxAttemptsByRef) {
			case gateReady:
				out.Runnable = append(out.Runnable, RunnableStep{
					StepRef:  exec.StepRef,
					StepCode: firstNonEmpty(exec.StepCode, step.StepCode),
					Attempt:  exec.Attempt + 1,
				})
			case gateWaiting:
				out.Blocked = append(out.Blocked, blockedStep(exec, step, BlockedWaitingDependencies))
			default:
				out.Blocked = append(out.Blocked, blockedStep(exec, step, BlockedDependencyMismatch))
			}

		case agg.StepStateNeedsAttention:
			out.Blocked = append(out.Blocked, blockedStep(exec, step, BlockedNeedsAttention))

		case agg.StepStateRunning:
			out.Blocked = append(out.Blocked, blockedStep(exec, step, BlockedInProgress))
		}

		out.StepExecutions = append(out.StepExecutions, cloneStepExecution(*exec))
	}

	if len(skipped) > 0 {
		out.Skipped = make([]string, 0, len(skipped))
		for _, stepRef := range prep.order {
			if _, ok := skipped[stepRef]; ok {
				out.Skipped = append(out.Skipped, stepRef)
			}
		}
	}

	return out, nil
}

func (s *svc) reconcileStates(prep *preparedInput, skipped map[string]struct{}) {
	for pass := 0; pass < len(prep.order)+1; pass++ {
		changed := false
		for _, stepRef := range prep.order {
			step := prep.stepsByRef[stepRef]
			exec := prep.executionsByRef[stepRef]
			if exec == nil {
				continue
			}

			if s.promoteRetryExhaustedToNeedsAttention(exec, prep.maxAttemptsByRef[stepRef]) {
				changed = true
			}

			if s.skipImpossiblePending(step, exec, prep, skipped) {
				changed = true
			}
		}
		if !changed {
			return
		}
	}
}

func (s *svc) promoteRetryExhaustedToNeedsAttention(exec *agg.StepExecution, maxAttempts uint32) bool {
	if exec == nil || exec.State != agg.StepStateFailed {
		return false
	}
	if exec.Attempt < maxAttempts {
		return false
	}
	if err := s.stateMachine.EnsureStepTransition(exec.State, agg.StepStateNeedsAttention); err != nil {
		return false
	}
	exec.State = agg.StepStateNeedsAttention
	return true
}

func (s *svc) skipImpossiblePending(step xplan.Step, exec *agg.StepExecution, prep *preparedInput, skipped map[string]struct{}) bool {
	if exec == nil || exec.State != agg.StepStatePending {
		return false
	}
	if evaluateGate(step, prep.executionsByRef, prep.maxAttemptsByRef) != gateImpossible {
		return false
	}
	if err := s.stateMachine.EnsureStepTransition(exec.State, agg.StepStateSkipped); err != nil {
		return false
	}
	now := s.now().UTC()
	exec.State = agg.StepStateSkipped
	exec.FailureCode = ""
	exec.FailureMsg = ""
	exec.CompletedAt = &now
	skipped[exec.StepRef] = struct{}{}
	return true
}

func evaluateGate(step xplan.Step, executionsByRef map[string]*agg.StepExecution, maxAttemptsByRef map[string]uint32) gate {
	depOutcomes := make(map[string]stepOutcome, len(step.DependsOn))
	for _, dep := range step.DependsOn {
		depExec := executionsByRef[dep]
		if depExec == nil {
			return gateWaiting
		}
		outcome := outcomeForStep(depExec, maxAttemptsByRef[dep])
		if outcome == outcomeUnknown {
			return gateWaiting
		}
		depOutcomes[dep] = outcome
	}

	policy := normalizeCommitPolicy(step.CommitPolicy)
	switch policy {
	case model.CommitPolicyAfterSuccess:
		return evaluateAll(stepCommitTargets(step), executionsByRef, maxAttemptsByRef, outcomeSuccess)
	case model.CommitPolicyAfterFailure:
		return evaluateAll(stepCommitTargets(step), executionsByRef, maxAttemptsByRef, outcomeFailure)
	case model.CommitPolicyAfterCanceled:
		return evaluateTerminal(stepCommitTargets(step), executionsByRef, maxAttemptsByRef)
	default:
		for _, outcome := range depOutcomes {
			if outcome == outcomeFailure || outcome == outcomeSkipped {
				return gateImpossible
			}
		}
		return gateReady
	}
}

func evaluateAll(
	refs []string,
	executionsByRef map[string]*agg.StepExecution,
	maxAttemptsByRef map[string]uint32,
	want stepOutcome,
) gate {
	for _, ref := range refs {
		exec := executionsByRef[ref]
		if exec == nil {
			return gateWaiting
		}
		outcome := outcomeForStep(exec, maxAttemptsByRef[ref])
		if outcome == outcomeUnknown {
			return gateWaiting
		}
		if outcome != want {
			return gateImpossible
		}
	}
	return gateReady
}

func evaluateTerminal(refs []string, executionsByRef map[string]*agg.StepExecution, maxAttemptsByRef map[string]uint32) gate {
	for _, ref := range refs {
		exec := executionsByRef[ref]
		if exec == nil {
			return gateWaiting
		}
		if outcomeForStep(exec, maxAttemptsByRef[ref]) == outcomeUnknown {
			return gateWaiting
		}
	}
	return gateReady
}

func outcomeForStep(exec *agg.StepExecution, maxAttempts uint32) stepOutcome {
	if exec == nil {
		return outcomeUnknown
	}
	if maxAttempts == 0 {
		maxAttempts = 1
	}
	switch exec.State {
	case agg.StepStateCompleted:
		return outcomeSuccess
	case agg.StepStateSkipped:
		return outcomeSkipped
	case agg.StepStateNeedsAttention:
		return outcomeFailure
	case agg.StepStateFailed:
		if exec.Attempt < maxAttempts {
			return outcomeUnknown
		}
		return outcomeFailure
	default:
		return outcomeUnknown
	}
}

func stepCommitTargets(step xplan.Step) []string {
	if len(step.CommitAfter) > 0 {
		return step.CommitAfter
	}
	return step.DependsOn
}

func normalizeCommitPolicy(policy model.CommitPolicy) model.CommitPolicy {
	switch strings.ToUpper(strings.TrimSpace(string(policy))) {
	case string(model.CommitPolicyImmediate):
		return model.CommitPolicyImmediate
	case string(model.CommitPolicyAfterSuccess):
		return model.CommitPolicyAfterSuccess
	case string(model.CommitPolicyAfterFailure):
		return model.CommitPolicyAfterFailure
	case string(model.CommitPolicyAfterCanceled):
		return model.CommitPolicyAfterCanceled
	default:
		return model.CommitPolicyUnspecified
	}
}

func blockedStep(exec *agg.StepExecution, step xplan.Step, reason BlockedReason) BlockedStep {
	stepCode := step.StepCode
	if exec != nil && strings.TrimSpace(exec.StepCode) != "" {
		stepCode = exec.StepCode
	}
	return BlockedStep{
		StepRef:  step.StepRef,
		StepCode: strings.TrimSpace(stepCode),
		Reason:   reason,
	}
}
