-- LA PUERTA PUBLICA DE LA IMPRENTA DIGITAL (2026-07-28)
-- Para el cliente que YA tiene su sistema administrativo homologado (Profit,
-- Saint, o uno propio) y solo necesita quien le asigne el numero de control de
-- sus ventas por medios electronicos (000102, art. 5).
--
-- Ese cliente NO instala nada nuestro: arma su documento en su sistema y nos lo
-- manda con su llave. Nosotros numeramos, resguardamos y respondemos.
--
-- La llave se guarda HASHEADA (bcrypt). Si alguien se lleva la tabla, no se
-- lleva las llaves de los clientes.
begin;

-- Cada emisor puede tener varias llaves (para rotarlas sin cortarle el servicio).
create table if not exists fiscal.llaves (
  id uuid primary key default gen_random_uuid(),
  emisor_id uuid not null references fiscal.emisores(id) on delete restrict,
  nombre text not null,
  llave_hash text not null,
  activa boolean not null default true,
  creada_en timestamptz not null default now(),
  revocada_en timestamptz,
  ultimo_uso timestamptz
);

create index if not exists llaves_emisor on fiscal.llaves (emisor_id) where activa;

-- Cada intento de emision queda registrado, entre o no entre. Sirve de rastro y
-- para frenar a quien ande probando llaves.
create table if not exists fiscal.intentos (
  id bigserial primary key,
  rif text,
  entro boolean not null,
  motivo text,
  desde text,
  en timestamptz not null default now()
);

create index if not exists intentos_rif_en on fiscal.intentos (rif, en desc);

revoke all on table fiscal.llaves, fiscal.intentos from anon, authenticated;

-- Genera una llave nueva para un emisor. Devuelve la llave EN CLARO una sola
-- vez: es la unica oportunidad de copiarla, despues solo queda su hash.
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;

  -- Prefijo legible para saber de un vistazo que es, sin revelar nada.
  v_llave := 'gxf_' || encode(extensions.gen_random_bytes(24), 'hex');
  insert into fiscal.llaves (emisor_id, nombre, llave_hash)
    values (v_emisor, p_nombre, extensions.crypt(v_llave, extensions.gen_salt('bf', 10)));
  return v_llave;
end;
$fn$;

/**
 * EMITIR CON LLAVE: la puerta que usa el sistema del cliente.
 * Revisa la llave, numera y guarda, todo en la misma operacion. Si la llave no
 * sirve, no dice por que (no se le regala informacion a quien esta probando).
 */
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;
  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_desde);
    raise exception 'No autorizado';
  end if;

  -- Freno: demasiados intentos fallidos seguidos desde el mismo origen.
  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;

  select l.id into v_llave_id
    from fiscal.llaves l
   where l.emisor_id = v_emisor and l.activa
     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_desde);
    raise exception 'No autorizado';
  end if;

  update fiscal.llaves set ultimo_uso = now() where id = v_llave_id;
  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$;

-- Puente controlado: solo lo llama el servidor (la Pages Function), nunca un
-- navegador. Igual que el validador del sello.
create or replace function public.fn_emitir_documento(
  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 sql
security definer
set search_path to 'public', 'fiscal'
as $fn$
  select * from fiscal.emitir_con_llave(p_llave, p_rif, p_serie, p_tipo, p_numero_documento,
                                        p_hash, p_fecha, p_base_bs, p_iva_bs, p_total_bs,
                                        p_documento, p_desde);
$fn$;

revoke execute on function public.fn_emitir_documento(text, text, text, text, bigint, text, timestamptz, numeric, numeric, numeric, jsonb, text) from anon, authenticated, public;
grant execute on function public.fn_emitir_documento(text, text, text, text, bigint, text, timestamptz, numeric, numeric, numeric, jsonb, text) to service_role;

commit;
