-- =============================================================
-- ARCHIVO: 06-auth-extended.sql
-- DESCRIPCION: Extensiones de autenticacion SaaS (JWT, OAuth, MFA)
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-10
-- EPIC: SAAS-CORE-AUTH (EPIC-SAAS-001)
-- HISTORIAS: US-001, US-002, US-003
-- =============================================================

-- =====================
-- MODIFICACIONES A TABLAS EXISTENTES
-- =====================

-- Agregar columnas OAuth a auth.users (US-002)
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS oauth_provider VARCHAR(50);
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS oauth_provider_id VARCHAR(255);
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS avatar_url TEXT;

-- Agregar columnas MFA a auth.users (US-003)
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS mfa_enabled BOOLEAN DEFAULT FALSE;
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS mfa_secret_encrypted TEXT;
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS mfa_backup_codes TEXT[];

-- Agregar columna superadmin (EPIC-SAAS-006)
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS is_superadmin BOOLEAN DEFAULT FALSE;

-- Indices para nuevas columnas
CREATE INDEX IF NOT EXISTS idx_users_oauth_provider ON auth.users(oauth_provider, oauth_provider_id) WHERE oauth_provider IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_users_mfa_enabled ON auth.users(mfa_enabled) WHERE mfa_enabled = TRUE;

-- =====================
-- TABLA: auth.sessions
-- Sesiones de usuario con refresh tokens (US-001)
-- =====================
CREATE TABLE IF NOT EXISTS auth.sessions (
    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,

    -- Token info
    refresh_token_hash VARCHAR(255) NOT NULL,
    jti VARCHAR(255) UNIQUE NOT NULL,  -- JWT ID para blacklist

    -- Device info
    device_info JSONB DEFAULT '{}',
    device_fingerprint VARCHAR(255),
    user_agent TEXT,
    ip_address INET,

    -- Geo info
    country_code VARCHAR(2),
    city VARCHAR(100),

    -- Timestamps
    last_activity_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMPTZ NOT NULL,
    revoked_at TIMESTAMPTZ,
    revoked_reason VARCHAR(100),

    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);

-- Indices para sessions
CREATE INDEX IF NOT EXISTS idx_sessions_user ON auth.sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_tenant ON auth.sessions(tenant_id);
CREATE INDEX IF NOT EXISTS idx_sessions_jti ON auth.sessions(jti);
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON auth.sessions(expires_at) WHERE revoked_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_sessions_active ON auth.sessions(user_id, revoked_at) WHERE revoked_at IS NULL;

-- =====================
-- TABLA: auth.token_blacklist
-- Tokens revocados/invalidados (US-001)
-- =====================
CREATE TABLE IF NOT EXISTS auth.token_blacklist (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    jti VARCHAR(255) UNIQUE NOT NULL,
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    token_type VARCHAR(20) NOT NULL CHECK (token_type IN ('access', 'refresh')),

    -- Metadata
    reason VARCHAR(100),
    revoked_by UUID REFERENCES auth.users(id),

    -- Timestamps
    expires_at TIMESTAMPTZ NOT NULL,
    revoked_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);

-- Indice para limpieza de tokens expirados
CREATE INDEX IF NOT EXISTS idx_token_blacklist_expires ON auth.token_blacklist(expires_at);
CREATE INDEX IF NOT EXISTS idx_token_blacklist_jti ON auth.token_blacklist(jti);

-- =====================
-- TABLA: auth.oauth_providers
-- Proveedores OAuth vinculados a usuarios (US-002)
-- =====================
CREATE TABLE IF NOT EXISTS auth.oauth_providers (
    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,

    -- Provider info
    provider VARCHAR(50) NOT NULL,  -- google, github, microsoft, apple
    provider_user_id VARCHAR(255) NOT NULL,
    provider_email VARCHAR(255),

    -- Tokens (encrypted)
    access_token_encrypted TEXT,
    refresh_token_encrypted TEXT,
    token_expires_at TIMESTAMPTZ,

    -- Profile data from provider
    profile_data JSONB DEFAULT '{}',

    -- Timestamps
    linked_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    last_used_at TIMESTAMPTZ,
    unlinked_at TIMESTAMPTZ,

    UNIQUE(provider, provider_user_id)
);

-- Indices para oauth_providers
CREATE INDEX IF NOT EXISTS idx_oauth_providers_user ON auth.oauth_providers(user_id);
CREATE INDEX IF NOT EXISTS idx_oauth_providers_provider ON auth.oauth_providers(provider, provider_user_id);
CREATE INDEX IF NOT EXISTS idx_oauth_providers_email ON auth.oauth_providers(provider_email);

-- =====================
-- TABLA: auth.mfa_devices
-- Dispositivos MFA registrados (US-003)
-- =====================
CREATE TABLE IF NOT EXISTS auth.mfa_devices (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,

    -- Device info
    device_type VARCHAR(50) NOT NULL,  -- totp, sms, email, hardware_key
    device_name VARCHAR(255),

    -- TOTP specific
    secret_encrypted TEXT,

    -- Status
    is_primary BOOLEAN DEFAULT FALSE,
    is_verified BOOLEAN DEFAULT FALSE,
    verified_at TIMESTAMPTZ,

    -- Usage tracking
    last_used_at TIMESTAMPTZ,
    use_count INTEGER DEFAULT 0,

    -- Timestamps
    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    disabled_at TIMESTAMPTZ
);

-- Indices para mfa_devices
CREATE INDEX IF NOT EXISTS idx_mfa_devices_user ON auth.mfa_devices(user_id);
CREATE INDEX IF NOT EXISTS idx_mfa_devices_primary ON auth.mfa_devices(user_id, is_primary) WHERE is_primary = TRUE;

-- =====================
-- TABLA: auth.mfa_backup_codes
-- Codigos de respaldo MFA (US-003)
-- =====================
CREATE TABLE IF NOT EXISTS auth.mfa_backup_codes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,

    -- Code (hashed)
    code_hash VARCHAR(255) NOT NULL,

    -- Status
    used_at TIMESTAMPTZ,

    -- Timestamps
    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMPTZ
);

-- Indices para mfa_backup_codes
CREATE INDEX IF NOT EXISTS idx_mfa_backup_codes_user ON auth.mfa_backup_codes(user_id);
CREATE INDEX IF NOT EXISTS idx_mfa_backup_codes_unused ON auth.mfa_backup_codes(user_id, used_at) WHERE used_at IS NULL;

-- =====================
-- TABLA: auth.login_attempts
-- Intentos de login para rate limiting y seguridad
-- =====================
CREATE TABLE IF NOT EXISTS auth.login_attempts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Identificacion
    email VARCHAR(255),
    ip_address INET NOT NULL,
    user_agent TEXT,

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

    -- MFA
    mfa_required BOOLEAN DEFAULT FALSE,
    mfa_passed BOOLEAN,

    -- Metadata
    tenant_id UUID REFERENCES auth.tenants(id),
    user_id UUID REFERENCES auth.users(id),

    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);

-- Indices para login_attempts
CREATE INDEX IF NOT EXISTS idx_login_attempts_email ON auth.login_attempts(email, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_login_attempts_ip ON auth.login_attempts(ip_address, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_login_attempts_cleanup ON auth.login_attempts(created_at);

-- =====================
-- RLS POLICIES
-- =====================
ALTER TABLE auth.sessions ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_sessions ON auth.sessions
    USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);

ALTER TABLE auth.oauth_providers ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_oauth ON auth.oauth_providers
    USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);

ALTER TABLE auth.mfa_devices ENABLE ROW LEVEL SECURITY;
CREATE POLICY user_isolation_mfa_devices ON auth.mfa_devices
    USING (user_id = current_setting('app.current_user_id', true)::uuid);

ALTER TABLE auth.mfa_backup_codes ENABLE ROW LEVEL SECURITY;
CREATE POLICY user_isolation_mfa_codes ON auth.mfa_backup_codes
    USING (user_id = current_setting('app.current_user_id', true)::uuid);

-- =====================
-- FUNCIONES
-- =====================

-- Funcion para limpiar sesiones expiradas
CREATE OR REPLACE FUNCTION auth.cleanup_expired_sessions()
RETURNS INTEGER AS $$
DECLARE
    deleted_count INTEGER;
BEGIN
    DELETE FROM auth.sessions
    WHERE expires_at < CURRENT_TIMESTAMP
    OR revoked_at IS NOT NULL;

    GET DIAGNOSTICS deleted_count = ROW_COUNT;
    RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;

-- Funcion para limpiar tokens expirados del blacklist
CREATE OR REPLACE FUNCTION auth.cleanup_expired_tokens()
RETURNS INTEGER AS $$
DECLARE
    deleted_count INTEGER;
BEGIN
    DELETE FROM auth.token_blacklist
    WHERE expires_at < CURRENT_TIMESTAMP;

    GET DIAGNOSTICS deleted_count = ROW_COUNT;
    RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;

-- Funcion para limpiar intentos de login antiguos (mas de 30 dias)
CREATE OR REPLACE FUNCTION auth.cleanup_old_login_attempts()
RETURNS INTEGER AS $$
DECLARE
    deleted_count INTEGER;
BEGIN
    DELETE FROM auth.login_attempts
    WHERE created_at < CURRENT_TIMESTAMP - INTERVAL '30 days';

    GET DIAGNOSTICS deleted_count = ROW_COUNT;
    RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;

-- Funcion para verificar rate limit de login
CREATE OR REPLACE FUNCTION auth.check_login_rate_limit(
    p_email VARCHAR(255),
    p_ip_address INET,
    p_max_attempts INTEGER DEFAULT 5,
    p_window_minutes INTEGER DEFAULT 15
)
RETURNS BOOLEAN AS $$
DECLARE
    attempt_count INTEGER;
BEGIN
    SELECT COUNT(*) INTO attempt_count
    FROM auth.login_attempts
    WHERE (email = p_email OR ip_address = p_ip_address)
    AND success = FALSE
    AND created_at > CURRENT_TIMESTAMP - (p_window_minutes || ' minutes')::INTERVAL;

    RETURN attempt_count < p_max_attempts;
END;
$$ LANGUAGE plpgsql;

-- Funcion para revocar todas las sesiones de un usuario
CREATE OR REPLACE FUNCTION auth.revoke_all_user_sessions(
    p_user_id UUID,
    p_reason VARCHAR(100) DEFAULT 'manual_revocation'
)
RETURNS INTEGER AS $$
DECLARE
    revoked_count INTEGER;
BEGIN
    UPDATE auth.sessions
    SET revoked_at = CURRENT_TIMESTAMP,
        revoked_reason = p_reason
    WHERE user_id = p_user_id
    AND revoked_at IS NULL;

    GET DIAGNOSTICS revoked_count = ROW_COUNT;
    RETURN revoked_count;
END;
$$ LANGUAGE plpgsql;

-- =====================
-- COMENTARIOS
-- =====================
COMMENT ON TABLE auth.sessions IS 'Sesiones de usuario con refresh tokens para JWT';
COMMENT ON TABLE auth.token_blacklist IS 'Tokens JWT revocados antes de su expiracion';
COMMENT ON TABLE auth.oauth_providers IS 'Proveedores OAuth vinculados a cuentas de usuario';
COMMENT ON TABLE auth.mfa_devices IS 'Dispositivos MFA registrados por usuario';
COMMENT ON TABLE auth.mfa_backup_codes IS 'Codigos de respaldo para MFA';
COMMENT ON TABLE auth.login_attempts IS 'Registro de intentos de login para seguridad';

COMMENT ON FUNCTION auth.cleanup_expired_sessions IS 'Limpia sesiones expiradas o revocadas';
COMMENT ON FUNCTION auth.cleanup_expired_tokens IS 'Limpia tokens del blacklist que ya expiraron';
COMMENT ON FUNCTION auth.check_login_rate_limit IS 'Verifica si un email/IP ha excedido intentos de login';
COMMENT ON FUNCTION auth.revoke_all_user_sessions IS 'Revoca todas las sesiones activas de un usuario';
