-- LA ANULACION VIAJA COMO SOLICITUD POR LA COLA (Bloque B, decidido 2026-07-29).
--
-- Gustito NUNCA escribe en el libro fiscal: pide, y la Estacion emite la nota
-- de credito. Dos vias de entrada:
--   1. El dueño o el encargado la piden desde el portal, con su motivo.
--   2. El sistema la pide SOLO cuando una venta YA SELLADA se cancela (el hueco
--      real de hoy: el libro quedaba diciendo que se vendio algo devuelto).
-- Si la venta cancelada todavia NO estaba sellada, no se pide nada: se descarta
-- de la cola con su rastro, porque una venta cancelada no debe facturarse.

-- 1. La cola aprende a descartar sin borrar (el rastro se queda).
alter table fiscal.cola_gustito drop constraint cola_gustito_estado_check;
alter table fiscal.cola_gustito add constraint cola_gustito_estado_check
  check (estado = any (array['pendiente'::text, 'sellado'::text, 'error'::text, 'descartada'::text]));

-- 2. Las solicitudes de anulacion, aparte de la cola de ventas a proposito:
--    una venta se sella una vez, una anulacion se pide una vez.
create table fiscal.anulaciones_gustito (
  id uuid primary key default gen_random_uuid(),
  restaurante_id uuid not null references public.restaurantes(id),
  emisor_id uuid not null references fiscal.emisores(id),
  tipo text not null check (tipo in ('pedido', 'cuenta')),
  origen_id uuid not null,
  motivo text not null,
  pedida_por text not null,
  estado text not null default 'pendiente' check (estado in ('pendiente', 'hecha', 'error')),
  creado_en timestamptz not null default now(),
  tomado_en timestamptz,
  hecha_en timestamptz,
  numero_nota bigint,
  intentos int not null default 0,
  ultimo_error text,
  unique (tipo, origen_id)
);
create index anulaciones_gustito_emisor_estado on fiscal.anulaciones_gustito (emisor_id, estado);
create index anulaciones_gustito_restaurante on fiscal.anulaciones_gustito (restaurante_id);

-- 3. El corazon: decidir que pasa segun el estado de la venta en la cola.
--    p_solo_sellada=true (portal): solo tiene sentido pedir nota de credito de
--    un documento que existe; si no esta sellado se responde claro y no se toca.
--    p_solo_sellada=false (cancelacion automatica): si aun no se sello, se
--    descarta de la cola; si ya se sello (o esta en manos de la Estacion), se
--    pide la nota de credito.
create or replace function fiscal.solicitar_anulacion(
  p_tipo text, p_origen_id uuid, p_restaurante_id uuid,
  p_motivo text, p_quien text, p_solo_sellada boolean
) returns jsonb
language plpgsql security definer set search_path to 'fiscal', 'public'
as $$
declare
  v_emisor uuid;
  v_cola fiscal.cola_gustito%rowtype;
begin
  select n.emisor_id into v_emisor
    from fiscal.negocios n where n.restaurante_id = p_restaurante_id;
  -- Sin vinculo con la imprenta no hay circuito fiscal que corregir.
  if v_emisor is null then return jsonb_build_object('ok', false, 'motivo', 'sin_vinculo'); end if;

  select * into v_cola from fiscal.cola_gustito
   where tipo = p_tipo and origen_id = p_origen_id;

  if v_cola.id is null then
    return jsonb_build_object('ok', false, 'motivo', 'sin_documento');
  end if;

  if v_cola.estado = 'descartada' then
    return jsonb_build_object('ok', true, 'accion', 'descartada');
  end if;

  if p_solo_sellada and v_cola.estado <> 'sellado' then
    return jsonb_build_object('ok', false, 'motivo', 'sin_sellar');
  end if;

  -- Venta que nunca llego a sellarse y ya no debe sellarse: se descarta con
  -- rastro. Si la Estacion ya la tomo, no se descarta (puede estar sellandola
  -- en este mismo segundo): se pide la nota de credito como con una sellada.
  if not p_solo_sellada and v_cola.estado in ('pendiente', 'error') and v_cola.tomado_en is null then
    update fiscal.cola_gustito
       set estado = 'descartada',
           ultimo_error = left('descartada: ' || p_motivo, 500)
     where id = v_cola.id and estado = v_cola.estado and tomado_en is null;
    if found then return jsonb_build_object('ok', true, 'accion', 'descartada'); end if;
    -- Se la llevaron entre la lectura y el update: cae al camino de solicitud.
  end if;

  insert into fiscal.anulaciones_gustito (restaurante_id, emisor_id, tipo, origen_id, motivo, pedida_por)
    values (p_restaurante_id, v_emisor, p_tipo, p_origen_id, p_motivo, p_quien)
    on conflict (tipo, origen_id) do nothing;
  if not found then
    return jsonb_build_object('ok', true, 'accion', 'ya_solicitada',
      'estado', (select estado from fiscal.anulaciones_gustito where tipo = p_tipo and origen_id = p_origen_id));
  end if;
  return jsonb_build_object('ok', true, 'accion', 'solicitada');
end;
$$;

-- 4. La puerta del portal: dueño o encargado piden la anulacion con su motivo.
--    Decidido por Gilberto 2026-07-29: ambos roles, y queda escrito quien fue.
create or replace function public.anular_venta_fiscal(
  p_restaurante uuid, p_tipo text, p_origen_id uuid, p_motivo text
) returns jsonb
language plpgsql security definer set search_path to 'public', 'fiscal'
as $$
declare
  v_motivo text;
  v_quien text;
begin
  if not (fn_es_dueno_de(p_restaurante) or fn_es_encargado_de(p_restaurante)) then
    raise exception 'Sin permiso';
  end if;
  if p_tipo not in ('pedido', 'cuenta') then raise exception 'Tipo invalido'; end if;
  v_motivo := nullif(trim(coalesce(p_motivo, '')), '');
  if v_motivo is null then raise exception 'La anulacion lleva su motivo, siempre'; end if;

  -- La venta tiene que ser de ESTE negocio: sin esto, cualquier dueño podria
  -- pedir anulaciones de ventas ajenas adivinando identificadores.
  if p_tipo = 'pedido' then
    if not exists (select 1 from pedidos where id = p_origen_id and restaurante_id = p_restaurante) then
      raise exception 'Esa venta no es de este negocio';
    end if;
  else
    if not exists (select 1 from cuentas_mesa where id = p_origen_id and restaurante_id = p_restaurante) then
      raise exception 'Esa venta no es de este negocio';
    end if;
  end if;

  v_quien := coalesce((select p.nombre from perfiles p where p.id = auth.uid()), 'portal')
    || case when fn_es_dueno_de(p_restaurante) then ' (dueño)' else ' (encargado)' end;

  return fiscal.solicitar_anulacion(p_tipo, p_origen_id, p_restaurante, v_motivo, v_quien, true);
end;
$$;

-- 5. La cancelacion automatica: si un pedido ya facturado se cancela, la nota
--    de credito se pide SOLA. Mismo espiritu que fiscal.encolar: un problema
--    aqui jamas tumba la cancelacion del pedido.
create or replace function public.fn_anular_pedido_fiscal()
returns trigger
language plpgsql security definer set search_path to 'public', 'fiscal'
as $$
begin
  if new.cuenta_id is null
     and new.estado = 'cancelado'
     and old.estado is distinct from 'cancelado' then
    begin
      perform fiscal.solicitar_anulacion(
        'pedido', new.id, new.restaurante_id,
        'Venta cancelada en el sistema de gestion', 'sistema', false);
    exception when others then
      null;
    end;
  end if;
  return new;
end;
$$;

drop trigger if exists tg_anular_pedido_fiscal on public.pedidos;
create trigger tg_anular_pedido_fiscal
  after update on public.pedidos
  for each row execute function public.fn_anular_pedido_fiscal();

-- 6. La Estacion HALA las solicitudes, igual que las ventas.
create or replace function fiscal.anulaciones_pendientes(p_llave text, p_rif text, p_limite integer default 20)
returns jsonb
language plpgsql security definer set search_path to 'fiscal', 'public'
as $$
declare
  v_emisor uuid;
  v_res jsonb;
begin
  v_emisor := fiscal.autenticar(p_llave, p_rif, 'estacion: anulaciones');
  with tomadas as (
    update fiscal.anulaciones_gustito a
       set tomado_en = now()
     where a.id in (
       select id from fiscal.anulaciones_gustito
        where emisor_id = v_emisor and estado = 'pendiente'
        order by creado_en
        limit greatest(1, least(coalesce(p_limite, 20), 100))
     )
    returning a.*
  )
  select coalesce(jsonb_agg(jsonb_build_object(
    'anulacion_id', t.id,
    'tipo', t.tipo,
    'origen_id', t.origen_id,
    'motivo', t.motivo,
    'pedida_por', t.pedida_por
  ) order by t.creado_en), '[]'::jsonb) into v_res from tomadas t;
  return v_res;
end;
$$;

-- 7. Y responde con el resultado: nota emitida, o su problema.
create or replace function fiscal.anulacion_resultado(
  p_llave text, p_rif text, p_id uuid, p_ok boolean, p_numero_nota bigint, p_error text
) returns jsonb
language plpgsql security definer set search_path to 'fiscal', 'public'
as $$
declare
  v_emisor uuid;
  v_intentos int;
begin
  v_emisor := fiscal.autenticar(p_llave, p_rif, 'estacion: anulacion_resultado');
  if p_ok then
    update fiscal.anulaciones_gustito
       set estado = 'hecha', hecha_en = now(), numero_nota = p_numero_nota, ultimo_error = null
     where id = p_id and emisor_id = v_emisor;
    if not found then raise exception 'Esa solicitud no es de este emisor'; end if;
    return jsonb_build_object('ok', true);
  end if;
  update fiscal.anulaciones_gustito
     set intentos = intentos + 1,
         ultimo_error = left(coalesce(p_error, 'sin detalle'), 500),
         tomado_en = null,
         estado = case when intentos + 1 >= 5 then 'error' else 'pendiente' end
   where id = p_id and emisor_id = v_emisor
  returning intentos into v_intentos;
  if not found then raise exception 'Esa solicitud no es de este emisor'; end if;
  return jsonb_build_object('ok', true, 'intentos', v_intentos);
end;
$$;

-- 8. El latido ahora tambien cuenta las anulaciones: son trabajo igual que las
--    ventas, y la Estacion solo hace la llamada pesada cuando hay algo.
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 $$
declare
  v_emisor uuid;
  v_serial text;
  v_pend int;
  v_anul int;
  v_ultima timestamptz;
  v_espera int;
begin
  v_emisor := fiscal.autenticar(p_llave, p_rif, 'estacion: latido');
  select serial into v_serial from fiscal.emisores where id = v_emisor;

  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';

  select count(*) into v_anul
    from fiscal.anulaciones_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 or v_anul > 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,
    'anulaciones_en_nube', v_anul,
    'habilitado', true,
    'serial', v_serial,
    'volver_en_segundos', v_espera
  );
end;
$$;

-- 9. La puerta unica de la Estacion aprende las dos acciones nuevas.
create or replace function public.fn_estacion(p_accion text, p_llave text, p_rif text, p_datos jsonb default '{}'::jsonb)
returns jsonb
language plpgsql security definer set search_path to 'public', 'fiscal'
as $$
begin
  case p_accion
    when 'pendientes' then
      return fiscal.ventas_pendientes(p_llave, p_rif, coalesce((p_datos->>'limite')::int, 50));
    when 'confirmar' then
      return fiscal.confirmar_sellado(p_llave, p_rif, (p_datos->>'cola_id')::uuid,
        nullif(p_datos->>'numero_documento','')::bigint,
        nullif(p_datos->>'numero_control','')::bigint,
        p_datos->>'token');
    when 'error' then
      return fiscal.reportar_error(p_llave, p_rif, (p_datos->>'cola_id')::uuid, p_datos->>'error');
    when 'anulaciones' then
      return fiscal.anulaciones_pendientes(p_llave, p_rif, coalesce((p_datos->>'limite')::int, 20));
    when 'anulacion_resultado' then
      return fiscal.anulacion_resultado(p_llave, p_rif, (p_datos->>'anulacion_id')::uuid,
        coalesce((p_datos->>'ok')::boolean, false),
        nullif(p_datos->>'numero_nota','')::bigint,
        p_datos->>'error');
    when 'latido' then
      return fiscal.latido(p_llave, p_rif, p_datos);
    when 'rescatar' then
      perform fiscal.autenticar(p_llave, p_rif, 'estacion: rescate');
      return jsonb_build_object('rescatadas', fiscal.rescatar_sin_encolar(coalesce((p_datos->>'horas')::int, 72)));
    when 'respaldo_hasta' then
      return fiscal.respaldo_hasta(p_llave, p_rif);
    when 'respaldo_guardar' then
      return fiscal.guardar_respaldo(p_llave, p_rif, coalesce(p_datos->'lineas', '[]'::jsonb));
    when 'respaldo_bajar' then
      return fiscal.bajar_respaldo(p_llave, p_rif, (p_datos->>'desde')::int, (p_datos->>'limite')::int);
    else
      raise exception 'Accion desconocida';
  end case;
end;
$$;
