import json
import secrets
from datetime import datetime, date

from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash

from app.extensions import db

# Controlled vocabulary the LLM picks from when classifying a diagnosis-type event.
# Kept as a fixed list (rather than unrestricted free text) so counts are actually
# aggregable across thousands of documents worded in different ways.
CONDITION_CATEGORIES = (
    "Cardiovascular",
    "Endocrino/Metabólico",
    "Respiratorio",
    "Digestivo",
    "Musculoesquelético",
    "Neurológico",
    "Mental/Psiquiátrico",
    "Dermatológico",
    "Infeccioso",
    "Oncológico",
    "Renal/Urológico",
    "Ginecológico/Obstétrico",
    "Oftalmológico/ORL",
    "Materno-infantil",
    "Otro",
)

AGE_RANGES = (
    (0, 9), (10, 19), (20, 29), (30, 39), (40, 49),
    (50, 59), (60, 69), (70, 79), (80, 200),
)


def age_range_label(age: int) -> str:
    if age is None:
        return "Desconocido"
    for low, high in AGE_RANGES:
        if low <= age <= high:
            return f"{low}+" if high == 200 else f"{low}-{high}"
    return "Desconocido"


class User(UserMixin, db.Model):
    __tablename__ = "users"

    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True, nullable=False, index=True)
    password_hash = db.Column(db.String(255), nullable=False)
    role = db.Column(db.String(20), nullable=False, default="patient")  # patient | admin
    is_active_account = db.Column(db.Boolean, nullable=False, default=True)
    accepted_terms_at = db.Column(db.DateTime, nullable=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    # Telegram linking (see app/telegram/): a chat_id identifies the account once
    # linked; the link_code is a short-lived one-time code used to prove it's you.
    telegram_chat_id = db.Column(db.String(64), unique=True, nullable=True, index=True)
    telegram_link_code = db.Column(db.String(12), nullable=True, index=True)
    telegram_link_code_expires_at = db.Column(db.DateTime, nullable=True)

    caregiver_links = db.relationship(
        "CaregiverLink", back_populates="user", cascade="all, delete-orphan"
    )

    def set_password(self, raw_password: str) -> None:
        self.password_hash = generate_password_hash(raw_password)

    def check_password(self, raw_password: str) -> bool:
        return check_password_hash(self.password_hash, raw_password)

    def is_admin(self) -> bool:
        return self.role == "admin"

    def generate_telegram_link_code(self, valid_minutes: int = 15) -> str:
        from datetime import timedelta

        code = f"{secrets.randbelow(1_000_000):06d}"
        self.telegram_link_code = code
        self.telegram_link_code_expires_at = datetime.utcnow() + timedelta(minutes=valid_minutes)
        return code

    @property
    def is_active(self) -> bool:  # overrides UserMixin.is_active (default True)
        return self.is_active_account

    @property
    def patients(self):
        """All patient profiles this account can manage (self and/or family members)."""
        return [link.patient for link in self.caregiver_links]


class CaregiverLink(db.Model):
    """Who can manage which patient profile — the caregiver model.

    One account (a son/daughter, a professional caregiver, or the person
    themselves) can be linked to several patient profiles (mom, dad, self),
    and in principle a patient profile could have more than one caregiver
    (e.g. two siblings). `is_primary` marks whoever created the profile.
    """

    __tablename__ = "caregiver_links"

    RELATIONSHIP_TYPES = (
        "self", "hijo_a", "conyuge", "cuidador_profesional", "otro_familiar",
    )

    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True)
    patient_id = db.Column(db.Integer, db.ForeignKey("patients.id"), nullable=False, index=True)
    relationship_type = db.Column(db.String(30), nullable=False, default="otro_familiar")
    is_primary = db.Column(db.Boolean, nullable=False, default=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    user = db.relationship("User", back_populates="caregiver_links")
    patient = db.relationship("Patient", back_populates="caregiver_links")

    __table_args__ = (db.UniqueConstraint("user_id", "patient_id", name="uq_caregiver_user_patient"),)


class Patient(db.Model):
    __tablename__ = "patients"

    id = db.Column(db.Integer, primary_key=True)
    created_by_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
    full_name = db.Column(db.String(255), nullable=False)
    birth_date = db.Column(db.Date, nullable=True)
    national_id = db.Column(db.String(50), nullable=True)
    phone = db.Column(db.String(30), nullable=True, index=True)  # E.164, used for WhatsApp lookup
    country = db.Column(db.String(60), nullable=True)
    city = db.Column(db.String(120), nullable=True)

    # Emergency quick-access (no login required — see /emergencia/<token>).
    emergency_token = db.Column(db.String(64), unique=True, nullable=True, index=True)
    emergency_contact_name = db.Column(db.String(255), nullable=True)
    emergency_contact_phone = db.Column(db.String(30), nullable=True)
    blood_type = db.Column(db.String(10), nullable=True)

    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    caregiver_links = db.relationship(
        "CaregiverLink", back_populates="patient", cascade="all, delete-orphan"
    )
    documents = db.relationship(
        "Document", back_populates="patient", cascade="all, delete-orphan",
        order_by="Document.uploaded_at.desc()",
    )
    medical_events = db.relationship(
        "MedicalEvent", back_populates="patient", cascade="all, delete-orphan",
        order_by="MedicalEvent.event_date.desc()",
    )
    summaries = db.relationship(
        "Summary", back_populates="patient", cascade="all, delete-orphan",
        order_by="Summary.version.desc()",
    )

    def age(self):
        return self.age_at(date.today())

    def age_at(self, on_date: date):
        if not self.birth_date or not on_date:
            return None
        return on_date.year - self.birth_date.year - (
            (on_date.month, on_date.day) < (self.birth_date.month, self.birth_date.day)
        )

    def latest_summary(self):
        return self.summaries[0] if self.summaries else None

    def is_senior(self) -> bool:
        age = self.age()
        return age is not None and age >= 60

    @staticmethod
    def generate_emergency_token() -> str:
        return secrets.token_urlsafe(32)


class Document(db.Model):
    __tablename__ = "documents"

    STATUS_PENDING = "pending"
    STATUS_PROCESSING = "processing"
    STATUS_DONE = "done"
    STATUS_ERROR = "error"

    CHANNELS = ("web", "email", "whatsapp")

    id = db.Column(db.Integer, primary_key=True)
    patient_id = db.Column(db.Integer, db.ForeignKey("patients.id"), nullable=False)
    original_filename = db.Column(db.String(255), nullable=False)
    storage_path = db.Column(db.String(500), nullable=False)
    mime_type = db.Column(db.String(100), nullable=False)
    status = db.Column(db.String(20), nullable=False, default=STATUS_PENDING, index=True)
    error_message = db.Column(db.Text, nullable=True)
    source_channel = db.Column(db.String(20), nullable=False, default="web", index=True)
    uploaded_at = db.Column(db.DateTime, default=datetime.utcnow)
    processed_at = db.Column(db.DateTime, nullable=True)

    patient = db.relationship("Patient", back_populates="documents")
    medical_events = db.relationship("MedicalEvent", back_populates="document")


class IntakeEvent(db.Model):
    """Logs every inbound item across every channel (web, email, WhatsApp later) —
    the 'todo lo que se recibe' audit trail, independent of whether it could be
    matched to a patient or turned into a processable Document.

    Unmatched/rejected rows (e.g. an email from an address that isn't any
    patient's registered account) stay here for an admin to triage manually;
    they deliberately do NOT get attached to a patient automatically.

    A caregiver account can manage several patient profiles, so a plain email
    can be genuinely ambiguous about which profile it belongs to — that's
    STATUS_AMBIGUOUS_PATIENT: the file is held in a per-account pending area
    (pending_storage_path) until the caregiver picks a profile from their own
    "documentos pendientes" screen (see app/profiles/routes.py).
    """

    __tablename__ = "intake_events"

    STATUS_MATCHED = "matched"
    STATUS_UNMATCHED_SENDER = "unmatched_sender"
    STATUS_REJECTED_ATTACHMENT = "rejected_attachment"
    STATUS_AMBIGUOUS_PATIENT = "ambiguous_patient"

    id = db.Column(db.Integer, primary_key=True)
    channel = db.Column(db.String(20), nullable=False, index=True)
    status = db.Column(db.String(30), nullable=False, index=True)
    sender_identifier = db.Column(db.String(255), nullable=True)  # sender email or phone
    subject = db.Column(db.String(500), nullable=True)
    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True, index=True)
    patient_id = db.Column(db.Integer, db.ForeignKey("patients.id"), nullable=True)
    document_id = db.Column(db.Integer, db.ForeignKey("documents.id"), nullable=True)
    pending_storage_path = db.Column(db.String(500), nullable=True)
    pending_original_filename = db.Column(db.String(255), nullable=True)
    notes = db.Column(db.Text, nullable=True)
    received_at = db.Column(db.DateTime, default=datetime.utcnow)

    user = db.relationship("User")
    patient = db.relationship("Patient")
    document = db.relationship("Document")


class Doctor(db.Model):
    """A prescribing physician, extracted from document letterheads/signatures.

    Entity resolution is a simple normalized-name (+ optional license number) match —
    good enough for MVP volumes; real fuzzy matching is future work (see README).
    """

    __tablename__ = "doctors"

    id = db.Column(db.Integer, primary_key=True)
    full_name = db.Column(db.String(255), nullable=False)
    normalized_name = db.Column(db.String(255), nullable=False, index=True)
    license_number = db.Column(db.String(50), nullable=True, index=True)
    specialty = db.Column(db.String(120), nullable=True)
    health_entity_id = db.Column(db.Integer, db.ForeignKey("health_entities.id"), nullable=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    health_entity = db.relationship("HealthEntity")
    medical_events = db.relationship("MedicalEvent", back_populates="doctor")


class HealthEntity(db.Model):
    """Clinic, hospital, insurer, lab or pharmacy mentioned in a document."""

    __tablename__ = "health_entities"

    TYPES = ("clinic", "hospital", "insurer", "lab", "pharmacy", "other")

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    normalized_name = db.Column(db.String(255), nullable=False, index=True)
    entity_type = db.Column(db.String(20), nullable=False, default="other")
    country = db.Column(db.String(60), nullable=True)
    city = db.Column(db.String(120), nullable=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    medical_events = db.relationship("MedicalEvent", back_populates="health_entity")


class Medication(db.Model):
    """Catalog of prescribed products, useful to sell trend analysis to pharma labs."""

    __tablename__ = "medications"

    id = db.Column(db.Integer, primary_key=True)
    generic_name = db.Column(db.String(255), nullable=False, index=True)
    normalized_generic_name = db.Column(db.String(255), nullable=False, index=True)
    brand_name = db.Column(db.String(255), nullable=True)
    manufacturer_lab = db.Column(db.String(255), nullable=True, index=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    medical_events = db.relationship("MedicalEvent", back_populates="medication")


class MedicalEvent(db.Model):
    __tablename__ = "medical_events"

    EVENT_TYPES = (
        "diagnosis",
        "medication",
        "allergy",
        "lab_result",
        "procedure",
        "vaccination",
        "other",
    )

    id = db.Column(db.Integer, primary_key=True)
    patient_id = db.Column(db.Integer, db.ForeignKey("patients.id"), nullable=False)
    document_id = db.Column(db.Integer, db.ForeignKey("documents.id"), nullable=True)
    event_type = db.Column(db.String(30), nullable=False, default="other")
    description = db.Column(db.Text, nullable=False)
    event_date = db.Column(db.Date, nullable=True)
    structured_data_json = db.Column(db.Text, nullable=True)  # raw JSON from the LLM extraction
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    # --- Analytics / population-health fields (all optional: depend on what the
    # source document actually contained; used to build aggregated trend reports) ---
    condition_category = db.Column(db.String(60), nullable=True, index=True)
    patient_age_at_event = db.Column(db.Integer, nullable=True, index=True)
    doctor_id = db.Column(db.Integer, db.ForeignKey("doctors.id"), nullable=True, index=True)
    health_entity_id = db.Column(
        db.Integer, db.ForeignKey("health_entities.id"), nullable=True, index=True
    )
    medication_id = db.Column(
        db.Integer, db.ForeignKey("medications.id"), nullable=True, index=True
    )

    patient = db.relationship("Patient", back_populates="medical_events")
    document = db.relationship("Document", back_populates="medical_events")
    doctor = db.relationship("Doctor", back_populates="medical_events")
    health_entity = db.relationship("HealthEntity", back_populates="medical_events")
    medication = db.relationship("Medication", back_populates="medical_events")

    @property
    def structured_data(self):
        if not self.structured_data_json:
            return {}
        try:
            return json.loads(self.structured_data_json)
        except ValueError:
            return {}

    @structured_data.setter
    def structured_data(self, value):
        self.structured_data_json = json.dumps(value, ensure_ascii=False)


class Summary(db.Model):
    __tablename__ = "summaries"

    id = db.Column(db.Integer, primary_key=True)
    patient_id = db.Column(db.Integer, db.ForeignKey("patients.id"), nullable=False)
    content = db.Column(db.Text, nullable=False)
    version = db.Column(db.Integer, nullable=False, default=1)
    generated_at = db.Column(db.DateTime, default=datetime.utcnow)

    patient = db.relationship("Patient", back_populates="summaries")


class Sponsor(db.Model):
    """Tracks NGOs, pharma companies, labs and health providers funding the free service."""

    __tablename__ = "sponsors"

    TYPES = ("ngo", "pharma", "lab", "provider", "other")
    STATUSES = ("prospect", "active", "inactive")

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    sponsor_type = db.Column(db.String(20), nullable=False, default="other")
    country = db.Column(db.String(60), nullable=True)
    contact_name = db.Column(db.String(255), nullable=True)
    contact_email = db.Column(db.String(255), nullable=True)
    status = db.Column(db.String(20), nullable=False, default="prospect")
    notes = db.Column(db.Text, nullable=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
