-- EL PANEL DE LA IMPRENTA: RASTRO PROPIO Y MAS DE UNA PERSONA (2026-07-29)
--
-- Nivel 2 del sprint de blindaje. Dos cosas que no tumban el negocio pero que
-- dejan muy mal parado el dia que alguien pregunte.
--
-- 1. EL PANEL NO DEJABA RASTRO. Suspender a un cliente, generarle una llave,
--    revocarsela, habilitarle la facturacion a un negocio: todo eso se podia
--    hacer y no quedaba escrito en ningun lado. Peor: `vinculado_por` era un
--    texto que mandaba el NAVEGADOR, o sea que quien lo llenaba era quien
--    quisiera y podia escribir cualquier cosa. Ahora quien hizo cada cosa sale
--    de la SESION, del lado del servidor, y no hay forma de escribirlo a mano.
--
-- 2. LA IMPRENTA DEPENDIA DE UNA SOLA PERSONA. Habia un solo operador y ni
--    siquiera existia manera de crear otro sin meter SQL a mano. Si a Gilberto
--    le pasa algo, la facturacion de todos sus clientes se queda sin quien la
--    atienda. Ahora el panel administra sus operadores.
--
-- Como el libro y como la bitacora de la Estacion, este rastro SOLO AGREGA: hay
-- un candado que revienta cualquier UPDATE o DELETE. Un registro de auditoria
-- que se puede editar no sirve para nada.
begin;

create table if not exists fiscal.eventos_panel (
  id bigserial primary key,
  en timestamptz not null default now(),
  quien text not null,
  desde text,
  accion text not null,
  detalle jsonb not null default '{}'::jsonb
);

comment on table fiscal.eventos_panel is 'Quien hizo que en el panel de la imprenta. Solo agrega: no se edita ni se borra.';
create index if not exists eventos_panel_en on fiscal.eventos_panel (en desc);

create or replace function fiscal.guard_eventos_panel_solo_agrega()
returns trigger
language plpgsql
as $fn$
begin
  raise exception 'El rastro del panel solo agrega: no se edita ni se borra';
end;
$fn$;

drop trigger if exists trg_eventos_panel_solo_agrega on fiscal.eventos_panel;
create trigger trg_eventos_panel_solo_agrega
  before update or delete on fiscal.eventos_panel
  for each row execute function fiscal.guard_eventos_panel_solo_agrega();

create or replace function fiscal.anotar_panel(p_quien text, p_desde text, p_accion text, p_detalle jsonb)
returns void
language sql
security definer
set search_path to 'fiscal', 'public'
as $fn$
  insert into fiscal.eventos_panel (quien, desde, accion, detalle)
  values (left(coalesce(nullif(btrim(p_quien), ''), 'sin identificar'), 60), p_desde, p_accion,
          coalesce(p_detalle, '{}'::jsonb));
$fn$;

create or replace function fiscal.movimientos_panel(p_limite int default 100)
returns table(en timestamptz, quien text, desde text, accion text, detalle jsonb)
language sql
stable
security definer
set search_path to 'fiscal', 'public'
as $fn$
  select e.en, e.quien, e.desde, e.accion, e.detalle
    from fiscal.eventos_panel e
   order by e.en desc
   limit least(coalesce(p_limite, 100), 500);
$fn$;

-- ===== LOS OPERADORES DE LA IMPRENTA =====
-- El panel ya tenia la tabla, pero no habia forma de administrarla sin SQL.
create or replace function fiscal.operadores_lista()
returns table(usuario text, nombre text, activo boolean, creado_en timestamptz, ultimo_acceso timestamptz)
language sql
stable
security definer
set search_path to 'fiscal', 'public'
as $fn$
  select o.usuario, o.nombre, o.activo, o.creado_en, o.ultimo_acceso
    from fiscal.operadores o
   order by o.activo desc, o.usuario;
$fn$;

create or replace function fiscal.guardar_operador(p_usuario text, p_nombre text, p_clave text)
returns text
language plpgsql
security definer
set search_path to 'fiscal', 'public', 'extensions'
as $fn$
declare v_usuario text := lower(btrim(coalesce(p_usuario, '')));
begin
  if v_usuario !~ '^[a-z0-9._-]{3,20}$' then
    raise exception 'El usuario va de 3 a 20 letras o numeros, sin espacios';
  end if;
  if btrim(coalesce(p_nombre, '')) = '' then
    raise exception 'Falta el nombre de la persona';
  end if;
  -- Una clave corta en un panel expuesto a internet no es una clave.
  if length(coalesce(p_clave, '')) < 12 then
    raise exception 'La clave debe tener al menos 12 caracteres';
  end if;

  insert into fiscal.operadores (usuario, clave_hash, nombre, activo)
  values (v_usuario, extensions.crypt(p_clave, extensions.gen_salt('bf', 10)), btrim(p_nombre), true)
  on conflict (usuario) do update
    set clave_hash = excluded.clave_hash, nombre = excluded.nombre, activo = true;
  return v_usuario;
end;
$fn$;

create or replace function fiscal.activar_operador(p_usuario text, p_activo boolean, p_yo text)
returns boolean
language plpgsql
security definer
set search_path to 'fiscal', 'public'
as $fn$
declare
  v_usuario text := lower(btrim(coalesce(p_usuario, '')));
  v_quedan int;
begin
  -- Dos candados para no quedarse afuera de su propia imprenta: nadie se apaga
  -- a si mismo, y nunca puede quedar cero operadores activos.
  if not p_activo then
    if v_usuario = lower(btrim(coalesce(p_yo, ''))) then
      raise exception 'No puedes apagarte a ti mismo';
    end if;
    select count(*) into v_quedan from fiscal.operadores where activo and usuario <> v_usuario;
    if v_quedan = 0 then
      raise exception 'Tiene que quedar al menos un operador activo';
    end if;
  end if;
  update fiscal.operadores set activo = p_activo where usuario = v_usuario;
  if not found then raise exception 'No existe ese operador'; end if;
  return p_activo;
end;
$fn$;

-- ===== LA PUERTA DEL PANEL, CON QUIEN Y DESDE DONDE =====
-- Cambia de firma (aparecen p_quien y p_desde, que los pone la Pages Function
-- desde la sesion y NUNCA el navegador), asi que se tumba la vieja primero.
drop function if exists public.fn_panel_imprenta(text, jsonb);
drop function if exists public.fn_panel_imprenta(text, jsonb, text, text);

create function public.fn_panel_imprenta(
  p_accion text,
  p_datos jsonb default '{}'::jsonb,
  p_quien text default null,
  p_desde text default null,
  -- El USUARIO del que esta operando (p_quien es su nombre, para mostrar). Se
  -- necesita aparte para que nadie se apague a si mismo: dos personas pueden
  -- llamarse igual, pero el usuario es unico.
  p_yo text default null
)
returns jsonb
language plpgsql
security definer
set search_path to 'public', 'fiscal'
as $fn$
declare
  v jsonb;
  v_quien text := coalesce(nullif(btrim(p_quien), ''), 'sin identificar');
begin
  case p_accion
    when 'entrar' then
      select to_jsonb(t) into v from fiscal.entrar_panel(
        p_datos->>'usuario', p_datos->>'clave', p_datos->>'desde') t;
    when 'tablero' then
      select to_jsonb(t) into v from fiscal.tablero() t;
    when 'clientes' then
      select coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) into v from fiscal.clientes() t;
    when 'guardar_cliente' then
      select to_jsonb(fiscal.guardar_cliente(
        p_datos->>'rif', p_datos->>'razon_social', p_datos->>'domicilio',
        p_datos->>'representante', p_datos->>'cedula', p_datos->>'correo',
        p_datos->>'telefono', p_datos->>'sistema', coalesce(p_datos->>'serie', 'A'),
        coalesce((p_datos->>'rif_comprobado')::boolean, false), p_datos->>'nota')) into v;
      perform fiscal.anotar_panel(v_quien, p_desde, 'cliente_guardado',
        jsonb_build_object('rif', p_datos->>'rif', 'razon_social', p_datos->>'razon_social'));
    when 'activar_cliente' then
      select to_jsonb(fiscal.activar_cliente(p_datos->>'rif', (p_datos->>'activo')::boolean)) into v;
      perform fiscal.anotar_panel(v_quien, p_desde,
        case when (p_datos->>'activo')::boolean then 'cliente_activado' else 'cliente_suspendido' end,
        jsonb_build_object('rif', p_datos->>'rif'));
    when 'llaves' then
      select coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) into v from fiscal.llaves_de(p_datos->>'rif') t;
    when 'crear_llave' then
      select to_jsonb(fiscal.crear_llave(p_datos->>'rif', coalesce(p_datos->>'nombre', 'llave'))) into v;
      perform fiscal.anotar_panel(v_quien, p_desde, 'llave_creada',
        jsonb_build_object('rif', p_datos->>'rif', 'nombre', p_datos->>'nombre'));
    when 'revocar_llave' then
      select to_jsonb(fiscal.revocar_llave((p_datos->>'id')::uuid)) into v;
      perform fiscal.anotar_panel(v_quien, p_desde, 'llave_revocada',
        jsonb_build_object('llave_id', p_datos->>'id'));
    when 'resumen_mensual' then
      select to_jsonb(t) into v from fiscal.resumen_mensual(
        p_datos->>'rif', (p_datos->>'anio')::int, (p_datos->>'mes')::int) t;
    when 'relacion_mensual' then
      select coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) into v from fiscal.relacion_mensual(
        p_datos->>'rif', (p_datos->>'anio')::int, (p_datos->>'mes')::int) t;
    when 'marcar_enviado' then
      -- Quien lo reporto sale de la sesion, no del navegador.
      select to_jsonb(fiscal.marcar_enviado(
        p_datos->>'rif', (p_datos->>'anio')::int, (p_datos->>'mes')::int,
        v_quien, p_datos->>'nota')) into v;
      perform fiscal.anotar_panel(v_quien, p_desde, 'periodo_reportado',
        jsonb_build_object('rif', p_datos->>'rif', 'anio', p_datos->>'anio', 'mes', p_datos->>'mes'));
    when 'negocios_gustito' then
      select coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) into v from fiscal.negocios_gustito() t;
    when 'buscar_negocio' then
      select coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) into v from fiscal.buscar_negocio(p_datos->>'texto') t;
    when 'vincular_negocio' then
      select fiscal.vincular_negocio((p_datos->>'restaurante_id')::uuid, p_datos->>'rif',
                                     p_datos->>'serie', v_quien, p_datos->>'nota') into v;
      perform fiscal.anotar_panel(v_quien, p_desde, 'negocio_habilitado',
        jsonb_build_object('restaurante_id', p_datos->>'restaurante_id', 'rif', p_datos->>'rif'));
    when 'desvincular_negocio' then
      select to_jsonb(fiscal.desvincular_negocio((p_datos->>'restaurante_id')::uuid)) into v;
      perform fiscal.anotar_panel(v_quien, p_desde, 'negocio_deshabilitado',
        jsonb_build_object('restaurante_id', p_datos->>'restaurante_id'));
    when 'estaciones' then
      select coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) into v from fiscal.estaciones_vivas() t;
    when 'cola' then
      select coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) into v from fiscal.cola_por_revisar() t;
    -- Rastro y operadores
    when 'movimientos' then
      select coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) into v
        from fiscal.movimientos_panel(coalesce((p_datos->>'limite')::int, 100)) t;
    when 'operadores' then
      select coalesce(jsonb_agg(to_jsonb(t)), '[]'::jsonb) into v from fiscal.operadores_lista() t;
    when 'guardar_operador' then
      select to_jsonb(fiscal.guardar_operador(
        p_datos->>'usuario', p_datos->>'nombre', p_datos->>'clave')) into v;
      perform fiscal.anotar_panel(v_quien, p_desde, 'operador_guardado',
        jsonb_build_object('usuario', lower(btrim(p_datos->>'usuario'))));
    when 'activar_operador' then
      select to_jsonb(fiscal.activar_operador(
        p_datos->>'usuario', (p_datos->>'activo')::boolean, p_yo)) into v;
      perform fiscal.anotar_panel(v_quien, p_desde,
        case when (p_datos->>'activo')::boolean then 'operador_activado' else 'operador_apagado' end,
        jsonb_build_object('usuario', lower(btrim(p_datos->>'usuario'))));
    else
      raise exception 'Accion desconocida';
  end case;
  return coalesce(v, 'null'::jsonb);
end;
$fn$;

commit;
