package orchestrator

import (
	"context"
	"fmt"
	"strings"

	"github.com/tech/sendico/payments/orchestrator/storage/model"
	"github.com/tech/sendico/pkg/ledgerconv"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/model/account_role"
	"github.com/tech/sendico/pkg/payments/rail"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	connectorv1 "github.com/tech/sendico/pkg/proto/connector/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/v2/bson"
	"go.uber.org/zap"
)

func (p *paymentExecutor) postLedgerDebit(ctx context.Context, payment *model.Payment, amount *moneyv1.Money, charges []*ledgerv1.PostingLine, idempotencyKey string, idx int, action model.RailOperation, 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(ctx, payment, amount, charges, idempotencyKey, idx, action, 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
	}
	p.logger.Info("Ledger debit posted",
		zap.String("payment_ref", paymentRef),
		zap.Int("step_index", idx),
		zap.String("action", string(action)),
		zap.String("entry_ref", strings.TrimSpace(ref)))
	return ref, nil
}

func (p *paymentExecutor) postLedgerCredit(ctx context.Context, payment *model.Payment, amount *moneyv1.Money, idempotencyKey string, idx int, action model.RailOperation, 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(ctx, payment, amount, nil, idempotencyKey, idx, action, 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
	}
	p.logger.Info("Ledger credit posted",
		zap.String("payment_ref", paymentRef),
		zap.Int("step_index", idx),
		zap.String("action", string(action)),
		zap.String("entry_ref", strings.TrimSpace(ref)))
	return ref, nil
}

func (p *paymentExecutor) postLedgerMove(ctx context.Context, payment *model.Payment, step *model.PaymentStep, 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", "move"), zap.String("payment_ref", paymentRef))
		return "", merrors.Internal("ledger_client_unavailable")
	}
	if payment == nil {
		return "", merrors.InvalidArgument("ledger: payment is required")
	}
	if payment.OrganizationRef == bson.NilObjectID {
		return "", merrors.InvalidArgument("ledger: organization_ref is required")
	}
	if step == nil {
		return "", merrors.InvalidArgument("ledger: step is required")
	}
	if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
		return "", merrors.InvalidArgument("ledger: amount is required")
	}
	fromRole, toRole, err := ledgerMoveRoles(step)
	if err != nil {
		return "", err
	}
	currency := strings.TrimSpace(amount.GetCurrency())
	fromAccount, err := p.resolveAccount(ctx, payment.OrganizationRef, currency, model.RailLedger, fromRole)
	if err != nil {
		return "", err
	}
	toAccount, err := p.resolveAccount(ctx, payment.OrganizationRef, currency, model.RailLedger, toRole)
	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(fromAccount),
		ToLedgerAccountRef:   strings.TrimSpace(toAccount),
		Money:                cloneProtoMoney(amount),
		Description:          paymentDescription(payment),
		Metadata:             cloneMetadata(payment.Metadata),
		FromRole:             ledgerRoleFromAccountRole(fromRole),
		ToRole:               ledgerRoleFromAccountRole(toRole),
	})
	if err != nil {
		p.logger.Warn("Ledger move failed",
			zap.String("payment_ref", paymentRef),
			zap.Int("step_index", idx),
			zap.String("from_role", string(fromRole)),
			zap.String("to_role", string(toRole)),
			zap.String("from_account", strings.TrimSpace(fromAccount)),
			zap.String("to_account", strings.TrimSpace(toAccount)),
			zap.String("amount", strings.TrimSpace(amount.GetAmount())),
			zap.String("currency", currency),
			zap.Error(err))
		return "", err
	}
	entryRef := strings.TrimSpace(resp.GetJournalEntryRef())
	p.logger.Info("Ledger move posted",
		zap.String("payment_ref", paymentRef),
		zap.Int("step_index", idx),
		zap.String("entry_ref", entryRef),
		zap.String("from_role", string(fromRole)),
		zap.String("to_role", string(toRole)),
		zap.String("from_account", strings.TrimSpace(fromAccount)),
		zap.String("to_account", strings.TrimSpace(toAccount)),
		zap.String("amount", strings.TrimSpace(amount.GetAmount())),
		zap.String("currency", currency))
	return entryRef, nil
}

func (p *paymentExecutor) ledgerTxForAction(ctx context.Context, 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 == bson.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 := ""
	operation := ""

	switch action {
	case model.RailOperationDebit, model.RailOperationExternalDebit:
		fromRail = model.RailLedger
		toRail = ledgerStepToRail(payment.PaymentPlan, idx, destRail)
		accountRef, contraRef, err = ledgerDebitAccount(payment)
		if err != nil {
			accountRef, contraRef, err = p.resolveLedgerAccountRef(ctx, payment, amount, action)
		}
		if err == nil {
			if blockRef := ledgerBlockAccountIfConfirmed(payment); blockRef != "" {
				accountRef = blockRef
				contraRef = ""
			}
		}
		if action == model.RailOperationExternalDebit {
			operation = "external.debit"
		}
	case model.RailOperationCredit, model.RailOperationExternalCredit:
		fromRail = ledgerStepFromRail(payment.PaymentPlan, idx, sourceRail)
		toRail = model.RailLedger
		accountRef, contraRef, err = ledgerCreditAccount(payment)
		if err != nil {
			accountRef, contraRef, err = p.resolveLedgerAccountRef(ctx, payment, amount, action)
		}
		externalRef = ledgerExternalReference(payment.ExecutionPlan, idx)
		if action == model.RailOperationExternalCredit {
			operation = "external.credit"
		}
	default:
		return rail.LedgerTx{}, merrors.InvalidArgument("ledger: unsupported action")
	}
	if err != nil {
		return rail.LedgerTx{}, err
	}
	isDebit := action == model.RailOperationDebit || action == model.RailOperationExternalDebit
	isCredit := action == model.RailOperationCredit || action == model.RailOperationExternalCredit
	if isCredit && strings.TrimSpace(accountRef) != "" {
		setLedgerAccountAttributes(payment, accountRef)
	}
	if isDebit && toRail == model.RailLedger {
		toRail = model.RailUnspecified
	}
	if isCredit && 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 isDebit {
		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,
		Operation:              operation,
		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 ledgerMoveRoles(step *model.PaymentStep) (account_role.AccountRole, account_role.AccountRole, error) {
	if step == nil {
		return "", "", merrors.InvalidArgument("ledger: step is required")
	}
	if step.FromRole == nil || strings.TrimSpace(string(*step.FromRole)) == "" {
		return "", "", merrors.InvalidArgument("ledger: from_role is required")
	}
	if step.ToRole == nil || strings.TrimSpace(string(*step.ToRole)) == "" {
		return "", "", merrors.InvalidArgument("ledger: to_role is required")
	}
	from := strings.ToLower(strings.TrimSpace(string(*step.FromRole)))
	to := strings.ToLower(strings.TrimSpace(string(*step.ToRole)))
	if from == "" || to == "" || strings.EqualFold(from, to) {
		return "", "", merrors.InvalidArgument("ledger: from_role and to_role must differ")
	}
	return account_role.AccountRole(from), account_role.AccountRole(to), nil
}

func ledgerRoleFromAccountRole(role account_role.AccountRole) ledgerv1.AccountRole {
	if strings.TrimSpace(string(role)) == "" {
		return ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED
	}
	if parsed, ok := ledgerconv.ParseAccountRole(string(role)); ok {
		return parsed
	}
	return ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED
}

func (p *paymentExecutor) resolveAccount(ctx context.Context, orgRef bson.ObjectID, asset string, rail model.Rail, role account_role.AccountRole) (string, error) {
	switch rail {
	case model.RailLedger:
		return p.resolveLedgerAccountByRole(ctx, orgRef, asset, role)
	default:
		return "", nil
	}
}

func (p *paymentExecutor) resolveLedgerAccountByRole(ctx context.Context, orgRef bson.ObjectID, asset string, role account_role.AccountRole) (string, error) {
	if p == nil || p.deps == nil || p.deps.ledger.client == nil {
		return "", merrors.Internal("ledger_client_unavailable")
	}
	if orgRef == bson.NilObjectID {
		return "", merrors.InvalidArgument("ledger: organization_ref is required")
	}
	currency := strings.TrimSpace(asset)
	if currency == "" {
		return "", merrors.InvalidArgument("ledger: asset is required")
	}
	if strings.TrimSpace(string(role)) == "" {
		return "", merrors.InvalidArgument("ledger: role is required")
	}

	resp, err := p.deps.ledger.client.ListConnectorAccounts(ctx, &connectorv1.ListAccountsRequest{
		OrganizationRef: orgRef.Hex(),
		Kind:            connectorv1.AccountKind_LEDGER_ACCOUNT,
		Asset:           currency,
	})
	if err != nil {
		return "", err
	}
	expectedRole := strings.ToLower(strings.TrimSpace(string(role)))
	for _, account := range resp.GetAccounts() {
		if account == nil {
			continue
		}
		if account.GetKind() != connectorv1.AccountKind_LEDGER_ACCOUNT {
			continue
		}
		if asset := strings.TrimSpace(account.GetAsset()); asset == "" || !strings.EqualFold(asset, currency) {
			continue
		}
		if strings.TrimSpace(account.GetOwnerRef()) != "" {
			continue
		}
		accRole := strings.ToLower(strings.TrimSpace(string(connectorAccountRole(account))))
		if accRole == "" || !strings.EqualFold(accRole, expectedRole) {
			continue
		}
		if ref := account.GetRef(); ref != nil {
			if accountID := strings.TrimSpace(ref.GetAccountId()); accountID != "" {
				return accountID, nil
			}
		}
	}
	return "", merrors.InvalidArgument("ledger: account role not found")
}

func (p *paymentExecutor) resolveLedgerAccountRef(ctx context.Context, payment *model.Payment, amount *moneyv1.Money, action model.RailOperation) (string, string, error) {
	if payment == nil {
		return "", "", merrors.InvalidArgument("ledger: payment is required")
	}
	if amount == nil || strings.TrimSpace(amount.GetCurrency()) == "" {
		return "", "", merrors.InvalidArgument("ledger: amount is required")
	}
	switch action {
	case model.RailOperationCredit, model.RailOperationExternalCredit:
		if account, _, err := ledgerDebitAccount(payment); err == nil && strings.TrimSpace(account) != "" {
			setLedgerAccountAttributes(payment, account)
			return account, "", nil
		}
	case model.RailOperationDebit, model.RailOperationExternalDebit:
		if account, _, err := ledgerCreditAccount(payment); err == nil && strings.TrimSpace(account) != "" {
			setLedgerAccountAttributes(payment, account)
			return account, "", nil
		}
	}
	account, err := p.resolveOrgOwnedLedgerAccount(ctx, payment, amount)
	if err != nil {
		return "", "", err
	}
	setLedgerAccountAttributes(payment, account)
	return account, "", nil
}

func (p *paymentExecutor) resolveOrgOwnedLedgerAccount(ctx context.Context, payment *model.Payment, amount *moneyv1.Money) (string, error) {
	if payment == nil {
		return "", merrors.InvalidArgument("ledger: payment is required")
	}
	if payment.OrganizationRef == bson.NilObjectID {
		return "", merrors.InvalidArgument("ledger: organization_ref is required")
	}
	if amount == nil || strings.TrimSpace(amount.GetCurrency()) == "" {
		return "", merrors.InvalidArgument("ledger: amount is required")
	}
	if p == nil || p.deps == nil || p.deps.ledger.client == nil {
		return "", merrors.Internal("ledger_client_unavailable")
	}

	currency := strings.TrimSpace(amount.GetCurrency())
	resp, err := p.deps.ledger.client.ListConnectorAccounts(ctx, &connectorv1.ListAccountsRequest{
		OrganizationRef: payment.OrganizationRef.Hex(),
		Kind:            connectorv1.AccountKind_LEDGER_ACCOUNT,
		Asset:           currency,
	})
	if err != nil {
		return "", err
	}
	for _, account := range resp.GetAccounts() {
		if account == nil {
			continue
		}
		if account.GetKind() != connectorv1.AccountKind_LEDGER_ACCOUNT {
			continue
		}
		asset := strings.TrimSpace(account.GetAsset())
		if asset == "" || !strings.EqualFold(asset, currency) {
			continue
		}
		if strings.TrimSpace(account.GetOwnerRef()) != "" {
			continue
		}
		if connectorAccountIsSettlement(account) {
			continue
		}
		if ref := account.GetRef(); ref != nil {
			if accountID := strings.TrimSpace(ref.GetAccountId()); accountID != "" {
				return accountID, nil
			}
		}
	}
	return "", merrors.InvalidArgument("ledger: org-owned account not found")
}

func connectorAccountIsSettlement(account *connectorv1.Account) bool {
	return connectorAccountRole(account) == account_role.AccountRoleSettlement
}

func connectorAccountRole(account *connectorv1.Account) account_role.AccountRole {
	if account == nil || account.GetProviderDetails() == nil {
		return ""
	}
	details := account.GetProviderDetails().AsMap()
	if value := strings.TrimSpace(fmt.Sprint(details["role"])); value != "" {
		if role, ok := account_role.Parse(value); ok {
			return role
		}
	}
	switch v := details["is_settlement"].(type) {
	case bool:
		if v {
			return account_role.AccountRoleSettlement
		}
	case string:
		if strings.EqualFold(strings.TrimSpace(v), "true") {
			return account_role.AccountRoleSettlement
		}
	}
	return ""
}

func setLedgerAccountAttributes(payment *model.Payment, accountRef string) {
	if payment == nil || strings.TrimSpace(accountRef) == "" {
		return
	}
	if payment.Intent.Attributes == nil {
		payment.Intent.Attributes = map[string]string{}
	}
	if attributeLookup(payment.Intent.Attributes, "ledger_debit_account_ref", "ledgerDebitAccountRef") == "" {
		payment.Intent.Attributes["ledger_debit_account_ref"] = accountRef
	}
	if attributeLookup(payment.Intent.Attributes, "ledger_credit_account_ref", "ledgerCreditAccountRef") == "" {
		payment.Intent.Attributes["ledger_credit_account_ref"] = accountRef
	}
}

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 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 ""
}
