package xplan

import (
	"github.com/tech/sendico/pkg/discovery"
	"strings"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/batchmeta"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
)

func (s *svc) expandSingleHop(ex *expansion, hop normalizedHop, intent model.PaymentIntent) error {
	if isExternalRail(hop.rail) {
		_, err := s.ensureExternalObserved(ex, hop, intent)
		return err
	}

	switch hop.role {
	case paymenttypes.QuoteRouteHopRoleSource:
		appendMainWithOptionalFee(ex, Step{
			StepCode:   singleHopCode(hop, "debit"),
			Kind:       StepKindFundsDebit,
			Action:     discovery.RailOperationDebit,
			Rail:       hop.rail,
			HopIndex:   hop.index,
			HopRole:    hop.role,
			Visibility: model.ReportVisibilityHidden,
		}, hop)
	case paymenttypes.QuoteRouteHopRoleDestination:
		ex.appendMain(Step{
			StepCode:   singleHopCode(hop, "credit"),
			Kind:       StepKindFundsCredit,
			Action:     discovery.RailOperationCredit,
			Rail:       hop.rail,
			HopIndex:   hop.index,
			HopRole:    hop.role,
			Visibility: model.ReportVisibilityHidden,
		})
	default:
		ex.appendMain(Step{
			StepCode:   singleHopCode(hop, "move"),
			Kind:       StepKindFundsMove,
			Action:     discovery.RailOperationMove,
			Rail:       hop.rail,
			HopIndex:   hop.index,
			HopRole:    hop.role,
			Visibility: model.ReportVisibilityHidden,
		})
	}
	return nil
}

func (s *svc) applyDefaultBoundary(
	ex *expansion,
	from normalizedHop,
	to normalizedHop,
	intent model.PaymentIntent,
) error {
	switch {
	case isExternalRail(from.rail) && isInternalRail(to.rail):
		if _, err := s.ensureExternalObserved(ex, from, intent); err != nil {
			return err
		}
		ex.appendMain(makeFundsCreditStep(from, to, internalRailForBoundary(from, to)))
		return nil

	case isInternalRail(from.rail) && isExternalRail(to.rail):
		internalRail := internalRailForBoundary(from, to)
		targets, err := payoutTargetsFromIntent(intent)
		if err != nil {
			return err
		}
		if to.rail == discovery.RailCardPayout && len(targets) > 0 {
			return s.applyBatchCardPayoutBoundary(ex, from, to, internalRail, intent, targets)
		}
		appendMainWithOptionalFee(ex, makeFundsBlockStep(from, to, internalRail), from)
		observeRef, err := s.ensureExternalObserved(ex, to, intent)
		if err != nil {
			return err
		}
		appendSettlementBranches(ex, from, to, internalRail, observeRef)
		return nil

	case isExternalRail(from.rail) && isExternalRail(to.rail):
		if _, err := s.ensureExternalObserved(ex, from, intent); err != nil {
			return err
		}
		internalRail := internalRailForBoundary(from, to)
		ex.appendMain(makeFundsCreditStep(from, to, internalRail))
		ex.appendMain(makeFundsBlockStep(from, to, internalRail))
		observeRef, err := s.ensureExternalObserved(ex, to, intent)
		if err != nil {
			return err
		}
		appendSettlementBranches(ex, from, to, internalRail, observeRef)
		return nil

	case isInternalRail(from.rail) && isInternalRail(to.rail):
		appendMainWithOptionalFee(ex, makeFundsMoveStep(from, to, internalRailForBoundary(from, to)), from)
		return nil

	default:
		return merrors.InvalidArgument("unsupported rail boundary")
	}
}

func (s *svc) applyBatchCardPayoutBoundary(
	ex *expansion,
	from normalizedHop,
	to normalizedHop,
	internalRail model.Rail,
	intent model.PaymentIntent,
	targets []batchmeta.PayoutTarget,
) error {
	blockRef := appendMainWithOptionalFee(ex, makeFundsBlockStep(from, to, internalRail), from)
	for i := range targets {
		target := targets[i]
		if target.Amount == nil {
			return merrors.InvalidArgument("intent_snapshot.attributes[" + batchmeta.AttrPayoutTargets + "]: amount is required")
		}
		if len(targets) > 1 && target.Card == nil {
			return merrors.InvalidArgument("intent_snapshot.attributes[" + batchmeta.AttrPayoutTargets + "]: card is required for each target")
		}
		targetMeta := batchmeta.StepMetadataForTarget(target, i)
		targetComment := strings.TrimSpace(target.Comment)

		sendStep := makeRailSendStep(to, intent)
		sendStep.DependsOn = []string{blockRef}
		sendStep.Comment = targetComment
		sendStep.Metadata = cloneMetadata(targetMeta)
		sendRef := ex.appendBranch(sendStep)

		observeStep := makeRailObserveStep(to, intent)
		observeStep.DependsOn = []string{sendRef}
		observeStep.Comment = targetComment
		observeStep.Metadata = cloneMetadata(targetMeta)
		observeRef := ex.appendBranch(observeStep)

		appendSettlementBranchesWithMetadata(ex, from, to, internalRail, sendRef, observeRef, targetMeta)
	}
	return nil
}

func (s *svc) ensureExternalObserved(ex *expansion, hop normalizedHop, intent model.PaymentIntent) (string, error) {
	key := observedKey(hop)
	if ref := strings.TrimSpace(ex.externalObserved[key]); ref != "" {
		return ref, nil
	}

	sendStep := makeRailSendStep(hop, intent)
	sendRef := ex.appendMain(sendStep)
	observeDependency := sendRef
	if ex.needsFeeStep(hop) {
		feeStep := makeRailFeeStep(hop)
		if observeDependency != "" {
			feeStep.DependsOn = []string{observeDependency}
		}
		observeDependency = ex.appendMain(feeStep)
		ex.markFeeEmitted(hop)
	}

	observeStep := makeRailObserveStep(hop, intent)
	if observeDependency != "" {
		observeStep.DependsOn = []string{observeDependency}
	}
	observeRef := ex.appendMain(observeStep)

	ex.externalObserved[key] = observeRef
	return observeRef, nil
}

func appendMainWithOptionalFee(ex *expansion, step Step, hop normalizedHop) string {
	mainRef := ex.appendMain(step)
	if !ex.needsFeeStep(hop) {
		return mainRef
	}
	feeStep := makeRailFeeStep(hop)
	if strings.TrimSpace(mainRef) != "" {
		feeStep.DependsOn = []string{mainRef}
	}
	feeRef := ex.appendMain(feeStep)
	ex.markFeeEmitted(hop)
	return feeRef
}

func makeRailFeeStep(hop normalizedHop) Step {
	return Step{
		StepCode:   singleHopCode(hop, "fee"),
		Kind:       StepKindRailSend,
		Action:     discovery.RailOperationFee,
		Rail:       hop.rail,
		Gateway:    hop.gateway,
		InstanceID: hop.instanceID,
		HopIndex:   hop.index,
		HopRole:    hop.role,
		Visibility: model.ReportVisibilityHidden,
	}
}

func appendGuards(ex *expansion, conditions *paymenttypes.QuoteExecutionConditions) {
	if conditions == nil {
		return
	}

	if conditions.LiquidityCheckRequiredAtExecution {
		ex.appendMain(Step{
			StepRef:    QuoteReadinessGuardStepRef,
			StepCode:   string(GuardOperationQuoteReadinessGuard),
			Kind:       StepKindLiquidityCheck,
			Action:     discovery.RailOperationUnspecified,
			Rail:       discovery.RailUnspecified,
			Visibility: model.ReportVisibilityHidden,
		})
	}

	if conditions.PrefundingRequired {
		ex.appendMain(Step{
			StepRef:    PrefundingGuardStepRef,
			StepCode:   string(GuardOperationPrefundingEnsure),
			Kind:       StepKindPrefunding,
			Action:     discovery.RailOperationUnspecified,
			Rail:       discovery.RailUnspecified,
			Visibility: model.ReportVisibilityHidden,
		})
	}
}

func makeRailSendStep(hop normalizedHop, intent model.PaymentIntent) Step {
	action, op := railActionForHop(hop)
	visibility := defaultVisibilityForAction(action, hop.role)
	userLabel := ""
	if visibility == model.ReportVisibilityUser {
		userLabel = defaultUserLabel(action, hop.rail, hop.role, intent.Kind)
	}
	return Step{
		StepCode:   singleHopCode(hop, op),
		Kind:       kindForAction(action),
		Action:     action,
		Rail:       hop.rail,
		Gateway:    hop.gateway,
		InstanceID: hop.instanceID,
		HopIndex:   hop.index,
		HopRole:    hop.role,
		Visibility: visibility,
		UserLabel:  userLabel,
	}
}

func railActionForHop(hop normalizedHop) (model.RailOperation, string) {
	if hop.rail == discovery.RailProviderSettlement {
		return discovery.RailOperationFXConvert, "fx_convert"
	}
	return discovery.RailOperationSend, "send"
}

func makeRailObserveStep(hop normalizedHop, intent model.PaymentIntent) Step {
	visibility := defaultVisibilityForAction(discovery.RailOperationObserveConfirm, hop.role)
	userLabel := ""
	if visibility == model.ReportVisibilityUser {
		userLabel = defaultUserLabel(discovery.RailOperationObserveConfirm, hop.rail, hop.role, intent.Kind)
	}
	return Step{
		StepCode:   singleHopCode(hop, "observe"),
		Kind:       StepKindRailObserve,
		Action:     discovery.RailOperationObserveConfirm,
		Rail:       hop.rail,
		Gateway:    hop.gateway,
		InstanceID: hop.instanceID,
		HopIndex:   hop.index,
		HopRole:    hop.role,
		Visibility: visibility,
		UserLabel:  userLabel,
	}
}

func makeFundsCreditStep(from normalizedHop, to normalizedHop, rail model.Rail) Step {
	return Step{
		StepCode:   edgeCode(from, to, rail, "credit"),
		Kind:       StepKindFundsCredit,
		Action:     discovery.RailOperationExternalCredit,
		Rail:       rail,
		HopIndex:   to.index,
		HopRole:    paymenttypes.QuoteRouteHopRoleTransit,
		Visibility: model.ReportVisibilityHidden,
	}
}

func makeFundsBlockStep(from normalizedHop, to normalizedHop, rail model.Rail) Step {
	return Step{
		StepCode:   edgeCode(from, to, rail, "block"),
		Kind:       StepKindFundsBlock,
		Action:     discovery.RailOperationBlock,
		Rail:       rail,
		HopIndex:   to.index,
		HopRole:    paymenttypes.QuoteRouteHopRoleTransit,
		Visibility: model.ReportVisibilityHidden,
	}
}

func makeFundsMoveStep(from normalizedHop, to normalizedHop, rail model.Rail) Step {
	return Step{
		StepCode:   edgeCode(from, to, rail, "move"),
		Kind:       StepKindFundsMove,
		Action:     discovery.RailOperationMove,
		Rail:       rail,
		HopIndex:   to.index,
		HopRole:    paymenttypes.QuoteRouteHopRoleTransit,
		Visibility: model.ReportVisibilityHidden,
	}
}

func appendSettlementBranches(
	ex *expansion,
	from normalizedHop,
	to normalizedHop,
	rail model.Rail,
	anchorObserveRef string,
) {
	if strings.TrimSpace(anchorObserveRef) == "" {
		return
	}
	anchorSendRef := strings.TrimSpace(anchorObserveRef)
	if anchorSendRef != "" {
		for i := range ex.steps {
			step := ex.steps[i]
			if strings.TrimSpace(step.StepRef) != anchorObserveRef {
				continue
			}
			if len(step.DependsOn) > 0 {
				anchorSendRef = strings.TrimSpace(step.DependsOn[0])
			} else {
				anchorSendRef = ""
			}
			break
		}
	}
	appendSettlementBranchesWithMetadata(ex, from, to, rail, anchorSendRef, anchorObserveRef, nil)
}

func appendSettlementBranchesWithMetadata(
	ex *expansion,
	from normalizedHop,
	to normalizedHop,
	rail model.Rail,
	anchorSendRef string,
	anchorObserveRef string,
	metadata map[string]string,
) {
	if strings.TrimSpace(anchorObserveRef) == "" {
		return
	}
	successStep := Step{
		StepCode:     edgeCode(from, to, rail, "debit"),
		Kind:         StepKindFundsDebit,
		Action:       discovery.RailOperationExternalDebit,
		DependsOn:    []string{anchorObserveRef},
		Rail:         rail,
		HopIndex:     to.index,
		HopRole:      paymenttypes.QuoteRouteHopRoleTransit,
		Visibility:   model.ReportVisibilityHidden,
		CommitPolicy: model.CommitPolicyAfterSuccess,
		CommitAfter:  []string{anchorObserveRef},
		Metadata:     mergeStepMetadata(metadata, map[string]string{"mode": "finalize_debit"}),
	}
	ex.appendBranch(successStep)

	if anchorSendRef != "" {
		sendFailureStep := Step{
			StepCode:     edgeCode(from, to, rail, "release"),
			Kind:         StepKindFundsRelease,
			Action:       discovery.RailOperationRelease,
			DependsOn:    []string{anchorSendRef},
			Rail:         rail,
			HopIndex:     to.index,
			HopRole:      paymenttypes.QuoteRouteHopRoleTransit,
			Visibility:   model.ReportVisibilityHidden,
			CommitPolicy: model.CommitPolicyAfterFailure,
			CommitAfter:  []string{anchorSendRef},
			Metadata:     mergeStepMetadata(metadata, map[string]string{"mode": "unlock_hold"}),
		}
		ex.appendBranch(sendFailureStep)
	}

	failureStep := Step{
		StepCode:     edgeCode(from, to, rail, "release"),
		Kind:         StepKindFundsRelease,
		Action:       discovery.RailOperationRelease,
		DependsOn:    []string{anchorObserveRef},
		Rail:         rail,
		HopIndex:     to.index,
		HopRole:      paymenttypes.QuoteRouteHopRoleTransit,
		Visibility:   model.ReportVisibilityHidden,
		CommitPolicy: model.CommitPolicyAfterFailure,
		CommitAfter:  []string{anchorObserveRef},
		Metadata:     mergeStepMetadata(metadata, map[string]string{"mode": "unlock_hold"}),
	}
	ex.appendBranch(failureStep)
}

func mergeStepMetadata(left, right map[string]string) map[string]string {
	if len(left) == 0 && len(right) == 0 {
		return nil
	}
	out := cloneMetadata(left)
	if out == nil {
		out = map[string]string{}
	}
	for key, value := range right {
		k := strings.TrimSpace(key)
		if k == "" {
			continue
		}
		v := strings.TrimSpace(value)
		if v == "" {
			continue
		}
		out[k] = v
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func payoutTargetsFromIntent(intent model.PaymentIntent) ([]batchmeta.PayoutTarget, error) {
	if len(intent.Attributes) == 0 {
		return nil, nil
	}
	raw := strings.TrimSpace(intent.Attributes[batchmeta.AttrPayoutTargets])
	if raw == "" {
		return nil, nil
	}
	targets, err := batchmeta.DecodePayoutTargets(raw)
	if err != nil {
		return nil, merrors.InvalidArgument("intent_snapshot.attributes[" + batchmeta.AttrPayoutTargets + "] is invalid")
	}
	return targets, nil
}
