#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import argparse
import os
import re
from typing import Dict, Optional, List

import pandas as pd

# ---- .env (backend/.env) ----
try:
    from dotenv import load_dotenv
    load_dotenv()
    load_dotenv(os.path.join(os.path.dirname(__file__), "..", ".env"))  # backend/.env
except Exception:
    pass

# ---- psycopg2 ----
try:
    import psycopg2
    import psycopg2.extras
except ImportError:
    psycopg2 = None


# -----------------------------
# 1) Mapeos
# -----------------------------

MAP_EXAMEN_PLANILLA = {
    "CONSULTA MEDICO EXPERTO": "MEDICO EXPERTO",
    "TELECONSULTA MEDICO EXPERTO": "MEDICO EXPERTO",
    "CONSULTA DE INFECTOLOGIA": "INFECTOLOGIA",
    "CONSULTA DE PSICOLOGIA": "PSICOLOGIA",
    "CONSULTA DE PSIQUIATRIA": "PSIQUIATRIA",
    "CONSULTA DE PSIQUIATRÍA": "PSIQUIATRIA",
    "CONSULTA CON ENFERMERIA": "ENFERMERIA",
    "CONSULTA CON ENFERMERÍA": "ENFERMERIA",
    "CONSULTA QUIMICO FARMACEUTICO": "QUIMICO FARMACEUTICO",
    "CONSULTA QUÍMICO FARMACÉUTICO": "QUIMICO FARMACEUTICO",
    "CONSULTA CON TRABAJO SOCIAL": "TRABAJO SOCIAL",
    "CONSULTA DE NUTRICION": "NUTRICION",
    "CONSULTA DE NUTRICIÓN": "NUTRICION",
    "CONSULTA ODONTOLOGICA": "ODONTOLOGIA",
    "CONSULTA ODONTOLÓGICA": "ODONTOLOGIA",
    "CONSULTA DE OFTALMOLOGIA": "OFTALMOLOGIA",
    "CONSULTA DE OFTALMOLOGÍA": "OFTALMOLOGIA",
}

MAP_ESPECIALIDAD_HISTOR = {
    "Medico Experto": "MEDICO EXPERTO",
    "Psicologia": "PSICOLOGIA",
    "Psiquiatria": "PSIQUIATRIA",
    "Infectologia": "INFECTOLOGIA",
    "Nutricion": "NUTRICION",
    "Trabajo Social": "TRABAJO SOCIAL",
    "Quimico Farmaceutico": "QUIMICO FARMACEUTICO",
    "Odontologo": "ODONTOLOGIA",
    "Jefe de Enfermeria": "ENFERMERIA",
    "Oftalmologia": "OFTALMOLOGIA",
}


def _norm(s: str) -> str:
    if s is None:
        return ""
    s = str(s).strip()
    s = re.sub(r"\s+", " ", s)
    s = s.upper()
    rep = str.maketrans("ÁÉÍÓÚÜÑ", "AEIOUUN")
    return s.translate(rep)


def map_especialidad_planilla(examen: str) -> Optional[str]:
    k = _norm(examen)
    if k in MAP_EXAMEN_PLANILLA:
        return MAP_EXAMEN_PLANILLA[k]
    for kk, vv in MAP_EXAMEN_PLANILLA.items():
        if kk in k:
            return vv
    return None


def map_modalidad_planilla(examen: str) -> str:
    k = _norm(examen)
    return "TELEMEDICINA" if "TELE" in k else "PRESENCIAL"


def map_especialidad_histor(esp: str) -> Optional[str]:
    if esp in MAP_ESPECIALIDAD_HISTOR:
        return MAP_ESPECIALIDAD_HISTOR[esp]
    kn = _norm(esp)
    for k, v in MAP_ESPECIALIDAD_HISTOR.items():
        if _norm(k) == kn:
            return v
    return None


# -----------------------------
# 2) Helpers Excel
# -----------------------------

def _buscar_col(df: pd.DataFrame, nombre: str) -> Optional[str]:
    cols = list(df.columns)
    limpias = {c: str(c).strip() for c in cols}
    for c, cl in limpias.items():
        if cl == nombre:
            return c
    for c, cl in limpias.items():
        if cl.startswith(nombre):
            return c
    return None


def _limpiar_doc(v) -> str:
    if pd.isna(v):
        return ""
    return str(v).split(".")[0].strip()


def _limpiar_texto(v) -> str:
    if pd.isna(v):
        return ""
    s = str(v).strip()
    return "" if s.lower() in ("nan", "none") else s


def _primero_no_vacio(serie: pd.Series) -> str:
    for x in serie.tolist():
        s = _limpiar_texto(x)
        if s:
            return s
    return ""


def _a_int_o_none(v) -> Optional[int]:
    s = _limpiar_texto(v)
    if not s:
        return None
    s = s.split(".")[0]
    return int(s) if s.isdigit() else None


# -----------------------------
# 3) Cargar Excels
# -----------------------------

def cargar_planilla(path: str, sheets: Optional[List[str]] = None) -> pd.DataFrame:
    xls = pd.ExcelFile(path)
    usar = sheets or xls.sheet_names

    frames = []
    for sh in usar:
        df = pd.read_excel(path, sheet_name=sh)
        col_doc = _buscar_col(df, "Documento")
        col_fecha = _buscar_col(df, "Fecha Cita")
        if not col_doc or not col_fecha:
            continue
        df["__sheet"] = sh
        frames.append(df)

    if not frames:
        raise RuntimeError("No se encontraron hojas válidas en PLANILLA (Documento y Fecha Cita).")

    df = pd.concat(frames, ignore_index=True).copy()

    col_sede = _buscar_col(df, "Sede")
    col_prof = _buscar_col(df, "Nombre Profesional")
    col_examen = _buscar_col(df, "Examen")
    col_fecha = _buscar_col(df, "Fecha Cita")
    col_estado = _buscar_col(df, "Estado")
    col_doc = _buscar_col(df, "Documento")
    col_pac = _buscar_col(df, "Paciente")
    col_cel = _buscar_col(df, "Celular")
    col_cor = _buscar_col(df, "Correo")

    col_tdoc = _buscar_col(df, "T. Doc") or _buscar_col(df, "TIPO") or _buscar_col(df, "Tipo")
    col_nplan = (
        _buscar_col(df, "Numero de planilla")
        or _buscar_col(df, "Número de planilla")
        or _buscar_col(df, "numero_planilla")
        or _buscar_col(df, "numero_plantilla")
        or _buscar_col(df, "numero_palntilla")
    )

    df["Documento"] = df[col_doc].apply(_limpiar_doc)

    df["Fecha Cita"] = pd.to_datetime(df[col_fecha], errors="coerce", dayfirst=True)
    vacias = int(df["Fecha Cita"].isna().sum())
    if vacias:
        print(f"⚠️  PLANILLA: {vacias} filas sin Fecha Cita (o inválidas). Se omiten.")
    df = df[df["Fecha Cita"].notna()].copy()

    df["fecha_cita"] = df["Fecha Cita"].dt.date
    df["hora_cita"] = df["Fecha Cita"].dt.floor("min").dt.time

    df["Examen"] = df[col_examen].astype(str) if col_examen else ""
    df["especialidad_db"] = df["Examen"].apply(map_especialidad_planilla)
    df["modalidad_db"] = df["Examen"].apply(map_modalidad_planilla)

    df["Sede"] = df[col_sede].astype(str).fillna("").str.strip() if col_sede else ""
    df["Nombre Profesional"] = df[col_prof].astype(str).fillna("").str.strip() if col_prof else ""
    df["Estado"] = df[col_estado].astype(str).fillna("").str.strip() if col_estado else ""
    df["Paciente"] = df[col_pac].astype(str).fillna("").str.strip() if col_pac else ""
    df["Celular"] = df[col_cel].astype(str).fillna("").str.strip() if col_cel else ""
    df["Correo"] = df[col_cor].astype(str).fillna("").str.strip() if col_cor else ""
    df["tipo_documento"] = df[col_tdoc].astype(str).fillna("").str.strip() if col_tdoc else ""

    # lo guardamos como numero_plantilla en el DF (independiente de cómo venga en Excel)
    if col_nplan:
        df["numero_plantilla"] = df[col_nplan].apply(_a_int_o_none)
    else:
        df["numero_plantilla"] = None

    df = df[(df["Documento"] != "") & df["especialidad_db"].notna()].copy()
    return df


def cargar_histor(path: str, sheet: Optional[str] = None) -> pd.DataFrame:
    sh = sheet or pd.ExcelFile(path).sheet_names[0]
    df = pd.read_excel(path, sheet_name=sh)

    if "Numero" not in df.columns or "Fecha Actividad" not in df.columns or "Especialidad" not in df.columns:
        raise RuntimeError("HISTOR debe tener: Numero, Fecha Actividad, Especialidad.")

    df["Numero"] = df["Numero"].apply(_limpiar_doc)

    col_id = _buscar_col(df, "Identificación") or _buscar_col(df, "Identificacion")
    if col_id:
        df["tipo_documento"] = df[col_id].apply(_limpiar_texto)
        df["tipo_documento"] = df["tipo_documento"].apply(
            lambda s: s if re.fullmatch(r"[A-Za-z]{1,5}", s) else ""
        )
    else:
        df["tipo_documento"] = ""

    df["especialidad_db"] = df["Especialidad"].apply(map_especialidad_histor)

    df["Fecha Actividad"] = pd.to_datetime(df["Fecha Actividad"], errors="coerce", dayfirst=True)
    vacias = int(df["Fecha Actividad"].isna().sum())
    if vacias:
        print(f"⚠️  HISTOR: {vacias} filas sin Fecha Actividad (o inválidas). Se omiten.")
    df = df[df["Fecha Actividad"].notna()].copy()

    df["fecha_cita"] = df["Fecha Actividad"].dt.date
    df["hora_cita"] = df["Fecha Actividad"].dt.floor("min").dt.time

    df = df[(df["Numero"] != "") & df["especialidad_db"].notna()].copy()
    return df


# -----------------------------
# 4) BD
# -----------------------------

def _get_db_url(args_db_url: Optional[str]) -> Optional[str]:
    if args_db_url:
        return args_db_url
    if os.getenv("DATABASE_URL"):
        return os.getenv("DATABASE_URL")

    host = os.getenv("PGHOST")
    user = os.getenv("PGUSER")
    pwd = os.getenv("PGPASSWORD")
    db = os.getenv("PGDATABASE")
    port = os.getenv("PGPORT", "5432")

    if all([host, user, pwd, db]):
        return f"postgresql://{user}:{pwd}@{host}:{port}/{db}"
    return None


def conectar(db_url: str):
    if psycopg2 is None:
        raise RuntimeError("Instala: python -m pip install psycopg2-binary")
    return psycopg2.connect(db_url)


def preparar_bd(conn):
    sql = """
    UPDATE cita SET hora_cita = '00:00'::time WHERE hora_cita IS NULL;

    DO $$
    BEGIN
      IF NOT EXISTS (
        SELECT 1 FROM pg_constraint WHERE conname = 'uq_cita_paciente_esp_fecha_hora'
      ) THEN
        ALTER TABLE cita
        ADD CONSTRAINT uq_cita_paciente_esp_fecha_hora
        UNIQUE (numero_documento, id_especialidad, fecha_cita, hora_cita);
      END IF;
    END $$;

    CREATE INDEX IF NOT EXISTS idx_cita_doc_fecha ON cita (numero_documento, fecha_cita);
    """
    with conn.cursor() as cur:
        cur.execute(sql)
    conn.commit()


def cargar_mapa_especialidades(conn) -> Dict[str, int]:
    with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
        cur.execute("SELECT id_especialidad, nombre FROM especialidad;")
        rows = cur.fetchall()
    return {_norm(r["nombre"]): int(r["id_especialidad"]) for r in rows}


def detectar_columna_plantilla(conn) -> Optional[str]:
    """
    Devuelve el nombre real de la columna de planilla en paciente:
    numero_plantilla / numero_planilla / numero_palntilla
    """
    candidatos = ["numero_plantilla", "numero_planilla", "numero_palntilla"]
    sql = """
    SELECT column_name
    FROM information_schema.columns
    WHERE table_schema='public'
      AND table_name='paciente'
      AND column_name = ANY(%s);
    """
    with conn.cursor() as cur:
        cur.execute(sql, (candidatos,))
        rows = [r[0] for r in cur.fetchall()]

    for c in candidatos:
        if c in rows:
            return c
    return None


def upsert_pacientes_desde_planilla(conn, df_plan: pd.DataFrame, col_planilla_bd: Optional[str]):
    if df_plan.empty:
        return

    sub = df_plan.copy()
    sub["doc"] = sub["Documento"].astype(str).str.strip()

    agg = {
        "tipo_documento": _primero_no_vacio,
        "Paciente": _primero_no_vacio,
        "Celular": _primero_no_vacio,
        "Correo": _primero_no_vacio,
        "numero_plantilla": lambda s: next((x for x in s.tolist() if pd.notna(x)), None),
    }
    sub = sub.groupby("doc", as_index=False).agg(agg)

    # armar SQL dinámico según exista columna de planilla en BD
    if col_planilla_bd:
        sql = f"""
        INSERT INTO paciente (numero_documento, tipo_documento, {col_planilla_bd}, nombre_completo, celular, correo)
        VALUES (%s,%s,%s,%s,%s,%s)
        ON CONFLICT (numero_documento)
        DO UPDATE SET
          tipo_documento = COALESCE(NULLIF(EXCLUDED.tipo_documento,''), paciente.tipo_documento),
          {col_planilla_bd} = COALESCE(EXCLUDED.{col_planilla_bd}, paciente.{col_planilla_bd}),
          nombre_completo = COALESCE(NULLIF(EXCLUDED.nombre_completo,''), paciente.nombre_completo),
          celular = COALESCE(NULLIF(EXCLUDED.celular,''), paciente.celular),
          correo  = COALESCE(NULLIF(EXCLUDED.correo,''), paciente.correo);
        """
    else:
        sql = """
        INSERT INTO paciente (numero_documento, tipo_documento, nombre_completo, celular, correo)
        VALUES (%s,%s,%s,%s,%s)
        ON CONFLICT (numero_documento)
        DO UPDATE SET
          tipo_documento = COALESCE(NULLIF(EXCLUDED.tipo_documento,''), paciente.tipo_documento),
          nombre_completo = COALESCE(NULLIF(EXCLUDED.nombre_completo,''), paciente.nombre_completo),
          celular = COALESCE(NULLIF(EXCLUDED.celular,''), paciente.celular),
          correo  = COALESCE(NULLIF(EXCLUDED.correo,''), paciente.correo);
        """

    data = []
    for _, r in sub.iterrows():
        doc = _limpiar_texto(r["doc"])
        if not doc:
            continue

        tdoc = _limpiar_texto(r.get("tipo_documento", ""))
        nom = _limpiar_texto(r.get("Paciente", ""))
        cel = _limpiar_texto(r.get("Celular", ""))
        cor = _limpiar_texto(r.get("Correo", ""))

        if col_planilla_bd:
            nplan = r.get("numero_plantilla", None)
            data.append((doc, tdoc, nplan, nom, cel, cor))
        else:
            data.append((doc, tdoc, nom, cel, cor))

    with conn.cursor() as cur:
        psycopg2.extras.execute_batch(cur, sql, data, page_size=800)
    conn.commit()
    print(f"✅ Pacientes PLANILLA upsert: {len(data)}")


def upsert_pacientes_desde_histor(conn, df_hist: pd.DataFrame):
    if df_hist.empty:
        return

    sub = df_hist.copy()
    sub["doc"] = sub["Numero"].astype(str).str.strip()
    sub = sub.groupby("doc", as_index=False).agg({"tipo_documento": _primero_no_vacio})

    sql = """
    INSERT INTO paciente (numero_documento, tipo_documento, nombre_completo, celular, correo)
    VALUES (%s,%s,'','','')
    ON CONFLICT (numero_documento)
    DO UPDATE SET
      tipo_documento = COALESCE(NULLIF(EXCLUDED.tipo_documento,''), paciente.tipo_documento);
    """

    data = []
    for _, r in sub.iterrows():
        doc = _limpiar_texto(r["doc"])
        if not doc:
            continue
        tdoc = _limpiar_texto(r.get("tipo_documento", ""))
        data.append((doc, tdoc))

    with conn.cursor() as cur:
        psycopg2.extras.execute_batch(cur, sql, data, page_size=1200)
    conn.commit()
    print(f"✅ Pacientes HISTOR asegurados: {len(data)}")


def upsert_citas_planilla(conn, df_plan: pd.DataFrame, mapa_esp: Dict[str, int]):
    sql = """
    INSERT INTO cita (
      numero_documento, id_especialidad, fecha_cita, hora_cita,
      tipo_cita, modalidad, asistio, observaciones
    )
    VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
    ON CONFLICT (numero_documento, id_especialidad, fecha_cita, hora_cita)
    DO UPDATE SET
      modalidad = EXCLUDED.modalidad,
      tipo_cita = COALESCE(EXCLUDED.tipo_cita, cita.tipo_cita),
      observaciones = EXCLUDED.observaciones
    RETURNING id_cita;
    """

    data = []
    for _, r in df_plan.iterrows():
        esp = r["especialidad_db"]
        esp_id = mapa_esp.get(_norm(esp))
        if not esp_id:
            continue

        obs = {
            "fuente": "PLANILLA",
            "sede": _limpiar_texto(r.get("Sede", "")),
            "profesional": _limpiar_texto(r.get("Nombre Profesional", "")),
            "estado_planilla": _limpiar_texto(r.get("Estado", "")),
            "sheet": _limpiar_texto(r.get("__sheet", "")),
        }
        obs_txt = " | ".join([f"{k}={v}" for k, v in obs.items() if v])

        data.append((
            _limpiar_texto(r["Documento"]),
            esp_id,
            r["fecha_cita"],
            r["hora_cita"],
            None,
            r["modalidad_db"],
            False,
            obs_txt
        ))

    with conn.cursor() as cur:
        for i in range(0, len(data), 700):
            bloque = data[i:i+700]
            psycopg2.extras.execute_batch(cur, sql, bloque, page_size=300)
    conn.commit()
    print(f"✅ Citas PLANILLA upsert: {len(data)}")


def marcar_asistencia_histor(conn, df_hist: pd.DataFrame, mapa_esp: Dict[str, int], tolerancia_min: int = 60) -> pd.DataFrame:
    resultados = []

    sql_buscar_exacta = """
      SELECT id_cita
      FROM cita
      WHERE numero_documento::text = %s
        AND id_especialidad = %s
        AND fecha_cita = %s
        AND hora_cita = %s::time
      LIMIT 1;
    """

    sql_buscar_cercana = """
      SELECT id_cita,
             ABS(EXTRACT(EPOCH FROM (hora_cita - %s::time))) AS diff
      FROM cita
      WHERE numero_documento::text = %s
        AND id_especialidad = %s
        AND fecha_cita = %s
        AND hora_cita IS NOT NULL
      ORDER BY diff ASC
      LIMIT 1;
    """

    sql_update = """
      UPDATE cita
      SET asistio = TRUE
      WHERE id_cita = %s
      RETURNING id_cita;
    """

    sql_insert = """
      INSERT INTO cita (
        numero_documento, id_especialidad, fecha_cita, hora_cita,
        tipo_cita, modalidad, asistio, observaciones
      )
      VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
      ON CONFLICT (numero_documento, id_especialidad, fecha_cita, hora_cita)
      DO UPDATE SET asistio = TRUE
      RETURNING id_cita;
    """

    tolerancia_seg = tolerancia_min * 60

    with conn.cursor() as cur:
        for _, r in df_hist.iterrows():
            doc = _limpiar_texto(r["Numero"])
            esp = r["especialidad_db"]
            esp_id = mapa_esp.get(_norm(esp))

            if not doc or not esp_id:
                resultados.append({**r.to_dict(), "accion": "SIN_DOC_O_ESPECIALIDAD"})
                continue

            f = r["fecha_cita"]
            h = r["hora_cita"]

            cur.execute(sql_buscar_exacta, (doc, esp_id, f, h.strftime("%H:%M")))
            row = cur.fetchone()
            if row:
                cur.execute(sql_update, (row[0],))
                resultados.append({**r.to_dict(), "accion": "UPDATE_EXACTA", "id_cita": row[0]})
                continue

            cur.execute(sql_buscar_cercana, (h.strftime("%H:%M"), doc, esp_id, f))
            row = cur.fetchone()
            if row and row[1] is not None and float(row[1]) <= tolerancia_seg:
                cur.execute(sql_update, (row[0],))
                resultados.append({**r.to_dict(), "accion": "UPDATE_CERCANA", "id_cita": row[0], "diff_seg": float(row[1])})
                continue

            obs = "fuente=HISTOR"
            cur.execute(sql_insert, (
                doc, esp_id, f, h.strftime("%H:%M"),
                "HISTOR", "PRESENCIAL", True, obs
            ))
            new_id = cur.fetchone()[0]
            resultados.append({**r.to_dict(), "accion": "INSERT", "id_cita": new_id})

    conn.commit()
    return pd.DataFrame(resultados)


# -----------------------------
# 5) Main
# -----------------------------

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--planilla", required=True)
    ap.add_argument("--histor", required=True)
    ap.add_argument("--db-url", default=None)
    ap.add_argument("--sheets", default=None)
    ap.add_argument("--tolerancia-min", type=int, default=60)
    ap.add_argument("--outdir", default="salida_importacion")
    args = ap.parse_args()

    os.makedirs(args.outdir, exist_ok=True)

    sheets = None
    if args.sheets:
        sheets = [s.strip() for s in args.sheets.split(",") if s.strip()]

    print("📥 Leyendo PLANILLA...")
    df_plan = cargar_planilla(args.planilla, sheets=sheets)
    print("   Filas válidas:", len(df_plan))

    print("📥 Leyendo HISTOR...")
    df_hist = cargar_histor(args.histor)
    print("   Filas válidas:", len(df_hist))

    db_url = _get_db_url(args.db_url)
    if not db_url:
        raise SystemExit("❌ No se pudo determinar DB URL. Revisa tu .env")

    print("🔌 Conectando a BD...")
    conn = conectar(db_url)

    try:
        print("🧱 Preparando BD (UNIQUE/índices)...")
        preparar_bd(conn)

        mapa_esp = cargar_mapa_especialidades(conn)
        print("   Especialidades en BD:", len(mapa_esp))

        col_planilla_bd = detectar_columna_plantilla(conn)
        if col_planilla_bd:
            print(f"🧩 Columna de planilla detectada en BD: {col_planilla_bd}")
        else:
            print("⚠️  No encontré columna de planilla en paciente (numero_plantilla/numero_planilla/numero_palntilla).")

        print("🧍 Upsert pacientes desde PLANILLA (sin duplicar)...")
        upsert_pacientes_desde_planilla(conn, df_plan, col_planilla_bd)

        print("🧍 Asegurando pacientes que aparecen en HISTOR...")
        upsert_pacientes_desde_histor(conn, df_hist)

        print("⬆️  Upsert de citas (PLANILLA)...")
        upsert_citas_planilla(conn, df_plan, mapa_esp)

        print("✅ Marcando asistencias (HISTOR)...")
        df_res = marcar_asistencia_histor(conn, df_hist, mapa_esp, tolerancia_min=args.tolerancia_min)

        df_plan.to_csv(os.path.join(args.outdir, "planilla_normalizada.csv"), index=False, encoding="utf-8-sig")
        df_hist.to_csv(os.path.join(args.outdir, "histor_normalizada.csv"), index=False, encoding="utf-8-sig")
        df_res.to_csv(os.path.join(args.outdir, "resultado_histor_vs_bd.csv"), index=False, encoding="utf-8-sig")

        resumen = df_res["accion"].value_counts().reset_index()
        resumen.columns = ["accion", "cantidad"]
        resumen.to_csv(os.path.join(args.outdir, "resumen_acciones.csv"), index=False, encoding="utf-8-sig")

        print("📄 Reportes guardados en:", args.outdir)
        print(resumen)

    finally:
        conn.close()


if __name__ == "__main__":
    main()
