package model

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

	"github.com/shopspring/decimal"
)

type GatewayDirection int

const (
	GatewayDirectionAny GatewayDirection = iota
	GatewayDirectionOut
	GatewayDirectionIn
)

func (d GatewayDirection) String() string {
	switch d {
	case GatewayDirectionOut:
		return "out"
	case GatewayDirectionIn:
		return "in"
	default:
		return "any"
	}
}

func NoEligibleGatewayMessage(network, currency string, action RailOperation, dir GatewayDirection) string {
	return fmt.Sprintf(
		"plan builder: no eligible gateway found for %s %s %s for direction %s",
		strings.ToUpper(strings.TrimSpace(network)),
		strings.ToUpper(strings.TrimSpace(currency)),
		ParseRailOperation(string(action)),
		dir.String(),
	)
}

func IsGatewayEligible(
	gw *GatewayInstanceDescriptor,
	rail Rail,
	network, currency string,
	action RailOperation,
	dir GatewayDirection,
	amount decimal.Decimal,
) error {
	if gw == nil {
		return gatewayIneligible(gw, "gateway instance is required")
	}
	if !gw.IsEnabled {
		return gatewayIneligible(gw, "gateway instance is disabled")
	}
	if gw.Rail != rail {
		return gatewayIneligible(gw, fmt.Sprintf("rail mismatch: want %s got %s", rail, gw.Rail))
	}
	if network != "" && gw.Network != "" && !strings.EqualFold(gw.Network, network) {
		return gatewayIneligible(gw, fmt.Sprintf("network mismatch: want %s got %s", network, gw.Network))
	}
	if currency != "" && len(gw.Currencies) > 0 {
		found := false
		for _, c := range gw.Currencies {
			if strings.EqualFold(c, currency) {
				found = true
				break
			}
		}
		if !found {
			return gatewayIneligible(gw, "currency not supported: "+currency)
		}
	}

	if !gatewayAllowsAction(gw.Operations, gw.Capabilities, action, dir) {
		return gatewayIneligible(gw, fmt.Sprintf("gateway does not allow action=%s dir=%s", action, dir.String()))
	}

	if currency != "" {
		if err := amountWithinLimits(gw, gw.Limits, currency, amount, action); err != nil {
			return err
		}
	}
	return nil
}

type gatewayIneligibleError struct {
	reason string
}

func (e gatewayIneligibleError) Error() string {
	return e.reason
}

func gatewayIneligible(gw *GatewayInstanceDescriptor, reason string) error {
	if strings.TrimSpace(reason) == "" {
		reason = "gateway instance is not eligible"
	}
	instanceID := ""
	if gw != nil {
		instanceID = gw.InstanceID
	}
	return gatewayIneligibleError{reason: fmt.Sprintf("gateway %s eligibility check error: %s", instanceID, reason)}
}

func gatewayAllowsAction(operations []RailOperation, cap RailCapabilities, action RailOperation, dir GatewayDirection) bool {
	normalized := NormalizeRailOperations(operations)
	if len(normalized) > 0 {
		return operationsAllowAction(normalized, action, dir)
	}
	return capabilityAllowsAction(cap, action, dir)
}

func capabilityAllowsAction(cap RailCapabilities, action RailOperation, dir GatewayDirection) bool {
	switch action {
	case discovery.RailOperationSend:
		switch dir {
		case GatewayDirectionOut:
			return cap.CanPayOut
		case GatewayDirectionIn:
			return cap.CanPayIn
		default:
			return cap.CanPayIn || cap.CanPayOut
		}
	case discovery.RailOperationExternalDebit, discovery.RailOperationExternalCredit:
		switch dir {
		case GatewayDirectionOut:
			return cap.CanPayOut
		case GatewayDirectionIn:
			return cap.CanPayIn
		default:
			return cap.CanPayIn || cap.CanPayOut
		}
	case discovery.RailOperationFee:
		return cap.CanSendFee
	case discovery.RailOperationObserveConfirm:
		return cap.RequiresObserveConfirm
	case discovery.RailOperationBlock:
		return cap.CanBlock
	case discovery.RailOperationRelease:
		return cap.CanRelease
	default:
		return true
	}
}

func operationsAllowAction(operations []RailOperation, action RailOperation, dir GatewayDirection) bool {
	action = ParseRailOperation(string(action))
	if action == discovery.RailOperationUnspecified {
		return false
	}

	if HasRailOperation(operations, action) {
		return true
	}

	switch action {
	case discovery.RailOperationSend:
		switch dir {
		case GatewayDirectionIn:
			return HasRailOperation(operations, discovery.RailOperationExternalDebit)
		case GatewayDirectionOut:
			return HasRailOperation(operations, discovery.RailOperationExternalCredit)
		default:
			return HasRailOperation(operations, discovery.RailOperationExternalDebit) ||
				HasRailOperation(operations, discovery.RailOperationExternalCredit)
		}
	case discovery.RailOperationExternalDebit:
		return HasRailOperation(operations, discovery.RailOperationSend)
	case discovery.RailOperationExternalCredit:
		return HasRailOperation(operations, discovery.RailOperationSend)
	default:
		return false
	}
}

func amountWithinLimits(gw *GatewayInstanceDescriptor, limits Limits, currency string, amount decimal.Decimal, action RailOperation) error {
	min := firstLimitValue(limits.MinAmount, "")
	max := firstLimitValue(limits.MaxAmount, "")
	perTxMin := firstLimitValue(limits.PerTxMinAmount, "")
	perTxMax := firstLimitValue(limits.PerTxMaxAmount, "")
	maxFee := firstLimitValue(limits.PerTxMaxFee, "")

	if override, ok := limits.CurrencyLimits[currency]; ok {
		min = firstLimitValue(override.MinAmount, min)
		max = firstLimitValue(override.MaxAmount, max)
		if action == discovery.RailOperationFee {
			maxFee = firstLimitValue(override.MaxFee, maxFee)
		}
	}

	if min != "" {
		if val, err := decimal.NewFromString(min); err == nil && amount.LessThan(val) {
			return gatewayIneligible(gw, fmt.Sprintf("amount %s %s below min limit %s", amount.String(), currency, val.String()))
		}
	}
	if perTxMin != "" {
		if val, err := decimal.NewFromString(perTxMin); err == nil && amount.LessThan(val) {
			return gatewayIneligible(gw, fmt.Sprintf("amount %s %s below per-tx min limit %s", amount.String(), currency, val.String()))
		}
	}
	if max != "" {
		if val, err := decimal.NewFromString(max); err == nil && amount.GreaterThan(val) {
			return gatewayIneligible(gw, fmt.Sprintf("amount %s %s exceeds max limit %s", amount.String(), currency, val.String()))
		}
	}
	if perTxMax != "" {
		if val, err := decimal.NewFromString(perTxMax); err == nil && amount.GreaterThan(val) {
			return gatewayIneligible(gw, fmt.Sprintf("amount %s %s exceeds per-tx max limit %s", amount.String(), currency, val.String()))
		}
	}
	if action == discovery.RailOperationFee && maxFee != "" {
		if val, err := decimal.NewFromString(maxFee); err == nil && amount.GreaterThan(val) {
			return gatewayIneligible(gw, fmt.Sprintf("fee amount %s %s exceeds max fee limit %s", amount.String(), currency, val.String()))
		}
	}

	return nil
}

func firstLimitValue(primary, fallback string) string {
	val := strings.TrimSpace(primary)
	if val != "" {
		return val
	}
	return strings.TrimSpace(fallback)
}
