import logging
import re
import uuid
from datetime import datetime, timedelta, timezone

from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from pydantic import BaseModel, field_validator

from app.core.deps import get_db, get_current_staff
from app.core.config import settings
from app.models.patient import Patient
from app.models.patient_department_map import PatientDepartmentMap

logger = logging.getLogger(__name__)

router = APIRouter()

MAX_DAILY_ID_LENGTH = 50
MAX_NAME_LENGTH = 100
MAX_GENDER_LENGTH = 20
MIN_AGE = 1
MAX_AGE = 120


class CreatePatientRequest(BaseModel):
    dailyPatientId: str
    name: str
    age: int
    gender: str

    @field_validator("dailyPatientId")
    @classmethod
    def strip_id(cls, v):
        v = v.strip()
        if not v:
            raise ValueError("dailyPatientId cannot be empty")
        if len(v) > MAX_DAILY_ID_LENGTH:
            raise ValueError(f"dailyPatientId max {MAX_DAILY_ID_LENGTH} chars")
        return v

    @field_validator("name")
    @classmethod
    def validate_name(cls, v):
        v = v.strip()
        if not v:
            raise ValueError("name cannot be empty")
        if len(v) > MAX_NAME_LENGTH:
            raise ValueError(f"name max {MAX_NAME_LENGTH} chars")
        if not re.match(r"^[a-zA-Z\s\-'\.]+$", v):
            raise ValueError("name must contain only letters, spaces, hyphens, or apostrophes")
        return v

    @field_validator("gender")
    @classmethod
    def validate_gender(cls, v):
        v = v.strip()
        if not v:
            raise ValueError("gender cannot be empty")
        if len(v) > MAX_GENDER_LENGTH:
            raise ValueError(f"gender max {MAX_GENDER_LENGTH} chars")
        return v

    @field_validator("age")
    @classmethod
    def valid_age(cls, v):
        if not MIN_AGE <= v <= MAX_AGE:
            raise ValueError(f"Age must be between {MIN_AGE} and {MAX_AGE}")
        return v


async def _attach_department_and_respond(
    db: AsyncSession, patient: Patient, dept_id: uuid.UUID, is_new: bool
) -> dict:
    # Ensure a dept-map entry exists for the calling staff's department
    existing_map = await db.execute(
        select(PatientDepartmentMap).where(
            PatientDepartmentMap.patient_id == patient.id,
            PatientDepartmentMap.department_id == dept_id,
        )
    )
    if not existing_map.scalar_one_or_none():
        db.add(PatientDepartmentMap(
            id=uuid.uuid4(),
            patient_id=patient.id,
            department_id=dept_id,
        ))
        await db.commit()
    return {
        "patientId": str(patient.id),
        "dailyId": patient.daily_id,
        "name": patient.name,
        "age": patient.age,
        "gender": patient.gender,
        "isNew": is_new,
    }


# ── POST /patients ─────────────────────────────────────────────────────────────

@router.post("", status_code=201)
async def create_patient(
    body: CreatePatientRequest,
    db: AsyncSession = Depends(get_db),
    staff=Depends(get_current_staff),
):
    dept_id = uuid.UUID(staff["departmentId"])
    now = datetime.now(timezone.utc)

    # Check if this patient already exists today (same daily_id + intake_date)
    existing = await db.execute(
        select(Patient).where(
            Patient.daily_id == body.dailyPatientId,
            Patient.intake_date == now.date(),
        )
    )
    patient = existing.scalar_one_or_none()

    if patient:
        logger.info("Patient already exists today, returning existing: %s", patient.id)
        return await _attach_department_and_respond(db, patient, dept_id, is_new=False)

    # Create new patient
    expires_at = now + timedelta(hours=settings.DATA_RETENTION_HOURS)
    patient = Patient(
        id=uuid.uuid4(),
        daily_id=body.dailyPatientId,
        name=body.name,
        age=body.age,
        gender=body.gender,
        expires_at=expires_at,
    )
    db.add(patient)
    try:
        await db.flush()
    except IntegrityError:
        # Another request created the same daily_id + intake_date first
        await db.rollback()
        existing = await db.execute(
            select(Patient).where(
                Patient.daily_id == body.dailyPatientId,
                Patient.intake_date == now.date(),
            )
        )
        patient = existing.scalar_one_or_none()
        if patient is None:
            raise
        logger.info("Patient creation raced with another request, returning existing: %s", patient.id)
        return await _attach_department_and_respond(db, patient, dept_id, is_new=False)

    logger.info("Patient created: %s", patient.id)
    return await _attach_department_and_respond(db, patient, dept_id, is_new=True)
