#nullable enable
using Baya.Domain.Common;

namespace Baya.Domain.Entities.Booking;

/// <summary>
/// The <b>confirmed engagement</b> — the source of truth for a service event and its money split. A
/// <see cref="Booking"/> exists <b>only</b> when the nurse accepted <i>and</i> payment was captured; it is
/// created 1:1 from an <c>accepted_awaiting_payment</c> booking request and never from an accept alone.
/// <para>
/// Money is IRR <c>BIGINT</c> only. The three amounts must always reconcile —
/// <see cref="GrossPriceIrr"/> = <see cref="BalinyaarCommissionIrr"/> + <see cref="NursePayoutAmount"/>,
/// all ≥ 0 — enforced both by a DB CHECK and by the conversion handler; <see cref="NursePayoutAmount"/> is
/// derived, never free-entered. <see cref="PlatformFeeRate"/>, <see cref="VariantSnapshotJson"/> and the
/// encrypted <see cref="AddressSnapshotJson"/> are <b>frozen at conversion</b>: later edits to the source
/// variant/address/config rows must never mutate an existing booking.
/// </para>
/// <para>
/// There is deliberately <b>no</b> "payout done" boolean — paid-ness is derived later from a
/// <c>nurse_payout_booking_links</c> row + the ledger (b13). Payout eligibility is derived from
/// <see cref="DisputeWindowEndsAt"/> passing with no open dispute, never from <see cref="Status"/> alone.
/// </para>
/// </summary>
public class Booking : BaseEntity<long>
{
    /// <summary>1:1 with the request that created it (UNIQUE) — the idempotency key for conversion.</summary>
    public long BookingRequestId { get; set; }

    // Denormalized FKs (copied from the request) for query performance.
    public long CustomerId { get; set; }
    public long NurseId { get; set; }
    public long PatientId { get; set; }
    public long VariantId { get; set; }
    public long CustomerAddressId { get; set; }

    /// <summary>The licensed center / merchant-of-record. <c>partner_centers</c> is DEFERRED to b15, so the
    /// FK stays nullable and unset for now.</summary>
    public long? PartnerCenterId { get; set; }

    /// <summary>Variant + option labels frozen at booking time (never re-resolved from the live variant).</summary>
    public string VariantSnapshotJson { get; set; } = null!;

    /// <summary>Full address frozen at booking time — <b>encrypted at rest</b> through the field encryptor.</summary>
    public string AddressSnapshotJson { get; set; } = null!;

    /// <summary>Total charged the customer (IRR).</summary>
    public long GrossPriceIrr { get; set; }

    /// <summary>Balinyaar's own cut (IRR) = round(<see cref="GrossPriceIrr"/> × <see cref="PlatformFeeRate"/>).</summary>
    public long BalinyaarCommissionIrr { get; set; }

    /// <summary>The commission rate snapshot frozen at conversion, for audit.</summary>
    public decimal PlatformFeeRate { get; set; }

    /// <summary>Derived: <see cref="GrossPriceIrr"/> − <see cref="BalinyaarCommissionIrr"/> (IRR).</summary>
    public long NursePayoutAmount { get; set; }

    /// <summary>Gateway cost on this payment (IRR), for true margin. Null until a capture sets it.</summary>
    public long? PspFeeAmount { get; set; }

    /// <summary>1 = single visit; &gt; 1 = multi-session engagement. Always ≥ 1.</summary>
    public short SessionCount { get; set; } = 1;

    // Engagement-level schedule; the per-visit schedule lives on booking_sessions.
    public DateOnly ScheduledDate { get; set; }
    public TimeOnly ScheduledTimeStart { get; set; }
    public TimeOnly ScheduledTimeEnd { get; set; }

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

    public DateTime? ConfirmedAt { get; private set; }
    public DateTime? CancelledAt { get; private set; }
    public string? CancellationReason { get; private set; }

    /// <summary>Who cancelled — a <see cref="CancellationActor"/> code.</summary>
    public string? CancelledBy { get; private set; }

    /// <summary>The resolved cancellation policy <c>code</c>, <b>frozen</b> at cancel time (a later policy
    /// edit must not change it). Absent a b11 refunds/cancellation-event table, the snapshot lives here.</summary>
    public string? CancellationPolicyCode { get; private set; }

    /// <summary>The resolved <c>refund_percentage</c> (0–100), frozen at cancel time.</summary>
    public decimal? CancellationRefundPercentage { get; private set; }

    /// <summary>The computed refundable amount (IRR) for the un-started sessions at cancel time. The refund
    /// ledger/execution is b11; this is the frozen figure b11 consumes.</summary>
    public long? RefundableAmountIrr { get; private set; }

    public DateTime? CompletedAt { get; private set; }

    /// <summary>Set on completion = <see cref="CompletedAt"/> + config(<c>dispute_window_hours</c>, 72). The
    /// <b>only</b> thing that makes a payout eligible (b13) — never <see cref="Status"/> = completed alone.</summary>
    public DateTime? DisputeWindowEndsAt { get; private set; }

    public DateTimeOffset? DeletedAt { get; set; }

    public ICollection<BookingSession> Sessions { get; set; } = new List<BookingSession>();
    public BookingCareInstruction? CareInstructions { get; set; }

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

    /// <summary>
    /// Applies a status change through the allowed-transition guard and stamps the matching lifecycle
    /// timestamp. Callers pre-check with <see cref="CanTransitionTo"/> and return a clean conflict; reaching
    /// an illegal edge here is a programming error, so it fails fast rather than overwriting a terminal state.
    /// </summary>
    public void TransitionTo(string target, DateTime now, string? actor = null, string? reason = null)
    {
        if (!BookingTransitions.CanTransition(Status, target))
            throw new InvalidOperationException($"Illegal booking transition {Status} → {target}.");

        Status = target;
        switch (target)
        {
            case BookingStatus.Confirmed:
                ConfirmedAt = now;
                break;
            case BookingStatus.Completed:
                CompletedAt = now;
                break;
            case BookingStatus.Cancelled:
                CancelledAt = now;
                CancelledBy = actor;
                CancellationReason = reason;
                break;
        }
    }

    /// <summary>Freezes the resolved cancellation policy snapshot + the computed refundable amount.</summary>
    public void RecordCancellationSnapshot(string policyCode, decimal refundPercentage, long refundableAmountIrr)
    {
        CancellationPolicyCode = policyCode;
        CancellationRefundPercentage = refundPercentage;
        RefundableAmountIrr = refundableAmountIrr;
    }

    /// <summary>Sets the dispute window on completion — the single payout-eligibility trigger for the booking.</summary>
    public void SetDisputeWindow(DateTime endsAt) => DisputeWindowEndsAt = endsAt;
}
