<?php
/**
 * Factory para Proyectos Administrativos
 *
 * Gestiona la creación, consulta y modificación de proyectos administrativos.
 * Los proyectos administrativos están asociados a trabajadores de la firma
 * y contienen 4 secciones fijas (3 de progreso + 1 de registros).
 *
 * @author NICC
 * @date 2026-01-31
 * @updated 2026-02-01
 */

namespace dokuwiki\plugin\bez\mdl;

require_once(dirname(__DIR__) . '/meta/path_helper.php');

class AdminThreadFactory {

    /** @var \helper_plugin_sqlite */
    protected $sqlite;

    /** @var string Usuario actual */
    protected $currentUser;

    /** @var array Secciones predefinidas para cada proyecto */
    protected $seccionesPredefinidas = [
        1 => [
            'nombre' => 'Responsabilidades de la Dirección',
            'template' => 'varios/menu/1.txt',
            'archivo' => '1_responsabilidades',
            'aporta_progreso' => 1
        ],
        2 => [
            'nombre' => 'Requisitos Éticos',
            'template' => 'varios/menu/2.txt',
            'archivo' => '2_requisitos_eticos',
            'aporta_progreso' => 1
        ],
        3 => [
            'nombre' => 'Recursos Humanos',
            'template' => 'varios/menu/4.txt',
            'archivo' => '3_recursos_humanos',
            'aporta_progreso' => 1
        ],
        4 => [
            'nombre' => 'Registros de Cumplimiento General',
            'template' => null,  // No tiene template, es enlace directo
            'archivo' => null,
            'ruta_fija' => 'encargos:general',  // Se sobrescribe con pagina_registros
            'aporta_progreso' => 0
        ]
    ];

    /**
     * Constructor
     * @param \helper_plugin_sqlite $sqlite Conexión a BD
     * @param string $currentUser Usuario actual
     */
    public function __construct($sqlite, $currentUser = null) {
        $this->sqlite = $sqlite;
        $this->currentUser = $currentUser ?: $_SERVER['REMOTE_USER'] ?? 'system';
    }

    /**
     * Determina la base del namespace de filelisting según paginaRegistros.
     *
     * Mapeo:
     *   encargos:general  → varios:menu
     *   encargos:general1 → varios:menu1
     *   encargos:generalN → varios:menuN
     *
     * @param string $paginaRegistros  ej: 'encargos:general', 'encargos:general1'
     * @return string  ej: 'varios:menu', 'varios:menu1'
     */
    protected function getMenuBase($paginaRegistros) {
        $partes  = explode(':', $paginaRegistros);
        $nombre  = end($partes);               // "general", "general1", …
        $suffix  = str_replace('general', '', $nombre); // "", "1", "2", …
        return 'varios:menu' . $suffix;        // "varios:menu", "varios:menu1", …
    }

    // =========================================
    // CONSECUTIVOS
    // =========================================

    /**
     * Obtiene el siguiente ID disponible para un proyecto administrativo
     * @return int
     */
    public function getNextId() {
        $this->ensureSequenceTable();
        // Actualizar y obtener siguiente ID
        $this->sqlite->query("UPDATE admin_thread_sequence SET last_id = last_id + 1 WHERE id = 1");
        $res = $this->sqlite->query("SELECT last_id FROM admin_thread_sequence WHERE id = 1");
        $arr = $this->sqlite->res2arr($res);
        if (empty($arr)) {
            // Tabla vacía — reinicializar con ID 1
            $this->sqlite->query("INSERT INTO admin_thread_sequence (id, last_id) VALUES (1, 1)");
            return 1;
        }
        return intval($arr[0]['last_id']);
    }

    /**
     * Consulta cuál sería el próximo ID sin incrementar el contador.
     *
     * Útil para que el formulario de creación pueda pre-seleccionar
     * el destino (generalN) que coincida con el ID del proyecto.
     *
     * @return int
     */
    public function peekNextId() {
        $this->ensureSequenceTable();
        $res = $this->sqlite->query("SELECT last_id FROM admin_thread_sequence WHERE id = 1");
        $arr = $this->sqlite->res2arr($res);
        if (empty($arr)) {
            $this->sqlite->query("INSERT INTO admin_thread_sequence (id, last_id) VALUES (1, 0)");
            return 1;
        }
        return intval($arr[0]['last_id']) + 1;
    }

    /**
     * Verifica si la tabla de secuencia existe, si no la crea
     */
    public function ensureSequenceTable() {
        $res = $this->sqlite->query("SELECT name FROM sqlite_master WHERE type='table' AND name='admin_thread_sequence'");
        $arr = $this->sqlite->res2arr($res);

        if (empty($arr)) {
            $this->sqlite->query("CREATE TABLE admin_thread_sequence (id INTEGER PRIMARY KEY, last_id INTEGER NOT NULL DEFAULT 0)");
            $this->sqlite->query("INSERT INTO admin_thread_sequence (id, last_id) VALUES (1, 0)");
        }
    }

    /**
     * Verifica si las tablas principales existen, si no las crea
     */
    public function ensureTables() {
        // Verificar tabla admin_thread
        $res = $this->sqlite->query("SELECT name FROM sqlite_master WHERE type='table' AND name='admin_thread'");
        $arr = $this->sqlite->res2arr($res);

        if (empty($arr)) {
            $this->sqlite->query("CREATE TABLE admin_thread (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                trabajador_id TEXT NOT NULL,
                trabajador_nombre TEXT NOT NULL,
                tipo_trabajador TEXT NOT NULL DEFAULT 'usuario',
                pagina_registros TEXT NOT NULL DEFAULT 'encargos:general',
                created_by TEXT NOT NULL,
                created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                state TEXT NOT NULL DEFAULT 'opened',
                closed_by TEXT,
                closed_at DATETIME,
                fecha_inicio TEXT,
                fecha_fin TEXT,
                observaciones TEXT,
                periodo TEXT DEFAULT NULL,
                privado INTEGER DEFAULT 0,
                ruta_manual_calidad TEXT DEFAULT NULL,
                ruta_codigo_etica TEXT DEFAULT NULL,
                deleted_at DATETIME DEFAULT NULL
            )");
            $this->sqlite->query("CREATE INDEX idx_admin_thread_trabajador ON admin_thread(trabajador_id)");
            $this->sqlite->query("CREATE INDEX idx_admin_thread_state ON admin_thread(state)");
        }

        // Verificar tabla admin_thread_progress
        $res = $this->sqlite->query("SELECT name FROM sqlite_master WHERE type='table' AND name='admin_thread_progress'");
        $arr = $this->sqlite->res2arr($res);

        if (empty($arr)) {
            $this->sqlite->query("CREATE TABLE admin_thread_progress (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                admin_thread_id INTEGER NOT NULL,
                seccion_id INTEGER NOT NULL,
                seccion_nombre TEXT NOT NULL,
                ruta_wiki TEXT NOT NULL,
                total_requerimientos INTEGER DEFAULT 0,
                requerimientos_terminados INTEGER DEFAULT 0,
                porcentaje_progreso REAL DEFAULT 0,
                aporta_progreso INTEGER DEFAULT 1,
                visible INTEGER DEFAULT 1,
                updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (admin_thread_id) REFERENCES admin_thread(id) ON DELETE CASCADE,
                UNIQUE(admin_thread_id, seccion_id)
            )");
        }

        // Asegurar tabla de secuencia
        $this->ensureSequenceTable();

        // Verificar tabla admin_thread_preferencia
        $res = $this->sqlite->query("SELECT name FROM sqlite_master WHERE type='table' AND name='admin_thread_preferencia'");
        $arr = $this->sqlite->res2arr($res);

        if (empty($arr)) {
            $this->sqlite->query("CREATE TABLE admin_thread_preferencia (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                admin_thread_id INTEGER NOT NULL UNIQUE,
                marcado_por TEXT NOT NULL,
                marcado_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (admin_thread_id) REFERENCES admin_thread(id) ON DELETE CASCADE
            )");
        }
    }

    // =========================================
    // DOCUMENTOS POR PROYECTO (Manual de Calidad / Código de Ética)
    // =========================================

    /**
     * Obtiene el índice (0-based) del participante de un proyecto,
     * basado en el orden de aparición de trabajador_id únicos.
     *
     * @param int $projectId ID del proyecto
     * @return int Índice del participante (0 = primer participante)
     */
    public function getParticipantIndex($projectId) {
        $project = $this->get($projectId);
        if (!$project) return 0;

        $trabajadorId = $project['trabajador_id'];

        // Obtener participantes únicos ordenados por primer proyecto creado
        $res = $this->sqlite->query(
            "SELECT trabajador_id, MIN(id) as first_project
             FROM admin_thread
             WHERE deleted_at IS NULL
             GROUP BY trabajador_id
             ORDER BY first_project ASC"
        );
        $participants = $this->sqlite->res2arr($res);

        foreach ($participants as $idx => $p) {
            if ($p['trabajador_id'] === $trabajadorId) {
                return $idx;
            }
        }
        return 0;
    }

    /**
     * Calcula la ruta por defecto de un documento según el participante del proyecto.
     *
     * Patrón (por participante único, no por ID de proyecto):
     *   Participante 0 (primero) → compartidos:{docName}
     *   Participante 1 (segundo) → compartidos:1:{docName}
     *   Participante N           → compartidos:{N}:{docName}
     *
     * @param int $projectId ID del proyecto administrativo
     * @param string $docName Nombre del documento (ej: 'manual_de_calidad')
     * @return string Ruta wiki del documento
     */
    public function getDefaultDocPath($projectId, $docName) {
        $idx = $this->getParticipantIndex($projectId);
        if ($idx <= 0) {
            return 'compartidos:' . $docName;
        }
        return 'compartidos:' . $idx . ':' . $docName;
    }

    /**
     * Obtiene las rutas de los documentos para un proyecto.
     * Si las rutas no están configuradas (NULL), usa las por defecto.
     *
     * @param int $projectId ID del proyecto
     * @return array|null ['manual_de_calidad' => '...', 'codigo_de_etica' => '...'] o null si no existe
     */
    public function getDocPaths($projectId) {
        $project = $this->get($projectId);
        if (!$project) return null;

        $manualPath = !empty($project['ruta_manual_calidad'])
            ? $project['ruta_manual_calidad']
            : $this->getDefaultDocPath($projectId, 'manual_de_calidad');

        $eticaPath = !empty($project['ruta_codigo_etica'])
            ? $project['ruta_codigo_etica']
            : $this->getDefaultDocPath($projectId, 'codigo_de_etica');

        return [
            'manual_de_calidad' => $manualPath,
            'codigo_de_etica' => $eticaPath
        ];
    }

    /**
     * Actualiza la página varios:generalidades con las rutas de documentos
     * del proyecto indicado.
     *
     * Reemplaza los links de manual de calidad (boton2:79.png) y
     * código de ética (boton2:26.png) con las rutas del proyecto.
     *
     * @param int $projectId ID del proyecto
     * @return bool true si se actualizó, false si no
     */
    public function actualizarGeneralidades($projectId) {
        $paths = $this->getDocPaths($projectId);
        if (!$paths) return false;

        $pageId = 'varios:generalidades';
        $content = rawWiki($pageId);
        if (empty($content)) return false;

        $updated = false;

        // Reemplazar link del Manual de Calidad (identificado por boton2:79.png)
        $newContent = preg_replace(
            '/\[\[\{:boton2:79\.png\}:[^\|]+\|/',
            '[[{:boton2:79.png}:' . $paths['manual_de_calidad'] . '|',
            $content
        );
        if ($newContent !== $content) {
            $content = $newContent;
            $updated = true;
        }

        // Reemplazar link del Código de Ética (identificado por boton2:26.png)
        $newContent = preg_replace(
            '/\[\[\{:boton2:26\.png\}:[^\|]+\|/',
            '[[{:boton2:26.png}:' . $paths['codigo_de_etica'] . '|',
            $content
        );
        if ($newContent !== $content) {
            $content = $newContent;
            $updated = true;
        }

        if ($updated) {
            saveWikiText($pageId, $content, "Actualización rutas documentos - Proyecto #$projectId", false);
        }

        return $updated;
    }

    /**
     * Asegura que existan las páginas de documentos para un proyecto.
     * Si no existen, las crea duplicando el contenido de las páginas base.
     *
     * @param int $projectId ID del proyecto
     */
    public function ensureDocumentPages($projectId) {
        $paths = $this->getDocPaths($projectId);
        if (!$paths) return;

        $baseDocuments = [
            'manual_de_calidad' => 'compartidos:manual_de_calidad',
            'codigo_de_etica' => 'compartidos:codigo_de_etica'
        ];

        foreach ($baseDocuments as $docKey => $basePath) {
            $targetPath = $paths[$docKey];

            // Si la ruta es la misma que la base, no copiar
            if ($targetPath === $basePath) continue;

            // Si ya existe, no sobrescribir
            if (page_exists($targetPath)) continue;

            // Copiar desde la base si existe
            if (page_exists($basePath)) {
                $content = rawWiki($basePath);
                saveWikiText(
                    $targetPath,
                    $content,
                    "Copia automática desde $basePath - Proyecto #$projectId",
                    false
                );
                $this->aprobarPagina($targetPath);
            }
        }
    }

    /**
     * Escanea recursivamente el namespace compartidos para listar
     * todas las páginas wiki disponibles.
     *
     * @return array Lista de páginas: [['id' => '...', 'name' => '...', 'namespace' => '...'], ...]
     */
    public function scanCompartidosNamespace() {
        $base = bez_resolve_path('data/pages/compartidos/');
        $results = [];

        if (!is_dir($base)) return $results;

        $this->_scanDirRecursive($base, 'compartidos', $results);
        return $results;
    }

    /**
     * Escaneo recursivo de directorio para buscar páginas wiki (.txt)
     *
     * @param string $dir Directorio a escanear
     * @param string $namespace Namespace acumulado
     * @param array &$results Array de resultados (por referencia)
     */
    protected function _scanDirRecursive($dir, $namespace, &$results) {
        $items = @scandir($dir);
        if ($items === false) return;

        foreach ($items as $item) {
            if ($item === '.' || $item === '..') continue;
            $path = $dir . $item;
            if (is_dir($path)) {
                $this->_scanDirRecursive($path . '/', $namespace . ':' . $item, $results);
            } elseif (substr($item, -4) === '.txt') {
                $pageName = substr($item, 0, -4);
                $results[] = [
                    'id' => $namespace . ':' . $pageName,
                    'name' => $pageName,
                    'namespace' => $namespace
                ];
            }
        }
    }

    /**
     * Verifica si un proyecto es el preferido actualmente
     *
     * @param int $projectId
     * @return bool
     */
    public function isPreferido($projectId) {
        try {
            $res = $this->sqlite->query("SELECT admin_thread_id FROM admin_thread_preferencia LIMIT 1");
            $arr = $this->sqlite->res2arr($res);
            return !empty($arr) && intval($arr[0]['admin_thread_id']) === intval($projectId);
        } catch (\Exception $e) {
            return false;
        }
    }

    // =========================================
    // CRUD DE PROYECTOS
    // =========================================

    /**
     * Crea un nuevo proyecto administrativo
     * @param string $trabajadorId Username del trabajador o 'firma_principal'
     * @param string $trabajadorNombre Nombre completo
     * @param string $tipoTrabajador 'usuario' o 'firma'
     * @param string $paginaRegistros Página para botón 4 (encargos:general, encargos:general1, etc.)
     * @param string $fechaInicio Fecha de inicio (opcional)
     * @param string $observaciones Observaciones (opcional)
     * @param string $periodo Año del periodo (opcional)
     * @param int $privado 1=privado, 0=público (opcional)
     * @param string|null $rutaManualCalidad Ruta wiki del manual de calidad (null=default)
     * @param string|null $rutaCodigoEtica Ruta wiki del código de ética (null=default)
     * @return int ID del proyecto creado
     */
    public function crear($trabajadorId, $trabajadorNombre, $tipoTrabajador = 'usuario', $paginaRegistros = 'encargos:general', $fechaInicio = null, $observaciones = null, $periodo = null, $privado = 0, $rutaManualCalidad = null, $rutaCodigoEtica = null) {
        // Asegurar que existen las tablas necesarias
        $this->ensureTables();

        // Obtener nuevo ID
        $newId = $this->getNextId();

        // Verificar qué columnas existen en la tabla
        $columnsExist = $this->checkColumnsExist();

        // Construir INSERT dinámicamente según las columnas disponibles
        $columns = ['id', 'trabajador_id', 'trabajador_nombre', 'created_by', 'fecha_inicio', 'observaciones'];
        $values = [$newId, $trabajadorId, $trabajadorNombre, $this->currentUser, $fechaInicio, $observaciones];

        if ($columnsExist['tipo_trabajador']) {
            $columns[] = 'tipo_trabajador';
            $values[] = $tipoTrabajador;
        }
        if ($columnsExist['pagina_registros']) {
            $columns[] = 'pagina_registros';
            $values[] = $paginaRegistros;
        }
        if ($columnsExist['periodo']) {
            $columns[] = 'periodo';
            $values[] = $periodo;
        }
        if ($columnsExist['privado']) {
            $columns[] = 'privado';
            $values[] = $privado;
        }
        if ($columnsExist['ruta_manual_calidad'] && $rutaManualCalidad !== null && $rutaManualCalidad !== '') {
            $columns[] = 'ruta_manual_calidad';
            $values[] = $rutaManualCalidad;
        }
        if ($columnsExist['ruta_codigo_etica'] && $rutaCodigoEtica !== null && $rutaCodigoEtica !== '') {
            $columns[] = 'ruta_codigo_etica';
            $values[] = $rutaCodigoEtica;
        }

        $placeholders = implode(', ', array_fill(0, count($columns), '?'));
        $sql = "INSERT INTO admin_thread (" . implode(', ', $columns) . ") VALUES ($placeholders)";
        $this->sqlite->query($sql, $values);

        // Crear secciones de progreso (pasando pagina_registros para el botón 4)
        $this->crearSecciones($newId, $paginaRegistros);

        // Asegurar que existan las carpetas de media para el destino numerado
        $this->ensureMediaFolders($paginaRegistros);

        // Crear páginas wiki desde templates (selecciona template según destino)
        $this->crearPaginasWiki($newId, $paginaRegistros);

        // Crear página de registros (encargos:general, general1, etc.)
        $this->crearPaginaRegistros($newId, $paginaRegistros);

        // Asegurar que existan las páginas de documentos (manual calidad, código ética)
        $this->ensureDocumentPages($newId);

        return $newId;
    }

    /**
     * Verifica qué columnas nuevas existen en la tabla admin_thread
     * @return array
     */
    protected function checkColumnsExist() {
        $result = [
            'tipo_trabajador' => false,
            'pagina_registros' => false,
            'periodo' => false,
            'privado' => false,
            'ruta_manual_calidad' => false,
            'ruta_codigo_etica' => false
        ];

        try {
            $res = $this->sqlite->query("PRAGMA table_info(admin_thread)");
            $columns = $this->sqlite->res2arr($res);

            foreach ($columns as $col) {
                $name = $col['name'];
                if (isset($result[$name])) {
                    $result[$name] = true;
                }
            }
        } catch (\Exception $e) {
            // Si falla, asumir que no existen
        }

        return $result;
    }

    /**
     * Asegura que existan las carpetas de media necesarias para el destino.
     *
     * Cuando el destino es "generalN" (N > 0), los formatos residen en
     * subcarpetas numeradas: /data/media/administrativa/N/, etc.
     * Si la subcarpeta /N/ no existe, se duplica desde /1/ (que siempre existe).
     *
     * También asegura que existan los templates de páginas (varios/menuN/)
     * copiándolos desde varios/menu1/ con los paths ajustados.
     *
     * Carpetas de media afectadas:
     *   - administrativa, comercial, operativo, rrhh, compartidos
     *
     * @param string $paginaRegistros ej: 'encargos:general4'
     */
    protected function ensureMediaFolders($paginaRegistros) {
        $partes = explode(':', $paginaRegistros);
        $nombre = end($partes); // "general", "general1", "general4", etc.
        $numero = str_replace('general', '', $nombre); // "", "1", "4", etc.

        // Solo aplica para destinos numerados (general1, general2, ...)
        if ($numero === '' || !is_numeric($numero)) {
            return;
        }

        $n = intval($numero);
        if ($n <= 0) return;

        // ---- 1. Carpetas de media ----
        $carpetasMedia = ['administrativa', 'comercial', 'operativo', 'rrhh', 'compartidos'];

        foreach ($carpetasMedia as $area) {
            $carpetaDestino = bez_resolve_path('data/media/' . $area . '/' . $n);
            $carpetaOrigen  = bez_resolve_path('data/media/' . $area . '/1');

            if (!is_dir($carpetaDestino) && is_dir($carpetaOrigen)) {
                $this->copyDirectoryRecursive($carpetaOrigen, $carpetaDestino);
            }
        }

        // ---- 2. Templates de páginas (varios/menuN/) ----
        $menuDestinoDir = bez_resolve_path('data/pages/varios/menu' . $n);
        $menuOrigenDir  = bez_resolve_path('data/pages/varios/menu1');

        if (!is_dir($menuDestinoDir) && is_dir($menuOrigenDir)) {
            // Copiar todos los archivos de menu1/ a menuN/
            mkdir($menuDestinoDir, 0755, true);

            $archivos = scandir($menuOrigenDir);
            if ($archivos !== false) {
                $areasReemplazo = ['compartidos', 'rrhh', 'administrativa', 'operativo', 'comercial'];

                foreach ($archivos as $archivo) {
                    if ($archivo === '.' || $archivo === '..') continue;

                    $srcPath = $menuOrigenDir . DIRECTORY_SEPARATOR . $archivo;
                    $dstPath = $menuDestinoDir . DIRECTORY_SEPARATOR . $archivo;

                    if (is_file($srcPath)) {
                        $contenido = file_get_contents($srcPath);

                        // Ajustar rutas de formatos: :area:1: → :area:N:
                        foreach ($areasReemplazo as $area) {
                            $contenido = str_replace(
                                ':' . $area . ':1:',
                                ':' . $area . ':' . $n . ':',
                                $contenido
                            );
                        }

                        file_put_contents($dstPath, $contenido);
                    }
                }
            }
        }
    }

    /**
     * Copia un directorio recursivamente
     *
     * @param string $source Directorio origen
     * @param string $dest Directorio destino
     */
    protected function copyDirectoryRecursive($source, $dest) {
        if (!is_dir($source)) return;

        if (!is_dir($dest)) {
            mkdir($dest, 0755, true);
        }

        $files = scandir($source);
        if ($files === false) return;

        foreach ($files as $file) {
            if ($file === '.' || $file === '..') continue;

            $srcPath = $source . DIRECTORY_SEPARATOR . $file;
            $dstPath = $dest . DIRECTORY_SEPARATOR . $file;

            if (is_dir($srcPath)) {
                $this->copyDirectoryRecursive($srcPath, $dstPath);
            } else {
                copy($srcPath, $dstPath);
            }
        }
    }

    /**
     * Crea las 4 secciones de progreso para un proyecto
     * @param int $adminThreadId
     * @param string $paginaRegistros Página para la sección 4
     */
    protected function crearSecciones($adminThreadId, $paginaRegistros = 'encargos:general') {
        $sql = "INSERT INTO admin_thread_progress (admin_thread_id, seccion_id, seccion_nombre, ruta_wiki, aporta_progreso)
                VALUES (?, ?, ?, ?, ?)";

        foreach ($this->seccionesPredefinidas as $seccionId => $seccion) {
            // Determinar ruta wiki
            if ($seccionId == 4) {
                // Botón 4: usar la página de registros seleccionada
                $rutaWiki = $paginaRegistros;
            } elseif (isset($seccion['ruta_fija'])) {
                $rutaWiki = $seccion['ruta_fija'];
            } else {
                $rutaWiki = "proyectos:administrativos:$adminThreadId:" . $seccion['archivo'];
            }

            $this->sqlite->query($sql, [
                $adminThreadId,
                $seccionId,
                $seccion['nombre'],
                $rutaWiki,
                $seccion['aporta_progreso']
            ]);
        }
    }

    /**
     * Crea las páginas wiki desde los templates.
     *
     * Selecciona el template correcto según paginaRegistros:
     *   encargos:general  → lee de data/pages/varios/menu/
     *   encargos:general1 → lee de data/pages/varios/menu1/
     *
     * @param int    $adminThreadId
     * @param string $paginaRegistros  ej: 'encargos:general'
     */
    protected function crearPaginasWiki($adminThreadId, $paginaRegistros = 'encargos:general') {
        // Base del template según destino (varios/menu, varios/menu1, …)
        $menuBase     = $this->getMenuBase($paginaRegistros);
        $templateBase = str_replace(':', '/', $menuBase);  // "varios/menu" o "varios/menu1"

        // Determinar número del destino para fallback inteligente
        $partesDest = explode(':', $paginaRegistros);
        $nombreDest = end($partesDest);
        $numeroDest = str_replace('general', '', $nombreDest); // "", "1", "4", etc.
        $esNumerado = ($numeroDest !== '' && is_numeric($numeroDest) && intval($numeroDest) > 0);

        foreach ($this->seccionesPredefinidas as $seccionId => $seccion) {
            // Solo crear si tiene template
            if (empty($seccion['template'])) {
                continue;
            }

            $destino = "proyectos:administrativos:$adminThreadId:" . $seccion['archivo'];

            // Verificar si ya existe
            if (page_exists($destino)) {
                continue;
            }

            // Intentar primero el template específico del destino (ej: varios/menu4/1.txt)
            $specificTemplate = str_replace('varios/menu/', $templateBase . '/', $seccion['template']);
            $specificPath     = bez_resolve_path('data/pages/' . $specificTemplate);
            $defaultPath      = bez_resolve_path('data/pages/' . str_replace(':', '/', $seccion['template']));

            $needsPathAdjust = false;

            if (file_exists($specificPath)) {
                // Template específico encontrado (ej: varios/menu4/1.txt)
                $templatePath = $specificPath;
            } elseif ($esNumerado) {
                // Para destinos numerados (generalN), usar menu1/ como fallback
                // en lugar de menu/ (root), ya que menu1/ tiene rutas con :1:
                // que podemos ajustar a :N:
                $menu1Template = str_replace('varios/menu/', 'varios/menu1/', $seccion['template']);
                $menu1Path = bez_resolve_path('data/pages/' . $menu1Template);
                if (file_exists($menu1Path)) {
                    $templatePath = $menu1Path;
                    $needsPathAdjust = true;
                } else {
                    $templatePath = $defaultPath;
                }
            } else {
                // Destino sin número (general) → usar template por defecto (menu/)
                $templatePath = $defaultPath;
            }

            if (!file_exists($templatePath)) {
                continue;
            }

            $contenido = file_get_contents($templatePath);

            // Si se usó menu1/ como fallback para menuN/, ajustar rutas de formatos
            // Ejemplo: {{:compartidos:1:archivo.docx}} → {{:compartidos:4:archivo.docx}}
            if ($needsPathAdjust) {
                $areasMedia = ['compartidos', 'rrhh', 'administrativa', 'operativo', 'comercial'];
                foreach ($areasMedia as $area) {
                    $contenido = str_replace(
                        ':' . $area . ':1:',
                        ':' . $area . ':' . $numeroDest . ':',
                        $contenido
                    );
                }
            }

            // Ajustar contenido del template (filelisting, enlaces, fechas)
            $contenido = $this->ajustarTemplate($contenido, $adminThreadId, $seccionId, $paginaRegistros);

            // Guardar página
            saveWikiText($destino, $contenido, "Creación automática - Proyecto Administrativo #$adminThreadId", false);

            // Aprobar automáticamente la página creada
            $this->aprobarPagina($destino);
        }
    }

    /**
     * Crea la página wiki de Registros de Cumplimiento General (Sección 4)
     * Esta página se crea en encargos:general, encargos:general1, etc.
     * según la selección hecha al crear el proyecto.
     *
     * Los botones y filelisting se ajustan según el destino:
     *   general  → varios:menu:7, varios:menu:8, :encargos:general
     *   general1 → varios:menu1:7, varios:menu1:8, :encargos:general1
     *
     * @param int $adminThreadId ID del proyecto administrativo
     * @param string $paginaRegistros Ruta wiki de la página (encargos:general, encargos:general1, etc.)
     */
    protected function crearPaginaRegistros($adminThreadId, $paginaRegistros) {
        // Determinar la parte del filelisting (encargos:general → general, encargos:general1 → general1)
        $partes = explode(':', $paginaRegistros);
        $nombrePagina = end($partes); // "general", "general1", "general2", etc.

        // Base del menú según destino (varios:menu, varios:menu1, …)
        $menuBase = $this->getMenuBase($paginaRegistros);

        // Construir contenido de la página
        $contenido = '[[{:boton2:titulo.png}es:bez:admin_thread:id:' . $adminThreadId . '|Regresar]]' . "\n";
        $contenido .= ' ' . "\n";
        $contenido .= '[[{boton2:orden7.png}' . $menuBase . ':7|REQUERIMIENTOS OPERATIVOS - APLICADO DE ACUERDO A PROGRAMACIÓN]]' . "\n";
        $contenido .= ' ' . "\n";
        $contenido .= ' ' . "\n";
        $contenido .= '[[{boton2:orden8.png}' . $menuBase . ':8|GOBERNANZA Y ANÁLISIS DE RIESGOS]]' . "\n";
        $contenido .= ' ' . "\n";
        $contenido .= ' ' . "\n";
        $contenido .= ' ' . "\n";
        $contenido .= '{{filelisting>:encargos:' . $nombrePagina . '}}' . "\n";
        $contenido .= ' ' . "\n";
        $contenido .= ' ' . "\n";
        $contenido .= '~~DISCUSSION~~' . "\n";

        // Guardar página (sobrescribe si ya existe)
        saveWikiText($paginaRegistros, $contenido, "Creación automática - Registros Proyecto #$adminThreadId", false);

        // Aprobar automáticamente la página creada
        $this->aprobarPagina($paginaRegistros);
    }

    /**
     * Debug log para aprobar página
     */
    protected function debugLogApproval($message) {
        $logFile = DOKU_PLUGIN . 'autoapprove/debug.log';
        $timestamp = date('Y-m-d H:i:s');
        @file_put_contents($logFile, "[$timestamp] [BEZ] $message\n", FILE_APPEND);
    }

    /**
     * Aprueba automáticamente una página recién creada
     * @param string $id ID de la página wiki
     */
    protected function aprobarPagina($id) {
        $this->debugLogApproval("aprobarPagina() called for: $id");

        // Cargar helper de publish
        $publishHelper = plugin_load('helper', 'publish');
        if (!$publishHelper) {
            $this->debugLogApproval("ERROR: publish helper not available");
            return; // Plugin publish no disponible
        }

        // Obtener la revisión actual de la página
        $file = wikiFN($id);
        $this->debugLogApproval("Wiki file path: $file");

        $revision = @filemtime($file);
        if (!$revision) {
            $revision = time();
            $this->debugLogApproval("No filemtime, using time(): $revision");
        } else {
            $this->debugLogApproval("Got revision from filemtime: $revision");
        }

        // Obtener aprobaciones existentes
        $meta = p_get_metadata($id);
        $approvals = isset($meta['approval']) ? $meta['approval'] : [];
        $this->debugLogApproval("Existing approvals: " . json_encode($approvals));

        // Crear entrada de aprobación
        if (!isset($approvals[$revision])) {
            $approvals[$revision] = [];
        }

        // Agregar aprobación del sistema
        $approvals[$revision]['bez_system'] = [
            'BEZ-System',            // client
            'system',                // user
            'system@bez',            // mail
            $revision                // timestamp
        ];

        $this->debugLogApproval("New approvals: " . json_encode($approvals));

        // Guardar metadata de aprobación
        $result = p_set_metadata($id, ['approval' => $approvals], true, true);
        $this->debugLogApproval("p_set_metadata result: " . var_export($result, true));

        // Verificar
        $checkMeta = p_get_metadata($id);
        $this->debugLogApproval("Verification: " . json_encode(isset($checkMeta['approval']) ? $checkMeta['approval'] : 'NOT SET'));
    }

    /**
     * Ajusta el contenido del template para el proyecto específico.
     *
     * Cambios:
     *  - Enlace "Regresar" → apunta al admin_thread
     *  - Filelisting: rutas relativas (..:{dir}) → absolutas (:varios:menu:{dir})
     *    según paginaRegistros (general → varios:menu, general1 → varios:menu1)
     *  - Fechas DO → fecha actual + 30 días
     *
     * @param string $contenido        Contenido original del template
     * @param int    $adminThreadId    ID del proyecto
     * @param int    $seccionId        ID de la sección
     * @param string $paginaRegistros  ej: 'encargos:general', 'encargos:general1'
     * @return string Contenido ajustado
     */
    protected function ajustarTemplate($contenido, $adminThreadId, $seccionId, $paginaRegistros = 'encargos:general') {
        // Cambiar enlace de regreso (mantener imagen, solo cambiar la URL)
        $contenido = preg_replace(
            '/\[\[(\{[^}]*\})[^\|]+\|Regresar\]\]/',
            '[[$1es:bez:admin_thread:id:' . $adminThreadId . '|Regresar]]',
            $contenido
        );

        // Ajustar rutas de filelisting: convertir relativas a absolutas
        // Template original:  {{filelisting>...:1_responsabilidades}}
        // Resultado correcto: {{filelisting>:varios:menu:1_responsabilidades}}  (para general)
        //                     {{filelisting>:varios:menu1:1_responsabilidades}} (para general1)
        $menuBase = $this->getMenuBase($paginaRegistros);
        $contenido = preg_replace_callback(
            '/\{\{filelisting>([^}]+)\}\}/',
            function($matches) use ($menuBase) {
                $original = $matches[1];  // ej: "...:1_responsabilidades"
                // Extraer la parte después de los puntos relativos (..:{dirección})
                if (preg_match('/\.{2,3}:(.+)/', $original, $m)) {
                    return '{{filelisting>:' . $menuBase . ':' . $m[1] . '}}';
                }
                // Si ya es ruta absoluta o no tiene ..., dejar como está
                return $matches[0];
            },
            $contenido
        );

        // Actualizar fechas en tareas DO (fecha actual + 30 días)
        $nuevaFecha = date('Y-m-d', strtotime('+30 days'));
        $contenido = preg_replace(
            '/<do\s+\d{4}-\d{2}-\d{2}\s+/',
            "<do $nuevaFecha ",
            $contenido
        );

        return $contenido;
    }

    /**
     * Obtiene un proyecto por ID
     * @param int $id
     * @return array|null
     */
    public function get($id) {
        // Intentar primero con la vista
        $sql = "SELECT * FROM admin_thread_view WHERE id = ?";
        $res = $this->sqlite->query($sql, [$id]);
        $arr = $this->sqlite->res2arr($res);

        if (!empty($arr)) {
            return $arr[0];
        }

        // Fallback: consulta directa a la tabla
        $sql = "SELECT * FROM admin_thread WHERE id = ? AND deleted_at IS NULL";
        $res = $this->sqlite->query($sql, [$id]);
        $arr = $this->sqlite->res2arr($res);
        return !empty($arr) ? $arr[0] : null;
    }

    /**
     * Obtiene todos los proyectos (activos)
     * @param array $filters Filtros opcionales
     * @return array
     */
    public function getAll($filters = []) {
        $sql = "SELECT * FROM admin_thread_view WHERE 1=1";
        $params = [];

        if (!empty($filters['trabajador_id'])) {
            $sql .= " AND trabajador_id = ?";
            $params[] = $filters['trabajador_id'];
        }

        if (!empty($filters['state'])) {
            $sql .= " AND state = ?";
            $params[] = $filters['state'];
        }

        if (!empty($filters['anio'])) {
            $sql .= " AND strftime('%Y', fecha_inicio) = ?";
            $params[] = $filters['anio'];
        }

        $sql .= " ORDER BY created_at DESC";

        $res = $this->sqlite->query($sql, $params);
        return $this->sqlite->res2arr($res);
    }

    /**
     * Obtiene las secciones de un proyecto con su progreso
     * @param int $adminThreadId
     * @return array
     */
    public function getSecciones($adminThreadId) {
        $sql = "SELECT * FROM admin_thread_progress WHERE admin_thread_id = ? ORDER BY seccion_id";
        $res = $this->sqlite->query($sql, [$adminThreadId]);
        return $this->sqlite->res2arr($res);
    }

    /**
     * Actualiza un proyecto
     * @param int $id
     * @param array $data
     */
    public function update($id, $data) {
        $campos = [];
        $params = [];

        // Campos básicos (siempre existen en el schema original)
        $camposBasicos = ['trabajador_id', 'trabajador_nombre', 'fecha_inicio', 'fecha_fin', 'observaciones', 'state'];

        // Campos nuevos (pueden no existir en schemas antiguos — se verifica con PRAGMA)
        $camposNuevos = ['pagina_registros', 'tipo_trabajador', 'periodo', 'privado', 'ruta_manual_calidad', 'ruta_codigo_etica'];

        // Verificar qué columnas nuevas existen
        $columnsExist = $this->checkColumnsExist();

        // Procesar campos básicos
        foreach ($camposBasicos as $campo) {
            if (isset($data[$campo])) {
                $campos[] = "$campo = ?";
                $params[] = $data[$campo];
            }
        }

        // Procesar campos nuevos solo si existen en la BD
        // Usar array_key_exists para permitir valores NULL (resetear a default)
        foreach ($camposNuevos as $campo) {
            if (array_key_exists($campo, $data) && !empty($columnsExist[$campo])) {
                $campos[] = "$campo = ?";
                $params[] = $data[$campo];
            }
        }

        if (empty($campos)) {
            return;
        }

        $campos[] = "updated_at = datetime('now')";

        // Si se cierra el proyecto
        if (isset($data['state']) && $data['state'] === 'closed') {
            $campos[] = "closed_by = ?";
            $params[] = $this->currentUser;
            $campos[] = "closed_at = datetime('now')";
        }

        $params[] = $id;

        $sql = "UPDATE admin_thread SET " . implode(', ', $campos) . " WHERE id = ?";
        $this->sqlite->query($sql, $params);
    }

    /**
     * Elimina un proyecto (soft delete)
     * @param int $id
     */
    public function delete($id) {
        $sql = "UPDATE admin_thread SET deleted_at = datetime('now') WHERE id = ?";
        $this->sqlite->query($sql, [$id]);
    }

    /**
     * Restaura un proyecto eliminado
     * @param int $id
     */
    public function restore($id) {
        $sql = "UPDATE admin_thread SET deleted_at = NULL WHERE id = ?";
        $this->sqlite->query($sql, [$id]);
    }

    // =========================================
    // PROGRESO
    // =========================================

    /**
     * Actualiza el progreso de una sección
     * @param int $adminThreadId
     * @param int $seccionId
     * @param int $total
     * @param int $terminados
     */
    public function actualizarProgreso($adminThreadId, $seccionId, $total, $terminados) {
        $porcentaje = $total > 0 ? round(($terminados / $total) * 100, 2) : 0;

        $sql = "UPDATE admin_thread_progress
                SET total_requerimientos = ?, requerimientos_terminados = ?, porcentaje_progreso = ?, updated_at = datetime('now')
                WHERE admin_thread_id = ? AND seccion_id = ?";
        $this->sqlite->query($sql, [$total, $terminados, $porcentaje, $adminThreadId, $seccionId]);
    }

    /**
     * Recalcula el progreso desde el plugin DO
     * @param int $adminThreadId
     * @param \helper_plugin_do $doPlugin
     */
    public function recalcularProgresoDesdeDoPlugin($adminThreadId, $doPlugin = null) {
        if (!$doPlugin) {
            $doPlugin = plugin_load('helper', 'do');
        }

        if (!$doPlugin) {
            return;
        }

        $secciones = $this->getSecciones($adminThreadId);

        foreach ($secciones as $seccion) {
            // Solo calcular para secciones que aportan progreso
            if ($seccion['aporta_progreso'] != 1) {
                continue;
            }

            $rutaWiki = $seccion['ruta_wiki'];

            // Obtener todas las tareas de la página via API pública del plugin DO
            $allStatuses = $doPlugin->getAllPageStatuses($rutaWiki);
            $total = count($allStatuses);
            $completadas = 0;

            foreach ($allStatuses as $task) {
                if (!empty($task['msg']) && in_array($task['msg'], ['Completado', 'No Aplica'])) {
                    $completadas++;
                }
            }

            $this->actualizarProgreso($adminThreadId, $seccion['seccion_id'], $total, $completadas);
        }
    }

    // =========================================
    // REPARACIÓN DE PÁGINAS EXISTENTES
    // =========================================

    /**
     * Repara las páginas wiki de TODOS los proyectos administrativos existentes.
     *
     * Corrige los filelisting que apuntaban a rutas incorrectas:
     *   - "proyectos:administrativos:X:archivos:Y" → ":varios:menu:Y"
     *   - "...:Y" (relativo) → ":varios:menu:Y" (absoluto)
     * Y regenera la página de registros (sección 4) con botones corregidos.
     *
     * @return array ['reparados' => int, 'errores' => int, 'detalles' => string[]]
     */
    public function repararPaginasExistentes() {
        $resultado = ['reparados' => 0, 'errores' => 0, 'detalles' => []];

        // Verificar si la columna pagina_registros existe
        $columnsExist = $this->checkColumnsExist();
        $tienePaginaRegistros = !empty($columnsExist['pagina_registros']);

        // Obtener todos los admin threads activos
        $fields = $tienePaginaRegistros ? 'id, pagina_registros' : 'id';
        $sql = "SELECT $fields FROM admin_thread WHERE deleted_at IS NULL";
        $res = $this->sqlite->query($sql);
        $threads = $this->sqlite->res2arr($res);

        if (empty($threads)) {
            $resultado['detalles'][] = "No hay proyectos administrativos para reparar.";
            return $resultado;
        }

        foreach ($threads as $thread) {
            $id = intval($thread['id']);
            $paginaRegistros = ($tienePaginaRegistros && !empty($thread['pagina_registros']))
                ? $thread['pagina_registros']
                : 'encargos:general';

            $menuBase = $this->getMenuBase($paginaRegistros);

            // ---- Reparar secciones 1-3 (páginas con filelisting) ----
            foreach ($this->seccionesPredefinidas as $seccionId => $seccion) {
                if (empty($seccion['archivo'])) continue;

                $paginaWiki = "proyectos:administrativos:$id:" . $seccion['archivo'];

                if (!page_exists($paginaWiki)) {
                    $resultado['detalles'][] = "Página no existe: $paginaWiki (omitida)";
                    continue;
                }

                $contenido = rawWiki($paginaWiki);
                $contenidoOriginal = $contenido;

                // Reemplazar filelisting con ruta absoluta correcta
                $contenido = preg_replace_callback(
                    '/\{\{filelisting>([^}]+)\}\}/',
                    function($matches) use ($menuBase) {
                        $original = $matches[1];

                        // Caso 1: ruta relativa (..:{dir})
                        if (preg_match('/\.{2,3}:(.+)/', $original, $m)) {
                            return '{{filelisting>:' . $menuBase . ':' . $m[1] . '}}';
                        }

                        // Caso 2: ruta incorrecta (proyectos:administrativos:X:archivos:{dir})
                        if (strpos($original, 'proyectos:administrativos:') === 0) {
                            $partes = explode(':', $original);
                            $dir = end($partes);
                            return '{{filelisting>:' . $menuBase . ':' . $dir . '}}';
                        }

                        // Si ya es ruta absoluta correcta, dejar como está
                        return $matches[0];
                    },
                    $contenido
                );

                if ($contenido !== $contenidoOriginal) {
                    saveWikiText($paginaWiki, $contenido, "Reparación filelisting - Proyecto Administrativo #$id", false);
                    $this->aprobarPagina($paginaWiki);
                    $resultado['reparados']++;
                    $resultado['detalles'][] = "Reparado: $paginaWiki → :$menuBase";
                }
            }

            // ---- Reparar sección 4 (página de registros) ----
            $this->crearPaginaRegistros($id, $paginaRegistros);
            $resultado['reparados']++;
            $resultado['detalles'][] = "Regenerado: $paginaRegistros (registros proyecto #$id)";
        }

        return $resultado;
    }

    // =========================================
    // PROYECTO POR DEFECTO
    // =========================================

    /**
     * Asegura que exista el proyecto administrativo por defecto.
     * Solo crea el proyecto si NO existe NINGÚN proyecto administrativo.
     * Si ya hay proyectos configurados, no toca la base de datos.
     *
     * Datos del proyecto por defecto:
     *   - Trabajador: "Firma Contable"
     *   - Tipo: firma
     *   - Página registros: encargos:general
     *   - Fecha creación: 2025-01-01
     *
     * @return int|null ID del proyecto por defecto, o null si ya existen proyectos
     */
    public function ensureDefaultProject() {
        // Asegurar que las tablas existan
        $this->ensureTables();

        // Verificar si ya existe CUALQUIER proyecto administrativo (no solo firma_principal)
        $sql = "SELECT COUNT(*) as total FROM admin_thread WHERE deleted_at IS NULL";
        $res = $this->sqlite->query($sql);
        $arr = $this->sqlite->res2arr($res);

        if (!empty($arr) && intval($arr[0]['total']) > 0) {
            // Ya existen proyectos configurados, no tocar nada
            return null;
        }

        // No existe ningún proyecto: crear el proyecto por defecto
        $newId = $this->getNextId();

        // Verificar columnas disponibles
        $columnsExist = $this->checkColumnsExist();

        $columns = ['id', 'trabajador_id', 'trabajador_nombre', 'created_by', 'created_at', 'fecha_inicio'];
        $values  = [$newId, 'firma_principal', 'Firma Contable', 'system', '2025-01-01 00:00:00', '2025-01-01'];

        if ($columnsExist['tipo_trabajador']) {
            $columns[] = 'tipo_trabajador';
            $values[]  = 'firma';
        }
        if ($columnsExist['pagina_registros']) {
            $columns[] = 'pagina_registros';
            $values[]  = 'encargos:general';
        }
        if ($columnsExist['periodo']) {
            $columns[] = 'periodo';
            $values[]  = '2025';
        }
        if ($columnsExist['privado']) {
            $columns[] = 'privado';
            $values[]  = 0;
        }

        $placeholders = implode(', ', array_fill(0, count($columns), '?'));
        $sql = "INSERT INTO admin_thread (" . implode(', ', $columns) . ") VALUES ($placeholders)";
        $this->sqlite->query($sql, $values);

        // Crear las 4 secciones de progreso
        $this->crearSecciones($newId, 'encargos:general');

        // Crear páginas wiki desde templates (selecciona template según destino)
        $this->crearPaginasWiki($newId, 'encargos:general');

        // Crear página de registros
        $this->crearPaginaRegistros($newId, 'encargos:general');

        // Marcar como preferencia por defecto
        try {
            $this->sqlite->query("DELETE FROM admin_thread_preferencia");
            $this->sqlite->query("INSERT INTO admin_thread_preferencia (admin_thread_id, marcado_por) VALUES (?, 'system')", [$newId]);
        } catch (\Exception $e) {
            // Si falla (tabla no existe), ignorar
        }

        return $newId;
    }

    // =========================================
    // TRABAJADORES (no se usa, ahora se usan usuarios de DokuWiki)
    // =========================================

    /**
     * Obtiene la lista de trabajadores disponibles
     * @return array
     * @deprecated Usar lista de usuarios de DokuWiki directamente
     */
    public function getTrabajadores() {
        // Fallback: lista vacía (se usan usuarios de DokuWiki directamente)
        return [];
    }

    /**
     * Obtiene proyectos de un trabajador específico
     * @param string $trabajadorId
     * @return array
     */
    public function getProyectosDeTrabajador($trabajadorId) {
        return $this->getAll(['trabajador_id' => $trabajadorId]);
    }
}
