import { Parser } from 'node-sql-parser';

/**
 * Valida el SQL que escribe el modelo antes de ejecutarlo (plan §6.1, consultas de
 * negocio). NO es la frontera de seguridad: la frontera es la base (rol soporte_ia
 * con acceso solo a las vistas, vistas filtradas por el alcance FIRMADO, privilegios
 * aplicados en cada vista). Esto es la capa de adelante: rechaza temprano, con un
 * error que el modelo puede corregir, y corta lo que la base no corta (funciones
 * caras, consultas sin límite).
 *
 * Reglas:
 *   - una sola sentencia, SELECT (con o sin WITH);
 *   - solo vistas soporte_ro.v_* de la lista permitida para el usuario (o CTE propios);
 *   - solo funciones de la lista blanca (nada de set_config, current_setting, pg_*…);
 *   - se envuelve con un LIMIT.
 */

const parser = new Parser();
const OPCIONES = { database: 'postgresql' };

export const MAX_FILAS = 200;

const FUNCIONES_PERMITIDAS = new Set([
  // agregados
  'count',
  'sum',
  'avg',
  'min',
  'max',
  'string_agg',
  'array_agg',
  'bool_and',
  'bool_or',
  'percentile_cont',
  'percentile_disc',
  'stddev',
  'variance',
  // ventana
  'row_number',
  'rank',
  'dense_rank',
  'lag',
  'lead',
  'first_value',
  'last_value',
  'ntile',
  // nulos y comparación
  'coalesce',
  'nullif',
  'greatest',
  'least',
  // números
  'round',
  'abs',
  'ceil',
  'ceiling',
  'floor',
  'trunc',
  'mod',
  'sign',
  // texto
  'lower',
  'upper',
  'trim',
  'ltrim',
  'rtrim',
  'btrim',
  'length',
  'char_length',
  'substring',
  'substr',
  'left',
  'right',
  'concat',
  'concat_ws',
  'replace',
  'position',
  'strpos',
  'split_part',
  'initcap',
  'lpad',
  'rpad',
  'unaccent',
  // fechas
  'to_char',
  'to_date',
  'to_number',
  'date_trunc',
  'date_part',
  'extract',
  'age',
  'now',
  'current_date',
  'current_timestamp',
  'localtimestamp',
  'make_date',
  'make_interval',
]);

// Respaldo por texto: si el parser no las viera (sintaxis rara), igual se rechazan.
const PROHIBIDO =
  /\b(set_config|current_setting|set_local|pg_\w+|dblink\w*|lo_\w+|copy|execute|prepare|query_to_xml\w*|xmlparse|generate_series|txid\w*|nextval|setval|currval)\b|\bfor\s+(update|share|no\s+key)\b|\binto\b/i;

export type ResultadoValidacion = { ok: true; sql: string } | { ok: false; error: string };

export function validarConsulta(sqlOriginal: string, vistasPermitidas: ReadonlySet<string>): ResultadoValidacion {
  const sql = (sqlOriginal ?? '').trim().replace(/;\s*$/, '');
  if (!sql) return { ok: false, error: 'La consulta está vacía.' };
  if (sql.length > 6000) return { ok: false, error: 'La consulta es demasiado larga.' };
  if (sql.includes(';')) return { ok: false, error: 'Una sola sentencia, sin ";".' };
  if (/--|\/\*/.test(sql)) return { ok: false, error: 'Sin comentarios en la consulta.' };
  const prohibido = sql.match(PROHIBIDO);
  if (prohibido) return { ok: false, error: `No se permite "${prohibido[0]}".` };

  let ast: unknown;
  let tablas: string[];
  try {
    ast = parser.astify(sql, OPCIONES);
    tablas = parser.tableList(sql, OPCIONES);
  } catch (err) {
    return { ok: false, error: `No se pudo interpretar el SQL: ${(err as Error).message.slice(0, 200)}` };
  }

  const sentencias = Array.isArray(ast) ? ast : [ast];
  if (sentencias.length !== 1) return { ok: false, error: 'Una sola sentencia.' };
  const raiz = sentencias[0] as { type?: string; with?: Array<{ name?: { value?: string } | string }> | null };
  if (String(raiz?.type).toLowerCase() !== 'select') return { ok: false, error: 'Solo se permiten consultas SELECT.' };

  const ctes = new Set(
    (raiz.with ?? [])
      .map((w) => (typeof w.name === 'string' ? w.name : w.name?.value))
      .filter((n): n is string => !!n)
      .map((n) => n.toLowerCase()),
  );

  for (const t of tablas) {
    const [operacion, esquema, tabla] = t.split('::');
    if (operacion !== 'select') return { ok: false, error: 'Solo lectura.' };
    const nombre = (tabla ?? '').toLowerCase();
    if ((esquema === 'null' || !esquema) && ctes.has(nombre)) continue;
    if (esquema !== 'soporte_ro') {
      return { ok: false, error: `Solo se pueden consultar vistas soporte_ro.v_* (no "${esquema}.${tabla}").` };
    }
    if (!vistasPermitidas.has(nombre)) {
      return { ok: false, error: `La vista soporte_ro.${nombre} no existe o el usuario no tiene permiso para verla.` };
    }
  }

  const funciones = new Set<string>();
  juntarFunciones(ast, funciones);
  for (const f of funciones) {
    if (!FUNCIONES_PERMITIDAS.has(f)) return { ok: false, error: `No se permite la función "${f}".` };
  }

  return { ok: true, sql: `SELECT * FROM (${sql}) AS consulta LIMIT ${MAX_FILAS}` };
}

function juntarFunciones(nodo: unknown, salida: Set<string>) {
  if (!nodo || typeof nodo !== 'object') return;
  if (Array.isArray(nodo)) {
    nodo.forEach((n) => juntarFunciones(n, salida));
    return;
  }
  const n = nodo as Record<string, unknown>;
  if (n.type === 'function' || n.type === 'aggr_func') {
    const nombre = nombreFuncion(n.name);
    if (nombre) salida.add(nombre);
  }
  for (const v of Object.values(n)) juntarFunciones(v, salida);
}

function nombreFuncion(nombre: unknown): string | null {
  if (typeof nombre === 'string') return nombre.toLowerCase();
  const partes = (nombre as { name?: Array<{ value?: string }>; schema?: { value?: string } } | null)?.name;
  if (!Array.isArray(partes)) return null;
  // Con esquema ("pg_catalog.set_config") el nombre completo no está en la lista blanca.
  const esquema = (nombre as { schema?: { value?: string } }).schema?.value;
  const base = partes
    .map((p) => p.value)
    .join('.')
    .toLowerCase();
  return esquema ? `${esquema.toLowerCase()}.${base}` : base;
}
