#nullable enable
namespace Baya.Domain.Entities.Payments;

/// <summary>
/// Builds the canonical, <b>balanced</b> ledger posting groups so the posting discipline lives in one
/// unit-testable place instead of a handler. Every group returned satisfies Σ(debit) = Σ(credit).
/// </summary>
public static class LedgerPosting
{
    /// <summary>
    /// The <b>card-capture group</b>: <c>DEBIT escrow_held gross = CREDIT platform_revenue commission +
    /// nurse_payable payout</c>, all under one fresh <see cref="LedgerEntry.TransactionGroupId"/>. The three
    /// amounts come <b>frozen from the booking</b> (b9) — never recomputed here — and must reconcile
    /// (<c>gross = commission + payout</c>); this method throws if they do not, so an unbalanced group can
    /// never be persisted.
    /// </summary>
    public static IReadOnlyList<LedgerEntry> CardCapture(
        long bookingId,
        long nurseId,
        long grossIrr,
        long commissionIrr,
        long payoutIrr,
        long paymentTransactionId,
        DateTime createdAt)
    {
        if (grossIrr != commissionIrr + payoutIrr)
            throw new InvalidOperationException(
                $"Card-capture group would not balance: gross {grossIrr} != commission {commissionIrr} + payout {payoutIrr}.");

        var group = Guid.NewGuid();
        return
        [
            Leg(group, LedgerAccountType.EscrowHeld, LedgerDirection.Debit, grossIrr, null, bookingId, LedgerSourceRefType.PaymentTransaction, paymentTransactionId, createdAt),
            Leg(group, LedgerAccountType.PlatformRevenue, LedgerDirection.Credit, commissionIrr, null, bookingId, LedgerSourceRefType.PaymentTransaction, paymentTransactionId, createdAt),
            Leg(group, LedgerAccountType.NursePayable, LedgerDirection.Credit, payoutIrr, nurseId, bookingId, LedgerSourceRefType.PaymentTransaction, paymentTransactionId, createdAt)
        ];
    }

    /// <summary>
    /// The <b>BNPL settle group</b>: the card-capture legs <b>plus</b> the provider-fee leg, all under one fresh
    /// <see cref="LedgerEntry.TransactionGroupId"/>, so <c>escrow_held</c> reflects the <b>net</b> cash actually
    /// received (<c>order − commission</c>), not the gross:
    /// <code>
    /// DEBIT  escrow_held        order (= gross)
    ///   CREDIT platform_revenue   commission
    ///   CREDIT nurse_payable      payout
    /// DEBIT  bnpl_fee_expense   bnpl_commission
    ///   CREDIT escrow_held        bnpl_commission
    /// </code>
    /// The three booking amounts come <b>frozen from the booking</b> (never recomputed) and must reconcile
    /// (<c>gross = commission + payout</c>); the <b>nurse's payout is invariant to payment method</b> — the BNPL
    /// commission is a platform expense and never touches it. Throws if the group would not balance, so an
    /// unbalanced settle can never be persisted. Posted once (the state guard + webhook dedup make a replay a no-op).
    /// </summary>
    public static IReadOnlyList<LedgerEntry> BnplSettle(
        long bookingId,
        long nurseId,
        long grossIrr,
        long commissionIrr,
        long payoutIrr,
        long bnplCommissionIrr,
        long bnplTransactionId,
        DateTime createdAt)
    {
        if (grossIrr != commissionIrr + payoutIrr)
            throw new InvalidOperationException(
                $"BNPL settle group would not balance: gross {grossIrr} != commission {commissionIrr} + payout {payoutIrr}.");
        if (bnplCommissionIrr < 0)
            throw new InvalidOperationException("BNPL provider commission must be non-negative.");

        var group = Guid.NewGuid();
        var legs = new List<LedgerEntry>
        {
            Leg(group, LedgerAccountType.EscrowHeld, LedgerDirection.Debit, grossIrr, null, bookingId, LedgerSourceRefType.BnplTransaction, bnplTransactionId, createdAt),
            Leg(group, LedgerAccountType.PlatformRevenue, LedgerDirection.Credit, commissionIrr, null, bookingId, LedgerSourceRefType.BnplTransaction, bnplTransactionId, createdAt),
            Leg(group, LedgerAccountType.NursePayable, LedgerDirection.Credit, payoutIrr, nurseId, bookingId, LedgerSourceRefType.BnplTransaction, bnplTransactionId, createdAt)
        };

        // The provider-fee leg: the merchant discount is a platform expense, so escrow reflects only the net cash.
        if (bnplCommissionIrr > 0)
        {
            legs.Add(Leg(group, LedgerAccountType.BnplFeeExpense, LedgerDirection.Debit, bnplCommissionIrr, null, bookingId, LedgerSourceRefType.BnplTransaction, bnplTransactionId, createdAt));
            legs.Add(Leg(group, LedgerAccountType.EscrowHeld, LedgerDirection.Credit, bnplCommissionIrr, null, bookingId, LedgerSourceRefType.BnplTransaction, bnplTransactionId, createdAt));
        }

        return legs;
    }

    /// <summary>
    /// The <b>pre-payout refund reversal</b> (the nurse has not been paid, the common case): <c>DEBIT
    /// platform_revenue fee + DEBIT nurse_payable payout / CREDIT refund_payable (sum)</c> under one group.
    /// Simply un-accrues what capture posted. The customer cash-back is cleared separately by
    /// <see cref="RefundPayableClearing"/> once the provider confirms it.
    /// </summary>
    public static IReadOnlyList<LedgerEntry> RefundReversalPrePayout(
        long bookingId,
        long nurseId,
        long platformFeeRefundedIrr,
        long nursePayoutRefundedIrr,
        long refundId,
        DateTime createdAt)
    {
        var group = Guid.NewGuid();
        var total = platformFeeRefundedIrr + nursePayoutRefundedIrr;
        var legs = new List<LedgerEntry>();

        if (platformFeeRefundedIrr > 0)
            legs.Add(Leg(group, LedgerAccountType.PlatformRevenue, LedgerDirection.Debit, platformFeeRefundedIrr, null, bookingId, LedgerSourceRefType.Refund, refundId, createdAt));
        if (nursePayoutRefundedIrr > 0)
            legs.Add(Leg(group, LedgerAccountType.NursePayable, LedgerDirection.Debit, nursePayoutRefundedIrr, nurseId, bookingId, LedgerSourceRefType.Refund, refundId, createdAt));

        legs.Add(Leg(group, LedgerAccountType.RefundPayable, LedgerDirection.Credit, total, null, bookingId, LedgerSourceRefType.Refund, refundId, createdAt));
        return legs;
    }

    /// <summary>
    /// The <b>post-payout clawback reversal</b> (the nurse was already paid — an irreversible IBAN transfer):
    /// identical to the pre-payout group except the payout leg debits <c>nurse_clawback_receivable</c> (money
    /// owed back by the nurse) instead of un-accruing <c>nurse_payable</c>: <c>DEBIT platform_revenue fee +
    /// DEBIT nurse_clawback_receivable payout / CREDIT refund_payable (sum)</c>. A <c>nurse_clawbacks</c> row
    /// tracks the workflow; b13 nets the receivable out of a later payout.
    /// </summary>
    public static IReadOnlyList<LedgerEntry> ClawbackReversalPostPayout(
        long bookingId,
        long nurseId,
        long platformFeeRefundedIrr,
        long nursePayoutRefundedIrr,
        long refundId,
        DateTime createdAt)
    {
        var group = Guid.NewGuid();
        var total = platformFeeRefundedIrr + nursePayoutRefundedIrr;
        var legs = new List<LedgerEntry>();

        if (platformFeeRefundedIrr > 0)
            legs.Add(Leg(group, LedgerAccountType.PlatformRevenue, LedgerDirection.Debit, platformFeeRefundedIrr, null, bookingId, LedgerSourceRefType.Refund, refundId, createdAt));
        if (nursePayoutRefundedIrr > 0)
            legs.Add(Leg(group, LedgerAccountType.NurseClawbackReceivable, LedgerDirection.Debit, nursePayoutRefundedIrr, nurseId, bookingId, LedgerSourceRefType.Refund, refundId, createdAt));

        legs.Add(Leg(group, LedgerAccountType.RefundPayable, LedgerDirection.Credit, total, null, bookingId, LedgerSourceRefType.Refund, refundId, createdAt));
        return legs;
    }

    /// <summary>
    /// Clears the customer cash-back once the provider confirms it (card immediately; BNPL/manual on
    /// reconciliation): <c>DEBIT refund_payable / CREDIT escrow_held</c> for the refunded total. Path- and
    /// channel-independent — the same second leg follows either the pre-payout or the clawback reversal.
    /// </summary>
    public static IReadOnlyList<LedgerEntry> RefundPayableClearing(
        long bookingId,
        long totalRefundedIrr,
        long refundId,
        DateTime createdAt)
    {
        var group = Guid.NewGuid();
        return
        [
            Leg(group, LedgerAccountType.RefundPayable, LedgerDirection.Debit, totalRefundedIrr, null, bookingId, LedgerSourceRefType.Refund, refundId, createdAt),
            Leg(group, LedgerAccountType.EscrowHeld, LedgerDirection.Credit, totalRefundedIrr, null, bookingId, LedgerSourceRefType.Refund, refundId, createdAt)
        ];
    }

    /// <summary>
    /// The <b>payout group</b> (b13): <c>DEBIT nurse_payable / CREDIT escrow_held</c> for the amount actually
    /// transferred to the nurse, under one fresh <see cref="LedgerEntry.TransactionGroupId"/>. Draining the
    /// <c>nurse_payable</c> accrual to a real bank transfer is the one irreversible money-out step; the balance
    /// (the signed sum over <c>nurse_payable</c> legs) drops by exactly what was paid. Posted once per payout —
    /// the payout status machine + the <c>nurse_payout_booking_links</c> UNIQUE make a retried execute a no-op.
    /// The clawback netted into the payout is <b>not</b> a leg here: the receivable was already booked by b11 and
    /// is cleared by marking the <c>nurse_clawbacks</c> row <c>recovered</c> (the net amount is simply lower).
    /// </summary>
    public static IReadOnlyList<LedgerEntry> NursePayout(
        long nurseId,
        long amountIrr,
        long payoutId,
        DateTime createdAt)
    {
        if (amountIrr <= 0)
            throw new InvalidOperationException("A payout ledger group requires a positive amount.");

        var group = Guid.NewGuid();
        return
        [
            Leg(group, LedgerAccountType.NursePayable, LedgerDirection.Debit, amountIrr, nurseId, null, LedgerSourceRefType.NursePayout, payoutId, createdAt),
            Leg(group, LedgerAccountType.EscrowHeld, LedgerDirection.Credit, amountIrr, null, null, LedgerSourceRefType.NursePayout, payoutId, createdAt)
        ];
    }

    /// <summary>
    /// The clawback <b>recovery</b> netting (b13): when a payout withholds a nurse's <c>pending</c> clawback, the
    /// withheld earnings clear the receivable — <c>DEBIT nurse_payable / CREDIT nurse_clawback_receivable</c> for
    /// the recovered amount, under one group. Together with the payout group (which debits <c>nurse_payable</c>
    /// by the paid net), this drains the nurse's <c>nurse_payable</c> by the full gross and zeroes the receivable,
    /// so the derived balances reconcile. Posted once per recovered clawback (the <c>recovered</c> status makes a
    /// retry a no-op).
    /// </summary>
    public static IReadOnlyList<LedgerEntry> ClawbackRecovery(
        long bookingId,
        long nurseId,
        long amountIrr,
        long clawbackId,
        DateTime createdAt)
    {
        if (amountIrr <= 0)
            throw new InvalidOperationException("A clawback-recovery group requires a positive amount.");

        var group = Guid.NewGuid();
        return
        [
            Leg(group, LedgerAccountType.NursePayable, LedgerDirection.Debit, amountIrr, nurseId, bookingId, LedgerSourceRefType.Clawback, clawbackId, createdAt),
            Leg(group, LedgerAccountType.NurseClawbackReceivable, LedgerDirection.Credit, amountIrr, nurseId, bookingId, LedgerSourceRefType.Clawback, clawbackId, createdAt)
        ];
    }

    /// <summary>
    /// The clawback <b>write-off</b> correction: <c>DEBIT bad_debt / CREDIT nurse_clawback_receivable</c> for
    /// the amount, when an admin declares the receivable uncollectable. A new balancing group — never an edit.
    /// </summary>
    public static IReadOnlyList<LedgerEntry> ClawbackWriteOff(
        long bookingId,
        long nurseId,
        long amountIrr,
        long clawbackId,
        DateTime createdAt)
    {
        var group = Guid.NewGuid();
        return
        [
            Leg(group, LedgerAccountType.BadDebt, LedgerDirection.Debit, amountIrr, null, bookingId, LedgerSourceRefType.Clawback, clawbackId, createdAt),
            Leg(group, LedgerAccountType.NurseClawbackReceivable, LedgerDirection.Credit, amountIrr, nurseId, bookingId, LedgerSourceRefType.Clawback, clawbackId, createdAt)
        ];
    }

    private static LedgerEntry Leg(
        Guid group, string account, string direction, long amount, long? nurse,
        long? bookingId, string sourceType, long sourceId, DateTime createdAt) => new()
    {
        TransactionGroupId = group,
        AccountType = account,
        Direction = direction,
        AmountIrr = amount,
        NurseId = nurse,
        BookingId = bookingId,
        SourceRefType = sourceType,
        SourceRefId = sourceId,
        CreatedAt = createdAt
    };
}
