package prmap

import (
	"strings"
	"time"

	"github.com/tech/sendico/payments/storage/model"
	payecon "github.com/tech/sendico/pkg/payments/economics"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
	accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1"
	fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
	oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
	quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func mapQuoteSnapshot(
	src *model.PaymentQuoteSnapshot,
	fallbackQuoteRef string,
	intentRef string,
) *quotationv2.PaymentQuote {
	if src == nil {
		return nil
	}
	resolvedSettlementMode := resolvedSettlementModeFromSnapshot(src)
	fxQuote := fxQuoteToProto(src.FXQuote)

	return &quotationv2.PaymentQuote{
		State:                   quotationv2.QuoteState_QUOTE_STATE_EXECUTABLE,
		BlockReason:             quotationv2.QuoteBlockReason_QUOTE_BLOCK_REASON_UNSPECIFIED,
		TransferPrincipalAmount: moneyToProto(src.DebitAmount),
		DestinationAmount:       moneyToProto(src.ExpectedSettlementAmount),
		FeeLines:                feeLinesToProto(src.FeeLines),
		FeeRules:                feeRulesToProto(src.FeeRules),
		FxQuote:                 fxQuote,
		QuoteRef:                firstNonEmpty(src.QuoteRef, fallbackQuoteRef),
		ExpiresAt:               quoteExpiryToProto(fxQuote),
		PricedAt:                quotePricedAtToProto(fxQuote),
		Route:                   routeToProto(src.Route),
		ExecutionConditions:     executionConditionsToProto(src.ExecutionConditions),
		PayerTotalDebitAmount:   moneyToProto(src.TotalCost),
		ResolvedSettlementMode:  resolvedSettlementMode,
		ResolvedFeeTreatment:    payecon.DefaultFeeTreatment(),
		IntentRef:               strings.TrimSpace(intentRef),
	}
}

func moneyToProto(src *paymenttypes.Money) *moneyv1.Money {
	if src == nil {
		return nil
	}
	return &moneyv1.Money{
		Amount:   strings.TrimSpace(src.GetAmount()),
		Currency: strings.ToUpper(strings.TrimSpace(src.GetCurrency())),
	}
}

func fxQuoteToProto(src *paymenttypes.FXQuote) *oraclev1.Quote {
	if src == nil {
		return nil
	}
	out := &oraclev1.Quote{
		QuoteRef:        strings.TrimSpace(src.QuoteRef),
		Side:            fxSideToProto(src.GetSide()),
		Price:           &moneyv1.Decimal{Value: strings.TrimSpace(src.GetPrice().GetValue())},
		BaseAmount:      moneyToProto(src.GetBaseAmount()),
		QuoteAmount:     moneyToProto(src.GetQuoteAmount()),
		ExpiresAtUnixMs: src.GetExpiresAtUnixMs(),
		Provider:        strings.TrimSpace(src.GetProvider()),
		RateRef:         strings.TrimSpace(src.GetRateRef()),
		Firm:            src.GetFirm(),
	}
	if pair := src.GetPair(); pair != nil {
		out.Pair = &fxv1.CurrencyPair{
			Base:  strings.ToUpper(strings.TrimSpace(pair.GetBase())),
			Quote: strings.ToUpper(strings.TrimSpace(pair.GetQuote())),
		}
	}
	if src.GetPricedAtUnixMs() > 0 {
		out.PricedAt = tsOrNil(time.UnixMilli(src.GetPricedAtUnixMs()))
	}
	return out
}

func routeToProto(src *paymenttypes.QuoteRouteSpecification) *quotationv2.RouteSpecification {
	if src == nil {
		return nil
	}
	out := &quotationv2.RouteSpecification{
		Rail:              strings.TrimSpace(src.Rail),
		Provider:          strings.TrimSpace(src.Provider),
		PayoutMethod:      strings.TrimSpace(src.PayoutMethod),
		Network:           strings.TrimSpace(src.Network),
		RouteRef:          strings.TrimSpace(src.RouteRef),
		PricingProfileRef: strings.TrimSpace(src.PricingProfileRef),
		Settlement:        routeSettlementToProto(src.Settlement),
	}
	if len(src.Hops) > 0 {
		out.Hops = make([]*quotationv2.RouteHop, 0, len(src.Hops))
		for _, hop := range src.Hops {
			if hop == nil {
				continue
			}
			out.Hops = append(out.Hops, &quotationv2.RouteHop{
				Index:      hop.Index,
				Rail:       strings.TrimSpace(hop.Rail),
				Gateway:    strings.TrimSpace(hop.Gateway),
				InstanceId: strings.TrimSpace(hop.InstanceID),
				Network:    strings.TrimSpace(hop.Network),
				Role:       routeHopRoleToProto(hop.Role),
			})
		}
	}
	return out
}

func routeSettlementToProto(src *paymenttypes.QuoteRouteSettlement) *quotationv2.RouteSettlement {
	if src == nil {
		return nil
	}
	out := &quotationv2.RouteSettlement{
		Model: strings.TrimSpace(src.Model),
	}
	if src.Asset != nil {
		out.Asset = &paymentv1.ChainAsset{
			Key: &paymentv1.ChainAssetKey{
				Chain:       strings.ToUpper(strings.TrimSpace(src.Asset.Chain)),
				TokenSymbol: strings.ToUpper(strings.TrimSpace(src.Asset.TokenSymbol)),
			},
		}
		if contract := strings.TrimSpace(src.Asset.ContractAddress); contract != "" {
			out.Asset.ContractAddress = &contract
		}
	}
	if out.Asset == nil && out.Model == "" {
		return nil
	}
	return out
}

func executionConditionsToProto(src *paymenttypes.QuoteExecutionConditions) *quotationv2.ExecutionConditions {
	if src == nil {
		return nil
	}
	return &quotationv2.ExecutionConditions{
		Readiness:                         readinessToProto(src.Readiness),
		BatchingEligible:                  src.BatchingEligible,
		PrefundingRequired:                src.PrefundingRequired,
		PrefundingCostIncluded:            src.PrefundingCostIncluded,
		LiquidityCheckRequiredAtExecution: src.LiquidityCheckRequiredAtExecution,
		LatencyHint:                       strings.TrimSpace(src.LatencyHint),
		Assumptions:                       cloneAssumptions(src.Assumptions),
	}
}

func feeLinesToProto(lines []*paymenttypes.FeeLine) []*feesv1.DerivedPostingLine {
	if len(lines) == 0 {
		return nil
	}
	out := make([]*feesv1.DerivedPostingLine, 0, len(lines))
	for _, line := range lines {
		if line == nil {
			continue
		}
		out = append(out, &feesv1.DerivedPostingLine{
			LedgerAccountRef: strings.TrimSpace(line.GetLedgerAccountRef()),
			Money:            moneyToProto(line.GetMoney()),
			LineType:         postingLineTypeToProto(line.GetLineType()),
			Side:             entrySideToProto(line.GetSide()),
			Meta:             cloneStringMap(line.Meta),
		})
	}
	return out
}

func feeRulesToProto(rules []*paymenttypes.AppliedRule) []*feesv1.AppliedRule {
	if len(rules) == 0 {
		return nil
	}
	out := make([]*feesv1.AppliedRule, 0, len(rules))
	for _, rule := range rules {
		if rule == nil {
			continue
		}
		out = append(out, &feesv1.AppliedRule{
			RuleId:      strings.TrimSpace(rule.RuleID),
			RuleVersion: strings.TrimSpace(rule.RuleVersion),
			Formula:     strings.TrimSpace(rule.Formula),
			Rounding:    roundingModeToProto(rule.Rounding),
			TaxCode:     strings.TrimSpace(rule.TaxCode),
			TaxRate:     strings.TrimSpace(rule.TaxRate),
			Parameters:  cloneStringMap(rule.Parameters),
		})
	}
	return out
}

func resolvedSettlementModeFromSnapshot(snapshot *model.PaymentQuoteSnapshot) paymentv1.SettlementMode {
	if snapshot == nil || snapshot.Route == nil || snapshot.Route.Settlement == nil {
		return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE
	}
	switch strings.ToUpper(strings.TrimSpace(snapshot.Route.Settlement.Model)) {
	case "FIX_RECEIVED", "SETTLEMENT_FIX_RECEIVED", "SETTLEMENT_MODE_FIX_RECEIVED":
		return paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED
	default:
		return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE
	}
}

func quoteExpiryToProto(src *oraclev1.Quote) *timestamppb.Timestamp {
	if src == nil || src.GetExpiresAtUnixMs() <= 0 {
		return nil
	}
	return tsOrNil(time.UnixMilli(src.GetExpiresAtUnixMs()))
}

func quotePricedAtToProto(src *oraclev1.Quote) *timestamppb.Timestamp {
	if src == nil || src.GetPricedAt() == nil {
		return nil
	}
	return timestamppb.New(src.GetPricedAt().AsTime().UTC())
}

func cloneAssumptions(src []string) []string {
	if len(src) == 0 {
		return nil
	}
	out := make([]string, 0, len(src))
	for _, item := range src {
		if trimmed := strings.TrimSpace(item); trimmed != "" {
			out = append(out, trimmed)
		}
	}
	return out
}

func cloneStringMap(src map[string]string) map[string]string {
	if len(src) == 0 {
		return nil
	}
	out := make(map[string]string, len(src))
	for k, v := range src {
		out[k] = v
	}
	return out
}

func fxSideToProto(side paymenttypes.FXSide) fxv1.Side {
	switch side {
	case paymenttypes.FXSideBuyBaseSellQuote:
		return fxv1.Side_BUY_BASE_SELL_QUOTE
	case paymenttypes.FXSideSellBaseBuyQuote:
		return fxv1.Side_SELL_BASE_BUY_QUOTE
	default:
		return fxv1.Side_SIDE_UNSPECIFIED
	}
}

func routeHopRoleToProto(role paymenttypes.QuoteRouteHopRole) quotationv2.RouteHopRole {
	switch role {
	case paymenttypes.QuoteRouteHopRoleSource:
		return quotationv2.RouteHopRole_ROUTE_HOP_ROLE_SOURCE
	case paymenttypes.QuoteRouteHopRoleTransit:
		return quotationv2.RouteHopRole_ROUTE_HOP_ROLE_TRANSIT
	case paymenttypes.QuoteRouteHopRoleDestination:
		return quotationv2.RouteHopRole_ROUTE_HOP_ROLE_DESTINATION
	default:
		return quotationv2.RouteHopRole_ROUTE_HOP_ROLE_UNSPECIFIED
	}
}

func readinessToProto(readiness paymenttypes.QuoteExecutionReadiness) quotationv2.QuoteExecutionReadiness {
	switch readiness {
	case paymenttypes.QuoteExecutionReadinessLiquidityReady:
		return quotationv2.QuoteExecutionReadiness_QUOTE_EXECUTION_READINESS_LIQUIDITY_READY
	case paymenttypes.QuoteExecutionReadinessLiquidityObtainable:
		return quotationv2.QuoteExecutionReadiness_QUOTE_EXECUTION_READINESS_LIQUIDITY_OBTAINABLE
	case paymenttypes.QuoteExecutionReadinessIndicative:
		return quotationv2.QuoteExecutionReadiness_QUOTE_EXECUTION_READINESS_INDICATIVE
	default:
		return quotationv2.QuoteExecutionReadiness_QUOTE_EXECUTION_READINESS_UNSPECIFIED
	}
}

func entrySideToProto(side paymenttypes.EntrySide) accountingv1.EntrySide {
	switch side {
	case paymenttypes.EntrySideDebit:
		return accountingv1.EntrySide_ENTRY_SIDE_DEBIT
	case paymenttypes.EntrySideCredit:
		return accountingv1.EntrySide_ENTRY_SIDE_CREDIT
	default:
		return accountingv1.EntrySide_ENTRY_SIDE_UNSPECIFIED
	}
}

func postingLineTypeToProto(lineType paymenttypes.PostingLineType) accountingv1.PostingLineType {
	switch lineType {
	case paymenttypes.PostingLineTypeFee:
		return accountingv1.PostingLineType_POSTING_LINE_FEE
	case paymenttypes.PostingLineTypeTax:
		return accountingv1.PostingLineType_POSTING_LINE_TAX
	case paymenttypes.PostingLineTypeSpread:
		return accountingv1.PostingLineType_POSTING_LINE_SPREAD
	case paymenttypes.PostingLineTypeReversal:
		return accountingv1.PostingLineType_POSTING_LINE_REVERSAL
	default:
		return accountingv1.PostingLineType_POSTING_LINE_TYPE_UNSPECIFIED
	}
}

func roundingModeToProto(mode paymenttypes.RoundingMode) moneyv1.RoundingMode {
	switch mode {
	case paymenttypes.RoundingModeHalfEven:
		return moneyv1.RoundingMode_ROUND_HALF_EVEN
	case paymenttypes.RoundingModeHalfUp:
		return moneyv1.RoundingMode_ROUND_HALF_UP
	case paymenttypes.RoundingModeDown:
		return moneyv1.RoundingMode_ROUND_DOWN
	default:
		return moneyv1.RoundingMode_ROUNDING_MODE_UNSPECIFIED
	}
}
