-- =====================================================
-- SCHEMA: auth (Extensiones)
-- PROPÓSITO: 2FA, API Keys, OAuth2, Grupos, ACL, Record Rules
-- MÓDULOS: MGN-001 (Fundamentos), MGN-002 (Usuarios), MGN-003 (Roles)
-- FECHA: 2025-12-08
-- VERSION: 1.0.0
-- DEPENDENCIAS: 01-auth.sql
-- SPECS RELACIONADAS:
--   - SPEC-TWO-FACTOR-AUTHENTICATION.md
--   - SPEC-SEGURIDAD-API-KEYS-PERMISOS.md
--   - SPEC-OAUTH2-SOCIAL-LOGIN.md
-- =====================================================

-- =====================================================
-- PARTE 1: GROUPS Y HERENCIA
-- =====================================================

-- Tabla: groups (Grupos de usuarios con herencia)
CREATE TABLE auth.groups (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
  code VARCHAR(100) NOT NULL,
  name VARCHAR(255) NOT NULL,
  description TEXT,

  -- Configuración
  is_system BOOLEAN NOT NULL DEFAULT FALSE,  -- Grupos del sistema no editables
  category VARCHAR(100),                      -- Categoría para agrupación (ventas, compras, etc.)
  color VARCHAR(20),

  -- API Keys
  api_key_max_duration_days INTEGER DEFAULT 30
    CHECK (api_key_max_duration_days >= 0),  -- 0 = sin expiración (solo grupos system)

  -- Auditoría
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_by UUID REFERENCES auth.users(id),
  updated_at TIMESTAMP,
  updated_by UUID REFERENCES auth.users(id),
  deleted_at TIMESTAMP,
  deleted_by UUID REFERENCES auth.users(id),

  CONSTRAINT uq_groups_code_tenant UNIQUE (tenant_id, code)
);

-- Tabla: group_implied (Herencia de grupos)
CREATE TABLE auth.group_implied (
  group_id UUID NOT NULL REFERENCES auth.groups(id) ON DELETE CASCADE,
  implied_group_id UUID NOT NULL REFERENCES auth.groups(id) ON DELETE CASCADE,

  PRIMARY KEY (group_id, implied_group_id),
  CONSTRAINT chk_group_no_self_imply CHECK (group_id != implied_group_id)
);

-- Tabla: user_groups (Many-to-Many usuarios-grupos)
CREATE TABLE auth.user_groups (
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  group_id UUID NOT NULL REFERENCES auth.groups(id) ON DELETE CASCADE,
  assigned_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  assigned_by UUID REFERENCES auth.users(id),

  PRIMARY KEY (user_id, group_id)
);

-- Índices para groups
CREATE INDEX idx_groups_tenant_id ON auth.groups(tenant_id);
CREATE INDEX idx_groups_code ON auth.groups(code);
CREATE INDEX idx_groups_category ON auth.groups(category);
CREATE INDEX idx_groups_is_system ON auth.groups(is_system);

-- Índices para user_groups
CREATE INDEX idx_user_groups_user_id ON auth.user_groups(user_id);
CREATE INDEX idx_user_groups_group_id ON auth.user_groups(group_id);

-- =====================================================
-- PARTE 2: MODELS Y ACL (Access Control Lists)
-- =====================================================

-- Tabla: models (Definición de modelos del sistema)
CREATE TABLE auth.models (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
  name VARCHAR(128) NOT NULL,          -- Nombre técnico (ej: 'sale.order')
  description VARCHAR(255),            -- Descripción legible
  module VARCHAR(64),                  -- Módulo al que pertenece
  is_transient BOOLEAN NOT NULL DEFAULT FALSE,  -- Modelo temporal

  -- Auditoría
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP,

  CONSTRAINT uq_models_name_tenant UNIQUE (tenant_id, name)
);

-- Tabla: model_access (Permisos CRUD por modelo y grupo)
CREATE TABLE auth.model_access (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
  name VARCHAR(255) NOT NULL,          -- Identificador legible

  model_id UUID NOT NULL REFERENCES auth.models(id) ON DELETE CASCADE,
  group_id UUID REFERENCES auth.groups(id) ON DELETE RESTRICT,  -- NULL = global

  -- Permisos CRUD
  perm_read BOOLEAN NOT NULL DEFAULT FALSE,
  perm_create BOOLEAN NOT NULL DEFAULT FALSE,
  perm_write BOOLEAN NOT NULL DEFAULT FALSE,
  perm_delete BOOLEAN NOT NULL DEFAULT FALSE,

  is_active BOOLEAN NOT NULL DEFAULT TRUE,

  -- Auditoría
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP,

  -- Un grupo solo puede tener un registro por modelo
  CONSTRAINT uq_model_access_model_group UNIQUE (model_id, group_id, tenant_id)
);

-- Índices para models
CREATE INDEX idx_models_name ON auth.models(name);
CREATE INDEX idx_models_tenant ON auth.models(tenant_id);
CREATE INDEX idx_models_module ON auth.models(module);

-- Índices para model_access
CREATE INDEX idx_model_access_model ON auth.model_access(model_id);
CREATE INDEX idx_model_access_group ON auth.model_access(group_id);
CREATE INDEX idx_model_access_active ON auth.model_access(is_active) WHERE is_active = TRUE;

-- =====================================================
-- PARTE 3: RECORD RULES (Row-Level Security)
-- =====================================================

-- Tabla: record_rules (Reglas de acceso a nivel de registro)
CREATE TABLE auth.record_rules (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
  name VARCHAR(255) NOT NULL,

  model_id UUID NOT NULL REFERENCES auth.models(id) ON DELETE CASCADE,

  -- Dominio como expresión JSON
  domain_expression JSONB NOT NULL,    -- [["company_id", "in", "user.company_ids"]]

  -- Permisos afectados
  perm_read BOOLEAN NOT NULL DEFAULT TRUE,
  perm_create BOOLEAN NOT NULL DEFAULT TRUE,
  perm_write BOOLEAN NOT NULL DEFAULT TRUE,
  perm_delete BOOLEAN NOT NULL DEFAULT TRUE,

  -- Regla global (sin grupos = aplica a todos)
  is_global BOOLEAN NOT NULL DEFAULT FALSE,

  is_active BOOLEAN NOT NULL DEFAULT TRUE,

  -- Auditoría
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP
);

-- Tabla: rule_groups (Relación M:N entre rules y groups)
CREATE TABLE auth.rule_groups (
  rule_id UUID NOT NULL REFERENCES auth.record_rules(id) ON DELETE CASCADE,
  group_id UUID NOT NULL REFERENCES auth.groups(id) ON DELETE CASCADE,

  PRIMARY KEY (rule_id, group_id)
);

-- Índices para record_rules
CREATE INDEX idx_record_rules_model ON auth.record_rules(model_id);
CREATE INDEX idx_record_rules_global ON auth.record_rules(is_global) WHERE is_global = TRUE;
CREATE INDEX idx_record_rules_active ON auth.record_rules(is_active) WHERE is_active = TRUE;

-- Índices para rule_groups
CREATE INDEX idx_rule_groups_rule ON auth.rule_groups(rule_id);
CREATE INDEX idx_rule_groups_group ON auth.rule_groups(group_id);

-- =====================================================
-- PARTE 4: FIELD PERMISSIONS
-- =====================================================

-- Tabla: model_fields (Campos del modelo con metadatos de seguridad)
CREATE TABLE auth.model_fields (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
  model_id UUID NOT NULL REFERENCES auth.models(id) ON DELETE CASCADE,

  name VARCHAR(128) NOT NULL,          -- Nombre técnico del campo
  field_type VARCHAR(64) NOT NULL,     -- Tipo: char, int, many2one, etc.
  description VARCHAR(255),            -- Etiqueta legible

  -- Seguridad por defecto
  is_readonly BOOLEAN NOT NULL DEFAULT FALSE,
  is_required BOOLEAN NOT NULL DEFAULT FALSE,

  -- Auditoría
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

  CONSTRAINT uq_model_field UNIQUE (model_id, name, tenant_id)
);

-- Tabla: field_permissions (Permisos de campo por grupo)
CREATE TABLE auth.field_permissions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
  field_id UUID NOT NULL REFERENCES auth.model_fields(id) ON DELETE CASCADE,
  group_id UUID NOT NULL REFERENCES auth.groups(id) ON DELETE CASCADE,

  -- Permisos
  can_read BOOLEAN NOT NULL DEFAULT TRUE,
  can_write BOOLEAN NOT NULL DEFAULT FALSE,

  CONSTRAINT uq_field_permission UNIQUE (field_id, group_id, tenant_id)
);

-- Índices para model_fields
CREATE INDEX idx_model_fields_model ON auth.model_fields(model_id);
CREATE INDEX idx_model_fields_name ON auth.model_fields(name);

-- Índices para field_permissions
CREATE INDEX idx_field_permissions_field ON auth.field_permissions(field_id);
CREATE INDEX idx_field_permissions_group ON auth.field_permissions(group_id);

-- =====================================================
-- PARTE 5: API KEYS
-- =====================================================

-- Tabla: api_keys (Autenticación para integraciones)
CREATE TABLE auth.api_keys (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,

  -- Descripción
  name VARCHAR(255) NOT NULL,          -- Descripción del propósito

  -- Seguridad
  key_index VARCHAR(16) NOT NULL,      -- Primeros 8 bytes del key (para lookup rápido)
  key_hash VARCHAR(255) NOT NULL,      -- Hash PBKDF2-SHA512 del key completo

  -- Scope y restricciones
  scope VARCHAR(100),                  -- NULL = acceso completo, 'rpc' = solo API
  allowed_ips INET[],                  -- IPs permitidas (opcional)

  -- Expiración
  expiration_date TIMESTAMPTZ,         -- NULL = sin expiración (solo system users)
  last_used_at TIMESTAMPTZ,            -- Último uso

  -- Estado
  is_active BOOLEAN NOT NULL DEFAULT TRUE,

  -- Auditoría
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  revoked_at TIMESTAMPTZ,
  revoked_by UUID REFERENCES auth.users(id),

  -- Constraints
  CONSTRAINT chk_key_index_length CHECK (LENGTH(key_index) = 16)
);

-- Índices para API Keys
CREATE INDEX idx_api_keys_lookup ON auth.api_keys (key_index, is_active)
  WHERE is_active = TRUE;
CREATE INDEX idx_api_keys_expiration ON auth.api_keys (expiration_date)
  WHERE expiration_date IS NOT NULL;
CREATE INDEX idx_api_keys_user ON auth.api_keys (user_id);
CREATE INDEX idx_api_keys_tenant ON auth.api_keys (tenant_id);

-- =====================================================
-- PARTE 6: TWO-FACTOR AUTHENTICATION (2FA)
-- =====================================================

-- Extensión de users para MFA
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
  mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE;

ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
  mfa_method VARCHAR(16) DEFAULT 'none'
  CHECK (mfa_method IN ('none', 'totp', 'sms', 'email'));

ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
  mfa_secret BYTEA;  -- Secreto TOTP encriptado con AES-256-GCM

ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
  backup_codes JSONB DEFAULT '[]';  -- Códigos de respaldo (array de hashes SHA-256)

ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
  backup_codes_count INTEGER NOT NULL DEFAULT 0;

ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
  mfa_setup_at TIMESTAMPTZ;

ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
  last_2fa_verification TIMESTAMPTZ;

-- Constraint de consistencia MFA
ALTER TABLE auth.users ADD CONSTRAINT chk_mfa_consistency CHECK (
  (mfa_enabled = TRUE AND mfa_secret IS NOT NULL AND mfa_method != 'none') OR
  (mfa_enabled = FALSE)
);

-- Índice para usuarios con MFA
CREATE INDEX idx_users_mfa_enabled ON auth.users(mfa_enabled) WHERE mfa_enabled = TRUE;

-- Tabla: trusted_devices (Dispositivos de confianza)
CREATE TABLE auth.trusted_devices (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  -- Relación con usuario
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,

  -- Identificación del dispositivo
  device_fingerprint VARCHAR(128) NOT NULL,
  device_name VARCHAR(128),            -- "iPhone de Juan", "Chrome en MacBook"
  device_type VARCHAR(32),             -- 'mobile', 'desktop', 'tablet'

  -- Información del dispositivo
  user_agent TEXT,
  browser_name VARCHAR(64),
  browser_version VARCHAR(32),
  os_name VARCHAR(64),
  os_version VARCHAR(32),

  -- Ubicación del registro
  registered_ip INET NOT NULL,
  registered_location JSONB,           -- {country, city, lat, lng}

  -- Estado de confianza
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
  trust_level VARCHAR(16) NOT NULL DEFAULT 'standard'
    CHECK (trust_level IN ('standard', 'high', 'temporary')),
  trust_expires_at TIMESTAMPTZ,        -- NULL = no expira

  -- Uso
  last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  last_used_ip INET,
  use_count INTEGER NOT NULL DEFAULT 1,

  -- Auditoría
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  revoked_at TIMESTAMPTZ,
  revoked_reason VARCHAR(128),

  -- Constraints
  CONSTRAINT uk_trusted_device_user_fingerprint UNIQUE (user_id, device_fingerprint)
);

-- Índices para trusted_devices
CREATE INDEX idx_trusted_devices_user ON auth.trusted_devices(user_id) WHERE is_active;
CREATE INDEX idx_trusted_devices_fingerprint ON auth.trusted_devices(device_fingerprint);
CREATE INDEX idx_trusted_devices_expires ON auth.trusted_devices(trust_expires_at)
  WHERE trust_expires_at IS NOT NULL AND is_active;

-- Tabla: verification_codes (Códigos de verificación temporales)
CREATE TABLE auth.verification_codes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  -- Relaciones
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  session_id UUID REFERENCES auth.sessions(id) ON DELETE CASCADE,

  -- Tipo de código
  code_type VARCHAR(16) NOT NULL
    CHECK (code_type IN ('totp_setup', 'sms', 'email', 'backup')),

  -- Código (hash SHA-256)
  code_hash VARCHAR(64) NOT NULL,
  code_length INTEGER NOT NULL DEFAULT 6,

  -- Destino (para SMS/Email)
  destination VARCHAR(256),            -- Teléfono o email

  -- Intentos
  attempts INTEGER NOT NULL DEFAULT 0,
  max_attempts INTEGER NOT NULL DEFAULT 5,

  -- Validez
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  expires_at TIMESTAMPTZ NOT NULL,
  used_at TIMESTAMPTZ,

  -- Metadata
  ip_address INET,
  user_agent TEXT,

  -- Constraint
  CONSTRAINT chk_code_not_expired CHECK (used_at IS NULL OR used_at <= expires_at)
);

-- Índices para verification_codes
CREATE INDEX idx_verification_codes_user ON auth.verification_codes(user_id, code_type)
  WHERE used_at IS NULL;
CREATE INDEX idx_verification_codes_expires ON auth.verification_codes(expires_at)
  WHERE used_at IS NULL;

-- Tabla: mfa_audit_log (Log de auditoría MFA)
CREATE TABLE auth.mfa_audit_log (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  -- Usuario
  user_id UUID NOT NULL REFERENCES auth.users(id),

  -- Evento
  event_type VARCHAR(32) NOT NULL
    CHECK (event_type IN (
      'mfa_setup_initiated',
      'mfa_setup_completed',
      'mfa_disabled',
      'totp_verified',
      'totp_failed',
      'backup_code_used',
      'backup_codes_regenerated',
      'device_trusted',
      'device_revoked',
      'anomaly_detected',
      'account_locked',
      'account_unlocked'
    )),

  -- Resultado
  success BOOLEAN NOT NULL,
  failure_reason VARCHAR(128),

  -- Contexto
  ip_address INET,
  user_agent TEXT,
  device_fingerprint VARCHAR(128),
  location JSONB,

  -- Metadata adicional
  metadata JSONB DEFAULT '{}',

  -- Timestamp
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Índices para mfa_audit_log
CREATE INDEX idx_mfa_audit_user ON auth.mfa_audit_log(user_id, created_at DESC);
CREATE INDEX idx_mfa_audit_event ON auth.mfa_audit_log(event_type, created_at DESC);
CREATE INDEX idx_mfa_audit_failures ON auth.mfa_audit_log(user_id, created_at DESC)
  WHERE success = FALSE;

-- =====================================================
-- PARTE 7: OAUTH2 PROVIDERS
-- =====================================================

-- Tabla: oauth_providers (Proveedores OAuth2)
CREATE TABLE auth.oauth_providers (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID REFERENCES auth.tenants(id) ON DELETE CASCADE,  -- NULL = global

  code VARCHAR(50) NOT NULL,
  name VARCHAR(100) NOT NULL,

  -- Configuración OAuth2
  client_id VARCHAR(255) NOT NULL,
  client_secret VARCHAR(500),          -- Encriptado con AES-256

  -- Endpoints OAuth2
  authorization_endpoint VARCHAR(500) NOT NULL,
  token_endpoint VARCHAR(500) NOT NULL,
  userinfo_endpoint VARCHAR(500) NOT NULL,
  jwks_uri VARCHAR(500),               -- Para validación de ID tokens

  -- Scopes y parámetros
  scope VARCHAR(500) NOT NULL DEFAULT 'openid profile email',
  response_type VARCHAR(50) NOT NULL DEFAULT 'code',

  -- PKCE Configuration
  pkce_enabled BOOLEAN NOT NULL DEFAULT TRUE,
  code_challenge_method VARCHAR(10) DEFAULT 'S256',

  -- Mapeo de claims
  claim_mapping JSONB NOT NULL DEFAULT '{
    "sub": "oauth_uid",
    "email": "email",
    "name": "name",
    "picture": "avatar_url"
  }'::jsonb,

  -- UI
  icon_class VARCHAR(100),             -- fa-google, fa-microsoft, etc.
  button_text VARCHAR(100),
  button_color VARCHAR(20),
  display_order INTEGER NOT NULL DEFAULT 10,

  -- Estado
  is_enabled BOOLEAN NOT NULL DEFAULT FALSE,
  is_visible BOOLEAN NOT NULL DEFAULT TRUE,

  -- Restricciones
  allowed_domains TEXT[],              -- NULL = todos permitidos
  auto_create_users BOOLEAN NOT NULL DEFAULT FALSE,
  default_role_id UUID REFERENCES auth.roles(id),

  -- Auditoría
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_by UUID REFERENCES auth.users(id),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_by UUID REFERENCES auth.users(id),

  -- Constraints
  CONSTRAINT uq_oauth_provider_code UNIQUE (code),
  CONSTRAINT chk_response_type CHECK (response_type IN ('code', 'token')),
  CONSTRAINT chk_pkce_method CHECK (code_challenge_method IN ('S256', 'plain'))
);

-- Índices para oauth_providers
CREATE INDEX idx_oauth_providers_enabled ON auth.oauth_providers(is_enabled);
CREATE INDEX idx_oauth_providers_tenant ON auth.oauth_providers(tenant_id);
CREATE INDEX idx_oauth_providers_code ON auth.oauth_providers(code);

-- Tabla: oauth_user_links (Vinculación usuario-proveedor)
CREATE TABLE auth.oauth_user_links (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  provider_id UUID NOT NULL REFERENCES auth.oauth_providers(id) ON DELETE CASCADE,

  -- Identificación OAuth
  oauth_uid VARCHAR(255) NOT NULL,     -- Subject ID del proveedor
  oauth_email VARCHAR(255),

  -- Tokens (encriptados)
  access_token TEXT,
  refresh_token TEXT,
  id_token TEXT,
  token_expires_at TIMESTAMPTZ,

  -- Metadata
  raw_userinfo JSONB,                  -- Datos completos del proveedor
  last_login_at TIMESTAMPTZ,
  login_count INTEGER NOT NULL DEFAULT 0,

  -- Auditoría
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,

  -- Constraints
  CONSTRAINT uq_provider_oauth_uid UNIQUE (provider_id, oauth_uid),
  CONSTRAINT uq_user_provider UNIQUE (user_id, provider_id)
);

-- Índices para oauth_user_links
CREATE INDEX idx_oauth_links_user ON auth.oauth_user_links(user_id);
CREATE INDEX idx_oauth_links_provider ON auth.oauth_user_links(provider_id);
CREATE INDEX idx_oauth_links_oauth_uid ON auth.oauth_user_links(oauth_uid);

-- Tabla: oauth_states (Estados OAuth2 temporales para CSRF)
CREATE TABLE auth.oauth_states (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  state VARCHAR(64) NOT NULL UNIQUE,

  -- PKCE
  code_verifier VARCHAR(128),

  -- Contexto
  provider_id UUID NOT NULL REFERENCES auth.oauth_providers(id),
  redirect_uri VARCHAR(500) NOT NULL,
  return_url VARCHAR(500),

  -- Vinculación con usuario existente (para linking)
  link_user_id UUID REFERENCES auth.users(id),

  -- Metadata
  ip_address INET,
  user_agent TEXT,

  -- Tiempo de vida
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  expires_at TIMESTAMPTZ NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL '10 minutes'),
  used_at TIMESTAMPTZ,

  -- Constraints
  CONSTRAINT chk_state_not_expired CHECK (expires_at > created_at)
);

-- Índices para oauth_states
CREATE INDEX idx_oauth_states_state ON auth.oauth_states(state);
CREATE INDEX idx_oauth_states_expires ON auth.oauth_states(expires_at);

-- Extensión de users para OAuth
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
  oauth_only BOOLEAN NOT NULL DEFAULT FALSE;

ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
  primary_oauth_provider_id UUID REFERENCES auth.oauth_providers(id);

-- =====================================================
-- PARTE 8: FUNCIONES DE UTILIDAD
-- =====================================================

-- Función: Obtener grupos efectivos de un usuario (incluyendo herencia)
CREATE OR REPLACE FUNCTION auth.get_user_effective_groups(p_user_id UUID)
RETURNS TABLE(group_id UUID) AS $$
WITH RECURSIVE effective_groups AS (
  -- Grupos asignados directamente
  SELECT ug.group_id
  FROM auth.user_groups ug
  WHERE ug.user_id = p_user_id

  UNION

  -- Grupos heredados
  SELECT gi.implied_group_id
  FROM auth.group_implied gi
  JOIN effective_groups eg ON gi.group_id = eg.group_id
)
SELECT DISTINCT group_id FROM effective_groups;
$$ LANGUAGE SQL STABLE;

COMMENT ON FUNCTION auth.get_user_effective_groups IS 'Obtiene todos los grupos de un usuario incluyendo herencia';

-- Función: Verificar permiso ACL
CREATE OR REPLACE FUNCTION auth.check_model_access(
  p_user_id UUID,
  p_model_name VARCHAR,
  p_mode VARCHAR  -- 'read', 'create', 'write', 'delete'
)
RETURNS BOOLEAN AS $$
DECLARE
  v_has_access BOOLEAN;
BEGIN
  -- Superusers tienen todos los permisos
  IF EXISTS (
    SELECT 1 FROM auth.users
    WHERE id = p_user_id AND is_superuser = TRUE AND deleted_at IS NULL
  ) THEN
    RETURN TRUE;
  END IF;

  -- Verificar ACL
  SELECT EXISTS (
    SELECT 1
    FROM auth.model_access ma
    JOIN auth.models m ON ma.model_id = m.id
    WHERE m.name = p_model_name
      AND ma.is_active = TRUE
      AND (
        ma.group_id IS NULL  -- Permiso global
        OR ma.group_id IN (SELECT auth.get_user_effective_groups(p_user_id))
      )
      AND CASE p_mode
        WHEN 'read' THEN ma.perm_read
        WHEN 'create' THEN ma.perm_create
        WHEN 'write' THEN ma.perm_write
        WHEN 'delete' THEN ma.perm_delete
        ELSE FALSE
      END
  ) INTO v_has_access;

  RETURN COALESCE(v_has_access, FALSE);
END;
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;

COMMENT ON FUNCTION auth.check_model_access IS 'Verifica si un usuario tiene permiso CRUD en un modelo';

-- Función: Limpiar estados OAuth expirados
CREATE OR REPLACE FUNCTION auth.cleanup_expired_oauth_states()
RETURNS INTEGER AS $$
DECLARE
  v_deleted INTEGER;
BEGIN
  WITH deleted AS (
    DELETE FROM auth.oauth_states
    WHERE expires_at < CURRENT_TIMESTAMP
       OR used_at IS NOT NULL
    RETURNING id
  )
  SELECT COUNT(*) INTO v_deleted FROM deleted;

  RETURN v_deleted;
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION auth.cleanup_expired_oauth_states IS 'Limpia estados OAuth expirados (ejecutar periódicamente)';

-- Función: Limpiar códigos de verificación expirados
CREATE OR REPLACE FUNCTION auth.cleanup_expired_verification_codes()
RETURNS INTEGER AS $$
DECLARE
  v_deleted INTEGER;
BEGIN
  WITH deleted AS (
    DELETE FROM auth.verification_codes
    WHERE expires_at < NOW() - INTERVAL '1 day'
    RETURNING id
  )
  SELECT COUNT(*) INTO v_deleted FROM deleted;

  RETURN v_deleted;
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION auth.cleanup_expired_verification_codes IS 'Limpia códigos de verificación expirados';

-- Función: Limpiar dispositivos de confianza expirados
CREATE OR REPLACE FUNCTION auth.cleanup_expired_trusted_devices()
RETURNS INTEGER AS $$
DECLARE
  v_deleted INTEGER;
BEGIN
  WITH updated AS (
    UPDATE auth.trusted_devices
    SET is_active = FALSE,
        revoked_at = NOW(),
        revoked_reason = 'expired'
    WHERE trust_expires_at < NOW() - INTERVAL '7 days'
      AND trust_expires_at IS NOT NULL
      AND is_active = TRUE
    RETURNING id
  )
  SELECT COUNT(*) INTO v_deleted FROM updated;

  RETURN v_deleted;
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION auth.cleanup_expired_trusted_devices IS 'Desactiva dispositivos de confianza expirados';

-- Función: Limpiar API keys expiradas
CREATE OR REPLACE FUNCTION auth.cleanup_expired_api_keys()
RETURNS INTEGER AS $$
DECLARE
  v_deleted INTEGER;
BEGIN
  WITH deleted AS (
    DELETE FROM auth.api_keys
    WHERE expiration_date IS NOT NULL
      AND expiration_date < NOW()
    RETURNING id
  )
  SELECT COUNT(*) INTO v_deleted FROM deleted;

  RETURN v_deleted;
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION auth.cleanup_expired_api_keys IS 'Limpia API keys expiradas';

-- =====================================================
-- PARTE 9: TRIGGERS
-- =====================================================

-- Trigger: Actualizar updated_at para grupos
CREATE TRIGGER trg_groups_updated_at
  BEFORE UPDATE ON auth.groups
  FOR EACH ROW
  EXECUTE FUNCTION auth.update_updated_at_column();

-- Trigger: Actualizar updated_at para oauth_providers
CREATE TRIGGER trg_oauth_providers_updated_at
  BEFORE UPDATE ON auth.oauth_providers
  FOR EACH ROW
  EXECUTE FUNCTION auth.update_updated_at_column();

-- Trigger: Actualizar updated_at para oauth_user_links
CREATE OR REPLACE FUNCTION auth.update_oauth_link_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = CURRENT_TIMESTAMP;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_oauth_user_links_updated_at
  BEFORE UPDATE ON auth.oauth_user_links
  FOR EACH ROW
  EXECUTE FUNCTION auth.update_oauth_link_updated_at();

-- =====================================================
-- PARTE 10: VISTAS
-- =====================================================

-- Vista: Usuarios con sus proveedores OAuth vinculados
CREATE OR REPLACE VIEW auth.users_oauth_summary AS
SELECT
  u.id,
  u.email,
  u.full_name,
  u.oauth_only,
  COUNT(ol.id) as linked_providers_count,
  ARRAY_AGG(op.name) FILTER (WHERE op.id IS NOT NULL) as linked_provider_names,
  MAX(ol.last_login_at) as last_oauth_login
FROM auth.users u
LEFT JOIN auth.oauth_user_links ol ON ol.user_id = u.id
LEFT JOIN auth.oauth_providers op ON op.id = ol.provider_id
WHERE u.deleted_at IS NULL
GROUP BY u.id;

COMMENT ON VIEW auth.users_oauth_summary IS 'Vista de usuarios con sus proveedores OAuth vinculados';

-- Vista: Permisos efectivos por usuario y modelo
CREATE OR REPLACE VIEW auth.user_model_access_view AS
SELECT DISTINCT
  u.id as user_id,
  u.email,
  m.name as model_name,
  BOOL_OR(ma.perm_read) as can_read,
  BOOL_OR(ma.perm_create) as can_create,
  BOOL_OR(ma.perm_write) as can_write,
  BOOL_OR(ma.perm_delete) as can_delete
FROM auth.users u
CROSS JOIN auth.models m
LEFT JOIN auth.user_groups ug ON ug.user_id = u.id
LEFT JOIN auth.model_access ma ON ma.model_id = m.id
  AND (ma.group_id IS NULL OR ma.group_id = ug.group_id)
  AND ma.is_active = TRUE
WHERE u.deleted_at IS NULL
GROUP BY u.id, u.email, m.name;

COMMENT ON VIEW auth.user_model_access_view IS 'Vista de permisos ACL efectivos por usuario y modelo';

-- =====================================================
-- PARTE 11: DATOS INICIALES
-- =====================================================

-- Proveedores OAuth2 preconfigurados (template)
-- NOTA: Solo se insertan como template, requieren client_id y client_secret
INSERT INTO auth.oauth_providers (
  code, name,
  authorization_endpoint, token_endpoint, userinfo_endpoint, jwks_uri,
  scope, icon_class, button_text, button_color,
  claim_mapping, display_order, is_enabled, client_id
) VALUES
-- Google
(
  'google', 'Google',
  'https://accounts.google.com/o/oauth2/v2/auth',
  'https://oauth2.googleapis.com/token',
  'https://openidconnect.googleapis.com/v1/userinfo',
  'https://www.googleapis.com/oauth2/v3/certs',
  'openid profile email',
  'fa-google', 'Continuar con Google', '#4285F4',
  '{"sub": "oauth_uid", "email": "email", "name": "name", "picture": "avatar_url"}',
  1, FALSE, 'CONFIGURE_ME'
),
-- Microsoft Azure AD
(
  'microsoft', 'Microsoft',
  'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
  'https://login.microsoftonline.com/common/oauth2/v2.0/token',
  'https://graph.microsoft.com/v1.0/me',
  'https://login.microsoftonline.com/common/discovery/v2.0/keys',
  'openid profile email User.Read',
  'fa-microsoft', 'Continuar con Microsoft', '#00A4EF',
  '{"id": "oauth_uid", "mail": "email", "displayName": "name"}',
  2, FALSE, 'CONFIGURE_ME'
),
-- GitHub
(
  'github', 'GitHub',
  'https://github.com/login/oauth/authorize',
  'https://github.com/login/oauth/access_token',
  'https://api.github.com/user',
  NULL,
  'read:user user:email',
  'fa-github', 'Continuar con GitHub', '#333333',
  '{"id": "oauth_uid", "email": "email", "name": "name", "avatar_url": "avatar_url"}',
  3, FALSE, 'CONFIGURE_ME'
)
ON CONFLICT (code) DO NOTHING;

-- =====================================================
-- COMENTARIOS EN TABLAS
-- =====================================================

COMMENT ON TABLE auth.groups IS 'Grupos de usuarios con herencia para control de acceso';
COMMENT ON TABLE auth.group_implied IS 'Herencia entre grupos (A implica B)';
COMMENT ON TABLE auth.user_groups IS 'Asignación de usuarios a grupos (many-to-many)';
COMMENT ON TABLE auth.models IS 'Definición de modelos del sistema para ACL';
COMMENT ON TABLE auth.model_access IS 'Permisos CRUD a nivel de modelo por grupo (ACL)';
COMMENT ON TABLE auth.record_rules IS 'Reglas de acceso a nivel de registro (row-level security)';
COMMENT ON TABLE auth.rule_groups IS 'Relación entre record rules y grupos';
COMMENT ON TABLE auth.model_fields IS 'Campos de modelo con metadatos de seguridad';
COMMENT ON TABLE auth.field_permissions IS 'Permisos de lectura/escritura por campo y grupo';
COMMENT ON TABLE auth.api_keys IS 'API Keys para autenticación de integraciones externas';
COMMENT ON TABLE auth.trusted_devices IS 'Dispositivos de confianza para bypass de 2FA';
COMMENT ON TABLE auth.verification_codes IS 'Códigos de verificación temporales para 2FA';
COMMENT ON TABLE auth.mfa_audit_log IS 'Log de auditoría de eventos MFA';
COMMENT ON TABLE auth.oauth_providers IS 'Proveedores OAuth2 configurados';
COMMENT ON TABLE auth.oauth_user_links IS 'Vinculación de usuarios con proveedores OAuth';
COMMENT ON TABLE auth.oauth_states IS 'Estados OAuth2 temporales para protección CSRF';

COMMENT ON COLUMN auth.api_keys.key_index IS 'Primeros 16 hex chars del key para lookup O(1)';
COMMENT ON COLUMN auth.api_keys.key_hash IS 'Hash PBKDF2-SHA512 del key completo';
COMMENT ON COLUMN auth.api_keys.scope IS 'Scope del API key (NULL=full, rpc=API only)';
COMMENT ON COLUMN auth.groups.api_key_max_duration_days IS 'Máxima duración en días para API keys de usuarios de este grupo (0=ilimitado)';

-- =====================================================
-- FIN DE EXTENSIONES AUTH
-- =====================================================
