"""
Patient model.

A patient is any individual who visits the lab for testing.
Their phone number is their identity — it doubles as the client portal
username for accessing reports and appointments.

Patient Code Format
───────────────────
Format: P/{YY}/{10-digit-sequence}
Example: P/26/0000000001 (first patient registered in 2026)

The code is generated in the service layer (not a DB server_default)
because it requires year-aware sequential logic. It is stored as a plain
string and is immutable once assigned.

Client Portal Access (PIN System)
──────────────────────────────────
When a bill is generated, a 6-digit PIN is created and its bcrypt hash
is stored in pin_hash. The plain PIN is printed on the bill receipt.

Client access: phone (username) + PIN → time-limited JWT
               → read-only view of reports and appointments.

The pin_hash is OVERWRITTEN every time a new bill is generated.
Previous PIN becomes invalid immediately on new bill creation.

The expiring_state table holds token revocations; this model holds the PIN hash.

Age vs Date of Birth
────────────────────
Both fields are nullable. The lab may enter either:
  - date_of_birth: preferred (enables accurate age calculation over time)
  - age: fallback when DOB is unknown (stored as-entered, not recalculated)

The application should prefer DOB when both are present.

Height / Weight
───────────────
Optional anthropometric data, used when test parameter reference ranges
are body-metric-dependent. Stored in cm and kg respectively.

Soft delete: YES — patient records must be retained for the full
             audit history of their orders, reports, and payments.
Lab scoping: YES — patients are registered at a specific branch.
"""

import datetime
import enum
from decimal import Decimal

import sqlalchemy as sa
from sqlalchemy import (
    Date,
    Index,
    Integer,
    Numeric,
    String,
    Text,
    UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship

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

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


class Gender(enum.StrEnum):
    """
    Biological sex — used to select the correct reference range for
    test parameters that differ by gender.
    """

    MALE = "male"
    FEMALE = "female"
    OTHER = "other"


GenderType = sa.Enum(
    Gender,
    name="gender",
    native_enum=True,
    create_constraint=True,
    values_callable=lambda obj: [e.value for e in obj],
)


# ── Model ──────────────────────────────────────────────────────────────────────


class Patient(UUIDPrimaryKeyMixin, TimestampMixin, SoftDeleteMixin, LabScopedMixin, Base):
    """
    A patient registered at a lab branch.

    Each patient is uniquely identified within a branch by their phone number.
    The patient_code is a human-readable, sequential code used on reports
    and printed labels.
    """

    __tablename__ = "patient"
    __table_args__ = (
        # phone is the login username — unique per branch
        UniqueConstraint("lab_id", "phone", name="uq_patient_lab_id_phone"),
        # patient_code is unique per branch (codes restart per lab if needed)
        UniqueConstraint("lab_id", "patient_code", name="uq_patient_lab_id_patient_code"),
        Index(
            "idx_patient_name_trgm", "name", postgresql_using="gin", postgresql_ops={"name": "gin_trgm_ops"}
        ),
        Index(
            "idx_patient_phone_trgm",
            "phone",
            postgresql_using="gin",
            postgresql_ops={"phone": "gin_trgm_ops"},
        ),
        Index(
            "idx_patient_code_trgm",
            "patient_code",
            postgresql_using="gin",
            postgresql_ops={"patient_code": "gin_trgm_ops"},
        ),
    )

    # ── Identity ───────────────────────────────────────────────────────────────
    patient_code: Mapped[str] = mapped_column(
        String(30),
        nullable=False,
        index=True,
        comment="Human-readable code: P/{YY}/{10-digit-seq} e.g. P/26/0000000001",
    )
    name: Mapped[str] = mapped_column(
        String(200),
        nullable=False,
        comment="Full name of the patient",
    )

    # ── Demographics ───────────────────────────────────────────────────────────
    date_of_birth: Mapped[datetime.date | None] = mapped_column(
        Date,
        nullable=True,
        comment="Date of birth — preferred over age field when available",
    )
    age: Mapped[int | None] = mapped_column(
        Integer,
        nullable=True,
        comment="Age in years — fallback when DOB is unknown. Not recalculated automatically.",
    )
    gender: Mapped[Gender] = mapped_column(
        GenderType,
        nullable=False,
        index=True,
        comment="Biological sex — used for test parameter reference range selection",
    )

    # ── Contact ────────────────────────────────────────────────────────────────
    phone: Mapped[str] = mapped_column(
        String(20),
        nullable=False,
        index=True,
        comment="Phone number — primary contact AND client portal username",
    )
    email: Mapped[str | None] = mapped_column(
        String(255),
        nullable=True,
        comment="Email address — optional, used for report delivery",
    )
    address: Mapped[str | None] = mapped_column(
        Text,
        nullable=True,
        comment="Residential or mailing address",
    )
    country: Mapped[str | None] = mapped_column(
        String(100),
        nullable=True,
        comment="Country — printed on reports for patients traveling abroad",
    )

    # ── Anthropometrics ────────────────────────────────────────────────────────
    height_cm: Mapped[Decimal | None] = mapped_column(
        Numeric(5, 2),
        nullable=True,
        comment="Height in centimetres — used for body-metric reference ranges",
    )
    weight_kg: Mapped[Decimal | None] = mapped_column(
        Numeric(5, 2),
        nullable=True,
        comment="Weight in kilograms — used for body-metric reference ranges",
    )

    # ── Client Portal Credential ───────────────────────────────────────────────
    pin_hash: Mapped[str | None] = mapped_column(
        String(255),
        nullable=True,
        comment=(
            "bcrypt hash of the 6-character alphanumeric PIN. "
            "OVERWRITTEN on every visit (order creation). "
            "Client portal login: phone + this PIN → time-limited JWT."
        ),
    )
    portal_pin_enc: Mapped[str | None] = mapped_column(
        Text,
        nullable=True,
        comment=(
            "Fernet-encrypted copy of the current PIN for display on bill "
            "receipts and signed reports. Rotated together with pin_hash."
        ),
    )

    # ── Relationships ──────────────────────────────────────────────────────────
    lab: Mapped["Lab"] = relationship(  # type: ignore[name-defined]
        "Lab",
        foreign_keys="[Patient.lab_id]",
        lazy="selectin",
    )
    # Child relationships loaded on demand in the repository layer:
    # appointments, orders, notifications — defined on those models

    def __repr__(self) -> str:
        return f"<Patient id={self.id!s:.8} code={self.patient_code!r} name={self.name!r}>"
