// backend/src/server.js
const express = require('express');
const cors = require('cors');
const path = require('path');
const fs = require('fs');
const multer = require('multer');
const { spawn } = require('child_process');
const readline = require('readline');

const { query } = require('./db');
require('dotenv').config();

const api = express();
const PUERTO = process.env.PORT || 2800;

// ==============================
// Middlewares
// ==============================
api.use(cors());
api.use(express.json());

// Ping simple
api.get('/', (req, res) => {
  res.send('API AUNARSalud OK ✅');
});

// ==============================
// LOG IMPORT (consola + frontend)
// ==============================
let importLog = [];        // líneas del último proceso
let importLogInfo = { running: false, inicio: null, fin: null };

function logImport(linea) {
  const ts = new Date().toISOString().slice(11, 19); // HH:MM:SS
  const txt = `[${ts}] ${linea}`;
  importLog.push(txt);
  if (importLog.length > 1500) importLog.shift();
  console.log(txt);
}

api.get('/api/importacion/log', (req, res) => {
  res.json({ ...importLogInfo, lines: importLog });
});

// ==============================
// Helpers Estado Importación (planilla/histor)
// ==============================
const uploadsDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true });

const salidaDir = path.join(__dirname, 'salida_importacion');
if (!fs.existsSync(salidaDir)) fs.mkdirSync(salidaDir, { recursive: true });

const STATE_FILE = path.join(__dirname, 'import_state.json');

function leerEstado() {
  try {
    if (!fs.existsSync(STATE_FILE)) return {};
    return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
  } catch {
    return {};
  }
}

function guardarEstado(s) {
  fs.writeFileSync(STATE_FILE, JSON.stringify(s, null, 2), 'utf-8');
}

function estadoParaFrontend(s) {
  return {
    planilla_cargada: !!s.planilla_path,
    histor_cargada: !!s.histor_path,
    planilla_nombre: s.planilla_nombre || null,
    histor_nombre: s.histor_nombre || null,
    ultima_sync: s.ultima_sync || null,
    ultima_sync_resultado: s.ultima_sync_resultado || null,
  };
}

// ==============================
// Multer (subida de archivos)
// ==============================
const storage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, uploadsDir),
  filename: (req, file, cb) => {
    const base = file.originalname.replace(/[^\w.\-() ]+/g, '_');
    const ts = Date.now();
    cb(null, `${ts}__${base}`);
  }
});
const upload = multer({ storage });

// ==============================
// Esquema + Estatus (6 meses)
// ==============================
async function asegurarColumnasPaciente() {
  try {
    await query(`ALTER TABLE paciente ADD COLUMN IF NOT EXISTS estatus varchar(20) DEFAULT 'ACTIVO';`);
    await query(`ALTER TABLE paciente ADD COLUMN IF NOT EXISTS fecha_ultima_actividad date;`);
  } catch (e) {
    console.warn('⚠️ No pude asegurar columnas (estatus/fecha_ultima_actividad) en paciente:', e.message);
  }
}

async function actualizarEstatusPacientes() {
  await asegurarColumnasPaciente();

  const r = await query(`
    WITH ult AS (
      SELECT
        p.numero_documento,
        NULLIF(
          GREATEST(
            COALESCE(MAX(c.fecha_cita)::date, '1900-01-01'::date),
            COALESCE(t.fecha_ingreso_ips::date, '1900-01-01'::date)
          ),
          '1900-01-01'::date
        ) AS last_act
      FROM paciente p
      LEFT JOIN cita c ON c.numero_documento = p.numero_documento
      LEFT JOIN tiempo t ON t.numero_documento = p.numero_documento
      GROUP BY p.numero_documento, t.fecha_ingreso_ips
    ),
    upd AS (
      UPDATE paciente p
      SET
        fecha_ultima_actividad = u.last_act,
        estatus = CASE
          WHEN u.last_act IS NULL THEN COALESCE(p.estatus,'ACTIVO')
          WHEN u.last_act < (CURRENT_DATE - INTERVAL '6 months') THEN 'ARCHIVADO'
          ELSE 'ACTIVO'
        END
      FROM ult u
      WHERE p.numero_documento = u.numero_documento
      RETURNING p.estatus
    )
    SELECT
      SUM(CASE WHEN estatus='ACTIVO' THEN 1 ELSE 0 END)::int AS activos,
      SUM(CASE WHEN estatus='ARCHIVADO' THEN 1 ELSE 0 END)::int AS archivados
    FROM upd;
  `);

  return r.rows?.[0] || { activos: 0, archivados: 0 };
}

api.post('/api/pacientes/actualizar-estatus', async (req, res) => {
  try {
    const conteo = await actualizarEstatusPacientes();
    res.json({ ok: true, ...conteo });
  } catch (e) {
    console.error('Error actualizando estatus:', e);
    res.status(500).json({ ok: false, error: e.message });
  }
});

// ==============================
// Ejecutar Python Import (manual)
// ==============================
let importEnCurso = false;

function correrImportPython(planillaPath, historPath) {
  return new Promise((resolve, reject) => {
    const scriptPath = path.join(__dirname, 'importar_citas_cac.py');

    const cmd = process.env.PYTHON || (process.platform === 'win32' ? 'py' : 'python3');
    const args = [];
    if (process.platform === 'win32' && cmd === 'py') args.push('-3');

    args.push(
      scriptPath,
      '--planilla', planillaPath,
      '--histor', historPath,
      '--outdir', salidaDir
    );

    importLog = [];
    importLogInfo = { running: true, inicio: new Date().toISOString(), fin: null };

    logImport(`🚀 Iniciando importación Python...`);
    logImport(`📄 PLANILLA: ${path.basename(planillaPath)}`);
    logImport(`📄 HISTOR: ${path.basename(historPath)}`);

    const proc = spawn(cmd, args, { cwd: __dirname });

    let stdoutAll = '';
    let stderrAll = '';

    const rlOut = readline.createInterface({ input: proc.stdout });
    rlOut.on('line', (line) => {
      stdoutAll += line + '\n';
      logImport(`PY> ${line}`);
    });

    const rlErr = readline.createInterface({ input: proc.stderr });
    rlErr.on('line', (line) => {
      stderrAll += line + '\n';
      logImport(`PY_ERR> ${line}`);
    });

    proc.on('close', (code) => {
      importLogInfo.running = false;
      importLogInfo.fin = new Date().toISOString();

      if (code === 0) {
        logImport(`✅ Python terminó OK (code ${code})`);
        resolve({ code, stdout: stdoutAll, stderr: stderrAll });
      } else {
        logImport(`❌ Python terminó con error (code ${code})`);
        reject({ code, stdout: stdoutAll, stderr: stderrAll });
      }
    });
  });
}

// ==============================
// Importación: estado + subir archivos
// ==============================
api.get('/api/importacion/estado', (req, res) => {
  const s = leerEstado();
  res.json(estadoParaFrontend(s));
});

// ✅ Subir PLANILLA (SOLO guarda y registra estado; NO procesa)
api.post('/api/importacion/planilla', upload.single('file'), async (req, res) => {
  try {
    if (!req.file) return res.status(400).json({ ok: false, error: 'No llegó archivo (file).' });

    logImport(`📥 Archivo PLANILLA cargado: ${req.file.originalname}`);

    const s = leerEstado();
    s.planilla_path = req.file.path;
    s.planilla_nombre = req.file.originalname;
    s.planilla_subida_en = new Date().toISOString();
    guardarEstado(s);

    return res.json({
      ok: true,
      mensaje: '✅ Planilla cargada. Lista para procesar cuando tú quieras.',
      estado: estadoParaFrontend(s)
    });
  } catch (err) {
    console.error('Error POST /api/importacion/planilla:', err);
    res.status(500).json({ ok: false, mensaje: 'Error subiendo planilla.', detalle: err?.message });
  }
});

// ✅ Subir HISTOR (SOLO guarda y registra estado; NO procesa)
api.post('/api/importacion/histor', upload.single('file'), async (req, res) => {
  try {
    if (!req.file) return res.status(400).json({ ok: false, error: 'No llegó archivo (file).' });

    logImport(`📥 Archivo HISTOR cargado: ${req.file.originalname}`);

    const s = leerEstado();
    s.histor_path = req.file.path;
    s.histor_nombre = req.file.originalname;
    s.histor_subida_en = new Date().toISOString();
    guardarEstado(s);

    return res.json({
      ok: true,
      mensaje: '✅ Histor cargado. Listo para procesar cuando tú quieras.',
      estado: estadoParaFrontend(s)
    });
  } catch (err) {
    console.error('Error POST /api/importacion/histor:', err);
    res.status(500).json({ ok: false, mensaje: 'Error subiendo histor.', detalle: err?.message });
  }
});

// ✅ Limpiar estado + borrar archivos subidos (opcional)
api.post('/api/importacion/limpiar', (req, res) => {
  const s = leerEstado();
  try { if (s.planilla_path && fs.existsSync(s.planilla_path)) fs.unlinkSync(s.planilla_path); } catch {}
  try { if (s.histor_path && fs.existsSync(s.histor_path)) fs.unlinkSync(s.histor_path); } catch {}
  guardarEstado({});
  logImport('🧹 Estado limpiado (planilla/histor).');
  res.json({ ok: true, mensaje: '✅ Estado limpiado. Puedes cargar excels de nuevo.' });
});

// ✅ Procesar MANUALMENTE (aquí sí corre Python)
api.post('/api/importacion/sincronizar', async (req, res) => {
  const s = leerEstado();

  if (!s.planilla_path || !s.histor_path) {
    return res.status(400).json({
      ok: false,
      mensaje: 'Faltan archivos para sincronizar (planilla/histor).',
      estado: estadoParaFrontend(s)
    });
  }

  if (importEnCurso) {
    return res.status(409).json({ ok: false, mensaje: 'Importación en curso, intenta en unos segundos.' });
  }

  importEnCurso = true;

  // conteo antes
  let nAntes = 0;
  try {
    const antes = await query('SELECT COUNT(*)::int AS n FROM cita;');
    nAntes = antes.rows?.[0]?.n || 0;
  } catch {}

  try {
    const py = await correrImportPython(s.planilla_path, s.histor_path);

    // conteo después
    let nDespues = nAntes;
    try {
      const despues = await query('SELECT COUNT(*)::int AS n FROM cita;');
      nDespues = despues.rows?.[0]?.n || nAntes;
    } catch {}

    const nuevas = Math.max(0, nDespues - nAntes);

    // estatus 6 meses
    const conteoEstatus = await actualizarEstatusPacientes();

    const s2 = leerEstado();
    s2.ultima_sync = new Date().toISOString();
    s2.ultima_sync_resultado = {
      motivo: 'manual',
      citas_antes: nAntes,
      citas_despues: nDespues,
      citas_nuevas: nuevas,
      estatus: conteoEstatus
    };
    guardarEstado(s2);

    return res.json({
      ok: true,
      mensaje: '✅ Sincronización ejecutada (manual).',
      resultado: s2.ultima_sync_resultado,
      stdout: py.stdout,
      stderr: py.stderr
    });
  } catch (e) {
    console.error('❌ Falló Python:', e);
    return res.status(500).json({
      ok: false,
      mensaje: '❌ Falló la sincronización (Python).',
      stdout: e?.stdout,
      stderr: e?.stderr,
      detalle: e
    });
  } finally {
    importEnCurso = false;
  }
});

// ==============================
// PLAN ANUAL (SIN LABORATORIOS)
// ==============================
const PLAN_ANUAL = {
  1: ['ENFERMERIA', 'PSICOLOGIA', 'TRABAJO SOCIAL', 'MEDICO EXPERTO', 'QUIMICO FARMACEUTICO'],
  2: ['MEDICO EXPERTO', 'NUTRICION'],
  3: ['INFECTOLOGIA', 'QUIMICO FARMACEUTICO'],
  4: ['MEDICO EXPERTO'],
  5: ['MEDICO EXPERTO'],
  6: ['MEDICO EXPERTO', 'ODONTOLOGIA', 'TRABAJO SOCIAL'],
  7: ['MEDICO EXPERTO'],
  8: ['MEDICO EXPERTO', 'OFTALMOLOGIA'],
  9: ['INFECTOLOGIA', 'NUTRICION'],
  10: ['MEDICO EXPERTO', 'PSIQUIATRIA'],
  11: ['MEDICO EXPERTO'],
  12: ['MEDICO EXPERTO'],
};

function toISO(d) {
  const x = new Date(d);
  const yyyy = x.getFullYear();
  const mm = String(x.getMonth() + 1).padStart(2, '0');
  const dd = String(x.getDate()).padStart(2, '0');
  return `${yyyy}-${mm}-${dd}`;
}

function addMonths(date, n) {
  const d = new Date(date);
  const day = d.getDate();
  d.setMonth(d.getMonth() + n);
  if (d.getDate() < day) d.setDate(0);
  return d;
}

function ajustarCicloAnual(inicio, hoy) {
  let start = new Date(inicio);
  let end = addMonths(start, 12);
  while (end <= hoy) {
    start = addMonths(start, 12);
    end = addMonths(start, 12);
  }
  return { start, end };
}

function mesPrograma(start, hoy) {
  for (let m = 1; m <= 12; m++) {
    const a = addMonths(start, m - 1);
    const b = addMonths(start, m);
    if (hoy >= a && hoy < b) return m;
  }
  return 12;
}

function normEsp(s) {
  if (!s) return '';
  return String(s)
    .trim()
    .toUpperCase()
    .replaceAll('Á','A').replaceAll('É','E').replaceAll('Í','I').replaceAll('Ó','O').replaceAll('Ú','U')
    .replaceAll('Ñ','N');
}

function contarLista(lista) {
  const c = {};
  for (const x of lista) {
    const k = normEsp(x);
    c[k] = (c[k] || 0) + 1;
  }
  return c;
}

function sumarConteos(a, b) {
  const out = { ...a };
  for (const k of Object.keys(b)) out[k] = (out[k] || 0) + b[k];
  return out;
}

function restarUno(conteo, k) {
  if ((conteo[k] || 0) > 0) conteo[k] -= 1;
}

function totalConteo(conteo) {
  return Object.values(conteo).reduce((s, v) => s + (Number(v) || 0), 0);
}

/**
 * ✅ Lógica solicitada (versión "analista"):
 * - Esperado NO se acumula (solo lo del mes).
 * - Asistidas del mes muestran lo que ocurrió en ese mes:
 *      asistidas_total = asistidas_mes + asistidas_arrastre
 * - Retraso = deuda acumulada al cierre del mes (pendientes_fin).
 */
function calcularMesesConArrastre(PLAN, mesActual, rangos, citasAsistidasOrdenadas) {
  const pendientes = {}; // conteo por especialidad pendiente acumulada
  const porMes = [];
  const extras = [];

  const esperadoBasePorEsp = {};
  const asistidasPlanPorEsp = {};

  for (let m = 1; m <= mesActual; m++) {
    const planMes = (PLAN[m] || []).map(normEsp);
    const esperadoMesConteo = contarLista(planMes);

    // acumular esperado base por especialidad (sin inflar por arrastre)
    for (const esp of Object.keys(esperadoMesConteo)) {
      esperadoBasePorEsp[esp] = (esperadoBasePorEsp[esp] || 0) + (esperadoMesConteo[esp] || 0);
    }

    const arrastreInicio = totalConteo(pendientes);
    const esperadoMes = totalConteo(esperadoMesConteo);
    const esperadoTotal = arrastreInicio + esperadoMes;

    const inicio = rangos[m].inicio;
    const fin = rangos[m].fin;

    const citasMes = citasAsistidasOrdenadas.filter(x => x.fecha >= inicio && x.fecha < fin);

    let asistidasArrastre = 0;
    let asistidasDelMes = 0;

    const pendientesLocal = { ...pendientes };
    const esperadoLocal = { ...esperadoMesConteo };

    for (const c of citasMes) {
      const esp = normEsp(c.especialidad);

      // 1) cubrir pendientes anteriores
      if ((pendientesLocal[esp] || 0) > 0) {
        restarUno(pendientesLocal, esp);
        asistidasArrastre += 1;
        asistidasPlanPorEsp[esp] = (asistidasPlanPorEsp[esp] || 0) + 1;
        continue;
      }

      // 2) cubrir lo esperado del mes
      if ((esperadoLocal[esp] || 0) > 0) {
        restarUno(esperadoLocal, esp);
        asistidasDelMes += 1;
        asistidasPlanPorEsp[esp] = (asistidasPlanPorEsp[esp] || 0) + 1;
        continue;
      }

      // 3) extra (no suma al plan)
      extras.push({ mes: m, fecha: toISO(c.fecha), especialidad: esp });
    }

    const pendientesFin = sumarConteos(pendientesLocal, esperadoLocal);

    // copiar a global
    for (const k of Object.keys(pendientes)) delete pendientes[k];
    for (const k of Object.keys(pendientesFin)) pendientes[k] = pendientesFin[k];

    const asistidasTotal = asistidasArrastre + asistidasDelMes;
    const pendientesFinTotal = totalConteo(pendientes);

    porMes.push({
      mes: m,
      inicio: toISO(inicio),
      fin: toISO(new Date(fin.getTime() - 86400000)),

      // analítica detallada
      esperado_mes: esperadoMes,                 // ✅ NO se acumula
      arrastre_inicio: arrastreInicio,
      esperado_total: esperadoTotal,            // informativo (no lo uses para la barra "Esperado")
      asistidas_mes: asistidasDelMes,
      asistidas_arrastre: asistidasArrastre,
      asistidas_total: asistidasTotal,          // ✅ lo que hizo en el mes (incluye ponerse al día)
      retraso_fin: pendientesFinTotal,          // ✅ deuda acumulada

      // compatibilidad para el front actual:
      // "Esperado" = solo mes, "Asistidas" = total del mes (mes + arrastre), "Pendientes" = retraso
      esperado: esperadoMes,
      asistidas: asistidasTotal,
      pendientes: pendientesFinTotal,
      retraso: pendientesFinTotal
    });
  }

  return { porMes, extras, esperadoBasePorEsp, asistidasPlanPorEsp, pendientesFinal: { ...pendientes } };
}

// ==============================
// HISTOGRAMA PACIENTE
// ==============================
api.get('/api/histogramas/paciente/:numero_documento', async (req, res) => {
  const { numero_documento } = req.params;

  try {
    await actualizarEstatusPacientes();

    const est = await query(
      `SELECT COALESCE(estatus,'ACTIVO') AS estatus
       FROM paciente
       WHERE numero_documento::text = $1
       LIMIT 1`,
      [numero_documento]
    );

    if (est.rows.length && est.rows[0].estatus === 'ARCHIVADO') {
      return res.status(403).json({ error: 'Paciente ARCHIVADO: no se incluye en reportes activos.' });
    }

    // ingreso (tiempo) o primera cita
    let ingreso = null;
    const ingRes = await query(
      `SELECT fecha_ingreso_ips
       FROM tiempo
       WHERE numero_documento::text = $1
       LIMIT 1`,
      [numero_documento]
    );

    if (ingRes.rows.length && ingRes.rows[0].fecha_ingreso_ips) {
      ingreso = new Date(ingRes.rows[0].fecha_ingreso_ips);
    } else {
      const minRes = await query(
        `SELECT MIN(fecha_cita) AS fecha
         FROM cita
         WHERE numero_documento::text = $1`,
        [numero_documento]
      );
      if (minRes.rows.length && minRes.rows[0].fecha) ingreso = new Date(minRes.rows[0].fecha);
    }

    if (!ingreso) {
      return res.status(404).json({ error: 'No hay fecha de ingreso ni citas para construir histograma.' });
    }

    const hoy = new Date();
    const ciclo = ajustarCicloAnual(ingreso, hoy);
    const start = ciclo.start;
    const end = addMonths(start, 12);
    const mesActual = mesPrograma(start, hoy);

    const rangos = {};
    for (let m = 1; m <= 12; m++) {
      rangos[m] = { inicio: addMonths(start, m - 1), fin: addMonths(start, m) };
    }

    const citasRes = await query(
      `SELECT e.nombre AS especialidad, c.fecha_cita, c.asistio
       FROM cita c
       JOIN especialidad e ON e.id_especialidad = c.id_especialidad
       WHERE c.numero_documento::text = $1
         AND c.fecha_cita >= $2
         AND c.fecha_cita < $3`,
      [numero_documento, toISO(start), toISO(end)]
    );

    const asistidasOrdenadas = citasRes.rows
      .filter(x => x.asistio === true)
      .map(x => ({ fecha: new Date(x.fecha_cita), especialidad: x.especialidad }))
      .sort((a, b) => a.fecha - b.fecha);

    const calc = calcularMesesConArrastre(PLAN_ANUAL, mesActual, rangos, asistidasOrdenadas);

    // ✅ "Hasta hoy" real (sin inflar esperado)
    const esperadoBaseHastaHoy = calc.porMes.reduce((s, x) => s + (x.esperado_mes || 0), 0);
    const asistidasPlanHastaHoy = calc.porMes.reduce((s, x) => s + (x.asistidas_total || 0), 0);

    const mesRow = calc.porMes.find(x => x.mes === mesActual) || null;

    // ✅ Este mes: % solo del mes (sin arrastre)
    const esperadoMesActual = mesRow ? (mesRow.esperado_mes || 0) : 0;
    const asistidasMesActual = mesRow ? (mesRow.asistidas_mes || 0) : 0;
    const asistidasArrastreMesActual = mesRow ? (mesRow.asistidas_arrastre || 0) : 0;
    const asistidasTotalMesActual = mesRow ? (mesRow.asistidas_total || 0) : 0;

    const cumplimiento = {
      // Este mes (plan del mes)
      esperado_mes_actual: esperadoMesActual,
      asistidas_mes_actual: asistidasMesActual,
      porcentaje_mes_actual: esperadoMesActual
        ? Math.round((asistidasMesActual / esperadoMesActual) * 100)
        : 0,

      // Info adicional (lo que se puso al día en este mes)
      asistidas_arrastre_mes_actual: asistidasArrastreMesActual,
      asistidas_total_mes_actual: asistidasTotalMesActual,

      // Retraso acumulado
      retraso_actual: mesRow ? (mesRow.retraso_fin || 0) : 0,

      // Hasta hoy (cumplimiento real del plan)
      esperado_base_hasta_hoy: esperadoBaseHastaHoy,
      asistidas_plan_hasta_hoy: asistidasPlanHastaHoy,
      porcentaje_base_hasta_hoy: esperadoBaseHastaHoy
        ? Math.round((asistidasPlanHastaHoy / esperadoBaseHastaHoy) * 100)
        : 0,
    };

    res.json({
      numero_documento,
      ingreso: {
        fecha: toISO(ingreso),
        fuente: (ingRes.rows.length && ingRes.rows[0].fecha_ingreso_ips) ? 'tiempo.fecha_ingreso_ips' : 'primera_cita'
      },
      ventana: {
        inicio: toISO(start),
        fin: toISO(new Date(end.getTime() - 86400000)),
        mes_programa: mesActual
      },
      cumplimiento,
      porMes: calc.porMes,
      extras: calc.extras
    });

  } catch (err) {
    console.error('Error GET /api/histogramas/paciente/:doc:', err);
    res.status(500).json({ error: 'Error generando histograma del paciente' });
  }
});

// ==============================
// HISTOGRAMA GENERAL
// ==============================
api.get('/api/histogramas/general', async (req, res) => {
  try {
    const hoy = new Date();
    await actualizarEstatusPacientes();

    // 1) pacientes activos con ingreso
    const pacientesRes = await query(`
      SELECT
        p.numero_documento::text AS doc,
        t.fecha_ingreso_ips AS ingreso_tiempo,
        MIN(c.fecha_cita) AS primera_cita
      FROM paciente p
      LEFT JOIN tiempo t ON t.numero_documento = p.numero_documento
      LEFT JOIN cita c ON c.numero_documento = p.numero_documento
      WHERE COALESCE(p.estatus,'ACTIVO') = 'ACTIVO'
      GROUP BY p.numero_documento, t.fecha_ingreso_ips
    `);

    const mapaPac = new Map();
    let minStart = null;
    let maxMes = 0;

    for (const r of pacientesRes.rows) {
      const doc = String(r.doc || '').trim();
      const ingreso = r.ingreso_tiempo || r.primera_cita;
      if (!doc || !ingreso) continue;

      const ciclo = ajustarCicloAnual(new Date(ingreso), hoy);
      const start = ciclo.start;
      const end = ciclo.end;
      const mesActual = mesPrograma(start, hoy);

      const rangos = {};
      for (let m = 1; m <= 12; m++) {
        rangos[m] = { inicio: addMonths(start, m - 1), fin: addMonths(start, m) };
      }

      mapaPac.set(doc, { doc, start, end, mesActual, rangos });
      if (!minStart || start < minStart) minStart = start;
      if (mesActual > maxMes) maxMes = mesActual;
    }

    if (!minStart || mapaPac.size === 0) {
      return res.json({
        pacientes_incluidos: 0,
        rango: { inicio: null, hoy: toISO(hoy) },
        total_esperado: 0,
        total_asistidas: 0,
        porcentaje: 0,
        porEspecialidad: [],
        porMesPrograma: []
      });
    }

    // 2) citas asistidas desde minStart
    const asistidasRes = await query(
      `SELECT
         c.numero_documento::text AS doc,
         e.nombre AS especialidad,
         c.fecha_cita
       FROM cita c
       JOIN especialidad e ON e.id_especialidad = c.id_especialidad
       WHERE c.asistio = TRUE
         AND c.fecha_cita >= $1
         AND c.fecha_cita <= $2`,
      [toISO(minStart), toISO(hoy)]
    );

    const asistidasPorDoc = new Map();
    for (const r of asistidasRes.rows) {
      const doc = String(r.doc || '').trim();
      if (!doc) continue;
      if (!asistidasPorDoc.has(doc)) asistidasPorDoc.set(doc, []);
      asistidasPorDoc.get(doc).push({ fecha: new Date(r.fecha_cita), especialidad: r.especialidad });
    }
    for (const [doc, arr] of asistidasPorDoc.entries()) {
      arr.sort((a, b) => a.fecha - b.fecha);
      asistidasPorDoc.set(doc, arr);
    }

    const porMesPrograma = Array.from({ length: maxMes }, (_, i) => ({
      mes: i + 1,

      // detallado
      esperado_mes: 0,
      asistidas_mes: 0,
      asistidas_arrastre: 0,
      asistidas_total: 0,
      retraso_fin: 0,

      // compatibilidad para gráfica actual:
      esperado: 0,    // esperado del mes (NO acumulado)
      asistidas: 0,   // asistidas del mes (incluye arrastre)
      pendientes: 0,  // retraso (deuda)
      retraso: 0
    }));

    const esperadoBasePorEsp = {};
    const asistidasPlanPorEsp = {};

    let totalEsperadoBase = 0;
    let totalAsistidasPlan = 0;

    for (const p of mapaPac.values()) {
      const arr = asistidasPorDoc.get(p.doc) || [];
      const asistidasVentana = arr.filter(x => x.fecha >= p.start && x.fecha < p.end && x.fecha <= hoy);

      const calc = calcularMesesConArrastre(PLAN_ANUAL, p.mesActual, p.rangos, asistidasVentana);

      // por especialidad (plan real)
      for (const esp of Object.keys(calc.esperadoBasePorEsp)) {
        esperadoBasePorEsp[esp] = (esperadoBasePorEsp[esp] || 0) + (calc.esperadoBasePorEsp[esp] || 0);
      }
      for (const esp of Object.keys(calc.asistidasPlanPorEsp)) {
        asistidasPlanPorEsp[esp] = (asistidasPlanPorEsp[esp] || 0) + (calc.asistidasPlanPorEsp[esp] || 0);
      }

      // totales base (sin inflar esperado)
      const espBase = calc.porMes.reduce((s, x) => s + (x.esperado_mes || 0), 0);
      const asisPlan = calc.porMes.reduce((s, x) => s + (x.asistidas_total || 0), 0);
      totalEsperadoBase += espBase;
      totalAsistidasPlan += asisPlan;

      // por mes
      for (const row of calc.porMes) {
        const idx = row.mes - 1;
        if (!porMesPrograma[idx]) continue;

        porMesPrograma[idx].esperado_mes += row.esperado_mes || 0;
        porMesPrograma[idx].asistidas_mes += row.asistidas_mes || 0;
        porMesPrograma[idx].asistidas_arrastre += row.asistidas_arrastre || 0;
        porMesPrograma[idx].asistidas_total += row.asistidas_total || 0;
        porMesPrograma[idx].retraso_fin += row.retraso_fin || 0;

        // compatibilidad
        porMesPrograma[idx].esperado += row.esperado || 0;
        porMesPrograma[idx].asistidas += row.asistidas || 0;
        porMesPrograma[idx].pendientes += row.pendientes || 0;
        porMesPrograma[idx].retraso += row.retraso || 0;
      }
    }

    const porcentaje = totalEsperadoBase
      ? Math.round((totalAsistidasPlan / totalEsperadoBase) * 100)
      : 0;

    const especialidades = Object.keys(esperadoBasePorEsp).sort();
    const porEspecialidad = especialidades.map((esp) => {
      const esperado = esperadoBasePorEsp[esp] || 0;
      const asistidas = asistidasPlanPorEsp[esp] || 0;
      const pendientes = Math.max(0, esperado - asistidas);
      const porcentaje = esperado ? Math.round((asistidas / esperado) * 100) : 0;
      return { especialidad: esp, esperado, asistidas, pendientes, porcentaje };
    });

    res.json({
      pacientes_incluidos: mapaPac.size,
      rango: { inicio: toISO(minStart), hoy: toISO(hoy) },

      // ✅ totales reales del plan (NO inflados)
      total_esperado: totalEsperadoBase,
      total_asistidas: totalAsistidasPlan,
      porcentaje,

      porEspecialidad,
      porMesPrograma
    });

  } catch (err) {
    console.error('Error GET /api/histogramas/general:', err);
    res.status(500).json({ error: 'Error generando histograma general' });
  }
});

// ==============================
// RUTAS EXISTENTES (Pacientes / Citas)
// ==============================
api.get('/api/pacientes', async (req, res) => {
  const { tipo, termino, estatus } = req.query;

  try {
    const valor = (termino || '').toString().trim();
    const filtroEstatus = (estatus || '').toString().trim().toUpperCase();

    const whereEstatus = (filtroEstatus === 'ACTIVO' || filtroEstatus === 'ARCHIVADO')
      ? ` AND COALESCE(p.estatus,'ACTIVO') = '${filtroEstatus}' `
      : '';

    if (!valor) {
      const resultado = await query(
        `SELECT
           p.tipo_documento,
           p.numero_documento::text AS numero_documento,
           p.nombre_completo,
           p.genero,
           p.edad,
           p.fecha_nacimiento,
           p.celular,
           p.correo,
           COALESCE(p.estatus,'ACTIVO') AS estatus,
           p.fecha_ultima_actividad
         FROM paciente p
         WHERE 1=1 ${whereEstatus}
         ORDER BY p.nombre_completo
         LIMIT 50`
      );
      return res.json(resultado.rows);
    }

    let sql = '';
    const param = `%${valor}%`;

    if (tipo === 'documento') {
      sql = `
        SELECT
          p.tipo_documento,
          p.numero_documento::text AS numero_documento,
          p.nombre_completo,
          p.genero,
          p.edad,
          p.fecha_nacimiento,
          p.celular,
          p.correo,
          COALESCE(p.estatus,'ACTIVO') AS estatus,
          p.fecha_ultima_actividad
        FROM paciente p
        WHERE p.numero_documento::text ILIKE $1
        ${whereEstatus}
        ORDER BY p.nombre_completo
        LIMIT 50
      `;
    } else if (tipo === 'nombre') {
      sql = `
        SELECT
          p.tipo_documento,
          p.numero_documento::text AS numero_documento,
          p.nombre_completo,
          p.genero,
          p.edad,
          p.fecha_nacimiento,
          p.celular,
          p.correo,
          COALESCE(p.estatus,'ACTIVO') AS estatus,
          p.fecha_ultima_actividad
        FROM paciente p
        WHERE p.nombre_completo ILIKE $1
        ${whereEstatus}
        ORDER BY p.nombre_completo
        LIMIT 50
      `;
    } else {
      return res.status(400).json({ error: 'Tipo de búsqueda no válido' });
    }

    const resultado = await query(sql, [param]);
    res.json(resultado.rows);
  } catch (err) {
    console.error('Error en GET /api/pacientes:', err);
    res.status(500).json({ error: 'Error consultando pacientes' });
  }
});

api.get('/api/pacientes/:numero_documento/citas', async (req, res) => {
  const { numero_documento } = req.params;

  try {
    const resultado = await query(
      `SELECT
         c.id_cita,
         c.numero_documento::text AS numero_documento,
         e.nombre AS especialidad,
         c.fecha_cita,
         c.hora_cita,
         c.tipo_cita,
         c.modalidad,
         c.asistio,
         c.observaciones
       FROM cita c
       JOIN especialidad e
         ON c.id_especialidad = e.id_especialidad
       WHERE c.numero_documento::text = $1
       ORDER BY c.fecha_cita DESC, c.id_cita DESC`,
      [numero_documento]
    );

    res.json(resultado.rows);
  } catch (err) {
    console.error('Error en GET /api/pacientes/:numero_documento/citas:', err);
    res.status(500).json({ error: 'Error consultando citas del paciente' });
  }
});

api.get('/api/especialidades', async (req, res) => {
  try {
    const resultado = await query(
      `SELECT id_especialidad, nombre
       FROM especialidad
       ORDER BY id_especialidad`
    );
    res.json(resultado.rows);
  } catch (err) {
    console.error('Error en GET /api/especialidades:', err);
    res.status(500).json({ error: 'Error consultando especialidades' });
  }
});

api.post('/api/citas', async (req, res) => {
  const {
    numero_documento,
    id_especialidad,
    fecha_cita,
    hora_cita,
    tipo_cita,
    modalidad,
    asistio,
    observaciones
  } = req.body;

  if (!numero_documento || !id_especialidad || !fecha_cita || !hora_cita || !modalidad) {
    return res.status(400).json({
      error: 'numero_documento, id_especialidad, fecha_cita, hora_cita y modalidad son obligatorios'
    });
  }

  try {
    const conflictoMismaEsp = await query(
      `SELECT 1
       FROM cita
       WHERE numero_documento::text = $1
         AND id_especialidad = $2
         AND fecha_cita = $3
         AND hora_cita = $4::time
       LIMIT 1`,
      [numero_documento, id_especialidad, fecha_cita, hora_cita]
    );

    if (conflictoMismaEsp.rows.length > 0) {
      return res.status(400).json({
        error: 'Ya existe una cita para este paciente, en esta especialidad, fecha y hora.'
      });
    }

    const conflicto30m = await query(
      `SELECT 1
       FROM cita
       WHERE numero_documento::text = $1
         AND fecha_cita = $2
         AND hora_cita IS NOT NULL
         AND ABS(EXTRACT(EPOCH FROM (hora_cita - $3::time))) < 1800
       LIMIT 1`,
      [numero_documento, fecha_cita, hora_cita]
    );

    if (conflicto30m.rows.length > 0) {
      return res.status(400).json({
        error: 'El paciente ya tiene otra cita ese día en un horario muy cercano (menos de 30 minutos).'
      });
    }

    const countRes = await query(
      'SELECT COUNT(*) AS total FROM cita WHERE id_especialidad = $1',
      [id_especialidad]
    );
    const total = Number(countRes.rows[0].total) || 0;
    const consecutivo = total + 1;
    const nuevoId = id_especialidad * 1_000_000 + consecutivo;

    const valorAsistio = typeof asistio === 'boolean' ? asistio : false;

    const existeRes = await query('SELECT 1 FROM cita WHERE id_cita = $1', [nuevoId]);
    if (existeRes.rows.length > 0) {
      return res.status(500).json({
        error: 'Conflicto al generar id_cita, ya existe un registro con ese ID'
      });
    }

    const resultado = await query(
      `WITH nueva AS (
         INSERT INTO cita (
           id_cita,
           numero_documento,
           id_especialidad,
           fecha_cita,
           hora_cita,
           tipo_cita,
           modalidad,
           asistio,
           observaciones
         )
         VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
         RETURNING *
       )
       SELECT
         n.id_cita,
         n.numero_documento::text AS numero_documento,
         e.nombre AS especialidad,
         n.fecha_cita,
         n.hora_cita,
         n.tipo_cita,
         n.modalidad,
         n.asistio,
         n.observaciones
       FROM nueva n
       JOIN especialidad e ON n.id_especialidad = e.id_especialidad`,
      [
        nuevoId,
        numero_documento,
        id_especialidad,
        fecha_cita,
        hora_cita,
        tipo_cita || null,
        modalidad,
        valorAsistio,
        observaciones || null
      ]
    );

    res.status(201).json(resultado.rows[0]);
  } catch (err) {
    console.error('Error en POST /api/citas:', err);
    res.status(500).json({ error: 'Error creando la cita' });
  }
});

api.patch('/api/citas/:id_cita/asistencia', async (req, res) => {
  const { id_cita } = req.params;
  const { asistio } = req.body;

  if (typeof asistio !== 'boolean') {
    return res.status(400).json({ error: 'El campo asistio debe ser booleano' });
  }

  try {
    const resultado = await query(
      `
      WITH datos AS (
        SELECT numero_documento
        FROM cita
        WHERE id_cita = $2
      ),
      ultimas AS (
        SELECT c.id_cita
        FROM cita c
        JOIN datos d ON c.numero_documento = d.numero_documento
        ORDER BY c.fecha_cita DESC, c.id_cita DESC
        LIMIT 2
      ),
      actualizada AS (
        UPDATE cita c
        SET asistio = $1
        WHERE c.id_cita IN (SELECT id_cita FROM ultimas)
          AND c.id_cita = $2
        RETURNING *
      )
      SELECT
        a.id_cita,
        a.numero_documento::text AS numero_documento,
        e.nombre AS especialidad,
        a.fecha_cita,
        a.hora_cita,
        a.tipo_cita,
        a.modalidad,
        a.asistio,
        a.observaciones
      FROM actualizada a
      JOIN especialidad e ON a.id_especialidad = e.id_especialidad;
      `,
      [asistio, id_cita]
    );

    if (resultado.rows.length === 0) {
      return res.status(400).json({
        error: 'Solo se puede modificar la asistencia de las dos últimas citas del paciente'
      });
    }

    res.json(resultado.rows[0]);
  } catch (err) {
    console.error('Error en PATCH /api/citas/:id_cita/asistencia:', err);
    res.status(500).json({ error: 'Error actualizando asistencia' });
  }
});

// ==============================
// Arrancar
// ==============================
api.listen(PUERTO, () => {
  console.log(`Servidor escuchando en http://localhost:${PUERTO}`);
});
