package orchestrator

import (
	"context"
	"errors"

	"github.com/tech/sendico/payments/orchestrator/storage"
	"github.com/tech/sendico/payments/orchestrator/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"go.uber.org/zap"
)

func analyzeExecutionPlan(plan *model.ExecutionPlan) (done bool, failed bool, rootErr error) {
	if plan == nil || len(plan.Steps) == 0 {
		return true, false, nil
	}

	done = true
	for _, s := range plan.Steps {
		if s == nil {
			continue
		}

		if s.State == model.OperationStateFailed {
			failed = true
			if rootErr == nil && s.Error != "" {
				rootErr = errors.New(s.Error)
			}
		}

		if !isStepFinal(s) { // created/waiting/processing
			done = false
		}
	}
	return done, failed, rootErr
}

func buildStepIndex(plan *model.PaymentPlan) map[string]int {
	m := make(map[string]int, len(plan.Steps))
	for i, s := range plan.Steps {
		if s == nil {
			continue
		}
		m[s.StepID] = i
	}
	return m
}

func isPlanComplete(payment *model.Payment) bool {
	if (payment.State == model.PaymentStateCancelled) ||
		(payment.State == model.PaymentStateSettled) ||
		(payment.State == model.PaymentStateFailed) {
		return true
	}
	return false
}

func isStepFinal(step *model.ExecutionStep) bool {
	if (step.State == model.OperationStateFailed) || (step.State == model.OperationStateSuccess) || (step.State == model.OperationStateCancelled) {
		return true
	}
	return false
}

func stepCodeIsDependent(code string, previousSteps []string) bool {
	for _, ps := range previousSteps {
		if ps == code {
			return true
		}
	}
	return false
}

func stepIsIndependent(
	step *model.ExecutionStep,
	plan *model.PaymentPlan,
	execSteps map[string]*model.ExecutionStep,
) bool {

	for _, s := range plan.Steps {
		if s.StepID != step.Code {
			continue
		}

		// Do not process step if it is already in a final state
		if isStepFinal(step) {
			return false
		}

		// If the step has no dependencies, it is independent
		if len(s.DependsOn) == 0 {
			return true
		}

		// All dependent steps must be successfully completed
		for _, dep := range s.DependsOn {
			depStep := execSteps[dep]
			if depStep == nil || depStep.State != model.OperationStateSuccess {
				return false
			}
		}

		return true
	}

	return false
}

func planStep(execStep *model.ExecutionStep, plan *model.PaymentPlan) *model.PaymentStep {
	if (execStep == nil) || (plan == nil) {
		return nil
	}
	for _, step := range plan.Steps {
		if step != nil {
			if step.StepID == execStep.Code {
				return step
			}
		}
	}
	return nil
}

func (p *paymentExecutor) pickIndependentSteps(
	ctx context.Context,
	l *zap.Logger,
	store storage.PaymentsStore,
	waiting []*model.ExecutionStep,
	payment *model.Payment,
	quote *orchestratorv1.PaymentQuote,
) error {

	logger := l.With(zap.Int("waiting_steps", len(waiting)))
	logger.Debug("Selecting independent steps for execution")

	execSteps := executionStepsByCode(payment.ExecutionPlan)
	planSteps := planStepsByID(payment.PaymentPlan)
	execQuote := executionQuote(payment, quote)
	charges := ledgerChargesFromFeeLines(execQuote.GetFeeLines())
	stepIdx := buildStepIndex(payment.PaymentPlan)

	for _, execStep := range waiting {
		if execStep == nil {
			continue
		}

		lg := logger.With(
			zap.String("step_code", execStep.Code),
			zap.String("step_state", string(execStep.State)),
		)

		planStep := planSteps[execStep.Code]
		if planStep == nil {
			lg.Warn("Plan step not found")
			continue
		}

		ready, waitingDep, blocked, err :=
			stepDependenciesReady(planStep, execSteps, planSteps, true)

		if err != nil {
			lg.Warn("Dependency evaluation failed", zap.Error(err))
			continue
		}

		if blocked {
			lg.Debug("Step permanently blocked by dependency failure")
			continue
		}

		if waitingDep {
			lg.Debug("Step waiting for dependencies")
			continue
		}

		if !ready {
			continue
		}

		lg.Debug("Executing independent step")
		idx := stepIdx[execStep.Code]

		async, err := p.executePlanStep(
			ctx,
			payment,
			planStep,
			execStep,
			quote,
			charges,
			idx,
		)
		if err != nil {
			lg.Warn("Step execution failed", zap.Error(err), zap.Bool("async", async))
			return err
		}
	}

	return nil
}

func (p *paymentExecutor) pickWaitingSteps(
	ctx context.Context,
	l *zap.Logger,
	store storage.PaymentsStore,
	payment *model.Payment,
	quote *orchestratorv1.PaymentQuote,
) error {
	if payment == nil || payment.ExecutionPlan == nil {
		l.Debug("No execution plan")
		return nil
	}

	logger := l.With(zap.Int("total_steps", len(payment.ExecutionPlan.Steps)))
	logger.Debug("Collecting waiting steps")

	waitingSteps := make([]*model.ExecutionStep, 0, len(payment.ExecutionPlan.Steps))
	for _, step := range payment.ExecutionPlan.Steps {
		if step == nil {
			continue
		}
		if step.State != model.OperationStatePlanned {
			continue
		}
		waitingSteps = append(waitingSteps, step)
	}

	if len(waitingSteps) == 0 {
		logger.Debug("No waiting steps to process")
		return nil
	}

	return p.pickIndependentSteps(ctx, logger, store, waitingSteps, payment, quote)
}

func (p *paymentExecutor) executePaymentPlan(
	ctx context.Context,
	store storage.PaymentsStore,
	payment *model.Payment,
	quote *orchestratorv1.PaymentQuote,
) error {

	if payment == nil {
		return merrors.InvalidArgument("plan must be provided")
	}

	logger := p.logger.With(zap.String("payment_ref", payment.PaymentRef))
	logger.Debug("Starting plan execution")

	if isPlanComplete(payment) {
		logger.Debug("Plan already completed")
		return nil
	}

	if payment.ExecutionPlan == nil {
		logger.Debug("Initializing execution plan from payment plan")
		payment.ExecutionPlan = ensureExecutionPlanForPlan(payment, payment.PaymentPlan)
		if err := store.Update(ctx, payment); err != nil {
			return err
		}
	}

	// Execute steps
	if err := p.pickWaitingSteps(ctx, logger, store, payment, quote); err != nil {
		logger.Warn("Step execution returned infrastructure error", zap.Error(err))
		return err
	}

	if err := store.Update(ctx, payment); err != nil {
		return err
	}

	done, failed, rootErr := analyzeExecutionPlan(payment.ExecutionPlan)
	if !done {
		return nil
	}

	if failed {
		payment.State = model.PaymentStateFailed
	} else {
		payment.State = model.PaymentStateSettled
	}

	if err := store.Update(ctx, payment); err != nil {
		logger.Warn("Failed to update final payment state", zap.Error(err))
		return err
	}

	if failed && rootErr != nil {
		return rootErr
	}
	return nil
}
