-- Dos frenos que no frenaban porque contaban por una direccion que cambia en
-- cada peticion. Ahora se cuenta por bloque, y ademas hay un tope global: por
-- seriales distintos en el verificador, y por fallos de la puerta en el panel.

alter table fiscal.consultas_serial add column if not exists grupo text;
alter table fiscal.consultas_serial add column if not exists serial text;
create index if not exists idx_consultas_serial_grupo on fiscal.consultas_serial (grupo, en);
update fiscal.consultas_serial set grupo = fiscal.grupo_de(desde) where grupo is null;

CREATE OR REPLACE FUNCTION fiscal.verificar_serial(p_serial text, p_desde text DEFAULT NULL::text)
 RETURNS jsonb
 LANGUAGE plpgsql
 SECURITY DEFINER
 SET search_path TO 'fiscal', 'public'
AS $function$
declare
  v record;
  v_cuantas int;
  v_limpio text;
  v_distintos int;
begin
  -- Se cuenta por BLOQUE de direcciones, no por la direccion exacta: la
  -- direccion cambia en cada peticion y el freno viejo nunca llegaba al tope.
  if p_desde is not null then
    select count(*) into v_cuantas from fiscal.consultas_serial
     where grupo = fiscal.grupo_de(p_desde) and en > now() - interval '1 hour';
    if v_cuantas >= 10 then
      raise exception 'Demasiadas consultas desde esta direccion. Intenta mas tarde.';
    end if;

    -- Y un tope global por SERIALES DISTINTOS: el serial es correlativo, asi que
    -- enumerar la cartera obliga a preguntar por muchos. Un cliente de verdad
    -- pregunta por el suyo, y el bloque que rota direcciones no se salva de este.
    select count(distinct serial) into v_distintos from fiscal.consultas_serial
     where en > now() - interval '1 hour' and serial is not null;
    if v_distintos >= 30 then
      raise exception 'Demasiadas consultas en este momento. Intenta mas tarde.';
    end if;

    insert into fiscal.consultas_serial (desde, grupo, serial)
      values (p_desde, fiscal.grupo_de(p_desde), upper(trim(coalesce(p_serial, ''))));
    delete from fiscal.consultas_serial where en < now() - interval '2 hours';
  end if;

  v_limpio := upper(trim(coalesce(p_serial, '')));
  -- La basura ni llega a la tabla: el verificador la rebota aqui mismo.
  if not fiscal.fn_serial_valido(v_limpio) then
    return jsonb_build_object('registrada', false, 'motivo', 'serial_invalido');
  end if;

  select e.serial, e.razon_social, e.rif, e.activo, e.creado_en into v
    from fiscal.emisores e
   where e.serial = v_limpio;
  if not found then
    return jsonb_build_object('registrada', false, 'motivo', 'no_registrada');
  end if;

  return jsonb_build_object(
    'registrada', true,
    'estado', case when v.activo then 'activa' else 'suspendida' end,
    'serial', v.serial,
    'razon_social', v.razon_social,
    'rif', v.rif,
    'desde', to_char(v.creado_en, 'MM-YYYY')
  );
end;
$function$
;

CREATE OR REPLACE FUNCTION fiscal.entrar_panel(p_usuario text, p_clave text, p_desde text DEFAULT NULL::text)
 RETURNS TABLE(entro boolean, nombre text, espera_seg integer)
 LANGUAGE plpgsql
 SECURITY DEFINER
 SET search_path TO 'fiscal', 'public', 'extensions'
AS $function$
declare
  -- La ventana y los topes en un solo lugar, para no buscarlos por el cuerpo.
  c_ventana    constant interval := interval '15 minutes';
  c_tope_usu   constant int := 8;   -- fallos con el mismo usuario
  c_tope_ip    constant int := 15;  -- fallos desde el mismo bloque de direcciones
  c_tope_usu_c constant int := 30;  -- lo mismo, desde un bloque conocido
  c_tope_ip_c  constant int := 40;
  c_tope_puerta constant int := 25; -- fallos de TODA la puerta en la ventana
  -- Hash de una clave que no existe. Sirve para gastar el mismo tiempo cuando
  -- el usuario no existe, y que el reloj no delate cuales nombres son reales.
  c_senuelo    constant text := '$2a$10$IHrhLAMm.gVpUS2Qwhf4DOze.yYhk8xdN3iOEi8owQJdcSX.kFSjG';

  v_usuario  text := left(lower(btrim(coalesce(p_usuario, ''))), 20);
  v_desde    text := nullif(btrim(coalesce(p_desde, '')), '');
  v_grupo    text := fiscal.grupo_de(p_desde);
  v_op       record;
  v_conocida boolean := false;
  v_n        int;
  v_viejo    timestamptz;
  v_tope_usu int;
  v_tope_ip  int;
  v_distintos int;
begin
  -- OJO con los alias: esta funcion devuelve una columna que se llama `entro`,
  -- asi que sin el `i.` de adelante Postgres no sabe si uno habla de la columna
  -- de la tabla o de lo que va a devolver, y truena con "entro is ambiguous".
  --
  -- Un bloque desde el que ya se entro bien alguna vez en el ultimo mes es "la
  -- casa": se le sube el tope en vez de trancarle la puerta.
  if v_grupo is not null then
    select exists(
      select 1 from fiscal.intentos i
       where i.tipo = 'panel' and i.entro and i.grupo = v_grupo and i.en > now() - interval '30 days'
    ) into v_conocida;
  end if;
  v_tope_usu := case when v_conocida then c_tope_usu_c else c_tope_usu end;
  v_tope_ip  := case when v_conocida then c_tope_ip_c else c_tope_ip end;

  -- FRENO POR USUARIO. Se mira antes de gastar bcrypt: probar claves tiene que
  -- costar barato para nosotros y caro para el que prueba. Este es el que de
  -- verdad protege la clave, porque no depende de por donde venga.
  select count(*), min(t.en) into v_n, v_viejo from (
    select i.en from fiscal.intentos i
     where i.tipo = 'panel' and i.quien = v_usuario and not i.entro and i.en > now() - c_ventana
     order by i.en desc limit v_tope_usu
  ) t;
  if v_n >= v_tope_usu then
    -- No se anota este intento: si se anotara, el que ataca mantendria la
    -- puerta trancada para siempre nada mas insistiendo.
    return query select false, null::text,
      greatest(1, ceil(extract(epoch from (v_viejo + c_ventana - now()))))::int;
    return;
  end if;

  -- FRENO POR BLOQUE DE DIRECCIONES. Contra el que prueba usuarios distintos
  -- desde un lado fijo (probando "admin", "gilberto", "imprenta"...).
  if v_grupo is not null then
    select count(*), min(t.en) into v_n, v_viejo from (
      select i.en from fiscal.intentos i
       where i.tipo = 'panel' and i.grupo = v_grupo and not i.entro and i.en > now() - c_ventana
       order by i.en desc limit v_tope_ip
    ) t;
    if v_n >= v_tope_ip then
      return query select false, null::text,
        greatest(1, ceil(extract(epoch from (v_viejo + c_ventana - now()))))::int;
      return;
    end if;
  end if;

  -- FRENO DE LA PUERTA ENTERA. Los dos frenos de arriba no atajan al que prueba
  -- NOMBRES distintos desde direcciones que cambian solas (probado: 20 usuarios
  -- inventados, ni un corte). Este cuenta los fallos de todos y, cuando llueven,
  -- hace esperar a cualquier intento que falle. Al que sabe su clave no le pasa
  -- nada, porque no falla; y no dice si el usuario existe o no, asi que tampoco
  -- delata nombres.
  select count(*), min(t.en) into v_n, v_viejo from (
    select i.en from fiscal.intentos i
     where i.tipo = 'panel' and not i.entro and i.en > now() - c_ventana
     order by i.en desc limit c_tope_puerta
  ) t;
  if v_n >= c_tope_puerta then
    insert into fiscal.intentos (quien, entro, motivo, desde, tipo, grupo)
    values (v_usuario, false, 'puerta frenada por exceso de intentos', v_desde, 'panel', v_grupo);
    return query select false, null::text,
      greatest(1, ceil(extract(epoch from (v_viejo + c_ventana - now()))))::int;
    return;
  end if;

  select * into v_op from fiscal.operadores where usuario = v_usuario and activo;

  if v_op.id is null then
    -- Se gasta el bcrypt igual, contra el señuelo, para que un usuario que no
    -- existe tarde lo mismo que uno que si.
    perform extensions.crypt(coalesce(p_clave, ''), c_senuelo);
    insert into fiscal.intentos (quien, entro, motivo, desde, tipo, grupo)
    values (v_usuario, false, 'usuario que no existe', v_desde, 'panel', v_grupo);
    return query select false, null::text, 0;
    return;
  end if;

  if v_op.clave_hash is distinct from extensions.crypt(coalesce(p_clave, ''), v_op.clave_hash) then
    -- Aca el insert SI queda: esta funcion responde que no, no revienta con
    -- `raise`, y por eso no se revierte la transaccion (ver 20260729060000).
    insert into fiscal.intentos (quien, entro, motivo, desde, tipo, grupo)
    values (v_usuario, false, 'clave incorrecta', v_desde, 'panel', v_grupo);
    return query select false, null::text, 0;
    return;
  end if;

  update fiscal.operadores set ultimo_acceso = now() where id = v_op.id;
  insert into fiscal.intentos (quien, entro, motivo, desde, tipo, grupo)
  values (v_usuario, true, 'entro al panel', v_desde, 'panel', v_grupo);
  return query select true, v_op.nombre, 0;
end;
$function$
;
