import React, { useEffect, useMemo, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import {
  Activity,
  AlertTriangle,
  BarChart3,
  CheckCircle2,
  ChevronRight,
  Columns3Cog,
  Database,
  Download,
  FileSpreadsheet,
  Gauge,
  ImageDown,
  KeyRound,
  Layers3,
  LockKeyhole,
  LogOut,
  Menu,
  Play,
  Plus,
  RefreshCw,
  Search,
  Shield,
  Sparkles,
  TimerReset,
  Trash2,
  Upload,
  UserCog,
  Users,
  X,
} from 'lucide-react';
import {
  Area,
  AreaChart,
  Bar,
  BarChart,
  CartesianGrid,
  Cell,
  Pie,
  PieChart,
  ResponsiveContainer,
  Tooltip,
  XAxis,
  YAxis,
} from 'recharts';
import * as XLSX from 'xlsx';
import './styles.css';

const API_DEFAULT = import.meta.env.VITE_WORKER_API_URL || localStorage.getItem('wow_api_base') || '';
const TOKEN_KEY = 'wow_panel_session_token';
const API_KEY = 'wow_api_base';
const PALETTE = ['#1d4ed8', '#0891b2', '#16a34a', '#f59e0b', '#dc2626', '#7c3aed', '#0f766e', '#be123c'];
const REPORT_CACHE_TTL_MS = 5 * 60 * 1000;
const INB_CACHE_VERSION = '2026-07-03-hourly-v1';
const inbReportCache = {
  active: { fetchedAt: 0, data: null, errors: {}, bundles: {} },
  inactive: { fetchedAt: 0, data: null, errors: {}, bundles: {} },
};

function clearInbReportCaches() {
  inbReportCache.active = { fetchedAt: 0, data: null, errors: {}, bundles: {} };
  inbReportCache.inactive = { fetchedAt: 0, data: null, errors: {}, bundles: {} };
}

try {
  const cachedVersion = localStorage.getItem('wow_inb_cache_version') || '';
  if (cachedVersion !== INB_CACHE_VERSION) {
    clearInbReportCaches();
    localStorage.setItem('wow_inb_cache_version', INB_CACHE_VERSION);
    localStorage.setItem('wow_reports_refresh_at', String(Date.now()));
  }
} catch {}
const INB_REPORT_KEYS = ['productividad', 'timeout', 'conclusiones', 'validadores', 'validadoresConclusiones', 'estados', 'alertas', 'divisiones'];

function inbKeysForTab(tab) {
  if (tab === 'resumen' || tab === 'productividad') return ['productividad'];
  if (tab === 'diana') return ['productividad'];
  if (tab === 'conclusiones') return ['productividad', 'conclusiones'];
  if (tab === 'timeout') return ['productividad', 'timeout'];
  if (tab === 'validadores') return ['productividad', 'validadores', 'validadoresConclusiones'];
  if (tab === 'estados') return ['productividad', 'estados'];
  if (tab === 'alertas') return ['productividad', 'alertas'];
  return ['productividad'];
}

function inbCacheBucket(includeInactive = false) {
  return includeInactive ? 'inactive' : 'active';
}

function readInbReportCache(includeInactive = false) {
  return inbReportCache[inbCacheBucket(includeInactive)] || { fetchedAt: 0, data: null, errors: {}, bundles: {} };
}

function writeInbReportCache(includeInactive = false, payload = {}) {
  const bucket = inbCacheBucket(includeInactive);
  inbReportCache[bucket] = {
    fetchedAt: Date.now(),
    data: payload.data || {},
    errors: payload.errors || {},
    bundles: inbReportCache[bucket]?.bundles || {},
  };
}

function isInbReportCacheFresh(includeInactive = false) {
  const cache = readInbReportCache(includeInactive);
  return Boolean(cache?.data) && ((Date.now() - Number(cache.fetchedAt || 0)) < REPORT_CACHE_TTL_MS);
}

function readInbBundleCache(includeInactive = false, reportDate = '') {
  const cache = readInbReportCache(includeInactive);
  return cache?.bundles?.[String(reportDate || '__default__')] || null;
}

function writeInbBundleCache(includeInactive = false, reportDate = '', payload = {}, promoteToActive = false) {
  const bucket = inbCacheBucket(includeInactive);
  const key = String(reportDate || '__default__');
  const previous = readInbReportCache(includeInactive);
  const bundles = { ...(previous?.bundles || {}) };
  bundles[key] = {
    fetchedAt: Date.now(),
    data: payload.data || {},
    errors: payload.errors || {},
  };
  inbReportCache[bucket] = {
    fetchedAt: promoteToActive ? Date.now() : Number(previous?.fetchedAt || 0),
    data: promoteToActive ? (payload.data || {}) : (previous?.data || null),
    errors: promoteToActive ? (payload.errors || {}) : (previous?.errors || {}),
    bundles,
  };
}

function isInbBundleCacheFresh(includeInactive = false, reportDate = '') {
  const cache = readInbBundleCache(includeInactive, reportDate);
  return Boolean(cache?.data) && ((Date.now() - Number(cache.fetchedAt || 0)) < REPORT_CACHE_TTL_MS);
}

const navItems = [
  { id: 'dashboard', label: 'Dashboard', icon: Gauge, roles: ['admin', 'user'] },
  { id: 'sync', label: 'Scraping', icon: Activity, roles: ['admin'] },
  { id: 'otp', label: 'OTP y sesión', icon: KeyRound, roles: ['admin', 'user'] },
  { id: 'reports', label: 'Reportería', icon: BarChart3, roles: ['admin', 'user'] },
  { id: 'consolidated', label: 'Consolidados', icon: Layers3, roles: ['admin', 'user'] },
  { id: 'uploads', label: 'Cargas y cruces', icon: FileSpreadsheet, roles: ['admin', 'user'] },
  { id: 'dotacion', label: 'Dotación', icon: Users, roles: ['admin', 'user'] },
  { id: 'users', label: 'Usuarios', icon: UserCog, roles: ['admin'] },
  { id: 'settings', label: 'Configuración', icon: Shield, roles: ['admin', 'user'] },
];

function safeDate(value) {
  if (!value) return '—';
  try {
    return new Intl.DateTimeFormat('es-PE', { dateStyle: 'medium', timeStyle: 'short', timeZone: 'America/Lima' }).format(new Date(value));
  } catch {
    return String(value);
  }
}

function number(value) {
  return new Intl.NumberFormat('es-PE').format(Number(value || 0));
}

function asArray(data) {
  return Array.isArray(data) ? data : [];
}

function normalizeInbHourlyRows(payload = {}) {
  const source = asArray(
    payload.resumenHoras
    || payload.resumenHora
    || payload.resumen_hora
    || payload.horas
    || payload.hourly
    || payload.porHora
    || [],
  );
  return source.map((row) => {
    const pendiente = numValue(firstValue(row, ['pendiente', 'pendientes'], 0));
    const retenido = numValue(firstValue(row, ['retenido', 'retenidos', 'retenidas'], 0));
    const totalGeneral = numValue(firstValue(row, ['total_general', 'solicitudes_wow', 'solicitudes'], 0));
    return {
      ...row,
      hora: firstValue(row, ['hora', 'Hora', 'label'], ''),
      pendiente,
      retenido,
      total_general: totalGeneral,
      pct_rete: pctValue(
        retenido,
        totalGeneral,
      ) ?? pctNumber(firstValue(row, ['pct_rete', 'retencion_pct', 'efectividad_pct', 'efectividad'], null)) ?? 0,
    };
  });
}

function normalizeInbBundle(payload = {}) {
  const resumenHoras = normalizeInbHourlyRows(payload);
  return {
    ...(payload || {}),
    resumenHoras,
  };
}

function wait(ms) {
  return new Promise((resolve) => window.setTimeout(resolve, ms));
}

function initials(name = '') {
  return name
    .split(' ')
    .filter(Boolean)
    .slice(0, 2)
    .map((x) => x[0]?.toUpperCase())
    .join('') || 'U';
}

function downloadJson(filename, data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();
  URL.revokeObjectURL(url);
}

function downloadCsv(filename, rows) {
  const data = asArray(rows);
  const keys = [...new Set(data.flatMap((row) => Object.keys(row || {})))];
  const csv = [
    keys.join(','),
    ...data.map((row) => keys.map((key) => JSON.stringify(row?.[key] ?? '')).join(',')),
  ].join('\n');
  const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();
  URL.revokeObjectURL(url);
}

function useApi() {
  const [apiBase, setApiBaseState] = useState(API_DEFAULT);
  const [token, setTokenState] = useState(localStorage.getItem(TOKEN_KEY) || '');

  const setApiBase = (value) => {
    const clean = String(value || '').trim().replace(/\/$/, '');
    setApiBaseState(clean);
    localStorage.setItem(API_KEY, clean);
  };
  const setToken = (value) => {
    setTokenState(value || '');
    if (value) localStorage.setItem(TOKEN_KEY, value);
    else localStorage.removeItem(TOKEN_KEY);
  };

  const request = async (path, options = {}) => {
    if (!apiBase) throw new Error('Configura la URL del Worker API.');
    const method = String(options.method || 'GET').toUpperCase();
    const doFetch = () => fetch(`${apiBase}${path}`, {
      cache: 'no-store',
      ...options,
      headers: {
        'content-type': 'application/json',
        ...(token ? { authorization: `Bearer ${token}` } : {}),
        ...(options.headers || {}),
      },
    });
    let res;
    try {
      res = await doFetch();
    } catch (error) {
      if (method === 'GET') {
        await wait(350);
        res = await doFetch();
      } else {
        throw error;
      }
    }
    const data = await res.json().catch(() => null);
    if (!res.ok) throw new Error(data?.message || data?.error || `HTTP ${res.status}`);
    return data;
  };

  return { apiBase, setApiBase, token, setToken, request };
}

function Toast({ toast, onClose }) {
  if (!toast) return null;
  return (
    <div className={`toast ${toast.type || 'info'}`}>
      <div>
        <strong>{toast.title}</strong>
        <p>{toast.message}</p>
      </div>
      <button className="icon-btn ghost" onClick={onClose}><X size={16} /></button>
    </div>
  );
}

function LoginScreen({ apiBase, setApiBase, request, setToken, onLogin }) {
  const [mode, setMode] = useState('login');
  const [form, setForm] = useState({ email: '', password: '', name: '', role: 'admin', adminKey: '' });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const submit = async (event) => {
    event.preventDefault();
    setLoading(true);
    setError('');
    try {
      if (mode === 'bootstrap') {
        if (!form.adminKey) throw new Error('Ingresa el ADMIN_API_KEY para crear el primer admin.');
        const res = await fetch(`${apiBase.replace(/\/$/, '')}/api/auth/bootstrap`, {
          method: 'POST',
          headers: { 'content-type': 'application/json', 'x-admin-key': form.adminKey },
          body: JSON.stringify({ email: form.email, password: form.password, name: form.name }),
        });
        const data = await res.json().catch(() => null);
        if (!res.ok) throw new Error(data?.error || data?.message || `HTTP ${res.status}`);
        setMode('login');
        setError('Admin creado. Ahora inicia sesión.');
      } else {
        const data = await request('/api/auth/login', { method: 'POST', body: JSON.stringify({ email: form.email, password: form.password }) });
        setToken(data.token);
        onLogin(data.user);
      }
    } catch (e) {
      setError(e.message);
    } finally {
      if (loadSeq === loadSeqRef.current) setLoading(false);
    }
  };

  return (
    <main className="login-shell">
      <section className="login-hero">
        <div className="brand-mark"><Sparkles size={28} /></div>
        <div>
          <p className="eyebrow">WOW Control Center</p>
          <h1>Operación, OTP y reportería en una sola consola.</h1>
          <p>Panel remoto para controlar el scraping mensual, cargar OTP, consultar reportes, cruzar archivos y administrar dotación.</p>
        </div>
        <div className="login-metrics">
          <div><strong>15 min</strong><span>cron activo</span></div>
          <div><strong>Mes completo</strong><span>cada consulta</span></div>
          <div><strong>Roles</strong><span>admin / usuario</span></div>
        </div>
      </section>

      <section className="login-card glass-card">
        <div className="tabs compact">
          <button className={mode === 'login' ? 'active' : ''} onClick={() => setMode('login')}>Ingresar</button>
          <button className={mode === 'bootstrap' ? 'active' : ''} onClick={() => setMode('bootstrap')}>Primer admin</button>
        </div>
        <form onSubmit={submit}>
          <label>Worker API URL
            <input value={apiBase} onChange={(e) => setApiBase(e.target.value)} placeholder="https://wow-bajas-worker.x.workers.dev" />
          </label>
          {mode === 'bootstrap' && (
            <label>Nombre
              <input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="Administrador" />
            </label>
          )}
          <label>Email
            <input value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} placeholder="admin@empresa.com" />
          </label>
          <label>Contraseña
            <input type="password" value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} placeholder="••••••••" />
          </label>
          {mode === 'bootstrap' && (
            <label>ADMIN_API_KEY temporal
              <input type="password" value={form.adminKey} onChange={(e) => setForm({ ...form, adminKey: e.target.value })} placeholder="Solo para crear el primer admin" />
            </label>
          )}
          {error && <div className={error.includes('creado') ? 'notice ok' : 'notice danger'}>{error}</div>}
          <button className="primary wide" disabled={loading}>{loading ? 'Procesando...' : mode === 'login' ? 'Entrar al panel' : 'Crear primer admin'}</button>
        </form>
      </section>
    </main>
  );
}

function AppShell({ user, setUser, api, children, page, setPage, collapsed, setCollapsed }) {
  const visibleNav = navItems.filter((item) => item.roles.includes(user?.role));
  return (
    <div className={`app-shell ${collapsed ? 'collapsed' : ''}`}>
      <aside className="sidebar">
        <div className="side-brand">
          <div className="logo">W</div>
          <div>
            <strong>WOW Control</strong>
            <span>Scraping & BI</span>
          </div>
        </div>
        <nav>
          {visibleNav.map((item) => {
            const Icon = item.icon;
            return (
              <button key={item.id} className={page === item.id ? 'active' : ''} onClick={() => setPage(item.id)}>
                <Icon size={18} />
                <span>{item.label}</span>
              </button>
            );
          })}
        </nav>
        <div className="side-footer">
          <div className="avatar">{initials(user?.name || user?.email)}</div>
          <div>
            <strong>{user?.name || 'Usuario'}</strong>
            <span>{user?.role === 'admin' ? 'Administrador' : 'Usuario'}</span>
          </div>
        </div>
      </aside>
      <main className="content">
        <header className="topbar">
          <button className="icon-btn" onClick={() => setCollapsed(!collapsed)}><Menu size={19} /></button>
          <div>
            <p className="eyebrow">Consola remota</p>
            <h2>{visibleNav.find((x) => x.id === page)?.label || 'Dashboard'}</h2>
          </div>
          <div className="top-actions">
            <span className={`role-chip ${user?.role}`}>{user?.role}</span>
            <button className="secondary" onClick={() => { api.setToken(''); setUser(null); }}><LogOut size={16} />Salir</button>
          </div>
        </header>
        {children}
      </main>
    </div>
  );
}

function MetricCard({ title, value, subtitle, icon: Icon, tone = 'blue' }) {
  return (
    <div className={`metric-card ${tone}`}>
      <div>
        <span>{title}</span>
        <strong>{value}</strong>
        {subtitle && <small>{subtitle}</small>}
      </div>
      <div className="metric-icon"><Icon size={22} /></div>
    </div>
  );
}

function DataTable({ rows, max = 8, empty = 'Sin datos.' }) {
  const data = asArray(rows).slice(0, max);
  if (!data.length) return <p className="muted pad">{empty}</p>;
  const keys = Object.keys(data[0] || {}).slice(0, 8);
  return (
    <div className="table-wrap">
      <table>
        <thead><tr>{keys.map((key) => <th key={key}>{key}</th>)}</tr></thead>
        <tbody>
          {data.map((row, idx) => (
            <tr key={idx}>{keys.map((key) => <td key={key}>{formatValue(row[key])}</td>)}</tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function formatValue(value) {
  if (value === null || value === undefined) return '—';
  if (typeof value === 'object') return JSON.stringify(value).slice(0, 80);
  if (String(value).match(/^\d{4}-\d{2}-\d{2}T/)) return safeDate(value);
  return String(value);
}

function formatPersonName(value) {
  if (value === null || value === undefined || value === '') return '—';
  return String(value)
    .replace(/Ã/g, 'É')
    .replace(/Ã³/g, 'ó')
    .replace(/Ã±/g, 'ñ')
    .replace(/Ã­/g, 'í')
    .replace(/Ã¡/g, 'á')
    .replace(/Ã©/g, 'é')
    .replace(/Ãº/g, 'ú')
    .replace(/Ã‘/g, 'Ñ')
    .replace(/\s+/g, ' ')
    .trim()
    .toLocaleUpperCase('es-PE');
}

function isPersonColumn(column) {
  const label = normalizeLookupKey(columnLabel(column));
  const key = normalizeLookupKey(columnKey(column));
  return ['asesor', 'supervisor', 'validador', 'nombre dotacion', 'nombre personal'].some((needle) => label === needle || key === needle || label.includes(needle));
}

function numValue(value) {
  if (value === null || value === undefined || value === '') return 0;
  const n = Number(String(value).replace(/,/g, ''));
  return Number.isFinite(n) ? n : 0;
}

function sumRows(rows, keys) {
  return asArray(rows).reduce((acc, row) => acc + numValue(firstValue(row, keys)), 0);
}

function avgDurationHms(rows, keys) {
  const values = asArray(rows)
    .map((row) => durationLikeToSeconds(firstValue(row, keys, null)))
    .filter((seconds) => seconds !== null && Number.isFinite(seconds));
  if (!values.length) return null;
  return secondsToHms(values.reduce((acc, seconds) => acc + seconds, 0) / values.length);
}

function weightedAverageDurationHms(rows, valueKeys, weightKeys) {
  const values = asArray(rows)
    .map((row) => ({
      seconds: durationLikeToSeconds(firstValue(row, valueKeys, null)),
      weight: numValue(firstValue(row, weightKeys, 0)),
    }))
    .filter((entry) => entry.seconds !== null && Number.isFinite(entry.seconds) && entry.weight > 0);
  if (!values.length) return null;
  const totalWeight = values.reduce((acc, entry) => acc + entry.weight, 0);
  if (!totalWeight) return null;
  const weightedSum = values.reduce((acc, entry) => acc + (entry.seconds * entry.weight), 0);
  return secondsToHms(weightedSum / totalWeight);
}

function normalizeLookupKey(value) {
  return String(value || '')
    .trim()
    .toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/[\s_-]+/g, ' ');
}

function firstValue(row, keys, fallback = null) {
  if (!row) return fallback;
  const list = Array.isArray(keys) ? keys : [keys];

  // 1) Exact match first. This keeps current behavior and supports presentational columns.
  for (const key of list) {
    if (row[key] !== undefined && row[key] !== null && row[key] !== '') return row[key];
  }

  // 2) Case/accent/underscore-insensitive fallback. Useful when Supabase views
  // return columns such as "Agent ID Genesys", "agent_id_genesys", or "agent id genesys".
  const lookup = new Map();
  for (const key of Object.keys(row)) lookup.set(normalizeLookupKey(key), key);
  for (const key of list) {
    const realKey = lookup.get(normalizeLookupKey(key));
    if (realKey && row[realKey] !== undefined && row[realKey] !== null && row[realKey] !== '') return row[realKey];
  }

  return fallback;
}

function pctValue(numerator, denominator) {
  const den = numValue(denominator);
  if (!den) return null;
  return (numValue(numerator) * 100) / den;
}

function fmtPct(value) {
  if (value === null || value === undefined || value === '') return '—';
  const n = Number(String(value).replace('%', '').replace(',', '.'));
  if (Number.isNaN(n)) return '—';
  return `${new Intl.NumberFormat('es-PE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n)}%`;
}

function fmtMin(value) {
  if (value === null || value === undefined || value === '') return '—';
  return `${new Intl.NumberFormat('es-PE', { maximumFractionDigits: 2 }).format(Number(value))} min`;
}

function isoDateOnly(value) {
  if (!value) return '';
  return String(value).slice(0, 10);
}

function uniq(values) {
  return [...new Set(values.filter((v) => v !== undefined && v !== null && String(v).trim() !== '').map((v) => String(v)))].sort((a, b) => a.localeCompare(b));
}

function normalizeDianaValue(value) {
  const raw = String(value ?? '').trim();
  if (!raw) return null;
  const numeric = Number(raw);
  return Number.isFinite(numeric) ? numeric : raw;
}

function normalizeQValue(value) {
  const raw = String(value ?? '').trim();
  if (!raw) return '';
  const cleaned = raw.toUpperCase().replace(/\s+/g, '');
  const match = cleaned.match(/Q?(\d+)/);
  if (match) return `Q${match[1]}`;
  return cleaned.startsWith('Q') ? cleaned : `Q${cleaned}`;
}

function qOrderValue(value) {
  const normalized = normalizeQValue(value);
  const match = normalized.match(/^Q(\d+)$/);
  return match ? Number(match[1]) : Number.POSITIVE_INFINITY;
}

function labelize(key) {
  const labels = {
    report_date: 'Fecha',
    tramo_label: 'Tramo',
    asesor_nombre: 'Asesor',
    validador_nombre: 'Validador',
    nombre_agente_raw: 'Asesor',
    division: 'División',
    equipo: 'Equipo',
    supervisor: 'Supervisor',
    llamadas_manejadas: 'Manejo',
    llamadas_contestadas: 'Contestadas',
    interacciones_manejadas: 'Interacciones',
    solicitudes_wow: 'Solicitudes WOW',
    retenidas: 'Retenidos',
    retenidas_wow: 'Retenidos',
    ejecutadas: 'Ejecutados',
    ejecutadas_wow: 'Ejecutados',
    pendientes: 'Pendientes',
    pendientes_wow: 'Pendientes',
    pct_retencion_sobre_llamadas: 'Retención INB %',
    pct_ejecucion_sobre_llamadas: 'Ejecución %',
    timeout_q: 'Timeout Q',
    timeout_pct: 'Timeout %',
    timeout_pct_interacciones: 'Timeout %',
    interacciones: 'Interacciones',
    timeout_puro_q: 'Puro Q',
    timeout_combinado_q: 'Combinado Q',
    manejo_medio_min: 'Manejo medio',
    conv_media_min: 'Conv. media',
    acw_media_min: 'ACW medio',
    conclusion_raw: 'Conclusión',
    participacion_pct: 'Participación %',
    q: 'Q',
    validaciones_q: 'Validaciones',
    conversaciones_q: 'Conversaciones',
    validaciones_con_timeout_q: 'Val. timeout',
    estado_principal: 'Estado principal',
    estado_secundario: 'Estado secundario',
    q_veces: 'Q veces',
    tiempo_total_min: 'Tiempo total',
    tiempo_total_alertas_min: 'Tiempo alertas',
    tiempo_promedio_alerta_min: 'Promedio alerta',
    participacion_tiempo_pct: 'Participación tiempo %',
    tipo_alerta: 'Tipo alerta',
    alerta_detalle: 'Detalle alerta',
    q_alertas: 'Q alertas',
    origen: 'Origen',
    nombre_raw: 'Nombre raw',
    status: 'Estado',
    total_q: 'Total Q',
    agent_id_genesys: 'Agent ID Genesys',
    antiguedad: 'Antigüedad',
    nro_rrhh: 'Q',
    nombre_dotacion: 'Nombre dotación',
    rol_operativo: 'Rol',
    es_validador: 'Validador',
    activo: 'Activo',
  };
  return labels[key] || key.replaceAll('_', ' ');
}

function columnKey(column) {
  return typeof column === 'string' ? column : column?.key;
}

function columnLabel(column) {
  return typeof column === 'string' ? labelize(column) : (column?.label || labelize(column?.key || ''));
}

function columnKeys(column) {
  if (typeof column === 'string') return [column];
  return [column?.key, ...(column?.alt || [])].filter(Boolean);
}

function columnValue(row, column) {
  return firstValue(row, columnKeys(column), null);
}

function columnId(column) {
  return columnKey(column) || columnLabel(column);
}

function boolValue(value) {
  if (value === true) return true;
  if (value === false || value === null || value === undefined) return false;
  return ['true', '1', 'si', 'sí', 'yes', 'y'].includes(String(value).trim().toLowerCase());
}

function parseDateParts(value) {
  const raw = String(value || '').trim();
  const match = raw.match(/^(\d{4})-(\d{2})-(\d{2})/);
  if (!match) return null;
  return {
    year: Number(match[1]),
    month: Number(match[2]),
    day: Number(match[3]),
  };
}

function limaTodayDate() {
  const parts = new Intl.DateTimeFormat('en-CA', {
    timeZone: 'America/Lima',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).formatToParts(new Date());
  return {
    year: Number(parts.find((part) => part.type === 'year')?.value || 0),
    month: Number(parts.find((part) => part.type === 'month')?.value || 0),
    day: Number(parts.find((part) => part.type === 'day')?.value || 0),
  };
}

function fullMonthsBetween(start, end) {
  let months = ((end.year - start.year) * 12) + (end.month - start.month);
  if (end.day < start.day) months -= 1;
  return Math.max(0, months);
}

function computeAntiguedad(fechaIngreso) {
  const start = parseDateParts(fechaIngreso);
  if (!start) return '';
  const months = fullMonthsBetween(start, limaTodayDate());
  if (months < 1) return '0 - 1 MES';
  if (months <= 3) return '1 - 3 MESES';
  return '+3 MESES';
}

function normalizeRole(value) {
  return String(value || '')
    .trim()
    .toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/[^a-z0-9]+/g, ' ')
    .trim();
}

function isReportVisibleRole(role) {
  const value = normalizeRole(role);
  // Reportería operativa: solo asesor, validador u otro.
  // Si la vista no trae rol, se oculta para no mezclar TD/supervisores de fuentes antiguas.
  if (!value) return false;
  return ['asesor', 'validador', 'otro', 'asesor validador'].includes(value);
}

function roleOf(row) {
  const explicit = firstValue(row, ['Rol', 'rol', 'rol_operativo', 'Rol operativo'], null);
  if (explicit) return explicit;
  if (firstValue(row, ['validador_nombre', 'Validador'], null)) return 'validador';
  return null;
}

function normalizeConsolidatedBundle(payload) {
  const source = typeof payload === 'string' ? (() => {
    try {
      return JSON.parse(payload);
    } catch {
      return {};
    }
  })() : (payload || {});
  return typeof source === 'object' && source ? source : {};
}

function compactSupervisorName(value) {
  const clean = String(value || '').trim();
  if (!clean) return '—';
  const parts = clean.split(/\s+/).filter(Boolean);
  if (parts.length < 2) return formatPersonName(clean);
  const first = parts[0];
  const second = parts[1];
  return `${formatPersonName(first)} ${second.charAt(0).toUpperCase()}.`;
}

function buildProductividadSupervisorRows(rows) {
  const grouped = new Map();
  for (const row of asArray(rows)) {
    const supervisor = firstValue(row, ['Supervisor', 'supervisor'], 'Sin supervisor') || 'Sin supervisor';
    if (!grouped.has(supervisor)) grouped.set(supervisor, []);
    grouped.get(supervisor).push(row);
  }

  return [...grouped.entries()].map(([supervisor, groupRows]) => {
    const manejo = sumRows(groupRows, ['Manejo', 'llamadas_manejadas', 'llamadas_manejadas_interacciones']);
    const interacciones = sumRows(groupRows, ['Interacciones', 'interacciones_manejadas', 'interacciones']);
    const solicitudes = sumRows(groupRows, ['Solicitudes', 'Solicitudes WOW', 'solicitudes_wow']);
    const retenidas = sumRows(groupRows, ['Retenidas', 'retenidos', 'Retenidos', 'retenidas', 'retenidas_wow']);
    const ejecutadas = sumRows(groupRows, ['Ejecutadas', 'ejecutadas', 'ejecutadas_wow']);
    const pendientes = sumRows(groupRows, ['Pendientes', 'pendientes', 'pendientes_wow']);
    const timeoutQ = sumRows(groupRows, ['Timeout Q', 'timeout_q']);
    return {
      Asesor: 'TOTAL EQUIPO',
      Q: '',
      'Antigüedad': '—',
      Supervisor: supervisor,
      Manejo: manejo,
      Interacciones: interacciones,
      Solicitudes: solicitudes,
      Retenidas: retenidas,
      Ejecutadas: ejecutadas,
      Pendientes: pendientes,
      'Timeout Q': timeoutQ,
      '% Solic/Manejo': pctValue(solicitudes, manejo),
      '% Pend/Manejo': pctValue(pendientes, manejo),
      '% Retención': pctValue(retenidas, manejo),
      '% Timeout': pctValue(timeoutQ, interacciones || manejo),
      'Manejo medio': weightedAverageDurationHms(groupRows, ['Manejo medio', 'manejo_medio', 'manejo_medio_min'], ['Manejo', 'llamadas_manejadas', 'llamadas_manejadas_interacciones']),
      'Conv media': weightedAverageDurationHms(groupRows, ['Conv media', 'conv_media', 'conv_media_min'], ['Interacciones', 'interacciones_manejadas', 'interacciones', 'Manejo', 'llamadas_manejadas']),
      ACW: weightedAverageDurationHms(groupRows, ['ACW', 'acw', 'acw_media_min'], ['Interacciones', 'interacciones_manejadas', 'interacciones', 'Manejo', 'llamadas_manejadas']),
      report_date: firstValue(groupRows[0], ['report_date', 'Fecha', 'fecha'], null),
      division: firstValue(groupRows[0], ['division', 'División'], null),
      equipo: firstValue(groupRows[0], ['equipo', 'Equipo'], null),
      rol_operativo: 'supervisor',
    };
  }).sort((a, b) => numValue(firstValue(b, ['Manejo'], 0)) - numValue(firstValue(a, ['Manejo'], 0)) || String(a.Supervisor || '').localeCompare(String(b.Supervisor || '')));
}

function buildDianaRows(rows) {
  const uniqueRows = new Map();
  for (const row of asArray(rows)) {
    const manejo = numValue(firstValue(row, ['Manejo', 'llamadas_manejadas'], 0));
    const retenidas = numValue(firstValue(row, ['Retenidas', 'retenidas', 'retenidas_wow'], 0));
    const diana = normalizeDianaValue(firstValue(row, ['Diana', 'diana_abril'], null));
    const agentId = String(firstValue(row, ['agent_id_genesys', 'Agent ID Genesys', 'ID del agente'], '') || '').trim();
    const asesor = firstValue(row, ['nombre_dotacion', 'Asesor', 'asesor_nombre', 'asesor'], '');
    const fechaIngreso = firstValue(row, ['fecha_ingreso', 'Fecha ingreso'], '');
    const normalized = {
      agentId,
      diana,
      q: normalizeQValue(firstValue(row, ['Q', 'nro_rrhh', 'nro', 'N°'], '')),
      asesor,
      antiguedad: firstValue(row, ['antiguedad', 'Antigüedad'], '') || computeAntiguedad(fechaIngreso) || '',
      turno: firstValue(row, ['Turno', 'turno'], ''),
      supervisor: firstValue(row, ['Supervisor', 'supervisor'], ''),
      llamadas: manejo,
      retenidas,
      retencionPct: pctValue(retenidas, manejo) || 0,
    };
    const key = agentId || normalizeLookupKey(String(asesor || '').split(' ').filter(Boolean).sort().join(' '));
    if (!key) continue;
    const current = uniqueRows.get(key);
    if (!current || normalized.llamadas > current.llamadas || normalized.retenidas > current.retenidas) {
      uniqueRows.set(key, normalized);
    }
  }
  return Array.from(uniqueRows.values())
    .filter((row) => row.asesor && row.diana !== null)
    .sort((a, b) => {
      const dianaA = a.diana === null ? 999 : Number(a.diana);
      const dianaB = b.diana === null ? 999 : Number(b.diana);
      const dianaCmp = dianaA - dianaB;
      if (dianaCmp) return dianaCmp;
      if (b.retencionPct !== a.retencionPct) return b.retencionPct - a.retencionPct;
      if (b.retenidas !== a.retenidas) return b.retenidas - a.retenidas;
      if (b.llamadas !== a.llamadas) return b.llamadas - a.llamadas;
      return String(a.asesor).localeCompare(String(b.asesor), 'es-PE', { sensitivity: 'base' });
    });
}

function dianaHasActivity(row) {
  return numValue(row?.llamadas) > 0 || numValue(row?.retenidas) > 0;
}

function buildDianaGroups(rows, mode = 'diana') {
  const groups = [];
  const grouped = new Map();
  for (const row of rows) {
    const key = mode === 'q' ? normalizeQValue(row.q) : String(row.diana);
    if (!key) continue;
    if (!grouped.has(key)) grouped.set(key, []);
    grouped.get(key).push(row);
  }
  for (const [groupKey, items] of grouped.entries()) {
    const llamadas = items.reduce((sum, item) => sum + item.llamadas, 0);
    const retenidas = items.reduce((sum, item) => sum + item.retenidas, 0);
    groups.push({
      key: groupKey,
      rows: items,
      summary: {
        llamadas,
        retenidas,
        retencionPct: pctValue(retenidas, llamadas) || 0,
      },
    });
  }
  return groups.sort((a, b) => {
    if (mode === 'q') return qOrderValue(a.key) - qOrderValue(b.key);
    return Number(a.key) - Number(b.key);
  });
}

function dianaGroupTone(value, mode = 'diana') {
  const order = mode === 'q' ? qOrderValue(value) : Number(value);
  if (order === 1) return { bg: '#d9f4d0', fg: '#0f5132' };
  if (order === 2) return { bg: '#fde68a', fg: '#854d0e' };
  if (order === 3) return { bg: '#fecdd3', fg: '#9f1239' };
  return { bg: '#ef4444', fg: '#ffffff' };
}

async function exportElementAsPng(element, filename) {
  if (!element) throw new Error('No se encontró el reporte para exportar.');
  const rect = element.getBoundingClientRect();
  const cloned = element.cloneNode(true);
  const styleText = Array.from(document.styleSheets).map((sheet) => {
    try {
      return Array.from(sheet.cssRules || []).map((rule) => rule.cssText).join('\n');
    } catch {
      return '';
    }
  }).join('\n');
  const wrapper = document.createElement('div');
  wrapper.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
  wrapper.innerHTML = `<style>${styleText}</style>`;
  wrapper.appendChild(cloned);
  const svg = `
    <svg xmlns="http://www.w3.org/2000/svg" width="${Math.ceil(rect.width)}" height="${Math.ceil(rect.height)}">
      <foreignObject width="100%" height="100%">${new XMLSerializer().serializeToString(wrapper)}</foreignObject>
    </svg>
  `;
  const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  try {
    const image = await new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = () => reject(new Error('No se pudo renderizar la imagen del reporte.'));
      img.src = url;
    });
    const scale = 2;
    const canvas = document.createElement('canvas');
    canvas.width = Math.ceil(rect.width * scale);
    canvas.height = Math.ceil(rect.height * scale);
    const ctx = canvas.getContext('2d');
    ctx.scale(scale, scale);
    ctx.drawImage(image, 0, 0);
    const downloadUrl = canvas.toDataURL('image/png');
    const anchor = document.createElement('a');
    anchor.href = downloadUrl;
    anchor.download = filename;
    anchor.click();
  } finally {
    URL.revokeObjectURL(url);
  }
}

function dianaRetentionTone(value) {
  if (value === null || value === undefined || value === '') return { bg: '#ffffff', fg: '#0f172a' };
  const pct = Number(value || 0);
  if (pct < 50) return { bg: '#fee2e2', fg: '#991b1b' };
  if (pct < 55) return { bg: '#fef3c7', fg: '#92400e' };
  return { bg: '#dcfce7', fg: '#166534' };
}

function dianaRetentionClass(value) {
  if (value === null || value === undefined || value === '') return '';
  const pct = Number(value || 0);
  if (pct < 50) return 'pct-low';
  if (pct < 55) return 'pct-mid';
  return 'pct-high';
}

function isAllowedOperationalAlert(row) {
  const tipo = normalizeLookupKey(firstValue(row, ['Tipo alerta', 'tipo_alerta'], ''));
  const detalle = normalizeLookupKey(firstValue(row, ['Detalle', 'alerta_detalle'], ''));
  const totalMin = numValue(firstValue(row, ['Min Alertas', 'tiempo_total_alertas_min'], 0));
  const avgMin = numValue(firstValue(row, ['Min Prom', 'tiempo_promedio_alerta_min'], 0));
  const isShort = detalle.includes('muy corto') || (avgMin > 0 && avgMin < 1);
  const isOcup = tipo.includes('ocup');
  const isDisp = tipo.includes('disp');
  const isCap = tipo.includes('cap') || tipo.includes('capacit');
  const isDesc = tipo.includes('descon');
  if (isShort) return isOcup || isDisp || isCap || isDesc;
  if (isOcup && totalMin > 15) return true;
  if (isCap && totalMin > 20) return true;
  return false;
}

function dianaTurnoTone(value) {
  const turno = normalizeLookupKey(value);
  if (turno.includes('tarde')) return { bg: '#fecdd3', fg: '#9f1239' };
  return { bg: '#dcfce7', fg: '#166534' };
}

function drawCanvasCell(ctx, { x, y, w, h, text, fill = '#ffffff', color = '#0f172a', align = 'center', font = '500 13px Arial', border = 'rgba(15,23,42,.14)', padding = 10 }) {
  ctx.fillStyle = fill;
  ctx.fillRect(x, y, w, h);
  ctx.strokeStyle = border;
  ctx.strokeRect(x, y, w, h);
  ctx.fillStyle = color;
  ctx.font = font;
  ctx.textBaseline = 'middle';
  ctx.textAlign = align;
  const tx = align === 'left' ? x + padding : align === 'right' ? x + w - padding : x + (w / 2);
  ctx.fillText(String(text ?? ''), tx, y + (h / 2));
}

async function exportDianaAsPng(rows, reportDate, filename, options = {}) {
  const mode = options.mode === 'q' ? 'q' : 'diana';
  const firstColumnLabel = mode === 'q' ? 'Q' : 'DIANA';
  const groups = buildDianaGroups(rows, mode);
  if (!groups.length) throw new Error('No hay datos para exportar.');
  const cols = { diana: 64, asesor: 300, antiguedad: 110, turno: 170, supervisor: 120, llamadas: 78, retenidas: 72, pct: 108 };
  const totalWidth = Object.values(cols).reduce((sum, value) => sum + value, 0);
  const bannerH = 34;
  const headerH = 32;
  const rowH = 28;
  const summaryH = 30;
  const gapH = 34;
  const footerH = 42;
  const bodyH = groups.reduce((sum, group, index) => sum + (group.rows.length * rowH) + summaryH + (index > 0 ? gapH : 0), 0) + gapH;
  const totalHeight = bannerH + headerH + bodyH + footerH;
  const scale = 2;
  const canvas = document.createElement('canvas');
  canvas.width = totalWidth * scale;
  canvas.height = totalHeight * scale;
  const ctx = canvas.getContext('2d');
  ctx.scale(scale, scale);
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(0, 0, totalWidth, totalHeight);
  drawCanvasCell(ctx, { x: 0, y: 0, w: totalWidth, h: bannerH, text: reportDate || '', fill: '#6b1f7a', color: '#ffffff', align: 'left', font: '700 14px Arial', padding: 18, border: '#6b1f7a' });
  let headerX = 0;
  for (const [label, width] of [[firstColumnLabel, cols.diana], ['ASESORES', cols.asesor], ['ANTIG.', cols.antiguedad], ['TURNO', cols.turno], ['SUPERVISOR', cols.supervisor], ['LLAM.', cols.llamadas], ['RET.', cols.retenidas], ['% RET / LLAM', cols.pct]]) {
    drawCanvasCell(ctx, { x: headerX, y: bannerH, w: width, h: headerH, text: label, fill: '#6b1f7a', color: '#ffffff', font: '700 12px Arial', border: '#5b1678' });
    headerX += width;
  }
  let y = bannerH + headerH;
  for (let groupIndex = 0; groupIndex < groups.length; groupIndex += 1) {
    const group = groups[groupIndex];
    if (groupIndex > 0) {
      ctx.fillStyle = '#ffffff';
      ctx.fillRect(0, y, totalWidth, gapH);
      y += gapH;
    }
    const groupHeight = group.rows.length * rowH;
    const groupColors = dianaGroupTone(group.key, mode);
    drawCanvasCell(ctx, { x: 0, y, w: cols.diana, h: groupHeight, text: group.key, fill: groupColors.bg, color: groupColors.fg, font: '700 34px Arial' });
    let rowY = y;
    for (const row of group.rows) {
      const turnoTone = dianaTurnoTone(row.turno);
      const hasActivity = dianaHasActivity(row);
      const pctTone = dianaRetentionTone(hasActivity ? row.retencionPct : null);
      drawCanvasCell(ctx, { x: cols.diana, y: rowY, w: cols.asesor, h: rowH, text: formatPersonName(row.asesor), fill: '#ffffff', color: '#0f172a', align: 'left', font: '500 12px Arial' });
      drawCanvasCell(ctx, { x: cols.diana + cols.asesor, y: rowY, w: cols.antiguedad, h: rowH, text: row.antiguedad || '-', fill: '#ffffff', color: '#0f172a', font: '500 11px Arial' });
      drawCanvasCell(ctx, { x: cols.diana + cols.asesor + cols.antiguedad, y: rowY, w: cols.turno, h: rowH, text: formatPersonName(row.turno), fill: turnoTone.bg, color: turnoTone.fg, font: '700 13px Arial' });
      drawCanvasCell(ctx, { x: cols.diana + cols.asesor + cols.antiguedad + cols.turno, y: rowY, w: cols.supervisor, h: rowH, text: compactSupervisorName(row.supervisor), fill: '#ffffff', color: '#0f172a', font: '500 12px Arial' });
      drawCanvasCell(ctx, { x: cols.diana + cols.asesor + cols.antiguedad + cols.turno + cols.supervisor, y: rowY, w: cols.llamadas, h: rowH, text: hasActivity ? number(row.llamadas) : '-', fill: '#ffffff', color: '#0f172a', font: '500 12px Arial' });
      drawCanvasCell(ctx, { x: cols.diana + cols.asesor + cols.antiguedad + cols.turno + cols.supervisor + cols.llamadas, y: rowY, w: cols.retenidas, h: rowH, text: hasActivity ? number(row.retenidas) : '-', fill: '#ffffff', color: '#0f172a', font: '500 12px Arial' });
      drawCanvasCell(ctx, { x: cols.diana + cols.asesor + cols.antiguedad + cols.turno + cols.supervisor + cols.llamadas + cols.retenidas, y: rowY, w: cols.pct, h: rowH, text: hasActivity ? fmtPct(row.retencionPct) : '-', fill: pctTone.bg, color: pctTone.fg, font: '700 12px Arial' });
      rowY += rowH;
    }
    const summaryTone = dianaRetentionTone(group.summary.retencionPct);
    drawCanvasCell(ctx, { x: 0, y: rowY, w: cols.diana + cols.asesor + cols.antiguedad + cols.turno + cols.supervisor, h: summaryH, text: '', fill: '#ffffff', color: '#0f172a' });
    drawCanvasCell(ctx, { x: cols.diana + cols.asesor + cols.antiguedad + cols.turno + cols.supervisor, y: rowY, w: cols.llamadas, h: summaryH, text: number(group.summary.llamadas), fill: '#ffffff', color: '#0f172a', font: '700 16px Arial' });
    drawCanvasCell(ctx, { x: cols.diana + cols.asesor + cols.antiguedad + cols.turno + cols.supervisor + cols.llamadas, y: rowY, w: cols.retenidas, h: summaryH, text: number(group.summary.retenidas), fill: '#ffffff', color: '#0f172a', font: '700 16px Arial' });
    drawCanvasCell(ctx, { x: cols.diana + cols.asesor + cols.antiguedad + cols.turno + cols.supervisor + cols.llamadas + cols.retenidas, y: rowY, w: cols.pct, h: summaryH, text: fmtPct(group.summary.retencionPct), fill: summaryTone.bg, color: summaryTone.fg, font: '700 16px Arial' });
    y = rowY + summaryH;
  }
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(0, y, totalWidth, gapH);
  y += gapH;
  const totalLlamadas = rows.reduce((sum, row) => sum + row.llamadas, 0);
  const totalRetenidas = rows.reduce((sum, row) => sum + row.retenidas, 0);
  const totalPct = pctValue(totalRetenidas, totalLlamadas) || 0;
  const totalTone = dianaRetentionTone(totalPct);
  drawCanvasCell(ctx, { x: 0, y, w: cols.diana + cols.asesor + cols.antiguedad + cols.turno + cols.supervisor, h: footerH, text: 'TOTAL', fill: '#ffffff', color: '#0f172a', font: '700 22px Arial' });
  drawCanvasCell(ctx, { x: cols.diana + cols.asesor + cols.antiguedad + cols.turno + cols.supervisor, y, w: cols.llamadas, h: footerH, text: number(totalLlamadas), fill: '#ffffff', color: '#0f172a', font: '700 18px Arial' });
  drawCanvasCell(ctx, { x: cols.diana + cols.asesor + cols.antiguedad + cols.turno + cols.supervisor + cols.llamadas, y, w: cols.retenidas, h: footerH, text: number(totalRetenidas), fill: '#ffffff', color: '#0f172a', font: '700 18px Arial' });
  drawCanvasCell(ctx, { x: cols.diana + cols.asesor + cols.antiguedad + cols.turno + cols.supervisor + cols.llamadas + cols.retenidas, y, w: cols.pct, h: footerH, text: fmtPct(totalPct), fill: totalTone.bg, color: totalTone.fg, font: '700 18px Arial' });
  const anchor = document.createElement('a');
  anchor.href = canvas.toDataURL('image/png');
  anchor.download = filename;
  anchor.click();
}

function reportDateOf(row) {
  return firstValue(row, ['Fecha', 'report_date'], '');
}

function divisionOf(row) {
  return firstValue(row, ['División', 'division'], '');
}

function supervisorOf(row) {
  return firstValue(row, ['Supervisor', 'supervisor', 'validador_supervisor'], '');
}

function tramoOf(row) {
  return firstValue(row, ['Tramo', 'tramo_label'], '');
}

function secondsToHms(totalSeconds) {
  const total = Math.max(0, Math.round(Number(totalSeconds || 0)));
  const h = Math.floor(total / 3600);
  const m = Math.floor((total % 3600) / 60);
  const s = total % 60;
  return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}

function minutesToHms(minutes) {
  return secondsToHms(Number(minutes || 0) * 60);
}

function durationLikeToSeconds(value) {
  if (value === null || value === undefined || value === '') return null;
  if (typeof value === 'number') return Number.isFinite(value) ? value * 60 : null;
  const raw = String(value).trim();
  if (!raw || raw === '—') return null;
  if (/^\d+:\d{2}(?::\d{2})?$/.test(raw)) {
    const parts = raw.split(':').map((part) => Number(part));
    return parts.length === 3
      ? (parts[0] * 3600) + (parts[1] * 60) + parts[2]
      : (parts[0] * 60) + parts[1];
  }
  const numeric = Number(raw.replace('%', '').replace(',', '.'));
  return Number.isFinite(numeric) ? numeric * 60 : null;
}

function durationLikeToHms(value) {
  const seconds = durationLikeToSeconds(value);
  return seconds === null ? '—' : secondsToHms(seconds);
}

function stateDurationToSeconds(value) {
  if (value === null || value === undefined || value === '') return null;
  if (typeof value === 'number') return Number.isFinite(value) ? value : null;
  const raw = String(value).trim();
  if (!raw || raw === '—') return null;
  if (/^\d+:\d{2}(?::\d{2})?$/.test(raw)) {
    const parts = raw.split(':').map((part) => Number(part));
    return parts.length === 3
      ? (parts[0] * 3600) + (parts[1] * 60) + parts[2]
      : (parts[0] * 60) + parts[1];
  }
  const numeric = Number(raw.replace(',', '.'));
  return Number.isFinite(numeric) ? numeric : null;
}

function pctNumber(value) {
  if (value === null || value === undefined || value === '') return null;
  const n = Number(String(value).replace('%', '').replace(',', '.'));
  return Number.isFinite(n) ? n : null;
}

function reportValue(row, column) {
  const key = columnKey(column) || '';
  const label = columnLabel(column) || '';
  let value = columnValue(row, column);
  if (typeof column === 'object' && column?.format === 'hmsMinutes') return durationLikeToHms(value);
  if ((value === null || value === undefined || value === '') && typeof column === 'object' && column?.sortKey) {
    const fallback = firstValue(row, [column.sortKey, ...(column.sortAlt || [])], null);
    if (fallback !== null && fallback !== undefined && fallback !== '') {
      if (String(label).startsWith('T ') || String(key).startsWith('T ')) return minutesToHms(fallback);
      value = fallback;
    }
  }
  if (key.includes('pct') || key.includes('participacion') || label.trim().startsWith('%')) return fmtPct(value);
  if (key.endsWith('_min') || key.includes('medio_min')) return fmtMin(value);
  if (isPersonColumn(column)) return formatPersonName(value);
  if (typeof value === 'number') return number(value);
  return formatValue(value);
}

function reportCellClass(row, column) {
  if (typeof column !== 'object') return '';

  if (column?.style === 'tmo') {
    const seconds = durationLikeToSeconds(columnValue(row, column));
    if (seconds !== null && seconds > 30 * 60) return 'report-cell-danger';
    if (seconds !== null && seconds > 15 * 60) return 'report-cell-warning';
  }

  if (column?.style === 'acw') {
    const seconds = durationLikeToSeconds(columnValue(row, column));
    if (seconds !== null && seconds > 90) return 'report-cell-danger';
  }

  if (column?.style === 'avg-alert') {
    const seconds = durationLikeToSeconds(columnValue(row, column));
    if (seconds !== null && seconds < 60) return 'report-cell-danger';
  }

  if (column?.style === 'ocupado-alert') {
    const seconds = durationLikeToSeconds(columnValue(row, column));
    if (seconds !== null && seconds > 20 * 60) return 'report-cell-danger';
    if (seconds !== null && seconds > 15 * 60) return 'report-cell-warning';
  }

  if (column?.style === 'disp-alert') {
    const seconds = durationLikeToSeconds(columnValue(row, column));
    const q = numValue(firstValue(row, ['q_disp', 'disp_q', 'Q Disp'], 0));
    if ((seconds !== null && seconds > 10 * 60) || q > 5) return 'report-cell-danger';
  }

  if (column?.style === 'comida-alert') {
    const seconds = durationLikeToSeconds(columnValue(row, column));
    const q = numValue(firstValue(row, ['q_comida', 'comida_q', 'Q Comida'], 0));
    if ((seconds !== null && seconds > 60 * 60) || q > 1) return 'report-cell-danger';
  }

  if (column?.style === 'retencion') {
    const pct = pctNumber(columnValue(row, column));
    if (pct === null) return '';
    if (pct < 50) return 'report-cell-retention-red';
    if (pct < 55) return 'report-cell-retention-amber';
    return 'report-cell-retention-green';
  }

  if (column?.style === 'timeout-q') {
    const count = numValue(columnValue(row, column));
    if (count > 3) return 'report-cell-danger';
    return '';
  }

  if (column?.style === 'solicitudes') {
    const pct = pctNumber(columnValue(row, column));
    if (pct === null) return '';
    if (pct < 50) return 'report-cell-retention-gray';
    if (pct < 85) return 'report-cell-retention-red';
    if (pct <= 95) return 'report-cell-retention-amber';
    return 'report-cell-retention-green';
  }

  if (column?.style === 'q-level') {
    const q = normalizeQValue(columnValue(row, column));
    if (q === 'Q1') return 'report-cell-q1';
    if (q === 'Q2') return 'report-cell-q2';
    if (q === 'Q3') return 'report-cell-q3';
    if (q === 'Q4') return 'report-cell-q4';
    return '';
  }

  if (!column?.alertKey) return '';
  const alert = firstValue(row, [column.alertKey, ...(column.alertAlt || [])], false);
  return boolValue(alert) ? 'report-cell-alert' : '';
}

function reportCellTone(row, column) {
  const className = reportCellClass(row, column);
  if (className.includes('report-cell-retention-gray')) return { bg: '#64748b', fg: '#ffffff' };
  if (className.includes('report-cell-retention-red')) return { bg: '#fee2e2', fg: '#991b1b' };
  if (className.includes('report-cell-retention-amber')) return { bg: '#fef3c7', fg: '#92400e' };
  if (className.includes('report-cell-retention-green')) return { bg: '#dcfce7', fg: '#166534' };
  if (className.includes('report-cell-q1')) return { bg: '#d9f4d0', fg: '#0f5132' };
  if (className.includes('report-cell-q2')) return { bg: '#fde68a', fg: '#854d0e' };
  if (className.includes('report-cell-q3')) return { bg: '#fecdd3', fg: '#9f1239' };
  if (className.includes('report-cell-q4')) return { bg: '#ef4444', fg: '#ffffff' };
  if (className.includes('report-cell-warning')) return { bg: '#fef3c7', fg: '#92400e' };
  if (className.includes('report-cell-danger')) return { bg: '#fee2e2', fg: '#7f1d1d' };
  if (className.includes('report-cell-alert')) return { bg: '#fee2e2', fg: '#991b1b' };
  return { bg: '#ffffff', fg: '#0f172a' };
}

function visibleColumnsForSelection(columns, selectedIds) {
  const base = Array.isArray(columns) ? columns : [];
  if (!selectedIds) return base;
  const selected = new Set(selectedIds);
  return base.filter((column) => selected.has(columnId(column)));
}

function rowsForCsvExport(rows, columns) {
  return asArray(rows).map((row) => Object.fromEntries(columns.map((column) => [columnLabel(column), reportValue(row, column)])));
}

function estimateColumnWidth(rows, column) {
  const id = normalizeLookupKey(columnLabel(column));
  if (id.includes('asesor')) return 250;
  if (id.includes('supervisor')) return 150;
  if (id.includes('equipo')) return 180;
  if (id.includes('detalle') || id.includes('conclusion')) return 260;
  if (id.includes('tipo alerta') || id.includes('estado')) return 150;
  if (id.includes('fecha')) return 110;
  if (id.includes('t ') || id.includes('timeout') || id.includes('porcentaje') || id.includes('%')) return 100;
  const sample = rows.slice(0, 25).map((row) => String(reportValue(row, column) || ''));
  const maxLen = Math.max(String(columnLabel(column)).length, ...sample.map((value) => value.length));
  return Math.max(72, Math.min(180, 18 + (maxLen * 7)));
}

async function exportReportTableAsPng({ title, subtitle = '', rows, columns, summaryRow = null, filename }) {
  const dataRows = asArray(rows);
  if (!dataRows.length) throw new Error('No hay filas para exportar.');
  const cols = Array.isArray(columns) ? columns : [];
  if (!cols.length) throw new Error('No hay columnas visibles para exportar.');
  const colWidths = cols.map((column) => estimateColumnWidth(dataRows, column));
  const totalWidth = colWidths.reduce((sum, width) => sum + width, 0);
  const bannerH = 34;
  const subBannerH = subtitle ? 22 : 0;
  const headerH = 34;
  const rowH = 28;
  const summaryH = summaryRow ? 34 : 0;
  const totalHeight = bannerH + subBannerH + headerH + (Math.min(dataRows.length, 180) * rowH) + summaryH;
  const scale = 2;
  const canvas = document.createElement('canvas');
  canvas.width = totalWidth * scale;
  canvas.height = totalHeight * scale;
  const ctx = canvas.getContext('2d');
  ctx.scale(scale, scale);
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(0, 0, totalWidth, totalHeight);

  drawCanvasCell(ctx, { x: 0, y: 0, w: totalWidth, h: bannerH, text: title, fill: '#6b1f7a', color: '#ffffff', align: 'left', font: '700 16px Arial', padding: 16, border: '#6b1f7a' });
  if (subtitle) drawCanvasCell(ctx, { x: 0, y: bannerH, w: totalWidth, h: subBannerH, text: subtitle, fill: '#f3e8ff', color: '#6b1f7a', align: 'left', font: '600 12px Arial', padding: 16, border: '#d8b4fe' });

  let y = bannerH + subBannerH;
  let x = 0;
  cols.forEach((column, index) => {
    drawCanvasCell(ctx, { x, y, w: colWidths[index], h: headerH, text: columnLabel(column), fill: '#6b1f7a', color: '#ffffff', font: '700 12px Arial', border: '#5b1678' });
    x += colWidths[index];
  });
  y += headerH;

  dataRows.slice(0, 180).forEach((row) => {
    let cellX = 0;
    cols.forEach((column, index) => {
      const tone = reportCellTone(row, column);
      const align = isPersonColumn(column) || normalizeLookupKey(columnLabel(column)).includes('detalle') ? 'left' : 'center';
      drawCanvasCell(ctx, {
        x: cellX,
        y,
        w: colWidths[index],
        h: rowH,
        text: reportValue(row, column),
        fill: tone.bg,
        color: tone.fg,
        align,
        font: tone.fg === '#0f172a' ? '500 12px Arial' : '700 12px Arial',
        border: 'rgba(148,163,184,.35)',
      });
      cellX += colWidths[index];
    });
    y += rowH;
  });

  if (summaryRow) {
    let cellX = 0;
    cols.forEach((column, index) => {
      const tone = reportCellTone(summaryRow, column);
      const align = isPersonColumn(column) ? 'left' : 'center';
      drawCanvasCell(ctx, {
        x: cellX,
        y,
        w: colWidths[index],
        h: summaryH,
        text: reportValue(summaryRow, column),
        fill: '#ffffff',
        color: tone.fg === '#0f172a' ? '#0f172a' : tone.fg,
        align,
        font: '700 14px Arial',
        border: 'rgba(37,99,235,.28)',
      });
      if (tone.bg !== '#ffffff' && normalizeLookupKey(columnLabel(column)).includes('%')) {
        ctx.fillStyle = tone.bg;
        ctx.fillRect(cellX + 1, y + 1, colWidths[index] - 2, summaryH - 2);
        ctx.strokeStyle = 'rgba(37,99,235,.28)';
        ctx.strokeRect(cellX, y, colWidths[index], summaryH);
        ctx.fillStyle = tone.fg;
        ctx.font = '700 14px Arial';
        ctx.textBaseline = 'middle';
        ctx.textAlign = 'center';
        ctx.fillText(String(reportValue(summaryRow, column) || ''), cellX + (colWidths[index] / 2), y + (summaryH / 2));
      }
      cellX += colWidths[index];
    });
  }

  const anchor = document.createElement('a');
  anchor.href = canvas.toDataURL('image/png');
  anchor.download = filename;
  anchor.click();
}

function sortValue(row, column) {
  if (typeof column === 'object' && column?.sortKey) {
    return firstValue(row, [column.sortKey, ...(column.sortAlt || [])], null);
  }
  return columnValue(row, column);
}

function sortPrimitive(value) {
  if (value === null || value === undefined || value === '') {
    return { type: 'empty', value: null };
  }

  if (typeof value === 'number') {
    return { type: 'number', value };
  }

  const raw = String(value).trim();

  // Soporta tiempos tipo 02:31, 01:00:00, 24:00:49 o 529:22.
  if (/^\d+:\d{2}(?::\d{2})?$/.test(raw)) {
    const parts = raw.split(':').map((part) => Number(part));
    const seconds = parts.length === 3
      ? (parts[0] * 3600) + (parts[1] * 60) + parts[2]
      : (parts[0] * 60) + parts[1];
    return { type: 'number', value: seconds };
  }

  const numeric = Number(raw.replace(/%/g, '').replace(/,/g, ''));
  if (Number.isFinite(numeric) && raw !== '') {
    return { type: 'number', value: numeric };
  }

  return { type: 'string', value: raw.toLocaleLowerCase('es-PE') };
}

function compareRowsByColumn(a, b, column, direction = 'desc') {
  const av = sortPrimitive(sortValue(a, column));
  const bv = sortPrimitive(sortValue(b, column));

  if (av.type === 'empty' && bv.type === 'empty') return 0;
  if (av.type === 'empty') return 1;
  if (bv.type === 'empty') return -1;

  let result = 0;
  if (av.type === 'number' && bv.type === 'number') {
    result = av.value - bv.value;
  } else {
    result = String(av.value).localeCompare(String(bv.value), 'es-PE', { numeric: true, sensitivity: 'base' });
  }

  return direction === 'asc' ? result : -result;
}

function ReportTable({
  rows,
  columns,
  max = 80,
  empty = 'Sin datos para los filtros seleccionados.',
  summaryRow = null,
  sortState: externalSortState = null,
  onSortChange = null,
  onDisplayRowsChange = null,
}) {
  const rawRows = asArray(rows);
  const explicitColumns = Array.isArray(columns);
  const cols = explicitColumns ? columns : Object.keys(rawRows[0] || {}).slice(0, 10);
  const [internalSortState, setInternalSortState] = useState(null);
  const sortState = externalSortState ?? internalSortState;
  const scrollRef = useRef(null);

  const sortedRows = useMemo(() => {
    if (!sortState?.key) return rawRows;
    const column = cols.find((candidate) => (columnKey(candidate) || columnLabel(candidate)) === sortState.key);
    if (!column) return rawRows;
    return rawRows.slice().sort((a, b) => compareRowsByColumn(a, b, column, sortState.direction));
  }, [rawRows, cols, sortState]);

  const data = useMemo(() => sortedRows.slice(0, max), [sortedRows, max]);

  useEffect(() => {
    onDisplayRowsChange?.(data);
  }, [data, onDisplayRowsChange]);

  if (!rawRows.length) return <p className="muted pad">{empty}</p>;
  if (explicitColumns && !cols.length) return <p className="muted pad">Selecciona al menos una columna para ver el reporte.</p>;

  function toggleSort(column) {
    const key = columnKey(column) || columnLabel(column);
    const nextState = ((current) => {
      if (current?.key === key) {
        return { key, direction: current.direction === 'desc' ? 'asc' : 'desc' };
      }
      return { key, direction: 'desc' };
    })(sortState);
    if (onSortChange) onSortChange(nextState);
    else setInternalSortState(nextState);
  }

  function scrollTable(direction) {
    const el = scrollRef.current;
    if (!el) return;
    const amount = Math.max(el.clientWidth * 0.72, 420);
    el.scrollBy({ left: direction * amount, behavior: 'smooth' });
  }

  return (
    <div className="report-table-shell">
      <div className="table-scroll-controls" aria-label="Desplazar tabla horizontalmente">
        <button type="button" onClick={() => scrollTable(-1)} title="Mover tabla a la izquierda">←</button>
        <button type="button" onClick={() => scrollTable(1)} title="Mover tabla a la derecha">→</button>
      </div>
      <div className="table-wrap report-table" ref={scrollRef}>
      <table>
        <thead><tr>{cols.map((column) => {
          const key = columnKey(column) || columnLabel(column);
          const isActive = sortState?.key === key;
          const arrow = isActive ? (sortState.direction === 'desc' ? '↓' : '↑') : '↕';
          return (
            <th key={key} className={isActive ? 'sortable-th active' : 'sortable-th'}>
              <button type="button" className="sort-btn" onClick={() => toggleSort(column)} title={`Ordenar por ${columnLabel(column)}`}>
                <span>{columnLabel(column)}</span>
                <b>{arrow}</b>
              </button>
            </th>
          );
        })}</tr></thead>
        <tbody>{data.map((row, idx) => <tr key={idx}>{cols.map((column) => {
          const key = columnKey(column) || columnLabel(column);
          return <td key={key} className={reportCellClass(row, column)}>{reportValue(row, column)}</td>;
        })}</tr>)}</tbody>
        {summaryRow ? (
          <tfoot>
            <tr className="report-total-row">
              {cols.map((column) => {
                const key = columnKey(column) || columnLabel(column);
                return <td key={key} className={reportCellClass(summaryRow, column)}>{reportValue(summaryRow, column)}</td>;
              })}
            </tr>
          </tfoot>
        ) : null}
      </table>
      </div>
    </div>
  );
}

function DianaBoard({ rows, reportDate, boardRef, mode = 'diana' }) {
  const groups = useMemo(() => buildDianaGroups(rows, mode), [rows, mode]);
  const totalLlamadas = rows.reduce((sum, row) => sum + row.llamadas, 0);
  const totalRetenidas = rows.reduce((sum, row) => sum + row.retenidas, 0);
  const totalPct = pctValue(totalRetenidas, totalLlamadas) || 0;
  const firstColumnLabel = mode === 'q' ? 'Q' : 'DIANA';
  const boardTitle = mode === 'q' ? 'Reporte por Q' : 'Reporte de Diana';

  if (!rows.length) return <p className="muted pad">Sin datos para construir la diana con los filtros seleccionados.</p>;

  return (
    <div className="diana-board" ref={boardRef}>
      <div className="diana-board-head">
        <div>
          <strong>{boardTitle}</strong>
          <span>{reportDate || 'Último corte disponible'}</span>
        </div>
      </div>
      <div className="diana-table-wrap">
        <table className="diana-table">
        <colgroup>
          <col className="diana-col-diana" style={{ width: '64px' }} />
          <col className="diana-col-asesor" style={{ width: '300px' }} />
          <col className="diana-col-antiguedad" style={{ width: '120px' }} />
          <col className="diana-col-turno" style={{ width: '170px' }} />
          <col className="diana-col-supervisor" style={{ width: '120px' }} />
          <col className="diana-col-llamadas" style={{ width: '78px' }} />
          <col className="diana-col-retenidas" style={{ width: '72px' }} />
          <col className="diana-col-pct" style={{ width: '108px' }} />
        </colgroup>
        <thead>
          <tr>
            <th>{firstColumnLabel}</th>
            <th>ASESORES</th>
            <th>ANTIG.</th>
            <th>TURNO</th>
            <th>SUPERVISOR</th>
            <th>LLAM.</th>
            <th>RET.</th>
            <th>% RET / LLAM</th>
          </tr>
        </thead>
        <tbody>
          {groups.map((group, groupIndex) => (
            <React.Fragment key={`diana-group-${group.key}`}>
              {groupIndex > 0 ? (
                <tr className="diana-gap-row" aria-hidden="true">
                  <td colSpan={8}></td>
                </tr>
              ) : null}
              {group.rows.map((row, index) => (
                <tr key={`diana-row-${group.key}-${index}-${normalizeLookupKey(row.asesor)}-${row.llamadas}-${row.retenidas}`}>
                  {index === 0 ? (
                    <td className={`diana-cell diana-level-${mode === 'q' ? qOrderValue(group.key) : group.key}`} rowSpan={group.rows.length}>{group.key}</td>
                  ) : null}
                  <td>{formatPersonName(row.asesor)}</td>
                  <td>{row.antiguedad || '-'}</td>
                  <td className={`turno-cell turno-${normalizeLookupKey(row.turno)}`}>{formatPersonName(row.turno)}</td>
                  <td>{compactSupervisorName(row.supervisor)}</td>
                  <td>{dianaHasActivity(row) ? number(row.llamadas) : '-'}</td>
                  <td>{dianaHasActivity(row) ? number(row.retenidas) : '-'}</td>
                  <td className={`diana-pct-cell ${dianaRetentionClass(dianaHasActivity(row) ? row.retencionPct : null)}`}>{dianaHasActivity(row) ? fmtPct(row.retencionPct) : '-'}</td>
                </tr>
              ))}
              <tr className="diana-summary-row" key={`diana-summary-${group.key}`}>
                <td colSpan={5}></td>
                <td>{number(group.summary.llamadas)}</td>
                <td>{number(group.summary.retenidas)}</td>
                <td className={`diana-pct-cell ${dianaRetentionClass(group.summary.retencionPct)}`}>{fmtPct(group.summary.retencionPct)}</td>
              </tr>
            </React.Fragment>
          ))}
        </tbody>
        <tfoot>
          <tr>
            <td colSpan={5}>TOTAL</td>
            <td>{number(totalLlamadas)}</td>
            <td>{number(totalRetenidas)}</td>
            <td className={`diana-pct-cell ${dianaRetentionClass(totalPct)}`}>{fmtPct(totalPct)}</td>
          </tr>
        </tfoot>
        </table>
      </div>
    </div>
  );
}

function Dashboard({ api, status, reports, refresh, loadReports }) {
  const summary = status?.summary || {};
  const oracle = status?.oracle?.status || {};
  const byDay = reports.byDay || [];
  const byStatus = reports.byStatus || [];
  const refreshDashboard = async () => {
    await Promise.allSettled([refresh(), loadReports()]);
  };
  return (
    <div className="page-grid">
      <section className="metrics-row">
        <MetricCard title="Total mes" value={number(summary.total_bajas)} subtitle="Registros en Supabase" icon={Database} />
        <MetricCard title="Sesión WOW" value={oracle.authStatus || '—'} subtitle={`vence ${safeDate(oracle.tokenExpiresAt)}`} icon={LockKeyhole} tone={oracle.authStatus === 'AUTH_OK' ? 'green' : 'amber'} />
        <MetricCard title="Último sync" value={oracle.lastSync?.status || '—'} subtitle={safeDate(oracle.lastSync?.completedAt)} icon={RefreshCw} tone="cyan" />
        <MetricCard title="Estados" value={number(summary.estados_distintos)} subtitle="distintos este mes" icon={Layers3} tone="purple" />
      </section>

      <section className="panel wide">
        <div className="panel-head">
          <div><h3>Volumen por día</h3><p>Solicitudes creadas por fecha Perú.</p></div>
          <button className="secondary" onClick={refreshDashboard}><RefreshCw size={16} />Actualizar</button>
        </div>
        <div className="chart-box">
          <ResponsiveContainer width="100%" height={270}>
            <AreaChart data={byDay}>
              <defs><linearGradient id="dayGrad" x1="0" y1="0" x2="0" y2="1"><stop offset="5%" stopColor="#2563eb" stopOpacity={0.35}/><stop offset="95%" stopColor="#2563eb" stopOpacity={0}/></linearGradient></defs>
              <CartesianGrid strokeDasharray="3 3" stroke="rgba(148,163,184,.25)" />
              <XAxis dataKey="fecha_peru" tick={{ fontSize: 11 }} />
              <YAxis tick={{ fontSize: 11 }} />
              <Tooltip />
              <Area type="monotone" dataKey="total" stroke="#2563eb" fill="url(#dayGrad)" strokeWidth={2} />
            </AreaChart>
          </ResponsiveContainer>
        </div>
      </section>

      <section className="panel">
        <div className="panel-head"><div><h3>Estados</h3><p>Distribución actual del mes.</p></div></div>
        <ResponsiveContainer width="100%" height={260}>
          <PieChart>
            <Pie data={byStatus} dataKey="total" nameKey="estado" innerRadius={55} outerRadius={90} paddingAngle={3}>
              {byStatus.map((_, i) => <Cell key={i} fill={PALETTE[i % PALETTE.length]} />)}
            </Pie>
            <Tooltip />
          </PieChart>
        </ResponsiveContainer>
        <DataTable rows={byStatus} max={5} />
      </section>

      <section className="panel">
        <div className="panel-head"><div><h3>Últimas ejecuciones</h3><p>Historial reciente de sincronización.</p></div></div>
        <DataTable rows={status?.syncRuns || []} max={6} />
      </section>
    </div>
  );
}

function SyncPage({ api, status, notify, refresh, user }) {
  const [loading, setLoading] = useState(false);
  const runSync = async () => {
    setLoading(true);
    try {
      const res = await api.request('/api/sync/current-month', { method: 'POST', body: '{}' });
      notify('Sync iniciado', res.status || res.jobId || 'El job fue enviado al agente.', 'success');
      setTimeout(refresh, 2500);
    } catch (e) {
      notify('Error al iniciar sync', e.message, 'danger');
    } finally {
      setLoading(false);
    }
  };
  return (
    <div className="page-grid">
      <section className="panel wide accent-panel">
        <div>
          <p className="eyebrow">Control de scraping</p>
          <h3>Sincronización del mes completo</h3>
          <p>Cada ejecución consulta desde el primer día del mes hasta el último día del mes en zona horaria Perú.</p>
        </div>
        <button className="primary" disabled={loading || user?.role !== 'admin'} onClick={runSync}><Play size={17} />{loading ? 'Enviando...' : 'Ejecutar ahora'}</button>
      </section>
      <section className="metrics-row">
        <MetricCard title="Estado sync" value={status?.oracle?.status?.syncStatus || '—'} subtitle="agente" icon={Activity} />
        <MetricCard title="Páginas" value={number(status?.oracle?.status?.lastSync?.pagesFetched)} subtitle="último job" icon={Layers3} tone="cyan" />
        <MetricCard title="Filas leídas" value={number(status?.oracle?.status?.lastSync?.rowsFetched)} subtitle="último job" icon={Database} tone="green" />
        <MetricCard title="Filas upsert" value={number(status?.oracle?.status?.lastSync?.rowsUpserted)} subtitle="Supabase" icon={CheckCircle2} tone="purple" />
      </section>
      <section className="panel wide">
        <div className="panel-head"><div><h3>Bitácora de sincronización</h3><p>Últimas ejecuciones registradas.</p></div></div>
        <DataTable rows={status?.syncRuns || []} max={12} />
      </section>
    </div>
  );
}

function OtpPage({ api, status, notify, refresh }) {
  const [otp, setOtp] = useState('');
  const [manualToken, setManualToken] = useState('');
  const [loading, setLoading] = useState('');
  const oracle = status?.oracle?.status || {};
  const startLogin = async () => {
    setLoading('start');
    try {
      const res = await api.request('/api/auth/start', { method: 'POST', body: '{}' });
      if (res.status === 'AUTH_OK') {
        notify('Sesión lista', res.message || 'WOW ya estaba autenticado. No hace falta OTP.', 'success');
      } else if (res.status === 'AWAITING_OTP') {
        notify('OTP requerido', res.message || 'Revisa el SMS e ingresa el código.', 'success');
      } else {
        notify('Login iniciado', res.message || res.status || 'Revisa el SMS/OTP.', 'success');
      }
      refresh();
    } catch (e) { notify('Error iniciando login', e.message, 'danger'); }
    finally { setLoading(''); }
  };
  const sendOtp = async () => {
    setLoading('otp');
    try {
      const res = await api.request('/api/auth/otp', { method: 'POST', body: JSON.stringify({ otp }) });
      notify('OTP enviado', `Estado: ${res.status || 'OK'}`, 'success');
      setOtp('');
      refresh();
    } catch (e) { notify('OTP rechazado', e.message, 'danger'); }
    finally { setLoading(''); }
  };
  const sendToken = async () => {
    setLoading('token');
    try {
      const res = await api.request('/api/token/manual', { method: 'POST', body: JSON.stringify({ token: manualToken }) });
      notify('Token actualizado', `Vence: ${safeDate(res.tokenExpiresAt)}`, 'success');
      setManualToken('');
      refresh();
    } catch (e) { notify('Error token manual', e.message, 'danger'); }
    finally { setLoading(''); }
  };
  const resetSession = async () => {
    setLoading('reset');
    try {
      const res = await api.request('/api/auth/reset', { method: 'POST', body: '{}' });
      notify('Sesión reiniciada', res.status || 'SESSION_RESET', 'success');
      setOtp('');
      setManualToken('');
      refresh();
    } catch (e) { notify('Error reseteando sesión', e.message, 'danger'); }
    finally { setLoading(''); }
  };
  return (
    <div className="page-grid two">
      <section className="panel">
        <div className="panel-head"><div><h3>Sesión WOW</h3><p>Controla el navegador persistente del agente.</p></div></div>
        <div className="status-list">
          <div><span>Auth</span><strong>{oracle.authStatus || '—'}</strong></div>
          <div><span>Token source</span><strong>{oracle.tokenSource || '—'}</strong></div>
          <div><span>Vencimiento</span><strong>{safeDate(oracle.tokenExpiresAt)}</strong></div>
          <div><span>Último error</span><strong>{oracle.lastError || 'Sin error'}</strong></div>
        </div>
        <button className="primary wide" onClick={startLogin} disabled={loading === 'start' || loading === 'reset'}><TimerReset size={17} />{loading === 'start' ? 'Iniciando...' : 'Iniciar / verificar login WOW'}</button>
        <button className="secondary wide" onClick={resetSession} disabled={loading === 'reset' || loading === 'start' || loading === 'otp'}><RefreshCw size={17} />{loading === 'reset' ? 'Reiniciando sesión...' : 'Cerrar / reiniciar sesión WOW'}</button>
      </section>
      <section className="panel otp-panel">
        <div className="panel-head"><div><h3>Cargar OTP</h3><p>El código se envía al navegador del agente, no al dispositivo actual.</p></div></div>
        <div className="otp-input-row">
          <input value={otp} onChange={(e) => setOtp(e.target.value.replace(/\D/g, '').slice(0, 6))} placeholder="000000" maxLength={6} />
          <button className="primary" onClick={sendOtp} disabled={otp.length !== 6 || loading === 'otp' || oracle.authStatus === 'AUTH_OK'}>Enviar</button>
        </div>
        <p className="muted">{oracle.authStatus === 'AUTH_OK' ? 'La sesión ya está autenticada. Solo usa OTP si WOW vuelve a pedirlo.' : 'Úsalo apenas llegue el SMS. Si vence, inicia login nuevamente.'}</p>
      </section>
      <section className="panel wide danger-zone">
        <div><h3>JWT manual de emergencia</h3><p>Solo para contingencia. La ruta normal es login + OTP + captura automática.</p></div>
        <textarea value={manualToken} onChange={(e) => setManualToken(e.target.value)} placeholder="eyJ0eXAiOiJKV1QiLC..." rows={4} />
        <button className="danger" onClick={sendToken} disabled={!manualToken || loading === 'token'}>Actualizar JWT manual</button>
      </section>
    </div>
  );
}

function ReportsPage({ api, notify }) {
  const initialCache = readInbReportCache(false);
  const [tab, setTab] = useState('resumen');
  const [productividadView, setProductividadView] = useState('asesor');
  const [dianaView, setDianaView] = useState('diana');
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState(initialCache.data || {});
  const [errors, setErrors] = useState(initialCache.errors || {});
  const [filters, setFilters] = useState({ reportDate: '', division: '', supervisor: '', tramo: '', q: '' });
  const [includeInactive, setIncludeInactive] = useState(false);
  const [availableDates, setAvailableDates] = useState([]);
  const dianaBoardRef = useRef(null);
  const loadSeqRef = useRef(0);
  const prefetchSeqRef = useRef(0);
  const lastRequestedDateRef = useRef('');
  const dataRef = useRef(initialCache.data || {});
  const errorsRef = useRef(initialCache.errors || {});
  const [columnPickerOpen, setColumnPickerOpen] = useState(false);
  const [visibleColumnsByTab, setVisibleColumnsByTab] = useState({});
  const [tableSortByTab, setTableSortByTab] = useState({});
  const [displayRowsByTab, setDisplayRowsByTab] = useState({});

  useEffect(() => { dataRef.current = data; }, [data]);
  useEffect(() => { errorsRef.current = errors; }, [errors]);

  const loadAvailableDates = async () => {
    const params = new URLSearchParams();
    if (includeInactive) params.set('includeInactive', '1');
    const query = params.toString();
    const res = await api.request(`/api/reports/inb/dates${query ? `?${query}` : ''}`);
    const dates = asArray(res.data || []);
    setAvailableDates(dates);
    return dates;
  };

  const fetchInbBundle = async (selectedDate, { forceRefresh = false } = {}) => {
    const params = new URLSearchParams();
    if (forceRefresh) {
      params.set('refresh', '1');
      params.set('ts', String(Date.now()));
    }
    if (includeInactive) params.set('includeInactive', '1');
    if (selectedDate) params.set('date', selectedDate);
    const query = params.toString();
    const res = await api.request(`/api/reports/inb/all${query ? `?${query}` : ''}`);
    return {
      data: { reportDate: selectedDate, ...(res.data || {}) },
      errors: res.errors || {},
    };
  };

  const applyInbBundle = (selectedDate, payload = {}, bundleErrors = {}) => {
    const nextData = { reportDate: selectedDate, ...normalizeInbBundle(payload || {}) };
    const nextErrors = bundleErrors || {};
    lastRequestedDateRef.current = selectedDate || '';
    dataRef.current = nextData;
    errorsRef.current = nextErrors;
    setData(nextData);
    setErrors(nextErrors);
    if (selectedDate && filters.reportDate !== selectedDate) {
      setFilters((current) => ({ ...current, reportDate: selectedDate }));
    }
    writeInbBundleCache(includeInactive, selectedDate, { data: nextData, errors: nextErrors }, true);
    writeInbReportCache(includeInactive, { data: nextData, errors: nextErrors });
  };

  const loadInbReports = async (forceRefresh = false) => {
    const loadSeq = ++loadSeqRef.current;
    setLoading(true);
    try {
      const dates = await loadAvailableDates();
      const selectedDate = filters.reportDate || dates[0] || '';
      lastRequestedDateRef.current = selectedDate || '';
      if (!forceRefresh && selectedDate && isInbBundleCacheFresh(includeInactive, selectedDate)) {
        const cached = readInbBundleCache(includeInactive, selectedDate);
        if (cached?.data) {
          applyInbBundle(selectedDate, cached.data, cached.errors || {});
          return;
        }
      }
      const bundle = await fetchInbBundle(selectedDate, { forceRefresh });
      if (loadSeq !== loadSeqRef.current) return;
      applyInbBundle(selectedDate, bundle.data, bundle.errors);
    } catch (e) {
      if (loadSeq !== loadSeqRef.current) return;
      const hasVisibleData = Boolean(dataRef.current?.reportDate) && Object.keys(dataRef.current || {}).some((key) => key !== 'reportDate' && Array.isArray(dataRef.current[key]) && dataRef.current[key].length);
      if (!hasVisibleData || forceRefresh) {
        notify?.('Error reportería INB', e.message, 'danger');
      }
    } finally {
      setLoading(false);
    }
  };

  const loadReportKeys = async (keys, { silent = false } = {}) => {
    const wantedKeys = [...new Set(asArray(keys).filter(Boolean))];
    if (!wantedKeys.length) return;
    const loadSeq = ++loadSeqRef.current;
    if (!silent) setLoading(true);
    try {
      const dates = availableDates.length ? availableDates : await loadAvailableDates();
      const selectedDate = filters.reportDate || dates[0] || '';
      const cached = selectedDate ? readInbBundleCache(includeInactive, selectedDate) : null;
      const cachedKeys = cached?.data || dataRef.current || {};
      const cachedErrors = cached?.errors || {};
      const hasAllKeys = wantedKeys.every((key) => key in cachedKeys);
      if (cached?.data && hasAllKeys && !wantedKeys.some((key) => key in cachedErrors)) {
        if (loadSeq === loadSeqRef.current) {
          applyInbBundle(selectedDate, cached.data, cachedErrors);
        }
        return;
      }
      const bundle = await fetchInbBundle(selectedDate, { forceRefresh: false });
      if (loadSeq !== loadSeqRef.current) return;
      applyInbBundle(selectedDate, bundle.data, bundle.errors);
    } catch (e) {
      if (loadSeq !== loadSeqRef.current) return;
      const hasVisibleData = Boolean(dataRef.current?.reportDate) && Object.keys(dataRef.current || {}).some((key) => key !== 'reportDate' && Array.isArray(dataRef.current[key]) && dataRef.current[key].length);
      if (!hasVisibleData) {
        notify?.('Error reportería INB', e.message, 'danger');
      }
    } finally {
      if (!silent && loadSeq === loadSeqRef.current) setLoading(false);
    }
  };

  useEffect(() => {
    lastRequestedDateRef.current = '';
    const cache = readInbReportCache(includeInactive);
    if (cache?.data) {
      dataRef.current = cache.data;
      errorsRef.current = cache.errors || {};
      setData(cache.data);
      setErrors(cache.errors || {});
      if (!isInbReportCacheFresh(includeInactive)) {
        loadInbReports(false).catch(() => {});
      }
    } else {
      dataRef.current = {};
      errorsRef.current = {};
      setData({});
      setErrors({});
      loadInbReports(false).catch(() => {});
    }
    const refresh = () => loadInbReports(true);
    const onStorage = (event) => {
      if (event.key === 'wow_reports_refresh_at') refresh();
    };
    window.addEventListener('wow-reports-refresh', refresh);
    window.addEventListener('storage', onStorage);
    return () => {
      window.removeEventListener('wow-reports-refresh', refresh);
      window.removeEventListener('storage', onStorage);
    };
  }, [includeInactive]);

  useEffect(() => {
    const desiredDate = filters.reportDate || availableDates[0] || '';
    const loadedDate = data.reportDate || '';
    if (loading || !desiredDate || desiredDate === loadedDate) return;
    if (lastRequestedDateRef.current === desiredDate) return;
    loadInbReports(false).catch(() => {});
  }, [availableDates, data.reportDate, filters.reportDate, loading]);

  useEffect(() => {
    const desiredDate = filters.reportDate || availableDates[0] || '';
    if (!desiredDate || data.reportDate !== desiredDate) return;
    const neededKeys = inbKeysForTab(tab);
    const missingKeys = neededKeys.filter((key) => !(key in data) && !(key in (errors || {})));
    if (!missingKeys.length) return;
    loadReportKeys(missingKeys, { silent: false }).catch(() => {});
  }, [availableDates, data, errors, filters.reportDate, tab]);

  const allRowsForFilters = useMemo(() => [
    ...(data.productividad || []),
    ...(data.timeout || []),
    ...(data.conclusiones || []),
    ...(data.validadores || []),
    ...(data.estados || []),
    ...(data.alertas || []),
  ], [data]);

  const activeRowsForFilters = useMemo(() => {
    if (tab === 'resumen') return (data.productividad || []).length ? (data.productividad || []) : allRowsForFilters;
    if (tab === 'productividad') return data.productividad || [];
    if (tab === 'diana') return data.productividad || [];
    if (tab === 'conclusiones') return data.conclusiones || [];
    if (tab === 'timeout') return data.timeout || [];
    if (tab === 'validadores') return [...(data.validadores || []), ...(data.validadoresConclusiones || [])];
    if (tab === 'estados') return data.estados || [];
    if (tab === 'alertas') return data.alertas || [];
    return allRowsForFilters;
  }, [allRowsForFilters, data, tab]);

  const reportDateSourceRows = useMemo(() => {
    if ((data.productividad || []).length) return data.productividad || [];
    if ((data.divisiones || []).length) return data.divisiones || [];
    return activeRowsForFilters;
  }, [activeRowsForFilters, data.divisiones, data.productividad]);

  const filterOptions = useMemo(() => ({
    dates: availableDates.length ? availableDates : uniq(reportDateSourceRows.map((r) => isoDateOnly(reportDateOf(r)))),
    divisions: uniq(activeRowsForFilters.map((r) => divisionOf(r))),
    supervisors: uniq(activeRowsForFilters.map((r) => supervisorOf(r))),
    tramos: uniq(activeRowsForFilters.map((r) => tramoOf(r))),
  }), [activeRowsForFilters, availableDates, reportDateSourceRows]);

  const effectiveReportDate = useMemo(() => {
    if (!filters.reportDate) return filterOptions.dates[0] || '';
    if (filterOptions.dates.includes(filters.reportDate)) return filters.reportDate;
    return filterOptions.dates[0] || '';
  }, [filterOptions.dates, filters.reportDate]);

  const fallbackReportDate = filters.reportDate && effectiveReportDate && filters.reportDate !== effectiveReportDate
    ? effectiveReportDate
    : '';

  const filterRows = (rows) => asArray(rows).filter((row) => {
    if (!isReportVisibleRole(roleOf(row))) return false;
    if (effectiveReportDate && isoDateOnly(reportDateOf(row)) !== effectiveReportDate) return false;
    if (filters.division && String(divisionOf(row) || '') !== filters.division) return false;
    if (filters.supervisor && String(supervisorOf(row) || '') !== filters.supervisor) return false;
    if (filters.tramo && String(tramoOf(row) || '') !== filters.tramo) return false;
    if (filters.q) {
      const q = filters.q.toLowerCase();
      const haystack = [
        firstValue(row, ['Asesor', 'asesor', 'asesor_nombre', 'nombre_agente_raw']),
        firstValue(row, ['Supervisor', 'supervisor', 'validador_supervisor']),
        firstValue(row, ['Equipo', 'equipo']),
        firstValue(row, ['Validador', 'validador_nombre']),
        firstValue(row, ['Conclusión', 'conclusion_raw']),
        firstValue(row, ['Detalle', 'alerta_detalle']),
        firstValue(row, ['Tipo alerta', 'tipo_alerta']),
        firstValue(row, ['estado_principal', 'estado_secundario']),
        row.nombre_dotacion,
        row.nombre_raw,
        row.agent_id_genesys,
      ].join(' ').toLowerCase();
      if (!haystack.includes(q)) return false;
    }
    return true;
  });

  const productividad = filterRows(data.productividad || []);
  const productividadSupervisor = useMemo(() => buildProductividadSupervisorRows(productividad), [productividad]);
  const productividadRows = productividadView === 'supervisor' ? productividadSupervisor : productividad;
  const conclusiones = filterRows(data.conclusiones || []);
  const timeout = filterRows(data.timeout || []);
  const validadores = filterRows(data.validadores || []);
  const validadoresConclusiones = filterRows(data.validadoresConclusiones || []);
  const productividadLookup = useMemo(() => {
    const byAgent = new Map();
    const byName = new Map();
    for (const row of productividad) {
      const payload = {
        llamadasRecibidas: numValue(firstValue(row, ['Manejo', 'llamadas_manejadas', 'llamadas_contestadas', 'Contestadas', 'llamadas_manejadas_interacciones', 'Interacciones', 'interacciones', 'interacciones_manejadas'], 0)),
      };
      const agentId = String(firstValue(row, ['agent_id_genesys', 'Agent ID Genesys', 'ID del agente'], '') || '').trim();
      if (agentId) byAgent.set(agentId, payload);
      const keys = [
        firstValue(row, ['Asesor', 'asesor_nombre', 'asesor', 'nombre_dotacion', 'nombre_genesys', 'nombre_wow', 'nombre_agente_raw'], ''),
      ]
        .flatMap((value) => {
          const exact = normalizeLookupKey(value);
          const loose = normalizeLookupKey(String(value || '').split(' ').filter(Boolean).sort().join(' '));
          return [exact, loose].filter(Boolean);
        });
      for (const key of keys) byName.set(key, payload);
    }
    return { byAgent, byName };
  }, [productividad]);

  const estados = useMemo(() => filterRows(data.estados || []).map((row) => {
    const agentId = String(firstValue(row, ['agent_id_genesys', 'Agent ID Genesys', 'ID del agente'], '') || '').trim();
    const rawName = firstValue(row, ['Asesor', 'asesor', 'asesor_nombre', 'nombre_agente_raw', 'nombre_dotacion'], '');
    const exactKey = normalizeLookupKey(rawName);
    const looseKey = normalizeLookupKey(String(rawName || '').split(' ').filter(Boolean).sort().join(' '));
    const extra = (agentId && productividadLookup.byAgent.get(agentId))
      || productividadLookup.byName.get(exactKey)
      || productividadLookup.byName.get(looseKey);
    const stateSeconds = (timeKeys, segKeys = []) => {
      const explicitSeg = numValue(firstValue(row, segKeys, NaN));
      if (Number.isFinite(explicitSeg) && explicitSeg > 0) return explicitSeg;
      const explicitTime = stateDurationToSeconds(firstValue(row, timeKeys, null));
      if (explicitTime !== null) return explicitTime;
      return 0;
    };
    const totalSeconds = stateSeconds(['t_total', 'T Total'], ['seg_total']);
    const descSeconds = stateSeconds(['t_desc', 'T Desc'], ['seg_desc']);
    const activeSeconds = Math.max(0, totalSeconds - descSeconds);
    const statePct = (keys, secondsKeys) => {
      const explicit = pctNumber(firstValue(row, keys, null));
      if (explicit !== null && explicit > 0) return explicit;
      if (secondsKeys === null) return explicit ?? 0;
      const seconds = stateSeconds(secondsKeys, secondsKeys.map((key) => key.replace(/^t_/, 'seg_').replace(/^T /, 'seg_')));
      if (!activeSeconds) return 0;
      return Number(((seconds / activeSeconds) * 100).toFixed(2));
    };
    const segDisp = stateSeconds(['t_disp', 'T Disp'], ['seg_disp']);
    const segCola = stateSeconds(['t_cola', 'T Cola'], ['seg_cola']);
    const segComida = stateSeconds(['t_comida', 'T Comida'], ['seg_comida']);
    const segCap = stateSeconds(['t_cap', 't_capa', 'T Capa'], ['seg_cap', 'seg_capa']);
    const segOcup = stateSeconds(['t_ocup', 't_ocupado', 'T Ocupado'], ['seg_ocup', 'seg_ocupado']);
    return {
      ...row,
      llamadas_recibidas: extra?.llamadasRecibidas ?? 0,
      seg_total: totalSeconds,
      t_total: secondsToHms(totalSeconds),
      seg_disp: segDisp,
      t_disp: secondsToHms(segDisp),
      pct_disp: statePct(['pct_disp', 'disp_pct'], ['t_disp', 'T Disp', 'seg_disp']),
      seg_cola: segCola,
      t_cola: secondsToHms(segCola),
      pct_cola: statePct(['pct_cola', 'cola_pct'], ['t_cola', 'T Cola', 'seg_cola']),
      seg_comida: segComida,
      t_comida: secondsToHms(segComida),
      pct_comida: statePct(['pct_comida', 'comida_pct'], ['t_comida', 'T Comida', 'seg_comida']),
      q_capa: numValue(firstValue(row, ['q_capa', 'q_cap'], 0)) || numValue(firstValue(row, ['q_cap'], 0)),
      seg_cap: segCap,
      seg_capa: segCap,
      t_cap: secondsToHms(segCap),
      t_capa: secondsToHms(segCap),
      pct_cap: statePct(['pct_cap', 'pct_capa', 'cap_pct'], ['t_cap', 't_capa', 'T Capa', 'seg_cap', 'seg_capa']),
      pct_capa: statePct(['pct_cap', 'pct_capa', 'cap_pct'], ['t_cap', 't_capa', 'T Capa', 'seg_cap', 'seg_capa']),
      q_ocupado: numValue(firstValue(row, ['q_ocupado', 'q_ocup'], 0)) || numValue(firstValue(row, ['q_ocup'], 0)),
      seg_ocup: segOcup,
      seg_ocupado: segOcup,
      t_ocup: secondsToHms(segOcup),
      t_ocupado: secondsToHms(segOcup),
      pct_ocup: statePct(['pct_ocup', 'pct_ocupado', 'ocup_pct'], ['t_ocup', 't_ocupado', 'T Ocupado', 'seg_ocup', 'seg_ocupado']),
      pct_ocupado: statePct(['pct_ocup', 'pct_ocupado', 'ocup_pct'], ['t_ocup', 't_ocupado', 'T Ocupado', 'seg_ocup', 'seg_ocupado']),
      seg_desc: descSeconds,
      t_desc: secondsToHms(descSeconds),
      pct_desc: totalSeconds ? Number(((descSeconds / totalSeconds) * 100).toFixed(2)) : 0,
    };
  }), [data.estados, productividadLookup, effectiveReportDate, filters.division, filters.supervisor, filters.tramo, filters.q]);

  const alertas = useMemo(() => filterRows(data.alertas || []).filter((row) => isAllowedOperationalAlert(row)), [data.alertas, effectiveReportDate, filters.division, filters.supervisor, filters.tramo, filters.q]);
  const dianaSource = useMemo(() => filterRows(data.productividad || []), [data.productividad, effectiveReportDate, filters.division, filters.supervisor, filters.tramo, filters.q]);
  const dianaRows = useMemo(() => buildDianaRows(dianaSource), [dianaSource]);
  const dianaTableRows = useMemo(() => dianaRows.map((row) => ({
    Diana: row.diana,
    Q: row.q || '',
    Asesor: row.asesor,
    Antigüedad: row.antiguedad,
    Turno: row.turno,
    Supervisor: row.supervisor,
    Llamadas: row.llamadas,
    Retenidas: row.retenidas,
    RetencionPct: row.retencionPct,
  })), [dianaRows]);
  const resumenHoras = useMemo(() => asArray(data.resumenHoras || []), [data.resumenHoras]);

  const summaryRows = productividad;
  const llamadas = sumRows(summaryRows, ['Manejo', 'llamadas_manejadas', 'llamadas_manejadas_interacciones']);
  const solicitudes = sumRows(summaryRows, ['Solicitudes', 'solicitudes_wow']);
  const retenidas = sumRows(summaryRows, ['Retenidas', 'retenidas', 'retenidas_wow']);
  const ejecutadas = sumRows(summaryRows, ['Ejecutadas', 'ejecutadas', 'ejecutadas_wow']);
  const pendientes = sumRows(summaryRows, ['Pendientes', 'pendientes', 'pendientes_wow']);
  const timeoutQ = sumRows(summaryRows, ['Timeout Q', 'timeout_q']);
  const retencionPct = pctValue(retenidas, llamadas);
  const timeoutPct = pctValue(timeoutQ, llamadas);
  const hasCutoffGap = retencionPct > 100;

  const tabs = [
    { id: 'resumen', label: 'Resumen INB' },
    { id: 'productividad', label: 'Productividad por asesor' },
    { id: 'diana', label: 'Diana para compartir' },
    { id: 'conclusiones', label: 'Conclusiones' },
    { id: 'timeout', label: 'Timeout' },
    { id: 'validadores', label: 'Validadores' },
    { id: 'estados', label: 'Estados' },
    { id: 'alertas', label: 'Alertas' },
  ];

  const productividadColumns = [
    { key: 'Asesor', label: 'Asesor', alt: ['asesor_nombre', 'asesor', 'nombre_agente_raw'] },
    { key: 'Antigüedad', label: 'Antigüedad', alt: ['antiguedad'] },
    { key: 'Q', label: 'Q', alt: ['Q', 'q', 'nro_rrhh', 'nro', 'N°'], style: 'q-level' },
    { key: 'Supervisor', label: 'Supervisor', alt: ['supervisor'] },
    { key: 'Manejo', label: 'Manejo', alt: ['llamadas_manejadas', 'llamadas_manejadas_interacciones'] },
    { key: 'Interacciones', label: 'Interacciones', alt: ['interacciones_manejadas', 'interacciones'] },
    { key: 'Solicitudes', label: 'Solicitudes', alt: ['Solicitudes WOW', 'solicitudes_wow'] },
    { key: 'Retenidas', label: 'Retenidas', alt: ['retenidos', 'Retenidos', 'retenidas', 'retenidas_wow'] },
    { key: 'Ejecutadas', label: 'Ejecutadas', alt: ['ejecutadas', 'ejecutadas_wow'] },
    { key: 'Pendientes', label: 'Pendientes', alt: ['pendientes', 'pendientes_wow'] },
    { key: 'Timeout Q', label: 'Timeout Q', alt: ['timeout_q'] },
    { key: '% Solic/Manejo', label: '% Solic/Manejo', alt: ['pct_solicitudes_sobre_llamadas', 'pct_solicitud_manejo'], style: 'solicitudes' },
    { key: '% Pend/Manejo', label: '% Pend/Manejo', alt: ['pct_pendientes_sobre_llamadas', 'pct_pendiente_manejo'] },
    { key: '% Retención', label: '% Retención', alt: ['Retención INB %', '% Retención INB', 'pct_retencion', 'pct_retencion_sobre_llamadas'], style: 'retencion' },
    { key: '% Timeout', label: '% Timeout', alt: ['timeout_pct', 'timeout_pct_interacciones'] },
    { key: 'Manejo medio', label: 'TMO', alt: ['manejo_medio', 'manejo_medio_min'], format: 'hmsMinutes', style: 'tmo' },
    { key: 'Conv media', label: 'Conv media', alt: ['conv_media', 'conv_media_min'], format: 'hmsMinutes' },
    { key: 'ACW', label: 'ACW', alt: ['acw', 'acw_media_min'], format: 'hmsMinutes', style: 'acw' },
  ];

  const productividadTotals = useMemo(() => {
    const manejoTotal = sumRows(productividadRows, ['Manejo', 'llamadas_manejadas', 'llamadas_manejadas_interacciones']);
    const interaccionesTotal = sumRows(productividadRows, ['Interacciones', 'interacciones_manejadas', 'interacciones']);
    const solicitudesTotal = sumRows(productividadRows, ['Solicitudes', 'Solicitudes WOW', 'solicitudes_wow']);
    const retenidasTotal = sumRows(productividadRows, ['Retenidas', 'retenidos', 'Retenidos', 'retenidas', 'retenidas_wow']);
    const ejecutadasTotal = sumRows(productividadRows, ['Ejecutadas', 'ejecutadas', 'ejecutadas_wow']);
    const pendientesTotal = sumRows(productividadRows, ['Pendientes', 'pendientes', 'pendientes_wow']);
    const timeoutTotal = sumRows(productividadRows, ['Timeout Q', 'timeout_q']);

    return {
      Asesor: 'TOTAL / PROMEDIO',
      Q: '—',
      'Antigüedad': '—',
      Supervisor: '—',
      Manejo: manejoTotal,
      Interacciones: interaccionesTotal,
      Solicitudes: solicitudesTotal,
      Retenidas: retenidasTotal,
      Ejecutadas: ejecutadasTotal,
      Pendientes: pendientesTotal,
      'Timeout Q': timeoutTotal,
      '% Solic/Manejo': pctValue(solicitudesTotal, manejoTotal),
      '% Pend/Manejo': pctValue(pendientesTotal, manejoTotal),
      '% Retención': pctValue(retenidasTotal, manejoTotal),
      '% Timeout': pctValue(timeoutTotal, interaccionesTotal || manejoTotal),
      'Manejo medio': productividadView === 'supervisor'
        ? weightedAverageDurationHms(productividadRows, ['Manejo medio', 'manejo_medio', 'manejo_medio_min'], ['Manejo'])
        : avgDurationHms(productividadRows, ['Manejo medio', 'manejo_medio', 'manejo_medio_min']),
      'Conv media': productividadView === 'supervisor'
        ? weightedAverageDurationHms(productividadRows, ['Conv media', 'conv_media', 'conv_media_min'], ['Interacciones', 'Manejo'])
        : avgDurationHms(productividadRows, ['Conv media', 'conv_media', 'conv_media_min']),
      ACW: productividadView === 'supervisor'
        ? weightedAverageDurationHms(productividadRows, ['ACW', 'acw', 'acw_media_min'], ['Interacciones', 'Manejo'])
        : avgDurationHms(productividadRows, ['ACW', 'acw', 'acw_media_min']),
    };
  }, [productividadRows, productividadView]);

  const resumenHorasColumns = [
    { key: 'hora', label: 'HORA' },
    { key: 'pendiente', label: 'Pendiente' },
    { key: 'retenido', label: 'Retenido' },
    { key: 'total_general', label: 'Total general' },
    { key: 'pct_rete', label: '% RETE', style: 'retencion' },
  ];

  const resumenHorasTotals = useMemo(() => {
    const pendienteTotal = sumRows(resumenHoras, ['pendiente']);
    const retenidoTotal = sumRows(resumenHoras, ['retenido']);
    const totalGeneral = sumRows(resumenHoras, ['total_general']);
    return {
      hora: 'TOTAL',
      pendiente: pendienteTotal,
      retenido: retenidoTotal,
      total_general: totalGeneral,
      pct_rete: pctValue(retenidoTotal, totalGeneral),
    };
  }, [resumenHoras]);
  const resumenSolicitudes = numValue(firstValue(resumenHorasTotals, ['total_general'], 0)) || solicitudes;
  const resumenRetenidas = numValue(firstValue(resumenHorasTotals, ['retenido'], 0)) || retenidas;
  const resumenRetencionPct = pctValue(resumenRetenidas, llamadas);

  const estadosColumns = [
    { key: 'Asesor', label: 'Asesor', alt: ['asesor', 'asesor_nombre', 'nombre_agente_raw'], locked: true },
    { key: 'Supervisor', label: 'Supervisor', alt: ['supervisor'], locked: true },
    { key: 'Llamadas', label: 'Llamadas', alt: ['llamadas_recibidas'], group: 'total' },
    { key: 'Q Total', label: 'Q Total', alt: ['q_total', 'total_q'], group: 'total' },
    { key: 'T Total', label: 'T Total', sortKey: 'seg_total', alt: ['t_total'], group: 'total' },
    { key: 'Q Disp', label: 'Q Disp', alt: ['q_disp', 'disp_q'], alertKey: 'Alerta disp', alertAlt: ['alerta_disponible'], group: 'disp', style: 'disp-alert' },
    { key: 'T Disp', label: 'T Disp', alt: ['t_disp', 'disp_t'], sortKey: 'seg_disp', sortAlt: ['t_disp', 'disp_t'], alertKey: 'Alerta disp', alertAlt: ['alerta_disponible'], group: 'disp', style: 'disp-alert' },
    { key: '% Disp', label: '% Disp', alt: ['pct_disp', 'disp_pct'], alertKey: 'Alerta disp', alertAlt: ['alerta_disponible'], group: 'disp', style: 'disp-alert' },
    { key: 'Q Cola', label: 'Q Cola', alt: ['q_cola', 'cola_q'], group: 'cola' },
    { key: 'T Cola', label: 'T Cola', alt: ['t_cola', 'cola_t'], sortKey: 'seg_cola', sortAlt: ['t_cola', 'cola_t'], group: 'cola' },
    { key: '% Cola', label: '% Cola', alt: ['pct_cola', 'cola_pct'], group: 'cola' },
    { key: 'Q Comida', label: 'Q Comida', alt: ['q_comida', 'comida_q'], alertKey: 'Alerta comida', alertAlt: ['alerta_comida'], group: 'comida', style: 'comida-alert' },
    { key: 'T Comida', label: 'T Comida', alt: ['t_comida', 'comida_t'], sortKey: 'seg_comida', sortAlt: ['t_comida', 'comida_t'], alertKey: 'Alerta comida', alertAlt: ['alerta_comida'], group: 'comida', style: 'comida-alert' },
    { key: '% Comida', label: '% Comida', alt: ['pct_comida', 'comida_pct'], alertKey: 'Alerta comida', alertAlt: ['alerta_comida'], group: 'comida', style: 'comida-alert' },
    { key: 'Q Capa', label: 'Q Capa', alt: ['q_capa', 'q_cap', 'cap_q'], alertKey: 'Alerta capa', alertAlt: ['alerta_cap'], group: 'capa' },
    { key: 'T Capa', label: 'T Capa', alt: ['t_capa', 't_cap', 'cap_t'], sortKey: 'seg_capa', sortAlt: ['seg_cap', 't_capa', 't_cap', 'cap_t'], alertKey: 'Alerta capa', alertAlt: ['alerta_cap'], group: 'capa' },
    { key: '% Capa', label: '% Capa', alt: ['pct_capa', 'pct_cap', 'cap_pct'], alertKey: 'Alerta capa', alertAlt: ['alerta_cap'], group: 'capa' },
    { key: 'Q Ocupado', label: 'Q Ocupado', alt: ['q_ocupado', 'q_ocup', 'ocup_q'], group: 'ocupado' },
    { key: 'T Ocupado', label: 'T Ocupado', alt: ['t_ocupado', 't_ocup', 'ocup_t'], sortKey: 'seg_ocupado', sortAlt: ['seg_ocup', 't_ocupado', 't_ocup', 'ocup_t'], group: 'ocupado', style: 'ocupado-alert' },
    { key: '% Ocupado', label: '% Ocupado', alt: ['pct_ocupado', 'pct_ocup', 'ocup_pct'], group: 'ocupado' },
    { key: 'Q Desc', label: 'Q Desc', alt: ['q_desc', 'desc_q'], alertKey: 'Alerta desc', alertAlt: ['alerta_desconectado'], group: 'desc' },
    { key: 'T Desc', label: 'T Desc', alt: ['t_desc', 'desc_t'], sortKey: 'seg_desc', sortAlt: ['t_desc', 'desc_t'], alertKey: 'Alerta desc', alertAlt: ['alerta_desconectado'], group: 'desc' },
    { key: '% Desc', label: '% Desc', alt: ['pct_desc', 'desc_pct'], alertKey: 'Alerta desc', alertAlt: ['alerta_desconectado'], group: 'desc' },
  ];

  const alertasColumns = [
    { key: 'Asesor', label: 'Asesor', alt: ['asesor', 'asesor_nombre', 'nombre_agente_raw'] },
    { key: 'Supervisor', label: 'Supervisor', alt: ['supervisor'] },
    { key: 'Equipo', label: 'Equipo', alt: ['equipo'] },
    { key: 'Tipo alerta', label: 'Tipo alerta', alt: ['tipo_alerta'] },
    { key: 'Detalle', label: 'Detalle', alt: ['alerta_detalle'] },
    { key: 'Q Alertas', label: 'Q Alertas', alt: ['q_alertas'] },
    { key: 'T Alertas', label: 'T Alertas', alt: ['tiempo_total_alertas_hms'], sortKey: 'Min Alertas', sortAlt: ['tiempo_total_alertas_min', 'tiempo_total_alertas_hms', 'T Alertas'] },
    { key: 'T Prom', label: 'T Prom', alt: ['tiempo_promedio_alerta_hms'], sortKey: 'Min Prom', sortAlt: ['tiempo_promedio_alerta_min', 'tiempo_promedio_alerta_hms', 'T Prom'], style: 'avg-alert' },
    { key: 'Primera pos', label: 'Primera pos', alt: ['primera_posicion'] },
    { key: 'Última pos', label: 'Última pos', alt: ['ultima_posicion'] },
  ];

  const tableByTab = {
    productividad: {
      rows: productividadRows,
      csv: productividadView === 'supervisor' ? 'productividad-supervisor-inb.csv' : 'productividad-inb.csv',
      columns: productividadColumns,
      summaryRow: productividadTotals,
      title: productividadView === 'supervisor' ? 'Productividad por supervisor' : 'Productividad por asesor',
    },
    diana: {
      rows: dianaTableRows,
      csv: dianaView === 'q' ? 'diana-q-inb.csv' : 'diana-inb.csv',
      columns: dianaView === 'q'
        ? ['Q', 'Asesor', 'Antigüedad', 'Turno', 'Supervisor', 'Llamadas', 'Retenidas', 'RetencionPct']
        : ['Diana', 'Asesor', 'Antigüedad', 'Turno', 'Supervisor', 'Llamadas', 'Retenidas', 'RetencionPct'],
    },
    conclusiones: {
      rows: conclusiones,
      csv: 'conclusiones-inb.csv',
      columns: [
        { key: 'Asesor', label: 'Asesor', alt: ['asesor_nombre'] },
        { key: 'Conclusión', label: 'Conclusión', alt: ['conclusion_raw'] },
        { key: 'Q', label: 'Q', alt: ['q'] },
        { key: 'Participación %', label: '% Part', alt: ['participacion_pct'] },
      ],
    },
    timeout: {
      rows: timeout,
      csv: 'timeout-inb.csv',
      columns: [
        { key: 'Asesor', label: 'Asesor', alt: ['asesor'] },
        { key: 'Supervisor', label: 'Supervisor', alt: ['supervisor'] },
        { key: 'Equipo', label: 'Equipo', alt: ['equipo'] },
        { key: 'Interacciones', label: 'Interacciones', alt: ['interacciones'] },
        { key: 'Timeout Q', label: 'Timeout Q', alt: ['timeout_q'], style: 'timeout-q' },
        { key: '% Timeout', label: '% Timeout', alt: ['timeout_pct'] },
        { key: 'Puro Q', label: 'Puro Q', alt: ['timeout_puro_q'] },
        { key: 'Combinado Q', label: 'Combinado Q', alt: ['timeout_combinado_q'] },
      ],
    },
    validadores: {
      rows: validadores,
      csv: 'validadores.csv',
      columns: ['validador_nombre', 'validaciones_q', 'conversaciones_q', 'validaciones_con_timeout_q'],
    },
    estados: {
      rows: estados,
      csv: 'estados-asesor.csv',
      columns: estadosColumns,
    },
    alertas: {
      rows: alertas,
      csv: 'alertas-estados-asesor.csv',
      columns: alertasColumns,
    },
  };

  const currentTable = tableByTab[tab];
  const resumenImageTable = tab === 'resumen' ? {
    rows: resumenHoras,
    csv: 'resumen-rete-por-hora.csv',
    columns: resumenHorasColumns,
    summaryRow: resumenHorasTotals,
    title: 'Resumen INB por hora',
  } : null;
  const activeTableKey = tab === 'productividad' ? `productividad-${productividadView}` : tab;
  const currentSortState = tableSortByTab[activeTableKey] || null;
  const currentDisplayedRows = displayRowsByTab[activeTableKey] || currentTable?.rows || [];
  const estadoGroupOptions = useMemo(() => {
    if (tab !== 'estados') return [];
    return [
      { id: 'total', label: 'Total' },
      { id: 'disp', label: 'Disponible' },
      { id: 'cola', label: 'Cola' },
      { id: 'comida', label: 'Comida' },
      { id: 'capa', label: 'Capa' },
      { id: 'ocupado', label: 'Ocupado' },
      { id: 'desc', label: 'Desconectado' },
    ];
  }, [tab]);
  const selectedEstadoGroups = visibleColumnsByTab.estados;
  const activeEstadoGroups = selectedEstadoGroups ?? estadoGroupOptions.map((item) => item.id);
  const visibleCurrentColumns = useMemo(
    () => {
      const columns = currentTable?.columns || [];
      if (tab !== 'estados') return visibleColumnsForSelection(columns, visibleColumnsByTab[activeTableKey]);
      return columns.filter((column) => column.locked || activeEstadoGroups.includes(column.group));
    },
    [activeEstadoGroups, activeTableKey, currentTable, tab, visibleColumnsByTab],
  );

  const toggleVisibleColumn = (column) => {
    const id = columnId(column);
    const baseColumns = Array.isArray(currentTable?.columns) ? currentTable.columns : [];
    const allIds = baseColumns.map((item) => columnId(item));
    setVisibleColumnsByTab((current) => {
      const activeIds = current[activeTableKey]?.length ? current[activeTableKey] : allIds;
      const exists = activeIds.includes(id);
      const next = exists
        ? activeIds.filter((item) => item !== id)
        : [...activeIds, id];
      return {
        ...current,
        [activeTableKey]: next.length ? next : allIds,
      };
    });
  };

  const toggleEstadoGroup = (groupId) => {
    const allGroups = estadoGroupOptions.map((item) => item.id);
    setVisibleColumnsByTab((current) => {
      const activeGroups = current.estados ?? allGroups;
      const exists = activeGroups.includes(groupId);
      return {
        ...current,
        estados: exists
          ? activeGroups.filter((item) => item !== groupId)
          : [...activeGroups, groupId],
      };
    });
  };

  const resetVisibleColumns = () => {
    setVisibleColumnsByTab((current) => {
      const next = { ...current };
      delete next[activeTableKey];
      return next;
    });
  };
  const clearVisibleColumns = () => {
    if (tab === 'estados') {
      setVisibleColumnsByTab((current) => ({
        ...current,
        estados: [],
      }));
      return;
    }
    setVisibleColumnsByTab((current) => ({
      ...current,
      [activeTableKey]: [],
    }));
  };
  const exportDianaImage = async () => {
    try {
      await exportDianaAsPng(
        dianaRows,
        effectiveReportDate || fallbackReportDate || data.reportDate || '',
        `${dianaView === 'q' ? 'diana-q' : 'diana-inb'}-${effectiveReportDate || 'ultimo-corte'}.png`,
        { mode: dianaView },
      );
      notify?.('Imagen generada', 'La diana quedó exportada en PNG.', 'success');
    } catch (error) {
      notify?.('Error exportando imagen', error.message, 'danger');
    }
  };
  const exportCurrentTableImage = async () => {
    const exportTable = tab === 'resumen' ? resumenImageTable : currentTable;
    if (!exportTable) return;
    try {
      const reportName = exportTable.title || tabs.find((item) => item.id === tab)?.label || 'Reporte';
      const subtitle = effectiveReportDate
        ? `Fecha: ${effectiveReportDate} | ${number(exportTable.rows.length)} filas`
        : `${number(exportTable.rows.length)} filas`;
      await exportReportTableAsPng({
        title: reportName,
        subtitle,
        rows: tab === 'resumen' ? exportTable.rows : currentDisplayedRows,
        columns: tab === 'resumen' ? exportTable.columns : visibleCurrentColumns,
        summaryRow: exportTable.summaryRow || null,
        filename: `${exportTable.csv?.replace(/\\.csv$/i, '') || tab}-${effectiveReportDate || 'ultimo-corte'}.png`,
      });
      notify?.('Imagen generada', 'El reporte visible quedó exportado en PNG.', 'success');
    } catch (error) {
      notify?.('Error exportando imagen', error.message, 'danger');
    }
  };
  const topAsesores = productividad.slice(0, 15).map((row) => ({
    asesor: firstValue(row, ['Asesor', 'asesor_nombre', 'asesor'], '—'),
    retencion: numValue(firstValue(row, ['% Retención', 'pct_retencion_sobre_llamadas'], 0)),
    manejo: numValue(firstValue(row, ['Manejo', 'llamadas_manejadas'], 0)),
  }));

  return (
    <div className="page-grid">
      <section className="panel wide report-hero">
        <div className="panel-head">
          <div>
            <p className="eyebrow">Reportería INB</p>
            <h3>Retención, manejo, timeout, estados y validadores</h3>
            <p>KPI principal: <strong>Retención INB % = Retenidos WOW / Manejo</strong>. Si supera 100%, se muestra el valor real con alerta de desfase por corte.</p>
          </div>
          <div className="actions">
            <button className="secondary" onClick={() => loadInbReports(true)} disabled={loading}><RefreshCw size={16}/>{loading ? 'Cargando...' : 'Actualizar'}</button>
            {currentTable && <button className="secondary" onClick={() => downloadCsv(currentTable.csv, rowsForCsvExport(tab === 'diana' ? currentTable.rows : currentDisplayedRows, tab === 'diana' ? currentTable.columns : visibleCurrentColumns))}><Download size={16}/>CSV</button>}
            {tab !== 'resumen' && tab !== 'diana' && currentTable && <button className="secondary" onClick={() => setColumnPickerOpen((current) => !current)}><Columns3Cog size={16}/>Columnas</button>}
            {((tab !== 'diana' && currentTable) || (tab === 'resumen' && resumenHoras.length)) && <button className="secondary" onClick={exportCurrentTableImage}><ImageDown size={16}/>Imagen</button>}
            {tab === 'diana' && <button className="secondary" onClick={exportDianaImage}><Download size={16}/>Imagen</button>}
          </div>
        </div>
        {tab !== 'resumen' && errors?.[tab] && <div className="notice danger">Esta vista no respondió: {errors[tab]}</div>}
        <div className="report-filters">
          <label>Fecha<select value={effectiveReportDate} onChange={(e)=>setFilters({...filters, reportDate:e.target.value})}><option value="">Todas</option>{filterOptions.dates.map((d)=><option key={d}>{d}</option>)}</select></label>
          <label>Divisi?n<select value={filters.division} onChange={(e)=>setFilters({...filters, division:e.target.value})}><option value="">Todas</option>{filterOptions.divisions.map((d)=><option key={d}>{d}</option>)}</select></label>
          <label>Supervisor<select value={filters.supervisor} onChange={(e)=>setFilters({...filters, supervisor:e.target.value})}><option value="">Todos</option>{filterOptions.supervisors.map((s)=><option key={s}>{s}</option>)}</select></label>
          <label>Buscar<input value={filters.q} onChange={(e)=>setFilters({...filters, q:e.target.value})} placeholder="Asesor, conclusi?n, alerta..." /></label>
          <label className="check"><input type="checkbox" checked={includeInactive} onChange={(e)=>setIncludeInactive(e.target.checked)} /> Incluir inactivos</label>
        </div>
        {fallbackReportDate && <div className="notice">El corte {filters.reportDate} no estÃ¡ disponible en esta vista. Mostrando el Ãºltimo corte con datos: {fallbackReportDate}.</div>}
        {tab !== 'resumen' && tab !== 'diana' && currentTable && columnPickerOpen && (
          <div className="column-picker">
            <div className="column-picker-head">
              <div>
                <strong>Columnas visibles</strong>
                <span>{tab === 'estados' ? 'Selecciona un estado y aparecerán sus columnas Q, T y %.' : 'Oculta o muestra columnas sin alterar el resultado del reporte.'}</span>
              </div>
              <div className="column-picker-actions">
                <button className="secondary" onClick={clearVisibleColumns}>Deseleccionar todo</button>
                <button className="secondary" onClick={resetVisibleColumns}>Restablecer</button>
              </div>
            </div>
            <div className="column-picker-grid">
              {tab === 'estados'
                ? estadoGroupOptions.map((group) => {
                    const checked = selectedEstadoGroups === undefined || selectedEstadoGroups.includes(group.id);
                    return (
                      <label key={group.id} className="column-chip">
                        <input type="checkbox" checked={checked} onChange={() => toggleEstadoGroup(group.id)} />
                        <span>{group.label}</span>
                      </label>
                    );
                  })
                : currentTable.columns.map((column) => {
                    const id = columnId(column);
                    const selectedIds = visibleColumnsByTab[activeTableKey];
                    const checked = selectedIds === undefined || selectedIds.includes(id);
                    return (
                      <label key={id} className="column-chip">
                        <input type="checkbox" checked={checked} onChange={() => toggleVisibleColumn(column)} />
                        <span>{columnLabel(column)}</span>
                      </label>
                    );
                  })}
            </div>
          </div>
        )}
        <div className="tabs report-tabs">{tabs.map((item)=><button key={item.id} className={tab === item.id ? 'active' : ''} onClick={()=>setTab(item.id)}>{item.label}</button>)}</div>
      </section>

      {tab === 'resumen' && (
        <>
          <section className="metrics-row">
            <MetricCard title="Manejo" value={number(llamadas)} subtitle="llamadas manejadas" icon={Activity} tone="cyan" />
            <MetricCard title="Retenidos" value={number(resumenRetenidas)} subtitle="WOW estado retenido" icon={CheckCircle2} tone="green" />
            <MetricCard title="Retención INB %" value={fmtPct(resumenRetencionPct)} subtitle={hasCutoffGap ? 'Desfase por corte' : 'Retenidos / Manejo'} icon={Gauge} tone={hasCutoffGap ? 'amber' : 'purple'} />
            <MetricCard title="Timeout %" value={fmtPct(timeoutPct)} subtitle={`${number(timeoutQ)} timeout`} icon={AlertTriangle} tone={timeoutPct > 15 ? 'amber' : 'cyan'} />
          </section>
          <section className="metrics-row">
            <MetricCard title="Solicitudes WOW" value={number(resumenSolicitudes)} subtitle="creadas/homologadas" icon={Database} />
            <MetricCard title="Ejecutados" value={number(ejecutadas)} subtitle="WOW estado ejecutado" icon={Layers3} />
            <MetricCard title="Pendientes" value={number(pendientes)} subtitle="WOW estado pendiente" icon={TimerReset} tone="amber" />
            <MetricCard title="Validadores" value={number(validadores.length)} subtitle="personas con validaciones" icon={Users} tone="green" />
          </section>
          <section className="panel wide">
            <div className="panel-head"><div><h3>Retención por asesor</h3><p>Ranking según Retenidos / Manejo.</p></div></div>
            <div className="chart-box">
              <ResponsiveContainer width="100%" height={285}>
                <BarChart data={topAsesores}>
                  <CartesianGrid strokeDasharray="3 3" stroke="rgba(148,163,184,.25)" />
                  <XAxis dataKey="asesor" tick={{ fontSize: 10 }} interval={0} angle={-20} textAnchor="end" height={80} />
                  <YAxis tick={{ fontSize: 11 }} />
                  <Tooltip />
                  <Bar dataKey="retencion" name="Retención %" fill="#2563eb" radius={[8,8,0,0]} />
                </BarChart>
              </ResponsiveContainer>
            </div>
          </section>
          <section className="panel wide">
            <div className="panel-head"><div><h3>Resumen por hora</h3><p>Pendiente, retenido y efectividad del corte seleccionado.</p></div></div>
            <ReportTable rows={resumenHoras} columns={resumenHorasColumns} summaryRow={resumenHorasTotals} max={24} empty="Sin datos horarios para el corte seleccionado." />
          </section>
        </>
      )}

      {tab === 'diana' && (
        <section className="panel wide">
          <div className="panel-head">
            <div><h3>{dianaView === 'q' ? 'Vista por Q' : 'Diana para compartir'}</h3><p>{number(dianaRows.length)} asesores visibles en el corte.</p></div>
            <div className="segmented-inline">
              <button className={dianaView === 'diana' ? 'secondary active' : 'secondary'} onClick={() => setDianaView('diana')}>Por diana</button>
              <button className={dianaView === 'q' ? 'secondary active' : 'secondary'} onClick={() => setDianaView('q')}>Por Q</button>
            </div>
          </div>
          <DianaBoard rows={dianaRows} reportDate={effectiveReportDate || fallbackReportDate || data.reportDate || ''} boardRef={dianaBoardRef} mode={dianaView} />
        </section>
      )}

      {tab !== 'resumen' && tab !== 'diana' && currentTable && (
        <section className="panel wide">
          <div className="panel-head">
            <div>
              <h3>{currentTable.title || tabs.find((t)=>t.id === tab)?.label}</h3>
              <p>{number(currentTable.rows.length)} filas después de filtros.</p>
            </div>
            {tab === 'productividad' ? (
              <div className="segmented-inline">
                <button className={productividadView === 'asesor' ? 'secondary active' : 'secondary'} onClick={() => setProductividadView('asesor')}>Por asesor</button>
                <button className={productividadView === 'supervisor' ? 'secondary active' : 'secondary'} onClick={() => setProductividadView('supervisor')}>Por supervisor</button>
              </div>
            ) : null}
          </div>
          <ReportTable
            rows={currentTable.rows}
            columns={visibleCurrentColumns}
            max={180}
            summaryRow={currentTable.summaryRow}
            sortState={currentSortState}
            onSortChange={(nextSort) => setTableSortByTab((current) => ({ ...current, [activeTableKey]: nextSort }))}
            onDisplayRowsChange={(nextRows) => setDisplayRowsByTab((current) => ({ ...current, [activeTableKey]: nextRows }))}
          />
        </section>
      )}

      {tab === 'validadores' && (
        <section className="panel wide">
          <div className="panel-head"><div><h3>Conclusiones por validador</h3><p>Detalle de resultados donde aparece como segundo usuario.</p></div></div>
          <ReportTable rows={validadoresConclusiones} columns={['validador_nombre','conclusion_raw','q','participacion_pct']} max={120} />
        </section>
      )}
    </div>
  );
}

function ConsolidatedReportsPage({ api, notify }) {
  const [tab, setTab] = useState('resumen');
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState({});
  const [includeInactive, setIncludeInactive] = useState(false);
  const [availableDates, setAvailableDates] = useState([]);
  const [filters, setFilters] = useState({ from: '', to: '', division: '', supervisor: '', q: '' });
  const [tableSortByTab, setTableSortByTab] = useState({});
  const [displayRowsByTab, setDisplayRowsByTab] = useState({});
  const [dianaView, setDianaView] = useState('diana');
  const loadSeqRef = useRef(0);
  const dianaBoardRef = useRef(null);

  const setPreset = (mode, dates = availableDates) => {
    const ordered = [...asArray(dates)];
    if (!ordered.length) return;
    const latest = ordered[0];
    const oldest = ordered[ordered.length - 1];
    if (mode === 'today') {
      setFilters((current) => ({ ...current, from: latest, to: latest }));
      return;
    }
    if (mode === 'yesterday') {
      const day = ordered[1] || latest;
      setFilters((current) => ({ ...current, from: day, to: day }));
      return;
    }
    if (mode === 'last7') {
      const window = ordered.slice(0, 7).sort((a, b) => String(a).localeCompare(String(b)));
      setFilters((current) => ({ ...current, from: window[0], to: window[window.length - 1] }));
      return;
    }
    if (mode === 'month') {
      const prefix = String(latest || '').slice(0, 7);
      const monthDates = ordered.filter((date) => String(date).startsWith(prefix)).sort((a, b) => String(a).localeCompare(String(b)));
      setFilters((current) => ({ ...current, from: monthDates[0] || oldest, to: monthDates[monthDates.length - 1] || latest }));
      return;
    }
    setFilters((current) => ({ ...current, from: oldest, to: latest }));
  };

  const loadAvailableDates = async () => {
    const params = new URLSearchParams();
    if (includeInactive) params.set('includeInactive', '1');
    const res = await api.request(`/api/reports/inb-consolidado/dates${params.toString() ? `?${params.toString()}` : ''}`);
    const dates = asArray(res.data || []);
    setAvailableDates(dates);
    if (!filters.from || !filters.to) {
      setPreset('month', dates);
    }
    return dates;
  };

  const loadConsolidated = async () => {
    const loadSeq = ++loadSeqRef.current;
    const dates = availableDates.length ? availableDates : await loadAvailableDates();
    const ordered = [...dates];
    const from = filters.from || ordered[ordered.length - 1] || '';
    const to = filters.to || ordered[0] || '';
    if (!from || !to) return;
    setLoading(true);
    try {
      const params = new URLSearchParams({ from, to });
      if (includeInactive) params.set('includeInactive', '1');
      const res = await api.request(`/api/reports/inb-consolidado/range?${params.toString()}`);
      if (loadSeq !== loadSeqRef.current) return;
      setData(normalizeConsolidatedBundle(res.data));
    } catch (error) {
      if (loadSeq !== loadSeqRef.current) return;
      notify?.('Error consolidados', error.message, 'danger');
    } finally {
      if (loadSeq === loadSeqRef.current) setLoading(false);
    }
  };

  useEffect(() => {
    loadAvailableDates().catch(() => {});
  }, [includeInactive]);

  useEffect(() => {
    if (!filters.from || !filters.to) return;
    loadConsolidated().catch(() => {});
  }, [filters.from, filters.to, includeInactive]);

  const baseRows = useMemo(() => [
    ...(data.productividad || []),
    ...(data.timeout || []),
    ...(data.conclusiones || []),
    ...(data.validadores || []),
    ...(data.estados || []),
    ...(data.alertas || []),
  ], [data]);

  const filterOptions = useMemo(() => ({
    divisions: uniq(baseRows.map((row) => divisionOf(row))),
    supervisors: uniq(baseRows.map((row) => supervisorOf(row))),
  }), [baseRows]);

  const filterRows = (rows) => asArray(rows).filter((row) => {
    if (!isReportVisibleRole(roleOf(row))) return false;
    if (filters.division && String(divisionOf(row) || '') !== filters.division) return false;
    if (filters.supervisor && String(supervisorOf(row) || '') !== filters.supervisor) return false;
    if (filters.q) {
      const q = filters.q.toLowerCase();
      const haystack = [
        firstValue(row, ['Asesor', 'asesor', 'asesor_nombre', 'nombre_agente_raw']),
        firstValue(row, ['Supervisor', 'supervisor']),
        firstValue(row, ['Equipo', 'equipo']),
        firstValue(row, ['Validador', 'validador_nombre']),
        firstValue(row, ['Conclusión', 'conclusion_raw']),
        firstValue(row, ['Detalle', 'alerta_detalle']),
        firstValue(row, ['Tipo alerta', 'tipo_alerta']),
      ].join(' ').toLowerCase();
      if (!haystack.includes(q)) return false;
    }
    return true;
  });

  const productividad = filterRows(data.productividad || []);
  const timeout = filterRows(data.timeout || []);
  const conclusiones = filterRows(data.conclusiones || []);
  const validadores = filterRows(data.validadores || []);
  const validadoresConclusiones = filterRows(data.validadoresConclusiones || []);
  const productividadLookup = useMemo(() => {
    const byAgent = new Map();
    const byName = new Map();
    for (const row of productividad) {
      const payload = { llamadasRecibidas: numValue(firstValue(row, ['Manejo', 'llamadas_manejadas', 'Interacciones', 'interacciones_manejadas'], 0)) };
      const agentId = String(firstValue(row, ['agent_id_genesys', 'Agent ID Genesys'], '') || '').trim();
      if (agentId) byAgent.set(agentId, payload);
      const rawName = firstValue(row, ['Asesor', 'asesor_nombre', 'nombre_dotacion'], '');
      const exact = normalizeLookupKey(rawName);
      const loose = normalizeLookupKey(String(rawName || '').split(' ').filter(Boolean).sort().join(' '));
      if (exact) byName.set(exact, payload);
      if (loose) byName.set(loose, payload);
    }
    return { byAgent, byName };
  }, [productividad]);

  const estados = useMemo(() => filterRows(data.estados || []).map((row) => {
    const agentId = String(firstValue(row, ['agent_id_genesys', 'Agent ID Genesys'], '') || '').trim();
    const rawName = firstValue(row, ['Asesor', 'asesor', 'asesor_nombre', 'nombre_dotacion'], '');
    const exact = normalizeLookupKey(rawName);
    const loose = normalizeLookupKey(String(rawName || '').split(' ').filter(Boolean).sort().join(' '));
    const extra = (agentId && productividadLookup.byAgent.get(agentId))
      || productividadLookup.byName.get(exact)
      || productividadLookup.byName.get(loose);
    const stateSeconds = (timeKeys, segKeys = []) => {
      const explicitSeg = numValue(firstValue(row, segKeys, NaN));
      if (Number.isFinite(explicitSeg) && explicitSeg > 0) return explicitSeg;
      const explicitTime = stateDurationToSeconds(firstValue(row, timeKeys, null));
      if (explicitTime !== null) return explicitTime;
      return 0;
    };
    const totalSeconds = stateSeconds(['t_total', 'T Total'], ['seg_total']);
    const descSeconds = stateSeconds(['t_desc', 'T Desc'], ['seg_desc']);
    const activeSeconds = Math.max(0, totalSeconds - descSeconds);
    const pctFromSeconds = (timeKeys) => {
      const seconds = stateSeconds(timeKeys, timeKeys.map((key) => key.replace(/^t_/, 'seg_').replace(/^T /, 'seg_')));
      return activeSeconds ? Number(((seconds / activeSeconds) * 100).toFixed(2)) : 0;
    };
    const segDisp = stateSeconds(['t_disp', 'T Disp'], ['seg_disp']);
    const segCola = stateSeconds(['t_cola', 'T Cola'], ['seg_cola']);
    const segComida = stateSeconds(['t_comida', 'T Comida'], ['seg_comida']);
    const segCap = stateSeconds(['t_cap', 'T Capa', 't_capa'], ['seg_cap', 'seg_capa']);
    const segOcup = stateSeconds(['t_ocup', 'T Ocupado', 't_ocupado'], ['seg_ocup', 'seg_ocupado']);
    return {
      ...row,
      llamadas_recibidas: extra?.llamadasRecibidas ?? 0,
      seg_total: totalSeconds,
      t_total: secondsToHms(totalSeconds),
      seg_disp: segDisp,
      t_disp: secondsToHms(segDisp),
      pct_disp: pctFromSeconds(['t_disp', 'T Disp', 'seg_disp']),
      seg_cola: segCola,
      t_cola: secondsToHms(segCola),
      pct_cola: pctFromSeconds(['t_cola', 'T Cola', 'seg_cola']),
      seg_comida: segComida,
      t_comida: secondsToHms(segComida),
      pct_comida: pctFromSeconds(['t_comida', 'T Comida', 'seg_comida']),
      q_capa: numValue(firstValue(row, ['q_capa', 'q_cap'], 0)) || numValue(firstValue(row, ['q_cap'], 0)),
      seg_cap: segCap,
      seg_capa: segCap,
      t_cap: secondsToHms(segCap),
      t_capa: secondsToHms(segCap),
      pct_cap: pctFromSeconds(['t_cap', 'T Capa', 'seg_cap', 'seg_capa']),
      pct_capa: pctFromSeconds(['t_cap', 'T Capa', 'seg_cap', 'seg_capa']),
      q_ocupado: numValue(firstValue(row, ['q_ocupado', 'q_ocup'], 0)) || numValue(firstValue(row, ['q_ocup'], 0)),
      seg_ocup: segOcup,
      seg_ocupado: segOcup,
      t_ocup: secondsToHms(segOcup),
      t_ocupado: secondsToHms(segOcup),
      pct_ocup: pctFromSeconds(['t_ocup', 'T Ocupado', 'seg_ocup', 'seg_ocupado']),
      pct_ocupado: pctFromSeconds(['t_ocup', 'T Ocupado', 'seg_ocup', 'seg_ocupado']),
      seg_desc: descSeconds,
      t_desc: secondsToHms(descSeconds),
      pct_desc: totalSeconds ? Number(((descSeconds / totalSeconds) * 100).toFixed(2)) : 0,
    };
  }), [data.estados, productividadLookup, filters.division, filters.supervisor, filters.q]);

  const alertas = useMemo(() => filterRows(data.alertas || []).filter((row) => isAllowedOperationalAlert(row)), [data.alertas, filters.division, filters.supervisor, filters.q]);
  const dianaSource = useMemo(() => filterRows(data.productividad || []), [data.productividad, filters.division, filters.supervisor, filters.q]);
  const dianaRows = useMemo(() => buildDianaRows(dianaSource), [dianaSource]);
  const dianaTableRows = useMemo(() => dianaRows.map((row) => ({
    Diana: row.diana,
    Q: row.q || '',
    Asesor: row.asesor,
    Antigüedad: row.antiguedad,
    Turno: row.turno,
    Supervisor: row.supervisor,
    Llamadas: row.llamadas,
    Retenidas: row.retenidas,
    RetencionPct: row.retencionPct,
  })), [dianaRows]);
  const resumenHoras = useMemo(() => asArray(data.resumenHoras || []), [data.resumenHoras]);

  const llamadas = sumRows(productividad, ['Manejo', 'llamadas_manejadas', 'llamadas_manejadas_interacciones']);
  const solicitudes = sumRows(productividad, ['Solicitudes', 'Solicitudes WOW', 'solicitudes_wow']);
  const retenidas = sumRows(productividad, ['Retenidas', 'retenidas', 'retenidas_wow']);
  const ejecutadas = sumRows(productividad, ['Ejecutadas', 'ejecutadas', 'ejecutadas_wow']);
  const pendientes = sumRows(productividad, ['Pendientes', 'pendientes', 'pendientes_wow']);
  const timeoutQ = sumRows(productividad, ['Timeout Q', 'timeout_q']);
  const retencionPct = pctValue(retenidas, llamadas);
  const timeoutPct = pctValue(timeoutQ, llamadas);

  const resumenHorasColumns = [
    { key: 'hora', label: 'HORA' },
    { key: 'pendiente', label: 'Pendiente' },
    { key: 'retenido', label: 'Retenido' },
    { key: 'total_general', label: 'Total general' },
    { key: 'pct_rete', label: '% RETE', style: 'retencion' },
  ];
  const resumenHorasTotals = useMemo(() => ({
    hora: 'TOTAL',
    pendiente: sumRows(resumenHoras, ['pendiente']),
    retenido: sumRows(resumenHoras, ['retenido']),
    total_general: sumRows(resumenHoras, ['total_general']),
    pct_rete: pctValue(sumRows(resumenHoras, ['retenido']), sumRows(resumenHoras, ['total_general'])),
  }), [resumenHoras]);

  const productividadColumns = [
    { key: 'Asesor', label: 'Asesor', alt: ['asesor_nombre'] },
    { key: 'Antigüedad', label: 'Antigüedad', alt: ['antiguedad'] },
    { key: 'Q', label: 'Q', alt: ['Q', 'q', 'nro_rrhh', 'nro', 'N°'], style: 'q-level' },
    { key: 'Supervisor', label: 'Supervisor', alt: ['supervisor'] },
    { key: 'Manejo', label: 'Manejo', alt: ['llamadas_manejadas'] },
    { key: 'Interacciones', label: 'Interacciones', alt: ['interacciones_manejadas', 'interacciones'] },
    { key: 'Solicitudes', label: 'Solicitudes', alt: ['Solicitudes WOW', 'solicitudes_wow'] },
    { key: 'Retenidas', label: 'Retenidas', alt: ['retenidas', 'retenidas_wow'] },
    { key: 'Ejecutadas', label: 'Ejecutadas', alt: ['ejecutadas', 'ejecutadas_wow'] },
    { key: 'Pendientes', label: 'Pendientes', alt: ['pendientes', 'pendientes_wow'] },
    { key: 'Timeout Q', label: 'Timeout Q', alt: ['timeout_q'] },
    { key: '% Solic/Manejo', label: '% Solic/Manejo', alt: ['pct_solicitudes_sobre_llamadas'], style: 'solicitudes' },
    { key: '% Retención', label: '% Retención', alt: ['pct_retencion_sobre_llamadas'], style: 'retencion' },
    { key: '% Timeout', label: '% Timeout', alt: ['timeout_pct'] },
  ];
  const timeoutColumns = [
    { key: 'Asesor', label: 'Asesor', alt: ['asesor_nombre'] },
    { key: 'Supervisor', label: 'Supervisor', alt: ['supervisor'] },
    { key: 'Interacciones', label: 'Interacciones', alt: ['interacciones'] },
    { key: 'Timeout Q', label: 'Timeout Q', alt: ['timeout_q'], style: 'timeout-q' },
    { key: '% Timeout', label: '% Timeout', alt: ['timeout_pct'] },
    { key: 'Puro Q', label: 'Puro Q', alt: ['timeout_puro_q'] },
    { key: 'Combinado Q', label: 'Combinado Q', alt: ['timeout_combinado_q'] },
  ];
  const conclusionColumns = [
    { key: 'Asesor', label: 'Asesor', alt: ['asesor_nombre'] },
    { key: 'Conclusión', label: 'Conclusión', alt: ['conclusion_raw'] },
    { key: 'Q', label: 'Q', alt: ['q'] },
    { key: 'Participación %', label: '% Part', alt: ['participacion_pct'] },
  ];
  const validadoresColumns = [
    { key: 'validador_nombre', label: 'Validador' },
    { key: 'validaciones_q', label: 'Validaciones' },
    { key: 'conversaciones_q', label: 'Conversaciones' },
    { key: 'validaciones_con_timeout_q', label: 'Val. timeout' },
  ];
  const estadosColumns = [
    { key: 'Asesor', label: 'Asesor', alt: ['asesor_nombre'] },
    { key: 'Supervisor', label: 'Supervisor', alt: ['supervisor'] },
    { key: 'Llamadas', label: 'Llamadas', alt: ['llamadas_recibidas'] },
    { key: 'Q Total', label: 'Q Total', alt: ['q_total', 'total_q'] },
    { key: 'T Total', label: 'T Total', alt: ['t_total'], sortKey: 'seg_total', sortAlt: ['t_total'] },
    { key: 'Q Disp', label: 'Q Disp', alt: ['q_disp'] },
    { key: 'T Disp', label: 'T Disp', alt: ['t_disp'], sortKey: 'seg_disp', sortAlt: ['t_disp'], style: 'disp-alert' },
    { key: '% Disp', label: '% Disp', alt: ['pct_disp'], style: 'disp-alert' },
    { key: 'Q Ocupado', label: 'Q Ocupado', alt: ['q_ocup', 'q_ocupado'] },
    { key: 'T Ocupado', label: 'T Ocupado', alt: ['t_ocup', 't_ocupado'], sortKey: 'seg_ocup', sortAlt: ['t_ocup', 't_ocupado'], style: 'ocupado-alert' },
    { key: '% Ocupado', label: '% Ocupado', alt: ['pct_ocup', 'pct_ocupado'] },
    { key: 'Q Desc', label: 'Q Desc', alt: ['q_desc'] },
    { key: 'T Desc', label: 'T Desc', alt: ['t_desc'], sortKey: 'seg_desc', sortAlt: ['t_desc'] },
  ];
  const alertasColumns = [
    { key: 'Asesor', label: 'Asesor', alt: ['asesor_nombre'] },
    { key: 'Supervisor', label: 'Supervisor', alt: ['supervisor'] },
    { key: 'Tipo alerta', label: 'Tipo alerta', alt: ['tipo_alerta'] },
    { key: 'Detalle', label: 'Detalle', alt: ['alerta_detalle'] },
    { key: 'Q Alertas', label: 'Q Alertas', alt: ['q_alertas'] },
    { key: 'T Alertas', label: 'T Alertas', alt: ['tiempo_total_alertas_hms'], sortKey: 'Min Alertas', sortAlt: ['tiempo_total_alertas_min', 'tiempo_total_alertas_hms'] },
    { key: 'T Prom', label: 'T Prom', alt: ['tiempo_promedio_alerta_hms'], sortKey: 'Min Prom', sortAlt: ['tiempo_promedio_alerta_min', 'tiempo_promedio_alerta_hms'], style: 'avg-alert' },
  ];

  const tabs = [
    { id: 'resumen', label: 'Resumen consolidado' },
    { id: 'productividad', label: 'Productividad' },
    { id: 'diana', label: 'Diana y Q' },
    { id: 'timeout', label: 'Timeout' },
    { id: 'conclusiones', label: 'Conclusiones' },
    { id: 'validadores', label: 'Validadores' },
    { id: 'estados', label: 'Estados' },
    { id: 'alertas', label: 'Alertas' },
  ];

  const tableByTab = {
    productividad: { rows: productividad, csv: 'consolidado-productividad.csv', columns: productividadColumns },
    diana: {
      rows: dianaTableRows,
      csv: dianaView === 'q' ? 'consolidado-diana-q.csv' : 'consolidado-diana.csv',
      columns: dianaView === 'q'
        ? ['Q', 'Asesor', 'Antigüedad', 'Turno', 'Supervisor', 'Llamadas', 'Retenidas', 'RetencionPct']
        : ['Diana', 'Asesor', 'Antigüedad', 'Turno', 'Supervisor', 'Llamadas', 'Retenidas', 'RetencionPct'],
      title: dianaView === 'q' ? 'Consolidado por Q' : 'Consolidado por Diana',
    },
    timeout: { rows: timeout, csv: 'consolidado-timeout.csv', columns: timeoutColumns },
    conclusiones: { rows: conclusiones, csv: 'consolidado-conclusiones.csv', columns: conclusionColumns },
    validadores: { rows: validadores, csv: 'consolidado-validadores.csv', columns: validadoresColumns },
    estados: { rows: estados, csv: 'consolidado-estados.csv', columns: estadosColumns },
    alertas: { rows: alertas, csv: 'consolidado-alertas.csv', columns: alertasColumns },
  };
  const currentTable = tableByTab[tab];
  const currentSortState = tableSortByTab[tab] || null;
  const currentDisplayedRows = displayRowsByTab[tab] || currentTable?.rows || [];

  const exportCurrentTableImage = async () => {
    const exportTable = tab === 'resumen'
      ? { rows: resumenHoras, columns: resumenHorasColumns, csv: 'consolidado-resumen-hora.csv', summaryRow: resumenHorasTotals, title: 'Consolidado INB por hora' }
      : currentTable;
    if (!exportTable) return;
    await exportReportTableAsPng({
      title: exportTable.title || tabs.find((item) => item.id === tab)?.label || 'Consolidado',
      subtitle: `${filters.from || data.from || ''} al ${filters.to || data.to || ''} | ${number(exportTable.rows.length)} filas`,
      rows: tab === 'resumen' ? exportTable.rows : currentDisplayedRows,
      columns: exportTable.columns,
      summaryRow: exportTable.summaryRow || null,
      filename: `${exportTable.csv.replace(/\.csv$/i, '')}.png`,
    });
    notify?.('Imagen generada', 'El consolidado visible quedó exportado en PNG.', 'success');
  };

  const exportDianaImage = async () => {
    try {
      await exportDianaAsPng(
        dianaRows,
        `${filters.from || data.from || ''} al ${filters.to || data.to || ''}`,
        `${dianaView === 'q' ? 'consolidado-q' : 'consolidado-diana'}-${filters.from || data.from || 'inicio'}-${filters.to || data.to || 'fin'}.png`,
        { mode: dianaView },
      );
      notify?.('Imagen generada', 'La vista consolidada quedó exportada en PNG.', 'success');
    } catch (error) {
      notify?.('Error exportando imagen', error.message, 'danger');
    }
  };

  return (
    <div className="page-grid">
      <section className="panel wide report-hero">
        <div className="panel-head">
          <div>
            <p className="eyebrow">Consolidados INB</p>
            <h3>Rangos de fechas sin tocar la reportería diaria</h3>
            <p>Vista separada para semana, mes o rango personalizado.</p>
          </div>
          <div className="actions">
            {tab === 'diana'
              ? <button className="secondary" onClick={exportDianaImage}><ImageDown size={16}/>Imagen</button>
              : ((tab === 'resumen' ? resumenHoras.length : currentTable) ? <button className="secondary" onClick={exportCurrentTableImage}><ImageDown size={16}/>Imagen</button> : null)}
            {currentTable ? <button className="secondary" onClick={() => downloadCsv(currentTable.csv, rowsForCsvExport(currentDisplayedRows, currentTable.columns))}><Download size={16}/>CSV</button> : null}
            <button className="secondary" onClick={() => loadConsolidated()} disabled={loading}><RefreshCw size={16}/>{loading ? 'Cargando...' : 'Actualizar'}</button>
          </div>
        </div>
        <div className="report-filters">
          <label>Desde<input type="date" value={filters.from} onChange={(e) => setFilters((current) => ({ ...current, from: e.target.value }))} /></label>
          <label>Hasta<input type="date" value={filters.to} onChange={(e) => setFilters((current) => ({ ...current, to: e.target.value }))} /></label>
          <label>División<select value={filters.division} onChange={(e) => setFilters((current) => ({ ...current, division: e.target.value }))}><option value="">Todas</option>{filterOptions.divisions.map((value) => <option key={value}>{value}</option>)}</select></label>
          <label>Supervisor<select value={filters.supervisor} onChange={(e) => setFilters((current) => ({ ...current, supervisor: e.target.value }))}><option value="">Todos</option>{filterOptions.supervisors.map((value) => <option key={value}>{value}</option>)}</select></label>
          <label>Buscar<input value={filters.q} onChange={(e) => setFilters((current) => ({ ...current, q: e.target.value }))} placeholder="Asesor, conclusión, alerta..." /></label>
          <label className="check"><input type="checkbox" checked={includeInactive} onChange={(e) => setIncludeInactive(e.target.checked)} /> Incluir inactivos</label>
        </div>
        <div className="actions">
          <button className="secondary" onClick={() => setPreset('today')}>Hoy</button>
          <button className="secondary" onClick={() => setPreset('yesterday')}>Ayer</button>
          <button className="secondary" onClick={() => setPreset('last7')}>Últimos 7 días</button>
          <button className="secondary" onClick={() => setPreset('month')}>Mes actual</button>
          <button className="secondary" onClick={() => setPreset('all')}>Todo disponible</button>
        </div>
        <div className="tabs report-tabs">{tabs.map((item) => <button key={item.id} className={tab === item.id ? 'active' : ''} onClick={() => setTab(item.id)}>{item.label}</button>)}</div>
        {asArray(data.missingSourceDates || []).length ? (
          <div className="notice danger">
            Faltan fuentes Genesys para este rango:
            {' '}
            {asArray(data.missingSourceDates || []).map((item) => `${item.date} (${asArray(item.missing || []).join(', ')})`).join(' | ')}
          </div>
        ) : null}
      </section>

      {tab === 'resumen' && (
        <>
          <section className="metrics-row">
            <MetricCard title="Manejo" value={number(llamadas)} subtitle="llamadas acumuladas" icon={Activity} tone="cyan" />
            <MetricCard title="Retenidos" value={number(retenidas)} subtitle="WOW retenido acumulado" icon={CheckCircle2} tone="green" />
            <MetricCard title="Retención INB %" value={fmtPct(retencionPct)} subtitle="Retenidos / Manejo" icon={Gauge} tone="purple" />
            <MetricCard title="Timeout %" value={fmtPct(timeoutPct)} subtitle={`${number(timeoutQ)} timeout`} icon={AlertTriangle} tone={timeoutPct > 15 ? 'amber' : 'cyan'} />
          </section>
          <section className="metrics-row">
            <MetricCard title="Solicitudes WOW" value={number(solicitudes)} subtitle="acumuladas" icon={Database} />
            <MetricCard title="Ejecutados" value={number(ejecutadas)} subtitle="acumulados" icon={Layers3} />
            <MetricCard title="Pendientes" value={number(pendientes)} subtitle="acumulados" icon={TimerReset} tone="amber" />
            <MetricCard title="Validadores" value={number(validadores.length)} subtitle="personas con validaciones" icon={Users} tone="green" />
          </section>
          <section className="panel wide">
            <div className="panel-head"><div><h3>Resumen por hora</h3><p>{data.from || filters.from || '—'} al {data.to || filters.to || '—'}</p></div></div>
            <ReportTable rows={resumenHoras} columns={resumenHorasColumns} summaryRow={resumenHorasTotals} max={24} empty="Sin datos consolidados para el rango." />
          </section>
        </>
      )}

      {tab === 'diana' && (
        <section className="panel wide">
          <div className="panel-head">
            <div>
              <h3>{dianaView === 'q' ? 'Vista consolidada por Q' : 'Vista consolidada por Diana'}</h3>
              <p>{number(dianaRows.length)} asesores visibles en el rango seleccionado.</p>
            </div>
            <div className="segmented-inline">
              <button className={dianaView === 'diana' ? 'secondary active' : 'secondary'} onClick={() => setDianaView('diana')}>Por diana</button>
              <button className={dianaView === 'q' ? 'secondary active' : 'secondary'} onClick={() => setDianaView('q')}>Por Q</button>
            </div>
          </div>
          <DianaBoard rows={dianaRows} reportDate={`${filters.from || data.from || ''} al ${filters.to || data.to || ''}`} boardRef={dianaBoardRef} mode={dianaView} />
        </section>
      )}

      {tab !== 'resumen' && tab !== 'diana' && currentTable && (
        <section className="panel wide">
          <div className="panel-head"><div><h3>{tabs.find((item) => item.id === tab)?.label}</h3><p>{number(currentTable.rows.length)} filas consolidadas.</p></div></div>
          <ReportTable
            rows={currentTable.rows}
            columns={currentTable.columns}
            max={200}
            sortState={currentSortState}
            onSortChange={(nextSort) => setTableSortByTab((current) => ({ ...current, [tab]: nextSort }))}
            onDisplayRowsChange={(nextRows) => setDisplayRowsByTab((current) => ({ ...current, [tab]: nextRows }))}
          />
        </section>
      )}

      {tab === 'validadores' && (
        <section className="panel wide">
          <div className="panel-head"><div><h3>Conclusiones por validador</h3><p>Detalle consolidado del rango.</p></div></div>
          <ReportTable rows={validadoresConclusiones} columns={['validador_nombre', 'conclusion_raw', 'q', 'participacion_pct']} max={160} />
        </section>
      )}
    </div>
  );
}

async function readWorkbook(file) {
  const buffer = await file.arrayBuffer();
  const workbook = XLSX.read(buffer, { type: 'array' });
  const sheetName = workbook.SheetNames[0];
  const sheet = workbook.Sheets[sheetName];
  const rows = XLSX.utils.sheet_to_json(sheet, { defval: null });
  const columns = rows[0] ? Object.keys(rows[0]) : [];
  return { sheetName, rows, columns };
}

function UploadsPage({ api, notify }) {
  const [reports, setReports] = useState([]);
  const [fileStates, setFileStates] = useState([]);
  const [uploading, setUploading] = useState(false);
  const [cross, setCross] = useState({ leftReportId: '', rightReportId: '', leftKey: '', rightKey: '' });
  const [crossResult, setCrossResult] = useState(null);

  const load = async () => {
    const res = await api.request('/api/uploads/reports');
    setReports(res.data || []);
  };
  useEffect(() => { load().catch(() => {}); }, []);

  const onFiles = async (files) => {
    const parsedFiles = await Promise.all(
      Array.from(files || []).map(async (file) => {
        const parsed = await readWorkbook(file);
        return { file, name: file.name.replace(/\.[^.]+$/, ''), kind: 'generic', ...parsed };
      })
    );
    setFileStates(parsedFiles);
  };
  const upload = async () => {
    if (!fileStates.length) return;
    setUploading(true);
    try {
      let insertedTotal = 0;
      let replacedTotal = 0;
      for (const fileState of fileStates) {
        if (!fileState?.rows?.length) continue;
        const res = await api.request('/api/uploads/reports', {
          method: 'POST',
          body: JSON.stringify({
            name: fileState.name,
            kind: fileState.kind,
            replaceExisting: true,
            refreshReports: false,
            rows: fileState.rows,
            columns: fileState.columns,
            meta: { sheetName: fileState.sheetName, filename: fileState.file.name }
          })
        });
        insertedTotal += Number(res.inserted || 0);
        replacedTotal += Number(res.replaced || 0);
      }
      const replacedText = replacedTotal ? ` Reemplazó ${replacedTotal} corte(s) anterior(es).` : '';
      await api.request('/api/reports/inb/refresh-cache', { method: 'POST' }).catch(() => null);
      notify('Reportes cargados', `${insertedTotal} filas guardadas.${replacedText} Reportería actualizada.`, 'success');
      setFileStates([]);
      localStorage.setItem('wow_reports_refresh_at', String(Date.now()));
      window.dispatchEvent(new Event('wow-reports-refresh'));
      await load();
    } finally {
      setUploading(false);
    }
  };
  const previewCross = async () => {
    const res = await api.request('/api/crosses/preview', { method: 'POST', body: JSON.stringify(cross) });
    setCrossResult(res);
  };
  const allColumns = useMemo(() => {
    const selected = reports.find((r) => r.id === cross.leftReportId)?.columns || [];
    const selectedRight = reports.find((r) => r.id === cross.rightReportId)?.columns || [];
    return { left: selected, right: selectedRight };
  }, [reports, cross.leftReportId, cross.rightReportId]);

  return (
    <div className="page-grid two">
      <section className="panel">
        <div className="panel-head"><div><h3>Cargar reporte externo</h3><p>Excel/CSV. Si cargas el mismo corte, reemplaza el anterior y refresca reportería.</p></div></div>
        <label className="dropzone">
          <Upload size={28} />
          <strong>Seleccionar archivo(s)</strong>
          <span>xlsx, xls o csv</span>
          <input type="file" accept=".xlsx,.xls,.csv" multiple onChange={(e) => e.target.files?.length && onFiles(e.target.files)} />
        </label>
        <div data-sgc-manual-upload data-api-base={api?.apiBase || ''} className="sgc-manual-upload-slot"></div>
        {fileStates.length ? <div className="preview-card multi">{fileStates.map((fileState, index) => <div className="preview-item" key={`upload-preview-${index}-${fileState.file.name}`}><input value={fileState.name} onChange={(e) => setFileStates((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, name: e.target.value } : item))} /><p>{fileState.file.name}</p><p>{number(fileState.rows.length)} filas · {fileState.columns.length} columnas</p></div>)}<button className="primary" onClick={upload} disabled={uploading}>{uploading ? 'Guardando...' : `Guardar ${fileStates.length} reporte(s)`}</button></div> : null}
      </section>
      <section className="panel">
        <div className="panel-head"><div><h3>Reportes cargados</h3><p>Disponibles para cruces.</p></div><button className="secondary" onClick={load}><RefreshCw size={16}/></button></div>
        <DataTable rows={reports} max={8} />
      </section>
      <section className="panel wide">
        <div className="panel-head"><div><h3>Cruce rápido</h3><p>Une dos reportes por columnas equivalentes.</p></div></div>
        <div className="form-grid four">
          <select value={cross.leftReportId} onChange={(e) => setCross({ ...cross, leftReportId: e.target.value, leftKey: '' })}><option value="">Reporte A</option>{reports.map((r) => <option key={r.id} value={r.id}>{r.name}</option>)}</select>
          <select value={cross.leftKey} onChange={(e) => setCross({ ...cross, leftKey: e.target.value })}><option value="">Columna A</option>{allColumns.left.map((c) => <option key={c}>{c}</option>)}</select>
          <select value={cross.rightReportId} onChange={(e) => setCross({ ...cross, rightReportId: e.target.value, rightKey: '' })}><option value="">Reporte B</option>{reports.map((r) => <option key={r.id} value={r.id}>{r.name}</option>)}</select>
          <select value={cross.rightKey} onChange={(e) => setCross({ ...cross, rightKey: e.target.value })}><option value="">Columna B</option>{allColumns.right.map((c) => <option key={c}>{c}</option>)}</select>
        </div>
        <button className="primary" onClick={previewCross}>Generar preview</button>
        {crossResult && <><div className="notice ok">Coincidencias mostradas: {crossResult.matchesShown}</div><DataTable rows={crossResult.matches || []} max={10} /></>}
      </section>
    </div>
  );
}

function DotacionPage({ api, notify, user }) {
  const [rows, setRows] = useState([]);
  const [query, setQuery] = useState('');
  const [editing, setEditing] = useState(null);
  const [dotacionTab, setDotacionTab] = useState('dotacion');
  const [supervisores, setSupervisores] = useState([]);
  const [equipos, setEquipos] = useState([]);
  const [editingSupervisor, setEditingSupervisor] = useState(null);
  const [editingEquipo, setEditingEquipo] = useState(null);
  const canEdit = user?.role === 'admin';

  const normalizeDotacionForUi = (row = {}) => {
    const role = firstValue(row, ['rol_operativo', 'Rol', 'rol'], 'asesor');
    const validador = firstValue(row, ['es_validador', 'Validador', 'validador'], false);
    const activo = firstValue(row, ['activo', 'Activo'], true);
    const fechaIngreso = firstValue(row, ['fecha_ingreso', 'Fecha ingreso'], '');
    return {
      ...row,
      id: firstValue(row, ['id', 'dotacion_id'], null),
      agent_id_genesys: firstValue(row, ['agent_id_genesys', 'Agent ID Genesys'], ''),
      nombre_dotacion: firstValue(row, ['nombre_dotacion', 'Asesor', 'asesor', 'asesor_nombre', 'nombre_personal'], ''),
      nombre_genesys: firstValue(row, ['nombre_genesys', 'Nombre Genesys', 'nombre_agente_raw'], ''),
      nombre_wow: firstValue(row, ['nombre_wow', 'Nombre WOW'], ''),
      division: firstValue(row, ['division', 'División'], 'BG'),
      equipo: firstValue(row, ['equipo', 'Equipo'], ''),
      supervisor: firstValue(row, ['supervisor', 'Supervisor'], ''),
      supervisor_id: firstValue(row, ['supervisor_id'], ''),
      equipo_id: firstValue(row, ['equipo_id'], ''),
      rol_operativo: String(role || 'asesor').toLowerCase(),
      es_validador: boolValue(validador),
      activo: boolValue(activo) || String(activo).trim().toLowerCase() === 'sí',
      dotacion_periodo: firstValue(row, ['dotacion_periodo', 'periodo'], ''),
      nro_rrhh: firstValue(row, ['Q', 'q', 'nro_rrhh', 'nro', 'N°'], ''),
      estado_rrhh: firstValue(row, ['estado_rrhh', 'estado', 'Estado'], ''),
      programa: firstValue(row, ['programa', 'Programa'], ''),
      coordinador: firstValue(row, ['coordinador', 'Coordinador'], ''),
      dni: firstValue(row, ['dni', 'DNI'], ''),
      nombre_personal: firstValue(row, ['nombre_personal', 'Asesor', 'asesor'], ''),
      condicion: firstValue(row, ['condicion', 'Jornada'], ''),
      turno: firstValue(row, ['turno', 'Turno'], ''),
      modalidad: firstValue(row, ['modalidad', 'Modalidad'], ''),
      break_horario: firstValue(row, ['break_horario', 'Break'], ''),
      horario_lunes: firstValue(row, ['horario_lunes', 'lunes', 'Lunes'], ''),
      horario_martes: firstValue(row, ['horario_martes', 'martes', 'Martes'], ''),
      horario_miercoles: firstValue(row, ['horario_miercoles', 'miercoles', 'Miércoles'], ''),
      horario_jueves: firstValue(row, ['horario_jueves', 'jueves', 'Jueves'], ''),
      horario_viernes: firstValue(row, ['horario_viernes', 'viernes', 'Viernes'], ''),
      horario_sabado: firstValue(row, ['horario_sabado', 'sabado', 'Sábado'], ''),
      horario_domingo: firstValue(row, ['horario_domingo', 'domingo', 'Domingo'], ''),
      diana_abril: firstValue(row, ['diana_abril', 'Diana'], ''),
      antiguedad: computeAntiguedad(fechaIngreso) || firstValue(row, ['antiguedad', 'Antigüedad'], ''),
      id_wolkvox: firstValue(row, ['id_wolkvox', 'ID Wolkvox'], ''),
      sgc: firstValue(row, ['sgc', 'nombre_sgc', 'SGC', 'sgc_nombre'], ''),
      fecha_ingreso: fechaIngreso,
      celular: firstValue(row, ['celular', 'Celular'], ''),
      correo_personal: firstValue(row, ['correo_personal', 'correo', 'Correo'], ''),
    };
  };

  const loadMasters = async () => {
    const [supRes, eqRes] = await Promise.all([
      api.request('/api/supervisores').catch(() => ({ data: [] })),
      api.request('/api/equipos').catch(() => ({ data: [] })),
    ]);
    setSupervisores(asArray(supRes.data));
    setEquipos(asArray(eqRes.data));
  };

  const load = async () => {
    const res = await api.request(`/api/dotacion${query ? `?q=${encodeURIComponent(query)}` : ''}`);
    setRows(asArray(res.data).map(normalizeDotacionForUi));
  };

  useEffect(() => { loadMasters().then(load).catch(() => {}); }, []);

  const applySupervisor = (supervisorId, base = editing) => {
    const sup = supervisores.find((s) => s.id === supervisorId);
    return {
      ...base,
      supervisor_id: supervisorId || '',
      supervisor: sup?.nombre || '',
      division: sup?.division || base.division || 'BG',
    };
  };

  const applyEquipo = (equipoId, base = editing) => {
    const equipo = equipos.find((e) => e.id === equipoId);
    const sup = supervisores.find((s) => s.id === equipo?.supervisor_id);
    return {
      ...base,
      equipo_id: equipoId || '',
      equipo: equipo?.nombre || '',
      supervisor_id: equipo?.supervisor_id || base.supervisor_id || '',
      supervisor: sup?.nombre || equipo?.supervisor || base.supervisor || '',
      division: equipo?.division || base.division || 'BG',
    };
  };

  const save = async () => {
    try {
      const payload = {
        ...editing,
        nombre_sgc: editing.sgc || editing.nombre_sgc || '',
        es_validador: Boolean(editing.es_validador),
        activo: editing.activo !== false,
      };
      const method = payload.id ? 'PATCH' : 'POST';
      const path = payload.id ? `/api/dotacion/${payload.id}` : '/api/dotacion';
      await api.request(path, { method, body: JSON.stringify(payload) });
      await api.request('/api/reports/inb/refresh-cache', { method: 'POST' }).catch(() => null);
      notify('Dotación guardada', 'El registro fue actualizado.', 'success');
      setEditing(null);
      await loadMasters();
      await load();
      localStorage.setItem('wow_reports_refresh_at', String(Date.now()));
      window.dispatchEvent(new Event('wow-reports-refresh'));
    } catch (e) {
      notify('No se pudo guardar dotación', e.message, 'danger');
    }
  };

  const remove = async (id) => {
    if (!confirm('¿Eliminar este registro de report_dotacion?')) return;
    await api.request(`/api/dotacion/${id}`, { method: 'DELETE' });
    await api.request('/api/reports/inb/refresh-cache', { method: 'POST' }).catch(() => null);
    notify('Registro eliminado', 'Dotación actualizada.', 'success');
    localStorage.setItem('wow_reports_refresh_at', String(Date.now()));
    window.dispatchEvent(new Event('wow-reports-refresh'));
    load();
  };

  const upload = async (file) => {
    const parsed = await readWorkbook(file);
    const res = await api.request('/api/dotacion/bulk-upsert', { method: 'POST', body: JSON.stringify({ rows: parsed.rows }) });
    await api.request('/api/reports/inb/refresh-cache', { method: 'POST' }).catch(() => null);
    notify('Dotación cargada', `${res.upserted} registros procesados.`, 'success');
    await loadMasters();
    localStorage.setItem('wow_reports_refresh_at', String(Date.now()));
    window.dispatchEvent(new Event('wow-reports-refresh'));
    load();
  };

  const saveSupervisor = async () => {
    const payload = { ...editingSupervisor, activo: editingSupervisor.activo !== false };
    const path = payload.id ? `/api/supervisores/${payload.id}` : '/api/supervisores';
    await api.request(path, { method: payload.id ? 'PATCH' : 'POST', body: JSON.stringify(payload) });
    notify('Supervisor guardado', 'Cambios aplicados.', 'success');
    setEditingSupervisor(null);
    await loadMasters();
    await load();
  };

  const saveEquipo = async () => {
    const payload = { ...editingEquipo, activo: editingEquipo.activo !== false };
    const path = payload.id ? `/api/equipos/${payload.id}` : '/api/equipos';
    await api.request(path, { method: payload.id ? 'PATCH' : 'POST', body: JSON.stringify(payload) });
    notify('Equipo guardado', 'Cambios aplicados.', 'success');
    setEditingEquipo(null);
    await loadMasters();
    await load();
  };

  const disableSupervisor = async (id) => {
    if (!confirm('¿Inactivar este supervisor?')) return;
    await api.request(`/api/supervisores/${id}`, { method: 'DELETE' });
    notify('Supervisor inactivado', 'Lista actualizada.', 'success');
    loadMasters();
  };

  const disableEquipo = async (id) => {
    if (!confirm('¿Inactivar este equipo?')) return;
    await api.request(`/api/equipos/${id}`, { method: 'DELETE' });
    notify('Equipo inactivado', 'Lista actualizada.', 'success');
    loadMasters();
  };

  const roleTotals = useMemo(() => rows.reduce((acc, r) => {
    const role = normalizeRole(r.rol_operativo || 'sin rol');
    acc[role || 'sin rol'] = (acc[role || 'sin rol'] || 0) + 1;
    return acc;
  }, {}), [rows]);

  const activeSupervisores = supervisores.filter((s) => s.activo !== false);
  const activeEquipos = equipos.filter((e) => e.activo !== false);

  return (
    <div className="page-grid">
      <section className="metrics-row">
        <MetricCard title="Total dotación" value={number(rows.length)} subtitle="dotación actual" icon={Users} />
        <MetricCard title="Asesores" value={number(roleTotals.asesor)} subtitle="cuentan en INB" icon={Gauge} tone="cyan" />
        <MetricCard title="Validadores" value={number(roleTotals.validador)} subtitle="medidos aparte" icon={CheckCircle2} tone="green" />
        <MetricCard title="Activos" value={number(rows.filter((r)=>r.activo !== false).length)} subtitle="habilitados" icon={Shield} tone="purple" />
      </section>

      <section className="panel wide">
        <div className="panel-head">
          <div><h3>Administración de dotación</h3><p>Asesores, supervisores y equipos. Al asignar equipo se hereda el supervisor del equipo.</p></div>
          <div className="actions"><button className="secondary" onClick={()=>{loadMasters(); load();}}><RefreshCw size={16}/>Actualizar</button></div>
        </div>
        <div className="tabs compact">
          <button className={dotacionTab === 'dotacion' ? 'active' : ''} onClick={()=>setDotacionTab('dotacion')}>Dotación</button>
          <button className={dotacionTab === 'supervisores' ? 'active' : ''} onClick={()=>setDotacionTab('supervisores')}>Supervisores</button>
          <button className={dotacionTab === 'equipos' ? 'active' : ''} onClick={()=>setDotacionTab('equipos')}>Equipos</button>
        </div>
      </section>

      {dotacionTab === 'dotacion' && <section className="panel wide">
        <div className="panel-head">
          <div><h3>Dotación del equipo</h3><p>Fuente maestra para homologar Genesys, WOW, validadores y división. El ID de equipo queda guardado, pero aquí solo mostramos el nombre.</p></div>
          <div className="actions"><div className="search"><Search size={16}/><input placeholder="Buscar" value={query} onChange={(e)=>setQuery(e.target.value)} onKeyDown={(e)=> e.key === 'Enter' && load()} /></div><button className="secondary" onClick={load}>Buscar</button>{canEdit && <button className="primary" onClick={()=>setEditing({ division: 'BG', rol_operativo: 'asesor', es_validador: false, activo: true })}><Plus size={16}/>Agregar</button>}</div>
        </div>
        {canEdit && <label className="inline-upload"><Upload size={16}/> Cargar Excel de dotación <input type="file" accept=".xlsx,.xls,.csv" onChange={(e)=> e.target.files?.[0] && upload(e.target.files[0])} /></label>}
        <div className="table-wrap dotacion-table"><table><thead><tr><th>Agent ID Genesys</th><th>Nombre dotación</th><th>División</th><th>Supervisor</th><th>Rol</th><th>Validador</th><th>Activo</th><th></th></tr></thead><tbody>{rows.map((r)=><tr key={r.id || r.agent_id_genesys || r.nombre_dotacion}><td>{r.agent_id_genesys || '—'}</td><td><strong>{formatPersonName(r.nombre_dotacion)}</strong><small className="muted block">WOW: {formatPersonName(r.nombre_wow)} · Genesys: {formatPersonName(r.nombre_genesys)}</small></td><td>{r.division}</td><td>{formatPersonName(r.supervisor)}</td><td><span className="pill">{r.rol_operativo}</span></td><td>{r.es_validador ? 'Sí' : 'No'}</td><td>{r.activo !== false ? 'Sí' : 'No'}</td><td>{canEdit && <div className="row-actions"><button className="icon-btn" onClick={()=>setEditing(normalizeDotacionForUi(r))}>✎</button><button className="icon-btn danger-text" onClick={()=>r.id && remove(r.id)}><Trash2 size={15}/></button></div>}</td></tr>)}</tbody></table></div>
      </section>}

      {dotacionTab === 'supervisores' && <section className="panel wide">
        <div className="panel-head"><div><h3>Supervisores</h3><p>Desde aquí puedes crear, editar o inactivar supervisores.</p></div>{canEdit && <button className="primary" onClick={()=>setEditingSupervisor({ division: 'BG', activo: true })}><Plus size={16}/>Nuevo supervisor</button>}</div>
        <div className="table-wrap"><table><thead><tr><th>Supervisor</th><th>División</th><th>Personas activas</th><th>Asesores INB</th><th>Validadores</th><th>Activo</th><th></th></tr></thead><tbody>{supervisores.map((s)=><tr key={s.id || s.nombre}><td><strong>{s.nombre}</strong></td><td>{s.division}</td><td>{number(s.personas_activas)}</td><td>{number(s.asesores_inb)}</td><td>{number(s.validadores)}</td><td>{s.activo !== false ? 'Sí' : 'No'}</td><td>{canEdit && <div className="row-actions"><button className="icon-btn" onClick={()=>setEditingSupervisor({...s})}>✎</button><button className="icon-btn danger-text" onClick={()=>disableSupervisor(s.id)}><Trash2 size={15}/></button></div>}</td></tr>)}</tbody></table></div>
      </section>}

      {dotacionTab === 'equipos' && <section className="panel wide">
        <div className="panel-head"><div><h3>Equipos</h3><p>Cada equipo tiene un supervisor. Al asignar un asesor a un equipo, se actualiza su supervisor.</p></div>{canEdit && <button className="primary" onClick={()=>setEditingEquipo({ division: 'BG', activo: true })}><Plus size={16}/>Nuevo equipo</button>}</div>
        <div className="table-wrap"><table><thead><tr><th>Equipo</th><th>Supervisor</th><th>División</th><th>Personas activas</th><th>Asesores INB</th><th>Validadores</th><th>Activo</th><th></th></tr></thead><tbody>{equipos.map((e)=><tr key={e.id || e.nombre}><td><strong>{e.nombre}</strong></td><td>{e.supervisor || '—'}</td><td>{e.division}</td><td>{number(e.personas_activas)}</td><td>{number(e.asesores_inb)}</td><td>{number(e.validadores)}</td><td>{e.activo !== false ? 'Sí' : 'No'}</td><td>{canEdit && <div className="row-actions"><button className="icon-btn" onClick={()=>setEditingEquipo({...e})}>✎</button><button className="icon-btn danger-text" onClick={()=>disableEquipo(e.id)}><Trash2 size={15}/></button></div>}</td></tr>)}</tbody></table></div>
      </section>}

      {editing && <Modal title={editing.id ? 'Editar dotación' : 'Nueva dotación'} onClose={() => setEditing(null)}>
        <div className="form-grid two">
          <label>Agent ID Genesys<input value={editing.agent_id_genesys || ''} onChange={(e)=>setEditing({...editing,agent_id_genesys:e.target.value})}/></label>
          <label>Nombre dotación<input value={editing.nombre_dotacion || ''} onChange={(e)=>setEditing({...editing,nombre_dotacion:e.target.value})}/></label>
          <label>Nombre Genesys<input value={editing.nombre_genesys || ''} onChange={(e)=>setEditing({...editing,nombre_genesys:e.target.value})}/></label>
          <label>Nombre WOW<input value={editing.nombre_wow || ''} onChange={(e)=>setEditing({...editing,nombre_wow:e.target.value})}/></label>
          <label>División<input value={editing.division || 'BG'} onChange={(e)=>setEditing({...editing,division:e.target.value})}/></label>
          <label>Rol<select value={editing.rol_operativo || 'asesor'} onChange={(e)=>setEditing({...editing,rol_operativo:e.target.value, es_validador: e.target.value === 'validador' ? true : editing.es_validador})}><option value="asesor">Asesor</option><option value="validador">Validador</option><option value="otro">Otro</option><option value="td">TD</option><option value="supervisor">Supervisor</option><option value="admin">Admin</option></select></label>
          <label>Supervisor<select value={editing.supervisor_id || ''} onChange={(e)=>setEditing(applySupervisor(e.target.value))}><option value="">Sin supervisor</option>{activeSupervisores.map((s)=><option key={s.id} value={s.id}>{s.nombre}</option>)}</select></label>
          <label>Equipo<select value={editing.equipo_id || ''} onChange={(e)=>setEditing(applyEquipo(e.target.value))}><option value="">Sin equipo</option>{activeEquipos.map((e)=><option key={e.id} value={e.id}>{e.nombre} · {e.supervisor || 'sin supervisor'}</option>)}</select></label>
          <label>Q<select value={editing.nro_rrhh || ''} onChange={(e)=>setEditing({...editing,nro_rrhh:e.target.value})}><option value="">Selecciona Q</option><option value="Q1">Q1</option><option value="Q2">Q2</option><option value="Q3">Q3</option><option value="Q4">Q4</option></select></label>
          <label>Estado<input value={editing.estado_rrhh || ''} onChange={(e)=>setEditing({...editing,estado_rrhh:e.target.value})}/></label>
          <label>Programa<input value={editing.programa || ''} onChange={(e)=>setEditing({...editing,programa:e.target.value})}/></label>
          <label>Coordinador<input value={editing.coordinador || ''} onChange={(e)=>setEditing({...editing,coordinador:e.target.value})}/></label>
          <label>DNI<input value={editing.dni || ''} onChange={(e)=>setEditing({...editing,dni:e.target.value})}/></label>
          <label>Nombre personal<input value={editing.nombre_personal || ''} onChange={(e)=>setEditing({...editing,nombre_personal:e.target.value})}/></label>
          <label>Jornada<input value={editing.condicion || ''} onChange={(e)=>setEditing({...editing,condicion:e.target.value})}/></label>
          <label>Turno<input value={editing.turno || ''} onChange={(e)=>setEditing({...editing,turno:e.target.value})}/></label>
          <label>Modalidad<input value={editing.modalidad || ''} onChange={(e)=>setEditing({...editing,modalidad:e.target.value})}/></label>
          <label>Break<input value={editing.break_horario || ''} onChange={(e)=>setEditing({...editing,break_horario:e.target.value})}/></label>
          <label>Lunes<input value={editing.horario_lunes || ''} onChange={(e)=>setEditing({...editing,horario_lunes:e.target.value})}/></label>
          <label>Martes<input value={editing.horario_martes || ''} onChange={(e)=>setEditing({...editing,horario_martes:e.target.value})}/></label>
          <label>Miércoles<input value={editing.horario_miercoles || ''} onChange={(e)=>setEditing({...editing,horario_miercoles:e.target.value})}/></label>
          <label>Jueves<input value={editing.horario_jueves || ''} onChange={(e)=>setEditing({...editing,horario_jueves:e.target.value})}/></label>
          <label>Viernes<input value={editing.horario_viernes || ''} onChange={(e)=>setEditing({...editing,horario_viernes:e.target.value})}/></label>
          <label>Sábado<input value={editing.horario_sabado || ''} onChange={(e)=>setEditing({...editing,horario_sabado:e.target.value})}/></label>
          <label>Domingo<input value={editing.horario_domingo || ''} onChange={(e)=>setEditing({...editing,horario_domingo:e.target.value})}/></label>
          <label>Diana<input value={editing.diana_abril || ''} onChange={(e)=>setEditing({...editing,diana_abril:e.target.value})}/></label>
          <label>Antigüedad<input value={computeAntiguedad(editing.fecha_ingreso || '') || editing.antiguedad || ''} readOnly /></label>
          <label>ID Wolkvox<input value={editing.id_wolkvox || ''} onChange={(e)=>setEditing({...editing,id_wolkvox:e.target.value})}/></label>
          <label>SGC<input value={editing.sgc || ''} onChange={(e)=>setEditing({...editing,sgc:e.target.value})}/></label>
          <label>Fecha ingreso<input value={editing.fecha_ingreso || ''} onChange={(e)=>setEditing({...editing,fecha_ingreso:e.target.value})}/></label>
          <label>Celular<input value={editing.celular || ''} onChange={(e)=>setEditing({...editing,celular:e.target.value})}/></label>
          <label>Correo<input value={editing.correo_personal || ''} onChange={(e)=>setEditing({...editing,correo_personal:e.target.value})}/></label>
          <label className="check"><input type="checkbox" checked={Boolean(editing.es_validador)} onChange={(e)=>setEditing({...editing,es_validador:e.target.checked})}/> Es validador</label>
          <label className="check"><input type="checkbox" checked={editing.activo !== false} onChange={(e)=>setEditing({...editing,activo:e.target.checked})}/> Activo</label>
        </div>
        <div className="modal-actions"><button className="secondary" onClick={()=>setEditing(null)}>Cancelar</button><button className="primary" onClick={save}>Guardar</button></div>
      </Modal>}

      {editingSupervisor && <Modal title={editingSupervisor.id ? 'Editar supervisor' : 'Nuevo supervisor'} onClose={() => setEditingSupervisor(null)}>
        <div className="form-grid two">
          <label>Supervisor<input value={editingSupervisor.nombre || ''} onChange={(e)=>setEditingSupervisor({...editingSupervisor,nombre:e.target.value})}/></label>
          <label>División<input value={editingSupervisor.division || 'BG'} onChange={(e)=>setEditingSupervisor({...editingSupervisor,division:e.target.value})}/></label>
          <label className="check"><input type="checkbox" checked={editingSupervisor.activo !== false} onChange={(e)=>setEditingSupervisor({...editingSupervisor,activo:e.target.checked})}/> Activo</label>
        </div>
        <div className="modal-actions"><button className="secondary" onClick={()=>setEditingSupervisor(null)}>Cancelar</button><button className="primary" onClick={saveSupervisor}>Guardar</button></div>
      </Modal>}

      {editingEquipo && <Modal title={editingEquipo.id ? 'Editar equipo' : 'Nuevo equipo'} onClose={() => setEditingEquipo(null)}>
        <div className="form-grid two">
          <label>Equipo<input value={editingEquipo.nombre || ''} onChange={(e)=>setEditingEquipo({...editingEquipo,nombre:e.target.value})}/></label>
          <label>División<input value={editingEquipo.division || 'BG'} onChange={(e)=>setEditingEquipo({...editingEquipo,division:e.target.value})}/></label>
          <label>Supervisor<select value={editingEquipo.supervisor_id || ''} onChange={(e)=>setEditingEquipo({...editingEquipo,supervisor_id:e.target.value})}><option value="">Sin supervisor</option>{activeSupervisores.map((s)=><option key={s.id} value={s.id}>{s.nombre}</option>)}</select></label>
          <label className="check"><input type="checkbox" checked={editingEquipo.activo !== false} onChange={(e)=>setEditingEquipo({...editingEquipo,activo:e.target.checked})}/> Activo</label>
        </div>
        <div className="modal-actions"><button className="secondary" onClick={()=>setEditingEquipo(null)}>Cancelar</button><button className="primary" onClick={saveEquipo}>Guardar</button></div>
      </Modal>}
    </div>
  );
}

function UsersPage({ api, notify }) {
  const [users, setUsers] = useState([]);
  const [editing, setEditing] = useState(null);
  const load = async () => { const res = await api.request('/api/users'); setUsers(res.data || []); };
  useEffect(() => { load().catch(() => {}); }, []);
  const save = async () => {
    if (editing.id) await api.request(`/api/users/${editing.id}`, { method: 'PATCH', body: JSON.stringify(editing) });
    else await api.request('/api/users', { method: 'POST', body: JSON.stringify(editing) });
    notify('Usuario guardado', 'Cambios aplicados.', 'success'); setEditing(null); load();
  };
  const resetPassword = async (u) => {
    const password = prompt(`Nueva contraseña para ${u.email}`);
    if (!password) return;
    await api.request(`/api/users/${u.id}/reset-password`, { method: 'POST', body: JSON.stringify({ password }) });
    notify('Contraseña actualizada', u.email, 'success');
  };
  const deactivate = async (u) => {
    if (!confirm(`¿Desactivar ${u.email}?`)) return;
    await api.request(`/api/users/${u.id}`, { method: 'DELETE' });
    notify('Usuario desactivado', u.email, 'success'); load();
  };
  return (
    <div className="page-grid">
      <section className="panel wide">
        <div className="panel-head"><div><h3>Usuarios y roles</h3><p>Admin gestiona usuarios; usuarios ven reportes y cargan OTP/reportes.</p></div><button className="primary" onClick={()=>setEditing({ role: 'user', is_active: true })}><Plus size={16}/>Crear usuario</button></div>
        <div className="table-wrap"><table><thead><tr><th>Nombre</th><th>Email</th><th>Rol</th><th>Activo</th><th>Último login</th><th></th></tr></thead><tbody>{users.map((u)=><tr key={u.id}><td>{u.name}</td><td>{u.email}</td><td><span className={`role-chip ${u.role}`}>{u.role}</span></td><td>{u.is_active ? 'Sí' : 'No'}</td><td>{safeDate(u.last_login_at)}</td><td><div className="row-actions"><button className="secondary small" onClick={()=>setEditing(u)}>Editar</button><button className="secondary small" onClick={()=>resetPassword(u)}>Clave</button><button className="secondary small danger-text" onClick={()=>deactivate(u)}>Desactivar</button></div></td></tr>)}</tbody></table></div>
      </section>
      {editing && <Modal title={editing.id ? 'Editar usuario' : 'Crear usuario'} onClose={()=>setEditing(null)}><div className="form-grid two"><label>Nombre<input value={editing.name || ''} onChange={(e)=>setEditing({...editing,name:e.target.value})}/></label><label>Email<input disabled={!!editing.id} value={editing.email || ''} onChange={(e)=>setEditing({...editing,email:e.target.value})}/></label><label>Rol<select value={editing.role || 'user'} onChange={(e)=>setEditing({...editing,role:e.target.value})}><option value="user">Usuario</option><option value="admin">Admin</option></select></label>{!editing.id && <label>Contraseña<input type="password" value={editing.password || ''} onChange={(e)=>setEditing({...editing,password:e.target.value})}/></label>}<label className="check"><input type="checkbox" checked={editing.is_active !== false} onChange={(e)=>setEditing({...editing,is_active:e.target.checked})}/> Activo</label></div><div className="modal-actions"><button className="secondary" onClick={()=>setEditing(null)}>Cancelar</button><button className="primary" onClick={save}>Guardar</button></div></Modal>}
    </div>
  );
}

function SettingsPage({ api, setUser, notify }) {
  const [apiBase, setApiBaseLocal] = useState(api.apiBase);
  const save = () => { api.setApiBase(apiBase); notify('Configuración guardada', 'Worker API actualizado localmente.', 'success'); };
  return <div className="page-grid two"><section className="panel"><h3>Conexión</h3><label>Worker API URL<input value={apiBase} onChange={(e)=>setApiBaseLocal(e.target.value)} /></label><button className="primary" onClick={save}>Guardar URL</button></section><section className="panel"><h3>Sesión local</h3><p className="muted">El token se guarda en este navegador. Cierra sesión si usas un equipo compartido.</p><button className="danger" onClick={()=>{api.setToken(''); setUser(null);}}>Cerrar sesión</button></section></div>;
}

function Modal({ title, children, onClose }) {
  return <div className="modal-backdrop"><div className="modal"><div className="modal-head"><h3>{title}</h3><button className="icon-btn" onClick={onClose}><X size={18}/></button></div>{children}</div></div>;
}

function MainApp() {
  const api = useApi();
  const [user, setUser] = useState(null);
  const [page, setPage] = useState('dashboard');
  const [collapsed, setCollapsed] = useState(false);
  const [status, setStatus] = useState(null);
  const [reports, setReports] = useState({});
  const [toast, setToast] = useState(null);

  const notify = (title, message, type = 'info') => setToast({ title, message, type });

  const loadMe = async () => {
    if (!api.token) return;
    const res = await api.request('/api/me');
    setUser(res.user);
  };
  const refresh = async () => {
    const res = await api.request('/api/status');
    setStatus(res);
  };
  const loadReports = async () => {
    const [byDay, byHour, byStatus, byMotive, byDistrict, summary] = await Promise.all([
      api.request('/api/reports/by-day'),
      api.request('/api/reports/by-hour'),
      api.request('/api/reports/by-status'),
      api.request('/api/reports/by-motive'),
      api.request('/api/reports/by-district'),
      api.request('/api/reports/summary-month'),
    ]);
    setReports({ byDay: byDay.data || [], byHour: byHour.data || [], byStatus: byStatus.data || [], byMotive: byMotive.data || [], byDistrict: byDistrict.data || [], summary: summary.data });
  };
  const refreshAll = async () => {
    await Promise.allSettled([refresh(), loadReports()]);
  };

  useEffect(() => { loadMe().catch(() => api.setToken('')); }, []);
  useEffect(() => { if (user) { refresh().catch((e)=>notify('Error status', e.message, 'danger')); loadReports().catch(()=>{}); } }, [user]);
  useEffect(() => {
    if (!user) return;
    const onRefresh = () => {
      refreshAll().catch(() => {});
    };
    const onStorage = (event) => {
      if (event.key === 'wow_reports_refresh_at') onRefresh();
    };
    window.addEventListener('wow-reports-refresh', onRefresh);
    window.addEventListener('storage', onStorage);
    const id = setInterval(() => refreshAll().catch(()=>{}), 30000);
    return () => {
      window.removeEventListener('wow-reports-refresh', onRefresh);
      window.removeEventListener('storage', onStorage);
      clearInterval(id);
    };
  }, [user]);

  if (!user) return <><LoginScreen apiBase={api.apiBase} setApiBase={api.setApiBase} request={api.request} setToken={api.setToken} onLogin={setUser} /><Toast toast={toast} onClose={() => setToast(null)} /></>;

  const common = { api, status, reports, refresh, loadReports, notify, user };
  return (
    <>
      <AppShell user={user} setUser={setUser} api={api} page={page} setPage={setPage} collapsed={collapsed} setCollapsed={setCollapsed}>
        {page === 'dashboard' && <Dashboard {...common} />}
        {page === 'sync' && <SyncPage {...common} />}
        {page === 'otp' && <OtpPage {...common} />}
        {page === 'reports' && <ReportsPage {...common} />}
        {page === 'consolidated' && <ConsolidatedReportsPage {...common} />}
        {page === 'uploads' && <UploadsPage {...common} />}
        {page === 'dotacion' && <DotacionPage {...common} />}
        {page === 'users' && <UsersPage {...common} />}
        {page === 'settings' && <SettingsPage api={api} setUser={setUser} notify={notify} />}
      </AppShell>
      <Toast toast={toast} onClose={() => setToast(null)} />
    </>
  );
}

createRoot(document.getElementById('root')).render(<MainApp />);
