package gateway_funding_profile

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

	"github.com/tech/sendico/payments/quotation/internal/shared"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/model/account_role"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
)

const (
	defaultCardGatewayKey = paymenttypes.DefaultCardsGatewayID
)

type CardGatewayFundingRoute struct {
	FundingAddress string
	FeeAddress     string
	FeeWalletRef   string
}

type StaticFundingProfileResolverInput struct {
	DefaultCardGateway string
	DefaultMode        model.FundingMode
	GatewayModes       map[string]model.FundingMode
	CardRoutes         map[string]CardGatewayFundingRoute
	FeeLedgerAccounts  map[string]string
	Profiles           map[string]*GatewayFundingProfile
}

type StaticFundingProfileResolver struct {
	defaultCardGateway string
	defaultMode        model.FundingMode
	gatewayModes       map[string]model.FundingMode
	cardRoutes         map[string]CardGatewayFundingRoute
	feeLedgerAccounts  map[string]string
	profiles           map[string]*GatewayFundingProfile
}

func NewStaticFundingProfileResolver(in StaticFundingProfileResolverInput) *StaticFundingProfileResolver {
	r := &StaticFundingProfileResolver{
		defaultCardGateway: normalizeGatewayKey(in.DefaultCardGateway),
		defaultMode:        shared.NormalizeFundingMode(in.DefaultMode),
		gatewayModes:       map[string]model.FundingMode{},
		cardRoutes:         map[string]CardGatewayFundingRoute{},
		feeLedgerAccounts:  map[string]string{},
		profiles:           map[string]*GatewayFundingProfile{},
	}
	if r.defaultCardGateway == "" {
		r.defaultCardGateway = defaultCardGatewayKey
	}
	if r.defaultMode == model.FundingModeUnspecified {
		r.defaultMode = model.FundingModeNone
	}
	for k, v := range in.GatewayModes {
		if key := normalizeGatewayKey(k); key != "" {
			r.gatewayModes[key] = shared.NormalizeFundingMode(v)
		}
	}
	for k, v := range in.CardRoutes {
		if key := normalizeGatewayKey(k); key != "" {
			r.cardRoutes[key] = CardGatewayFundingRoute{
				FundingAddress: strings.TrimSpace(v.FundingAddress),
				FeeAddress:     strings.TrimSpace(v.FeeAddress),
				FeeWalletRef:   strings.TrimSpace(v.FeeWalletRef),
			}
		}
	}
	for k, v := range in.FeeLedgerAccounts {
		if key := normalizeGatewayKey(k); key != "" {
			if val := strings.TrimSpace(v); val != "" {
				r.feeLedgerAccounts[key] = val
			}
		}
	}
	for k, v := range in.Profiles {
		if key := normalizeGatewayKey(k); key != "" && v != nil {
			r.profiles[key] = cloneGatewayFundingProfile(v)
		}
	}
	return r
}

func (r *StaticFundingProfileResolver) ResolveGatewayFundingProfile(
	_ context.Context,
	req FundingProfileRequest,
) (*GatewayFundingProfile, error) {
	if r == nil {
		return nil, nil //nolint:nilnil // nil resolver means no static funding configuration
	}

	gatewayKey := r.gatewayKey(req)
	attrs := cloneStringMap(req.Attributes)
	if attrs == nil {
		attrs = map[string]string{}
	}

	modeSet := false
	mode := model.FundingModeUnspecified

	var profile *GatewayFundingProfile
	if gatewayKey != "" {
		if configured, ok := r.profiles[gatewayKey]; ok && configured != nil {
			profile = cloneGatewayFundingProfile(configured)
			mode = shared.NormalizeFundingMode(profile.Mode)
			modeSet = mode != model.FundingModeUnspecified
		}
	}
	if profile == nil {
		profile = &GatewayFundingProfile{}
	}

	profile.GatewayID = firstNonEmpty(
		strings.TrimSpace(profile.GatewayID),
		gatewayKey,
	)
	profile.InstanceID = firstNonEmpty(
		strings.TrimSpace(profile.InstanceID),
		strings.TrimSpace(req.InstanceID),
	)
	if profile.Rail == discovery.RailUnspecified {
		profile.Rail = req.Rail
	}
	if strings.TrimSpace(profile.Network) == "" {
		profile.Network = strings.TrimSpace(req.Network)
	}
	if strings.TrimSpace(profile.Currency) == "" {
		profile.Currency = normalizedCurrency(req.Currency, req.Amount)
	}

	if !modeSet && gatewayKey != "" {
		if configured, ok := r.gatewayModes[gatewayKey]; ok {
			mode = shared.NormalizeFundingMode(configured)
			modeSet = mode != model.FundingModeUnspecified
		}
	}
	if !modeSet {
		mode = r.defaultMode
	}
	profile.Mode = mode

	sourceAsset := sourceAsset(req.Source)

	if gatewayKey != "" {
		if route, ok := r.cardRoutes[gatewayKey]; ok {
			if profile.FundingDestination == nil && route.FundingAddress != "" {
				profile.FundingDestination = &model.PaymentEndpoint{
					Type: model.EndpointTypeExternalChain,
					ExternalChain: &model.ExternalChainEndpoint{
						Address: route.FundingAddress,
						Asset:   cloneAsset(sourceAsset),
					},
				}
			}
			if profile.FeeDestination == nil {
				switch {
				case route.FeeWalletRef != "":
					profile.FeeDestination = &model.PaymentEndpoint{
						Type: model.EndpointTypeManagedWallet,
						ManagedWallet: &model.ManagedWalletEndpoint{
							ManagedWalletRef: route.FeeWalletRef,
							Asset:            cloneAsset(sourceAsset),
						},
					}
				case route.FeeAddress != "":
					profile.FeeDestination = &model.PaymentEndpoint{
						Type: model.EndpointTypeExternalChain,
						ExternalChain: &model.ExternalChainEndpoint{
							Address: route.FeeAddress,
							Asset:   cloneAsset(sourceAsset),
						},
					}
				}
			}
		}
	}

	if feeAccount := r.feeLedgerAccounts[gatewayKey]; feeAccount != "" {
		if profile.Defaults == nil {
			profile.Defaults = map[string]string{}
		}
		if strings.TrimSpace(profile.Defaults["fee_ledger_account_ref"]) == "" {
			profile.Defaults["fee_ledger_account_ref"] = feeAccount
		}
	}
	if len(attrs) > 0 {
		if profile.Defaults == nil {
			profile.Defaults = map[string]string{}
		}
		if _, ok := profile.Defaults["gateway"]; !ok && gatewayKey != "" {
			profile.Defaults["gateway"] = gatewayKey
		}
	}

	if profile.Reserve == nil {
		if reserve := reservePolicyFromRequest(req); reserve != nil {
			profile.Reserve = reserve
		}
	}

	if profile.Mode == model.FundingModeNone && profile.Reserve != nil {
		profile.Mode = model.FundingModeBalanceReserve
	}

	if isEmptyFundingProfile(profile) {
		return nil, nil //nolint:nilnil // nil profile means funding profile is intentionally omitted
	}
	return profile, nil
}

func (r *StaticFundingProfileResolver) gatewayKey(req FundingProfileRequest) string {
	if key := normalizeGatewayKey(req.GatewayID); key != "" {
		return key
	}
	if key := normalizeGatewayKey(req.Attributes["gateway"]); key != "" {
		return key
	}
	return ""
}

func normalizeGatewayKey(key string) string {
	return strings.ToLower(strings.TrimSpace(key))
}

func normalizedCurrency(currency string, amount *moneyv1.Money) string {
	if cur := strings.ToUpper(strings.TrimSpace(currency)); cur != "" {
		return cur
	}
	if amount != nil {
		return strings.ToUpper(strings.TrimSpace(amount.GetCurrency()))
	}
	return ""
}

func sourceAsset(source *model.PaymentEndpoint) *paymenttypes.Asset {
	if source == nil {
		return nil
	}
	if source.ManagedWallet != nil {
		return source.ManagedWallet.Asset
	}
	if source.ExternalChain != nil {
		return source.ExternalChain.Asset
	}
	return nil
}

func reservePolicyFromRequest(req FundingProfileRequest) *ReservePolicy {
	if req.Source == nil && len(req.Attributes) == 0 {
		return nil
	}
	reserve := &ReservePolicy{
		DebitAccountRef: strings.TrimSpace(lookupAttr(req.Attributes,
			"ledger_debit_account_ref",
			"ledgerDebitAccountRef",
		)),
		BlockAccountRef: strings.TrimSpace(lookupAttr(req.Attributes,
			"ledger_block_account_ref",
			"ledgerBlockAccountRef",
			"ledger_hold_account_ref",
			"ledgerHoldAccountRef",
			"ledger_debit_contra_account_ref",
			"ledgerDebitContraAccountRef",
		)),
		ReleaseOnFail:   true,
		ReleaseOnCancel: true,
	}
	if req.Source != nil && req.Source.Ledger != nil {
		if reserve.DebitAccountRef == "" {
			reserve.DebitAccountRef = strings.TrimSpace(req.Source.Ledger.LedgerAccountRef)
		}
		if reserve.BlockAccountRef == "" {
			reserve.BlockAccountRef = strings.TrimSpace(req.Source.Ledger.ContraLedgerAccountRef)
		}
	}

	if role, ok := parseAccountRole(lookupAttr(req.Attributes, "from_role", "fromRole")); ok {
		reserve.FromRole = &role
	}
	if role, ok := parseAccountRole(lookupAttr(req.Attributes, "to_role", "toRole")); ok {
		reserve.ToRole = &role
	}

	if reserve.DebitAccountRef == "" &&
		reserve.BlockAccountRef == "" &&
		reserve.FromRole == nil &&
		reserve.ToRole == nil {
		return nil
	}
	return reserve
}

func parseAccountRole(value string) (account_role.AccountRole, bool) {
	role, ok := account_role.Parse(strings.TrimSpace(value))
	if !ok || role == "" {
		return "", false
	}
	return role, true
}

func lookupAttr(attrs map[string]string, keys ...string) string {
	if len(attrs) == 0 {
		return ""
	}
	for _, key := range keys {
		if key == "" {
			continue
		}
		if val := strings.TrimSpace(attrs[key]); val != "" {
			return val
		}
	}
	return ""
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return ""
}

func cloneGatewayFundingProfile(src *GatewayFundingProfile) *GatewayFundingProfile {
	if src == nil {
		return nil
	}
	return &GatewayFundingProfile{
		GatewayID:          strings.TrimSpace(src.GatewayID),
		InstanceID:         strings.TrimSpace(src.InstanceID),
		Rail:               src.Rail,
		Network:            strings.TrimSpace(src.Network),
		Currency:           strings.ToUpper(strings.TrimSpace(src.Currency)),
		Mode:               shared.NormalizeFundingMode(src.Mode),
		FundingDestination: clonePaymentEndpoint(src.FundingDestination),
		FeeDestination:     clonePaymentEndpoint(src.FeeDestination),
		Reserve:            cloneReservePolicy(src.Reserve),
		DepositCheck:       cloneDepositCheck(src.DepositCheck),
		Defaults:           cloneStringMap(src.Defaults),
	}
}

func isEmptyFundingProfile(profile *GatewayFundingProfile) bool {
	if profile == nil {
		return true
	}
	if profile.FundingDestination != nil || profile.FeeDestination != nil || profile.Reserve != nil || profile.DepositCheck != nil {
		return false
	}
	if profile.Mode != model.FundingModeUnspecified && profile.Mode != model.FundingModeNone {
		return false
	}
	if len(profile.Defaults) > 0 {
		return false
	}
	if profile.GatewayID != "" {
		return false
	}
	return true
}
