-- LA CERRADURA DE LAS MAQUINAS TIENE QUE SER RAPIDA (2026-07-29)
--
-- Problema que planteo Gilberto pensando en 100 restaurantes: cada Estacion
-- toca la puerta cada 20 segundos, y cada toque verificaba la llave con bcrypt.
--
-- bcrypt es lento A PROPOSITO (cost 10, unos 100 ms) porque las claves humanas
-- son adivinables y hay que castigar la fuerza bruta. Pero estas llaves son 24
-- bytes al azar (192 bits): no hay diccionario ni fuerza bruta posible contra
-- eso, ni con todo el tiempo del mundo. Usar bcrypt ahi no suma seguridad, solo
-- quema CPU de la base.
--
-- Cuentas con 100 negocios: 300 llamadas por minuto x 100 ms = 30 segundos de
-- CPU por minuto, o sea medio nucleo permanente SOLO abriendo la puerta.
-- Con SHA-256 indexado: menos de 1 ms por llamada. Unas 100 veces menos.
--
-- Es el mismo criterio de GitHub y Stripe con sus llaves de API: hash rapido +
-- indice unico, porque el secreto tiene entropia de sobra.
--
-- Las claves de PERSONAS (fiscal.operadores) NO se tocan: esas siguen con
-- bcrypt, que es donde si hace falta.
begin;

-- 1. LA CERRADURA NUEVA ----------------------------------------------------
alter table fiscal.llaves
  add column if not exists llave_sha text;

create unique index if not exists llaves_sha_idx on fiscal.llaves(llave_sha)
  where llave_sha is not null;

create or replace function fiscal.huella_llave(p_llave text)
returns text
language sql
immutable
as $fn$
  select encode(sha256(convert_to(coalesce(p_llave, ''), 'UTF8')), 'hex');
$fn$;

-- Las llaves que ya estan entregadas no se pueden convertir de golpe: solo
-- existe su bcrypt, y la llave en claro la tiene el cliente. Se convierten
-- SOLAS la primera vez que se usan (abajo). Nadie tiene que hacer nada.
create or replace function fiscal.crear_llave(p_rif text, p_nombre text)
returns text
language plpgsql
security definer
set search_path to 'fiscal', 'public', 'extensions'
as $fn$
declare
  v_emisor uuid;
  v_llave text;
begin
  select id into v_emisor from fiscal.emisores where rif = public.fn_rif_normalizado(p_rif) and activo;
  if v_emisor is null then raise exception 'Ese RIF no esta registrado como emisor'; end if;

  v_llave := 'gxf_' || encode(extensions.gen_random_bytes(24), 'hex');
  -- Se guardan las dos: el sha para entrar rapido, el bcrypt porque no cuesta
  -- nada tenerlo y deja el camino viejo funcionando sin sobresaltos.
  insert into fiscal.llaves (emisor_id, nombre, llave_hash, llave_sha)
    values (v_emisor, p_nombre,
            extensions.crypt(v_llave, extensions.gen_salt('bf', 10)),
            fiscal.huella_llave(v_llave));
  return v_llave;
end;
$fn$;

-- 2. ABRIR LA PUERTA -------------------------------------------------------
-- Primero por el camino rapido (indice unico). Si esa llave todavia es de las
-- viejas, se cae al camino lento UNA sola vez y de paso queda convertida.
create or replace function fiscal.autenticar(p_llave text, p_rif text, p_motivo text)
returns uuid
language plpgsql
security definer
set search_path to 'fiscal', 'public', 'extensions'
as $fn$
declare
  v_emisor uuid;
  v_llave_id uuid;
  v_recientes int;
begin
  select id into v_emisor from fiscal.emisores
   where rif = public.fn_rif_normalizado(p_rif) and activo;
  if v_emisor is null then
    insert into fiscal.intentos (rif, entro, motivo, desde) values (p_rif, false, 'emisor no registrado', p_motivo);
    raise exception 'No autorizado';
  end if;

  -- CAMINO RAPIDO: un vistazo al indice, sin criptografia cara.
  select l.id into v_llave_id
    from fiscal.llaves l
   where l.emisor_id = v_emisor and l.activa
     and l.llave_sha = fiscal.huella_llave(p_llave);

  if v_llave_id is null then
    -- Antes de gastar en bcrypt, el freno: si alguien esta probando llaves, que
    -- no nos cueste CPU. Esto va DESPUES del camino rapido a proposito, para
    -- que un cliente legitimo con muchas llamadas nunca lo toque.
    select count(*) into v_recientes from fiscal.intentos
     where rif = public.fn_rif_normalizado(p_rif) and not entro and en > now() - interval '5 minutes';
    if v_recientes >= 20 then
      raise exception 'Demasiados intentos fallidos. Espera unos minutos.';
    end if;

    -- CAMINO VIEJO: llave entregada antes de este cambio. Se acepta y se
    -- convierte, para que la proxima vez entre por el rapido.
    select l.id into v_llave_id
      from fiscal.llaves l
     where l.emisor_id = v_emisor and l.activa and l.llave_sha is null
       and l.llave_hash = extensions.crypt(p_llave, l.llave_hash)
     limit 1;

    if v_llave_id is null then
      insert into fiscal.intentos (rif, entro, motivo, desde) values (p_rif, false, 'llave invalida', p_motivo);
      raise exception 'No autorizado';
    end if;
    update fiscal.llaves set llave_sha = fiscal.huella_llave(p_llave) where id = v_llave_id;
  end if;

  update fiscal.llaves set ultimo_uso = now() where id = v_llave_id;
  return v_emisor;
end;
$fn$;

-- La puerta de los sistemas de terceros usa la misma cerradura: no tiene
-- sentido que emitir sea 100 veces mas caro que sondear.
create or replace function fiscal.emitir_con_llave(
  p_llave text,
  p_rif text,
  p_serie text,
  p_tipo text,
  p_numero_documento bigint,
  p_hash text,
  p_fecha timestamptz,
  p_base_bs numeric,
  p_iva_bs numeric,
  p_total_bs numeric,
  p_documento jsonb,
  p_desde text default null
)
returns table(numero_control bigint, token text, ya_estaba boolean)
language plpgsql
security definer
set search_path to 'fiscal', 'public', 'extensions'
as $fn$
declare v_emisor uuid;
begin
  v_emisor := fiscal.autenticar(p_llave, p_rif, coalesce(p_desde, 'emision'));
  insert into fiscal.intentos (rif, entro, motivo, desde) values (p_rif, true, 'emision', p_desde);

  return query select * from fiscal.recibir_documento(
    p_rif, p_serie, p_tipo, p_numero_documento, p_hash, p_fecha,
    p_base_bs, p_iva_bs, p_total_bs, p_documento
  );
end;
$fn$;

-- 3. SONDEAR BARATO --------------------------------------------------------
-- El latido ya dice cuantas ventas hay esperando. Con eso, la Estacion solo
-- hace la llamada pesada cuando de verdad hay algo que sellar: un negocio sin
-- ventas nuevas no mueve una sola fila.
--
-- Ademas se le responde cada cuanto volver a preguntar (lo decide la nube, no
-- la Estacion): en hora pico rapido, de madrugada despacio. Asi se puede
-- ajustar el ritmo de todos sin actualizar el programa de nadie.
create or replace function fiscal.latido(p_llave text, p_rif text, p_datos jsonb default '{}'::jsonb)
returns jsonb
language plpgsql
security definer
set search_path to 'fiscal', 'public'
as $fn$
declare
  v_emisor uuid;
  v_pend int;
  v_ultima timestamptz;
  v_espera int;
begin
  v_emisor := fiscal.autenticar(p_llave, p_rif, 'estacion: latido');

  insert into fiscal.estaciones (emisor_id, nombre, version, equipo, ultimo_latido,
                                 pendientes, ultimo_documento, libro_integro)
    values (v_emisor,
            coalesce(nullif(p_datos->>'nombre', ''), 'Estacion Fiscal'),
            p_datos->>'version',
            coalesce(nullif(p_datos->>'equipo', ''), 'unico'),
            now(),
            coalesce((p_datos->>'pendientes')::int, 0),
            nullif(p_datos->>'ultimo_documento', '')::bigint,
            (p_datos->>'libro_integro')::boolean)
    on conflict (emisor_id, equipo) do update set
      nombre = excluded.nombre,
      version = excluded.version,
      ultimo_latido = now(),
      pendientes = excluded.pendientes,
      ultimo_documento = excluded.ultimo_documento,
      libro_integro = excluded.libro_integro;

  select count(*), max(creado_en) into v_pend, v_ultima
    from fiscal.cola_gustito
   where emisor_id = v_emisor and estado = 'pendiente';

  -- El ritmo lo marca el movimiento del negocio, no el reloj.
  v_espera := case
    when v_pend > 0 then 10                                              -- hay trabajo: ya mismo
    when v_ultima > now() - interval '15 minutes' then 20                -- vendio hace nada: atento
    when v_ultima > now() - interval '2 hours' then 60                   -- venia vendiendo: tranquilo
    else 300                                                             -- cerrado o sin movimiento
  end;

  return jsonb_build_object(
    'ok', true,
    'pendientes_en_nube', v_pend,
    'habilitado', true,
    'volver_en_segundos', v_espera
  );
end;
$fn$;

commit;
