import { Icon } from "@iconify/react";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react";
import styled from "styled-components";
import {
    getAuditFilterOptions,
    getAuditLogs,
} from "../../../api/auditoria.service";
import { sanitizeCell } from "../../_standards";
import {
  fmtFechaCalendario,
  fmtFechaHora,
  hoyISO,
} from "../../../utils/fecha";

// ─── Mapas de presentación ───────────────────────────────────────────────────
const ACT = {
  CREATE: { c: "#10b981", i: "lucide:plus-circle", l: "Creación" },
  UPDATE: { c: "#3b82f6", i: "lucide:pencil", l: "Actualización" },
  DELETE: { c: "#ef4444", i: "lucide:trash-2", l: "Eliminación" },
  APPROVE: { c: "#10b981", i: "lucide:check-circle", l: "Aprobación" },
  REJECT: { c: "#f59e0b", i: "lucide:x-circle", l: "Rechazo" },
  CHANGE_STATUS: { c: "#8b5cf6", i: "lucide:refresh-cw", l: "Cambio estado" },
};
// entity_type es el nombre interno que usa el backend (a veces el de la tabla):
// nunca se muestra crudo, siempre pasa por getEnt.
const ENT_LABELS = {
  arqueo_caja: "Arqueo de caja",
  asignacion_sucursal: "Asignación sucursal",
  autorizacion_caja: "Autorización de caja",
  ayuda_ia_mensaje: "Mensaje de ayuda IA",
  caja: "Caja",
  categoria: "Categoría",
  cliente: "Cliente",
  cliente_credito: "Crédito de cliente",
  cliente_credito_config: "Config. crédito de cliente",
  cob_gestion: "Gestión de cobranza",
  cob_gestion_mora: "Gestión de mora",
  cob_interes_cobrado: "Interés cobrado",
  cob_interes_manual: "Interés manual",
  cob_refinanciacion: "Refinanciación",
  comision: "Comisión",
  comision_cobranza: "Comisión de cobranza",
  comision_supervisor: "Comisión de supervisor",
  compra: "Compra",
  compra_cab: "Compra",
  compra_det: "Detalle de compra",
  comprobante_pago: "Comprobante pago",
  config_rrhh: "Configuración RRHH",
  cont_asientos: "Asiento contable",
  cont_ejercicios: "Ejercicio contable",
  cont_origen_registry: "Origen contable",
  cont_periodos: "Período contable",
  cont_plan_cuentas: "Plan de cuentas",
  cont_tipo_cambio: "Tipo de cambio",
  cotizacion_moneda: "Cotización de moneda",
  crm_actividades: "Actividad CRM",
  crm_canales: "Canal CRM",
  crm_etapas: "Etapa CRM",
  crm_leads: "Prospecto CRM",
  crm_oportunidades: "Oportunidad CRM",
  crm_origenes: "Origen CRM",
  crm_pipelines: "Embudo CRM",
  cuenta_cobrar: "Cuenta a cobrar",
  cuentas_cobrar: "Cuenta a cobrar",
  cuentas_pagar_empleado: "Cuenta a pagar a empleado",
  cuota_calculada: "Cuota calculada",
  cuota_individual: "Cuota individual",
  deposito: "Depósito",
  empresa: "Empresa",
  empresa_moneda: "Moneda de empresa",
  empresas_correo: "Correo de empresa",
  factura: "Factura",
  factura_intereses_moratorios: "Interés moratorio",
  gasto: "Gasto",
  gasto_cab: "Gasto",
  gasto_rendicion: "Rendición de gasto",
  lista_precios: "Lista de precios",
  lista_precios_producto: "Precio en lista",
  marca: "Marca",
  moneda: "Moneda",
  movimiento_caja: "Movimiento de caja",
  nota_credito: "Nota de crédito",
  nota_credito_compra: "Nota de crédito de compra",
  nota_credito_sifen_envio: "Envío SIFEN de nota de crédito",
  nota_remision: "Nota de remisión",
  nota_remision_sifen_envio: "Envío SIFEN de nota de remisión",
  nota_remision_sifen_sync: "Sincronización SIFEN de nota de remisión",
  numeracion: "Numeración de documentos",
  numeracion_documento: "Numeración de documentos",
  oferta: "Oferta",
  orden_compra: "Orden de compra",
  orden_venta: "Orden de venta",
  pago_manual: "Pago manual",
  pedido: "Pedido",
  perfil: "Perfil",
  pin_supervisor_validacion: "Validación PIN supervisor",
  plan: "Plan",
  plan_cuotas: "Plan de cuotas",
  pos_config: "Config POS",
  privilegio: "Privilegio",
  producto: "Producto",
  producto_atributo: "Atributo de producto",
  producto_atributo_valor: "Valor de atributo",
  producto_imagen: "Imagen de producto",
  producto_presentacion: "Presentación de producto",
  producto_variantes: "Variante de producto",
  promesas_pago: "Promesa de pago",
  proveedor: "Proveedor",
  proveedor_marca: "Marca de proveedor",
  punto_expedicion: "Punto de expedición",
  recepcion_compra: "Recepción de compra",
  recibo_cobro: "Recibo de cobro",
  recibos_cobro: "Recibo de cobro",
  recibo_multi_integracion: "Recibo de integración",
  reglas_precio: "Regla de precio",
  rendicion_adelanto: "Rendición de adelanto",
  rendicion_viatico: "Rendición de viático",
  rrhh_empleados: "Empleado",
  rrhh_liquidacion: "Liquidación de sueldo",
  rubro: "Rubro",
  sesion_caja: "Sesión de caja",
  solicitud_credito: "Solicitud de crédito",
  stock: "Stock",
  stock_ajuste: "Ajuste de stock",
  stock_deposito: "Stock por depósito",
  stock_reserva: "Reserva de stock",
  stock_transferencia: "Transferencia de stock",
  sucursal: "Sucursal",
  suscripcion: "Suscripción",
  suscripcion_modulos: "Módulos suscripción",
  suscripcion_submodulos: "Submódulos suscripción",
  suscripcion_submodulos_incluidos: "Submódulos incluidos",
  tes_cheques: "Cheque",
  tes_movimiento: "Movimiento de tesorería",
  tes_movimientos: "Movimiento de tesorería",
  tes_movimiento_det: "Detalle de movimiento de tesorería",
  tes_transferencias: "Transferencia de tesorería",
  tipo_gasto: "Tipo de gasto",
  usuario: "Usuario",
  usuario_empresa: "Usuario de empresa",
  usuario_pin_supervisor: "PIN de supervisor",
  vendedor_cobrador: "Vendedor/Cobrador",
  verificacion_vencimientos: "Verificación de vencimientos",
};

const capitalizar = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);

// "fecha_vencimiento" / "fechaVencimiento" → "Fecha vencimiento"; sin sufijo _id.
const humanizar = (k) =>
  capitalizar(
    String(k ?? "")
      .replace(/([a-z])([A-Z])/g, "$1_$2")
      .replace(/(^|_)id$/i, "")
      .replace(/_+/g, " ")
      .trim()
      .toLowerCase(),
  );

// Respaldo para entidades nuevas que todavía no están en ENT_LABELS: se le
// quitan prefijo de módulo y sufijos de tabla para no mostrar el nombre crudo.
const getEnt = (t) => {
  if (!t) return "—";
  if (ENT_LABELS[t]) return ENT_LABELS[t];
  const limpio = String(t)
    .replace(/^(cont|tes|cob|crm|rrhh|stk|pos)_/i, "")
    .replace(/_(cab|det)$/i, "");
  return humanizar(limpio) || "Otro";
};

// Nombre de las acciones que registra el backend; las que no estén acá se
// humanizan, nunca se muestra el código crudo ("CONT_CONC_REGENERAR_OK").
const ACT_LABELS = {
  EDITAR: "Edición", CREAR: "Creación", ELIMINAR: "Eliminación", ANULAR: "Anulación", CANCELAR: "Cancelación",
  CANCEL: "Cancelación", CONFIRMAR: "Confirmación", CONFIRM: "Confirmación", ACTIVAR: "Activación",
  DEACTIVATE: "Desactivación", ERROR: "Error", SKIP: "Omitido", CLOSE: "Cierre", REOPEN: "Reapertura",
  AUTHORIZE: "Autorización", TRANSITION: "Cambio de estado", AUTO_TRANSITION: "Cambio de estado automático",
  AUTO_CREATE: "Creación automática", AUTO_CLOSE: "Cierre automático", REVERT: "Reversión", REVERTIR: "Reversión",
  CORRECT: "Corrección", EXECUTE: "Ejecución", PROCESAR: "Procesamiento", RENEGOTIATE: "Renegociación",
  MARK_AS_PAID: "Marcado como pagado", BLOCK: "Bloqueo", LOGIN: "Inicio de sesión", LOGOUT: "Cierre de sesión",
  SWITCH_EMPRESA: "Cambio de empresa", CAMBIO_PLAN: "Cambio de plan", CRON_EXECUTION: "Proceso automático",
  BULK_UPDATE: "Actualización masiva", BULK_UPDATE_FILAS: "Actualización masiva",
  FACTURA_CREATE: "Factura emitida", FACTURA_CANCEL: "Factura anulada",
  RECEPCION_CREATE: "Recepción registrada", RECEPCION_UPDATE: "Recepción actualizada", RECEPCION_CANCEL: "Recepción anulada",
  RETRY_EMIT_OK: "Reintento de emisión exitoso", RETRY_EMIT_FAIL: "Reintento de emisión fallido",
  EXCEPTION_APPROVED: "Excepción aprobada", APLICAR_OVERRIDE_LIMITES: "Límites ajustados",
  ELIMINAR_OVERRIDE_LIMITES: "Límites restablecidos",
  CONT_CONC_REGENERAR_OK: "Asiento regenerado", CONT_CONC_REGENERAR_FALLA: "Regeneración sin asiento",
  CONT_CONC_REGENERAR_ERROR: "Error al regenerar", CONT_CONC_REGENERAR_TODOS: "Regeneración masiva",
  CONT_RECONTABILIZAR: "Recontabilización",
  TES_TRANSFERENCIA_CREADA: "Transferencia creada", TES_TRANSFERENCIA_ANULADA: "Transferencia anulada",
  TES_RECLASIFICAR_CATEGORIA: "Reclasificación de categoría", TES_CHEQUES_DIFERIDO_TRANSICION: "Cheque diferido",
  TES_CHEQUE_REGISTRADO: "Cheque registrado", TES_CHEQUE_RECHAZADO: "Cheque rechazado", TES_CHEQUE_ELIMINADO: "Cheque eliminado",
  TES_CHEQUE_EDITADO: "Cheque editado", TES_CHEQUE_DEVUELTO: "Cheque devuelto", TES_CHEQUE_DEPOSITO_DESHECHO: "Depósito de cheque deshecho",
  TES_CHEQUE_DEPOSITADO: "Cheque depositado", TES_CHEQUE_DEBITADO: "Cheque debitado", TES_CHEQUE_ANULADO: "Cheque anulado",
  MAR_CONDICION_CAMBIAR: "Cambio de condición (Marangatu)", MAR_COMPRA_RECHAZAR: "Compra rechazada (Marangatu)",
  MAR_COMPRA_CONFIRMAR: "Compra confirmada (Marangatu)",
};
const humanizarAccion = (a) => {
  const t = String(a ?? "").replace(/^(CONT|TES|MAR|COB|CRM)_/, "").replace(/_/g, " ").toLowerCase();
  return t.charAt(0).toUpperCase() + t.slice(1);
};
const getAct = (a) => ACT[a] || { c: "#6b7280", i: "lucide:activity", l: ACT_LABELS[a] || humanizarAccion(a) };

// ─── Saneamiento de identificadores internos ─────────────────────────────────
const UUID_SRC =
  "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
const UUID_EXACTO = new RegExp(`^${UUID_SRC}$`, "i");
const UUID_EN_TEXTO = new RegExp(`\\(?\\s*${UUID_SRC}\\s*\\)?`, "gi");
const esUuid = (v) => typeof v === "string" && UUID_EXACTO.test(v.trim());
// Además de los UUID completos, los registros viejos traen recortes que el backend
// ya no genera: "(id 5b0a5ed0)", "#5b0a5ed0", "ID: 5b0a5ed0".
const ID_RECORTADO = /\s*\(id [0-9a-f]{6,8}\)|#[0-9a-f]{8}\b|\bID:?\s*[0-9a-f]{8}\b/gi;
const sinUuids = (s) =>
  String(s)
    .replace(UUID_EN_TEXTO, " ")
    .replace(ID_RECORTADO, "")
    .replace(/\(\s*\)/g, "")
    .replace(/\s{2,}/g, " ")
    .replace(/\s+([.,;:])/g, "$1")
    .trim();

// Claves que no aportan nada al usuario (identidad del registro, multiempresa, secretos).
const CLAVES_OCULTAS = new Set([
  "id",
  "uuid",
  "empresa_id",
  "empresaId",
  "password",
  "password_hash",
  "hash",
  "salt",
  "token",
  "refresh_token",
  "__v",
]);
const esReferencia = (k) => /(_id|Id)$/.test(k);

const ETIQUETAS_CLAVE = {
  created_at: "Fecha de creación",
  updated_at: "Última modificación",
  deleted_at: "Fecha de baja",
  activo: "Activo",
  estado: "Estado",
};
const etiquetaClave = (k) => ETIQUETAS_CLAVE[k] || humanizar(k);

// Valor legible (sin ids). null = no se puede mostrar sin exponer un id.
function fmtValor(v) {
  if (v === null || v === undefined || v === "") return "—";
  if (typeof v === "boolean") return v ? "Sí" : "No";
  if (typeof v === "number") return String(v);
  if (typeof v === "string") {
    if (esUuid(v)) return null;
    if (/^\d{4}-\d{2}-\d{2}$/.test(v)) return fmtFechaCalendario(v);
    if (/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/.test(v)) return fmtFechaHora(v, v);
    return sinUuids(v) || "—";
  }
  if (Array.isArray(v)) {
    return `${v.length} elemento${v.length === 1 ? "" : "s"}`;
  }
  if (typeof v === "object") {
    // Objeto anidado (relación): se resume con su dato humano principal.
    const principal = [
      "nombre",
      "razon_social",
      "descripcion",
      "numero",
      "codigo",
    ]
      .map((k) => v[k])
      .find((x) => (typeof x === "string" || typeof x === "number") && !esUuid(String(x)));
    if (principal !== undefined) return String(principal);
    const n = entradasLegibles(v).length;
    return n ? `${n} dato${n === 1 ? "" : "s"}` : "—";
  }
  return String(v);
}

// Lista clave → valor para mostrar un registro sin ids ni claves técnicas.
function entradasLegibles(obj) {
  if (!obj || typeof obj !== "object" || Array.isArray(obj)) return [];
  return Object.entries(obj)
    .filter(([k]) => !CLAVES_OCULTAS.has(k) && !esReferencia(k))
    .map(([k, v]) => ({ key: k, label: etiquetaClave(k), valor: fmtValor(v) }))
    .filter((e) => e.valor !== null);
}

// La descripción automática del backend es "ACCION entidad (uuid)": se traduce.
const fmtDescripcion = (log) => {
  const d = sinUuids(log.descripcion || "");
  if (!d) return "—";
  if (d === `${log.action} ${log.entity_type}`) {
    return `${getAct(log.action).l}: ${getEnt(log.entity_type)}`;
  }
  return d;
};
const fmtDate = (d) => {
  if (!d) return "—";
  const normalized = typeof d === "string" ? d.replace(" ", "T") : d;
  const dt = new Date(normalized);
  if (isNaN(dt.getTime())) return "—";
  return dt.toLocaleDateString("es-PY", {
    day: "2-digit",
    month: "short",
    year: "numeric",
    hour: "2-digit",
    minute: "2-digit",
  });
};

// ─── Fecha de hoy en formato YYYY-MM-DD (fecha local, no UTC) ────────────────
const todayStr = () => {
  const d = new Date();
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${y}-${m}-${day}`;
};

// ─── Estado inicial de filtros ────────────────────────────────────────────────
const INITIAL_FILTERS = {
  action: "",
  entity_type: "",
  user_id: "",
  search: "",
  fecha_desde: todayStr(),
  fecha_hasta: todayStr(),
  empresa_filtro_id: "",
  page: 1,
  limit: 25,
};

// ─── Diff visual helper ──────────────────────────────────────────────────────
function computeDiff(oldVal, newVal) {
  if (!oldVal && !newVal) return [];
  const allKeys = new Set([
    ...Object.keys(oldVal || {}),
    ...Object.keys(newVal || {}),
  ]);
  const diffs = [];
  for (const key of allKeys) {
    if (CLAVES_OCULTAS.has(key)) continue;
    const ov = oldVal?.[key];
    const nv = newVal?.[key];
    const ovStr = JSON.stringify(ov);
    const nvStr = JSON.stringify(nv);
    if (ovStr !== nvStr) {
      const oldTxt = ov === undefined ? "—" : fmtValor(ov);
      const newTxt = nv === undefined ? "—" : fmtValor(nv);
      diffs.push({
        key,
        label: etiquetaClave(key),
        old: oldTxt,
        new: newTxt,
        // Referencias a otros registros (xxx_id o valor UUID): se informa
        // que cambió, sin mostrar los ids.
        soloCambio: esReferencia(key) || oldTxt === null || newTxt === null,
      });
    }
  }
  return diffs;
}

// ─── Exportar a CSV ───────────────────────────────────────────────────────────
function exportCsv(logs) {
  const header = [
    "Fecha",
    "Acción",
    "Módulo",
    "Descripción",
    "Usuario",
    "Empresa",
  ];
  const rows = logs.map((l) => [
    fmtDate(l.created_at),
    getAct(l.action).l,
    getEnt(l.entity_type),
    fmtDescripcion(l).replace(/,/g, ";"),
    l.user_nombre || "Sistema",
    l.empresa_nombre || l.empresa_ruc || "",
  ]);
  const csv = [header, ...rows].map((r) => r.map(sanitizeCell).join(",")).join("\n");
  const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8;" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = `auditoria_${hoyISO()}.csv`;
  a.click();
  URL.revokeObjectURL(url);
}

// ─── Valor de un registro como lista legible ─────────────────────────────────
function renderValores(valor) {
  if (!valor || typeof valor !== "object" || Array.isArray(valor)) {
    return <KvList><KvVal>{fmtValor(valor) ?? "—"}</KvVal></KvList>;
  }
  const entradas = entradasLegibles(valor);
  if (entradas.length === 0) {
    return <KvList><KvVal>Sin datos para mostrar</KvVal></KvList>;
  }
  return (
    <KvList>
      {entradas.map((e) => (
        <KvRow key={e.key}>
          <KvKey>{e.label}</KvKey>
          <KvVal>{e.valor}</KvVal>
        </KvRow>
      ))}
    </KvList>
  );
}

// ─── Componente principal ─────────────────────────────────────────────────────
export function AuditoriaLogs() {
  const [filters, setFilters] = useState(INITIAL_FILTERS);
  const [searchInput, setSearchInput] = useState("");
  const [expanded, setExpanded] = useState(null);
  const debounceRef = useRef(null);

  // Debounce de búsqueda con useRef (sin memory-leak)
  useEffect(() => {
    if (debounceRef.current) clearTimeout(debounceRef.current);
    debounceRef.current = setTimeout(() => {
      setFilters((p) => ({ ...p, search: searchInput, page: 1 }));
    }, 400);
    return () => clearTimeout(debounceRef.current);
  }, [searchInput]);

  // Query principal — se refetcha cuando cambian los filtros
  const {
    data: logsData,
    isLoading,
    isFetching,
    error,
  } = useQuery({
    queryKey: ["audit-logs", filters],
    queryFn: () => getAuditLogs(filters),
    keepPreviousData: true,
    staleTime: 30_000, // 30 s de cache entre paginaciones
  });

  // Filter-options: muy estable, 15 min de cache
  const { data: fo } = useQuery({
    queryKey: ["audit-filter-options"],
    queryFn: getAuditFilterOptions,
    staleTime: 15 * 60_000,
  });

  const logs = logsData?.data || [];
  const pg = logsData?.pagination || { page: 1, totalPages: 1, total: 0 };

  const setFilter = (key, val) =>
    setFilters((p) => ({ ...p, [key]: val, page: 1 }));
  const hasFilters =
    filters.action ||
    filters.entity_type ||
    filters.user_id ||
    filters.search ||
    filters.fecha_desde ||
    filters.fecha_hasta ||
    filters.empresa_filtro_id;
  const clearAll = () => {
    setFilters(INITIAL_FILTERS);
    setSearchInput("");
  };

  return (
    <Box>
      {/* ── Filtros ── */}
      <Filters>
        <FRow>
          <SearchBox>
            <Icon icon="lucide:search" />
            <input
              placeholder="Buscar en descripción o tipo..."
              value={searchInput}
              onChange={(e) => setSearchInput(e.target.value)}
            />
            {searchInput && (
              <button onClick={() => setSearchInput("")}>
                <Icon icon="lucide:x" />
              </button>
            )}
          </SearchBox>

          <Sel
            value={filters.action}
            onChange={(e) => setFilter("action", e.target.value)}
          >
            <option value="">Todas las acciones</option>
            {(fo?.actions || []).map((a) => (
              <option key={a} value={a}>
                {getAct(a).l}
              </option>
            ))}
          </Sel>

          <Sel
            value={filters.entity_type}
            onChange={(e) => setFilter("entity_type", e.target.value)}
          >
            <option value="">Todos los módulos</option>
            {(fo?.entity_types || []).map((t) => (
              <option key={t} value={t}>
                {getEnt(t)}
              </option>
            ))}
          </Sel>

          {(fo?.empresas || []).length > 0 && (
            <Sel
              value={filters.empresa_filtro_id}
              onChange={(e) => setFilter("empresa_filtro_id", e.target.value)}
            >
              <option value="">Todas las empresas</option>
              {fo.empresas.map((e) => (
                <option key={e.id} value={e.id}>
                  {e.razon_social}
                </option>
              ))}
            </Sel>
          )}
        </FRow>

        {/* Sin filtros por terminal/caja/sesión: pedían pegar ids internos que
            la pantalla ya no muestra. */}
        <FRow>
          <DateWrap>
            <span>Desde</span>
            <input
              type="date"
              value={filters.fecha_desde}
              onChange={(e) => setFilter("fecha_desde", e.target.value)}
            />
          </DateWrap>
          <DateWrap>
            <span>Hasta</span>
            <input
              type="date"
              value={filters.fecha_hasta}
              onChange={(e) => setFilter("fecha_hasta", e.target.value)}
            />
          </DateWrap>

          <LimitSel
            value={filters.limit}
            onChange={(e) =>
              setFilters((p) => ({
                ...p,
                limit: Number(e.target.value),
                page: 1,
              }))
            }
          >
            <option value={25}>25 por página</option>
            <option value={50}>50 por página</option>
            <option value={100}>100 por página</option>
          </LimitSel>

          {hasFilters && (
            <ClearBtn onClick={clearAll}>
              <Icon icon="lucide:filter-x" />
              Limpiar
            </ClearBtn>
          )}

          <RightGroup>
            {isFetching && !isLoading && (
              <FetchingDot title="Actualizando..." />
            )}
            <Counter>
              <Icon icon="lucide:hash" />
              {pg.total} registro{pg.total !== 1 ? "s" : ""}
            </Counter>
            {logs.length > 0 && (
              <CsvBtn
                onClick={() => exportCsv(logs)}
                title="Exportar página actual a CSV"
              >
                <Icon icon="lucide:download" />
                CSV
              </CsvBtn>
            )}
          </RightGroup>
        </FRow>
      </Filters>

      {/* ── Tabla ── */}
      <TableWrap>
        {isLoading ? (
          <StateBox>
            <Icon icon="lucide:loader-2" className="spin" />
            <span>Cargando registros...</span>
          </StateBox>
        ) : error ? (
          <StateBox $error>
            <Icon icon="lucide:alert-triangle" />
            <h4>Error al cargar</h4>
            <p>No se pudieron obtener los logs. Intentá recargar.</p>
          </StateBox>
        ) : logs.length === 0 ? (
          <StateBox>
            <Icon icon="lucide:file-search" />
            <h4>Sin resultados</h4>
            <p>
              {hasFilters
                ? "Ningún registro coincide con los filtros activos."
                : "Aún no hay registros de auditoría."}
            </p>
          </StateBox>
        ) : (
          <Table>
            <thead>
              <tr>
                <Th $w="148px">Fecha</Th>
                <Th $w="120px">Acción</Th>
                <Th $w="150px">Módulo</Th>
                <Th>Descripción</Th>
                <Th $w="32px" />
              </tr>
            </thead>
            <tbody>
              {logs.map((log) => {
                const act = getAct(log.action);
                const open = expanded === log.id;
                return [
                  <TR
                    key={log.id}
                    onClick={() => setExpanded(open ? null : log.id)}
                    $open={open}
                  >
                    <Td $muted>{fmtDate(log.created_at)}</Td>
                    <Td>
                      <ActionBadge $color={act.c}>
                        <Icon icon={act.i} />
                        {act.l}
                      </ActionBadge>
                    </Td>
                    <Td>
                      <EntityBadge>{getEnt(log.entity_type)}</EntityBadge>
                    </Td>
                    <Td $truncate>{fmtDescripcion(log)}</Td>
                    <Td>
                      <ExpandIcon $open={open}>
                        <Icon icon="lucide:chevron-down" />
                      </ExpandIcon>
                    </Td>
                  </TR>,

                  open && (
                    <DetailRow key={`${log.id}-detail`}>
                      <td colSpan={5}>
                        <DetailGrid>
                          <DetailItem>
                            <DLabel>Usuario</DLabel>
                            <DValue>{log.user_nombre || "Sistema"}</DValue>
                          </DetailItem>
                          <DetailItem>
                            <DLabel>Empresa</DLabel>
                            <DValue>
                              {log.empresa_nombre || "—"}
                              {log.empresa_ruc && (
                                <DValueSub>RUC: {log.empresa_ruc}</DValueSub>
                              )}
                            </DValue>
                          </DetailItem>
                          <DetailItem>
                            <DLabel>Fecha</DLabel>
                            <DValue>{fmtDate(log.created_at)}</DValue>
                          </DetailItem>
                          <DetailItem>
                            <DLabel>IP</DLabel>
                            <DValue>{log.ip_address || "—"}</DValue>
                          </DetailItem>
                          {/* Terminal, caja y sesión llegan sólo como ids (sin
                              nombre), por eso no se muestran. */}
                          {/* ── Diff visual ── */}
                          {log.old_value && log.new_value ? (
                            <DetailFull>
                              <DLabel>Cambios</DLabel>
                              <DiffTable>
                                <thead>
                                  <tr>
                                    <DiffTh>Campo</DiffTh>
                                    <DiffTh $color="#ef4444">Anterior</DiffTh>
                                    <DiffTh $color="#10b981">Nuevo</DiffTh>
                                  </tr>
                                </thead>
                                <tbody>
                                  {computeDiff(
                                    log.old_value,
                                    log.new_value,
                                  ).map((d) =>
                                    d.soloCambio ? (
                                      <tr key={d.key}>
                                        <DiffTd $bold>{d.label}</DiffTd>
                                        <DiffTd colSpan={2}>Cambió</DiffTd>
                                      </tr>
                                    ) : (
                                      <tr key={d.key}>
                                        <DiffTd $bold>{d.label}</DiffTd>
                                        <DiffTd
                                          $bg="#ef444412"
                                          $color="#ef4444"
                                        >
                                          {d.old}
                                        </DiffTd>
                                        <DiffTd
                                          $bg="#10b98112"
                                          $color="#10b981"
                                        >
                                          {d.new}
                                        </DiffTd>
                                      </tr>
                                    ),
                                  )}
                                  {computeDiff(log.old_value, log.new_value)
                                    .length === 0 && (
                                    <tr>
                                      <DiffTd colSpan={3}>
                                        Sin diferencias detectadas
                                      </DiffTd>
                                    </tr>
                                  )}
                                </tbody>
                              </DiffTable>
                            </DetailFull>
                          ) : (
                            <>
                              {log.old_value && (
                                <DetailFull>
                                  <DLabel>Valor anterior</DLabel>
                                  {renderValores(log.old_value)}
                                </DetailFull>
                              )}
                              {log.new_value && (
                                <DetailFull>
                                  <DLabel>Valor nuevo</DLabel>
                                  {renderValores(log.new_value)}
                                </DetailFull>
                              )}
                            </>
                          )}
                        </DetailGrid>
                      </td>
                    </DetailRow>
                  ),
                ];
              })}
            </tbody>
          </Table>
        )}
      </TableWrap>

      {/* ── Paginación ── */}
      {pg.totalPages > 1 && (
        <Pagination>
          <PageBtn
            disabled={pg.page <= 1}
            onClick={() => setFilters((p) => ({ ...p, page: p.page - 1 }))}
          >
            <Icon icon="lucide:chevron-left" />
            Anterior
          </PageBtn>
          <PageInfo>
            Pág {pg.page} de {pg.totalPages}
          </PageInfo>
          <PageBtn
            disabled={pg.page >= pg.totalPages}
            onClick={() => setFilters((p) => ({ ...p, page: p.page + 1 }))}
          >
            Siguiente
            <Icon icon="lucide:chevron-right" />
          </PageBtn>
        </Pagination>
      )}
    </Box>
  );
}

// ─── Styled Components ────────────────────────────────────────────────────────
const Box = styled.div`
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
`;
const Filters = styled.div`
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
`;
const FRow = styled.div`
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  align-items: center;
  @media (max-width: 768px) {
    flex-direction: column;
    align-items: stretch;
  }
`;

const SearchBox = styled.div`
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: 1;
  min-width: 220px;
  background: ${(p) => p.theme.bg3 || "#222"};
  border: 1px solid ${(p) => p.theme.bg4 || "#333"};
  border-radius: 8px;
  padding: 0 0.75rem;
  height: 2.25rem;
  > svg {
    width: 0.9rem;
    height: 0.9rem;
    color: ${(p) => p.theme.text || "#fff"}50;
    flex-shrink: 0;
  }
  input {
    flex: 1;
    background: none;
    border: none;
    color: ${(p) => p.theme.text || "#fff"};
    font-size: 0.8rem;
    outline: none;
    &::placeholder {
      color: ${(p) => p.theme.text || "#fff"}40;
    }
  }
  button {
    background: none;
    border: none;
    cursor: pointer;
    color: ${(p) => p.theme.text || "#fff"}50;
    display: flex;
    align-items: center;
    svg {
      width: 0.85rem;
      height: 0.85rem;
    }
    &:hover {
      color: #ef4444;
    }
  }
`;
const selBase = `
  height:2.25rem;padding:0 0.75rem;border-radius:8px;font-size:0.8rem;
  background:${(p) => p.theme.bg3 || "#222"};border:1px solid ${(p) => p.theme.bg4 || "#333"};
  color:${(p) => p.theme.text || "#fff"};outline:none;cursor:pointer;
  option{background:${(p) => p.theme.bg2 || "#111"};}
`;
const Sel = styled.select`
  ${selBase}min-width:140px;
`;
const LimitSel = styled.select`
  ${selBase}min-width:120px;
`;
const DateWrap = styled.div`
  display: flex;
  align-items: center;
  gap: 0.375rem;
  span {
    font-size: 0.75rem;
    color: ${(p) => p.theme.text || "#fff"}60;
    white-space: nowrap;
  }
  input {
    height: 2.25rem;
    padding: 0 0.5rem;
    border-radius: 8px;
    font-size: 0.8rem;
    background: ${(p) => p.theme.bg3 || "#222"};
    border: 1px solid ${(p) => p.theme.bg4 || "#333"};
    color: ${(p) => p.theme.text || "#fff"};
    outline: none;
  }
`;
const ClearBtn = styled.button`
  display: flex;
  align-items: center;
  gap: 0.375rem;
  height: 2.25rem;
  padding: 0 0.75rem;
  border-radius: 8px;
  font-size: 0.75rem;
  font-weight: 500;
  cursor: pointer;
  background: #ef444415;
  border: 1px solid #ef444430;
  color: #ef4444;
  svg {
    width: 0.85rem;
    height: 0.85rem;
  }
  &:hover {
    background: #ef444425;
  }
`;
const RightGroup = styled.div`
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-left: auto;
`;
const FetchingDot = styled.span`
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #3b82f6;
  animation: pulse 1.2s ease-in-out infinite;
  @keyframes pulse {
    0%,
    100% {
      opacity: 1;
    }
    50% {
      opacity: 0.3;
    }
  }
`;
const Counter = styled.div`
  display: flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.75rem;
  color: ${(p) => p.theme.text || "#fff"}60;
  svg {
    width: 0.8rem;
    height: 0.8rem;
  }
`;
const CsvBtn = styled.button`
  display: flex;
  align-items: center;
  gap: 0.25rem;
  height: 2.25rem;
  padding: 0 0.6rem;
  border-radius: 8px;
  font-size: 0.72rem;
  font-weight: 500;
  cursor: pointer;
  background: #10b98115;
  border: 1px solid #10b98130;
  color: #10b981;
  svg {
    width: 0.8rem;
    height: 0.8rem;
  }
  &:hover {
    background: #10b98125;
  }
`;

const TableWrap = styled.div`
  border: 1px solid ${(p) => p.theme.bg4 || "#333"};
  border-radius: 10px;
  overflow: hidden;
`;
const StateBox = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: 3rem 1rem;
  color: ${(p) => (p.$error ? "#ef4444" : `${p.theme.text || "#fff"}60`)};
  text-align: center;
  .spin {
    animation: spin 1s linear infinite;
  }
  @keyframes spin {
    to {
      transform: rotate(360deg);
    }
  }
  svg {
    width: 2rem;
    height: 2rem;
  }
  h4 {
    margin: 0;
    color: ${(p) => p.theme.text || "#fff"};
  }
  p {
    margin: 0;
    font-size: 0.85rem;
  }
`;
const Table = styled.table`
  width: 100%;
  border-collapse: collapse;
`;
const Th = styled.th`
  text-align: left;
  padding: 0.55rem 0.75rem;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: ${(p) => p.theme.text || "#fff"}60;
  background: ${(p) => p.theme.bg3 || "#222"};
  border-bottom: 1px solid ${(p) => p.theme.bg4 || "#333"};
  ${(p) => p.$w && `width:${p.$w};`}
`;
const TR = styled.tr`
  cursor: pointer;
  background: ${(p) =>
    p.$open ? `${p.theme.bg3 || "#222"}60` : "transparent"};
  transition: background 0.15s;
  &:hover {
    background: ${(p) => p.theme.bg3 || "#222"}40;
  }
  &:not(:last-child) {
    border-bottom: 1px solid ${(p) => p.theme.bg4 || "#333"}20;
  }
`;
const Td = styled.td`
  padding: 0.55rem 0.75rem;
  font-size: 0.8rem;
  color: ${(p) =>
    p.$muted ? `${p.theme.text || "#fff"}70` : p.theme.text || "#fff"};
  ${(p) =>
    p.$truncate &&
    `max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;`}
`;
const ActionBadge = styled.span`
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.18rem 0.45rem;
  border-radius: 6px;
  font-size: 0.7rem;
  font-weight: 600;
  background: ${(p) => p.$color}18;
  color: ${(p) => p.$color};
  svg {
    width: 0.72rem;
    height: 0.72rem;
  }
`;
const EntityBadge = styled.span`
  font-size: 0.75rem;
  color: ${(p) => p.theme.text || "#fff"}80;
`;
const ExpandIcon = styled.div`
  transition: transform 0.2s;
  transform: rotate(${(p) => (p.$open ? "180deg" : "0")});
  svg {
    width: 0.85rem;
    height: 0.85rem;
    color: ${(p) => p.theme.text || "#fff"}40;
  }
`;

const DetailRow = styled.tr`
  background: ${(p) => p.theme.bg3 || "#222"}30;
  border-bottom: 1px solid ${(p) => p.theme.bg4 || "#333"}40;
`;
const DetailGrid = styled.div`
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
  padding: 0.75rem;
  @media (max-width: 600px) {
    grid-template-columns: 1fr;
  }
`;
const DetailItem = styled.div`
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
`;
const DetailFull = styled.div`
  grid-column: 1/-1;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
`;
const DLabel = styled.span`
  font-size: 0.65rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: ${(p) => p.theme.text || "#fff"}50;
`;
const DValue = styled.span`
  font-size: 0.78rem;
  color: ${(p) => p.theme.text || "#fff"};
  word-break: break-all;
  ${(p) => p.$mono && `font-family:monospace;font-size:0.72rem;`}
`;
const DValueSub = styled.span`
  display: block;
  font-size: 0.65rem;
  font-family: monospace;
  color: ${(p) => p.theme.text || "#fff"}45;
  margin-top: 0.1rem;
  word-break: break-all;
`;
const KvList = styled.div`
  display: flex;
  flex-direction: column;
  font-size: 0.72rem;
  background: ${(p) => p.theme.bg2 || "#111"};
  border: 1px solid ${(p) => p.theme.bg4 || "#333"};
  border-radius: 6px;
  padding: 0.35rem 0.5rem;
  overflow-y: auto;
  max-height: 220px;
`;
const KvRow = styled.div`
  display: grid;
  grid-template-columns: minmax(120px, 35%) 1fr;
  gap: 0.5rem;
  padding: 0.2rem 0;
  &:not(:last-child) {
    border-bottom: 1px solid ${(p) => p.theme.bg4 || "#333"}20;
  }
`;
const KvKey = styled.span`
  font-weight: 600;
  color: ${(p) => p.theme.text || "#fff"};
`;
const KvVal = styled.span`
  color: ${(p) => p.theme.text || "#fff"}90;
  word-break: break-word;
`;
const DiffTable = styled.table`
  width: 100%;
  border-collapse: collapse;
  font-size: 0.72rem;
  border: 1px solid ${(p) => p.theme.bg4 || "#333"};
  border-radius: 6px;
  overflow: hidden;
`;
const DiffTh = styled.th`
  text-align: left;
  padding: 0.35rem 0.5rem;
  font-size: 0.65rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  background: ${(p) => p.theme.bg3 || "#222"};
  color: ${(p) => p.$color || `${p.theme.text || "#fff"}60`};
  border-bottom: 1px solid ${(p) => p.theme.bg4 || "#333"};
`;
const DiffTd = styled.td`
  padding: 0.3rem 0.5rem;
  font-size: 0.72rem;
  font-family: monospace;
  word-break: break-all;
  border-bottom: 1px solid ${(p) => p.theme.bg4 || "#333"}20;
  background: ${(p) => p.$bg || "transparent"};
  color: ${(p) => p.$color || `${p.theme.text || "#fff"}90`};
  ${(p) =>
    p.$bold &&
    `font-weight:600;font-family:inherit;color:${p.theme.text || "#fff"};`}
`;

const Pagination = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  padding: 0.5rem 0;
`;
const PageBtn = styled.button`
  display: flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.4rem 0.75rem;
  border-radius: 8px;
  font-size: 0.8rem;
  font-weight: 500;
  cursor: pointer;
  background: ${(p) => p.theme.bg3 || "#222"};
  border: 1px solid ${(p) => p.theme.bg4 || "#333"};
  color: ${(p) => p.theme.text || "#fff"};
  svg {
    width: 0.85rem;
    height: 0.85rem;
  }
  &:disabled {
    opacity: 0.4;
    cursor: not-allowed;
  }
  &:hover:not(:disabled) {
    background: ${(p) => p.theme.bg4 || "#333"};
  }
`;
const PageInfo = styled.span`
  font-size: 0.8rem;
  color: ${(p) => p.theme.text || "#fff"}70;
`;
