package xplan

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

	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
)

func (s *svc) applyPolicy(
	ex *expansion,
	policy Policy,
	from normalizedHop,
	to normalizedHop,
	intent model.PaymentIntent,
) error {
	if len(policy.Steps) == 0 {
		return merrors.InvalidArgument("policy.steps are required")
	}

	anchorRef := ""
	for i := range policy.Steps {
		step, err := policyStepToStep(policy.Steps[i], from, to, intent)
		if err != nil {
			return merrors.InvalidArgument("policy " + strings.TrimSpace(policy.ID) + " step[" + itoa(i) + "]: " + err.Error())
		}
		anchorRef = ex.appendMain(step)
	}
	if strings.TrimSpace(anchorRef) == "" {
		return merrors.InvalidArgument("policy produced no anchor step")
	}

	for i := range policy.Success {
		step, err := policyStepToStep(policy.Success[i], from, to, intent)
		if err != nil {
			return merrors.InvalidArgument("policy " + strings.TrimSpace(policy.ID) + " success[" + itoa(i) + "]: " + err.Error())
		}
		if len(step.DependsOn) == 0 {
			step.DependsOn = []string{anchorRef}
		}
		if len(step.CommitAfter) == 0 {
			step.CommitAfter = cloneStringSlice(step.DependsOn)
		}
		step.CommitPolicy = model.CommitPolicyAfterSuccess
		ex.appendBranch(step)
	}

	for i := range policy.Failure {
		step, err := policyStepToStep(policy.Failure[i], from, to, intent)
		if err != nil {
			return merrors.InvalidArgument("policy " + strings.TrimSpace(policy.ID) + " failure[" + itoa(i) + "]: " + err.Error())
		}
		if len(step.DependsOn) == 0 {
			step.DependsOn = []string{anchorRef}
		}
		if len(step.CommitAfter) == 0 {
			step.CommitAfter = cloneStringSlice(step.DependsOn)
		}
		step.CommitPolicy = model.CommitPolicyAfterFailure
		ex.appendBranch(step)
	}

	return nil
}

func selectPolicy(from normalizedHop, to normalizedHop, policies []Policy) *Policy {
	best := -1
	bestPriority := 0

	for i := range policies {
		policy := &policies[i]
		if !policyEnabled(*policy) {
			continue
		}
		if !policyMatches(policy.Match, from, to) {
			continue
		}

		if best == -1 || policy.Priority > bestPriority {
			best = i
			bestPriority = policy.Priority
		}
	}

	if best == -1 {
		return nil
	}
	return &policies[best]
}

func policyEnabled(policy Policy) bool {
	if policy.Enabled == nil {
		return true
	}
	return *policy.Enabled
}

func policyMatches(match EdgeMatch, from normalizedHop, to normalizedHop) bool {
	return endpointMatches(match.Source, from) && endpointMatches(match.Target, to)
}

func endpointMatches(match EndpointMatch, hop normalizedHop) bool {
	if match.Rail != nil && normalizeRail(string(*match.Rail)) != hop.rail {
		return false
	}
	if match.Custody != nil && *match.Custody != custodyForRail(hop.rail) {
		return false
	}
	if gateway := strings.TrimSpace(match.Gateway); gateway != "" && !strings.EqualFold(gateway, hop.gateway) {
		return false
	}
	if network := strings.TrimSpace(match.Network); network != "" && !strings.EqualFold(network, hop.network) {
		return false
	}
	if strings.TrimSpace(match.Method) != "" {
		// Method-matching is reserved for the next phase once method is passed in intent/route context.
		return false
	}
	return true
}

func policyStepToStep(spec PolicyStep, from normalizedHop, to normalizedHop, intent model.PaymentIntent) (Step, error) {
	code := strings.TrimSpace(spec.Code)
	if code == "" {
		return Step{}, merrors.InvalidArgument("code is required")
	}

	action := normalizeAction(spec.Action)
	if action == discovery.RailOperationUnspecified {
		return Step{}, merrors.InvalidArgument("action is required")
	}

	rail := inferPolicyRail(spec, action, from, to)
	if rail == discovery.RailUnspecified {
		return Step{}, merrors.InvalidArgument("rail could not be inferred")
	}

	hopIndex, hopRole, gateway, instanceID := resolveStepContext(rail, action, from, to)

	visibility := model.NormalizeReportVisibility(spec.Visibility)
	if visibility == model.ReportVisibilityUnspecified {
		visibility = defaultVisibilityForAction(action, hopRole)
	}

	userLabel := strings.TrimSpace(spec.UserLabel)
	if userLabel == "" && visibility == model.ReportVisibilityUser {
		userLabel = defaultUserLabel(action, rail, hopRole, intent.Kind)
	}

	return Step{
		StepCode:   code,
		Kind:       kindForAction(action),
		Action:     action,
		DependsOn:  cloneStringSlice(spec.DependsOn),
		Rail:       rail,
		Gateway:    gateway,
		InstanceID: instanceID,
		HopIndex:   hopIndex,
		HopRole:    hopRole,
		Visibility: visibility,
		UserLabel:  userLabel,
		Metadata:   cloneMetadata(spec.Metadata),
	}, nil
}

func normalizeAction(action model.RailOperation) model.RailOperation {
	switch strings.ToUpper(strings.TrimSpace(string(action))) {
	case string(discovery.RailOperationDebit):
		return discovery.RailOperationDebit
	case string(discovery.RailOperationCredit):
		return discovery.RailOperationCredit
	case string(discovery.RailOperationExternalDebit):
		return discovery.RailOperationExternalDebit
	case string(discovery.RailOperationExternalCredit):
		return discovery.RailOperationExternalCredit
	case string(discovery.RailOperationMove):
		return discovery.RailOperationMove
	case string(discovery.RailOperationSend):
		return discovery.RailOperationSend
	case string(discovery.RailOperationFee):
		return discovery.RailOperationFee
	case string(discovery.RailOperationObserveConfirm):
		return discovery.RailOperationObserveConfirm
	case string(discovery.RailOperationFXConvert):
		return discovery.RailOperationFXConvert
	case string(discovery.RailOperationBlock):
		return discovery.RailOperationBlock
	case string(discovery.RailOperationRelease):
		return discovery.RailOperationRelease
	default:
		return discovery.RailOperationUnspecified
	}
}

func inferPolicyRail(spec PolicyStep, action model.RailOperation, from normalizedHop, to normalizedHop) model.Rail {
	if spec.Rail != nil {
		return normalizeRail(string(*spec.Rail))
	}

	switch action {
	case discovery.RailOperationSend, discovery.RailOperationFXConvert, discovery.RailOperationObserveConfirm, discovery.RailOperationFee:
		return to.rail
	case discovery.RailOperationBlock,
		discovery.RailOperationRelease,
		discovery.RailOperationDebit,
		discovery.RailOperationCredit,
		discovery.RailOperationExternalDebit,
		discovery.RailOperationExternalCredit,
		discovery.RailOperationMove:
		return internalRailForBoundary(from, to)
	default:
		return discovery.RailUnspecified
	}
}

func resolveStepContext(
	rail model.Rail,
	action model.RailOperation,
	from normalizedHop,
	to normalizedHop,
) (uint32, paymenttypes.QuoteRouteHopRole, string, string) {
	if rail == to.rail && (action == discovery.RailOperationSend || action == discovery.RailOperationFXConvert || action == discovery.RailOperationObserveConfirm || action == discovery.RailOperationFee) {
		return to.index, to.role, to.gateway, to.instanceID
	}
	if rail == from.rail {
		return from.index, from.role, from.gateway, from.instanceID
	}
	if rail == to.rail {
		return to.index, to.role, to.gateway, to.instanceID
	}
	return to.index, paymenttypes.QuoteRouteHopRoleTransit, "", ""
}

func kindForAction(action model.RailOperation) StepKind {
	switch action {
	case discovery.RailOperationSend, discovery.RailOperationFXConvert, discovery.RailOperationFee:
		return StepKindRailSend
	case discovery.RailOperationObserveConfirm:
		return StepKindRailObserve
	case discovery.RailOperationCredit, discovery.RailOperationExternalCredit:
		return StepKindFundsCredit
	case discovery.RailOperationDebit, discovery.RailOperationExternalDebit:
		return StepKindFundsDebit
	case discovery.RailOperationMove:
		return StepKindFundsMove
	case discovery.RailOperationBlock:
		return StepKindFundsBlock
	case discovery.RailOperationRelease:
		return StepKindFundsRelease
	default:
		return StepKindUnspecified
	}
}
