package ssched

import (
	"strings"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/oshared"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
	svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/discovery"
	"github.com/tech/sendico/pkg/merrors"
)

func (s *svc) prepareInput(in Input) (*preparedInput, error) {
	if len(in.Steps) == 0 {
		return nil, merrors.InvalidArgument("steps are required")
	}

	stepsByRef := make(map[string]xplan.Step, len(in.Steps))
	order := make([]string, 0, len(in.Steps))

	for i := range in.Steps {
		step, err := normalizeGraphStep(in.Steps[i], i)
		if err != nil {
			return nil, err
		}
		if _, exists := stepsByRef[step.StepRef]; exists {
			return nil, merrors.InvalidArgument("steps[" + itoa(i) + "].step_ref must be unique")
		}
		stepsByRef[step.StepRef] = step
		order = append(order, step.StepRef)
	}

	for i := range order {
		step := stepsByRef[order[i]]
		for _, dep := range step.DependsOn {
			if _, ok := stepsByRef[dep]; !ok {
				return nil, merrors.InvalidArgument("step dependency is unknown: " + dep)
			}
		}
		for _, dep := range step.CommitAfter {
			if _, ok := stepsByRef[dep]; !ok {
				return nil, merrors.InvalidArgument("step commit_after dependency is unknown: " + dep)
			}
		}
	}

	maxAttemptsByRef := buildMaxAttemptsByRef(order, in.Retry)
	executionsByRef, err := s.normalizeStepExecutions(in.StepExecutions, stepsByRef, maxAttemptsByRef)
	if err != nil {
		return nil, err
	}
	seedMissingExecutions(order, stepsByRef, executionsByRef, maxAttemptsByRef)

	return &preparedInput{
		stepsByRef:       stepsByRef,
		order:            order,
		executionsByRef:  executionsByRef,
		maxAttemptsByRef: maxAttemptsByRef,
	}, nil
}

func normalizeGraphStep(step xplan.Step, index int) (xplan.Step, error) {
	step.StepRef = strings.TrimSpace(step.StepRef)
	step.StepCode = strings.TrimSpace(step.StepCode)
	step.Comment = strings.TrimSpace(step.Comment)
	if step.StepRef == "" {
		return xplan.Step{}, merrors.InvalidArgument("steps[" + itoa(index) + "].step_ref is required")
	}
	if step.StepCode == "" {
		step.StepCode = step.StepRef
	}
	step.DependsOn = normalizeRefList(step.DependsOn)
	step.CommitAfter = normalizeRefList(step.CommitAfter)
	return step, nil
}

func normalizeRefList(refs []string) []string {
	if len(refs) == 0 {
		return nil
	}
	out := make([]string, 0, len(refs))
	seen := make(map[string]struct{}, len(refs))
	for i := range refs {
		ref := strings.TrimSpace(refs[i])
		if ref == "" {
			continue
		}
		if _, ok := seen[ref]; ok {
			continue
		}
		seen[ref] = struct{}{}
		out = append(out, ref)
	}
	return out
}

func buildMaxAttemptsByRef(order []string, retry RetryPolicy) map[string]uint32 {
	defaultMax := retry.MaxAttempts
	if defaultMax == 0 {
		defaultMax = 1
	}
	out := make(map[string]uint32, len(order))
	for i := range order {
		out[order[i]] = defaultMax
	}
	for stepRef, maxAttempts := range retry.MaxAttemptsByStepRef {
		stepRef = strings.TrimSpace(stepRef)
		if stepRef == "" || maxAttempts == 0 {
			continue
		}
		out[stepRef] = maxAttempts
	}
	return out
}

func (s *svc) normalizeStepExecutions(
	steps []agg.StepExecution,
	stepsByRef map[string]xplan.Step,
	maxAttemptsByRef map[string]uint32,
) (map[string]*agg.StepExecution, error) {
	if len(steps) == 0 {
		return map[string]*agg.StepExecution{}, nil
	}

	out := make(map[string]*agg.StepExecution, len(steps))
	for i := range steps {
		exec, err := s.normalizeStepExecution(steps[i], i)
		if err != nil {
			return nil, err
		}
		stepRef := exec.StepRef
		if _, ok := stepsByRef[stepRef]; !ok {
			return nil, merrors.InvalidArgument("stepExecutions[" + itoa(i) + "].step_ref is unknown: " + stepRef)
		}
		if _, exists := out[stepRef]; exists {
			return nil, merrors.InvalidArgument("stepExecutions[" + itoa(i) + "].step_ref must be unique")
		}
		if exec.Attempt == 0 {
			exec.Attempt = 1
		}
		if maxAttemptsByRef[stepRef] == 0 {
			maxAttemptsByRef[stepRef] = 1
		}
		stepCode := strings.TrimSpace(exec.StepCode)
		if stepCode == "" {
			stepCode = stepsByRef[stepRef].StepCode
		}
		exec.StepCode = stepCode
		step := stepsByRef[stepRef]
		if exec.Rail == discovery.RailUnspecified {
			exec.Rail = step.Rail
		}
		if strings.TrimSpace(exec.Gateway) == "" {
			exec.Gateway = strings.TrimSpace(step.Gateway)
		}
		if strings.TrimSpace(exec.InstanceID) == "" {
			exec.InstanceID = strings.TrimSpace(step.InstanceID)
		}
		if strings.TrimSpace(exec.Comment) == "" {
			exec.Comment = strings.TrimSpace(step.Comment)
		}
		exec.ReportVisibility = effectiveStepVisibility(exec.ReportVisibility, stepsByRef[stepRef].Visibility)
		exec.UserLabel = firstNonEmpty(exec.UserLabel, stepsByRef[stepRef].UserLabel)
		cloned := cloneStepExecution(exec)
		out[stepRef] = &cloned
	}
	return out, nil
}

func (s *svc) normalizeStepExecution(exec agg.StepExecution, index int) (agg.StepExecution, error) {
	exec.StepRef = strings.TrimSpace(exec.StepRef)
	exec.StepCode = strings.TrimSpace(exec.StepCode)
	exec.Comment = strings.TrimSpace(exec.Comment)
	exec.FailureCode = strings.TrimSpace(exec.FailureCode)
	exec.FailureMsg = strings.TrimSpace(exec.FailureMsg)
	exec.UserLabel = strings.TrimSpace(exec.UserLabel)
	exec.Gateway = strings.TrimSpace(exec.Gateway)
	exec.InstanceID = strings.TrimSpace(exec.InstanceID)
	exec.Rail = model.ParseRail(string(exec.Rail))
	exec.ReportVisibility = model.NormalizeReportVisibility(exec.ReportVisibility)
	exec.ExternalRefs = oshared.CloneExternalRefs(exec.ExternalRefs)
	exec.ExecutedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.ExecutedMoney)
	exec.ConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.ConvertedMoney)
	exec.PlannedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.PlannedMoney)
	exec.PlannedConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.PlannedConvertedMoney)
	if exec.StepRef == "" {
		return agg.StepExecution{}, merrors.InvalidArgument("stepExecutions[" + itoa(index) + "].step_ref is required")
	}
	if !model.IsValidReportVisibility(exec.ReportVisibility) {
		return agg.StepExecution{}, merrors.InvalidArgument("stepExecutions[" + itoa(index) + "].report_visibility is invalid")
	}

	state, ok := normalizeStepState(exec.State)
	if !ok {
		return agg.StepExecution{}, merrors.InvalidArgument("stepExecutions[" + itoa(index) + "].state is invalid")
	}
	exec.State = state
	if err := s.stateMachine.EnsureStepTransition(exec.State, exec.State); err != nil {
		return agg.StepExecution{}, merrors.InvalidArgument("stepExecutions[" + itoa(index) + "].state is invalid")
	}
	return exec, nil
}

func seedMissingExecutions(
	order []string,
	stepsByRef map[string]xplan.Step,
	executionsByRef map[string]*agg.StepExecution,
	maxAttemptsByRef map[string]uint32,
) {
	for i := range order {
		stepRef := order[i]
		if _, ok := executionsByRef[stepRef]; ok {
			continue
		}
		step := stepsByRef[stepRef]
		attempt := uint32(1)
		if maxAttemptsByRef[stepRef] == 0 {
			maxAttemptsByRef[stepRef] = 1
		}
		executionsByRef[stepRef] = &agg.StepExecution{
			StepRef:               step.StepRef,
			StepCode:              step.StepCode,
			Rail:                  step.Rail,
			Gateway:               strings.TrimSpace(step.Gateway),
			InstanceID:            strings.TrimSpace(step.InstanceID),
			Comment:               strings.TrimSpace(step.Comment),
			ReportVisibility:      effectiveStepVisibility(model.ReportVisibilityUnspecified, step.Visibility),
			UserLabel:             strings.TrimSpace(step.UserLabel),
			State:                 agg.StepStatePending,
			Attempt:               attempt,
			PlannedMoney:          nil,
			PlannedConvertedMoney: nil,
		}
	}
}

func effectiveStepVisibility(execVisibility, graphVisibility model.ReportVisibility) model.ReportVisibility {
	execVisibility = model.NormalizeReportVisibility(execVisibility)
	graphVisibility = model.NormalizeReportVisibility(graphVisibility)
	if execVisibility != model.ReportVisibilityUnspecified {
		return execVisibility
	}
	return graphVisibility
}

func normalizeStepState(state agg.StepState) (agg.StepState, bool) {
	switch strings.ToLower(strings.TrimSpace(string(state))) {
	case "":
		return agg.StepStateUnspecified, true
	case string(agg.StepStateUnspecified):
		return agg.StepStateUnspecified, true
	case string(agg.StepStatePending):
		return agg.StepStatePending, true
	case string(agg.StepStateRunning):
		return agg.StepStateRunning, true
	case string(agg.StepStateCompleted):
		return agg.StepStateCompleted, true
	case string(agg.StepStateFailed):
		return agg.StepStateFailed, true
	case string(agg.StepStateNeedsAttention):
		return agg.StepStateNeedsAttention, true
	case string(agg.StepStateSkipped):
		return agg.StepStateSkipped, true
	default:
		return agg.StepStateUnspecified, false
	}
}

func cloneStepExecution(exec agg.StepExecution) agg.StepExecution {
	out := exec
	out.ExternalRefs = oshared.CloneExternalRefs(exec.ExternalRefs)
	out.ExecutedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.ExecutedMoney)
	out.ConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.ConvertedMoney)
	out.PlannedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.PlannedMoney)
	out.PlannedConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.PlannedConvertedMoney)
	return out
}

func firstNonEmpty(values ...string) string {
	for i := range values {
		val := strings.TrimSpace(values[i])
		if val != "" {
			return val
		}
	}
	return ""
}

func max(left, right uint32) uint32 {
	if left > right {
		return left
	}
	return right
}

func itoa(v int) string {
	if v == 0 {
		return "0"
	}
	var buf [20]byte
	i := len(buf)
	for v > 0 {
		i--
		buf[i] = byte('0' + v%10)
		v /= 10
	}
	return string(buf[i:])
}
