"""Claim data models — ClaimData, ClaimLineData, DiagnosisCode."""

from __future__ import annotations

import re
from datetime import date

from pydantic import BaseModel, ConfigDict, field_validator, model_validator

from claim_validator.shared.validators.npi import _check_luhn_npi

# Pre-compiled patterns for validation
_ICD10_RE = re.compile(r"^[A-Z]\d{2}(\.[A-Z0-9]{1,4})?$")
_CPT_HCPCS_RE = re.compile(r"^\d{5}$|^[A-Z]\d{4}$")
_MODIFIER_RE = re.compile(r"^[A-Z0-9]{2}$")
_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")

_VALID_DX_TYPES = frozenset(
    ("principal", "admitting", "secondary", "external_cause", "other")
)
_VALID_GENDERS = frozenset(("M", "F", "U"))


def _validate_date_not_future(v: str | None, field_name: str) -> str | None:
    """Validate YYYY-MM-DD format and ensure the date is not in the future."""
    if v is None:
        return v
    if not _DATE_RE.match(v):
        msg = f"{field_name} must be in YYYY-MM-DD format"
        raise ValueError(msg)
    try:
        parsed = date.fromisoformat(v)
    except ValueError:
        msg = f"{field_name} is not a valid calendar date"
        raise ValueError(msg) from None
    if parsed > date.today():
        msg = f"{field_name} must not be in the future"
        raise ValueError(msg)
    return v


def _validate_npi_field(v: str | None) -> str | None:
    """Validate an NPI: 10 digits + Luhn check."""
    if v is None:
        return v
    if len(v) != 10 or not v.isdigit():
        msg = "NPI must be exactly 10 numeric digits"
        raise ValueError(msg)
    if not _check_luhn_npi(v):
        msg = "NPI fails Luhn check-digit validation"
        raise ValueError(msg)
    return v


class DiagnosisCode(BaseModel):
    """ICD-10 diagnosis code with pointer position and type."""

    model_config = ConfigDict(frozen=True, strict=False)

    code: str
    pointer: int = 1
    type: str = "principal"

    @field_validator("code")
    @classmethod
    def _normalize_and_validate_code(cls, v: str) -> str:
        v = v.strip().upper()
        if not _ICD10_RE.match(v):
            msg = "Invalid ICD-10 code format (expected e.g. J06.9 or A00)"
            raise ValueError(msg)
        return v

    @field_validator("pointer")
    @classmethod
    def _validate_pointer(cls, v: int) -> int:
        if not 1 <= v <= 12:
            msg = "Diagnosis pointer must be between 1 and 12"
            raise ValueError(msg)
        return v

    @field_validator("type")
    @classmethod
    def _validate_type(cls, v: str) -> str:
        if v not in _VALID_DX_TYPES:
            msg = (
                f"Invalid diagnosis type '{v}'; "
                f"must be one of {sorted(_VALID_DX_TYPES)}"
            )
            raise ValueError(msg)
        return v


class ClaimLineData(BaseModel):
    """Single service line on a CMS-1500 claim."""

    model_config = ConfigDict(frozen=True, strict=False)

    procedure_code: str
    modifiers: list[str] = []
    diagnosis_pointers: list[int] = []
    charge_amount: float
    units: float = 1.0
    service_date_from: str | None = None
    service_date_to: str | None = None
    place_of_service: str | None = None
    rendering_provider_npi: str | None = None

    @field_validator("procedure_code")
    @classmethod
    def _normalize_and_validate_procedure_code(cls, v: str) -> str:
        v = v.strip().upper()
        if not _CPT_HCPCS_RE.match(v):
            msg = "Invalid CPT/HCPCS code format (expected 5 digits or letter+4 digits)"
            raise ValueError(msg)
        return v

    @field_validator("charge_amount")
    @classmethod
    def _validate_charge_amount(cls, v: float) -> float:
        if v <= 0:
            msg = "Charge amount must be positive"
            raise ValueError(msg)
        if v > 999999.99:
            msg = "Charge amount must not exceed 999999.99"
            raise ValueError(msg)
        return round(v, 2)

    @field_validator("units")
    @classmethod
    def _validate_units(cls, v: float) -> float:
        if not 0.5 <= v <= 9999:
            msg = "Units must be between 0.5 and 9999"
            raise ValueError(msg)
        return v

    @field_validator("modifiers")
    @classmethod
    def _validate_modifiers(cls, v: list[str]) -> list[str]:
        result = []
        for mod in v:
            mod = mod.strip().upper()
            if not _MODIFIER_RE.match(mod):
                msg = f"Invalid modifier '{mod}'; each modifier must be 2 alphanumeric characters"
                raise ValueError(msg)
            result.append(mod)
        return result

    @field_validator("service_date_from")
    @classmethod
    def _validate_service_date_from(cls, v: str | None) -> str | None:
        return _validate_date_not_future(v, "service_date_from")

    @field_validator("service_date_to")
    @classmethod
    def _validate_service_date_to(cls, v: str | None) -> str | None:
        return _validate_date_not_future(v, "service_date_to")

    @field_validator("rendering_provider_npi")
    @classmethod
    def _validate_rendering_npi(cls, v: str | None) -> str | None:
        return _validate_npi_field(v)


class ClaimData(BaseModel):
    """CMS-1500 professional healthcare claim."""

    model_config = ConfigDict(frozen=True, strict=False)

    # Provider information
    billing_provider_npi: str | None = None
    billing_provider_taxonomy: str | None = None
    rendering_provider_npi: str | None = None

    # Subscriber / insurance
    subscriber_id: str | None = None
    subscriber_first_name: str | None = None
    subscriber_last_name: str | None = None
    subscriber_dob: str | None = None
    subscriber_gender: str | None = None

    # Patient demographics
    patient_first_name: str | None = None
    patient_last_name: str | None = None
    patient_dob: str | None = None
    patient_gender: str | None = None
    patient_relationship: str | None = None

    # Payer
    payer_id: str | None = None
    payer_name: str | None = None

    # Claim metadata
    claim_type: str = "professional"
    place_of_service: str | None = None
    total_charge: float | None = None
    filing_date: str | None = None

    # Clinical data
    diagnosis_codes: list[DiagnosisCode] = []
    lines: list[ClaimLineData] = []

    @field_validator("billing_provider_npi", "rendering_provider_npi")
    @classmethod
    def _validate_npi(cls, v: str | None) -> str | None:
        return _validate_npi_field(v)

    @field_validator("patient_gender", "subscriber_gender")
    @classmethod
    def _validate_gender(cls, v: str | None) -> str | None:
        if v is None:
            return v
        v = v.strip().upper()
        if v not in _VALID_GENDERS:
            msg = f"Gender must be one of M, F, U (got '{v}')"
            raise ValueError(msg)
        return v

    @field_validator("patient_dob", "subscriber_dob")
    @classmethod
    def _validate_dob(cls, v: str | None) -> str | None:
        return _validate_date_not_future(v, "date of birth")

    @field_validator("filing_date")
    @classmethod
    def _validate_filing_date(cls, v: str | None) -> str | None:
        return _validate_date_not_future(v, "filing_date")

    @field_validator("total_charge")
    @classmethod
    def _validate_total_charge(cls, v: float | None) -> float | None:
        if v is None:
            return v
        if v <= 0:
            msg = "Total charge must be positive"
            raise ValueError(msg)
        return round(v, 2)

    @model_validator(mode="after")
    def _check_required_collections(self) -> ClaimData:
        if not self.diagnosis_codes:
            msg = "At least one diagnosis code is required"
            raise ValueError(msg)
        if not self.lines:
            msg = "At least one claim line is required"
            raise ValueError(msg)
        return self
