<?php

namespace dokuwiki\plugin\bez\mdl;

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

use Assetic\Exception\Exception;

class ThreadFactory extends Factory {

    public function get_table_view() {
        return 'thread_view';
    }

    public function get_years_scope() {
        $r = $this->model->sqlite->query('SELECT create_date FROM thread ORDER BY id LIMIT 1');
        $date = $this->model->sqlite->res2single($r);

        //get only year
		$first =  (int) substr($date, 0, strpos($date, '-'));
        $last = (int) date('Y');

		$years = array();
		for ($year = $first; $year <= $last; $year++) {
			$years[] = (string) $year;
        }
		return $years;
    }

    /**
     * Obtener el año de operación de un thread con lazy-loading.
     * 1. Si ya tiene year en BD → retorna directo
     * 2. Si no → extrae del título (formato "Año: YYYY")
     * 3. Si no match → usa año del create_date
     * 4. Actualiza la BD para futuras consultas
     *
     * @param Thread $thread
     * @return string Año de operación (4 dígitos)
     */
    public function get_year(Thread $thread) {
        // Paso 1: Si ya tiene year almacenado, retornar directo
        if (!empty($thread->year)) {
            return $thread->year;
        }

        $year = null;

        // Paso 2: Extraer del título (formato "Año: YYYY")
        if (preg_match('/Año:\s*(\d{4})/', $thread->title, $matches)) {
            $year = $matches[1];
        }

        // Paso 3: Fallback al año del create_date
        if (empty($year) && !empty($thread->create_date)) {
            $pos = strpos($thread->create_date, '-');
            if ($pos !== false) {
                $year = substr($thread->create_date, 0, $pos);
            }
        }

        // Paso 4: Fallback final al año actual
        if (empty($year)) {
            $year = date('Y');
        }

        // Paso 5: Actualizar la BD para futuras consultas
        $this->model->sqlite->query(
            'UPDATE thread SET year = ? WHERE id = ?',
            $year, $thread->id
        );

        return $year;
    }

    public function users_involvement(\DatePeriod $period=NULL) {
        if ($period) {
            $from = $period->getStartDate()->format(\DateTime::ISO8601);
            $to = $period->getEndDate()->format(\DateTime::ISO8601);

            $sql = "SELECT thread_participant.user_id,
                       SUM(thread_participant.original_poster) AS original_poster_sum,
                       SUM(thread_participant.coordinator) AS coordinator_sum,
                       SUM(thread_participant.commentator) AS commentator_sum,
                       SUM(thread_participant.task_assignee) AS task_assignee_sum
                       FROM thread_participant JOIN thread ON thread_participant.thread_id = thread.id
                       WHERE thread.create_date BETWEEN ? AND ?
                       GROUP BY user_id
                       ORDER BY user_id";
            $r = $this->model->sqlite->query($sql, $from, $to);
        } else {
            $sql = "SELECT user_id,
                           SUM(original_poster) AS original_poster_sum,
                           SUM(coordinator) AS coordinator_sum,
                           SUM(commentator) AS commentator_sum,
                           SUM(task_assignee) AS task_assignee_sum
                           FROM thread_participant
                           GROUP BY user_id
                           ORDER BY user_id";

            $r = $this->model->sqlite->query($sql);
        }
        return $r;
    }

    public function kpi(\DatePeriod $period=NULL) {
        if ($period) {
            $from = $period->getStartDate()->format(\DateTime::ISO8601);
            $to = $period->getEndDate()->format(\DateTime::ISO8601);

            $sql = "SELECT COUNT(*)*1.0/COUNT(DISTINCT thread_id) AS kpi
                       FROM thread_participant JOIN thread ON thread_participant.thread_id = thread.id
                       WHERE thread.create_date BETWEEN ? AND ?";
            $r = $this->model->sqlite->query($sql, $from, $to);
        } else {
            $sql = "SELECT COUNT(*)*1.0/COUNT(DISTINCT thread_id) AS kpi
                      FROM thread_participant";

            $r = $this->model->sqlite->query($sql);
        }

        return $r->fetchColumn();
    }

    public function bez_activity(\DatePeriod $period=NULL) {
        if ($period) {
            $from = $period->getStartDate()->format(\DateTime::ISO8601);
            $to = $period->getEndDate()->format(\DateTime::ISO8601);

            $sql = "SELECT COUNT(DISTINCT user_id)
                      FROM (SELECT user_id
                              FROM thread_participant JOIN thread ON thread_participant.thread_id = thread.id
                              WHERE create_date BETWEEN ? AND ?
                            UNION
                            SELECT user_id
                              FROM task_participant JOIN task ON task_participant.task_id = task.id
                              WHERE create_date BETWEEN ? AND ?)";
            $r = $this->model->sqlite->query($sql, $from, $to, $from, $to);
        } else {
            $sql = "SELECT COUNT(DISTINCT user_id)
                      FROM (SELECT user_id FROM thread_participant
                            UNION
                            SELECT user_id FROM task_participant)";

            $r = $this->model->sqlite->query($sql);
        }
        $active_users = $r->fetchColumn();
        $wiki_users = count($this->model->userFactory->get_all());

        // Prevenir división por cero
        if ($wiki_users <= 0) {
            return 0;
        }

        return $active_users/$wiki_users * 100;
    }


    public function report_issue(\DatePeriod $period=NULL) {
        if ($period) {
            $from = $period->getStartDate()->format(\DateTime::ISO8601);
            $to = $period->getEndDate()->format(\DateTime::ISO8601);

            $sql = "SELECT label_name,
                           COUNT(CASE WHEN state = 'proposal' THEN 1 END) AS proposal,
                           COUNT(CASE WHEN state = 'opened' THEN 1 END) AS opened,
                           COUNT(CASE WHEN state = 'done' THEN 1 END) AS done,
                           COUNT(CASE WHEN state = 'closed' THEN 1 END) AS closed,
                           COUNT(CASE WHEN state = 'rejected' THEN 1 END) AS rejected,
                           COUNT(*) AS count_all,
                           SUM(task_sum_cost) AS sum_all,
                           SUM(CASE WHEN state = 'closed' THEN task_sum_cost END) AS sum_closed,
                           (CASE WHEN state = 'closed' THEN
                            AVG(julianday(close_date) - julianday(create_date))
                            END) AS avg_closed
                      FROM thread_view
                      WHERE type = 'issue' AND create_date BETWEEN ? AND ?
                      GROUP BY label_name
                      ORDER BY label_name";

            $r = $this->model->sqlite->query($sql, $from, $to);
        } else {
            $sql = "SELECT label_name,
                           COUNT(CASE WHEN state = 'proposal' THEN 1 END) AS proposal,
                           COUNT(CASE WHEN state = 'opened' THEN 1 END) AS opened,
                           COUNT(CASE WHEN state = 'done' THEN 1 END) AS done,
                           COUNT(CASE WHEN state = 'closed' THEN 1 END) AS closed,
                           COUNT(CASE WHEN state = 'rejected' THEN 1 END) AS rejected,
                           COUNT(*) AS count_all,
                           SUM(task_sum_cost) AS sum_all,
                           SUM(CASE WHEN state = 'closed' THEN task_sum_cost END) AS sum_closed,
                           (CASE WHEN state = 'closed' THEN
                            AVG(julianday(close_date) - julianday(create_date))
                            END) AS avg_closed
                      FROM thread_view
                      WHERE type = 'issue'
                      GROUP BY label_name
                      ORDER BY label_name";

            $r = $this->model->sqlite->query($sql);
        }
        return $r;
    }

    public function report_project(\DatePeriod $period=NULL) {
        if ($period) {
            $from = $period->getStartDate()->format(\DateTime::ISO8601);
            $to = $period->getEndDate()->format(\DateTime::ISO8601);

            $sql = "SELECT label_name,
                           COUNT(CASE WHEN state = 'proposal' THEN 1 END) AS proposal,
                           COUNT(CASE WHEN state = 'opened' THEN 1 END) AS opened,
                           COUNT(CASE WHEN state = 'done' THEN 1 END) AS done,
                           COUNT(CASE WHEN state = 'closed' THEN 1 END) AS closed,
                           COUNT(CASE WHEN state = 'rejected' THEN 1 END) AS rejected,
                           COUNT(*) AS count_all,
                           SUM(task_sum_cost) AS sum_all,
                           SUM(CASE WHEN state = 'closed' THEN task_sum_cost END) AS sum_closed,
                           (CASE WHEN state = 'closed' THEN
                            AVG(julianday(close_date) - julianday(create_date))
                            END) AS avg_closed
                      FROM thread_view
                      WHERE type = 'project' AND create_date BETWEEN ? AND ?";

            $r = $this->model->sqlite->query($sql, $from, $to);
        } else {
            $sql = "SELECT label_name,
                           COUNT(CASE WHEN state = 'proposal' THEN 1 END) AS proposal,
                           COUNT(CASE WHEN state = 'opened' THEN 1 END) AS opened,
                           COUNT(CASE WHEN state = 'done' THEN 1 END) AS done,
                           COUNT(CASE WHEN state = 'closed' THEN 1 END) AS closed,
                           COUNT(CASE WHEN state = 'rejected' THEN 1 END) AS rejected,
                           COUNT(*) AS count_all,
                           SUM(task_sum_cost) AS sum_all,
                           SUM(CASE WHEN state = 'closed' THEN task_sum_cost END) AS sum_closed,
                           (CASE WHEN state = 'closed' THEN
                            AVG(julianday(close_date) - julianday(create_date))
                            END) AS avg_closed
                      FROM thread_view
                      WHERE type = 'project'";

            $r = $this->model->sqlite->query($sql);
        }
        return $r;
    }

    public function initial_save(Entity $thread, $data) {
        $label_ids = array();
        if (isset($data['label_id']) && $data['label_id'] != '') {
            $label_ids[] = $data['label_id'];
        }
        try {
            $this->beginTransaction();

            // Obtener el próximo ID de la secuencia ANTES de insertar
            $next_id = $this->consul_id();

            // Asignar el ID al thread antes de guardar
            $reflectionClass = new \ReflectionClass($thread);
            $reflectionProperty = $reflectionClass->getProperty('id');
            $reflectionProperty->setAccessible(true);
            $reflectionProperty->setValue($thread, $next_id);

            // Guardar con el ID pre-asignado
            $thread->set_data($data);
            $this->insertWithId($thread);

            // Actualizar la secuencia con el nuevo ID
            $this->updateSequence($next_id);

            foreach($label_ids as $label_id) {
                $thread->add_label($label_id);
            }

            $thread->set_participant_flags($thread->original_poster, array('original_poster', 'subscribent'));
            if($thread->coordinator != null) {
                $thread->set_participant_flags($thread->coordinator, array('coordinator', 'subscribent'));
            }

            if ($this->model->get_level() >= BEZ_AUTH_LEADER && $this->model->get_level() != BEZ_AUTH_SYSADMIN) {
                $private = false;
                if (isset($data['private'])) {
                    $private = true;
                }
                $thread->set_private_flag($private);
            }

            $this->commitTransaction();

            if ($thread->state != 'proposal' && $this->model->user_nick != $thread->coordinator) {
                $thread->mail_inform_coordinator();
            } elseif ($thread->state == 'proposal') {
                $thread->mail_inform_admins();
            }

        } catch(Exception $exception) {
            $this->rollbackTransaction();
        }
    }

    /**
     * Funcion que almacena la informacion a la tabla de progreso R**G.
     */
    public function progreso_proyecto($codigo, $data){
        // Enviamos la informacion a la tabla de progreso.
        $res = 1;
        try {
            $sql = "INSERT OR IGNORE INTO progreso_proyecto (proyecto_id, nombre_submodulo, ruta_submodulo, procesos_ejecucion) VALUES (?, ?, ?, ?)";
            foreach ($data as $submodulo) {
                $this->model->sqlite->query($sql, array($codigo, $submodulo['nombre'], $submodulo['ruta'], $submodulo['procesos_ejecucion']));
            }
        } catch (\Throwable $th) {
            $res = 0;
        }
        
        return $res;
    }

    /**
     * Función que almacena la información de progreso del proyecto R**G.
     */
    public function getPorcentajeProyecto($proyecto) {

        $porcentajeFinal = 0;
        $cantidadTotal = 0;
        $cantidadFinalizados = 0;
        $contadorTotalEjecucion = 0;
        $contadorFinalizadosEjecucion = 0;
        $referencia_ejecucion = 0;
        $porcentajes = array();

        // Consultamos el proyecto en especifico.
        $sql = "SELECT nombre_submodulo,
                    total_requerimientos,
                    requerimientos_terminados,
                    porcentaje_progreso,
                    procesos_ejecucion
                FROM progreso_proyecto 
                WHERE proyecto_id = ?
                AND nombre_submodulo NOT LIKE '%EJECUCIÓN DEL PROYECTO%'
                AND mostrar_submodulo = 1";
        $respuesta = $this->model->sqlite->query($sql, $proyecto);

        // Consultamos el proceso de ejecucion.
        $sqlEjecucion = "SELECT nombre_submodulo
                FROM progreso_proyecto 
                WHERE proyecto_id = ?
                AND procesos_ejecucion = 1";
        $respuestaEjecucion = $this->model->sqlite->query($sqlEjecucion, $proyecto);
        $resEjecucion = $respuestaEjecucion->fetchAll();
        
        // Iteramos sobre la respuesta para obtener los resultados
        foreach ($respuesta as $res) {
            if (intval($res["procesos_ejecucion"]) === 0){
                $porcentajes[] = $res['porcentaje_progreso'];

                if($res['porcentaje_progreso'] == 100){
                    $cantidadFinalizados ++;
                }
            }

            if(is_array($resEjecucion) && !empty($resEjecucion)){
                $partes = explode(' ', $resEjecucion[0]['nombre_submodulo'], 2);
                $referencia_ejecucion = $partes[0]; 
            }

            if ($referencia_ejecucion != 0 && stripos($res['nombre_submodulo'], $referencia_ejecucion) !== false) {
                $contadorTotalEjecucion ++;
                if (isset($res['total_requerimientos']) && isset($res['requerimientos_terminados']) &&  $res['total_requerimientos'] == $res['requerimientos_terminados']){
                    $contadorFinalizadosEjecucion ++;
                }
            }
        }

        if(empty($porcentajes)){
            return $porcentajeFinal;
        }

        $cantidadTotal = count($porcentajes);

        // Prevenir división por cero
        if ($cantidadTotal <= 0) {
            return $porcentajeFinal;
        }

        $porcentajeProyectoGeneral = round((($cantidadFinalizados / $cantidadTotal) * 100));

        if ($contadorTotalEjecucion > 1){
            // Prevenir división por cero: $total debe ser > 0
            $total = $contadorTotalEjecucion - 1;
            $porcentajeProyectoEjecucion = round(($contadorFinalizadosEjecucion / $total) * 100);
            if ($porcentajeProyectoEjecucion == 0) {
                $porcentajeProyectoEjecucion = 50;
            }
            
        } else {
            $porcentajeProyectoEjecucion = 50;
        }

        $porcentajeFinal = $porcentajeProyectoGeneral < $porcentajeProyectoEjecucion ? $porcentajeProyectoGeneral : $porcentajeProyectoEjecucion;

        return $porcentajeFinal;
    }

    /**
     * Funcion para traer todos los porcentajes del proyecto R**G
     */
    public function getPorcentajeProyectoDetalle($proyecto) {

        $porcentajes = array();

        $sql = "SELECT *
                FROM progreso_proyecto
                WHERE proyecto_id = ?
                AND nombre_submodulo NOT LIKE '%EJECUCIÓN DEL PROYECTO%'
                ORDER BY
                    procesos_ejecucion DESC,
                    CAST(nombre_submodulo AS INTEGER) ASC,
                    CASE
                        WHEN instr(nombre_submodulo, '(') > 0 THEN
                            CAST(substr(nombre_submodulo, instr(nombre_submodulo, '(') + 1) AS INTEGER)
                        ELSE 0
                    END ASC,
                    id_progreso ASC";
        $respuesta = $this->model->sqlite->query($sql, $proyecto);

        foreach ($respuesta as $res){
            $porcentajes[] = $res;
        }

        return $porcentajes;
    }

    /**
     * Modelo para traer el ultimo id de proyecto.
     * Usa una tabla de secuencia para garantizar que los IDs nunca se reutilicen,
     * incluso si se eliminan proyectos físicamente.
     */
    public function consul_id() {
        // Asegurar que la tabla de secuencia existe
        $this->ensureSequenceTable();

        // Obtener el siguiente ID de la tabla de secuencia
        $sql = "SELECT last_id + 1 AS next_id FROM thread_sequence WHERE id = 1";
        $r = $this->model->sqlite->query($sql);
        $res = $r->fetch();
        if ($res && isset($res['next_id'])) {
            return $res['next_id'];
        }

        // Fallback: usar MAX(id) de la tabla thread
        $sql = "SELECT COALESCE(MAX(id), 0) + 1 AS next_id FROM thread";
        $r = $this->model->sqlite->query($sql);
        $res = $r->fetch();
        return $res['next_id'];
    }

    /**
     * Actualiza la secuencia después de crear un proyecto
     * @param int $new_id El ID del proyecto recién creado
     */
    public function updateSequence($new_id) {
        $this->ensureSequenceTable();
        $this->model->sqlite->query(
            'UPDATE thread_sequence SET last_id = ? WHERE id = 1 AND last_id < ?',
            $new_id,
            $new_id
        );
    }

    /**
     * Asegura que la tabla thread_sequence existe, creándola si es necesario
     */
    protected function ensureSequenceTable() {
        if (!$this->sequenceTableExists()) {
            // Crear la tabla de secuencia
            $this->model->sqlite->query(
                "CREATE TABLE IF NOT EXISTS thread_sequence (
                    id INTEGER PRIMARY KEY,
                    last_id INTEGER NOT NULL DEFAULT 0
                )"
            );
            // Inicializar con el máximo ID actual
            $this->model->sqlite->query(
                "INSERT OR REPLACE INTO thread_sequence (id, last_id)
                 SELECT 1, COALESCE(MAX(id), 0) FROM thread"
            );
        }
    }

    /**
     * Verifica si la tabla thread_sequence existe
     * @return bool
     */
    protected function sequenceTableExists() {
        try {
            $result = $this->model->sqlite->query(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='thread_sequence'"
            );
            $row = $result->fetch();
            return $row !== false;
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * Inserta un thread con un ID específico (no auto-generado)
     * @param Entity $obj El thread a insertar
     */
    protected function insertWithId(Entity $obj) {
        $set = array();
        $execute = array();
        $columns = array();

        foreach ($obj->get_columns() as $column) {
            if ($obj->$column === null) continue;
            $set[] = ":$column";
            $columns[] = $column;
            $value = $obj->$column;
            if ($value === '') {
                $execute[':'.$column] = null;
            } else {
                $execute[':'.$column] = $value;
            }
        }

        $query = 'INSERT INTO '.$this->get_table_name().'
                    ('.implode(',', $columns).')
                    VALUES ('.implode(',', $set).')';

        $sth = $this->model->db->prepare($query);
        $sth->execute($execute);
    }

    protected function update(Entity $obj) {
        if ($obj->state == 'done') {
            $prev_state = $obj->state;
            $reflectionClass = new \ReflectionClass('dokuwiki\plugin\bez\mdl\Thread');
            $reflectionProperty = $reflectionClass->getProperty('state');
            $reflectionProperty->setAccessible(true);
            $reflectionProperty->setValue($obj, 'opened');
        }
        try {
            parent::update($obj);
        } finally {
            if (isset($prev_state)) {
                $reflectionProperty->setValue($obj, $prev_state);
            }
        }
    }

    public function update_save(Entity $thread, $data) {
        $prev_coordinator = $thread->coordinator;

        $label_ids = array();
        if (isset($data['label_id']) && $data['label_id'] != '') {
            $label_ids[] = $data['label_id'];
        }
        try {
            $this->beginTransaction();
            parent::update_save($thread, $data);

            $cur_label_ids = array_keys($thread->get_labels());
            $labels_to_add = array_diff($label_ids, $cur_label_ids);
            $labels_to_rem = array_diff($cur_label_ids, $label_ids);

            foreach($labels_to_add as $label_id) {
                $thread->add_label($label_id);
            }

            foreach($labels_to_rem as $label_id) {
                $thread->remove_label($label_id);
            }

            if ($thread->coordinator != null && $thread->coordinator != $prev_coordinator) {
                if ($prev_coordinator != null) {
                    $thread->remove_participant_flags($prev_coordinator, array('coordinator'));
                }
                $thread->set_participant_flags($thread->coordinator, array('subscribent', 'coordinator'));
            }

            if ($thread->acl_of('private') >= BEZ_PERMISSION_CHANGE) {
                $private = false;
                if (isset($data['private'])) {
                    $private = true;
                }
                $thread->set_private_flag($private);
            }

            $this->commitTransaction();
        } catch(Exception $exception) {
            $this->rollbackTransaction();
        }

        if ($thread->state != 'proposal' && $this->model->user_nick != $thread->coordinator) {
            $thread->mail_inform_coordinator();
        }
    }

    public function delete(Entity $obj) {
        // Verificar permiso DELETE antes de proceder
        if ($obj->acl_of('id') < BEZ_PERMISSION_DELETE) {
            throw new \dokuwiki\plugin\bez\meta\PermissionDeniedException('No tiene permiso para eliminar este proyecto');
        }
        // Guardar el ID antes de eliminar
        $project_id = $obj->id;

        // Iniciar log de eliminación
        $log = $this->initDeleteLog($project_id);

        // PASO 1: Eliminar todas las tareas relacionadas con el proyecto
        $log .= "Paso 1: Eliminando tareas del proyecto...\n";
        $this->deleteProjectTasks($project_id);
        $log .= "  - Tareas eliminadas\n";

        // PASO 2: Eliminar todos los comentarios del proyecto
        $log .= "Paso 2: Eliminando comentarios del proyecto...\n";
        $this->deleteProjectComments($project_id);
        $log .= "  - Comentarios eliminados\n";

        // PASO 3: Eliminar registros de progreso del proyecto
        $log .= "Paso 3: Eliminando registros de progreso...\n";
        $this->deleteProjectProgress($project_id);
        $log .= "  - Registros de progreso eliminados\n";

        // PASO 4: Limpiar permisos ACL del proyecto
        $log .= "Paso 4: Limpiando permisos ACL...\n";
        $this->cleanupProjectACL($project_id);
        $log .= "  - ACL limpiado\n";

        // PASO 5: Limpiar grupo de usuarios del proyecto
        $log .= "Paso 5: Limpiando grupos de usuarios...\n";
        $this->cleanupProjectUserGroups($project_id);
        $log .= "  - Grupos limpiados\n";

        // PASO 6: Eliminar carpetas wiki del proyecto (incluye respaldo)
        $log .= "Paso 6: Eliminando carpetas wiki...\n";
        $log .= $this->cleanupProjectWikiPages($project_id);

        // PASO 7: Soft delete o delete físico en base de datos
        $log .= "Paso 7: Eliminando de base de datos...\n";
        $column_exists = $this->checkDeletedAtColumnExists();

        try {
            // Usar transaccion para operaciones destructivas
            $pdo = $this->model->sqlite->getAdapter()->getPdo();
            $pdo->beginTransaction();

            if ($column_exists) {
                // Soft delete: marcar como eliminado
                $deleted_at = date('Y-m-d H:i:s');
                $this->model->sqlite->query(
                    'UPDATE thread SET deleted_at = ? WHERE id = ?',
                    $deleted_at,
                    $project_id
                );
                $log .= "  - Soft delete completado (deleted_at: $deleted_at)\n";
            } else {
                // Delete físico
                $this->model->sqlite->query('DELETE FROM thread_label WHERE thread_id=?', $project_id);
                $this->model->sqlite->query('DELETE FROM thread_participant WHERE thread_id=?', $project_id);
                $this->model->sqlite->query('DELETE FROM thread WHERE id=?', $project_id);
                $log .= "  - Delete físico completado\n";
            }

            $pdo->commit();
        } catch (\Exception $e) {
            if (isset($pdo) && $pdo->inTransaction()) {
                $pdo->rollBack();
            }
            $log .= "  - ERROR: " . $e->getMessage() . " (rollback ejecutado)\n";
            dbglog('bez ThreadFactory: permanent_delete error: ' . $e->getMessage());
        }

        // Guardar log
        $log .= "=== FIN DE ELIMINACIÓN ===\n\n";
        $this->saveDeleteLog($log);
    }

    /**
     * Inicia el log de eliminación
     */
    protected function initDeleteLog($project_id) {
        $log = "=== ELIMINANDO PROYECTO: $project_id ===\n";
        $log .= "Fecha: " . date('Y-m-d H:i:s') . "\n";
        $log .= "Usuario web: " . (isset($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'] : 'desconocido') . "\n";
        $log .= "Sistema: " . PHP_OS . "\n";
        $log .= "PHP Version: " . PHP_VERSION . "\n";

        // Obtener usuario del proceso PHP
        if (function_exists('posix_getpwuid') && function_exists('posix_geteuid')) {
            $processUser = posix_getpwuid(posix_geteuid());
            $log .= "Usuario proceso PHP: " . ($processUser['name'] ?? 'desconocido') . "\n";
        } elseif (function_exists('exec') && !$this->isExecDisabled()) {
            $whoami = '';
            try {
                $whoami = exec('whoami');
            } catch (\Exception $e) {
                $whoami = '';
            }
            $log .= "Usuario proceso (whoami): " . ($whoami ?: 'no disponible') . "\n";
        }

        // Verificar funciones deshabilitadas
        $disabled = ini_get('disable_functions');
        if (!empty($disabled)) {
            $log .= "Funciones deshabilitadas: $disabled\n";
        } else {
            $log .= "Funciones deshabilitadas: ninguna\n";
        }

        // Verificar open_basedir
        $open_basedir = ini_get('open_basedir');
        if (!empty($open_basedir)) {
            $log .= "open_basedir: $open_basedir\n";
        }

        // Verificar safe_mode (PHP < 5.4)
        if (version_compare(PHP_VERSION, '5.4.0', '<')) {
            $safe_mode = ini_get('safe_mode');
            $log .= "safe_mode: " . ($safe_mode ? 'ON' : 'OFF') . "\n";
        }

        return $log;
    }

    /**
     * Guarda el log de eliminación con manejo de errores apropiado
     * @param string $log Contenido del log
     */
    protected function saveDeleteLog($log) {
        $log_file = bez_resolve_path('data/tmp/delete_project_log.txt');
        $this->safeFilePutContents($log_file, $log, FILE_APPEND | LOCK_EX);
    }

    /**
     * Elimina todas las tareas relacionadas con un proyecto
     */
    protected function deleteProjectTasks($project_id) {
        // Primero eliminar comentarios de las tareas
        $this->model->sqlite->query(
            'DELETE FROM task_comment WHERE task_id IN (SELECT id FROM task WHERE thread_id=?)',
            $project_id
        );
        // Eliminar participantes de las tareas
        $this->model->sqlite->query(
            'DELETE FROM task_participant WHERE task_id IN (SELECT id FROM task WHERE thread_id=?)',
            $project_id
        );
        // Eliminar las tareas
        $this->model->sqlite->query('DELETE FROM task WHERE thread_id=?', $project_id);
    }

    /**
     * Elimina todos los comentarios de un proyecto
     */
    protected function deleteProjectComments($project_id) {
        $this->model->sqlite->query('DELETE FROM thread_comment WHERE thread_id=?', $project_id);
    }

    /**
     * Elimina los registros de progreso de un proyecto
     * Esto previene que el sistema procese submódulos de proyectos eliminados
     */
    protected function deleteProjectProgress($project_id) {
        $this->model->sqlite->query('DELETE FROM progreso_proyecto WHERE proyecto_id=?', $project_id);
    }

    /**
     * Verifica si la columna deleted_at existe en la tabla thread
     * @return bool
     */
    protected function checkDeletedAtColumnExists() {
        try {
            $result = $this->model->sqlite->query("PRAGMA table_info(thread)");
            foreach ($result as $row) {
                if ($row['name'] === 'deleted_at') {
                    return true;
                }
            }
        } catch (\Exception $e) {
            return false;
        }
        return false;
    }

    /**
     * Obtiene la lista de proyectos eliminados (soft delete)
     * @return array Lista de proyectos eliminados con id, title, deleted_at
     */
    public function get_deleted_projects() {
        $sql = "SELECT id, title, type, coordinator, deleted_at
                FROM thread
                WHERE deleted_at IS NOT NULL
                ORDER BY deleted_at DESC";
        $r = $this->model->sqlite->query($sql);
        $projects = [];
        foreach ($r as $row) {
            $projects[] = $row;
        }
        return $projects;
    }

    /**
     * Restaura un proyecto eliminado (soft delete)
     * @param int $project_id ID del proyecto a restaurar
     * @return bool True si se restauró correctamente
     */
    public function restore_project($project_id) {
        // Verificar que el proyecto existe y está eliminado
        $sql = "SELECT id FROM thread WHERE id = ? AND deleted_at IS NOT NULL";
        $r = $this->model->sqlite->query($sql, $project_id);
        $row = $r->fetch();

        if (!$row) {
            return false;
        }

        // Restaurar el proyecto (quitar la marca de eliminado)
        $this->model->sqlite->query(
            'UPDATE thread SET deleted_at = NULL WHERE id = ?',
            $project_id
        );

        return true;
    }

    /**
     * Elimina permanentemente un proyecto (eliminación física)
     * Solo debe usarse para proyectos ya marcados como eliminados
     * @param int $project_id ID del proyecto a eliminar permanentemente
     * @return bool True si se eliminó correctamente
     */
    public function permanent_delete($project_id) {
        // Verificar que el proyecto existe y está eliminado
        $sql = "SELECT id FROM thread WHERE id = ? AND deleted_at IS NOT NULL";
        $r = $this->model->sqlite->query($sql, $project_id);
        $row = $r->fetch();

        if (!$row) {
            return false;
        }

        // Eliminar registros relacionados
        $this->model->sqlite->query('DELETE FROM thread_label WHERE thread_id=?', $project_id);
        $this->model->sqlite->query('DELETE FROM thread_participant WHERE thread_id=?', $project_id);

        // Eliminar el thread permanentemente
        $this->model->sqlite->query('DELETE FROM thread WHERE id=?', $project_id);

        return true;
    }

    /**
     * Elimina los permisos ACL asociados a un proyecto
     * Usa bloqueo de archivo para prevenir race conditions
     */
    protected function cleanupProjectACL($project_id) {
        $acl_file = DOKU_CONF . 'acl.auth.php';

        if (!file_exists($acl_file)) return;

        $project_namespace = 'es:bez:thread:id:' . $project_id;

        // Abrir con bloqueo exclusivo para prevenir race conditions
        $handle = fopen($acl_file, 'c+');
        if (!$handle) return;

        if (!flock($handle, LOCK_EX)) {
            fclose($handle);
            return;
        }

        try {
            // Leer todo el contenido mientras tenemos el bloqueo
            $content = stream_get_contents($handle);
            $lines = explode("\n", $content);
            $new_lines = [];

            foreach ($lines as $line) {
                $trimmed = trim($line);

                // Mantener comentarios y líneas vacías
                if (empty($trimmed) || $trimmed[0] === '#') {
                    $new_lines[] = $line;
                    continue;
                }

                // Eliminar líneas que contengan el namespace del proyecto
                if (strpos($trimmed, $project_namespace) === 0) {
                    continue; // No incluir esta línea
                }

                // También eliminar permisos del grupo @proyecto{id}
                if (strpos($trimmed, '@proyecto' . $project_id) !== false) {
                    continue;
                }

                $new_lines[] = $line;
            }

            // Truncar y escribir el nuevo contenido
            ftruncate($handle, 0);
            rewind($handle);
            fwrite($handle, implode("\n", $new_lines));
            fflush($handle);
        } finally {
            flock($handle, LOCK_UN);
            fclose($handle);
        }
    }

    /**
     * Elimina el grupo del proyecto de todos los usuarios
     * Usa bloqueo de archivo para prevenir race conditions
     */
    protected function cleanupProjectUserGroups($project_id) {
        $users_file = DOKU_CONF . 'users.auth.php';

        if (!file_exists($users_file)) return;

        $group_name = 'proyecto' . $project_id;

        // Abrir con bloqueo exclusivo para prevenir race conditions
        $handle = fopen($users_file, 'c+');
        if (!$handle) return;

        if (!flock($handle, LOCK_EX)) {
            fclose($handle);
            return;
        }

        try {
            // Leer todo el contenido mientras tenemos el bloqueo
            $content = stream_get_contents($handle);
            $lines = explode("\n", $content);
            $new_lines = [];

            foreach ($lines as $line) {
                $trimmed = trim($line);

                // Mantener comentarios y líneas vacías
                if (empty($trimmed) || $trimmed[0] === '#') {
                    $new_lines[] = $line;
                    continue;
                }

                // Parsear línea de usuario: login:passwordhash:Real Name:email:groups
                $parts = explode(':', $trimmed);
                if (count($parts) >= 5) {
                    $groups = array_filter(array_map('trim', explode(',', $parts[4])));

                    // Remover el grupo del proyecto
                    $groups = array_filter($groups, function($g) use ($group_name) {
                        return $g !== $group_name;
                    });

                    $parts[4] = implode(',', $groups);
                    $new_lines[] = implode(':', $parts);
                } else {
                    $new_lines[] = $line;
                }
            }

            // Truncar y escribir el nuevo contenido
            ftruncate($handle, 0);
            rewind($handle);
            fwrite($handle, implode("\n", $new_lines));
            fflush($handle);
        } finally {
            flock($handle, LOCK_UN);
            fclose($handle);
        }
    }

    /**
     * Normaliza una ruta para el sistema operativo actual
     */
    protected function normalizePath($path) {
        // Reemplazar todas las barras por el separador del sistema
        $path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
        // Eliminar barras duplicadas
        $path = preg_replace('#' . preg_quote(DIRECTORY_SEPARATOR, '#') . '+#', DIRECTORY_SEPARATOR, $path);
        // Eliminar barra final
        return rtrim($path, DIRECTORY_SEPARATOR);
    }

    /**
     * Obtiene la ruta base de datos de DokuWiki
     */
    protected function getDataDir() {
        global $conf;
        if (isset($conf['datadir'])) {
            return $this->normalizePath($conf['datadir']);
        }
        // Fallback: usar la ruta relativa a DOKU_INC
        return $this->normalizePath(bez_resolve_path('data/pages'));
    }

    /**
     * Obtiene la ruta de metadatos de DokuWiki
     */
    protected function getMetaDir() {
        global $conf;
        if (isset($conf['metadir'])) {
            return $this->normalizePath($conf['metadir']);
        }
        return $this->normalizePath(bez_resolve_path('data/meta'));
    }

    /**
     * Obtiene la ruta de attic de DokuWiki
     */
    protected function getOldDir() {
        global $conf;
        if (isset($conf['olddir'])) {
            return $this->normalizePath($conf['olddir']);
        }
        return $this->normalizePath(bez_resolve_path('data/attic'));
    }

    /**
     * Obtiene la ruta de media de DokuWiki
     */
    protected function getMediaDir() {
        global $conf;
        if (isset($conf['mediadir'])) {
            return $this->normalizePath($conf['mediadir']);
        }
        return $this->normalizePath(bez_resolve_path('data/media'));
    }

    /**
     * Guarda una copia de la carpeta general del proyecto en la carpeta de eliminados
     */
    protected function backupProjectGeneralFolder($project_id) {
        $datadir = $this->getDataDir();
        $ds = DIRECTORY_SEPARATOR;

        // Ruta de la carpeta general del proyecto (normalizada)
        $general_folder = $this->normalizePath($datadir . $ds . 'proyectos' . $ds . 'auditorias' . $ds . 'programas_auditoria' . $ds . 'general' . $ds . $project_id);

        // Verificar si la carpeta existe
        clearstatcache(true, $general_folder);
        if (!is_dir($general_folder)) {
            return;
        }

        // Crear carpeta de eliminados si no existe
        $eliminados_dir = $this->normalizePath($datadir . $ds . 'proyectos' . $ds . 'eliminados');
        if (!is_dir($eliminados_dir)) {
            mkdir($eliminados_dir, 0755, true);
        }

        // Nombre de la carpeta de respaldo con fecha y hora
        $timestamp = date('Y-m-d_His');
        $backup_folder = $this->normalizePath($eliminados_dir . $ds . $project_id . '_' . $timestamp);

        // Copiar toda la carpeta recursivamente
        $this->copyDirectoryRecursive($general_folder, $backup_folder);
    }

    /**
     * Copia un directorio recursivamente
     */
    protected function copyDirectoryRecursive($source, $dest) {
        $source = $this->normalizePath($source);
        $dest = $this->normalizePath($dest);

        clearstatcache(true, $source);
        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;

            $src_path = $this->normalizePath($source . DIRECTORY_SEPARATOR . $file);
            $dst_path = $this->normalizePath($dest . DIRECTORY_SEPARATOR . $file);

            clearstatcache(true, $src_path);
            if (is_dir($src_path)) {
                $this->copyDirectoryRecursive($src_path, $dst_path);
            } else {
                copy($src_path, $dst_path);
            }
        }
    }

    /**
     * Elimina las carpetas wiki asociadas al proyecto
     * @return string Log de la operación
     */
    protected function cleanupProjectWikiPages($project_id) {
        $log = "";
        $datadir = $this->getDataDir();
        $metadir = $this->getMetaDir();
        $olddir = $this->getOldDir();
        $mediadir = $this->getMediaDir();

        $log .= "  --- RUTAS BASE ---\n";
        $log .= "  DataDir (pages): $datadir\n";
        $log .= "  MetaDir: $metadir\n";
        $log .= "  OldDir (attic): $olddir\n";
        $log .= "  MediaDir: $mediadir\n";
        $log .= "  DOKU_INC: " . DOKU_INC . "\n";

        // Verificar que las carpetas base existen
        $log .= "  --- VERIFICACIÓN DE RUTAS ---\n";
        $log .= "  DataDir existe: " . (is_dir($datadir) ? "SÍ" : "NO") . "\n";
        $log .= "  DataDir permisos: " . (is_readable($datadir) ? "lectura " : "") . (is_writable($datadir) ? "escritura" : "") . "\n";

        // Verificar una ruta específica de prueba
        $test_path = $datadir . '/proyectos/control/' . $project_id;
        $log .= "  Ruta de prueba: $test_path\n";
        $log .= "  Ruta de prueba existe: " . (file_exists($test_path) ? "SÍ" : "NO") . "\n";
        if (file_exists($test_path)) {
            $log .= "  Ruta de prueba es directorio: " . (is_dir($test_path) ? "SÍ" : "NO") . "\n";
            $log .= "  Ruta de prueba permisos: " . decoct(fileperms($test_path) & 0777) . "\n";
        }

        // Primero, guardar copia de la carpeta general en carpeta de eliminados
        $this->backupProjectGeneralFolder($project_id);
        $log .= "  - Backup creado en carpeta 'eliminados'\n";

        // Lista completa de CARPETAS a eliminar (basada en la estructura real)
        $wiki_folders = [
            'proyectos/control/' . $project_id,
            'proyectos/generales/' . $project_id,
            'proyectos/auditorias/programas_auditoria/general/' . $project_id,
            'proyectos/auditorias/programas_auditoria/28_efectivo/' . $project_id,
            'proyectos/auditorias/programas_auditoria/29_inversiones/' . $project_id,
            'proyectos/auditorias/programas_auditoria/31_cuentas_cobrar/' . $project_id,
            'proyectos/auditorias/programas_auditoria/32_activos_intangibles/' . $project_id,
            'proyectos/auditorias/programas_auditoria/33_gastos_anticipado/' . $project_id,
            'proyectos/auditorias/programas_auditoria/34_pasivos_provisiones/' . $project_id,
            'proyectos/auditorias/programas_auditoria/35_impuestos_ganancias/' . $project_id,
            'proyectos/auditorias/programas_auditoria/36_ingresos_operacionales/' . $project_id,
            'proyectos/auditorias/programas_auditoria/37_contingencias/' . $project_id,
            'proyectos/auditorias/programas_auditoria/38_patrimonio/' . $project_id,
            'proyectos/auditorias/programas_auditoria/39_gastos_operacionales/' . $project_id,
            'proyectos/auditorias/programas_auditoria/40_costo_ventas/' . $project_id,
            'proyectos/auditorias/programas_auditoria/41_propiedad/' . $project_id,
            'proyectos/auditorias/programas_auditoria/43_estimaciones/' . $project_id,
            'proyectos/auditorias/programas_auditoria/44_gastos_personales/' . $project_id,
            'proyectos/auditorias/programas_auditoria/45_inventarios/' . $project_id,
            'proyectos/auditorias/programas_auditoria/46_otros_asuntos/' . $project_id,
            'proyectos/auditorias/programas_auditoria/47_ica_bimestral/' . $project_id,
            'proyectos/auditorias/programas_auditoria/48_iva_bimestral/' . $project_id,
            'proyectos/auditorias/programas_auditoria/49_rete_fte/' . $project_id,
            'proyectos/auditorias/programas_auditoria/50_rte_ica/' . $project_id,
            'proyectos/auditorias/programas_auditoria/51_regimen_simple/' . $project_id,
            'proyectos/auditorias/programas_auditoria/auditoria_de_cierre/' . $project_id,
            'proyectos/auditorias/programas_auditoria/auditoria_de_cierre2/' . $project_id,
            'proyectos/auditorias/programas_auditoria/bancos/' . $project_id,
            'proyectos/auditorias/programas_auditoria/bonos/' . $project_id,
            'proyectos/auditorias/programas_auditoria/cartera/' . $project_id,
            'proyectos/auditorias/programas_auditoria/consolidado/' . $project_id,
            'proyectos/auditorias/programas_auditoria/cuentas_por_cobrar/' . $project_id,
            'proyectos/auditorias/programas_auditoria/cuentas_por_cobrar2/' . $project_id,
            'proyectos/auditorias/programas_auditoria/cuentas_por_pagar/' . $project_id,
            'proyectos/auditorias/programas_auditoria/diferidos/' . $project_id,
            'proyectos/auditorias/programas_auditoria/disponible/' . $project_id,
            'proyectos/auditorias/programas_auditoria/disponible2/' . $project_id,
            'proyectos/auditorias/programas_auditoria/impuesto_al_patrimonio/' . $project_id,
            'proyectos/auditorias/programas_auditoria/impuestos/' . $project_id,
            'proyectos/auditorias/programas_auditoria/impuestos1/' . $project_id,
            'proyectos/auditorias/programas_auditoria/impuestos2/' . $project_id,
            'proyectos/auditorias/programas_auditoria/impuestos3/' . $project_id,
            'proyectos/auditorias/programas_auditoria/impuestos4/' . $project_id,
            'proyectos/auditorias/programas_auditoria/impuestos5/' . $project_id,
            'proyectos/auditorias/programas_auditoria/impuestos6/' . $project_id,
            'proyectos/auditorias/programas_auditoria/impuestos7/' . $project_id,
            'proyectos/auditorias/programas_auditoria/impuestos8/' . $project_id,
            'proyectos/auditorias/programas_auditoria/impuestos9/' . $project_id,
            'proyectos/auditorias/programas_auditoria/impuestos10/' . $project_id,
            'proyectos/auditorias/programas_auditoria/impuestos11/' . $project_id,
            'proyectos/auditorias/programas_auditoria/impuestos12/' . $project_id,
            'proyectos/auditorias/programas_auditoria/gasnomina/' . $project_id,
            'proyectos/auditorias/programas_auditoria/nomina/' . $project_id,
            'proyectos/auditorias/programas_auditoria/aportes/' . $project_id,
            'proyectos/auditorias/programas_auditoria/liquidacion/' . $project_id,
            'proyectos/auditorias/programas_auditoria/cumplimiento2/' . $project_id,
            'proyectos/auditorias/programas_auditoria/inventarios/' . $project_id,
            'proyectos/auditorias/programas_auditoria/inventarios2/' . $project_id,
            'proyectos/auditorias/programas_auditoria/inversiones/' . $project_id,
            'proyectos/auditorias/programas_auditoria/normatividad/' . $project_id,
            'proyectos/auditorias/programas_auditoria/obligaciones_financieras/' . $project_id,
            'proyectos/auditorias/programas_auditoria/otros_activos/' . $project_id,
            'proyectos/auditorias/programas_auditoria/pasivos_laborales/' . $project_id,
            'proyectos/auditorias/programas_auditoria/patrimonio/' . $project_id,
            'proyectos/auditorias/programas_auditoria/ppe/' . $project_id,
            'proyectos/auditorias/programas_auditoria/prestaciones/' . $project_id,
            'proyectos/auditorias/programas_auditoria/reestructuracion/' . $project_id,
        ];

        $deleted_count = 0;
        $not_found_count = 0;
        $error_count = 0;

        // Directorios base donde buscar
        $base_dirs = [
            'pages' => $datadir,
            'meta' => $metadir,
            'attic' => $olddir,
            'media' => $mediadir
        ];

        // Eliminar carpetas en cada ubicación
        foreach ($wiki_folders as $folder_path) {
            foreach ($base_dirs as $type => $base_dir) {
                // Normalizar la ruta completa para evitar problemas con separadores
                $full_path = $this->normalizePath($base_dir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $folder_path));

                clearstatcache(true, $full_path);

                if (!file_exists($full_path)) {
                    $not_found_count++;
                    continue;
                }

                $log .= "  [$type] Intentando eliminar: $full_path\n";

                // Método directo: eliminar archivos primero, luego carpeta
                $result = $this->forceDeleteDirectory($full_path);
                $log .= $result['log'];

                // Forzar limpieza de cache múltiples veces
                clearstatcache(true, $full_path);
                clearstatcache(true);
                usleep(50000); // Esperar 50ms

                // Verificación adicional con realpath
                $still_exists = file_exists($full_path) || is_dir($full_path);

                if (!$still_exists) {
                    $deleted_count++;
                    $log .= "    -> CONFIRMADO: Eliminado exitosamente\n";
                } else {
                    $error_count++;
                    // Información adicional de diagnóstico
                    $real_path = realpath($full_path);
                    $log .= "    -> ERROR: Aún existe (realpath: " . ($real_path ?: 'N/A') . ")\n";

                    // Intentar obtener más información
                    if (is_dir($full_path)) {
                        $remaining = $this->safeScandir($full_path);
                        if ($remaining) {
                            $remaining_files = array_diff($remaining, ['.', '..']);
                            $log .= "    -> Archivos restantes: " . count($remaining_files) . "\n";
                            if (count($remaining_files) <= 5) {
                                $log .= "    -> Nombres: " . implode(', ', $remaining_files) . "\n";
                            }
                        }
                    }
                }
            }
        }

        $log .= "  RESUMEN: $deleted_count eliminados, $not_found_count no existían, $error_count errores\n";
        return $log;
    }

    /**
     * Fuerza la eliminación de un directorio y su contenido
     * Usa múltiples métodos en cascada para asegurar la eliminación
     * @param string $dir Ruta del directorio
     * @return array ['success' => bool, 'log' => string]
     */
    protected function forceDeleteDirectory($dir) {
        $log = "";

        if (!file_exists($dir)) {
            return ['success' => true, 'log' => "No existe\n"];
        }

        // Verificar permisos
        $perms = fileperms($dir);
        $owner = function_exists('fileowner') ? fileowner($dir) : 'N/A';
        $log .= "      Permisos: " . decoct($perms & 0777) . ", Owner: $owner\n";

        // Listar contenido antes de eliminar
        $contents = $this->safeScandir($dir);
        if ($contents) {
            $file_count = count($contents) - 2; // Restar . y ..
            $log .= "      Archivos dentro: $file_count\n";
        }

        // MÉTODO 1: Eliminar archivos uno por uno con PHP (método seguro)
        $log .= "      Método 1 (PHP recursivo)... ";
        $this->deleteAllFilesInDirectory($dir);
        $this->safeRmdir($dir);
        clearstatcache(true);
        usleep(100000);
        if (!file_exists($dir) && !is_dir($dir)) {
            $log .= "OK\n";
            return ['success' => true, 'log' => $log];
        }
        $log .= "FALLÓ\n";

        // MÉTODO 2: io_rmdir de DokuWiki
        $log .= "      Método 2 (io_rmdir)... ";
        if (function_exists('io_rmdir')) {
            $result = io_rmdir($dir, true);
            $log .= "resultado: " . ($result ? "true" : "false") . "... ";
            clearstatcache(true);
            usleep(100000);
            if (!file_exists($dir) && !is_dir($dir)) {
                $log .= "OK\n";
                return ['success' => true, 'log' => $log];
            }
            $log .= "FALLÓ\n";
        } else {
            $log .= "NO DISPONIBLE\n";
        }

        // MÉTODO 3: Comando del sistema (Linux/Windows)
        $log .= "      Método 3 (comando sistema)... ";
        $isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';

        if (function_exists('shell_exec') && !$this->isShellExecDisabled()) {
            $escaped = escapeshellarg($dir);
            if ($isWindows) {
                $output = shell_exec("rmdir /s /q $escaped 2>&1");
            } else {
                $output = shell_exec("rm -rf $escaped 2>&1");
            }
            $log .= "output: " . trim($output ?: "ninguno") . "... ";
            clearstatcache(true);
            usleep(100000);
            if (!file_exists($dir) && !is_dir($dir)) {
                $log .= "OK\n";
                return ['success' => true, 'log' => $log];
            }
            $log .= "FALLÓ\n";
        } else {
            $log .= "shell_exec NO DISPONIBLE\n";
        }

        // MÉTODO 4: exec como alternativa
        $log .= "      Método 4 (exec)... ";
        if (function_exists('exec') && !$this->isExecDisabled()) {
            $escaped = escapeshellarg($dir);
            $exec_output = [];
            $return_var = -1;
            if ($isWindows) {
                exec("rmdir /s /q $escaped 2>&1", $exec_output, $return_var);
            } else {
                exec("rm -rf $escaped 2>&1", $exec_output, $return_var);
            }
            $log .= "return_var: $return_var... ";
            clearstatcache(true);
            usleep(100000);
            if (!file_exists($dir) && !is_dir($dir)) {
                $log .= "OK\n";
                return ['success' => true, 'log' => $log];
            }
            $log .= "FALLÓ\n";
        } else {
            $log .= "exec NO DISPONIBLE\n";
        }

        // Verificación final
        clearstatcache(true);
        $still_exists = file_exists($dir) || is_dir($dir);
        $log .= "      Verificación final: " . ($still_exists ? "AÚN EXISTE" : "ELIMINADO") . "\n";

        return ['success' => !$still_exists, 'log' => $log];
    }

    /**
     * Verifica si shell_exec está deshabilitado
     * @return bool
     */
    protected function isShellExecDisabled() {
        $disabled = explode(',', ini_get('disable_functions'));
        $disabled = array_map('trim', $disabled);
        return in_array('shell_exec', $disabled);
    }

    /**
     * Verifica si exec está deshabilitado
     * @return bool
     */
    protected function isExecDisabled() {
        $disabled = explode(',', ini_get('disable_functions'));
        $disabled = array_map('trim', $disabled);
        return in_array('exec', $disabled);
    }

    /**
     * Elimina todos los archivos dentro de un directorio recursivamente
     * Método unificado con manejo de errores apropiado
     * @param string $dir Ruta del directorio
     * @return bool True si se eliminó correctamente
     */
    protected function deleteAllFilesInDirectory($dir) {
        if (!is_dir($dir)) {
            // Es un archivo, eliminarlo
            $this->safeChmod($dir, 0777);
            return $this->safeUnlink($dir);
        }

        // Listar contenido con manejo de errores
        $items = $this->safeScandir($dir);
        if ($items === false) {
            return false;
        }

        $success = true;
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }

            $path = $this->normalizePath($dir . DIRECTORY_SEPARATOR . $item);

            if (is_dir($path)) {
                // Recursión para subdirectorios
                $this->deleteAllFilesInDirectory($path);
                // Eliminar directorio vacío
                $this->safeChmod($path, 0777);
                if (!$this->safeRmdir($path)) {
                    $success = false;
                }
            } else {
                // Eliminar archivo
                $this->safeChmod($path, 0777);
                if (!$this->safeUnlink($path)) {
                    $success = false;
                }
            }
        }
        return $success;
    }

    /**
     * Elimina un directorio y todo su contenido recursivamente
     * Método consolidado que usa forceDeleteDirectory internamente
     * @param string $dir Ruta del directorio
     * @return bool True si se eliminó correctamente
     */
    protected function deleteDirectoryRecursive($dir) {
        clearstatcache();
        $dir = $this->normalizePath($dir);

        if (!file_exists($dir)) {
            return true;
        }

        if (!is_dir($dir)) {
            $this->safeChmod($dir, 0777);
            return $this->safeUnlink($dir);
        }

        // Usar el método principal unificado
        $result = $this->forceDeleteDirectory($dir);
        return $result['success'];
    }

    // =========================================================================
    // MÉTODOS AUXILIARES CON MANEJO DE ERRORES
    // =========================================================================

    /**
     * Wrapper seguro para chmod sin supresión de errores
     * @param string $path Ruta del archivo/directorio
     * @param int $mode Modo de permisos
     * @return bool
     */
    protected function safeChmod($path, $mode) {
        if (!file_exists($path)) {
            return false;
        }
        try {
            return chmod($path, $mode);
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * Wrapper seguro para unlink sin supresión de errores
     * @param string $path Ruta del archivo
     * @return bool
     */
    protected function safeUnlink($path) {
        if (!file_exists($path) || is_dir($path)) {
            return false;
        }
        try {
            return unlink($path);
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * Wrapper seguro para rmdir sin supresión de errores
     * @param string $path Ruta del directorio
     * @return bool
     */
    protected function safeRmdir($path) {
        if (!is_dir($path)) {
            return false;
        }
        try {
            return rmdir($path);
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * Wrapper seguro para scandir sin supresión de errores
     * @param string $dir Ruta del directorio
     * @return array|false
     */
    protected function safeScandir($dir) {
        if (!is_dir($dir) || !is_readable($dir)) {
            return false;
        }
        try {
            return scandir($dir);
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * Wrapper seguro para file_put_contents
     * @param string $filename Ruta del archivo
     * @param mixed $data Contenido a escribir
     * @param int $flags Flags opcionales
     * @return int|false
     */
    protected function safeFilePutContents($filename, $data, $flags = 0) {
        try {
            return file_put_contents($filename, $data, $flags);
        } catch (\Exception $e) {
            return false;
        }
    }
}
