-- =========================================================================
--  Esquema de base de datos: ENLACES EXTERNOS, NOTAS Y ADJUNTOS MANUALES
--  Buscador Multisearch — Entrega 6 (monitorización externa)
-- =========================================================================
--
--  PostgreSQL 14+. Extiende los esquemas base (03_db_procesos_schema.sql
--  y 07_db_alertas_y_reglas.sql). Las tablas nuevas referencian las
--  existentes (procesos, eventos_proceso, usuarios).
--
--  Cargar con: psql -d buscador -f 15_db_enlaces_y_notas_schema.sql
-- =========================================================================

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";


-- =========================================================================
--  TABLA: enlaces_proceso
--  URLs externas a monitorizar para cada proceso (tablón electrónico,
--  sede, web del proceso, web del tribunal, otras webs relevantes).
-- =========================================================================

CREATE TABLE enlaces_proceso (
    id_enlace           UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    id_proceso          VARCHAR(200) NOT NULL REFERENCES procesos(id_proceso)
                            ON DELETE CASCADE,

    -- Quién lo añadió
    id_usuario_creador  UUID REFERENCES usuarios(id_usuario)
                            ON DELETE SET NULL,

    -- Clasificación
    tipo                VARCHAR(40) NOT NULL,
        -- 'tablon_electronico', 'sede_electronica', 'web_proceso',
        -- 'web_tribunal', 'otra'
    descripcion         TEXT,        -- "Página oficial del proceso TAG Madrid 2026"
    url                 TEXT NOT NULL,
    dominio             VARCHAR(200), -- extraído al insertar, para rate limit

    -- Configuración de chequeo
    activo              BOOLEAN NOT NULL DEFAULT TRUE,
    frecuencia_minutos  INTEGER NOT NULL DEFAULT 240,
        -- por defecto 4h; se puede sobreescribir manualmente
    frecuencia_auto     BOOLEAN NOT NULL DEFAULT TRUE,
        -- si TRUE, la frecuencia se adapta al estado del proceso

    -- Estado de la última comprobación
    ultimo_check        TIMESTAMPTZ,
    proximo_check       TIMESTAMPTZ,
    ultimo_hash         VARCHAR(64),     -- SHA-256 del contenido normalizado
    ultimo_cambio       TIMESTAMPTZ,     -- cuándo cambió por última vez
    num_archivos_detectados INTEGER NOT NULL DEFAULT 0,
    estado_ultimo_check VARCHAR(20),
        -- 'ok', 'sin_cambios', 'cambio_detectado', 'archivo_nuevo',
        -- 'error_red', 'error_robots', 'error_4xx', 'error_5xx'
    ultimo_error        TEXT,

    -- Notas del responsable
    notas               TEXT,

    -- Metadatos
    fecha_alta          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    fecha_baja          TIMESTAMPTZ
);

CREATE INDEX idx_enlaces_proceso        ON enlaces_proceso(id_proceso);
CREATE INDEX idx_enlaces_proximo_check  ON enlaces_proceso(proximo_check)
                                            WHERE activo = TRUE
                                              AND fecha_baja IS NULL;
CREATE INDEX idx_enlaces_dominio        ON enlaces_proceso(dominio);

-- Restricción: una misma URL no se duplica por proceso
CREATE UNIQUE INDEX idx_enlaces_url_proceso_unico
    ON enlaces_proceso(id_proceso, url)
    WHERE fecha_baja IS NULL;


-- =========================================================================
--  TABLA: snapshots_enlace
--  Histórico de capturas de cada URL. Solo se guarda cuando hay cambio.
-- =========================================================================

CREATE TABLE snapshots_enlace (
    id_snapshot         UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    id_enlace           UUID NOT NULL REFERENCES enlaces_proceso(id_enlace)
                            ON DELETE CASCADE,

    fecha_captura       TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    hash_contenido      VARCHAR(64) NOT NULL,

    -- HTTP
    codigo_http         SMALLINT,
    content_type        VARCHAR(80),
    bytes_descargados   INTEGER,

    -- Contenido (solo se guarda el texto extraído, no el HTML completo,
    -- para ahorrar espacio. El HTML completo se puede guardar en disco
    -- si hace falta).
    texto_extraido      TEXT,

    -- Lista de archivos detectados en esta captura
    archivos_detectados JSONB,
        -- [{"url": "...", "nombre": "...", "tipo": "pdf"}, ...]

    -- Diff resumido respecto al snapshot anterior
    num_palabras_nuevas INTEGER,
    num_palabras_eliminadas INTEGER,
    archivos_nuevos     TEXT[],    -- nombres de archivos nuevos respecto al snapshot anterior
    archivos_eliminados TEXT[],

    -- Búsqueda full-text
    tsv                 TSVECTOR
);

CREATE INDEX idx_snapshots_enlace   ON snapshots_enlace(id_enlace, fecha_captura DESC);
CREATE INDEX idx_snapshots_tsv      ON snapshots_enlace USING GIN(tsv);

CREATE OR REPLACE FUNCTION snapshots_update_trigger()
RETURNS TRIGGER AS $$
BEGIN
    NEW.tsv := to_tsvector('spanish', coalesce(NEW.texto_extraido, ''));
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER snapshots_update_tsv
    BEFORE INSERT OR UPDATE ON snapshots_enlace
    FOR EACH ROW EXECUTE FUNCTION snapshots_update_trigger();


-- =========================================================================
--  TABLA: archivos_detectados
--  Archivos PDF/DOCX/DOC detectados en URLs monitorizadas. Pueden estar
--  procesados o no por el parser, descargados o no.
-- =========================================================================

CREATE TABLE archivos_detectados (
    id_archivo          UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    id_enlace           UUID NOT NULL REFERENCES enlaces_proceso(id_enlace)
                            ON DELETE CASCADE,
    id_proceso          VARCHAR(200) NOT NULL REFERENCES procesos(id_proceso)
                            ON DELETE CASCADE,

    -- Origen
    url_archivo         TEXT NOT NULL,
    nombre_archivo      VARCHAR(300),
    tipo                VARCHAR(20),    -- 'pdf', 'docx', 'doc', 'xlsx', 'otro'
    bytes               INTEGER,

    -- Estado
    descargado          BOOLEAN NOT NULL DEFAULT FALSE,
    descargado_a        TEXT,           -- ruta local relativa
    hash_archivo        VARCHAR(64),    -- SHA-256 del archivo descargado
    procesado           BOOLEAN NOT NULL DEFAULT FALSE,
    procesador_usado    VARCHAR(40),    -- 'parser_bases', 'clasificador_subfases', null
    resultado_proceso   JSONB,          -- output del parser/clasificador

    -- Clasificación derivada
    fase_detectada      VARCHAR(40),
    modificador_detectado VARCHAR(40),
    id_evento_creado    UUID REFERENCES eventos_proceso(id_evento)
                            ON DELETE SET NULL,

    -- Acción para el usuario (cuando el sistema no sabe qué hacer)
    requiere_revision   BOOLEAN NOT NULL DEFAULT FALSE,
    motivo_revision     TEXT,

    -- Metadatos
    fecha_primera_deteccion TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    fecha_descarga          TIMESTAMPTZ,
    fecha_procesamiento     TIMESTAMPTZ
);

CREATE INDEX idx_archivos_enlace        ON archivos_detectados(id_enlace);
CREATE INDEX idx_archivos_proceso       ON archivos_detectados(id_proceso);
CREATE INDEX idx_archivos_pendientes    ON archivos_detectados(procesado, descargado)
                                            WHERE procesado = FALSE;
CREATE INDEX idx_archivos_revision      ON archivos_detectados(requiere_revision)
                                            WHERE requiere_revision = TRUE;
CREATE INDEX idx_archivos_hash          ON archivos_detectados(hash_archivo)
                                            WHERE hash_archivo IS NOT NULL;

-- Restricción: no duplicar archivos por enlace (mismo URL)
CREATE UNIQUE INDEX idx_archivos_url_enlace_unico
    ON archivos_detectados(id_enlace, url_archivo);


-- =========================================================================
--  TABLA: notas_proceso
--  Notas manuales, pendientes, contactos y observaciones asociados a
--  un proceso.
-- =========================================================================

CREATE TABLE notas_proceso (
    id_nota             UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    id_proceso          VARCHAR(200) NOT NULL REFERENCES procesos(id_proceso)
                            ON DELETE CASCADE,

    id_usuario_autor    UUID REFERENCES usuarios(id_usuario)
                            ON DELETE SET NULL,

    -- Clasificación
    tipo                VARCHAR(20) NOT NULL DEFAULT 'nota',
        -- 'nota', 'pendiente', 'contacto', 'observacion', 'alerta_interna'

    -- Contenido
    titulo              VARCHAR(200),
    contenido           TEXT NOT NULL,
        -- texto libre, Markdown ligero (negrita, listas, enlaces)

    -- Específico de tipo='pendiente'
    completado          BOOLEAN NOT NULL DEFAULT FALSE,
    fecha_completado    TIMESTAMPTZ,
    id_usuario_asignado UUID REFERENCES usuarios(id_usuario)
                            ON DELETE SET NULL,
    fecha_limite        DATE,

    -- Específico de tipo='contacto'
    contacto_nombre     VARCHAR(200),
    contacto_rol        VARCHAR(200),
    contacto_telefono   VARCHAR(40),
    contacto_email      VARCHAR(200),
    contacto_organismo  VARCHAR(200),

    -- Prioridad
    prioridad           VARCHAR(10) NOT NULL DEFAULT 'normal',
        -- 'baja', 'normal', 'alta'

    -- Visibilidad (privada al autor o compartida con la organización)
    visibilidad         VARCHAR(20) NOT NULL DEFAULT 'organizacion',
        -- 'privada', 'organizacion'

    -- Metadatos
    fecha_creacion      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    fecha_actualizacion TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- Búsqueda full-text
    tsv                 TSVECTOR
);

CREATE INDEX idx_notas_proceso          ON notas_proceso(id_proceso, fecha_creacion DESC);
CREATE INDEX idx_notas_tipo             ON notas_proceso(id_proceso, tipo);
CREATE INDEX idx_notas_pendientes       ON notas_proceso(id_usuario_asignado, completado, fecha_limite)
                                            WHERE tipo = 'pendiente' AND completado = FALSE;
CREATE INDEX idx_notas_tsv              ON notas_proceso USING GIN(tsv);

CREATE OR REPLACE FUNCTION notas_update_trigger()
RETURNS TRIGGER AS $$
BEGIN
    NEW.tsv := to_tsvector('spanish',
        coalesce(NEW.titulo, '') || ' ' ||
        coalesce(NEW.contenido, '') || ' ' ||
        coalesce(NEW.contacto_nombre, '') || ' ' ||
        coalesce(NEW.contacto_organismo, '')
    );
    NEW.fecha_actualizacion := NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER notas_update_tsv
    BEFORE INSERT OR UPDATE ON notas_proceso
    FOR EACH ROW EXECUTE FUNCTION notas_update_trigger();


-- =========================================================================
--  TABLA: etiquetas_proceso
--  Etiquetas libres por usuario u organización (para filtrar luego).
-- =========================================================================

CREATE TABLE etiquetas_proceso (
    id_etiqueta         UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    id_proceso          VARCHAR(200) NOT NULL REFERENCES procesos(id_proceso)
                            ON DELETE CASCADE,

    etiqueta            VARCHAR(80) NOT NULL,
    color               VARCHAR(20),   -- "#hexcolor" opcional

    -- Ámbito
    id_usuario          UUID REFERENCES usuarios(id_usuario)
                            ON DELETE CASCADE,
    organizacion        VARCHAR(80),
    -- (NULL en ambos = etiqueta del sistema)

    fecha_creacion      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_etiquetas_proceso  ON etiquetas_proceso(id_proceso);
CREATE INDEX idx_etiquetas_texto    ON etiquetas_proceso USING GIN(etiqueta gin_trgm_ops);
CREATE INDEX idx_etiquetas_usuario  ON etiquetas_proceso(id_usuario)
                                        WHERE id_usuario IS NOT NULL;

-- Restricción: no duplicar etiqueta-proceso para mismo ámbito
CREATE UNIQUE INDEX idx_etiquetas_unica
    ON etiquetas_proceso(id_proceso, etiqueta,
                         COALESCE(id_usuario::text, ''),
                         COALESCE(organizacion, ''));


-- =========================================================================
--  TABLA: adjuntos_manuales_proceso
--  Archivos que el equipo sube manualmente a un proceso (los que llegan
--  por otra vía que no sea el watcher ni los boletines).
-- =========================================================================

CREATE TABLE adjuntos_manuales_proceso (
    id_adjunto          UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    id_proceso          VARCHAR(200) NOT NULL REFERENCES procesos(id_proceso)
                            ON DELETE CASCADE,
    id_usuario          UUID REFERENCES usuarios(id_usuario)
                            ON DELETE SET NULL,

    nombre_archivo      VARCHAR(300) NOT NULL,
    nombre_original     VARCHAR(300),
    tipo_mime           VARCHAR(80),
    bytes               INTEGER,
    hash_archivo        VARCHAR(64),
    ruta_almacenamiento TEXT NOT NULL,

    descripcion         TEXT,

    -- ¿Procesarlo automáticamente como si viniera del watcher?
    procesar_automatico BOOLEAN NOT NULL DEFAULT FALSE,
    procesado           BOOLEAN NOT NULL DEFAULT FALSE,
    resultado_proceso   JSONB,

    fecha_subida        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    fecha_procesamiento TIMESTAMPTZ
);

CREATE INDEX idx_adjuntos_proceso   ON adjuntos_manuales_proceso(id_proceso,
                                                                 fecha_subida DESC);
CREATE INDEX idx_adjuntos_hash      ON adjuntos_manuales_proceso(hash_archivo)
                                        WHERE hash_archivo IS NOT NULL;


-- =========================================================================
--  TABLA: dominios_visitados
--  Para rate limiting global: una request cada 10s por dominio.
-- =========================================================================

CREATE TABLE dominios_visitados (
    dominio             VARCHAR(200) PRIMARY KEY,
    ultima_visita       TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    requests_24h        INTEGER NOT NULL DEFAULT 0,
    fecha_reset_contador TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    -- Cache de robots.txt
    robots_txt          TEXT,
    robots_txt_fetched  TIMESTAMPTZ,
    robots_permite_bot  BOOLEAN NOT NULL DEFAULT TRUE
);


-- =========================================================================
--  VISTA: enlaces_para_chequear
--  URLs que ya toca volver a visitar.
-- =========================================================================

CREATE OR REPLACE VIEW enlaces_para_chequear AS
SELECT
    e.id_enlace,
    e.id_proceso,
    e.url,
    e.dominio,
    e.tipo,
    e.frecuencia_minutos,
    e.ultimo_check,
    e.ultimo_hash,
    p.estado AS estado_proceso
FROM enlaces_proceso e
JOIN procesos p ON p.id_proceso = e.id_proceso
WHERE e.activo = TRUE
  AND e.fecha_baja IS NULL
  AND p.estado NOT IN ('finalizado', 'archivado')
  AND (e.proximo_check IS NULL OR e.proximo_check <= NOW())
ORDER BY
    -- Priorizar procesos en estado activo
    CASE p.estado
        WHEN 'plazo_abierto' THEN 1
        WHEN 'plazo_cerrado' THEN 2
        WHEN 'en_pruebas' THEN 3
        WHEN 'bases_publicadas' THEN 4
        WHEN 'oep_publicada' THEN 5
        ELSE 9
    END,
    e.proximo_check NULLS FIRST;


-- =========================================================================
--  VISTA: pendientes_de_usuario
--  Tareas pendientes asignadas al usuario, no completadas.
-- =========================================================================

CREATE OR REPLACE VIEW pendientes_de_usuario AS
SELECT
    n.id_nota,
    n.id_proceso,
    n.titulo,
    n.contenido,
    n.prioridad,
    n.fecha_limite,
    n.fecha_creacion,
    n.id_usuario_asignado,
    p.administracion_nombre,
    p.cuerpo_nombre,
    -- Días hasta el límite (negativo = pasado)
    CASE
        WHEN n.fecha_limite IS NULL THEN NULL
        ELSE (n.fecha_limite - CURRENT_DATE)::INTEGER
    END AS dias_para_limite
FROM notas_proceso n
JOIN procesos p ON p.id_proceso = n.id_proceso
WHERE n.tipo = 'pendiente'
  AND n.completado = FALSE
ORDER BY
    CASE WHEN n.fecha_limite IS NULL THEN 1 ELSE 0 END,
    n.fecha_limite ASC,
    CASE n.prioridad
        WHEN 'alta' THEN 1
        WHEN 'normal' THEN 2
        WHEN 'baja' THEN 3
    END;


-- =========================================================================
--  FUNCIÓN: calcular_proximo_check
--  Devuelve la próxima fecha de chequeo según el estado del proceso y
--  la frecuencia configurada.
-- =========================================================================

CREATE OR REPLACE FUNCTION calcular_proximo_check(
    p_id_enlace UUID
) RETURNS TIMESTAMPTZ AS $$
DECLARE
    v_minutos       INTEGER;
    v_auto          BOOLEAN;
    v_estado        VARCHAR(40);
BEGIN
    SELECT e.frecuencia_minutos, e.frecuencia_auto, p.estado
    INTO v_minutos, v_auto, v_estado
    FROM enlaces_proceso e
    JOIN procesos p ON p.id_proceso = e.id_proceso
    WHERE e.id_enlace = p_id_enlace;

    -- Si frecuencia_auto, recalcular según estado
    IF v_auto THEN
        v_minutos := CASE v_estado
            WHEN 'oep_publicada'      THEN 1440  -- 24h
            WHEN 'bases_publicadas'   THEN 720   -- 12h
            WHEN 'plazo_abierto'      THEN 240   -- 4h
            WHEN 'plazo_cerrado'      THEN 120   -- 2h
            WHEN 'en_pruebas'         THEN 240   -- 4h
            ELSE 720
        END;
    END IF;

    RETURN NOW() + (v_minutos || ' minutes')::INTERVAL;
END;
$$ LANGUAGE plpgsql;


-- =========================================================================
--  COMENTARIOS DE USO
-- =========================================================================
--
--  Flujo típico de monitorización:
--
--  1. Usuario registra una URL desde el dashboard:
--     INSERT INTO enlaces_proceso (id_proceso, tipo, url, descripcion, ...)
--
--  2. Un job nocturno (o cada 5 minutos) lee la vista
--     `enlaces_para_chequear` y procesa los pendientes:
--     - Llama a 16_web_watcher.py para cada uno
--     - El watcher comprueba robots.txt, hace rate limit, descarga,
--       compara con ultimo_hash, detecta cambios y archivos nuevos.
--
--  3. Si hay cambio sin archivos nuevos:
--     - INSERT en snapshots_enlace con el nuevo hash y texto
--     - UPDATE enlaces_proceso con ultimo_hash, ultimo_cambio, proximo_check
--     - Crea evento en eventos_notificacion para avisar al usuario
--
--  4. Si hay archivo nuevo:
--     - INSERT en archivos_detectados con la URL del archivo
--     - El watcher descarga el archivo y lo guarda en disco
--     - UPDATE archivos_detectados con descargado=TRUE, hash, ruta
--     - Heurística decide si pasar por parser_bases.py:
--       * Si parece bases (nombre contiene "bases", "convocatoria"...)
--         → parser_bases.py + crear evento de fase "bases"
--       * Si parece lista de admitidos, calificaciones, tribunal...
--         → clasificador_fases + crear evento de la sub-fase correspondiente
--       * Si no encaja → requiere_revision = TRUE
--
--  5. Para notas, contactos, etiquetas y adjuntos manuales: CRUD
--     normal desde el dashboard, sin pipeline automática.
--
--  Ejemplos de queries útiles:
--
--  -- Próximas URLs a chequear:
--  SELECT * FROM enlaces_para_chequear LIMIT 20;
--
--  -- Archivos pendientes de procesar por el parser:
--  SELECT * FROM archivos_detectados
--  WHERE descargado = TRUE AND procesado = FALSE
--  ORDER BY fecha_descarga;
--
--  -- Pendientes del usuario actual:
--  SELECT * FROM pendientes_de_usuario
--  WHERE id_usuario_asignado = '<uuid>';
--
--  -- Buscar en todas las notas el texto "tribunal retraso":
--  SELECT n.*, p.cuerpo_nombre, p.administracion_nombre
--  FROM notas_proceso n
--  JOIN procesos p ON p.id_proceso = n.id_proceso
--  WHERE n.tsv @@ plainto_tsquery('spanish', 'tribunal retraso');
