<?php
/**
 * Admin: Panel de administracion para Sincronizacion Wasabi S3
 * - Probar conexion con Wasabi
 * - Ver estadisticas (archivos sincronizados, espacio liberado)
 * - Migrar archivos existentes a Wasabi
 *
 * @author NICC
 * @date   2026-03-16
 */

class admin_plugin_wasabisync extends \dokuwiki\Extension\AdminPlugin {

    /** @var helper_plugin_wasabisync */
    private $helper;

    public function getMenuSort() { return 500; }
    public function forAdminOnly() { return true; }

    public function getMenuText($language) {
        return 'Sincronizacion Wasabi S3';
    }

    private function getHelper() {
        if ($this->helper === null) {
            $this->helper = plugin_load('helper', 'wasabisync');
        }
        return $this->helper;
    }

    /** Operaciones que requieren conexion directa (sin Cloudflare) por timeout */
    private static $HEAVY_ACTIONS = ['migrate_batch', 'verify_integrity', 'retruncate'];

    /**
     * Flash storage basado en archivos (DokuWiki no persiste $_SESSION custom vars)
     */
    private function flashFile() {
        global $conf;
        $dir = ($conf['tmpdir'] ?? (DOKU_INC . 'data/tmp'));
        return $dir . '/wasabi_flash_' . md5(session_id()) . '.json';
    }

    private function flashSet($key, $value) {
        $file = $this->flashFile();
        $data = file_exists($file) ? json_decode(file_get_contents($file), true) : [];
        $data[$key] = $value;
        file_put_contents($file, json_encode($data));
    }

    private function flashGet($key, $delete = true) {
        $file = $this->flashFile();
        if (!file_exists($file)) return null;
        $data = json_decode(file_get_contents($file), true);
        if (!isset($data[$key])) return null;
        $val = $data[$key];
        if ($delete) {
            unset($data[$key]);
            if (empty($data)) @unlink($file);
            else file_put_contents($file, json_encode($data));
        }
        return $val;
    }

    /**
     * Detecta si la conexion pasa por Cloudflare (header CF-RAY presente)
     */
    private function isCloudflare() {
        return isset($_SERVER['HTTP_CF_RAY']) || isset($_SERVER['HTTP_CF_CONNECTING_IP']);
    }

    /**
     * Mapeo de dominios Cloudflare a dominios directos (sin Cloudflare)
     * Cloudflare domain => direct domain (cPanel primary)
     */
    private function getDirectDomain() {
        $host = $_SERVER['HTTP_HOST'] ?? '';
        $map = [
            'sistemavirtualdegestion.com' => 'systemsie.site',
            'susprofesionales.com'        => 'susprofesionales.systemsie.site',
            'gyhcontadores.com'           => 'gyhcontadores.systemsie.site',
        ];
        foreach ($map as $cfDomain => $directDomain) {
            if (stripos($host, $cfDomain) !== false) {
                return str_ireplace($cfDomain, $directDomain, $host);
            }
        }
        return null;
    }

    /**
     * Redirige a dominio directo (sin Cloudflare) para operaciones pesadas.
     * Retorna true si se hizo redirect (debe salir), false si no aplica.
     */
    private function redirectIfCloudflare($action) {
        if (!in_array($action, self::$HEAVY_ACTIONS)) return false;
        if (!$this->isCloudflare()) return false;

        $directHost = $this->getDirectDomain();
        if (!$directHost) return false;

        // Construir URL directa preservando URI completa
        $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
        $directUrl = $scheme . '://' . $directHost . $_SERVER['REQUEST_URI'];

        // Guardar mensaje informativo en sesion
        $this->flashSet('wasabi_result', [
            'ok' => true,
            'msg' => 'Redirigido a conexion directa (sin Cloudflare) para evitar timeout de 100 seg. Ejecute la operacion nuevamente.'
        ]);

        header('Location: ' . $directUrl);
        exit;
    }

    /**
     * Verifica si el plugin requiere contrasena adicional y si ya fue ingresada
     */
    /**
     * Obtiene la contrasena del plugin (de config o fallback)
     */
    private function getPluginPassword() {
        $pw = trim($this->getConf('admin_password'));
        return $pw !== '' ? $pw : 'nicc2026wasabi';
    }

    private function isPluginAuthenticated() {
        // DokuWiki maneja sesiones con prefijo propio, $_SESSION directo no persiste.
        // Usar cookie HMAC como alternativa segura.
        $pw = $this->getPluginPassword();
        $secret = $pw . session_id();
        $expected = hash_hmac('sha256', 'wasabi_plugin_auth', $secret);
        return isset($_COOKIE['wasabi_auth']) && hash_equals($expected, $_COOKIE['wasabi_auth']);
    }

    /**
     * Renderiza formulario de contrasena de acceso al plugin
     */
    private function renderPasswordGate() {
        $error = '';
        $error = $this->flashGet('wasabi_pw_error') ?: '';

        echo '<h1>Sincronizacion Wasabi S3</h1>';
        echo '<div style="max-width:420px;margin:40px auto;background:#fff;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,0.10);padding:32px;">';
        echo '<div style="text-align:center;margin-bottom:18px;">';
        echo '<span style="font-size:2.5em;">&#128274;</span>';
        echo '<h2 style="margin:8px 0 4px;font-size:1.2em;color:#1e293b;">Ingrese la clave de acceso para continuar.</h2>';
        echo '</div>';
        if ($error) {
            echo '<div style="background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:10px;margin-bottom:14px;color:#991b1b;font-size:0.88em;text-align:center;">' . hsc($error) . '</div>';
        }
        echo '<form method="post" action="">';
        echo formSecurityToken(false);
        echo '<input type="hidden" name="wasabi_action" value="plugin_login" />';
        echo '<input type="password" name="wasabi_plugin_pw" autocomplete="off" required placeholder="Clave de acceso" ';
        echo 'style="width:100%;padding:10px 14px;border:1px solid #cbd5e1;border-radius:8px;font-size:1em;margin-bottom:16px;" />';
        echo '<button type="submit" style="width:100%;padding:11px;background:#2563eb;color:#fff;border:none;border-radius:8px;font-size:1em;font-weight:600;cursor:pointer;">Ingresar</button>';
        echo '</form>';
        echo '</div>';
    }

    /**
     * Maneja acciones POST (probar conexion, migrar, guardar credenciales)
     */
    public function handle() {
        global $INPUT;
        if (!$INPUT->has('wasabi_action')) return;

        // Verificar CSRF
        if (!checkSecurityToken()) return;

        $action = $INPUT->str('wasabi_action');

        // Login del plugin
        if ($action === 'plugin_login') {
            $inputPw = $INPUT->str('wasabi_plugin_pw');
            if ($inputPw === $this->getPluginPassword()) {
                $secret = $this->getPluginPassword() . session_id();
                $token = hash_hmac('sha256', 'wasabi_plugin_auth', $secret);
                setcookie('wasabi_auth', $token, 0, '/', '', true, true);
                $_COOKIE['wasabi_auth'] = $token; // disponible en el mismo request
            } else {
                $this->flashSet('wasabi_pw_error', 'Clave incorrecta.');
            }
            return;
        }

        // Logout del plugin
        if ($action === 'plugin_logout') {
            setcookie('wasabi_auth', '', time() - 3600, '/', '', true, true);
            unset($_COOKIE['wasabi_auth']);
            return;
        }

        // Bloquear acciones si no esta autenticado
        if (!$this->isPluginAuthenticated()) return;

        if ($action === 'save_credentials') {
            $this->handleSaveCredentials();
            return;
        }

        if ($action === 'toggle_enabled') {
            $this->handleToggleEnabled();
            return;
        }

        // Operaciones pesadas: redirigir a dominio directo si pasa por Cloudflare
        if ($this->redirectIfCloudflare($action)) {
            return;
        }

        $helper = $this->getHelper();
        if (!$helper) return;

        if ($action === 'test') {
            $this->handleTest($helper);
        } elseif ($action === 'migrate_batch') {
            $this->handleMigrateBatch($helper);
        } elseif ($action === 'verify_integrity') {
            $this->handleVerifyIntegrity($helper);
        } elseif ($action === 'retruncate') {
            $this->handleRetruncate($helper);
        } elseif ($action === 'debug_upload') {
            $this->handleDebugUpload($helper);
        } elseif ($action === 'reset_db') {
            $this->handleResetDB($helper);
        } elseif ($action === 'rebuild_db') {
            $this->handleRebuildDB($helper);
        }
    }

    /**
     * Renderiza el panel de administracion
     */
    public function html() {
        // Gate de contrasena
        if (!$this->isPluginAuthenticated()) {
            $this->renderPasswordGate();
            return;
        }

        $helper = $this->getHelper();

        echo '<h1>Sincronizacion Wasabi S3</h1>';

        // Boton logout (siempre visible, ya que siempre hay password gate)
        if (true) {
            echo '<div style="text-align:right;margin:-8px 0 6px;">';
            echo '<form method="post" action="" style="display:inline;">';
            echo formSecurityToken(false);
            echo '<input type="hidden" name="wasabi_action" value="plugin_logout" />';
            echo '<button type="submit" style="padding:4px 14px;background:#64748b;color:#fff;border:none;border-radius:4px;font-size:0.8em;cursor:pointer;">&#128274; Cerrar sesion plugin</button>';
            echo '</form>';
            echo '</div>';
        }

        // Indicador de conexion: Cloudflare vs Directa
        if ($this->isCloudflare()) {
            echo '<div style="background:#fef3c7;border:1px solid #f59e0b;border-radius:6px;padding:10px 14px;margin:8px 0;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;">';
            echo '<span style="color:#92400e;">&#9729; Conexion via <strong>Cloudflare</strong> &mdash; operaciones pesadas se redirigiran automaticamente a conexion directa.</span>';
            $directHost = $this->getDirectDomain();
            if ($directHost) {
                $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
                $directUrl = $scheme . '://' . $directHost . $_SERVER['REQUEST_URI'];
                echo '<a href="' . htmlspecialchars($directUrl) . '" style="padding:5px 12px;background:#2563eb;color:#fff;border-radius:4px;text-decoration:none;font-size:0.85em;white-space:nowrap;">&#9889; Ir a conexion directa</a>';
            }
            echo '</div>';
        } else {
            echo '<div style="background:#d1fae5;border:1px solid #10b981;border-radius:6px;padding:10px 14px;margin:8px 0;">';
            echo '<span style="color:#065f46;">&#9889; Conexion <strong>directa</strong> al servidor &mdash; sin limites de timeout ni upload.</span>';
            echo '</div>';
        }

        // Verificar configuracion
        if (!$helper || !$helper->isReady()) {
            if (!$this->getConf('enabled')) {
                echo '<div style="background:#fef3c7;border:1px solid #f59e0b;border-radius:8px;padding:14px 18px;margin:10px 0;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;">';
                echo '<div style="display:flex;align-items:center;gap:8px;">';
                echo '<span style="font-size:1.3em;">&#9888;&#65039;</span>';
                echo '<span style="color:#92400e;font-weight:600;">Plugin desactivado</span>';
                echo '</div>';
                echo '<form method="post" action="" style="margin:0;">';
                echo formSecurityToken(false);
                echo '<input type="hidden" name="wasabi_action" value="toggle_enabled" />';
                echo '<input type="hidden" name="wasabi_toggle" value="1" />';
                echo '<button type="submit" style="padding:8px 20px;background:#16a34a;color:#fff;border:none;border-radius:6px;font-weight:600;font-size:0.9em;cursor:pointer;">&#9889; Activar Plugin</button>';
                echo '</form>';
                echo '</div>';
            } else {
                echo '<div class="error">' . $this->getLang('no_credentials') . '</div>';
            }
            $this->renderConfig();
            return;
        }

        // Resultado de accion (si hay)
        $r = $this->flashGet('wasabi_result');
        if ($r) {
            $class = $r['ok'] ? 'success' : 'error';
            echo '<div class="' . $class . '">' . hsc($r['msg']) . '</div>';
        }

        // Debug output (si hay)
        $dbg = $this->flashGet('wasabi_debug');
        if ($dbg) {
            echo '<div style="background:#1e1e1e;color:#0f0;padding:15px;border-radius:4px;margin:10px 0;font-family:monospace;font-size:12px;white-space:pre-wrap;max-height:500px;overflow-y:auto;">';
            echo hsc($dbg);
            echo '</div>';
        }

        // Informacion del wiki
        echo '<div class="level2">';
        echo '<h2>Informacion</h2>';
        echo '<table class="inline">';
        echo '<tr><th>Wiki ID</th><td>' . hsc($helper->getWikiId()) . '</td></tr>';
        echo '<tr><th>Bucket</th><td>' . hsc($this->getConf('wasabi_bucket')) . '</td></tr>';
        echo '<tr><th>Region</th><td>' . hsc($this->getConf('wasabi_region')) . '</td></tr>';
        echo '<tr><th>Endpoint</th><td>' . hsc($this->getConf('wasabi_endpoint')) . '</td></tr>';
        echo '<tr><th>Estado</th><td style="display:flex;align-items:center;gap:10px;">';
        echo '<span style="color:green;font-weight:bold;">&#9989; Activo</span>';
        echo '<form method="post" action="" style="margin:0;display:inline;">';
        echo formSecurityToken(false);
        echo '<input type="hidden" name="wasabi_action" value="toggle_enabled" />';
        echo '<input type="hidden" name="wasabi_toggle" value="0" />';
        echo '<button type="submit" onclick="return confirm(\'¿Desactivar el plugin Sincronizacion Wasabi S3?\\n\\nLos archivos en Wasabi NO se eliminan.\')" ';
        echo 'style="padding:4px 12px;background:#dc2626;color:#fff;border:none;border-radius:4px;font-size:0.8em;cursor:pointer;">Desactivar</button>';
        echo '</form>';
        echo '</td></tr>';
        echo '</table>';
        echo '</div>';

        // Estadisticas
        $stats = $helper->dbStats();
        echo '<div class="level2">';
        echo '<h2>' . $this->getLang('stats_title') . '</h2>';
        echo '<table class="inline">';
        echo '<tr><th>' . $this->getLang('stats_files') . '</th><td><strong>' . $stats['count'] . '</strong></td></tr>';
        echo '<tr><th>' . $this->getLang('stats_size') . '</th><td><strong>' . $helper->formatSize($stats['size']) . '</strong></td></tr>';
        echo '</table>';
        echo '</div>';

        // Boton probar conexion
        echo '<div class="level2">';
        echo '<h2>' . $this->getLang('btn_test') . '</h2>';
        echo '<p>Sube un archivo de prueba a Wasabi, verifica que existe, lo descarga y lo elimina.</p>';
        echo '<form method="post" action="">';
        echo formSecurityToken(false);
        echo '<input type="hidden" name="wasabi_action" value="test" />';
        echo '<button type="submit" class="btn btn-default">' . $this->getLang('btn_test') . '</button>';
        echo '</form>';
        echo '</div>';

        // Seccion migracion
        echo '<div class="level2">';
        echo '<h2>' . $this->getLang('migrate_title') . '</h2>';
        echo '<p>' . $this->getLang('migrate_desc') . '</p>';
        $this->renderMigrationPanel($helper);
        echo '</div>';
    }

    /**
     * Formulario de credenciales directo (sin depender del config manager)
     */
    private function renderConfig() {
        // Leer valores actuales de conf/local.php
        $current = $this->readCurrentCredentials();

        echo '<div class="level2">';
        echo '<h2>Configuracion de Credenciales</h2>';
        echo '<p>Ingrese las credenciales de Wasabi S3. Se guardan en <code>conf/local.php</code> de este wiki.</p>';

        // Resultado de guardar (si hay)
        $r = $this->flashGet('wasabi_result');
        if ($r) {
            $class = $r['ok'] ? 'success' : 'error';
            echo '<div class="' . $class . '">' . hsc($r['msg']) . '</div>';
        }

        echo '<form method="post" action="">';
        echo formSecurityToken(false);
        echo '<input type="hidden" name="wasabi_action" value="save_credentials" />';
        echo '<table class="inline">';

        echo '<tr><th><label for="w_key">Clave de Acceso</label></th>';
        echo '<td><input type="text" id="w_key" name="wasabi_key" value="' . hsc($current['wasabi_key']) . '" size="40" autocomplete="off" /></td></tr>';

        echo '<tr><th><label for="w_secret">Clave Secreta</label></th>';
        echo '<td><input type="password" id="w_secret" name="wasabi_secret" value="' . hsc($current['wasabi_secret']) . '" size="40" autocomplete="off" /></td></tr>';

        echo '<tr><th><label for="w_bucket">Bucket</label></th>';
        echo '<td><input type="text" id="w_bucket" name="wasabi_bucket" value="' . hsc($current['wasabi_bucket'] ?: 'nicc-archivos') . '" size="30" /></td></tr>';

        echo '<tr><th><label for="w_region">Region</label></th>';
        echo '<td><input type="text" id="w_region" name="wasabi_region" value="' . hsc($current['wasabi_region'] ?: 'us-east-1') . '" size="20" /></td></tr>';

        echo '<tr><th><label for="w_endpoint">Endpoint</label></th>';
        echo '<td><input type="text" id="w_endpoint" name="wasabi_endpoint" value="' . hsc($current['wasabi_endpoint'] ?: 's3.wasabisys.com') . '" size="30" /></td></tr>';

        echo '<tr><th><label for="w_enabled">Activado</label></th>';
        $checked = ($current['enabled'] ?? 1) ? ' checked' : '';
        echo '<td><input type="checkbox" id="w_enabled" name="wasabi_enabled" value="1"' . $checked . ' /></td></tr>';

        echo '</table>';
        echo '<button type="submit" class="btn btn-default" style="margin-top:10px;">Guardar Credenciales</button>';
        echo '</form>';

        echo '</div>';
    }

    /**
     * Guarda un valor de configuracion del plugin en conf/local.php
     */
    private function savePluginConf($key, $value) {
        $localPhp = DOKU_INC . 'conf/local.php';
        $content = '';
        if (file_exists($localPhp)) {
            $content = file_get_contents($localPhp);
            if ($content === false) return false;
        } else {
            $content = "<?php\n";
        }

        $confLine = "\$conf['plugin']['wasabisync']['" . $key . "']";
        $pattern = '/^\s*\$conf\s*\[\s*[\'"]plugin[\'"]\s*\]\s*\[\s*[\'"]wasabisync[\'"]\s*\]\s*\[\s*[\'"]\Q' . $key . '\E[\'"]\s*\]\s*=.*$/m';

        if (is_int($value)) {
            $newLine = $confLine . ' = ' . $value . ';';
        } else {
            $newLine = $confLine . " = '" . addcslashes($value, "'\\") . "';";
        }

        if (preg_match($pattern, $content)) {
            $content = preg_replace($pattern, $newLine, $content);
        } else {
            $content = rtrim($content) . "\n" . $newLine . "\n";
        }

        return @file_put_contents($localPhp, $content) !== false;
    }

    /**
     * Lee las credenciales actuales de conf/local.php
     */
    private function readCurrentCredentials() {
        $defaults = [
            'enabled' => 1,
            'wasabi_key' => '',
            'wasabi_secret' => '',
            'wasabi_bucket' => 'nicc-archivos',
            'wasabi_region' => 'us-east-1',
            'wasabi_endpoint' => 's3.wasabisys.com',
        ];

        // Leer desde la configuracion de DokuWiki (ya cargada)
        foreach ($defaults as $k => $v) {
            $val = $this->getConf($k);
            if ($val !== null && $val !== '') {
                $defaults[$k] = $val;
            }
        }

        return $defaults;
    }

    /**
     * Guarda credenciales en conf/local.php
     */
    private function handleSaveCredentials() {
        global $INPUT;

        $localPhp = DOKU_INC . 'conf/local.php';

        // Leer contenido actual
        $content = '';
        if (file_exists($localPhp)) {
            $content = file_get_contents($localPhp);
            if ($content === false) {
                $this->flashSet('wasabi_result',['ok' => false, 'msg' => 'No se pudo leer conf/local.php']);
                return;
            }
        }

        // Valores del formulario
        $fields = [
            'wasabi_key'     => $INPUT->str('wasabi_key', ''),
            'wasabi_secret'  => $INPUT->str('wasabi_secret', ''),
            'wasabi_bucket'  => $INPUT->str('wasabi_bucket', 'nicc-archivos'),
            'wasabi_region'  => $INPUT->str('wasabi_region', 'us-east-1'),
            'wasabi_endpoint'=> $INPUT->str('wasabi_endpoint', 's3.wasabisys.com'),
            'enabled'        => $INPUT->has('wasabi_enabled') ? 1 : 0,
        ];

        // Validar campos obligatorios
        if (empty($fields['wasabi_key']) || empty($fields['wasabi_secret'])) {
            $this->flashSet('wasabi_result',['ok' => false, 'msg' => 'Clave de Acceso y Clave Secreta son obligatorios.']);
            return;
        }

        // Crear backup
        if (file_exists($localPhp)) {
            $backupPath = $localPhp . '.bak_wasabi_' . date('Ymd_His');
            @copy($localPhp, $backupPath);
        }

        // Actualizar o agregar cada linea de configuracion
        foreach ($fields as $key => $value) {
            $confLine = '$conf[\'plugin\'][\'wasabisync\'][\'' . $key . '\']';
            $pattern = '/^\s*\$conf\s*\[\s*[\'"]plugin[\'"]\s*\]\s*\[\s*[\'"]wasabisync[\'"]\s*\]\s*\[\s*[\'"]\Q' . $key . '\E[\'"]\s*\]\s*=.*$/m';

            if (is_int($value)) {
                $newLine = $confLine . ' = ' . $value . ';';
            } else {
                $newLine = $confLine . ' = \'' . addcslashes($value, "'\\") . '\';';
            }

            if (preg_match($pattern, $content)) {
                $content = preg_replace($pattern, $newLine, $content);
            } else {
                $content = rtrim($content) . "\n" . $newLine . "\n";
            }
        }

        // Escribir
        if (@file_put_contents($localPhp, $content) === false) {
            $this->flashSet('wasabi_result',['ok' => false, 'msg' => 'No se pudo escribir conf/local.php. Verifique permisos.']);
            return;
        }

        $this->flashSet('wasabi_result',['ok' => true, 'msg' => 'Credenciales guardadas correctamente. Recargue la pagina para aplicar.']);
    }

    /**
     * Activa o desactiva el plugin escribiendo en conf/local.php
     */
    private function handleToggleEnabled() {
        global $INPUT;

        $enable = (int)$INPUT->str('wasabi_toggle', '0');
        $localPhp = DOKU_INC . 'conf/local.php';

        // Leer contenido actual
        $content = '';
        if (file_exists($localPhp)) {
            $content = file_get_contents($localPhp);
            if ($content === false) {
                $this->flashSet('wasabi_result',['ok' => false, 'msg' => 'No se pudo leer conf/local.php']);
                return;
            }
        } else {
            $content = "<?php\n";
        }

        $confLine = "\$conf['plugin']['wasabisync']['enabled'] = {$enable};";
        $pattern = '/^\s*\$conf\s*\[\s*[\'"]plugin[\'"]\s*\]\s*\[\s*[\'"]wasabisync[\'"]\s*\]\s*\[\s*[\'"]enabled[\'"]\s*\]\s*=.*$/m';

        if (preg_match($pattern, $content)) {
            $content = preg_replace($pattern, $confLine, $content);
        } else {
            $content = rtrim($content) . "\n" . $confLine . "\n";
        }

        if (@file_put_contents($localPhp, $content) === false) {
            $this->flashSet('wasabi_result',['ok' => false, 'msg' => 'No se pudo escribir conf/local.php. Verifique permisos.']);
            return;
        }

        $estado = $enable ? 'ACTIVADO' : 'DESACTIVADO';
        $this->flashSet('wasabi_result',['ok' => true, 'msg' => "Plugin Sincronizacion Wasabi S3 {$estado} correctamente."]);
    }

    /**
     * Panel de migracion de archivos existentes
     */
    private function renderMigrationPanel($helper) {
        global $conf;

        // Conteo rapido: solo filesystem (sin consultar SQLite por archivo)
        $mediaDir = $conf['mediadir'] ?? (DOKU_INC . 'data/media');
        $counts = $this->quickCountMedia($mediaDir);
        $stats = $helper->dbStats();

        // Pendientes = archivos con contenido real - archivos ya en Wasabi
        $pending = max(0, $counts['with_content'] - $stats['count']);

        echo '<table class="inline">';
        echo '<tr><th>Archivos locales (total)</th><td>' . $counts['total'] . '</td></tr>';
        echo '<tr><th>Ya en Wasabi</th><td>' . $stats['count'] . '</td></tr>';
        echo '<tr><th>Pendientes de migrar</th><td><strong>' . $pending . '</strong></td></tr>';
        echo '<tr><th>Espacio a liberar (estimado)</th><td><strong>' . $helper->formatSize($counts['content_size']) . '</strong></td></tr>';
        echo '</table>';

        if ($pending > 0) {
            echo '<div style="margin-top:10px;display:flex;gap:15px;align-items:flex-end;flex-wrap:wrap;">';

            // Boton por lotes
            echo '<form method="post" action="">';
            echo formSecurityToken(false);
            echo '<input type="hidden" name="wasabi_action" value="migrate_batch" />';
            echo '<label>Archivos por lote: <input type="number" name="batch_size" value="50" min="1" max="5000" style="width:70px;" /></label> ';
            echo '<button type="submit" class="btn btn-default">' . $this->getLang('btn_migrate') . ' (lote)</button>';
            echo '</form>';

            // Boton migrar todo
            echo '<form method="post" action="" onsubmit="return confirm(\'Migrar TODOS los ' . $pending . ' archivos pendientes a Wasabi?\\nEsto puede tomar varios minutos.\');">';
            echo formSecurityToken(false);
            echo '<input type="hidden" name="wasabi_action" value="migrate_batch" />';
            echo '<input type="hidden" name="batch_size" value="' . $pending . '" />';
            echo '<button type="submit" class="btn btn-default" style="background:#059669;color:#fff;font-weight:bold;">Migrar TODO (' . $pending . ' archivos)</button>';
            echo '</form>';

            echo '</div>';
        } else {
            echo '<p style="color:green;font-weight:bold;">Todos los archivos estan sincronizados con Wasabi.</p>';
        }

        // Herramientas de diagnostico
        echo '<div style="margin-top:20px;padding:12px;background:#e8f4fd;border:1px solid #2196F3;border-radius:4px;">';
        echo '<strong>Herramientas:</strong>';
        echo '<div style="margin-top:8px;display:flex;gap:10px;flex-wrap:wrap;">';

        // Debug upload
        echo '<form method="post" action="">';
        echo formSecurityToken(false);
        echo '<input type="hidden" name="wasabi_action" value="debug_upload" />';
        echo '<button type="submit" class="btn btn-default" style="background:#2196F3;color:#fff;">Debug: subir 1 archivo</button>';
        echo '</form>';

        // Reset DB
        echo '<form method="post" action="" onsubmit="return confirm(\'Borrar TODOS los registros de SQLite?\\nLos archivos en Wasabi NO se borran.\');">';
        echo formSecurityToken(false);
        echo '<input type="hidden" name="wasabi_action" value="reset_db" />';
        echo '<button type="submit" class="btn btn-default" style="background:#dc2626;color:#fff;">Reiniciar SQLite</button>';
        echo '</form>';

        // Reconstruir SQLite desde Wasabi
        echo '<form method="post" action="" onsubmit="return confirm(\'Reconstruir SQLite listando todos los objetos en Wasabi?\\nEsto NO borra datos, solo registra archivos existentes en Wasabi.\');">';
        echo formSecurityToken(false);
        echo '<input type="hidden" name="wasabi_action" value="rebuild_db" />';
        echo '<button type="submit" class="btn btn-default" style="background:#7c3aed;color:#fff;font-weight:bold;">Reconstruir SQLite desde Wasabi</button>';
        echo '</form>';

        echo '</div></div>';

        // ============================================================
        // SECCION: Verificacion de Integridad + Re-truncar
        // ============================================================
        if ($counts['with_content'] > 0 && $stats['count'] > 0) {
            $integrity = $this->flashGet('wasabi_integrity', false);
            $hasValidCheck = false;
            $checkAge = 0;

            if ($integrity) {
                $checkAge = time() - $integrity['timestamp'];
                $hasValidCheck = ($checkAge < 3600 && $integrity['safe_to_truncate']);
            }

            // --- PASO 1: Verificar Integridad ---
            echo '<div style="margin-top:15px;padding:16px;background:#eff6ff;border:2px solid #3b82f6;border-radius:8px;">';
            echo '<h3 style="margin:0 0 8px;color:#1e40af;">&#128737; Verificacion de Integridad (obligatorio antes de truncar)</h3>';
            echo '<p style="font-size:0.9em;color:#334155;margin:0 0 8px;">Compara <strong>CADA archivo local</strong> contra Wasabi usando HEAD request. ';
            echo 'Verifica que el archivo <strong>existe en Wasabi</strong> y que el <strong>tamaño coincide</strong> con SQLite.</p>';

            echo '<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">';
            echo '<form method="post" action="" onsubmit="return confirm(\'Verificar integridad de todos los archivos?\\nEsto puede tomar varios minutos (1 HEAD request por archivo).\\n\\nNO modifica ni borra ningun archivo.\');">';
            echo formSecurityToken(false);
            echo '<input type="hidden" name="wasabi_action" value="verify_integrity" />';
            echo '<button type="submit" class="btn btn-default" style="background:#3b82f6;color:#fff;font-weight:bold;">&#128269; Verificar Integridad</button>';
            echo '</form>';

            // Mostrar estado de la ultima verificacion
            if ($integrity) {
                $agoMin = round($checkAge / 60);
                if ($integrity['safe_to_truncate']) {
                    echo '<span style="background:#dcfce7;color:#166534;padding:4px 10px;border-radius:4px;font-size:0.85em;font-weight:600;">';
                    echo '&#9989; Verificado OK: ' . $integrity['verified'] . ' archivos (hace ' . $agoMin . ' min)';
                    echo '</span>';
                } else {
                    echo '<span style="background:#fee2e2;color:#991b1b;padding:4px 10px;border-radius:4px;font-size:0.85em;font-weight:600;">';
                    echo '&#10060; PROBLEMAS: ' . $integrity['problems'] . ' archivos con fallo (hace ' . $agoMin . ' min)';
                    echo '</span>';
                }
            }
            echo '</div>';
            echo '</div>';

            // --- PASO 2: Re-truncar (solo disponible si verificacion OK) ---
            echo '<div style="margin-top:10px;padding:16px;border-radius:8px;';
            if ($hasValidCheck) {
                echo 'background:#f0fdf4;border:2px solid #16a34a;">';
                echo '<h3 style="margin:0 0 8px;color:#166534;">&#128274; Re-truncar archivos locales (verificacion aprobada)</h3>';
                echo '<p style="font-size:0.9em;color:#334155;margin:0 0 8px;">La verificacion confirmo que <strong>' . $integrity['verified'] . ' archivos</strong> estan seguros en Wasabi. ';
                echo 'Cada archivo se re-verifica individualmente con HEAD antes de truncar.</p>';
                echo '<form method="post" action="" onsubmit="return confirm(\'Re-truncar archivos locales verificados?\\n\\nProceso seguro:\\n1. Verifica OTRA VEZ con HEAD que el archivo existe en Wasabi\\n2. Compara tamaño SQLite vs Wasabi\\n3. SOLO trunca si todo coincide\\n\\nArchivos que no pasen la verificacion se PROTEGEN.\\n\\nContinuar?\');">';
                echo formSecurityToken(false);
                echo '<input type="hidden" name="wasabi_action" value="retruncate" />';
                echo '<button type="submit" class="btn btn-default" style="background:#16a34a;color:#fff;font-weight:bold;">&#128274; Re-truncar (con doble verificacion)</button>';
                echo '</form>';
            } else {
                echo 'background:#f8fafc;border:2px dashed #94a3b8;">';
                echo '<h3 style="margin:0 0 8px;color:#64748b;">&#128274; Re-truncar archivos locales</h3>';
                if (!$integrity) {
                    echo '<p style="font-size:0.9em;color:#94a3b8;">&#9888;&#65039; Debe ejecutar <strong>Verificar Integridad</strong> primero.</p>';
                } elseif (!$integrity['safe_to_truncate']) {
                    echo '<p style="font-size:0.9em;color:#dc2626;">&#10060; La verificacion encontro problemas. Corrija los ' . $integrity['problems'] . ' archivos con fallo antes de truncar.</p>';
                } else {
                    echo '<p style="font-size:0.9em;color:#d97706;">&#9203; La verificacion expiro (hace ' . round($checkAge / 60) . ' min, maximo 60 min). Ejecute la verificacion otra vez.</p>';
                }
                echo '<button disabled style="background:#cbd5e1;color:#94a3b8;padding:8px 16px;border:none;border-radius:4px;font-weight:bold;cursor:not-allowed;">&#128274; Re-truncar (bloqueado)</button>';
            }
            echo '</div>';
        }
    }

    /**
     * VERIFICACION DE INTEGRIDAD antes de cualquier truncado
     * Compara CADA archivo local contra Wasabi (HEAD) para confirmar existencia y tamaño
     */
    private function handleVerifyIntegrity($helper) {
        global $conf;
        @set_time_limit(600);

        $mediaDir = $conf['mediadir'] ?? (DOKU_INC . 'data/media');
        $localFiles = $this->scanMediaFiles($mediaDir, '', 50000);

        // Liberar sesion para no bloquear otros requests durante la verificacion masiva
        session_write_close();

        $lines = [];
        $lines[] = "=== VERIFICACION DE INTEGRIDAD WASABI ===";
        $lines[] = "Wiki ID: " . $helper->getWikiId();
        $lines[] = "Fecha: " . date('Y-m-d H:i:s');
        $lines[] = "Archivos locales encontrados: " . count($localFiles);
        $lines[] = "";

        // Ejecutar verificacion masiva
        $lines[] = "--- Verificando cada archivo contra Wasabi (HEAD) ---";
        $lines[] = "";

        $summary = $helper->verifyBulkIntegrity($localFiles, function($current, $total, $mediaId) use (&$lines) {
            // Solo loguear cada 100 archivos para no saturar la sesion
            if ($current % 100 === 0 || $current === $total) {
                $lines[] = "  Progreso: $current / $total ...";
            }
        });

        $lines[] = "";
        $lines[] = "=== RESULTADO ===";
        $lines[] = sprintf("Verificados OK:     %d", $summary['verified']);
        $lines[] = sprintf("Stubs (0 bytes):    %d", $summary['stubs']);
        $lines[] = sprintf("Sin registro SQLite: %d", $summary['missing_db']);
        $lines[] = sprintf("NO existe en Wasabi: %d", $summary['missing_s3']);
        $lines[] = sprintf("Tamaño diferente:   %d", $summary['size_mismatch']);
        $lines[] = sprintf("Errores conexion:   %d", $summary['errors']);
        $lines[] = sprintf("Tamaño verificado:  %s", $helper->formatSize($summary['total_verified_size']));
        $lines[] = "";

        if ($summary['safe_to_truncate']) {
            $lines[] = "*** RESULTADO: SEGURO PARA TRUNCAR ***";
            $lines[] = "Todos los archivos con contenido estan verificados en Wasabi con tamaño correcto.";
        } else {
            $lines[] = "*** RESULTADO: NO ES SEGURO TRUNCAR ***";
            $lines[] = "Hay archivos que NO se pudieron verificar. NO proceda con re-truncar.";
            $lines[] = "";

            // Mostrar detalles de archivos problematicos
            if (!empty($summary['details'])) {
                $lines[] = "--- Archivos con problemas ---";
                $maxShow = min(50, count($summary['details']));
                for ($i = 0; $i < $maxShow; $i++) {
                    $d = $summary['details'][$i];
                    $icon = '?';
                    switch ($d['status']) {
                        case 'missing_db':    $icon = '[NO-DB]'; break;
                        case 'missing_s3':    $icon = '[NO-S3]'; break;
                        case 'size_mismatch': $icon = '[SIZE!]'; break;
                        case 'error':         $icon = '[ERROR]'; break;
                    }
                    $lines[] = sprintf("  %s %s (%s) - %s",
                        $icon, $d['media_id'], $helper->formatSize($d['size']), $d['detail']);
                }
                if (count($summary['details']) > $maxShow) {
                    $lines[] = "  ... y " . (count($summary['details']) - $maxShow) . " mas.";
                }
            }
        }

        // Re-abrir sesion para guardar resultados (cerrada antes de verificacion masiva)
        @session_start();

        // Guardar resultado en sesion para que retruncate pueda usarlo
        $this->flashSet('wasabi_integrity', [
            'timestamp'        => time(),
            'safe_to_truncate' => $summary['safe_to_truncate'],
            'verified'         => $summary['verified'],
            'total_checked'    => $summary['total_checked'],
            'problems'         => $summary['missing_db'] + $summary['missing_s3'] + $summary['size_mismatch'] + $summary['errors'],
        ]);

        $this->flashSet('wasabi_debug', implode("\n", $lines));

        if ($summary['safe_to_truncate']) {
            $msg = sprintf(
                "INTEGRIDAD OK: %d archivos verificados (%s). Es seguro proceder con re-truncar.",
                $summary['verified'],
                $helper->formatSize($summary['total_verified_size'])
            );
            $this->flashSet('wasabi_result',['ok' => true, 'msg' => $msg]);
        } else {
            $problemas = $summary['missing_db'] + $summary['missing_s3'] + $summary['size_mismatch'] + $summary['errors'];
            $msg = sprintf(
                "INTEGRIDAD FALLIDA: %d archivos con problemas (%d sin DB, %d sin S3, %d tamaño diferente, %d errores). NO es seguro truncar.",
                $problemas, $summary['missing_db'], $summary['missing_s3'], $summary['size_mismatch'], $summary['errors']
            );
            $this->flashSet('wasabi_result',['ok' => false, 'msg' => $msg]);
        }
    }

    /**
     * Re-trunca archivos locales que ya estan en Wasabi
     * REQUIERE verificacion de integridad previa (handleVerifyIntegrity)
     * Cada archivo se verifica individualmente con HEAD antes de truncar
     */
    private function handleRetruncate($helper) {
        global $conf;
        @set_time_limit(600);

        // ============================================================
        // PASO 1: Verificar que se haya ejecutado verificacion reciente
        // ============================================================
        $integrity = $this->flashGet('wasabi_integrity', false);
        $maxAge = 3600; // La verificacion debe tener menos de 1 hora

        if (!$integrity) {
            $this->flashSet('wasabi_result', [
                'ok' => false,
                'msg' => 'BLOQUEADO: Debe ejecutar "Verificar Integridad" antes de re-truncar. No se ha realizado ninguna verificacion.'
            ]);
            return;
        }

        $age = time() - $integrity['timestamp'];
        if ($age > $maxAge) {
            $this->flashSet('wasabi_result', [
                'ok' => false,
                'msg' => sprintf(
                    'BLOQUEADO: La verificacion tiene %d minutos de antiguedad (maximo: %d min). Ejecute "Verificar Integridad" nuevamente.',
                    round($age / 60), round($maxAge / 60)
                )
            ]);
            return;
        }

        if (!$integrity['safe_to_truncate']) {
            $this->flashSet('wasabi_result', [
                'ok' => false,
                'msg' => sprintf(
                    'BLOQUEADO: La verificacion encontro %d archivos con problemas. Corrija los problemas antes de re-truncar.',
                    $integrity['problems']
                )
            ]);
            return;
        }

        // ============================================================
        // PASO 2: Re-truncar con verificacion HEAD individual
        // ============================================================

        // Liberar sesion para no bloquear otros requests durante HEAD masivo
        session_write_close();

        $mediaDir = $conf['mediadir'] ?? (DOKU_INC . 'data/media');
        $localFiles = $this->scanMediaFiles($mediaDir, '', 50000);

        $truncated = 0;
        $freed = 0;
        $skippedNoDb = 0;
        $skippedNoS3 = 0;
        $skippedSizeMismatch = 0;
        $failTruncate = 0;

        $lines = [];
        $lines[] = "=== RE-TRUNCAR CON VERIFICACION HEAD ===";
        $lines[] = "Fecha: " . date('Y-m-d H:i:s');
        $lines[] = "";

        foreach ($localFiles as $f) {
            if ($f['size'] <= 0) continue; // Ya es stub

            // PASO 2a: Verificar en SQLite
            $record = $helper->dbLookup($f['media_id']);
            if (!$record) {
                $skippedNoDb++;
                continue;
            }

            // PASO 2b: Verificar en Wasabi con HEAD
            $head = $helper->headObject($record['wasabi_key']);
            if (!$head['exists']) {
                $skippedNoS3++;
                $lines[] = "[NO-S3] SALTADO: {$f['media_id']} - NO existe en Wasabi, archivo local PROTEGIDO";
                continue;
            }

            // PASO 2c: Comparar tamaños
            $dbSize = (int)$record['file_size'];
            $wasabiSize = (int)$head['size'];
            if ($wasabiSize > 0 && $dbSize > 0 && $wasabiSize !== $dbSize) {
                $skippedSizeMismatch++;
                $lines[] = sprintf(
                    "[SIZE!] SALTADO: %s - SQLite=%s vs Wasabi=%s, archivo local PROTEGIDO",
                    $f['media_id'], $helper->formatSize($dbSize), $helper->formatSize($wasabiSize)
                );
                continue;
            }

            // PASO 2d: TODO VERIFICADO - truncar
            $fh = @fopen($f['path'], 'w');
            if ($fh) {
                ftruncate($fh, 0);
                fclose($fh);

                // Verificar que realmente se trunco
                clearstatcache(true, $f['path']);
                if (filesize($f['path']) === 0) {
                    $truncated++;
                    $freed += $f['size'];
                } else {
                    $failTruncate++;
                    $lines[] = "[FAIL] {$f['media_id']} - ftruncate OK pero filesize > 0";
                }
            } else {
                $failTruncate++;
                $lines[] = "[FAIL] {$f['media_id']} - No se pudo abrir para escritura (permisos)";
            }
        }

        // Re-abrir sesion para guardar resultados
        @session_start();

        // Invalidar la verificacion (ya se uso)
        $this->flashGet('wasabi_integrity');

        $lines[] = "";
        $lines[] = "=== RESULTADO RE-TRUNCAR ===";
        $lines[] = "Truncados OK: $truncated";
        $lines[] = "Espacio liberado: " . $helper->formatSize($freed);
        $lines[] = "Saltados (sin SQLite): $skippedNoDb";
        $lines[] = "Saltados (NO en Wasabi): $skippedNoS3";
        $lines[] = "Saltados (tamaño diferente): $skippedSizeMismatch";
        $lines[] = "Fallos truncar: $failTruncate";

        $this->flashSet('wasabi_debug', implode("\n", $lines));

        $msg = "Re-truncado: $truncated archivos, " . $helper->formatSize($freed) . " liberados.";
        $problems = $skippedNoS3 + $skippedSizeMismatch + $failTruncate;
        if ($skippedNoS3 > 0)  $msg .= " $skippedNoS3 saltados (no estan en Wasabi).";
        if ($skippedSizeMismatch > 0) $msg .= " $skippedSizeMismatch saltados (tamaño diferente).";
        if ($failTruncate > 0) $msg .= " $failTruncate no se pudieron truncar (permisos).";

        $this->flashSet('wasabi_result',['ok' => ($problems === 0), 'msg' => $msg]);
    }

    /**
     * Prueba de conexion: sube, verifica, descarga y borra un archivo test
     */
    private function handleTest($helper) {
        $testKey = $helper->getWikiId() . '/_test_wasabisync_' . time() . '.txt';
        $testContent = 'Wasabi sync test - ' . date('c');
        $tmpFile = tempnam(sys_get_temp_dir(), 'wtest_');
        file_put_contents($tmpFile, $testContent);

        $errors = [];

        // 1. PUT
        $r = $helper->putObject($tmpFile, $testKey, 'text/plain');
        if (!$r['ok']) {
            $errors[] = 'PUT fallo: ' . $r['error'];
        }
        @unlink($tmpFile);

        // 2. HEAD (verificar existencia)
        if (empty($errors)) {
            $r = $helper->headObject($testKey);
            if (!$r['exists']) {
                $errors[] = 'HEAD fallo: archivo no encontrado despues de subir';
            }
        }

        // 3. Pre-signed URL (verificar descarga)
        if (empty($errors)) {
            $url = $helper->getPresignedUrl($testKey, 60);
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT        => 15,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_SSL_VERIFYPEER => true,
            ]);
            $body = curl_exec($ch);
            $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            if ($code !== 200 || $body !== $testContent) {
                $errors[] = "GET pre-signed fallo: HTTP $code";
            }
        }

        // 4. DELETE
        if (empty($errors)) {
            $r = $helper->deleteObject($testKey);
            if (!$r['ok']) {
                $errors[] = 'DELETE fallo: ' . $r['error'];
            }
        }

        // Resultado
        if (empty($errors)) {
            $this->flashSet('wasabi_result',['ok' => true, 'msg' => $this->getLang('test_success')]);
        } else {
            $this->flashSet('wasabi_result',['ok' => false, 'msg' => $this->getLang('test_fail') . implode('; ', $errors)]);
        }
    }

    /**
     * Migra un lote de archivos existentes a Wasabi
     */
    private function handleMigrateBatch($helper) {
        global $conf, $INPUT;

        $batchSize = max(1, min(50000, $INPUT->int('batch_size', 50)));
        @set_time_limit(600); // 10 minutos para migraciones grandes
        $mediaDir = $conf['mediadir'] ?? (DOKU_INC . 'data/media');
        $localFiles = $this->scanMediaFiles($mediaDir, '', 50000);

        // Liberar sesion para no bloquear otros requests durante PUT+HEAD masivos
        session_write_close();

        $migrated = 0;
        $totalSize = 0;
        $errors = [];

        foreach ($localFiles as $f) {
            if ($migrated >= $batchSize) break;

            // Solo archivos con contenido real (no stubs de 0 bytes)
            if ($f['size'] <= 0) continue;

            // Ya esta en Wasabi?
            $record = $helper->dbLookup($f['media_id']);
            if ($record) continue;

            // Subir a Wasabi
            $key = $helper->mediaIdToKey($f['media_id']);
            $mime = mimetype($f['path'], true);
            $mimeType = $mime[1] ?? 'application/octet-stream';

            $result = $helper->putObject($f['path'], $key, $mimeType);

            if ($result['ok']) {
                // VERIFICAR que realmente llego a Wasabi con HEAD
                $head = $helper->headObject($key);
                if (!$head['exists']) {
                    $errors[] = $f['media_id'] . ': PUT retorno OK pero HEAD no encuentra el archivo en Wasabi';
                    if (count($errors) >= 3) break;
                    continue;
                }

                // VERIFICAR tamaño: comparar local vs Wasabi
                $wasabiSize = (int)$head['size'];
                if ($wasabiSize > 0 && $wasabiSize !== $f['size']) {
                    $errors[] = sprintf('%s: tamaño no coincide (local=%s, wasabi=%s) - archivo local PROTEGIDO',
                        $f['media_id'], $helper->formatSize($f['size']), $helper->formatSize($wasabiSize));
                    if (count($errors) >= 3) break;
                    continue;
                }

                // Registrar en SQLite
                $helper->dbRegister($f['media_id'], $key, $f['size'], filemtime($f['path']), $mimeType);

                // Truncar archivo local a 0 bytes (stub)
                $fh = @fopen($f['path'], 'w');
                if ($fh) {
                    ftruncate($fh, 0);
                    fclose($fh);
                } else {
                    @file_put_contents($f['path'], '');
                }

                // Verificar truncacion
                clearstatcache(true, $f['path']);
                if (filesize($f['path']) > 0) {
                    $errors[] = $f['media_id'] . ': en Wasabi OK pero no se pudo truncar localmente';
                }

                $migrated++;
                $totalSize += $f['size'];
            } else {
                $errors[] = $f['media_id'] . ': ' . $result['error'];
                if (count($errors) >= 3) break; // Parar si hay muchos errores
            }
        }

        // Re-abrir sesion para guardar resultados
        @session_start();

        $msg = sprintf($this->getLang('migrate_done'), $migrated, $helper->formatSize($totalSize));
        if (!empty($errors)) {
            $msg .= ' Errores: ' . implode('; ', $errors);
        }

        $this->flashSet('wasabi_result',['ok' => empty($errors), 'msg' => $msg]);
    }

    /**
     * Debug: sube UN solo archivo y muestra todo el proceso en pantalla
     */
    private function handleDebugUpload($helper) {
        global $conf;
        @set_time_limit(120);

        $mediaDir = $conf['mediadir'] ?? (DOKU_INC . 'data/media');
        $localFiles = $this->scanMediaFiles($mediaDir, '', 100);

        // Buscar el primer archivo con contenido que NO este en SQLite
        $target = null;
        foreach ($localFiles as $f) {
            if ($f['size'] > 0) {
                $record = $helper->dbLookup($f['media_id']);
                if (!$record) {
                    $target = $f;
                    break;
                }
            }
        }

        $lines = [];
        $lines[] = "=== DEBUG UPLOAD ===";
        $lines[] = "DOKU_INC: " . DOKU_INC;
        $lines[] = "Wiki ID: " . $helper->getWikiId();
        $lines[] = "Bucket: " . $helper->getConf('wasabi_bucket');
        $lines[] = "Region: " . $helper->getConf('wasabi_region');
        $lines[] = "Endpoint: " . $helper->getConf('wasabi_endpoint');
        $lines[] = "Clave de Acceso: " . substr($helper->getConf('wasabi_key'), 0, 6) . '...';
        $lines[] = "Clave Secreta: " . (empty($helper->getConf('wasabi_secret')) ? 'VACIO!' : 'configurado (' . strlen($helper->getConf('wasabi_secret')) . ' chars)');
        $lines[] = "isReady: " . ($helper->isReady() ? 'SI' : 'NO');
        $lines[] = "curl disponible: " . (function_exists('curl_init') ? 'SI' : 'NO');
        $lines[] = "Archivos escaneados: " . count($localFiles);
        $lines[] = "";

        if (!$target) {
            $lines[] = "ERROR: No hay archivos pendientes para subir.";
            $lines[] = "Archivos con contenido encontrados:";
            $count = 0;
            foreach ($localFiles as $f) {
                if ($f['size'] > 0) {
                    $inDb = $helper->dbLookup($f['media_id']) ? 'SI' : 'NO';
                    $lines[] = "  - {$f['media_id']} ({$f['size']} bytes) en_SQLite=$inDb";
                    $count++;
                    if ($count >= 5) break;
                }
            }
            $this->flashSet('wasabi_debug', implode("\n", $lines));
            return;
        }

        $lines[] = "Archivo seleccionado: {$target['media_id']}";
        $lines[] = "Ruta: {$target['path']}";
        $lines[] = "Tamano: {$target['size']} bytes";
        $lines[] = "Existe: " . (file_exists($target['path']) ? 'SI' : 'NO');
        $lines[] = "Legible: " . (is_readable($target['path']) ? 'SI' : 'NO');
        $lines[] = "";

        // Generar key
        $key = $helper->mediaIdToKey($target['media_id']);
        $lines[] = "Key Wasabi: $key";

        // MIME
        $mime = mimetype($target['path'], true);
        $mimeType = $mime[1] ?? 'application/octet-stream';
        $lines[] = "MIME: $mimeType";
        $lines[] = "";

        // Intentar subir
        $lines[] = "--- Iniciando putObject ---";
        $result = $helper->putObject($target['path'], $key, $mimeType);
        $lines[] = "Resultado: " . json_encode($result);
        $lines[] = "";

        if ($result['ok']) {
            // Verificar con HEAD
            $lines[] = "--- Verificando con headObject ---";
            $head = $helper->headObject($key);
            $lines[] = "HEAD resultado: " . json_encode($head);
            $lines[] = "";

            // SEGURIDAD: verificar que HEAD confirma existencia
            if (!$head['exists']) {
                $lines[] = "ABORTAR: PUT retorno OK pero HEAD no encuentra el archivo en Wasabi.";
                $lines[] = "Archivo local PROTEGIDO - NO se trunca.";
                $this->flashSet('wasabi_debug', implode("\n", $lines));
                return;
            }

            // SEGURIDAD: verificar tamaño
            $wasabiSize = (int)$head['size'];
            if ($wasabiSize > 0 && $wasabiSize !== $target['size']) {
                $lines[] = sprintf("ABORTAR: tamaño no coincide (local=%d, wasabi=%d).", $target['size'], $wasabiSize);
                $lines[] = "Archivo local PROTEGIDO - NO se trunca.";
                $this->flashSet('wasabi_debug', implode("\n", $lines));
                return;
            }

            // Registrar en SQLite
            $lines[] = "--- Registrando en SQLite ---";
            $dbOk = $helper->dbRegister($target['media_id'], $key, $target['size'], filemtime($target['path']), $mimeType);
            $lines[] = "dbRegister: " . ($dbOk ? 'OK' : 'FALLO');

            // Verificar registro
            $record = $helper->dbLookup($target['media_id']);
            $lines[] = "dbLookup: " . ($record ? json_encode($record) : 'NO ENCONTRADO');
            $lines[] = "";

            // Truncar (solo si HEAD verifico OK)
            $lines[] = "--- Truncando archivo local ---";
            $fh = @fopen($target['path'], 'w');
            if ($fh) {
                $trOk = ftruncate($fh, 0);
                fclose($fh);
                $lines[] = "ftruncate: " . ($trOk ? 'OK' : 'FALLO');
            } else {
                $lines[] = "fopen('w') FALLO - intentando file_put_contents";
                $fpOk = file_put_contents($target['path'], '');
                $lines[] = "file_put_contents: " . ($fpOk !== false ? 'OK' : 'FALLO');
            }

            // Verificar tamano final
            clearstatcache(true, $target['path']);
            $newSize = filesize($target['path']);
            $lines[] = "Tamano despues de truncar: $newSize bytes";
            $lines[] = "";

            if ($newSize === 0) {
                $lines[] = "EXITO COMPLETO: archivo subido a Wasabi y truncado localmente.";
            } else {
                $lines[] = "PROBLEMA: archivo subido pero NO se pudo truncar (permisos del servidor).";
            }
        } else {
            $lines[] = "FALLO: el archivo NO se subio a Wasabi.";
            $lines[] = "Error: " . $result['error'];
            $lines[] = "HTTP Status: " . $result['status'];
        }

        $this->flashSet('wasabi_debug', implode("\n", $lines));
    }

    /**
     * Resetea la base SQLite (borra todos los registros falsos)
     */
    private function handleResetDB($helper) {
        $db = $helper->getDB();
        if ($db) {
            $db->query('DELETE FROM wasabi_files');
            $this->flashSet('wasabi_result',['ok' => true, 'msg' => 'Base de datos SQLite limpiada. Todos los registros eliminados.']);
        } else {
            $this->flashSet('wasabi_result',['ok' => false, 'msg' => 'No se pudo conectar a SQLite.']);
        }
    }

    /**
     * Reconstruye la base SQLite desde los objetos existentes en Wasabi
     * Lista todos los objetos en el bucket con prefijo del wiki actual
     * y los registra en SQLite.
     */
    private function handleRebuildDB($helper) {
        @set_time_limit(300);

        // Liberar sesion para no bloquear otros requests durante listObjects + registro masivo
        session_write_close();

        $lines = [];
        $lines[] = "=== RECONSTRUIR SQLITE DESDE WASABI ===";
        $lines[] = "Wiki ID: " . $helper->getWikiId();
        $lines[] = "Prefijo: " . $helper->getWikiId() . "/";
        $lines[] = "";

        // Listar objetos en Wasabi
        $lines[] = "--- Listando objetos en Wasabi ---";
        $prefix = $helper->getWikiId() . '/';
        $result = $helper->listObjects($prefix);

        if (!$result['ok']) {
            $lines[] = "ERROR: No se pudo listar Wasabi: " . $result['error'];
            @session_start();
            $this->flashSet('wasabi_debug', implode("\n", $lines));
            $this->flashSet('wasabi_result',['ok' => false, 'msg' => 'Error listando Wasabi: ' . $result['error']]);
            return;
        }

        $objects = $result['objects'];
        $lines[] = "Objetos encontrados: " . count($objects);
        $lines[] = "";

        if (empty($objects)) {
            $lines[] = "No hay objetos en Wasabi con prefijo '$prefix'.";
            $lines[] = "Nada que reconstruir.";
            @session_start();
            $this->flashSet('wasabi_debug', implode("\n", $lines));
            $this->flashSet('wasabi_result',['ok' => true, 'msg' => 'No se encontraron objetos en Wasabi para este wiki.']);
            return;
        }

        // Registrar cada objeto en SQLite
        $lines[] = "--- Registrando en SQLite ---";
        $registered = 0;
        $skipped = 0;
        $errors = 0;
        $totalSize = 0;

        foreach ($objects as $obj) {
            $key = $obj['key'];
            $size = $obj['size'];

            // Ignorar archivos de test
            if (strpos($key, '_test_wasabisync_') !== false) {
                $skipped++;
                continue;
            }

            // Convertir key a media_id
            $mediaId = $helper->keyToMediaId($key);
            if (!$mediaId) {
                $lines[] = "  SKIP (no es de este wiki): $key";
                $skipped++;
                continue;
            }

            // Detectar MIME type desde la extension
            $ext = strtolower(pathinfo($key, PATHINFO_EXTENSION));
            $mimeMap = [
                'pdf'  => 'application/pdf',
                'doc'  => 'application/msword',
                'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
                'xls'  => 'application/vnd.ms-excel',
                'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                'ppt'  => 'application/vnd.ms-powerpoint',
                'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
                'jpg'  => 'image/jpeg',
                'jpeg' => 'image/jpeg',
                'png'  => 'image/png',
                'gif'  => 'image/gif',
                'zip'  => 'application/zip',
                'rar'  => 'application/x-rar-compressed',
                'txt'  => 'text/plain',
                'csv'  => 'text/csv',
                'html' => 'text/html',
                'mp4'  => 'video/mp4',
                'mp3'  => 'audio/mpeg',
            ];
            $mimeType = $mimeMap[$ext] ?? 'application/octet-stream';

            // Parsear fecha de Wasabi (ISO 8601)
            $mtime = strtotime($obj['lastModified']) ?: time();

            // Registrar en SQLite (INSERT OR REPLACE)
            $ok = $helper->dbRegister($mediaId, $key, $size, $mtime, $mimeType);
            if ($ok) {
                $registered++;
                $totalSize += $size;
            } else {
                $errors++;
                $lines[] = "  ERROR registrando: $mediaId";
            }
        }

        $lines[] = "";
        $lines[] = "--- Resultado ---";
        $lines[] = "Registrados: $registered";
        $lines[] = "Omitidos: $skipped";
        $lines[] = "Errores: $errors";
        $lines[] = "Tamano total: " . $helper->formatSize($totalSize);
        $lines[] = "";

        // Mostrar primeros registros como muestra
        $stats = $helper->dbStats();
        $lines[] = "Total en SQLite ahora: " . $stats['count'] . " archivos, " . $helper->formatSize($stats['size']);

        // Re-abrir sesion para guardar resultados
        @session_start();

        $this->flashSet('wasabi_debug', implode("\n", $lines));

        $msg = "Reconstruccion completada: $registered archivos registrados en SQLite (" . $helper->formatSize($totalSize) . ").";
        if ($errors > 0) $msg .= " $errors errores.";
        if ($skipped > 0) $msg .= " $skipped omitidos.";

        $this->flashSet('wasabi_result',['ok' => ($errors === 0), 'msg' => $msg]);
    }

    /**
     * Conteo rapido de archivos media (solo filesystem, sin SQLite)
     * @return array ['total' => int, 'with_content' => int, 'stubs' => int, 'content_size' => int]
     */
    private function quickCountMedia($dir) {
        $result = ['total' => 0, 'with_content' => 0, 'stubs' => 0, 'content_size' => 0];
        $this->_countMediaRecursive($dir, $result);
        return $result;
    }

    private function _countMediaRecursive($dir, &$result) {
        $dir = rtrim(str_replace('\\', '/', $dir), '/');
        $handle = @opendir($dir);
        if (!$handle) return;

        while (($entry = readdir($handle)) !== false) {
            if ($entry[0] === '.') continue;
            if ($entry === '_dummy') continue;

            $fullPath = $dir . '/' . $entry;

            if (is_dir($fullPath)) {
                $this->_countMediaRecursive($fullPath, $result);
            } elseif (is_file($fullPath)) {
                $result['total']++;
                $size = filesize($fullPath);
                if ($size > 0) {
                    $result['with_content']++;
                    $result['content_size'] += $size;
                } else {
                    $result['stubs']++;
                }
            }
        }
        closedir($handle);
    }

    /**
     * Escanea archivos media locales recursivamente (con detalle)
     * @return array [['path' => ..., 'media_id' => ..., 'size' => ...], ...]
     */
    private function scanMediaFiles($dir, $ns = '', $maxFiles = 500) {
        $files = [];
        $dir = rtrim(str_replace('\\', '/', $dir), '/');

        $handle = @opendir($dir);
        if (!$handle) return $files;

        while (($entry = readdir($handle)) !== false) {
            if ($entry[0] === '.') continue;
            if ($entry === '_dummy') continue;

            $fullPath = $dir . '/' . $entry;

            if (count($files) >= $maxFiles) break;

            if (is_dir($fullPath)) {
                $subNs = $ns ? ($ns . ':' . $entry) : $entry;
                $remaining = $maxFiles - count($files);
                if ($remaining <= 0) break;
                $subFiles = $this->scanMediaFiles($fullPath, $subNs, $remaining);
                $files = array_merge($files, $subFiles);
            } elseif (is_file($fullPath)) {
                $mediaId = $ns ? ($ns . ':' . $entry) : $entry;
                $files[] = [
                    'path'     => $fullPath,
                    'media_id' => $mediaId,
                    'size'     => filesize($fullPath),
                ];
            }
        }
        closedir($handle);

        return $files;
    }
}
