// AGUANTA CIEN RESTAURANTES? (2026-07-29)
//
// Pregunta de Gilberto: si cada Estacion toca la puerta cada 20 segundos, con
// cien negocios no se ahoga la base?
//
// Se mide DENTRO de la base (no por la red, que la latencia tapa todo):
//   1. Cuanto cuesta abrir la puerta por el camino rapido vs el viejo.
//   2. Que una llave vieja sigue entrando y se convierte sola.
//   3. Que el ritmo se afloja cuando el negocio no tiene movimiento.
//   4. Cuanto costaria de verdad un dia entero con cien restaurantes.
import { readFileSync } from 'node:fs';

const REF = 'szzhhlevtrcambcoytiz';
const PAT = readFileSync(new URL('../.sb_token', import.meta.url), 'utf8').trim();
const PUERTA = process.env.PUERTA ?? 'https://sello.gustitoxpress.app';

let fallos = 0;
const check = (cond, msg) => {
  console.log((cond ? '  OK  ' : '  XX  ') + msg);
  if (!cond) fallos++;
};

async function sql(query) {
  const r = await fetch(`https://api.supabase.com/v1/projects/${REF}/database/query`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${PAT}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ query }),
  });
  const t = await r.text();
  if (!r.ok) throw new Error(t.slice(0, 300));
  return JSON.parse(t);
}
const uno = async (q) => (await sql(q))[0];
const lit = (s) => `'${String(s).replace(/'/g, "''")}'`;

const RIF = 'V-22606827-5';
const VUELTAS = 50;

const limpiar = () =>
  sql(`delete from fiscal.intentos where quien=${lit(RIF)};
       delete from fiscal.estaciones where emisor_id in (select id from fiscal.emisores where rif=${lit(RIF)});
       delete from fiscal.llaves where emisor_id in (select id from fiscal.emisores where rif=${lit(RIF)});
       delete from fiscal.emisores where rif=${lit(RIF)};`);

try {
  await limpiar();
  await sql(`insert into fiscal.emisores (rif, razon_social, domicilio_fiscal, llave_hash, rif_comprobado)
             values (${lit(RIF)}, 'Medicion de carga', 'Araure', 'sin-uso', true)`);
  const llave = (await uno(`select fiscal.crear_llave(${lit(RIF)}, 'medicion') as k`)).k;

  console.log('\n== 1. La cerradura nueva ==');
  const guardado = await uno(`select llave_sha is not null as tiene_sha, llave_hash is not null as tiene_bcrypt
                                from fiscal.llaves where emisor_id in (select id from fiscal.emisores where rif=${lit(RIF)})`);
  check(guardado.tiene_sha && guardado.tiene_bcrypt, 'la llave nueva guarda las dos cerraduras');

  // Se mide adentro de la base para que la red no ensucie el numero, y con un
  // bucle procedural en vez de una consulta: en SQL el planner reordena, cachea
  // expresiones constantes y descarta las que nadie usa, y el numero sale
  // bonito y falso (tres veces pise esa piedra antes de escribirlo asi).
  const medidor = `
    create or replace function pg_temp.medir(p_que text, p_n int, p_llave text, p_rif text)
    returns numeric language plpgsql as $med$
    declare t0 timestamptz; i int; basura text; v_hash text;
    begin
      select l.llave_hash into v_hash from fiscal.llaves l
        join fiscal.emisores e on e.id = l.emisor_id where e.rif = p_rif limit 1;
      t0 := clock_timestamp();
      for i in 1..p_n loop
        if p_que = 'rapido' then
          perform fiscal.autenticar(p_llave, p_rif, 'medicion');
        else
          -- La llave se ensucia con el numero de vuelta: si no, es la misma
          -- cuenta y no se puede confiar en que la repita.
          basura := extensions.crypt(p_llave || i::text, v_hash);
        end if;
      end loop;
      return round(extract(epoch from (clock_timestamp() - t0)) * 1000 / p_n, 3);
    end $med$;`;

  const medir = async (etiqueta, que, vueltas) => {
    const r = await uno(`${medidor} select pg_temp.medir(${lit(que)}, ${vueltas}, ${lit(llave)}, ${lit(RIF)}) as ms`);
    console.log(`       ${etiqueta}: ${r.ms} ms por llamada`);
    return Number(r.ms);
  };

  const rapido = await medir('camino rapido (sha256 + indice)', 'rapido', VUELTAS);
  check(rapido < 5, `abrir la puerta cuesta menos de 5 ms (${rapido} ms)`);

  console.log('\n== 2. Una llave vieja sigue entrando y se convierte sola ==');
  await sql(`update fiscal.llaves set llave_sha = null
              where emisor_id in (select id from fiscal.emisores where rif=${lit(RIF)})`);
  const entroVieja = await uno(`select fiscal.autenticar(${lit(llave)}, ${lit(RIF)}, 'medicion') is not null as ok`);
  check(entroVieja.ok, 'una llave entregada antes del cambio sigue entrando');
  const convertida = await uno(`select llave_sha is not null as ya from fiscal.llaves
                                 where emisor_id in (select id from fiscal.emisores where rif=${lit(RIF)})`);
  check(convertida.ya, 'y queda convertida sola, sin que el cliente haga nada');

  // Se mide el bcrypt PURO. Medirlo por fiscal.autenticar daria un numero
  // engañoso: en la primera vuelta la llave se convierte y las demas ya
  // entran por el camino rapido, asi que el promedio saldria bajito y falso.
  const VUELTAS_LENTAS = 10;
  // Medir el camino viejo por fiscal.autenticar daria un numero engañoso: en la
  // primera vuelta la llave se convierte y el resto ya entra por el rapido. Se
  // mide el bcrypt puro, que es lo que se quito de en medio.
  const lento = await medir('camino viejo (bcrypt puro)', 'lento', VUELTAS_LENTAS);
  check(lento > rapido * 10, `el camino viejo es mucho mas caro (${lento} ms)`);

  const veces = Math.round(lento / Math.max(rapido, 0.001));
  console.log(`       => la cerradura nueva es ~${veces} veces mas barata`);

  console.log('\n== 3. Cuentas para cien restaurantes ==');
  // Cada Estacion manda un latido; solo hala cuando de verdad hay ventas.
  const porDia = (segundos) => Math.round((100 * 86400) / segundos);
  const cpuSegundos = (llamadas, ms) => Math.round((llamadas * ms) / 1000);
  const llamadasAntes = porDia(20);
  const llamadasDespues = porDia(60); // promedio real: pico rapido, madrugada lento
  console.log(`       antes:   ${llamadasAntes.toLocaleString('es-VE')} llamadas/dia x ${lento} ms = ${cpuSegundos(llamadasAntes, lento).toLocaleString('es-VE')} s de CPU`);
  console.log(`       ahora:   ${llamadasDespues.toLocaleString('es-VE')} llamadas/dia x ${rapido} ms = ${cpuSegundos(llamadasDespues, rapido).toLocaleString('es-VE')} s de CPU`);
  check(cpuSegundos(llamadasDespues, rapido) < 86400 * 0.05,
    'el sondeo de cien restaurantes gasta menos del 5% de un nucleo en todo el dia');

  console.log('\n== 4. El ritmo se afloja cuando no hay movimiento ==');
  const sinNada = await uno(`select fiscal.latido(${lit(llave)}, ${lit(RIF)}, '{"equipo":"caja-1"}'::jsonb) as r`);
  check(sinNada.r.volver_en_segundos === 300,
    `sin ventas manda esperar 5 minutos (${sinNada.r.volver_en_segundos} s)`);

  // Se le mete una venta a la cola para ver que se acelera.
  const rest = await uno(`select id from public.restaurantes where slug='demo-gustito'`);
  await sql(`insert into fiscal.cola_gustito (restaurante_id, emisor_id, tipo, origen_id)
             select ${lit(rest.id)}, id, 'pedido', gen_random_uuid() from fiscal.emisores where rif=${lit(RIF)}`);
  const conVenta = await uno(`select fiscal.latido(${lit(llave)}, ${lit(RIF)}, '{"equipo":"caja-1"}'::jsonb) as r`);
  check(conVenta.r.volver_en_segundos === 10 && conVenta.r.pendientes_en_nube === 1,
    `con una venta esperando se pone atenta (${conVenta.r.volver_en_segundos} s)`);

  await sql(`delete from fiscal.cola_gustito where emisor_id in (select id from fiscal.emisores where rif=${lit(RIF)})`);
  const reciente = await uno(`select fiscal.latido(${lit(llave)}, ${lit(RIF)}, '{"equipo":"caja-1"}'::jsonb) as r`);
  check(reciente.r.volver_en_segundos === 300,
    'y al vaciarse la cola vuelve a aflojar');

  console.log('\n== 5. Los intentos fallidos SI quedan registrados ==');
  // Esto es lo que cazo el bug: dentro de la base el `raise exception` revertia
  // el insert del intento, asi que el freno anti fuerza bruta contaba siempre
  // cero. Ahora lo anota la puerta, que es otra transaccion. Se prueba por el
  // camino real (HTTP), no por SQL, porque es ahi donde vive el arreglo.
  const antes = Date.now();
  for (let i = 0; i < 3; i++) {
    await fetch(`${PUERTA}/api/estacion`, {
      method: 'POST',
      headers: {
        authorization: 'Bearer gxf_llavefalsa000000000000000000000000000000000000',
        'content-type': 'application/json',
      },
      body: JSON.stringify({ accion: 'latido', rif: RIF, datos: {} }),
    });
  }
  // waitUntil corre despues de responder: se le da un respiro.
  await new Promise((r) => setTimeout(r, 1500));
  const fallidos = await uno(`select count(*)::int as n from fiscal.intentos where quien=${lit(RIF)} and not entro`);
  check(fallidos.n === 3, `los intentos con llave falsa quedan registrados (${fallidos.n})`);
  console.log(`       tres intentos fallidos tardaron ${Date.now() - antes} ms en total (incluye red)`);

  console.log('\n== 6. Y a los 20 se traba la puerta ==');
  await sql(`insert into fiscal.intentos (quien, entro, motivo)
             select ${lit(RIF)}, false, 'relleno de prueba' from generate_series(1, 20)`);
  const trabada = await fetch(`${PUERTA}/api/estacion`, {
    method: 'POST',
    headers: {
      authorization: 'Bearer gxf_llavefalsa000000000000000000000000000000000000',
      'content-type': 'application/json',
    },
    body: JSON.stringify({ accion: 'latido', rif: RIF, datos: {} }),
  });
  check(trabada.status === 429, `quien anda probando llaves se queda afuera (HTTP ${trabada.status})`);

  // Y el cliente legitimo sigue entrando: el freno va despues del camino
  // rapido, asi que a el ni lo roza.
  const legitimo = await fetch(`${PUERTA}/api/estacion`, {
    method: 'POST',
    headers: { authorization: `Bearer ${llave}`, 'content-type': 'application/json' },
    body: JSON.stringify({ accion: 'latido', rif: RIF, datos: { equipo: 'caja-1' } }),
  });
  check(legitimo.status === 200, `y el negocio de verdad no se ve afectado (HTTP ${legitimo.status})`);
} catch (e) {
  console.log('  XX  ' + String(e).slice(0, 300));
  fallos++;
} finally {
  await limpiar();
}

console.log(fallos === 0 ? '\nCARGA: TODO VERDE' : `\n${fallos} FALLO(S)`);
process.exit(fallos === 0 ? 0 : 1);
