"""
Billing and payment models.

Tables in this file:
  discount_rule — branch-level discount rules (percentage or fixed amount)
  bill          — 1:1 invoice for a visit order with financial summary
  payment       — payment transactions applied to a bill

Billing Architecture & Lifecycle
────────────────────────────────
1. Order-to-Bill 1:1 Mapping:
   Every completed or confirmed VisitOrder generates exactly one Bill.
   The bill subtotal is computed directly from the immutable `price_at_order`
   snapshots in `order_test` and `order_package`.

2. Discount Rules:
   Discounts can be applied via a predefined DiscountRule (e.g. 10% Senior Citizen)
   or customized at billing time.
   `discount_type`: PERCENTAGE (e.g. 15.00%) or FIXED (e.g. 500.00 NPR).

3. Financial Formula:
   total_amount = subtotal - discount_amount + tax_amount
   due_amount   = total_amount - paid_amount

4. Bill Lifecycle:
   UNPAID  -> no payments recorded yet
   PARTIAL -> advance paid, balance remaining (due_amount > 0)
   PAID    -> fully settled (due_amount == 0)
   VOID    -> cancelled/voided (with audit trail)

5. Payment Transactions:
   Supports split/multiple payment modes (cash, card, bank transfer, mobile wallet).
   Payment records are immutable audit ledger entries (no soft delete).

Soft Delete Decisions
─────────────────────
  DiscountRule -> YES (SoftDeleteMixin): retired rules are archived without losing billing history.
  Bill         -> YES (SoftDeleteMixin): legal and accounting documents are preserved.
  Payment      -> NO  (no soft delete): financial ledger records are immutable. Corrections
                      require adjustment or refund transaction entries.

Lab Scoping
───────────
  DiscountRule -> YES (LabScopedMixin): discounts are configured per branch.
  Bill         -> YES (LabScopedMixin): invoices and numbering are scoped per branch.
  Payment      -> YES (LabScopedMixin): payments belong to the branch where cash/pos was handled.
"""

import datetime
import enum
import uuid
from decimal import Decimal
from typing import cast

import sqlalchemy as sa
from sqlalchemy import (
    Boolean,
    CheckConstraint,
    ForeignKey,
    Numeric,
    String,
    Text,
    UniqueConstraint,
)
from sqlalchemy.dialects.postgresql import TIMESTAMP, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.models.base import (
    Base,
    LabScopedMixin,
    SoftDeleteMixin,
    TimestampMixin,
    UUIDPrimaryKeyMixin,
)

# ── Enums ──────────────────────────────────────────────────────────────────────


class BillStatus(enum.StrEnum):
    """Lifecycle state of an invoice."""

    UNPAID = "unpaid"
    PARTIAL = "partial"
    PAID = "paid"
    VOID = "void"


class DiscountType(enum.StrEnum):
    """Type of discount calculation."""

    PERCENTAGE = "percentage"
    FIXED = "fixed"


class PaymentMode(enum.StrEnum):
    """Payment transaction tender modes."""

    CASH = "cash"
    CARD = "card"
    TRANSFER = "transfer"
    CHEQUE = "cheque"
    MOBILE_BANKING = "mobile_banking"
    OTHER = "other"


BillStatusType = sa.Enum(
    BillStatus,
    name="bill_status",
    native_enum=True,
    create_constraint=True,
    values_callable=lambda obj: [e.value for e in obj],
)

DiscountTypeType = sa.Enum(
    DiscountType,
    name="discount_type",
    native_enum=True,
    create_constraint=True,
    values_callable=lambda obj: [e.value for e in obj],
)

PaymentModeType = sa.Enum(
    PaymentMode,
    name="payment_mode",
    native_enum=True,
    create_constraint=True,
    values_callable=lambda obj: [e.value for e in obj],
)


# ── Models ─────────────────────────────────────────────────────────────────────


class DiscountRule(UUIDPrimaryKeyMixin, TimestampMixin, SoftDeleteMixin, LabScopedMixin, Base):
    """
    Configurable discount rule available at a lab branch.

    Examples:
      - "Senior Citizen Discount", PERCENTAGE, 10.00
      - "Hospital Staff Family", PERCENTAGE, 20.00
      - "Festival Special Voucher", FIXED, 500.00
    """

    __tablename__ = "discount_rule"
    __table_args__ = (UniqueConstraint("lab_id", "name", name="uq_discount_rule_lab_id_name"),)

    name: Mapped[str] = mapped_column(
        String(100),
        nullable=False,
        comment="Human-readable rule name (e.g. 'Senior Citizen Discount')",
    )
    code: Mapped[str | None] = mapped_column(
        String(30),
        nullable=True,
        index=True,
        comment="Optional short discount code (e.g. 'SR10')",
    )
    discount_type: Mapped[DiscountType] = mapped_column(
        DiscountTypeType,
        nullable=False,
        default=DiscountType.PERCENTAGE,
        server_default="percentage",
        comment="Calculation method: percentage or fixed amount",
    )
    discount_value: Mapped[Decimal] = mapped_column(
        Numeric(10, 2),
        nullable=False,
        comment="Value: percentage rate (e.g. 10.00) or fixed currency amount (e.g. 500.00)",
    )
    is_active: Mapped[bool] = mapped_column(
        Boolean,
        nullable=False,
        default=True,
        server_default=sa.true(),
        index=True,
        comment="Flag indicating if this discount rule is currently selectable at billing",
    )
    description: Mapped[str | None] = mapped_column(
        Text,
        nullable=True,
        comment="Internal notes or eligibility criteria for the discount",
    )

    # Relationships
    lab: Mapped["Lab"] = relationship(  # type: ignore[name-defined]
        "Lab",
        foreign_keys="[DiscountRule.lab_id]",
        lazy="selectin",
    )
    bills: Mapped[list["Bill"]] = relationship(
        "Bill",
        foreign_keys="[Bill.discount_rule_id]",
        lazy="noload",
    )

    def __repr__(self) -> str:
        return (
            f"<DiscountRule id={self.id!s:.8} "
            f"name={self.name!r} "
            f"type={self.discount_type.value!r} "
            f"value={self.discount_value}>"
        )


class Bill(UUIDPrimaryKeyMixin, TimestampMixin, SoftDeleteMixin, LabScopedMixin, Base):
    """
    Invoice issued for a VisitOrder.

    1:1 relationship with VisitOrder. Financial balances are tracked
    via subtotal, discount, tax, paid_amount, and due_amount.
    """

    __tablename__ = "bill"
    __table_args__ = (
        UniqueConstraint("lab_id", "bill_number", name="uq_bill_lab_id_bill_number"),
        UniqueConstraint("order_id", name="uq_bill_order_id"),
        CheckConstraint("discount_amount <= subtotal", name="ck_bill_discount_lte_subtotal"),
        CheckConstraint("discount_amount >= 0", name="ck_bill_discount_gte_zero"),
        CheckConstraint("total_amount >= 0", name="ck_bill_total_gte_zero"),
        CheckConstraint("due_amount >= 0", name="ck_bill_due_gte_zero"),
        CheckConstraint("paid_amount >= 0", name="ck_bill_paid_gte_zero"),
        CheckConstraint("subtotal >= 0", name="ck_bill_subtotal_gte_zero"),
    )

    # ── Foreign Keys ───────────────────────────────────────────────────────────
    order_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey("visit_order.id", ondelete="RESTRICT"),
        nullable=False,
        unique=True,
        index=True,
        comment="FK → visit_order.id (1:1 with VisitOrder)",
    )
    patient_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey("patient.id", ondelete="RESTRICT"),
        nullable=False,
        index=True,
        comment="FK → patient.id. Denormalized for rapid patient invoice queries.",
    )
    discount_rule_id: Mapped[uuid.UUID | None] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey("discount_rule.id", ondelete="SET NULL"),
        nullable=True,
        index=True,
        comment="FK → discount_rule.id. Null if manual or no discount applied.",
    )

    # ── Invoice Details ────────────────────────────────────────────────────────
    bill_number: Mapped[str] = mapped_column(
        String(30),
        nullable=False,
        index=True,
        comment="Human-readable invoice sequence: INV{YY}{6-digit-seq} e.g. INV260001",
    )
    status: Mapped[BillStatus] = mapped_column(
        BillStatusType,
        nullable=False,
        default=BillStatus.UNPAID,
        server_default="unpaid",
        index=True,
        comment="Settlement status of the bill",
    )

    # ── Financial Amounts ──────────────────────────────────────────────────────
    subtotal: Mapped[Decimal] = mapped_column(
        Numeric(12, 2),
        nullable=False,
        default=Decimal("0.00"),
        server_default="0.00",
        comment="Sum of prices (price_at_order) of all ordered tests and packages",
    )
    discount_amount: Mapped[Decimal] = mapped_column(
        Numeric(12, 2),
        nullable=False,
        default=Decimal("0.00"),
        server_default="0.00",
        comment="Total discount subtracted from subtotal",
    )
    tax_amount: Mapped[Decimal] = mapped_column(
        Numeric(12, 2),
        nullable=False,
        default=Decimal("0.00"),
        server_default="0.00",
        comment="Tax/VAT amount applied to the invoice",
    )
    total_amount: Mapped[Decimal] = mapped_column(
        Numeric(12, 2),
        nullable=False,
        default=Decimal("0.00"),
        server_default="0.00",
        comment="Final payable amount: (subtotal - discount_amount + tax_amount)",
    )
    paid_amount: Mapped[Decimal] = mapped_column(
        Numeric(12, 2),
        nullable=False,
        default=Decimal("0.00"),
        server_default="0.00",
        comment="Total payments received and settled against this bill",
    )
    due_amount: Mapped[Decimal] = mapped_column(
        Numeric(12, 2),
        nullable=False,
        default=Decimal("0.00"),
        server_default="0.00",
        comment="Outstanding balance due: (total_amount - paid_amount)",
    )

    notes: Mapped[str | None] = mapped_column(
        Text,
        nullable=True,
        comment="Billing remarks, terms, or cashier notes",
    )

    # ── Relationships ──────────────────────────────────────────────────────────
    lab: Mapped["Lab"] = relationship(  # type: ignore[name-defined]
        "Lab",
        foreign_keys="[Bill.lab_id]",
        lazy="selectin",
    )
    order: Mapped["VisitOrder"] = relationship(  # type: ignore[name-defined]
        "VisitOrder",
        foreign_keys="[Bill.order_id]",
        lazy="selectin",
    )
    patient: Mapped["Patient"] = relationship(  # type: ignore[name-defined]
        "Patient",
        foreign_keys="[Bill.patient_id]",
        lazy="selectin",
    )
    discount_rule: Mapped["DiscountRule | None"] = relationship(
        "DiscountRule",
        foreign_keys="[Bill.discount_rule_id]",
        lazy="selectin",
    )
    payments: Mapped[list["Payment"]] = relationship(
        "Payment",
        back_populates="bill",
        lazy="noload",
        cascade="all, delete-orphan",
    )

    @property
    def destination_country(self) -> str | None:
        """Destination country of the associated visit order, if abroad."""
        if self.order and self.order.destination_country:
            return cast(str, self.order.destination_country)
        return None

    @property
    def is_abroad(self) -> bool:
        """Flag indicating pre-departure / abroad visa medical invoice."""
        return bool(self.destination_country and self.destination_country.strip())

    def __repr__(self) -> str:
        return (
            f"<Bill id={self.id!s:.8} "
            f"number={self.bill_number!r} "
            f"total={self.total_amount} "
            f"status={self.status.value!r}>"
        )


class Payment(UUIDPrimaryKeyMixin, TimestampMixin, LabScopedMixin, Base):
    """
    Individual payment receipt transaction recorded against a Bill.

    Immutable accounting record (no soft delete). Multiple payments can be
    applied to a single bill (e.g. advance cash payment, balance card payment).
    """

    __tablename__ = "payment"

    # ── Foreign Keys ───────────────────────────────────────────────────────────
    bill_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey("bill.id", ondelete="RESTRICT"),
        nullable=False,
        index=True,
        comment="FK → bill.id. The invoice to which this payment is credited.",
    )
    received_by: Mapped[uuid.UUID | None] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey("user.id", ondelete="SET NULL"),
        nullable=True,
        index=True,
        comment="FK → user.id. Staff member (accountant/receptionist) who took payment.",
    )

    # ── Transaction Details ────────────────────────────────────────────────────
    amount: Mapped[Decimal] = mapped_column(
        Numeric(12, 2),
        nullable=False,
        comment="Tendered payment transaction amount",
    )
    payment_mode: Mapped[PaymentMode] = mapped_column(
        PaymentModeType,
        nullable=False,
        default=PaymentMode.CASH,
        server_default="cash",
        index=True,
        comment="Tender mode: cash, card, bank transfer, mobile wallet, or cheque",
    )
    transaction_reference: Mapped[str | None] = mapped_column(
        String(100),
        nullable=True,
        comment="Card slip number, UPI/mobile transaction ID, or bank reference",
    )
    paid_at: Mapped[datetime.datetime] = mapped_column(
        TIMESTAMP(timezone=True),
        nullable=False,
        default=lambda: datetime.datetime.now(datetime.UTC),
        server_default=sa.text("now()"),
        index=True,
        comment="Exact timestamp when payment was tendered (TIMESTAMPTZ)",
    )
    notes: Mapped[str | None] = mapped_column(
        Text,
        nullable=True,
        comment="Cashier remarks or transaction notes",
    )

    # ── Relationships ──────────────────────────────────────────────────────────
    lab: Mapped["Lab"] = relationship(  # type: ignore[name-defined]
        "Lab",
        foreign_keys="[Payment.lab_id]",
        lazy="selectin",
    )
    bill: Mapped["Bill"] = relationship(
        "Bill",
        back_populates="payments",
        foreign_keys="[Payment.bill_id]",
        lazy="selectin",
    )
    received_by_user: Mapped["User | None"] = relationship(  # type: ignore[name-defined]
        "User",
        foreign_keys="[Payment.received_by]",
        lazy="selectin",
    )

    def __repr__(self) -> str:
        return (
            f"<Payment id={self.id!s:.8} "
            f"bill_id={self.bill_id!s:.8} "
            f"amount={self.amount} "
            f"mode={self.payment_mode.value!r}>"
        )
