package orchestrator

import (
	"context"
	"strings"

	"github.com/tech/sendico/payments/orchestrator/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/payments/rail"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.uber.org/zap"
)

func (p *paymentExecutor) postLedgerDebit(ctx context.Context, payment *model.Payment, amount *moneyv1.Money, charges []*ledgerv1.PostingLine, idempotencyKey string, idx int, quote *orchestratorv1.PaymentQuote) (string, error) {
	paymentRef := ""
	if payment != nil {
		paymentRef = strings.TrimSpace(payment.PaymentRef)
	}
	if p.deps.ledger.internal == nil {
		p.logger.Error("Ledger client unavailable", zap.String("action", "debit"), zap.String("payment_ref", paymentRef))
		return "", merrors.Internal("ledger_client_unavailable")
	}
	tx, err := p.ledgerTxForAction(payment, amount, charges, idempotencyKey, idx, model.RailOperationDebit, quote)
	if err != nil {
		p.logger.Warn("Ledger debit preparation failed", zap.String("payment_ref", paymentRef), zap.Int("step_index", idx), zap.Error(err))
		return "", err
	}
	ref, err := p.deps.ledger.internal.CreateTransaction(ctx, tx)
	if err != nil {
		p.logger.Warn("Ledger debit failed", zap.String("payment_ref", paymentRef), zap.Int("step_index", idx), zap.Error(err))
		return "", err
	}
	return ref, nil
}

func (p *paymentExecutor) postLedgerCredit(ctx context.Context, payment *model.Payment, amount *moneyv1.Money, idempotencyKey string, idx int, quote *orchestratorv1.PaymentQuote) (string, error) {
	paymentRef := ""
	if payment != nil {
		paymentRef = strings.TrimSpace(payment.PaymentRef)
	}
	if p.deps.ledger.internal == nil {
		p.logger.Error("Ledger client unavailable", zap.String("action", "credit"), zap.String("payment_ref", paymentRef))
		return "", merrors.Internal("ledger_client_unavailable")
	}
	tx, err := p.ledgerTxForAction(payment, amount, nil, idempotencyKey, idx, model.RailOperationCredit, quote)
	if err != nil {
		p.logger.Warn("Ledger credit preparation failed", zap.String("payment_ref", paymentRef), zap.Int("step_index", idx), zap.Error(err))
		return "", err
	}
	ref, err := p.deps.ledger.internal.CreateTransaction(ctx, tx)
	if err != nil {
		p.logger.Warn("Ledger credit failed", zap.String("payment_ref", paymentRef), zap.Int("step_index", idx), zap.Error(err))
		return "", err
	}
	return ref, nil
}

func (p *paymentExecutor) postLedgerBlock(ctx context.Context, payment *model.Payment, amount *moneyv1.Money, idempotencyKey string, idx int) (string, error) {
	paymentRef := ""
	if payment != nil {
		paymentRef = strings.TrimSpace(payment.PaymentRef)
	}
	if p.deps.ledger.internal == nil {
		p.logger.Error("Ledger client unavailable", zap.String("action", "block"), zap.String("payment_ref", paymentRef))
		return "", merrors.Internal("ledger_client_unavailable")
	}
	if payment == nil {
		return "", merrors.InvalidArgument("ledger: payment is required")
	}
	if payment.OrganizationRef == primitive.NilObjectID {
		return "", merrors.InvalidArgument("ledger: organization_ref is required")
	}
	if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
		return "", merrors.InvalidArgument("ledger: amount is required")
	}
	sourceAccount, err := ledgerDebitAccountRef(payment)
	if err != nil {
		return "", err
	}
	blockAccount, err := ledgerBlockAccount(payment)
	if err != nil {
		return "", err
	}
	resp, err := p.deps.ledger.internal.TransferInternal(ctx, &ledgerv1.TransferRequest{
		IdempotencyKey:       strings.TrimSpace(idempotencyKey),
		OrganizationRef:      payment.OrganizationRef.Hex(),
		FromLedgerAccountRef: strings.TrimSpace(sourceAccount),
		ToLedgerAccountRef:   strings.TrimSpace(blockAccount),
		Money:                cloneProtoMoney(amount),
		Description:          paymentDescription(payment),
		Metadata:             cloneMetadata(payment.Metadata),
	})
	if err != nil {
		p.logger.Warn("Ledger block failed",
			zap.String("payment_ref", paymentRef),
			zap.Int("step_index", idx),
			zap.String("from_account", strings.TrimSpace(sourceAccount)),
			zap.String("to_account", strings.TrimSpace(blockAccount)),
			zap.String("amount", strings.TrimSpace(amount.GetAmount())),
			zap.String("currency", strings.TrimSpace(amount.GetCurrency())),
			zap.Error(err))
		return "", err
	}
	entryRef := strings.TrimSpace(resp.GetJournalEntryRef())
	p.logger.Info("Ledger block posted",
		zap.String("payment_ref", paymentRef),
		zap.Int("step_index", idx),
		zap.String("entry_ref", entryRef),
		zap.String("from_account", strings.TrimSpace(sourceAccount)),
		zap.String("to_account", strings.TrimSpace(blockAccount)),
		zap.String("amount", strings.TrimSpace(amount.GetAmount())),
		zap.String("currency", strings.TrimSpace(amount.GetCurrency())))
	return entryRef, nil
}

func (p *paymentExecutor) postLedgerRelease(ctx context.Context, payment *model.Payment, amount *moneyv1.Money, idempotencyKey string, idx int) (string, error) {
	paymentRef := ""
	if payment != nil {
		paymentRef = strings.TrimSpace(payment.PaymentRef)
	}
	if p.deps.ledger.internal == nil {
		p.logger.Error("Ledger client unavailable", zap.String("action", "release"), zap.String("payment_ref", paymentRef))
		return "", merrors.Internal("ledger_client_unavailable")
	}
	if payment == nil {
		return "", merrors.InvalidArgument("ledger: payment is required")
	}
	if payment.OrganizationRef == primitive.NilObjectID {
		return "", merrors.InvalidArgument("ledger: organization_ref is required")
	}
	if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
		return "", merrors.InvalidArgument("ledger: amount is required")
	}
	sourceAccount, err := ledgerDebitAccountRef(payment)
	if err != nil {
		return "", err
	}
	blockAccount, err := ledgerBlockAccount(payment)
	if err != nil {
		return "", err
	}
	resp, err := p.deps.ledger.internal.TransferInternal(ctx, &ledgerv1.TransferRequest{
		IdempotencyKey:       strings.TrimSpace(idempotencyKey),
		OrganizationRef:      payment.OrganizationRef.Hex(),
		FromLedgerAccountRef: strings.TrimSpace(blockAccount),
		ToLedgerAccountRef:   strings.TrimSpace(sourceAccount),
		Money:                cloneProtoMoney(amount),
		Description:          paymentDescription(payment),
		Metadata:             cloneMetadata(payment.Metadata),
	})
	if err != nil {
		p.logger.Warn("Ledger release failed",
			zap.String("payment_ref", paymentRef),
			zap.Int("step_index", idx),
			zap.String("from_account", strings.TrimSpace(blockAccount)),
			zap.String("to_account", strings.TrimSpace(sourceAccount)),
			zap.String("amount", strings.TrimSpace(amount.GetAmount())),
			zap.String("currency", strings.TrimSpace(amount.GetCurrency())),
			zap.Error(err))
		return "", err
	}
	entryRef := strings.TrimSpace(resp.GetJournalEntryRef())
	p.logger.Info("Ledger release posted",
		zap.String("payment_ref", paymentRef),
		zap.Int("step_index", idx),
		zap.String("entry_ref", entryRef),
		zap.String("from_account", strings.TrimSpace(blockAccount)),
		zap.String("to_account", strings.TrimSpace(sourceAccount)),
		zap.String("amount", strings.TrimSpace(amount.GetAmount())),
		zap.String("currency", strings.TrimSpace(amount.GetCurrency())))
	return entryRef, nil
}

func (p *paymentExecutor) ledgerTxForAction(payment *model.Payment, amount *moneyv1.Money, charges []*ledgerv1.PostingLine, idempotencyKey string, idx int, action model.RailOperation, quote *orchestratorv1.PaymentQuote) (rail.LedgerTx, error) {
	if payment == nil {
		return rail.LedgerTx{}, merrors.InvalidArgument("ledger: payment is required")
	}
	if payment.OrganizationRef == primitive.NilObjectID {
		return rail.LedgerTx{}, merrors.InvalidArgument("ledger: organization_ref is required")
	}
	if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
		return rail.LedgerTx{}, merrors.InvalidArgument("ledger: amount is required")
	}

	sourceRail, _, err := railFromEndpoint(payment.Intent.Source, payment.Intent.Attributes, true)
	if err != nil {
		sourceRail = model.RailUnspecified
	}
	destRail, _, err := railFromEndpoint(payment.Intent.Destination, payment.Intent.Attributes, false)
	if err != nil {
		destRail = model.RailUnspecified
	}

	fromRail := model.RailUnspecified
	toRail := model.RailUnspecified
	accountRef := ""
	contraRef := ""
	externalRef := ""

	switch action {
	case model.RailOperationDebit:
		fromRail = model.RailLedger
		toRail = ledgerStepToRail(payment.PaymentPlan, idx, destRail)
		accountRef, contraRef, err = ledgerDebitAccount(payment)
		if err == nil {
			if blockRef := ledgerBlockAccountIfConfirmed(payment); blockRef != "" {
				accountRef = blockRef
				contraRef = ""
			}
		}
	case model.RailOperationCredit:
		fromRail = ledgerStepFromRail(payment.PaymentPlan, idx, sourceRail)
		toRail = model.RailLedger
		accountRef, contraRef, err = ledgerCreditAccount(payment)
		externalRef = ledgerExternalReference(payment.ExecutionPlan, idx)
	default:
		return rail.LedgerTx{}, merrors.InvalidArgument("ledger: unsupported action")
	}
	if err != nil {
		return rail.LedgerTx{}, err
	}
	if action == model.RailOperationDebit && toRail == model.RailLedger {
		toRail = model.RailUnspecified
	}
	if action == model.RailOperationCredit && fromRail == model.RailLedger {
		fromRail = model.RailUnspecified
	}

	planID := payment.PaymentRef
	if payment.PaymentPlan != nil && strings.TrimSpace(payment.PaymentPlan.ID) != "" {
		planID = strings.TrimSpace(payment.PaymentPlan.ID)
	}

	feeAmount := ""
	if action == model.RailOperationDebit {
		if feeMoney := resolveFeeAmount(payment, quote); feeMoney != nil {
			feeAmount = strings.TrimSpace(feeMoney.GetAmount())
		}
	}

	fxRate := ""
	if quote != nil && quote.GetFxQuote() != nil && quote.GetFxQuote().GetPrice() != nil {
		fxRate = strings.TrimSpace(quote.GetFxQuote().GetPrice().GetValue())
	}

	return rail.LedgerTx{
		PaymentPlanID:          planID,
		Currency:               strings.TrimSpace(amount.GetCurrency()),
		Amount:                 strings.TrimSpace(amount.GetAmount()),
		FeeAmount:              feeAmount,
		FromRail:               ledgerRailValue(fromRail),
		ToRail:                 ledgerRailValue(toRail),
		ExternalReferenceID:    externalRef,
		FXRateUsed:             fxRate,
		IdempotencyKey:         strings.TrimSpace(idempotencyKey),
		CreatedAt:              planTimestamp(payment),
		OrganizationRef:        payment.OrganizationRef.Hex(),
		LedgerAccountRef:       strings.TrimSpace(accountRef),
		ContraLedgerAccountRef: strings.TrimSpace(contraRef),
		Description:            paymentDescription(payment),
		Charges:                charges,
		Metadata:               cloneMetadata(payment.Metadata),
	}, nil
}

func ledgerRailValue(railValue model.Rail) string {
	if railValue == model.RailUnspecified || strings.TrimSpace(string(railValue)) == "" {
		return ""
	}
	return string(railValue)
}

func ledgerStepFromRail(plan *model.PaymentPlan, idx int, fallback model.Rail) model.Rail {
	if plan == nil || idx <= 0 {
		return fallback
	}
	for i := idx - 1; i >= 0; i-- {
		step := plan.Steps[i]
		if step == nil {
			continue
		}
		if step.Rail != model.RailLedger && step.Rail != model.RailUnspecified {
			return step.Rail
		}
	}
	return fallback
}

func ledgerStepToRail(plan *model.PaymentPlan, idx int, fallback model.Rail) model.Rail {
	if plan == nil || idx < 0 {
		return fallback
	}
	for i := idx + 1; i < len(plan.Steps); i++ {
		step := plan.Steps[i]
		if step == nil {
			continue
		}
		if step.Rail != model.RailLedger && step.Rail != model.RailUnspecified {
			return step.Rail
		}
	}
	return fallback
}

func ledgerExternalReference(plan *model.ExecutionPlan, idx int) string {
	if plan == nil || idx <= 0 {
		return ""
	}
	for i := idx - 1; i >= 0; i-- {
		step := plan.Steps[i]
		if step == nil {
			continue
		}
		if ref := strings.TrimSpace(step.TransferRef); ref != "" {
			return ref
		}
	}
	return ""
}

func ledgerDebitAccount(payment *model.Payment) (string, string, error) {
	if payment == nil {
		return "", "", merrors.InvalidArgument("ledger: payment is required")
	}
	intent := payment.Intent
	if intent.Source.Ledger != nil && strings.TrimSpace(intent.Source.Ledger.LedgerAccountRef) != "" {
		return strings.TrimSpace(intent.Source.Ledger.LedgerAccountRef), strings.TrimSpace(intent.Source.Ledger.ContraLedgerAccountRef), nil
	}
	if ref := attributeLookup(intent.Attributes, "ledger_debit_account_ref", "ledgerDebitAccountRef"); ref != "" {
		return ref, strings.TrimSpace(attributeLookup(intent.Attributes, "ledger_debit_contra_account_ref", "ledgerDebitContraAccountRef")), nil
	}
	return "", "", merrors.InvalidArgument("ledger: source account is required")
}

func ledgerDebitAccountRef(payment *model.Payment) (string, error) {
	account, _, err := ledgerDebitAccount(payment)
	return account, err
}

func ledgerBlockAccount(payment *model.Payment) (string, error) {
	if payment == nil {
		return "", merrors.InvalidArgument("ledger: payment is required")
	}
	intent := payment.Intent
	if intent.Source.Ledger != nil {
		if ref := strings.TrimSpace(intent.Source.Ledger.ContraLedgerAccountRef); ref != "" {
			return ref, nil
		}
	}
	if ref := attributeLookup(intent.Attributes,
		"ledger_block_account_ref",
		"ledgerBlockAccountRef",
		"ledger_hold_account_ref",
		"ledgerHoldAccountRef",
		"ledger_debit_contra_account_ref",
		"ledgerDebitContraAccountRef",
	); ref != "" {
		return ref, nil
	}
	return "", merrors.InvalidArgument("ledger: block account is required")
}

func ledgerBlockAccountIfConfirmed(payment *model.Payment) string {
	if payment == nil {
		return ""
	}
	if !blockStepConfirmed(payment.PaymentPlan, payment.ExecutionPlan) {
		return ""
	}
	ref, err := ledgerBlockAccount(payment)
	if err != nil {
		return ""
	}
	return ref
}

func ledgerCreditAccount(payment *model.Payment) (string, string, error) {
	if payment == nil {
		return "", "", merrors.InvalidArgument("ledger: payment is required")
	}
	intent := payment.Intent
	if intent.Destination.Ledger != nil && strings.TrimSpace(intent.Destination.Ledger.LedgerAccountRef) != "" {
		return strings.TrimSpace(intent.Destination.Ledger.LedgerAccountRef), strings.TrimSpace(intent.Destination.Ledger.ContraLedgerAccountRef), nil
	}
	if ref := attributeLookup(intent.Attributes, "ledger_credit_account_ref", "ledgerCreditAccountRef"); ref != "" {
		return ref, strings.TrimSpace(attributeLookup(intent.Attributes, "ledger_credit_contra_account_ref", "ledgerCreditContraAccountRef")), nil
	}
	return "", "", merrors.InvalidArgument("ledger: destination account is required")
}

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