package orchestrator

import (
	"context"
	"strings"

	"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"
)

func (p *paymentExecutor) executePaymentPlan(ctx context.Context, store storage.PaymentsStore, payment *model.Payment, quote *orchestratorv1.PaymentQuote) error {
	if store == nil {
		return errStorageUnavailable
	}
	if payment == nil {
		return merrors.InvalidArgument("payment plan: payment is required")
	}
	plan := payment.PaymentPlan
	if plan == nil || len(plan.Steps) == 0 {
		return merrors.InvalidArgument("payment plan: steps are required")
	}

	execQuote := executionQuote(payment, quote)
	charges := ledgerChargesFromFeeLines(execQuote.GetFeeLines())

	order, _, err := planExecutionOrder(plan)
	if err != nil {
		return p.failPayment(ctx, store, payment, model.PaymentFailureCodePolicy, strings.TrimSpace(err.Error()), err)
	}

	execPlan := ensureExecutionPlanForPlan(payment, plan)
	execSteps := executionStepsByCode(execPlan)
	planSteps := planStepsByID(plan)
	asyncSubmitted := false

	for _, idx := range order {
		step := plan.Steps[idx]
		if step == nil {
			return p.failPayment(ctx, store, payment, model.PaymentFailureCodePolicy, "payment plan: step is required", merrors.InvalidArgument("payment plan: step is required"))
		}
		stepID := planStepID(step, idx)
		execStep := execSteps[stepID]
		if execStep == nil {
			execStep = &model.ExecutionStep{Code: stepID}
			execSteps[stepID] = execStep
		}
		if step.Action == model.RailOperationRelease {
			setExecutionStepStatus(execStep, executionStepStatusSkipped)
			continue
		}
		status := executionStepStatus(execStep)
		switch status {
		case executionStepStatusConfirmed, executionStepStatusSkipped:
			continue
		case executionStepStatusFailed:
			payment.State = model.PaymentStateFailed
			payment.FailureCode = failureCodeForStep(step)
			return p.persistPayment(ctx, store, payment)
		case executionStepStatusCancelled:
			payment.State = model.PaymentStateCancelled
			payment.FailureCode = model.PaymentFailureCodePolicy
			return p.persistPayment(ctx, store, payment)
		case executionStepStatusSubmitted:
			asyncSubmitted = true
			continue
		}

		ready, blocked, err := stepDependenciesReady(step, execSteps, planSteps, false)
		if err != nil {
			return p.failPayment(ctx, store, payment, model.PaymentFailureCodePolicy, strings.TrimSpace(err.Error()), err)
		}
		if blocked {
			payment.State = model.PaymentStateFailed
			payment.FailureCode = failureCodeForStep(step)
			return p.persistPayment(ctx, store, payment)
		}
		if !ready {
			continue
		}

		async, err := p.executePlanStep(ctx, payment, step, execStep, execQuote, charges, idx)
		if err != nil {
			return p.failPayment(ctx, store, payment, failureCodeForStep(step), strings.TrimSpace(err.Error()), err)
		}
		if async {
			asyncSubmitted = true
		}
	}

	if asyncSubmitted && !executionPlanComplete(execPlan) {
		if blockStepConfirmed(plan, execPlan) {
			payment.State = model.PaymentStateFundsReserved
		} else {
			payment.State = model.PaymentStateSubmitted
		}
		return p.persistPayment(ctx, store, payment)
	}
	payment.State = model.PaymentStateSettled
	payment.FailureCode = model.PaymentFailureCodeUnspecified
	payment.FailureReason = ""
	return p.persistPayment(ctx, store, payment)
}
