package orchestrator

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

	ledgerclient "github.com/tech/sendico/ledger/client"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/batchmeta"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/erecon"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/ledgerconv"
	"github.com/tech/sendico/pkg/merrors"
	"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"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
)

const (
	ledgerMetadataMode = "mode"
)

type gatewayLedgerExecutor struct {
	ledgerClient ledgerclient.Client
}

type ledgerRoles struct {
	from account_role.AccountRole
	to   account_role.AccountRole
}

func (e *gatewayLedgerExecutor) ExecuteLedger(ctx context.Context, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
	if req.Payment == nil {
		return nil, merrors.InvalidArgument("ledger step: payment is required")
	}
	if e == nil || e.ledgerClient == nil {
		return nil, merrors.InvalidArgument("ledger step: ledger client is required")
	}

	action := model.ParseRailOperation(string(req.Step.Action))
	switch action {
	case discovery.RailOperationDebit,
		discovery.RailOperationCredit,
		discovery.RailOperationExternalDebit,
		discovery.RailOperationExternalCredit,
		discovery.RailOperationMove,
		discovery.RailOperationBlock,
		discovery.RailOperationRelease:
	default:
		return nil, merrors.InvalidArgument("ledger step: unsupported action")
	}

	amount, err := ledgerAmountForStep(req.Payment, req.Step, req.StepExecution, action)
	if err != nil {
		return nil, err
	}
	roles, err := ledgerRolesForStep(req.Step, action)
	if err != nil {
		return nil, err
	}

	idempotencyKey := ledgerStepIdempotencyKey(req.Payment, req.Step)
	organizationRef := req.Payment.OrganizationRef.Hex()
	description := ledgerDescription(req.Step)
	metadata := ledgerTransferMetadata(req.Payment, req.Step, roles)
	charges, err := ledgerChargesForStep(req.Payment, req.Step, action, amount)
	if err != nil {
		return nil, err
	}

	var resp *ledgerv1.PostResponse
	switch action {
	case discovery.RailOperationExternalCredit:
		resp, err = e.ledgerClient.PostExternalCreditWithCharges(ctx, &ledgerv1.PostCreditRequest{
			IdempotencyKey:  idempotencyKey,
			OrganizationRef: organizationRef,
			Money:           amount,
			Description:     description,
			Charges:         charges,
			Metadata:        metadata,
			Role:            ledgerRoleToProto(roles.to),
		})
	case discovery.RailOperationExternalDebit:
		resp, err = e.ledgerClient.PostExternalDebitWithCharges(ctx, &ledgerv1.PostDebitRequest{
			IdempotencyKey:  idempotencyKey,
			OrganizationRef: organizationRef,
			Money:           amount,
			Description:     description,
			Charges:         charges,
			Metadata:        metadata,
			Role:            ledgerRoleToProto(roles.from),
		})
	default:
		resp, err = e.ledgerClient.TransferInternal(ctx, &ledgerv1.TransferRequest{
			IdempotencyKey:  idempotencyKey,
			OrganizationRef: organizationRef,
			Money:           amount,
			Description:     description,
			Metadata:        metadata,
			FromRole:        ledgerRoleToProto(roles.from),
			ToRole:          ledgerRoleToProto(roles.to),
		})
	}
	if err != nil {
		return nil, err
	}
	if resp == nil || strings.TrimSpace(resp.GetJournalEntryRef()) == "" {
		return nil, merrors.Internal("ledger step: journal entry reference is missing")
	}

	step := req.StepExecution
	step.State = agg.StepStateCompleted
	step.FailureCode = ""
	step.FailureMsg = ""
	step.ExecutedMoney = protoMoneyToPaymentMoney(amount)
	step.ConvertedMoney = nil
	step.ExternalRefs = appendLedgerExternalRef(step.ExternalRefs, agg.ExternalRef{
		GatewayInstanceID: firstNonEmpty(strings.TrimSpace(req.Step.InstanceID), strings.TrimSpace(req.Step.Gateway)),
		Kind:              erecon.ExternalRefKindLedger,
		Ref:               strings.TrimSpace(resp.GetJournalEntryRef()),
	})

	return &sexec.ExecuteOutput{StepExecution: step}, nil
}

func ledgerAmountForStep(
	payment *agg.Payment,
	step xplan.Step,
	stepExecution agg.StepExecution,
	action model.RailOperation,
) (*moneyv1.Money, error) {
	if planned := plannedStepMoney(stepExecution); planned != nil {
		return protoMoneyRequired(planned, "ledger step: planned amount is invalid")
	}
	if override, ok := batchmeta.AmountFromMetadata(step.Metadata); ok && override != nil {
		return protoMoneyRequired(override, "ledger step: override amount is invalid")
	}
	sourceMoney := sourceMoneyForLedger(payment)
	settlementMoney := settlementMoneyForLedger(payment, sourceMoney)
	payoutMoney := payoutMoneyForLedger(payment, settlementMoney)

	if fromRail, toRail, ok := ledgerBoundaryRails(payment, step); ok {
		switch {
		case isLedgerExternalRail(fromRail) && isLedgerExternalRail(toRail):
			return protoMoneyRequired(sourceMoney, "ledger step: source amount is required")
		case isLedgerExternalRail(fromRail) && isLedgerInternalRail(toRail):
			return protoMoneyRequired(settlementMoney, "ledger step: settlement amount is required")
		case isLedgerInternalRail(fromRail) && isLedgerExternalRail(toRail):
			if toRail == discovery.RailCardPayout {
				return protoMoneyRequired(payoutMoney, "ledger step: payout amount is required")
			}
			return protoMoneyRequired(settlementMoney, "ledger step: settlement amount is required")
		case isLedgerInternalRail(fromRail) && isLedgerInternalRail(toRail):
			return protoMoneyRequired(settlementMoney, "ledger step: settlement amount is required")
		}
	}

	switch action {
	case discovery.RailOperationCredit, discovery.RailOperationExternalCredit:
		return protoMoneyRequired(settlementMoney, "ledger step: settlement amount is required")
	case discovery.RailOperationDebit, discovery.RailOperationExternalDebit:
		if sourceMoney != nil {
			return protoMoneyRequired(sourceMoney, "ledger step: source amount is required")
		}
		return protoMoneyRequired(settlementMoney, "ledger step: settlement amount is required")
	case discovery.RailOperationMove, discovery.RailOperationBlock, discovery.RailOperationRelease:
		if settlementMoney != nil {
			return protoMoneyRequired(settlementMoney, "ledger step: settlement amount is required")
		}
		return protoMoneyRequired(sourceMoney, "ledger step: source amount is required")
	default:
		return nil, merrors.InvalidArgument("ledger step: unsupported action")
	}
}

func sourceMoneyForLedger(payment *agg.Payment) *paymenttypes.Money {
	if payment == nil {
		return nil
	}
	if payment.QuoteSnapshot != nil && payment.QuoteSnapshot.DebitAmount != nil {
		return payment.QuoteSnapshot.DebitAmount
	}
	return payment.IntentSnapshot.Amount
}

func settlementMoneyForLedger(payment *agg.Payment, source *paymenttypes.Money) *paymenttypes.Money {
	if payment != nil && payment.QuoteSnapshot != nil && payment.QuoteSnapshot.ExpectedSettlementAmount != nil {
		return payment.QuoteSnapshot.ExpectedSettlementAmount
	}
	return source
}

func ledgerChargesForStep(payment *agg.Payment, step xplan.Step, action model.RailOperation, amount *moneyv1.Money) ([]*ledgerv1.PostingLine, error) {
	if !shouldAttachLedgerCharges(payment, step, action) {
		return nil, nil
	}
	if payment == nil || payment.QuoteSnapshot == nil {
		return nil, nil
	}
	currency := ""
	if amount != nil {
		currency = strings.TrimSpace(amount.GetCurrency())
	}
	return feeLinesToLedgerCharges(payment.QuoteSnapshot.FeeLines, currency)
}

func shouldAttachLedgerCharges(payment *agg.Payment, step xplan.Step, action model.RailOperation) bool {
	switch action {
	case discovery.RailOperationExternalCredit, discovery.RailOperationExternalDebit:
	default:
		return false
	}

	fromRail, toRail, ok := ledgerBoundaryRails(payment, step)
	if !ok {
		// Prefer attaching on external debits when boundary metadata is unavailable.
		return action == discovery.RailOperationExternalDebit
	}

	switch action {
	case discovery.RailOperationExternalCredit:
		return isLedgerExternalRail(fromRail) && isLedgerInternalRail(toRail)
	case discovery.RailOperationExternalDebit:
		return isLedgerExternalRail(toRail)
	default:
		return false
	}
}

func feeLinesToLedgerCharges(lines []*paymenttypes.FeeLine, currency string) ([]*ledgerv1.PostingLine, error) {
	if len(lines) == 0 {
		return nil, nil
	}

	result := make([]*ledgerv1.PostingLine, 0, len(lines))
	for i := range lines {
		line := lines[i]
		if line == nil || line.GetMoney() == nil {
			continue
		}
		accountRef := strings.TrimSpace(line.GetLedgerAccountRef())
		if accountRef == "" {
			continue
		}

		lineCurrency := strings.TrimSpace(line.GetMoney().GetCurrency())
		if currency != "" && !strings.EqualFold(lineCurrency, currency) {
			continue
		}

		amount, err := decimal.NewFromString(strings.TrimSpace(line.GetMoney().GetAmount()))
		if err != nil {
			return nil, merrors.InvalidArgument(fmt.Sprintf("ledger step: fee_lines[%d].money.amount is invalid", i))
		}
		amount = normalizeChargeAmountForSide(amount, line.GetSide())

		result = append(result, &ledgerv1.PostingLine{
			LedgerAccountRef: accountRef,
			Money: &moneyv1.Money{
				Amount:   amount.String(),
				Currency: lineCurrency,
			},
			LineType: feeLineTypeToLedger(line.GetLineType()),
		})
	}
	if len(result) == 0 {
		return nil, nil
	}
	return result, nil
}

func normalizeChargeAmountForSide(amount decimal.Decimal, side paymenttypes.EntrySide) decimal.Decimal {
	switch side {
	case paymenttypes.EntrySideDebit:
		if amount.Sign() > 0 {
			return amount.Neg()
		}
	case paymenttypes.EntrySideCredit:
		if amount.Sign() < 0 {
			return amount.Neg()
		}
	}
	return amount
}

func feeLineTypeToLedger(lineType paymenttypes.PostingLineType) ledgerv1.LineType {
	switch lineType {
	case paymenttypes.PostingLineTypeSpread:
		return ledgerv1.LineType_LINE_SPREAD
	case paymenttypes.PostingLineTypeReversal:
		return ledgerv1.LineType_LINE_REVERSAL
	default:
		// Ledger supports fee/spread/reversal only; map TAX/UNSPECIFIED to fee.
		return ledgerv1.LineType_LINE_FEE
	}
}

func payoutMoneyForLedger(_ *agg.Payment, settlement *paymenttypes.Money) *paymenttypes.Money {
	return settlement
}

func protoMoneyRequired(m *paymenttypes.Money, errMsg string) (*moneyv1.Money, error) {
	if m == nil {
		return nil, merrors.InvalidArgument(errMsg)
	}
	amount := strings.TrimSpace(m.GetAmount())
	currency := strings.TrimSpace(m.GetCurrency())
	if amount == "" || currency == "" {
		return nil, merrors.InvalidArgument(errMsg)
	}
	return &moneyv1.Money{Amount: amount, Currency: currency}, nil
}

func ledgerRolesForStep(step xplan.Step, action model.RailOperation) (ledgerRoles, error) {
	roles, ok, err := ledgerRolesFromMetadata(step.Metadata)
	if err != nil {
		return ledgerRoles{}, err
	}
	if ok {
		return roles, nil
	}

	mode := strings.ToLower(strings.TrimSpace(step.Metadata[ledgerMetadataMode]))
	switch action {
	case discovery.RailOperationBlock:
		return ledgerRoles{from: account_role.AccountRoleOperating, to: account_role.AccountRoleHold}, nil
	case discovery.RailOperationRelease:
		return ledgerRoles{from: account_role.AccountRoleHold, to: account_role.AccountRoleOperating}, nil
	case discovery.RailOperationCredit, discovery.RailOperationExternalCredit:
		return ledgerRoles{from: account_role.AccountRolePending, to: account_role.AccountRoleOperating}, nil
	case discovery.RailOperationDebit, discovery.RailOperationExternalDebit:
		if mode == "finalize_debit" {
			return ledgerRoles{from: account_role.AccountRoleHold, to: account_role.AccountRoleTransit}, nil
		}
		return ledgerRoles{from: account_role.AccountRoleOperating, to: account_role.AccountRoleTransit}, nil
	case discovery.RailOperationMove:
		return ledgerRoles{from: account_role.AccountRoleOperating, to: account_role.AccountRoleTransit}, nil
	default:
		return ledgerRoles{}, merrors.InvalidArgument("ledger step: unsupported action")
	}
}

func ledgerRolesFromMetadata(metadata map[string]string) (ledgerRoles, bool, error) {
	fromValue, fromFound := metadataLookup(metadata, "from_role", "fromRole")
	toValue, toFound := metadataLookup(metadata, "to_role", "toRole")
	if !fromFound && !toFound {
		return ledgerRoles{}, false, nil
	}
	if strings.TrimSpace(fromValue) == "" || strings.TrimSpace(toValue) == "" {
		return ledgerRoles{}, false, merrors.InvalidArgument("ledger step: from_role and to_role must both be provided")
	}

	fromRole, ok := account_role.Parse(fromValue)
	if !ok || fromRole == "" {
		return ledgerRoles{}, false, merrors.InvalidArgument("ledger step: invalid from_role")
	}
	toRole, ok := account_role.Parse(toValue)
	if !ok || toRole == "" {
		return ledgerRoles{}, false, merrors.InvalidArgument("ledger step: invalid to_role")
	}
	if fromRole == toRole {
		return ledgerRoles{}, false, merrors.InvalidArgument("ledger step: from_role and to_role must differ")
	}
	return ledgerRoles{from: fromRole, to: toRole}, true, nil
}

func metadataLookup(metadata map[string]string, keys ...string) (string, bool) {
	if len(metadata) == 0 {
		return "", false
	}
	for i := range keys {
		key := strings.TrimSpace(keys[i])
		if key == "" {
			continue
		}
		value, ok := metadata[key]
		if !ok {
			continue
		}
		return value, true
	}
	return "", false
}

func ledgerRoleToProto(role account_role.AccountRole) ledgerv1.AccountRole {
	parsed, _ := ledgerconv.ParseAccountRole(string(role))
	return parsed
}

func ledgerTransferMetadata(payment *agg.Payment, step xplan.Step, roles ledgerRoles) map[string]string {
	out := transferMetadata(step)
	if out == nil {
		out = map[string]string{}
	}
	if quoteRef := firstNonEmpty(strings.TrimSpace(payment.QuotationRef), strings.TrimSpace(quoteRefFromSnapshot(payment.QuoteSnapshot))); quoteRef != "" {
		out[settlementMetadataQuoteRef] = quoteRef
	}
	if roles.from != "" {
		out[account_role.MetadataKeyFromRole] = string(roles.from)
	}
	if roles.to != "" {
		out[account_role.MetadataKeyToRole] = string(roles.to)
	}
	if mode := strings.TrimSpace(step.Metadata[ledgerMetadataMode]); mode != "" {
		out[ledgerMetadataMode] = mode
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func ledgerStepIdempotencyKey(payment *agg.Payment, step xplan.Step) string {
	base := strings.TrimSpace(payment.IdempotencyKey)
	if base == "" {
		base = strings.TrimSpace(payment.PaymentRef)
	}
	stepToken := firstNonEmpty(strings.TrimSpace(step.StepRef), strings.TrimSpace(step.StepCode), "ledger")
	if base == "" {
		return "ledger:" + stepToken
	}
	return base + ":" + stepToken
}

func ledgerDescription(step xplan.Step) string {
	code := strings.TrimSpace(step.StepCode)
	if code == "" {
		code = strings.TrimSpace(step.StepRef)
	}
	action := strings.ToLower(strings.TrimSpace(string(step.Action)))
	if code == "" {
		return "orchestration ledger " + action
	}
	return fmt.Sprintf("orchestration ledger %s %s", action, code)
}

func appendLedgerExternalRef(existing []agg.ExternalRef, ref agg.ExternalRef) []agg.ExternalRef {
	ref.GatewayInstanceID = strings.TrimSpace(ref.GatewayInstanceID)
	ref.Kind = strings.TrimSpace(ref.Kind)
	ref.Ref = strings.TrimSpace(ref.Ref)
	if ref.Kind == "" || ref.Ref == "" {
		return existing
	}
	for i := range existing {
		item := existing[i]
		if strings.EqualFold(strings.TrimSpace(item.GatewayInstanceID), ref.GatewayInstanceID) &&
			strings.EqualFold(strings.TrimSpace(item.Kind), ref.Kind) &&
			strings.EqualFold(strings.TrimSpace(item.Ref), ref.Ref) {
			return existing
		}
	}
	return append(existing, ref)
}

func ledgerBoundaryRails(payment *agg.Payment, step xplan.Step) (model.Rail, model.Rail, bool) {
	fromIndex, toIndex, ok := parseLedgerEdgeStepCode(step.StepCode)
	if !ok || payment == nil || payment.QuoteSnapshot == nil || payment.QuoteSnapshot.Route == nil {
		return discovery.RailUnspecified, discovery.RailUnspecified, false
	}

	var fromRail model.Rail = discovery.RailUnspecified
	var toRail model.Rail = discovery.RailUnspecified
	for i := range payment.QuoteSnapshot.Route.Hops {
		hop := payment.QuoteSnapshot.Route.Hops[i]
		if hop == nil {
			continue
		}
		if hop.Index == fromIndex {
			fromRail = model.ParseRail(hop.Rail)
		}
		if hop.Index == toIndex {
			toRail = model.ParseRail(hop.Rail)
		}
	}
	if fromRail == discovery.RailUnspecified || toRail == discovery.RailUnspecified {
		return discovery.RailUnspecified, discovery.RailUnspecified, false
	}
	return fromRail, toRail, true
}

func parseLedgerEdgeStepCode(stepCode string) (uint32, uint32, bool) {
	code := strings.ToLower(strings.TrimSpace(stepCode))
	if !strings.HasPrefix(code, "edge.") || !strings.Contains(code, ".ledger.") {
		return 0, 0, false
	}
	var (
		from uint32
		to   uint32
		op   string
	)
	if _, err := fmt.Sscanf(code, "edge.%d_%d.ledger.%s", &from, &to, &op); err != nil {
		return 0, 0, false
	}
	if strings.TrimSpace(op) == "" {
		return 0, 0, false
	}
	return from, to, true
}

func isLedgerInternalRail(rail model.Rail) bool {
	return rail == discovery.RailLedger
}

func isLedgerExternalRail(rail model.Rail) bool {
	switch rail {
	case discovery.RailCrypto, discovery.RailProviderSettlement, discovery.RailCardPayout, discovery.RailFiatOnRamp:
		return true
	default:
		return false
	}
}

var _ sexec.LedgerExecutor = (*gatewayLedgerExecutor)(nil)
