-- LA IMPRENTA: SU AUTORIZACION Y SUS RANGOS DE NUMERACION.
--
-- Hueco que encontro Gilberto (2026-07-30): "tenemos elegante para asignar al
-- usuario final pero la parte de nosotros donde queda". Cierto: el sistema
-- estaba armado completo para el CLIENTE (emisor) y de la IMPRENTA no habia ni
-- una fila. Faltaban tres cosas: los datos de la autorizacion, los rangos de
-- numeracion, y el techo (el numero de control se asignaba con un contador que
-- sumaba uno hasta el infinito, sin saber si estaba dentro de lo autorizado).
--
-- EL MODELO ES DE DOS NIVELES, que es como funciona de verdad:
--   1. El SENIAT le asigna RANGOS a la imprenta        -> fiscal.rangos_imprenta
--   2. La imprenta reparte SUB-RANGOS a cada cliente   -> fiscal.rangos_emisor
--   3. Cada documento toma el siguiente numero del sub-rango de SU cliente
--
-- Evidencia de que asi es: The Factory HKA, que YA esta autorizada, expone en
-- su API los metodos "AsignarNumeraciones (Asignacion de Numeros de Control por
-- Rangos)" y "ConsultaNumeraciones (rangos asignados de la empresa)". El modelo
-- de rangos no es una suposicion nuestra.
--
-- ⚠ COMPATIBILIDAD: hoy Gilberto NO es imprenta autorizada (opera bajo el
-- paraguas de HKA) y Arepa Power esta facturando. Un emisor SIN rango asignado
-- sigue numerando como hasta ahora. Los rangos entran en juego el dia que haya
-- autorizacion propia, sin migrar nada ni parar a nadie.

begin;

-- ===== 1. LA IMPRENTA: quien es y con que autorizacion opera =====
-- Una sola fila (candado abajo): esta instalacion es UNA imprenta.
create table if not exists fiscal.imprenta (
  id boolean primary key default true,
  rif text not null,
  razon_social text not null,
  domicilio_fiscal text not null,
  /** El numero de la Providencia Administrativa que la autoriza. */
  providencia text,
  providencia_fecha date,
  /** Si la autorizacion tiene vencimiento. Vacio = indefinida. */
  vigencia_hasta date,
  /** Falso mientras se tramita: se puede cargar todo antes de estar autorizada. */
  autorizada boolean not null default false,
  actualizado_en timestamptz not null default now(),
  constraint imprenta_una_sola check (id),
  constraint imprenta_rif_valido check (public.fn_rif_valido(rif))
);

-- ===== 2. LOS RANGOS QUE EL SENIAT LE ASIGNA A LA IMPRENTA =====
create table if not exists fiscal.rangos_imprenta (
  id uuid primary key default gen_random_uuid(),
  desde bigint not null,
  hasta bigint not null,
  /** Oficio o documento del SENIAT con el que llego el rango. */
  oficio text,
  asignado_en date not null default current_date,
  /** Un rango se anula si el SENIAT lo revoca; nunca se borra. */
  anulado boolean not null default false,
  nota text,
  creado_en timestamptz not null default now(),
  constraint rango_imprenta_ordenado check (hasta >= desde),
  constraint rango_imprenta_positivo check (desde > 0)
);

-- Dos rangos de la imprenta NO se pueden solapar: seria numerar dos veces el
-- mismo control. El candado lo pone la base, no la pantalla.
create extension if not exists btree_gist;
alter table fiscal.rangos_imprenta drop constraint if exists rangos_imprenta_sin_solape;
alter table fiscal.rangos_imprenta add constraint rangos_imprenta_sin_solape
  exclude using gist (int8range(desde, hasta, '[]') with &&) where (not anulado);

-- ===== 3. LOS SUB-RANGOS QUE LA IMPRENTA LE REPARTE A CADA CLIENTE =====
create table if not exists fiscal.rangos_emisor (
  id uuid primary key default gen_random_uuid(),
  emisor_id uuid not null references fiscal.emisores(id) on delete cascade,
  /** De cual rango de la imprenta salio este pedazo. */
  rango_imprenta_id uuid not null references fiscal.rangos_imprenta(id),
  desde bigint not null,
  hasta bigint not null,
  /** El proximo numero por entregar. Al pasar de `hasta`, el rango se agoto. */
  proximo bigint not null,
  entregado_en date not null default current_date,
  anulado boolean not null default false,
  creado_en timestamptz not null default now(),
  constraint rango_emisor_ordenado check (hasta >= desde),
  constraint rango_emisor_proximo_dentro check (proximo >= desde and proximo <= hasta + 1)
);

-- Tampoco se solapan entre clientes: un mismo numero de control no puede estar
-- prometido a dos negocios distintos.
alter table fiscal.rangos_emisor drop constraint if exists rangos_emisor_sin_solape;
alter table fiscal.rangos_emisor add constraint rangos_emisor_sin_solape
  exclude using gist (int8range(desde, hasta, '[]') with &&) where (not anulado);

create index if not exists rangos_emisor_por_emisor on fiscal.rangos_emisor (emisor_id) where not anulado;

-- Un sub-rango tiene que caber DENTRO del rango de la imprenta del que sale.
create or replace function fiscal.tg_subrango_cabe()
returns trigger
language plpgsql
as $$
declare r record;
begin
  select desde, hasta, anulado into r from fiscal.rangos_imprenta where id = new.rango_imprenta_id;
  if not found then raise exception 'Ese rango de la imprenta no existe'; end if;
  if r.anulado then raise exception 'Ese rango de la imprenta esta anulado'; end if;
  if new.desde < r.desde or new.hasta > r.hasta then
    raise exception 'El rango del cliente (% al %) no cabe en el rango autorizado (% al %)',
      new.desde, new.hasta, r.desde, r.hasta;
  end if;
  return new;
end $$;

drop trigger if exists tg_subrango_cabe on fiscal.rangos_emisor;
create trigger tg_subrango_cabe
  before insert or update of desde, hasta, rango_imprenta_id on fiscal.rangos_emisor
  for each row execute function fiscal.tg_subrango_cabe();

-- ===== 4. ASIGNAR EL NUMERO DE CONTROL, AHORA CON TECHO =====
create or replace function fiscal.asignar_control(p_emisor uuid, p_serie text)
returns bigint
language plpgsql
security definer
set search_path to 'fiscal', 'public'
as $function$
declare
  v_control bigint;
  v_rango   record;
begin
  -- ¿Este cliente tiene rango asignado por la imprenta?
  select id, desde, hasta, proximo into v_rango
    from fiscal.rangos_emisor
   where emisor_id = p_emisor and not anulado and proximo <= hasta
   order by desde
   limit 1
   for update;

  if found then
    -- Con rango: se entrega el siguiente y NO se pasa del techo. Si se acaba,
    -- se REVIENTA a proposito: emitir fuera del rango autorizado seria emitir
    -- un documento invalido, y eso es peor que no emitir.
    update fiscal.rangos_emisor
       set proximo = proximo + 1
     where id = v_rango.id
    returning v_rango.proximo into v_control;
    return v_control;
  end if;

  -- ¿Tenia rangos pero se le acabaron todos?
  if exists (select 1 from fiscal.rangos_emisor where emisor_id = p_emisor and not anulado) then
    raise exception 'Se agotaron los numeros de control asignados a este contribuyente. La imprenta debe asignarle un rango nuevo.';
  end if;

  -- Sin rangos configurados: numeracion libre, como hasta ahora. Es el caso
  -- mientras la imprenta opera bajo el paraguas de un tercero autorizado.
  insert into fiscal.series (emisor_id, serie, ultimo_control)
    values (p_emisor, p_serie, 0)
    on conflict (emisor_id, serie) do nothing;

  update fiscal.series
     set ultimo_control = ultimo_control + 1
   where emisor_id = p_emisor and serie = p_serie
  returning ultimo_control into v_control;

  if v_control is null then
    raise exception 'No se pudo asignar numero de control al emisor %', p_emisor;
  end if;
  return v_control;
end;
$function$;

-- ===== 5. LO QUE VE LA IMPRENTA EN SU PANEL =====

/** Cuanto le queda a la imprenta de lo que el SENIAT le autorizo. */
create or replace function fiscal.resumen_numeracion()
returns jsonb
language sql
stable security definer
set search_path to 'fiscal', 'public'
as $function$
  select jsonb_build_object(
    'autorizada', coalesce((select autorizada from fiscal.imprenta where id), false),
    'providencia', (select providencia from fiscal.imprenta where id),
    'providencia_fecha', (select providencia_fecha from fiscal.imprenta where id),
    'vigencia_hasta', (select vigencia_hasta from fiscal.imprenta where id),
    -- Numeros que el SENIAT autorizo en total.
    'autorizados', coalesce((select sum(hasta - desde + 1) from fiscal.rangos_imprenta where not anulado), 0),
    -- Los que ya estan repartidos entre clientes.
    'repartidos', coalesce((select sum(hasta - desde + 1) from fiscal.rangos_emisor where not anulado), 0),
    -- Los que los clientes ya gastaron.
    'usados', coalesce((select sum(proximo - desde) from fiscal.rangos_emisor where not anulado), 0),
    -- Lo que queda por repartir: si esto llega a cero, hay que pedirle mas al
    -- SENIAT ANTES de que un cliente se quede sin numeros.
    'por_repartir', coalesce((select sum(hasta - desde + 1) from fiscal.rangos_imprenta where not anulado), 0)
                  - coalesce((select sum(hasta - desde + 1) from fiscal.rangos_emisor where not anulado), 0)
  );
$function$;

/** Como va cada cliente con su rango: lo que le queda y si esta por acabarse. */
create or replace function fiscal.rangos_de_clientes()
returns table(serial text, rif text, razon_social text, desde bigint, hasta bigint,
              proximo bigint, quedan bigint, gastado_pct numeric, entregado_en date)
language sql
stable security definer
set search_path to 'fiscal', 'public'
as $function$
  select e.serial, e.rif, e.razon_social, r.desde, r.hasta, r.proximo,
         (r.hasta - r.proximo + 1) as quedan,
         round(((r.proximo - r.desde)::numeric / nullif(r.hasta - r.desde + 1, 0)) * 100, 1) as gastado_pct,
         r.entregado_en
    from fiscal.rangos_emisor r
    join fiscal.emisores e on e.id = r.emisor_id
   where not r.anulado
   order by (r.hasta - r.proximo + 1) asc;  -- primero al que menos le queda
$function$;

/**
 * Le entrega un rango a un cliente. Lo toma del rango de la imprenta indicado
 * y arranca donde quedo el ultimo entregado, para no dejar huecos ni pisar a
 * nadie. Devuelve el rango entregado.
 */
create or replace function fiscal.entregar_rango(p_rif text, p_cuantos bigint, p_rango_imprenta uuid default null)
returns jsonb
language plpgsql
security definer
set search_path to 'fiscal', 'public'
as $function$
declare
  v_emisor uuid;
  v_rango  record;
  v_desde  bigint;
  v_hasta  bigint;
begin
  if p_cuantos is null or p_cuantos < 1 then
    raise exception 'Indica cuantos numeros se le entregan';
  end if;
  select id into v_emisor from fiscal.emisores where rif = public.fn_rif_normalizado(p_rif);
  if v_emisor is null then raise exception 'Ese contribuyente no esta registrado'; end if;

  -- El rango de la imprenta: el indicado, o el primero con espacio libre.
  select ri.* into v_rango
    from fiscal.rangos_imprenta ri
   where not ri.anulado
     and (p_rango_imprenta is null or ri.id = p_rango_imprenta)
   order by ri.desde
   limit 1;
  if not found then raise exception 'La imprenta no tiene rangos autorizados cargados'; end if;

  -- Se arranca donde termino el ultimo pedazo repartido de ESE rango.
  select coalesce(max(re.hasta) + 1, v_rango.desde) into v_desde
    from fiscal.rangos_emisor re
   where re.rango_imprenta_id = v_rango.id and not re.anulado;
  v_hasta := v_desde + p_cuantos - 1;

  if v_hasta > v_rango.hasta then
    raise exception 'No alcanza: en ese rango autorizado quedan % numeros libres y estas pidiendo %',
      greatest(v_rango.hasta - v_desde + 1, 0), p_cuantos;
  end if;

  insert into fiscal.rangos_emisor (emisor_id, rango_imprenta_id, desde, hasta, proximo)
    values (v_emisor, v_rango.id, v_desde, v_hasta, v_desde);

  return jsonb_build_object('desde', v_desde, 'hasta', v_hasta, 'cuantos', p_cuantos);
end;
$function$;

/** Carga o actualiza los datos de la imprenta (una sola fila). */
create or replace function fiscal.guardar_imprenta(
  p_rif text, p_razon_social text, p_domicilio text,
  p_providencia text default null, p_providencia_fecha date default null,
  p_vigencia_hasta date default null, p_autorizada boolean default false)
returns jsonb
language plpgsql
security definer
set search_path to 'fiscal', 'public'
as $function$
begin
  if not public.fn_rif_valido(p_rif) then
    raise exception 'Ese RIF no es valido, revisa el ultimo digito';
  end if;
  -- Sin providencia no hay autorizacion que valga: no se deja marcar autorizada.
  if p_autorizada and coalesce(trim(p_providencia), '') = '' then
    raise exception 'Para marcarla autorizada hace falta el numero de la Providencia';
  end if;
  insert into fiscal.imprenta (id, rif, razon_social, domicilio_fiscal, providencia,
                               providencia_fecha, vigencia_hasta, autorizada, actualizado_en)
    values (true, public.fn_rif_normalizado(p_rif), trim(p_razon_social), trim(p_domicilio),
            nullif(trim(p_providencia), ''), p_providencia_fecha, p_vigencia_hasta, p_autorizada, now())
  on conflict (id) do update set
    rif = excluded.rif, razon_social = excluded.razon_social,
    domicilio_fiscal = excluded.domicilio_fiscal, providencia = excluded.providencia,
    providencia_fecha = excluded.providencia_fecha, vigencia_hasta = excluded.vigencia_hasta,
    autorizada = excluded.autorizada, actualizado_en = now();
  return fiscal.resumen_numeracion();
end;
$function$;

/** Carga un rango que asigno el SENIAT. */
create or replace function fiscal.cargar_rango_imprenta(p_desde bigint, p_hasta bigint, p_oficio text default null, p_nota text default null)
returns jsonb
language plpgsql
security definer
set search_path to 'fiscal', 'public'
as $function$
begin
  if p_desde is null or p_hasta is null or p_hasta < p_desde then
    raise exception 'El rango va del numero menor al mayor';
  end if;
  insert into fiscal.rangos_imprenta (desde, hasta, oficio, nota)
    values (p_desde, p_hasta, nullif(trim(p_oficio), ''), nullif(trim(p_nota), ''));
  return fiscal.resumen_numeracion();
end;
$function$;

commit;
