"""
Appointment models.

Tables in this file:
  appointment_slot — defines available booking windows per branch
  appointment      — a patient's booked (or walk-in) appointment

Slot Capacity & booked_count
─────────────────────────────
AppointmentSlot carries a denormalized `booked_count` counter alongside
`capacity`. This is intentional for performance:

  - Checking availability: WHERE booked_count < capacity (O(1), no JOIN)
  - Rendering a slot grid: no subquery needed to count per-slot bookings

Consistency contract:
  The service layer MUST increment booked_count inside the same DB transaction
  that creates the Appointment row, and decrement it on cancellation.
  Use SELECT FOR UPDATE on the slot row to prevent race conditions.

  Do NOT rely on triggers or application code outside the transaction — if the
  transaction rolls back, the counter rolls back with it (ACID guarantee).

Appointment Lifecycle
─────────────────────
  pending   → confirmed → checked_in → completed
                       ↓               ↓
                  cancelled         no_show (patient didn't arrive)

  Walk-in patients skip pending/confirmed and go straight to checked_in.

Relationship to VisitOrder
──────────────────────────
  visit_order_id on Appointment is nullable and is SET when the receptionist
  creates an order for this appointment. It is the forward link from appointment
  to order. The VisitOrder model carries the reverse FK (appointment_id).

  An appointment without an order = booked but not yet processed.
  An appointment with an order  = patient has arrived and order is active.

Soft Delete Decisions
─────────────────────
  AppointmentSlot → YES: slot history is useful for capacity analytics.
  Appointment     → YES: cancelled/no-show records are retained for reporting.

Lab Scoping
───────────
  Both models: YES (LabScopedMixin).
"""

import datetime
import enum
import uuid

import sqlalchemy as sa
from sqlalchemy import Boolean, ForeignKey, Integer, 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 AppointmentStatus(enum.StrEnum):
    """Lifecycle state of an appointment."""

    PENDING = "pending"  # Booked, not yet confirmed by staff
    CONFIRMED = "confirmed"  # Staff confirmed the booking
    CHECKED_IN = "checked_in"  # Patient has arrived at the lab
    COMPLETED = "completed"  # Visit order created and patient processed
    CANCELLED = "cancelled"  # Cancelled by patient or staff
    NO_SHOW = "no_show"  # Patient did not arrive


class BookingChannel(enum.StrEnum):
    """How the appointment was booked."""

    WALK_IN = "walk_in"  # Patient arrived without a prior booking
    PHONE = "phone"  # Staff booked on behalf of patient via phone
    ONLINE = "online"  # Patient booked via online form (future)
    PORTAL = "portal"  # Patient booked via client portal link


AppointmentStatusType = sa.Enum(
    AppointmentStatus,
    name="appointment_status",
    native_enum=True,
    create_constraint=True,
    values_callable=lambda obj: [e.value for e in obj],
)

BookingChannelType = sa.Enum(
    BookingChannel,
    name="booking_channel",
    native_enum=True,
    create_constraint=True,
    values_callable=lambda obj: [e.value for e in obj],
)


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


class AppointmentSlot(UUIDPrimaryKeyMixin, TimestampMixin, SoftDeleteMixin, LabScopedMixin, Base):
    """
    A defined booking window — the unit of appointment scheduling.

    Slots are created by the admin/manager for specific date/time ranges.
    Multiple patients can book into one slot up to `capacity`.

    The `booked_count` is a denormalized counter — see module docstring
    for the consistency contract.

    Indexes:
      lab_id   → LabScopedMixin (already indexed)
      start_at → for date-range queries (slot grid, availability checks)
    """

    __tablename__ = "appointment_slot"

    start_at: Mapped[datetime.datetime] = mapped_column(
        TIMESTAMP(timezone=True),
        nullable=False,
        index=True,
        comment="Slot start time (TIMESTAMPTZ) — index supports date-range queries",
    )
    end_at: Mapped[datetime.datetime] = mapped_column(
        TIMESTAMP(timezone=True),
        nullable=False,
        comment="Slot end time (TIMESTAMPTZ)",
    )
    capacity: Mapped[int] = mapped_column(
        Integer,
        nullable=False,
        default=1,
        server_default="1",
        comment="Maximum number of appointments allowed in this slot",
    )
    booked_count: Mapped[int] = mapped_column(
        Integer,
        nullable=False,
        default=0,
        server_default="0",
        comment=(
            "Denormalized booking count. Incremented/decremented by the service layer "
            "inside a transaction with SELECT FOR UPDATE on this row."
        ),
    )
    is_active: Mapped[bool] = mapped_column(
        Boolean,
        nullable=False,
        default=True,
        server_default="true",
        comment="False = slot is closed to new bookings (e.g., holiday, maintenance)",
    )

    # Relationships
    lab: Mapped["Lab"] = relationship(  # type: ignore[name-defined]
        "Lab",
        foreign_keys="[AppointmentSlot.lab_id]",
        lazy="selectin",
    )
    appointments: Mapped[list["Appointment"]] = relationship(
        back_populates="slot",
        lazy="noload",
    )

    @property
    def is_full(self) -> bool:
        """True if this slot has no remaining capacity."""
        return self.booked_count >= self.capacity

    @property
    def available_slots(self) -> int:
        """Remaining bookable places in this slot."""
        return max(0, self.capacity - self.booked_count)

    def __repr__(self) -> str:
        return (
            f"<AppointmentSlot id={self.id!s:.8} "
            f"start={self.start_at.isoformat()} "
            f"capacity={self.capacity} "
            f"booked={self.booked_count}>"
        )


class Appointment(UUIDPrimaryKeyMixin, TimestampMixin, SoftDeleteMixin, LabScopedMixin, Base):
    """
    A patient's appointment — either pre-booked or walk-in.

    Walk-ins: slot_id is NULL. status starts at CHECKED_IN.
    Pre-booked: slot_id is set. status starts at PENDING or CONFIRMED.

    visit_order_id is NULL until the receptionist creates a visit order
    for this appointment. Setting this link completes the appointment → order
    handoff.

    contact_name / contact_phone: optional — used when the person booking
    the appointment is a family member or caregiver, not the patient themselves.
    """

    __tablename__ = "appointment"

    patient_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey("patient.id", ondelete="RESTRICT"),
        nullable=False,
        index=True,
        comment="FK → patient.id",
    )
    slot_id: Mapped[uuid.UUID | None] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey("appointment_slot.id", ondelete="SET NULL"),
        nullable=True,
        index=True,
        comment="FK → appointment_slot.id. NULL for walk-in appointments.",
    )
    visit_order_id: Mapped[uuid.UUID | None] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey(
            "visit_order.id",
            ondelete="SET NULL",
            use_alter=True,
            name="fk_appointment_visit_order_id_visit_order",
        ),
        nullable=True,
        index=True,
        comment=(
            "FK → visit_order.id. Set when the receptionist creates an order. "
            "NULL = appointment exists but no order yet."
        ),
    )
    status: Mapped[AppointmentStatus] = mapped_column(
        AppointmentStatusType,
        nullable=False,
        default=AppointmentStatus.PENDING,
        server_default="pending",
        index=True,
        comment="Appointment lifecycle state",
    )
    booked_via: Mapped[BookingChannel] = mapped_column(
        BookingChannelType,
        nullable=False,
        default=BookingChannel.WALK_IN,
        server_default="walk_in",
        comment="How this appointment was created",
    )
    contact_name: Mapped[str | None] = mapped_column(
        String(200),
        nullable=True,
        comment="Name of person who made the booking (if different from patient)",
    )
    contact_phone: Mapped[str | None] = mapped_column(
        String(20),
        nullable=True,
        comment="Phone of booking contact (if different from patient)",
    )
    notes: Mapped[str | None] = mapped_column(
        Text,
        nullable=True,
        comment="Optional internal notes from staff",
    )

    # Relationships
    lab: Mapped["Lab"] = relationship(  # type: ignore[name-defined]
        "Lab",
        foreign_keys="[Appointment.lab_id]",
        lazy="selectin",
    )
    patient: Mapped["Patient"] = relationship(  # type: ignore[name-defined]
        "Patient",
        foreign_keys="[Appointment.patient_id]",
        lazy="selectin",
    )
    slot: Mapped["AppointmentSlot | None"] = relationship(
        back_populates="appointments",
        foreign_keys="[Appointment.slot_id]",
        lazy="selectin",
    )
    # visit_order relationship defined on VisitOrder (owning side)

    def __repr__(self) -> str:
        return (
            f"<Appointment id={self.id!s:.8} patient_id={self.patient_id!s:.8} status={self.status.value!r}>"
        )


class LabScheduleConfig(UUIDPrimaryKeyMixin, TimestampMixin, LabScopedMixin, Base):
    """
    Operating hours and slot duration configuration per laboratory branch.
    Configured by super admin / owner.
    """

    __tablename__ = "lab_schedule_config"
    __table_args__ = (UniqueConstraint("lab_id", name="uq_lab_schedule_config_lab_id"),)
    lab_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("lab.id", ondelete="CASCADE"), nullable=False, index=True
    )

    open_time: Mapped[str] = mapped_column(
        String(5),
        nullable=False,
        default="07:00",
        server_default="07:00",
        comment="Daily opening time HH:MM format",
    )
    close_time: Mapped[str] = mapped_column(
        String(5),
        nullable=False,
        default="19:00",
        server_default="19:00",
        comment="Daily closing time HH:MM format",
    )
    slot_duration_minutes: Mapped[int] = mapped_column(
        Integer,
        nullable=False,
        default=30,
        server_default="30",
        comment="Duration of each appointment booking window in minutes",
    )
    slot_capacity: Mapped[int] = mapped_column(
        Integer,
        nullable=False,
        default=2,
        server_default="8",  # Existing DB default; the ORM/service default remains 2.
        comment="Maximum patients bookable per time window",
    )
    operating_days: Mapped[str] = mapped_column(
        String(50),
        nullable=False,
        default="0,1,2,3,4,5,6",
        server_default="0,1,2,3,4,5,6",
        comment="Comma-separated active days of week (0=Mon, 1=Tue, ..., 6=Sun)",
    )
    is_active: Mapped[bool] = mapped_column(
        Boolean,
        nullable=False,
        default=True,
        server_default="true",
        comment="Whether online/quick booking is actively open for this lab",
    )

    # Relationship
    lab: Mapped["Lab"] = relationship(  # type: ignore[name-defined]
        "Lab",
        foreign_keys="[LabScheduleConfig.lab_id]",
        lazy="selectin",
    )


class LabHoliday(UUIDPrimaryKeyMixin, TimestampMixin, SoftDeleteMixin, LabScopedMixin, Base):
    """
    Holiday, festival, or emergency closure date ranges for the lab branch.
    Configured by super admin / owner.
    """

    __tablename__ = "lab_holiday"
    lab_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("lab.id", ondelete="CASCADE"), nullable=False, index=True
    )

    start_date: Mapped[datetime.date] = mapped_column(
        sa.Date,
        nullable=False,
        index=True,
        comment="First day of closure/holiday",
    )
    end_date: Mapped[datetime.date] = mapped_column(
        sa.Date,
        nullable=False,
        index=True,
        comment="Last day of closure/holiday (inclusive)",
    )
    title: Mapped[str] = mapped_column(
        String(200),
        nullable=False,
        comment="Reason or name of holiday (e.g. Dashain Festival, Public Holiday)",
    )
    notes: Mapped[str | None] = mapped_column(
        Text,
        nullable=True,
        comment="Optional internal notes or instructions",
    )
    is_closed: Mapped[bool] = mapped_column(
        Boolean,
        nullable=False,
        default=True,
        server_default="true",
        comment="True if lab is completely closed for collection",
    )

    # Relationship
    lab: Mapped["Lab"] = relationship(  # type: ignore[name-defined]
        "Lab",
        foreign_keys="[LabHoliday.lab_id]",
        lazy="selectin",
    )
