-- =========================================================================
-- NEXUS · PERSONAS JS — Base de datos completa
--
-- Archivo generado automáticamente. NO editar a mano: se regenera en cada
-- compilación a partir de supabase/migrations/.
--
-- Cómo usarlo:
--   1. Entra a tu proyecto en supabase.com
--   2. Menú izquierdo → SQL Editor → New query
--   3. Pega TODO este archivo y presiona Run
--
-- Se puede ejecutar sobre una base vacía. Incluye 4 migraciones.
-- =========================================================================


-- ####################  0001_nucleo.sql  ####################

-- =========================================================================
-- NEXUS · PERSONAS JS — Migración 0001 · Núcleo del modelo de datos
--
-- Implementa las entidades de la sección 9 del documento de arquitectura.
-- Reglas que esta migración hace cumplir a nivel de base de datos:
--   · Todo dato cuelga de un tenant (aislamiento estricto, Fase 10 ya
--     preparada).
--   · El rol cliente NUNCA accede a datos crudos individuales.
--   · Los consentimientos y la auditoría son obligatorios (ISO 10667 /
--     EU AI Act).
--   · Los identificadores candidato–proceso son únicos.
--
-- Nombres de tabla en inglés: se conservan tal como los fija el documento de
-- arquitectura §9 para que el modelo sea rastreable contra él. Las columnas de
-- negocio y todos los comentarios van en español.
-- =========================================================================

create extension if not exists "pgcrypto";

-- -------------------------------------------------------------------------
-- Tipos enumerados
-- -------------------------------------------------------------------------

create type rol_usuario as enum ('admin', 'consultor', 'cliente', 'candidato');

create type estado_proceso as enum (
  'borrador',      -- creado, aún sin perfil de cargo
  'abierto',       -- recibiendo candidatos
  'evaluacion',    -- candidatos rindiendo
  'analisis',      -- matriz e informes en preparación
  'cerrado',       -- terna entregada
  'anulado'
);

create type estado_candidato as enum (
  'invitado',
  'en_curso',
  'rendido',
  'informado',
  'descartado',    -- SIEMPRE por decisión humana, nunca automática
  'seleccionado'
);

create type tipo_esquema as enum ('solicitud', 'perfil', 'candidato');

create type estado_informe as enum ('borrador', 'validado', 'entregado');

-- -------------------------------------------------------------------------
-- Tenants y usuarios
-- -------------------------------------------------------------------------

create table tenants (
  id          uuid primary key default gen_random_uuid(),
  nombre      text not null,
  -- Branding por tenant: logo, colores. Habilita el white-label (Fase 10)
  -- sin rehacer la arquitectura.
  branding    jsonb not null default '{}'::jsonb,
  plan        text not null default 'interno',
  creado_en   timestamptz not null default now()
);

comment on table tenants is
  'Empresa u operación aislada. Al inicio existe un único tenant (la consultoría propia).';

-- Perfil de aplicación ligado a la identidad de Supabase Auth.
create table users (
  id          uuid primary key references auth.users (id) on delete cascade,
  tenant_id   uuid not null references tenants (id) on delete cascade,
  email       text not null,
  nombre      text,
  role        rol_usuario not null default 'consultor',
  activo      boolean not null default true,
  creado_en   timestamptz not null default now()
);

create index on users (tenant_id);
create unique index on users (tenant_id, email);

comment on column users.role is
  'admin gestiona todo; consultor opera procesos; cliente ve solo lo suyo y nunca datos crudos.';

-- Empresas cliente atendidas por el tenant.
create table clients (
  id          uuid primary key default gen_random_uuid(),
  tenant_id   uuid not null references tenants (id) on delete cascade,
  nombre      text not null,
  rubro       text,
  contacto    jsonb not null default '{}'::jsonb,
  creado_en   timestamptz not null default now()
);

create index on clients (tenant_id);

-- Vincula un usuario con rol 'cliente' a las empresas que puede ver.
create table client_users (
  client_id   uuid not null references clients (id) on delete cascade,
  user_id     uuid not null references users (id) on delete cascade,
  primary key (client_id, user_id)
);

-- -------------------------------------------------------------------------
-- Configuración: esquemas de formulario y diccionario de factores
-- -------------------------------------------------------------------------

-- Formularios como esquemas versionados (principio config-driven §3.1).
-- Agregar un campo = insertar una versión nueva aquí. Nunca tocar código.
create table schemas (
  id          uuid primary key default gen_random_uuid(),
  tenant_id   uuid not null references tenants (id) on delete cascade,
  tipo        tipo_esquema not null,
  version     integer not null,
  definicion  jsonb not null,
  vigente     boolean not null default false,
  creado_por  uuid references users (id),
  creado_en   timestamptz not null default now(),
  unique (tenant_id, tipo, version)
);

create index on schemas (tenant_id, tipo);

-- Solo un esquema vigente por tipo y tenant.
create unique index schemas_vigente_unico
  on schemas (tenant_id, tipo)
  where vigente;

comment on table schemas is
  'Definición JSON de los formularios. Es CONFIGURACIÓN, no código: se edita desde el panel.';

-- Catálogo editable de factores, facetas y competencias (§3.3).
create table factor_dictionary (
  id          uuid primary key default gen_random_uuid(),
  tenant_id   uuid not null references tenants (id) on delete cascade,
  -- 'modulo' | 'dominio' | 'faceta' | 'competencia'
  tipo        text not null,
  codigo      text not null,
  nombre      text not null,
  descriptor  text,
  -- Dominio padre para las facetas; null en los demás casos.
  padre       text,
  -- Bandas, polos y textos interpretativos.
  bandas      jsonb not null default '{}'::jsonb,
  orden       integer not null default 0,
  activo      boolean not null default true,
  creado_en   timestamptz not null default now(),
  unique (tenant_id, tipo, codigo)
);

create index on factor_dictionary (tenant_id, tipo);

comment on table factor_dictionary is
  'Agregar un factor nuevo (cognitivo, de personalidad o competencia) se hace por datos, no por código.';

-- -------------------------------------------------------------------------
-- Procesos de selección
-- -------------------------------------------------------------------------

create table processes (
  id            uuid primary key default gen_random_uuid(),
  tenant_id     uuid not null references tenants (id) on delete cascade,
  client_id     uuid references clients (id) on delete set null,
  titulo_cargo  text not null,
  estado        estado_proceso not null default 'borrador',
  -- Respuestas crudas de la Ficha de Solicitud, según el esquema vigente.
  solicitud     jsonb not null default '{}'::jsonb,
  schema_id     uuid references schemas (id),
  fecha_inicio  date not null default current_date,
  fecha_cierre  date,
  creado_por    uuid references users (id),
  creado_en     timestamptz not null default now(),
  actualizado_en timestamptz not null default now()
);

create index on processes (tenant_id, estado);
create index on processes (client_id);

-- Perfil de éxito del cargo: qué se espera y cuánto pesa cada cosa.
create table job_profiles (
  id                uuid primary key default gen_random_uuid(),
  tenant_id         uuid not null references tenants (id) on delete cascade,
  process_id        uuid not null references processes (id) on delete cascade,
  -- Datos del cargo según el esquema de perfil vigente.
  datos             jsonb not null default '{}'::jsonb,
  -- { "V": {"min":3,"peso":0.10}, ... } — contrato perfil.json
  modulos           jsonb not null default '{}'::jsonb,
  -- { "C": {"orient":"alto","peso":0.40}, ... }
  personalidad      jsonb not null default '{}'::jsonb,
  facetas_clave     text[] not null default '{}',
  schema_id         uuid references schemas (id),
  version           integer not null default 1,
  creado_por        uuid references users (id),
  creado_en         timestamptz not null default now(),
  unique (process_id, version)
);

create index on job_profiles (tenant_id);

comment on column job_profiles.modulos is
  'Los pesos de cada bloque deben sumar 1. Se valida en la aplicación (validarPesos).';

-- -------------------------------------------------------------------------
-- Candidatos y evaluación
-- -------------------------------------------------------------------------

create table candidates (
  id            uuid primary key default gen_random_uuid(),
  tenant_id     uuid not null references tenants (id) on delete cascade,
  process_id    uuid not null references processes (id) on delete cascade,
  nombre        text not null,
  rut           text,
  email         text,
  telefono      text,
  cv_url        text,
  -- Token del link único de rendición. Se valida en el servidor, no por RLS.
  invite_token  text not null unique default encode(gen_random_bytes(24), 'hex'),
  token_expira  timestamptz,
  estado        estado_candidato not null default 'invitado',
  -- Datos adicionales según el esquema de candidato vigente.
  datos         jsonb not null default '{}'::jsonb,
  creado_en     timestamptz not null default now()
);

create index on candidates (tenant_id);
create index on candidates (process_id, estado);

-- Un mismo correo no puede aparecer dos veces en el mismo proceso
-- (regla: ID único candidato–proceso).
create unique index candidates_email_unico_por_proceso
  on candidates (process_id, lower(email))
  where email is not null;

comment on column candidates.invite_token is
  'Secreto de acceso del candidato. Nunca se muestra al rol cliente.';

-- Configuración de módulos activados para una rendición concreta.
create table assessments (
  id              uuid primary key default gen_random_uuid(),
  tenant_id       uuid not null references tenants (id) on delete cascade,
  candidate_id    uuid not null references candidates (id) on delete cascade,
  config_modulos  jsonb not null default '{}'::jsonb,
  iniciado_en     timestamptz,
  finalizado_en   timestamptz,
  -- Avance guardado automáticamente para que el candidato pueda pausar.
  avance          jsonb not null default '{}'::jsonb,
  creado_en       timestamptz not null default now()
);

create index on assessments (tenant_id);
create unique index on assessments (candidate_id);

-- Datos brutos: una fila por ítem respondido. El rol cliente jamás los ve.
create table responses (
  id            uuid primary key default gen_random_uuid(),
  tenant_id     uuid not null references tenants (id) on delete cascade,
  candidate_id  uuid not null references candidates (id) on delete cascade,
  modulo        text not null,
  item_id       text not null,
  respuesta     jsonb not null,
  -- Milisegundos empleados. Insumo del control de integridad (Fase 7).
  tiempo_ms     integer,
  score         numeric,
  creado_en     timestamptz not null default now(),
  unique (candidate_id, modulo, item_id)
);

create index on responses (tenant_id);
create index on responses (candidate_id, modulo);

comment on table responses is
  'DATOS CRUDOS. Acceso restringido a admin y consultor. Nunca al cliente.';

-- Resultados calculados: lo que consumen el informe y la matriz.
create table results (
  id                  uuid primary key default gen_random_uuid(),
  tenant_id           uuid not null references tenants (id) on delete cascade,
  candidate_id        uuid not null references candidates (id) on delete cascade,
  scores_modulo       jsonb not null default '{}'::jsonb,
  big_five_dominios   jsonb not null default '{}'::jsonb,
  big_five_facetas    jsonb not null default '{}'::jsonb,
  bandas_1_7          jsonb not null default '{}'::jsonb,
  -- Recordatorio permanente e inseparable del dato.
  sin_baremo          boolean not null default true,
  calculado_en        timestamptz not null default now(),
  unique (candidate_id)
);

create index on results (tenant_id);

comment on column results.sin_baremo is
  'Siempre verdadero mientras no exista grupo normativo (Fase 9). Los puntajes no son percentiles.';

-- -------------------------------------------------------------------------
-- Match Engine
-- -------------------------------------------------------------------------

create table match_scores (
  id                uuid primary key default gen_random_uuid(),
  tenant_id         uuid not null references tenants (id) on delete cascade,
  candidate_id      uuid not null references candidates (id) on delete cascade,
  job_profile_id    uuid not null references job_profiles (id) on delete cascade,
  global            numeric not null,
  cognitivo         numeric not null,
  personalidad      numeric not null,
  banda             integer not null check (banda between 1 and 7),
  por_competencia   jsonb not null default '{}'::jsonb,
  -- Rastro completo: qué se esperaba, qué obtuvo, peso y aporte por factor.
  explicacion       jsonb not null default '{}'::jsonb,
  -- Versión del motor que produjo el número, para poder recalcular histórico.
  motor_version     text not null default '1.0.0',
  calculado_en      timestamptz not null default now(),
  unique (candidate_id, job_profile_id)
);

create index on match_scores (tenant_id);
create index on match_scores (job_profile_id, global desc);

comment on table match_scores is
  'El índice ORDENA y RECOMIENDA. La decisión es del consultor: ningún descarte automático.';

-- -------------------------------------------------------------------------
-- Informes
-- -------------------------------------------------------------------------

-- Informes como plantillas versionadas (§3.2).
create table report_templates (
  id          uuid primary key default gen_random_uuid(),
  tenant_id   uuid not null references tenants (id) on delete cascade,
  nombre      text not null,
  -- 'candidato' | 'manager' | 'experto' — mismo dato, distinta plantilla.
  audiencia   text not null default 'experto',
  version     integer not null,
  estructura  jsonb not null,
  vigente     boolean not null default false,
  creado_en   timestamptz not null default now(),
  unique (tenant_id, nombre, version)
);

create index on report_templates (tenant_id);

create table reports (
  id            uuid primary key default gen_random_uuid(),
  tenant_id     uuid not null references tenants (id) on delete cascade,
  candidate_id  uuid not null references candidates (id) on delete cascade,
  template_id   uuid references report_templates (id),
  xlsx_url      text,
  pdf_url       text,
  narrativa     jsonb not null default '{}'::jsonb,
  estado        estado_informe not null default 'borrador',
  validado_por  uuid references users (id),
  validado_en   timestamptz,
  creado_en     timestamptz not null default now()
);

create index on reports (tenant_id);
create index on reports (candidate_id);

comment on column reports.estado is
  'Nace en borrador. Solo una persona lo pasa a validado: human-in-the-loop obligatorio.';

-- -------------------------------------------------------------------------
-- Bucle de validez (Fase 9)
-- -------------------------------------------------------------------------

create table performance_outcomes (
  id            uuid primary key default gen_random_uuid(),
  tenant_id     uuid not null references tenants (id) on delete cascade,
  candidate_id  uuid not null references candidates (id) on delete cascade,
  metrica       text not null,
  valor         numeric not null,
  fecha         date not null,
  fuente        text,
  creado_en     timestamptz not null default now()
);

create index on performance_outcomes (tenant_id);
create index on performance_outcomes (candidate_id);

comment on table performance_outcomes is
  'Desempeño real posterior (6-12 meses). Es el activo que valida la tesis del producto.';

-- -------------------------------------------------------------------------
-- Gobernanza: consentimientos y auditoría
-- -------------------------------------------------------------------------

create table consents (
  id            uuid primary key default gen_random_uuid(),
  tenant_id     uuid not null references tenants (id) on delete cascade,
  candidate_id  uuid not null references candidates (id) on delete cascade,
  tipo          text not null,
  version_texto text not null,
  -- Texto íntegro aceptado, para poder demostrar qué se consintió.
  texto         text not null,
  aceptado      boolean not null,
  ip_hash       text,
  creado_en     timestamptz not null default now()
);

create index on consents (tenant_id);
create index on consents (candidate_id);

comment on table consents is
  'Consentimiento explícito previo (lógica GDPR). Se guarda el texto exacto aceptado.';

create table audit_logs (
  id          uuid primary key default gen_random_uuid(),
  tenant_id   uuid not null references tenants (id) on delete cascade,
  actor_id    uuid references users (id),
  accion      text not null,
  entidad     text not null,
  entidad_id  uuid,
  metadata    jsonb not null default '{}'::jsonb,
  creado_en   timestamptz not null default now()
);

create index on audit_logs (tenant_id, creado_en desc);
create index on audit_logs (entidad, entidad_id);

comment on table audit_logs is
  'Registro de acciones sensibles. Requisito de EU AI Act e ISO 10667. Nunca guarda contenido de crisis ni datos crudos.';

-- -------------------------------------------------------------------------
-- Mantenimiento de actualizado_en
-- -------------------------------------------------------------------------

create or replace function tocar_actualizado_en()
returns trigger
language plpgsql
as $$
begin
  new.actualizado_en := now();
  return new;
end;
$$;

create trigger processes_actualizado
  before update on processes
  for each row execute function tocar_actualizado_en();


-- ####################  0002_rls.sql  ####################

-- =========================================================================
-- NEXUS · PERSONAS JS — Migración 0002 · Row-Level Security multi-tenant
--
-- Aislamiento estricto por tenant y por rol, aplicado por la base de datos.
-- Aunque la aplicación tuviera un error, la base NO entrega datos de otro
-- tenant ni datos crudos individuales al rol cliente.
--
-- Matriz de acceso:
--   admin     → todo lo de su tenant.
--   consultor → todo lo de su tenant (opera los procesos).
--   cliente   → solo los procesos de las empresas que tiene asignadas, y
--               SOLO datos agregados/informes. Nunca responses ni results.
--   candidato → no accede por RLS. Entra por token único, validado en el
--               servidor; la escritura de su rendición usa la clave de
--               servicio con el token ya verificado.
-- =========================================================================

-- -------------------------------------------------------------------------
-- Funciones de contexto
--
-- SECURITY DEFINER: se ejecutan con los permisos del dueño y por eso pueden
-- leer `users` sin disparar recursión de políticas. `search_path` fijo para
-- que no puedan ser secuestradas.
-- -------------------------------------------------------------------------

create schema if not exists app;

create or replace function app.tenant_actual()
returns uuid
language sql
stable
security definer
set search_path = public, pg_temp
as $$
  select tenant_id from public.users where id = auth.uid() and activo;
$$;

create or replace function app.rol_actual()
returns rol_usuario
language sql
stable
security definer
set search_path = public, pg_temp
as $$
  select role from public.users where id = auth.uid() and activo;
$$;

-- Staff = quien opera la plataforma. Ve datos crudos.
create or replace function app.es_staff()
returns boolean
language sql
stable
security definer
set search_path = public, pg_temp
as $$
  select coalesce(
    (select role in ('admin', 'consultor')
       from public.users where id = auth.uid() and activo),
    false);
$$;

create or replace function app.es_admin()
returns boolean
language sql
stable
security definer
set search_path = public, pg_temp
as $$
  select coalesce(
    (select role = 'admin' from public.users where id = auth.uid() and activo),
    false);
$$;

-- Procesos que el usuario con rol cliente tiene permitido ver.
create or replace function app.procesos_visibles_cliente()
returns setof uuid
language sql
stable
security definer
set search_path = public, pg_temp
as $$
  select p.id
    from public.processes p
    join public.client_users cu on cu.client_id = p.client_id
   where cu.user_id = auth.uid();
$$;

-- Verdadero si el candidato pertenece a un proceso visible por el cliente.
create or replace function app.candidato_visible_cliente(cand uuid)
returns boolean
language sql
stable
security definer
set search_path = public, pg_temp
as $$
  select exists (
    select 1
      from public.candidates c
     where c.id = cand
       and c.process_id in (select app.procesos_visibles_cliente())
  );
$$;

-- -------------------------------------------------------------------------
-- Activar RLS en todas las tablas
-- -------------------------------------------------------------------------

alter table tenants              enable row level security;
alter table users                enable row level security;
alter table clients              enable row level security;
alter table client_users         enable row level security;
alter table schemas              enable row level security;
alter table factor_dictionary    enable row level security;
alter table processes            enable row level security;
alter table job_profiles         enable row level security;
alter table candidates           enable row level security;
alter table assessments          enable row level security;
alter table responses            enable row level security;
alter table results              enable row level security;
alter table match_scores         enable row level security;
alter table report_templates     enable row level security;
alter table reports              enable row level security;
alter table performance_outcomes enable row level security;
alter table consents             enable row level security;
alter table audit_logs           enable row level security;

-- Sin política que lo permita, nada pasa. Ese es el punto de partida seguro.

-- -------------------------------------------------------------------------
-- Tenants y usuarios
-- -------------------------------------------------------------------------

create policy tenants_lectura on tenants
  for select using (id = app.tenant_actual());

create policy tenants_admin on tenants
  for update using (id = app.tenant_actual() and app.es_admin());

-- Cada quien se ve a sí mismo; el staff ve a los usuarios de su tenant.
create policy users_lectura on users
  for select using (
    id = auth.uid()
    or (tenant_id = app.tenant_actual() and app.es_staff())
  );

create policy users_admin_escribe on users
  for all using (tenant_id = app.tenant_actual() and app.es_admin())
  with check (tenant_id = app.tenant_actual() and app.es_admin());

-- -------------------------------------------------------------------------
-- Clientes
-- -------------------------------------------------------------------------

create policy clients_staff on clients
  for all using (tenant_id = app.tenant_actual() and app.es_staff())
  with check (tenant_id = app.tenant_actual() and app.es_staff());

-- El usuario cliente ve solo la ficha de su propia empresa.
create policy clients_lectura_cliente on clients
  for select using (
    tenant_id = app.tenant_actual()
    and exists (
      select 1 from client_users cu
       where cu.client_id = clients.id and cu.user_id = auth.uid()
    )
  );

create policy client_users_staff on client_users
  for all using (app.es_staff()) with check (app.es_staff());

create policy client_users_propio on client_users
  for select using (user_id = auth.uid());

-- -------------------------------------------------------------------------
-- Configuración: esquemas y diccionario
--
-- Todo el tenant puede LEER la configuración vigente (la necesita para
-- renderizar formularios e informes). Solo el staff la modifica.
-- -------------------------------------------------------------------------

create policy schemas_lectura on schemas
  for select using (tenant_id = app.tenant_actual());

create policy schemas_escritura on schemas
  for all using (tenant_id = app.tenant_actual() and app.es_staff())
  with check (tenant_id = app.tenant_actual() and app.es_staff());

create policy diccionario_lectura on factor_dictionary
  for select using (tenant_id = app.tenant_actual());

create policy diccionario_escritura on factor_dictionary
  for all using (tenant_id = app.tenant_actual() and app.es_staff())
  with check (tenant_id = app.tenant_actual() and app.es_staff());

create policy plantillas_lectura on report_templates
  for select using (tenant_id = app.tenant_actual());

create policy plantillas_escritura on report_templates
  for all using (tenant_id = app.tenant_actual() and app.es_staff())
  with check (tenant_id = app.tenant_actual() and app.es_staff());

-- -------------------------------------------------------------------------
-- Procesos y perfiles de cargo
-- -------------------------------------------------------------------------

create policy processes_staff on processes
  for all using (tenant_id = app.tenant_actual() and app.es_staff())
  with check (tenant_id = app.tenant_actual() and app.es_staff());

create policy processes_lectura_cliente on processes
  for select using (
    tenant_id = app.tenant_actual()
    and id in (select app.procesos_visibles_cliente())
  );

create policy perfiles_staff on job_profiles
  for all using (tenant_id = app.tenant_actual() and app.es_staff())
  with check (tenant_id = app.tenant_actual() and app.es_staff());

create policy perfiles_lectura_cliente on job_profiles
  for select using (
    tenant_id = app.tenant_actual()
    and process_id in (select app.procesos_visibles_cliente())
  );

-- -------------------------------------------------------------------------
-- Candidatos
--
-- El cliente ve que existen candidatos y su estado, pero el token de
-- invitación y los datos de contacto se filtran en la capa de aplicación
-- mediante una vista restringida (ver 0003).
-- -------------------------------------------------------------------------

create policy candidates_staff on candidates
  for all using (tenant_id = app.tenant_actual() and app.es_staff())
  with check (tenant_id = app.tenant_actual() and app.es_staff());

create policy candidates_lectura_cliente on candidates
  for select using (
    tenant_id = app.tenant_actual()
    and process_id in (select app.procesos_visibles_cliente())
  );

create policy assessments_staff on assessments
  for all using (tenant_id = app.tenant_actual() and app.es_staff())
  with check (tenant_id = app.tenant_actual() and app.es_staff());

-- -------------------------------------------------------------------------
-- DATOS CRUDOS — solo staff. El cliente no tiene ninguna política de lectura,
-- por lo tanto la base de datos le devuelve cero filas.
-- -------------------------------------------------------------------------

create policy responses_staff on responses
  for all using (tenant_id = app.tenant_actual() and app.es_staff())
  with check (tenant_id = app.tenant_actual() and app.es_staff());

create policy results_staff on results
  for all using (tenant_id = app.tenant_actual() and app.es_staff())
  with check (tenant_id = app.tenant_actual() and app.es_staff());

-- -------------------------------------------------------------------------
-- Match scores e informes
--
-- El cliente sí ve el índice de idoneidad y los informes validados: es el
-- producto que compra. No ve el dato crudo que hay detrás.
-- -------------------------------------------------------------------------

create policy match_staff on match_scores
  for all using (tenant_id = app.tenant_actual() and app.es_staff())
  with check (tenant_id = app.tenant_actual() and app.es_staff());

create policy match_lectura_cliente on match_scores
  for select using (
    tenant_id = app.tenant_actual()
    and app.candidato_visible_cliente(candidate_id)
  );

create policy reports_staff on reports
  for all using (tenant_id = app.tenant_actual() and app.es_staff())
  with check (tenant_id = app.tenant_actual() and app.es_staff());

-- El cliente solo ve informes que una persona ya validó. Nunca borradores.
create policy reports_lectura_cliente on reports
  for select using (
    tenant_id = app.tenant_actual()
    and estado in ('validado', 'entregado')
    and app.candidato_visible_cliente(candidate_id)
  );

-- -------------------------------------------------------------------------
-- Gobernanza
-- -------------------------------------------------------------------------

create policy performance_staff on performance_outcomes
  for all using (tenant_id = app.tenant_actual() and app.es_staff())
  with check (tenant_id = app.tenant_actual() and app.es_staff());

create policy consents_staff on consents
  for all using (tenant_id = app.tenant_actual() and app.es_staff())
  with check (tenant_id = app.tenant_actual() and app.es_staff());

-- La auditoría se lee, no se edita. Nadie puede borrar ni modificar un
-- registro de auditoría desde la aplicación.
create policy audit_lectura on audit_logs
  for select using (tenant_id = app.tenant_actual() and app.es_staff());

create policy audit_insercion on audit_logs
  for insert with check (tenant_id = app.tenant_actual());


-- ####################  0003_vistas_y_siembra.sql  ####################

-- =========================================================================
-- NEXUS · PERSONAS JS — Migración 0003 · Vistas restringidas y siembra
--
-- Dos cosas:
--   1. Una vista que el rol cliente puede consultar sin exponer el token de
--      invitación ni los datos de contacto del candidato.
--   2. La función `app.sembrar_tenant`, que crea un tenant nuevo con su
--      diccionario de factores completo (6 módulos + 5 dominios + 30 facetas).
--      Los ESQUEMAS de formulario los siembra la aplicación, porque su fuente
--      es `src/domain/esquemas-semilla.ts`.
-- =========================================================================

-- -------------------------------------------------------------------------
-- 1 · Vista de candidatos para el rol cliente
--
-- `security_invoker` hace que la vista respete las políticas RLS de quien
-- consulta, en lugar de las del creador. Sin esa opción, una vista es un
-- agujero que salta la seguridad de fila.
-- -------------------------------------------------------------------------

create view candidatos_vista_cliente
with (security_invoker = true)
as
select
  c.id,
  c.tenant_id,
  c.process_id,
  c.nombre,
  c.estado,
  c.creado_en,
  m.global        as idoneidad_global,
  m.banda         as idoneidad_banda,
  -- El cliente ve si hay un informe disponible, no su contenido en borrador.
  exists (
    select 1 from reports r
     where r.candidate_id = c.id
       and r.estado in ('validado', 'entregado')
  ) as informe_disponible
from candidates c
left join match_scores m on m.candidate_id = c.id;

comment on view candidatos_vista_cliente is
  'Proyección sin token de invitación, RUT, correo ni teléfono. Es lo máximo que el rol cliente puede ver de un candidato.';

-- -------------------------------------------------------------------------
-- 2 · Siembra del diccionario de factores para un tenant
-- -------------------------------------------------------------------------

create or replace function app.sembrar_diccionario(p_tenant uuid)
returns void
language plpgsql
security definer
set search_path = public, pg_temp
as $$
begin
  -- Los 6 módulos de la batería.
  insert into factor_dictionary (tenant_id, tipo, codigo, nombre, descriptor, orden)
  values
    (p_tenant, 'modulo', 'V',  'Razonamiento verbal',
     'Comprender información escrita, inferir lo implícito y razonar con lenguaje.', 1),
    (p_tenant, 'modulo', 'S',  'Series de letras y números',
     'Detectar reglas y regularidades en secuencias. Razonamiento inductivo.', 2),
    (p_tenant, 'modulo', 'M',  'Razonamiento figural (matricial)',
     'Razonamiento abstracto no verbal. Mejor estimador individual de capacidad general.', 3),
    (p_tenant, 'modulo', 'R',  'Rotación tridimensional',
     'Capacidad visoespacial: representar y rotar objetos mentalmente.', 4),
    (p_tenant, 'modulo', 'HB', 'Habilidades blandas',
     'Juicio declarado ante situaciones laborales cotidianas (SJT).', 5),
    (p_tenant, 'modulo', 'LG', 'Liderazgo y gestión',
     'Juicio declarado ante situaciones de conducción de equipos (SJT).', 6)
  on conflict (tenant_id, tipo, codigo) do nothing;

  -- Los 5 dominios Big Five (estructura IPIP-NEO, dominio público).
  insert into factor_dictionary (tenant_id, tipo, codigo, nombre, descriptor, orden)
  values
    (p_tenant, 'dominio', 'O', 'Apertura a la experiencia',
     'Curiosidad, interés por ideas y matices frente a preferencia por lo conocido.', 1),
    (p_tenant, 'dominio', 'C', 'Responsabilidad',
     'Orden, método y orientación al cumplimiento frente a flexibilidad y espontaneidad.', 2),
    (p_tenant, 'dominio', 'E', 'Extraversión',
     'Sociabilidad e iniciativa interpersonal frente a reserva y trabajo individual.', 3),
    (p_tenant, 'dominio', 'A', 'Amabilidad',
     'Cooperación y búsqueda de acuerdo frente a firmeza y competitividad.', 4),
    (p_tenant, 'dominio', 'N', 'Reacciones emocionales',
     'Reactividad ante la presión frente a estabilidad y recuperación rápida.', 5)
  on conflict (tenant_id, tipo, codigo) do nothing;

  -- Las 30 facetas, colgando de su dominio.
  insert into factor_dictionary (tenant_id, tipo, codigo, nombre, padre, orden)
  values
    (p_tenant, 'faceta', 'fantasia',       'Fantasía',              'O', 1),
    (p_tenant, 'faceta', 'estetica',       'Estética',              'O', 2),
    (p_tenant, 'faceta', 'sentimientos',   'Sentimientos',          'O', 3),
    (p_tenant, 'faceta', 'acciones',       'Acciones',              'O', 4),
    (p_tenant, 'faceta', 'ideas',          'Ideas',                 'O', 5),
    (p_tenant, 'faceta', 'valores',        'Valores',               'O', 6),
    (p_tenant, 'faceta', 'competencia',    'Competencia',           'C', 7),
    (p_tenant, 'faceta', 'orden',          'Orden',                 'C', 8),
    (p_tenant, 'faceta', 'deber',          'Sentido del deber',     'C', 9),
    (p_tenant, 'faceta', 'logro',          'Necesidad de logro',    'C', 10),
    (p_tenant, 'faceta', 'autodisciplina', 'Autodisciplina',        'C', 11),
    (p_tenant, 'faceta', 'deliberacion',   'Deliberación',          'C', 12),
    (p_tenant, 'faceta', 'afecto',         'Afecto',                'E', 13),
    (p_tenant, 'faceta', 'gregarismo',     'Gregarismo',            'E', 14),
    (p_tenant, 'faceta', 'asertividad',    'Asertividad',           'E', 15),
    (p_tenant, 'faceta', 'actividad',      'Actividad',             'E', 16),
    (p_tenant, 'faceta', 'sensaciones',    'Búsqueda de sensaciones','E', 17),
    (p_tenant, 'faceta', 'alegria',        'Alegría',               'E', 18),
    (p_tenant, 'faceta', 'confianza',      'Confianza',             'A', 19),
    (p_tenant, 'faceta', 'franqueza',      'Franqueza',             'A', 20),
    (p_tenant, 'faceta', 'altruismo',      'Altruismo',             'A', 21),
    (p_tenant, 'faceta', 'conciliacion',   'Conciliación',          'A', 22),
    (p_tenant, 'faceta', 'modestia',       'Modestia',              'A', 23),
    (p_tenant, 'faceta', 'sensibilidad',   'Sensibilidad',          'A', 24),
    (p_tenant, 'faceta', 'ansiedad',       'Ansiedad',              'N', 25),
    (p_tenant, 'faceta', 'hostilidad',     'Hostilidad',            'N', 26),
    (p_tenant, 'faceta', 'depresion',      'Desánimo',              'N', 27),
    (p_tenant, 'faceta', 'ansiedadsocial', 'Ansiedad social',       'N', 28),
    (p_tenant, 'faceta', 'impulsividad',   'Impulsividad',          'N', 29),
    (p_tenant, 'faceta', 'vulnerabilidad', 'Vulnerabilidad',        'N', 30)
  on conflict (tenant_id, tipo, codigo) do nothing;
end;
$$;

comment on function app.sembrar_diccionario is
  'Carga el catálogo base. A partir de aquí el diccionario se edita desde el panel, no por migración.';

-- Crea un tenant nuevo con su diccionario listo. Devuelve su id.
create or replace function app.sembrar_tenant(p_nombre text)
returns uuid
language plpgsql
security definer
set search_path = public, pg_temp
as $$
declare
  v_id uuid;
begin
  insert into tenants (nombre) values (p_nombre) returning id into v_id;
  perform app.sembrar_diccionario(v_id);
  return v_id;
end;
$$;

-- -------------------------------------------------------------------------
-- 3 · Alta del primer usuario
--
-- Supabase Auth crea la identidad; esta función crea su perfil de aplicación.
-- Se invoca una sola vez, desde el asistente de puesta en marcha.
-- -------------------------------------------------------------------------

create or replace function app.registrar_usuario(
  p_auth_id uuid,
  p_tenant  uuid,
  p_email   text,
  p_nombre  text,
  p_rol     rol_usuario
)
returns void
language sql
security definer
set search_path = public, pg_temp
as $$
  insert into public.users (id, tenant_id, email, nombre, role)
  values (p_auth_id, p_tenant, p_email, p_nombre, p_rol)
  on conflict (id) do update
    set tenant_id = excluded.tenant_id,
        email     = excluded.email,
        nombre    = excluded.nombre,
        role      = excluded.role;
$$;


-- ####################  0004_integridad.sql  ####################

-- =========================================================================
-- NEXUS · PERSONAS JS — Migración 0004 · Control de integridad
--
-- Agrega dónde guardar la evaluación de integridad de cada rendición: si se
-- puede confiar en ella, y por qué. Sin esto, un informe no es defendible ante
-- la pregunta "¿y si respondió al azar?".
--
-- Es una migración ADITIVA e IDEMPOTENTE: se puede ejecutar sobre una base que
-- ya está en uso, y ejecutarla dos veces no rompe nada.
-- =========================================================================

alter table results
  add column if not exists integridad jsonb not null default '{}'::jsonb;

comment on column results.integridad is
  'Evaluación de integridad de la rendición: indicadores, alertas y veredicto. Orienta la revisión humana; nunca invalida ni descarta por sí sola.';

-- Índice para poder listar rápido las rendiciones que merecen revisión.
create index if not exists results_integridad_veredicto
  on results ((integridad ->> 'veredicto'));

-- -------------------------------------------------------------------------
-- Analítica de equidad
--
-- Para monitorear impacto adverso hace falta saber a qué grupo pertenece cada
-- persona, y eso es dato sensible. Se guarda **solo lo que el candidato
-- declara voluntariamente** y ya vive en `candidates.datos`; aquí no se crea
-- ninguna columna nueva de datos personales.
--
-- Esta vista agrega y no expone individuos: entrega conteos por grupo.
-- -------------------------------------------------------------------------

create or replace view equidad_por_proceso
with (security_invoker = true)
as
select
  c.tenant_id,
  c.process_id,
  coalesce(nullif(c.datos ->> 'genero', ''), 'sin declarar') as grupo,
  count(*)                                          as evaluados,
  count(*) filter (where c.estado = 'seleccionado')  as seleccionados,
  count(*) filter (where c.estado = 'descartado')    as descartados
from candidates c
group by c.tenant_id, c.process_id, coalesce(nullif(c.datos ->> 'genero', ''), 'sin declarar');

comment on view equidad_por_proceso is
  'Conteos por grupo declarado para el análisis de impacto adverso (regla de los 4/5). No expone individuos.';
