#nullable enable
using Baya.Domain.Common;

namespace Baya.Domain.Entities.Payouts;

/// <summary>
/// A weekly aggregation of amounts owed for completed, payout-eligible, unpaid bookings — the operational unit of
/// the payout run, matching the PAYA settlement cycle. An admin (later, a scheduled job) opens a batch; it
/// materializes one <see cref="NursePayout"/> per nurse with earnings in the window, then submits them all to the
/// bank rail in one go.
/// <para>
/// <b>Holiday-aware:</b> <see cref="PeriodEnd"/> and <see cref="ProcessingDate"/> are shifted off bank-closed days
/// (via <c>IHolidayCalendar</c>) to the next business day — a batch landing on a multi-day Nowruz closure would
/// otherwise fail, since PAYA/SATNA does not settle on closed days. <b>Invariant:</b>
/// <see cref="TotalAmount"/> = Σ(<c>nurse_payouts.net_amount_irr</c>) and <see cref="PayoutCount"/> =
/// COUNT(payouts) — set by the handler when the rows are materialized and asserted by a verified invariant.
/// Money is IRR <c>BIGINT</c>, no floats.
/// </para>
/// </summary>
public class NursePayoutBatch : BaseEntity<long>, IAuditable
{
    public DateOnly PeriodStart { get; set; }

    /// <summary>Window end — shifted off <c>is_bank_closed</c> days to the next business day.</summary>
    public DateOnly PeriodEnd { get; set; }

    /// <summary>The date the transfers are submitted — shifted off <c>is_bank_closed</c> days.</summary>
    public DateOnly ProcessingDate { get; set; }

    /// <summary>Σ(<c>net_amount_irr</c>) across this batch's payouts (IRR). Set at materialization.</summary>
    public long TotalAmount { get; set; }

    public int PayoutCount { get; set; }

    /// <summary>Guarded — mutated only through <see cref="TransitionTo"/> so every write goes through the machine.</summary>
    public string Status { get; private set; } = PayoutBatchStatus.Draft;

    /// <summary>The admin who initiated the run (FK <c>users</c>), or <c>null</c> for a system-initiated
    /// (scheduled/unattended) batch — the weekly cron has no human initiator (refinement-phase-7).</summary>
    public int? InitiatedByAdminId { get; set; }

    public DateTime? ProcessedAt { get; private set; }

    public string? FailureNotes { get; private set; }

    public DateTimeOffset? DeletedAt { get; set; }

    public ICollection<NursePayout> Payouts { get; set; } = new List<NursePayout>();

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

    /// <summary>Applies a guarded status change and, on a settling edge, stamps <see cref="ProcessedAt"/>. Reaching
    /// an illegal edge is a programming error (callers pre-check), so it fails fast rather than corrupting state.</summary>
    public void TransitionTo(string target, DateTime now, string? failureNotes = null)
    {
        if (!PayoutBatchTransitions.CanTransition(Status, target))
            throw new InvalidOperationException($"Illegal payout-batch transition {Status} → {target}.");

        Status = target;
        if (target is PayoutBatchStatus.Completed or PayoutBatchStatus.PartiallyFailed or PayoutBatchStatus.Failed)
            ProcessedAt = now;
        if (failureNotes is not null)
            FailureNotes = failureNotes;
    }

    /// <summary>Freezes the batch totals when the payouts are materialized (the CHECK-mirroring invariant).</summary>
    public void SetTotals(long totalAmount, int payoutCount)
    {
        TotalAmount = totalAmount;
        PayoutCount = payoutCount;
    }

    /// <summary>Re-derives the batch's terminal status from its payouts after an execute or a retry: all paid →
    /// <c>completed</c>; some failed but some paid → <c>partially_failed</c>; all failed → <c>failed</c>. A no-op
    /// when the resulting edge isn't allowed from the current status (e.g. already <c>partially_failed</c> with a
    /// still-failed row). Requires the <see cref="Payouts"/> collection to be loaded.</summary>
    public void RecomputeSettlement(DateTime now)
    {
        var anyFailed = Payouts.Any(p => p.Status == PayoutStatus.Failed);
        var anyPaid = Payouts.Any(p => p.Status == PayoutStatus.Paid);

        var target = !anyFailed
            ? PayoutBatchStatus.Completed
            : anyPaid ? PayoutBatchStatus.PartiallyFailed : PayoutBatchStatus.Failed;

        if (CanTransitionTo(target))
            TransitionTo(target, now, target == PayoutBatchStatus.Failed ? "All payouts failed at the bank rail." : null);
    }
}
