#nullable enable
using Baya.Domain.Common;

namespace Baya.Domain.Entities.Refunds;

/// <summary>
/// An admin-initiated, ticket-linked reversal of a captured booking payment. Refunds are <b>1:N</b> per
/// <c>payment_transaction</c> — partials exist (a shortened visit) — so the "Σ refunded ≤ captured" invariant
/// is a <b>handler</b> check under the booking refund lock, not a single-row constraint.
/// <para>
/// A refund <b>decomposes across both fee legs</b>: <see cref="Amount"/> = <see cref="PlatformFeeRefundedIrr"/>
/// (portion of the platform commission reversed) + <see cref="NursePayoutRefundedIrr"/> (portion of the nurse
/// payout reversed — this leg drives a <c>nurse_clawbacks</c> receivable when the nurse was already paid).
/// Money is IRR <c>BIGINT</c> only. The channel is chosen from the original payment type; the ledger legs are
/// the same across channels — only <see cref="RefundChannel"/>, the external reference and the customer ETA
/// differ.
/// </para>
/// </summary>
public class Refund : BaseEntity<long>, IAuditable
{
    /// <summary>The captured transaction being reversed (N:1 — a transaction may have several partial refunds).</summary>
    public long PaymentTransactionId { get; set; }

    public long BookingId { get; set; }

    /// <summary>The customer the refund is <i>for</i> (not the admin actor).</summary>
    public long RequestedByCustomerId { get; set; }

    /// <summary>Forward-dep on <c>tickets</c> (b15). Nullable now; "ticket required" is a config-gated
    /// handler/validator rule so admin refunds are testable before b15 wires the real FK target.</summary>
    public long? TicketId { get; set; }

    /// <summary>Total refunded (IRR) = <see cref="PlatformFeeRefundedIrr"/> + <see cref="NursePayoutRefundedIrr"/>.</summary>
    public long Amount { get; set; }

    /// <summary>Portion of <c>balinyaar_commission_irr</c> being reversed (IRR).</summary>
    public long PlatformFeeRefundedIrr { get; set; }

    /// <summary>Portion of <c>nurse_payout_amount</c> being reversed (IRR) — drives a clawback if already paid.</summary>
    public long NursePayoutRefundedIrr { get; set; }

    /// <summary>The resolved refund fraction applied to the booking amounts to derive the legs (0–1).</summary>
    public decimal RefundPercentage { get; set; }

    /// <summary>A <see cref="RefundChannel"/> code — how the money physically flows back.</summary>
    public string RefundChannel { get; set; } = null!;

    public string? ReasonCategory { get; set; }
    public string? ReasonNotes { get; set; }

    /// <summary>Guarded — mutated only through the cohesive methods so every write goes through the machine.</summary>
    public string Status { get; private set; } = RefundStatus.Approved;

    public int? ApprovedByAdminId { get; set; }
    public string? RejectedReason { get; private set; }
    public string? AdminNotes { get; set; }

    /// <summary>The PSP card-refund reference, when the channel is <c>psp_card</c>.</summary>
    public string? GatewayRefundReference { get; private set; }

    /// <summary>The BNPL provider revert id, when the channel is <c>bnpl_revert</c> (or the manual bank ref).</summary>
    public string? ExternalRevertReference { get; private set; }

    /// <summary>The ~7–10 business-day BNPL customer window; null for instant card refunds.</summary>
    public DateOnly? ExpectedCustomerRefundEta { get; private set; }

    /// <summary>Snapshot of the b9 cancellation policy <c>code</c> that produced this refund — never re-resolved live.</summary>
    public string? CancellationPolicyCode { get; set; }

    /// <summary>Snapshot of the resolved refund percentage (0–100) frozen at cancel time.</summary>
    public decimal? RefundPercentageApplied { get; set; }

    public DateTime? ProcessedAt { get; private set; }

    public DateTimeOffset? DeletedAt { get; set; }

    public bool CanTransitionTo(string target) => RefundTransitions.CanTransition(Status, target);

    private void Transition(string target)
    {
        if (!RefundTransitions.CanTransition(Status, target))
            throw new InvalidOperationException($"Illegal refund transition {Status} → {target}.");
        Status = target;
    }

    /// <summary>Card path — the reversal is effectively immediate; no customer-facing ETA.</summary>
    public void MarkSucceededCard(string gatewayRefundReference, DateTime now)
    {
        Transition(RefundStatus.Succeeded);
        GatewayRefundReference = gatewayRefundReference;
        ExpectedCustomerRefundEta = null;
        ProcessedAt = now;
    }

    /// <summary>BNPL/manual path — the provider revert is accepted but the customer cash-back is async; the
    /// refund waits in <c>processing</c> until reconciliation confirms it and surfaces the ETA meanwhile.</summary>
    public void MarkProcessing(string? externalRevertReference, DateOnly? expectedCustomerRefundEta)
    {
        Transition(RefundStatus.Processing);
        ExternalRevertReference = externalRevertReference;
        ExpectedCustomerRefundEta = expectedCustomerRefundEta;
    }

    /// <summary>Reconciliation confirmed the customer cash-back for a <c>processing</c> refund (BNPL/manual) —
    /// the admin <c>confirm_settlement</c> or the provider cash-back callback. Stamps <see cref="ProcessedAt"/>
    /// (the settled-at instant); the caller posts the <c>refund_payable ↔ escrow_held</c> clearing in the same
    /// commit. (Not async — the name only mirrored the BNPL/manual "async settlement" concept.)</summary>
    public void MarkSucceededReconciled(DateTime now)
    {
        Transition(RefundStatus.Succeeded);
        ProcessedAt = now;
    }

    public void MarkFailed(string? reason)
    {
        Transition(RefundStatus.Failed);
        RejectedReason = reason;
    }
}
