-- ============================================================================
-- 13-audit-triggers-ddl.sql
-- Schema: audit
-- ERP Construccion - GAP-007: Audit Triggers
-- ============================================================================
-- Descripcion: Sistema de auditoría detallada con:
--   - Registro de cambios a nivel de campo
--   - Triggers automáticos para tablas críticas
--   - Historial completo de modificaciones
-- ============================================================================
-- Autor: Claude-Arquitecto-Orquestador
-- Fecha: 2026-02-04
-- Version: 1.0.0
-- GAP: GAP-007
-- ============================================================================

-- Crear schema si no existe
CREATE SCHEMA IF NOT EXISTS audit;

-- ============================================================================
-- ENUMS
-- ============================================================================

-- Tipo de operación
CREATE TYPE audit.operation_type AS ENUM (
    'INSERT',
    'UPDATE',
    'DELETE',
    'TRUNCATE'
);

-- ============================================================================
-- TABLA DE AUDIT LOG
-- ============================================================================

CREATE TABLE IF NOT EXISTS audit.change_log (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Contexto
    tenant_id UUID,
    user_id UUID,
    session_id VARCHAR(100),

    -- Operación
    operation audit.operation_type NOT NULL,
    table_schema VARCHAR(63) NOT NULL,
    table_name VARCHAR(63) NOT NULL,
    record_id UUID,

    -- Datos
    old_data JSONB,
    new_data JSONB,
    changed_fields TEXT[],

    -- Metadatos
    client_ip INET,
    user_agent TEXT,
    application_name VARCHAR(100),

    -- Timestamp
    executed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- Para queries eficientes
    executed_date DATE GENERATED ALWAYS AS (executed_at::date) STORED
);

-- Índices para change_log
CREATE INDEX IF NOT EXISTS idx_change_log_tenant ON audit.change_log(tenant_id);
CREATE INDEX IF NOT EXISTS idx_change_log_user ON audit.change_log(user_id);
CREATE INDEX IF NOT EXISTS idx_change_log_table ON audit.change_log(table_schema, table_name);
CREATE INDEX IF NOT EXISTS idx_change_log_record ON audit.change_log(record_id);
CREATE INDEX IF NOT EXISTS idx_change_log_executed ON audit.change_log(executed_at DESC);
CREATE INDEX IF NOT EXISTS idx_change_log_date ON audit.change_log(executed_date);
CREATE INDEX IF NOT EXISTS idx_change_log_operation ON audit.change_log(operation);

-- Particionamiento por fecha (opcional, para grandes volúmenes)
-- La tabla puede ser particionada posteriormente si es necesario

-- ============================================================================
-- FUNCIÓN DE TRIGGER GENÉRICA
-- ============================================================================

CREATE OR REPLACE FUNCTION audit.log_changes()
RETURNS TRIGGER AS $$
DECLARE
    v_tenant_id UUID;
    v_user_id UUID;
    v_session_id TEXT;
    v_old_data JSONB;
    v_new_data JSONB;
    v_changed_fields TEXT[];
    v_record_id UUID;
    v_key TEXT;
BEGIN
    -- Obtener contexto de la sesión
    BEGIN
        v_tenant_id := current_setting('app.current_tenant_id', true)::uuid;
    EXCEPTION WHEN OTHERS THEN
        v_tenant_id := NULL;
    END;

    BEGIN
        v_user_id := current_setting('app.current_user_id', true)::uuid;
    EXCEPTION WHEN OTHERS THEN
        v_user_id := NULL;
    END;

    BEGIN
        v_session_id := current_setting('app.session_id', true);
    EXCEPTION WHEN OTHERS THEN
        v_session_id := NULL;
    END;

    -- Determinar datos según operación
    IF TG_OP = 'INSERT' THEN
        v_new_data := to_jsonb(NEW);
        v_old_data := NULL;
        v_record_id := NEW.id;
        v_changed_fields := ARRAY(SELECT jsonb_object_keys(v_new_data));

    ELSIF TG_OP = 'UPDATE' THEN
        v_old_data := to_jsonb(OLD);
        v_new_data := to_jsonb(NEW);
        v_record_id := NEW.id;

        -- Calcular campos modificados
        v_changed_fields := ARRAY(
            SELECT key FROM (
                SELECT key, v_old_data->key AS old_val, v_new_data->key AS new_val
                FROM jsonb_object_keys(v_new_data) AS key
            ) AS changes
            WHERE old_val IS DISTINCT FROM new_val
        );

        -- Si no hay cambios reales, no registrar
        IF array_length(v_changed_fields, 1) IS NULL THEN
            RETURN NEW;
        END IF;

    ELSIF TG_OP = 'DELETE' THEN
        v_old_data := to_jsonb(OLD);
        v_new_data := NULL;
        v_record_id := OLD.id;
        v_changed_fields := NULL;
    END IF;

    -- Insertar registro de auditoría
    INSERT INTO audit.change_log (
        tenant_id,
        user_id,
        session_id,
        operation,
        table_schema,
        table_name,
        record_id,
        old_data,
        new_data,
        changed_fields,
        client_ip,
        application_name
    ) VALUES (
        v_tenant_id,
        v_user_id,
        v_session_id,
        TG_OP::audit.operation_type,
        TG_TABLE_SCHEMA,
        TG_TABLE_NAME,
        v_record_id,
        v_old_data,
        v_new_data,
        v_changed_fields,
        inet_client_addr(),
        current_setting('application_name', true)
    );

    -- Retornar el registro apropiado
    IF TG_OP = 'DELETE' THEN
        RETURN OLD;
    ELSE
        RETURN NEW;
    END IF;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- ============================================================================
-- FUNCIÓN PARA CREAR TRIGGER DE AUDITORÍA
-- ============================================================================

CREATE OR REPLACE FUNCTION audit.enable_audit_trigger(
    p_schema_name TEXT,
    p_table_name TEXT
)
RETURNS VOID AS $$
DECLARE
    v_trigger_name TEXT;
BEGIN
    v_trigger_name := 'audit_' || p_table_name || '_changes';

    -- Eliminar trigger si existe
    EXECUTE format(
        'DROP TRIGGER IF EXISTS %I ON %I.%I',
        v_trigger_name, p_schema_name, p_table_name
    );

    -- Crear trigger
    EXECUTE format(
        'CREATE TRIGGER %I
         AFTER INSERT OR UPDATE OR DELETE ON %I.%I
         FOR EACH ROW EXECUTE FUNCTION audit.log_changes()',
        v_trigger_name, p_schema_name, p_table_name
    );

    RAISE NOTICE 'Audit trigger creado: %.%', p_schema_name, p_table_name;
END;
$$ LANGUAGE plpgsql;

-- ============================================================================
-- FUNCIÓN PARA DESACTIVAR TRIGGER DE AUDITORÍA
-- ============================================================================

CREATE OR REPLACE FUNCTION audit.disable_audit_trigger(
    p_schema_name TEXT,
    p_table_name TEXT
)
RETURNS VOID AS $$
DECLARE
    v_trigger_name TEXT;
BEGIN
    v_trigger_name := 'audit_' || p_table_name || '_changes';

    EXECUTE format(
        'DROP TRIGGER IF EXISTS %I ON %I.%I',
        v_trigger_name, p_schema_name, p_table_name
    );

    RAISE NOTICE 'Audit trigger eliminado: %.%', p_schema_name, p_table_name;
END;
$$ LANGUAGE plpgsql;

-- ============================================================================
-- ACTIVAR AUDITORÍA EN TABLAS CRÍTICAS
-- ============================================================================

-- Construction: Proyectos y Fraccionamientos
SELECT audit.enable_audit_trigger('construction', 'fraccionamientos');
SELECT audit.enable_audit_trigger('construction', 'proyectos');

-- Estimates: Estimaciones (facturación)
SELECT audit.enable_audit_trigger('estimates', 'estimaciones');
SELECT audit.enable_audit_trigger('estimates', 'anticipos');

-- Finance: Transacciones financieras
SELECT audit.enable_audit_trigger('finance', 'journal_entries');
SELECT audit.enable_audit_trigger('finance', 'invoices');
SELECT audit.enable_audit_trigger('finance', 'payments');

-- Assets: Activos fijos
SELECT audit.enable_audit_trigger('assets', 'assets');
SELECT audit.enable_audit_trigger('assets', 'depreciation_schedule');

-- Documents: Documentos y firmas
SELECT audit.enable_audit_trigger('documents', 'documents');
SELECT audit.enable_audit_trigger('documents', 'digital_signatures');

-- HR: Empleados
SELECT audit.enable_audit_trigger('hr', 'employees');

-- HSE: Incidentes de seguridad
SELECT audit.enable_audit_trigger('hse', 'incidentes');

-- ============================================================================
-- FUNCIONES DE CONSULTA
-- ============================================================================

-- Obtener historial de cambios de un registro
CREATE OR REPLACE FUNCTION audit.get_record_history(
    p_table_schema TEXT,
    p_table_name TEXT,
    p_record_id UUID,
    p_limit INT DEFAULT 100
)
RETURNS TABLE (
    change_id UUID,
    operation audit.operation_type,
    changed_fields TEXT[],
    old_data JSONB,
    new_data JSONB,
    user_id UUID,
    executed_at TIMESTAMPTZ
) AS $$
BEGIN
    RETURN QUERY
    SELECT
        cl.id,
        cl.operation,
        cl.changed_fields,
        cl.old_data,
        cl.new_data,
        cl.user_id,
        cl.executed_at
    FROM audit.change_log cl
    WHERE cl.table_schema = p_table_schema
      AND cl.table_name = p_table_name
      AND cl.record_id = p_record_id
    ORDER BY cl.executed_at DESC
    LIMIT p_limit;
END;
$$ LANGUAGE plpgsql;

-- Obtener cambios recientes por tenant
CREATE OR REPLACE FUNCTION audit.get_recent_changes(
    p_tenant_id UUID,
    p_hours INT DEFAULT 24,
    p_limit INT DEFAULT 100
)
RETURNS TABLE (
    change_id UUID,
    table_schema VARCHAR,
    table_name VARCHAR,
    record_id UUID,
    operation audit.operation_type,
    user_id UUID,
    executed_at TIMESTAMPTZ
) AS $$
BEGIN
    RETURN QUERY
    SELECT
        cl.id,
        cl.table_schema,
        cl.table_name,
        cl.record_id,
        cl.operation,
        cl.user_id,
        cl.executed_at
    FROM audit.change_log cl
    WHERE cl.tenant_id = p_tenant_id
      AND cl.executed_at >= NOW() - (p_hours || ' hours')::interval
    ORDER BY cl.executed_at DESC
    LIMIT p_limit;
END;
$$ LANGUAGE plpgsql;

-- ============================================================================
-- COMENTARIOS
-- ============================================================================

COMMENT ON SCHEMA audit IS 'GAP-007: Sistema de auditoría detallada';
COMMENT ON TABLE audit.change_log IS 'Registro de todos los cambios en tablas auditadas';
COMMENT ON FUNCTION audit.log_changes() IS 'Trigger function para registrar cambios';
COMMENT ON FUNCTION audit.enable_audit_trigger(TEXT, TEXT) IS 'Habilitar auditoría en una tabla';
COMMENT ON FUNCTION audit.disable_audit_trigger(TEXT, TEXT) IS 'Deshabilitar auditoría en una tabla';

-- ============================================================================
-- FIN DEL SCRIPT
-- Tablas auditadas: 13 tablas críticas
-- ============================================================================
