<?php
/**
 * Backup a Wasabi S3 v2.0 - aplicar.php
 * ========================================
 * UI web para backup de todos los wikis DokuWiki a nicc-sistema.
 *
 * v2.0: Arquitectura por lotes (batches de 30 archivos) para evitar
 *       timeout de Cloudflare (100s). Sin pantalla de login.
 *       Lock con auto-expiracion. Error handler global.
 *
 * Acciones AJAX (via POST ?action=xxx):
 *   scan         - Lista todos los wikis (<1s)
 *   list_files   - Lista archivos de UN wiki (<5s)
 *   upload_batch - Sube batch de max 30 archivos (<60s)
 *   cleanup      - Borra obsoletos de Wasabi para un wiki
 *   test         - Prueba conexion a Wasabi
 */

// ============================================================
// ERROR HANDLER GLOBAL - NUNCA PAGINA EN BLANCO
// ============================================================
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');

// Detectar si es request AJAX
$_IS_AJAX = (
    isset($_GET['action']) ||
    isset($_POST['action']) ||
    (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') ||
    (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false)
);

set_exception_handler(function($e) {
    global $_IS_AJAX;
    if ($_IS_AJAX) {
        if (!headers_sent()) {
            header('Content-Type: application/json; charset=utf-8');
            http_response_code(500);
        }
        echo json_encode([
            'status' => 'error',
            'error'  => 'Excepcion: ' . $e->getMessage(),
            'file'   => basename($e->getFile()) . ':' . $e->getLine(),
        ]);
    } else {
        if (!headers_sent()) header('Content-Type: text/html; charset=utf-8');
        echo '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Error</title></head><body style="background:#0a0e27;color:#e0e0e0;font-family:monospace;padding:40px">';
        echo '<div style="background:#c62828;color:white;padding:20px;border-radius:8px;max-width:800px;margin:0 auto">';
        echo '<h2 style="margin:0 0 10px">Error Fatal</h2>';
        echo '<pre style="white-space:pre-wrap;word-break:break-all">' . htmlspecialchars($e->getMessage()) . "\n\n" . htmlspecialchars($e->getTraceAsString()) . '</pre>';
        echo '</div></body></html>';
    }
    exit(1);
});

set_error_handler(function($severity, $msg, $file, $line) {
    if (!(error_reporting() & $severity)) return false;
    throw new \ErrorException($msg, 0, $severity, $file, $line);
});

register_shutdown_function(function() {
    global $_IS_AJAX;
    $err = error_get_last();
    if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
        if ($_IS_AJAX) {
            if (!headers_sent()) {
                header('Content-Type: application/json; charset=utf-8');
                http_response_code(500);
            }
            echo json_encode([
                'status' => 'error',
                'error'  => 'Fatal: ' . $err['message'],
                'file'   => basename($err['file']) . ':' . $err['line'],
            ]);
        } else {
            if (!headers_sent()) header('Content-Type: text/html; charset=utf-8');
            echo '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Error Fatal</title></head><body style="background:#0a0e27;color:#e0e0e0;font-family:monospace;padding:40px">';
            echo '<div style="background:#c62828;color:white;padding:20px;border-radius:8px;max-width:800px;margin:0 auto">';
            echo '<h2 style="margin:0 0 10px">Error Fatal PHP</h2>';
            echo '<pre style="white-space:pre-wrap">' . htmlspecialchars($err['message'] . ' en ' . basename($err['file']) . ':' . $err['line']) . '</pre>';
            echo '</div></body></html>';
        }
    }
});

// ============================================================
// INCLUDES Y CONFIGURACION
// ============================================================

require_once __DIR__ . '/functions.php';

@set_time_limit(MAX_REQUEST_TIME);
@ini_set('memory_limit', MEMORY_LIMIT);

// ============================================================
// ROUTER AJAX - Todas las acciones via GET ?action=xxx o POST action=xxx
// ============================================================

$action = '';
if (isset($_GET['action'])) {
    $action = $_GET['action'];
} elseif (isset($_POST['action'])) {
    $action = $_POST['action'];
}

// Funcion helper para respuestas JSON
function jsonResponse($data, $httpCode = 200) {
    if (!headers_sent()) {
        header('Content-Type: application/json; charset=utf-8');
        if ($httpCode !== 200) http_response_code($httpCode);
    }
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    exit;
}

// ---- Test conexion ----
if ($action === 'test') {
    $tmpFile = tempnam(sys_get_temp_dir(), 'wasabi_test_');
    file_put_contents($tmpFile, 'backup_test_' . date('Y-m-d H:i:s'));
    $putResult = wasabi_put('_test_backup_connection.txt', $tmpFile, 'text/plain', 1);
    @unlink($tmpFile);

    if ($putResult['ok']) {
        jsonResponse(['status' => 'ok', 'mensaje' => 'Conexion a ' . WASABI_BUCKET . ' exitosa (region: ' . WASABI_REGION . ')']);
    } else {
        jsonResponse(['status' => 'error', 'error' => 'Fallo: HTTP ' . $putResult['status'] . ' ' . $putResult['error']]);
    }
}

// ---- Escanear wikis ----
if ($action === 'scan') {
    $wikis = scanWikis();
    jsonResponse(['status' => 'ok', 'total' => count($wikis), 'wikis' => $wikis]);
}

// ---- Listar archivos de UN wiki (con pre-filtrado via manifest o Wasabi LIST) ----
if ($action === 'list_files') {
    @set_time_limit(MAX_REQUEST_TIME);

    $wikiName = isset($_POST['wiki']) ? trim($_POST['wiki']) : (isset($_GET['wiki']) ? trim($_GET['wiki']) : '');
    $forceResync = !empty($_POST['forceResync']);

    if (!$wikiName || !isValidWikiName($wikiName)) {
        jsonResponse(['status' => 'error', 'error' => 'Nombre de wiki no valido'], 400);
    }

    $wikiPath = SEARCH_PATH . '/' . $wikiName;
    if (!isDokuWiki($wikiPath)) {
        jsonResponse(['status' => 'error', 'error' => 'No es un wiki DokuWiki']);
    }

    // Obtener archivos locales (con size + mtime)
    $localFiles = getBackupFiles($wikiPath);
    $totalSize = 0;
    foreach ($localFiles as $f) {
        $totalSize += $f['size'];
    }

    // Pre-filtrar: usar manifest (rapido) o Wasabi LIST (lento)
    $needUpload = [];
    $unchangedKeys = [];
    $skippedCount = 0;
    $source = 'wasabi'; // 'manifest' o 'wasabi'

    $manifest = $forceResync ? null : loadManifest($wikiName);

    if ($manifest !== null) {
        // MANIFEST MODE: comparar size + mtime (sin consultar Wasabi)
        $source = 'manifest';
        foreach ($localFiles as $f) {
            $key = $wikiName . '/' . $f['relative'];
            if (isset($manifest[$f['relative']])
                && $manifest[$f['relative']]['s'] === $f['size']
                && $manifest[$f['relative']]['m'] === $f['mtime']) {
                $skippedCount++;
                $unchangedKeys[] = $key;
            } else {
                $needUpload[] = $f['relative'];
            }
        }
    } else {
        // WASABI MODE: comparar solo size (requiere network)
        $prefix = $wikiName . '/';
        $existingKeys = wasabi_list_with_sizes($prefix);
        foreach ($localFiles as $f) {
            $key = $wikiName . '/' . $f['relative'];
            if (isset($existingKeys[$key]) && $existingKeys[$key] === $f['size']) {
                $skippedCount++;
                $unchangedKeys[] = $key;
            } else {
                $needUpload[] = $f['relative'];
            }
        }
    }

    jsonResponse([
        'status'        => 'ok',
        'wiki'          => $wikiName,
        'files'         => $needUpload,
        'unchangedKeys' => $unchangedKeys,
        'count'         => count($localFiles),
        'needUpload'    => count($needUpload),
        'skipped'       => $skippedCount,
        'totalSize'     => $totalSize,
        'source'        => $source,
    ]);
}

// ---- Guardar manifest despues de backup exitoso ----
if ($action === 'save_manifest') {
    $wikiName = isset($_POST['wiki']) ? trim($_POST['wiki']) : '';
    if (!$wikiName || !isValidWikiName($wikiName)) {
        jsonResponse(['status' => 'error', 'error' => 'Wiki no valido'], 400);
    }
    $result = saveManifest($wikiName);
    jsonResponse(['status' => $result ? 'ok' : 'error']);
}

// ---- Borrar todos los manifests (forzar resync) ----
if ($action === 'clear_manifests') {
    deleteAllManifests();
    jsonResponse(['status' => 'ok']);
}

// ---- Upload batch de archivos ----
if ($action === 'upload_batch') {
    @set_time_limit(MAX_REQUEST_TIME);

    $wikiName = isset($_POST['wiki']) ? trim($_POST['wiki']) : '';
    $filesJson = isset($_POST['files']) ? $_POST['files'] : '';

    if (!$wikiName || !isValidWikiName($wikiName)) {
        jsonResponse(['status' => 'error', 'error' => 'Nombre de wiki no valido'], 400);
    }

    $files = json_decode($filesJson, true);
    if (!is_array($files) || empty($files)) {
        jsonResponse(['status' => 'error', 'error' => 'Lista de archivos invalida'], 400);
    }

    // Limitar a BATCH_SIZE
    if (count($files) > BATCH_SIZE) {
        $files = array_slice($files, 0, BATCH_SIZE);
    }

    // Per-wiki lock (permite wikis en paralelo)
    $lock = acquireWikiLock($wikiName);
    if (!$lock) {
        jsonResponse(['status' => 'error', 'error' => "Wiki {$wikiName} ya en proceso. Reintentando..."]);
    }

    $startTime = microtime(true);
    $result = uploadBatch($wikiName, $files);
    $elapsed = round(microtime(true) - $startTime, 1);

    touchWikiLock($lock);
    releaseWikiLock($lock);

    _log("BATCH {$wikiName}: {$result['uploaded']} OK, {$result['skipped']} skip, {$result['errors']} err, {$elapsed}s (" . count($files) . " archivos)");

    jsonResponse([
        'status'       => $result['ok'] ? 'ok' : 'partial',
        'wiki'         => $wikiName,
        'uploaded'     => $result['uploaded'],
        'skipped'      => $result['skipped'],
        'errors'       => $result['errors'],
        'errorDetails' => $result['errorDetails'],
        'keys'         => $result['keys'],
        'bytesSaved'   => $result['bytesSaved'],
        'time'         => $elapsed,
        'batchSize'    => count($files),
    ]);
}

// ---- Cleanup: borrar obsoletos de Wasabi ----
if ($action === 'cleanup') {
    @set_time_limit(MAX_REQUEST_TIME);

    $wikiName = isset($_POST['wiki']) ? trim($_POST['wiki']) : '';
    $keysJson = isset($_POST['keys']) ? $_POST['keys'] : '';

    if (!$wikiName || !isValidWikiName($wikiName)) {
        jsonResponse(['status' => 'error', 'error' => 'Nombre de wiki no valido'], 400);
    }

    $uploadedKeys = json_decode($keysJson, true);
    if (!is_array($uploadedKeys)) {
        jsonResponse(['status' => 'error', 'error' => 'Lista de keys invalida'], 400);
    }

    // Solo limpiar si se subio al menos un archivo
    if (empty($uploadedKeys)) {
        jsonResponse(['status' => 'ok', 'wiki' => $wikiName, 'deleted' => 0, 'obsolete' => 0]);
    }

    $lock = acquireWikiLock($wikiName);
    if (!$lock) {
        jsonResponse(['status' => 'error', 'error' => "Wiki {$wikiName} bloqueado"]);
    }

    $result = cleanupWiki($wikiName, $uploadedKeys);

    releaseWikiLock($lock);

    _log("CLEANUP {$wikiName}: {$result['deleted']} borrados, {$result['obsolete']} obsoletos, {$result['errors']} errores");

    jsonResponse([
        'status'   => $result['ok'] ? 'ok' : 'error',
        'wiki'     => $wikiName,
        'deleted'  => $result['deleted'],
        'obsolete' => $result['obsolete'],
        'errors'   => $result['errors'],
    ]);
}

// ============================================================
// Si no hay accion AJAX, mostrar la UI HTML
// ============================================================

// Leer ultimo progreso del cron (si existe)
$lastCron = null;
if (file_exists(PROGRESS_FILE)) {
    $lastCron = @json_decode(@file_get_contents(PROGRESS_FILE), true);
}

?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Backup a Wasabi - SIE Deployer</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', sans-serif; background: #0a0e27; color: #e0e0e0; min-height: 100vh; padding: 20px; }
        h1 { color: #00e5ff; margin-bottom: 5px; font-size: 24px; }
        h2 { color: #ffd740; margin: 20px 0 10px; font-size: 16px; }
        .subtitle { color: #90a4ae; margin-bottom: 20px; font-size: 14px; }
        .card { background: #1a1e3a; border-radius: 12px; padding: 20px; margin-bottom: 20px; border: 1px solid #2a2e4a; }
        .info-bar { background: #1a237e; padding: 12px 18px; border-radius: 8px; color: #82b1ff; margin-bottom: 15px; font-family: monospace; font-size: 13px; }
        .btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: bold; margin-right: 8px; margin-bottom: 8px; transition: all 0.2s; display: inline-flex; align-items: center; gap: 6px; }
        .btn-primary { background: #1565c0; color: white; }
        .btn-primary:hover { background: #1976d2; }
        .btn-success { background: #2e7d32; color: white; }
        .btn-success:hover { background: #388e3c; }
        .btn-danger { background: #c62828; color: white; }
        .btn-danger:hover { background: #d32f2f; }
        .btn-warning { background: #e65100; color: white; }
        .btn-warning:hover { background: #f57c00; }
        .btn:disabled { opacity: 0.4; cursor: not-allowed; }

        .progress-bar { width: 100%; height: 30px; background: #0d1117; border-radius: 15px; overflow: hidden; margin: 12px 0; border: 1px solid #2a2e4a; }
        .progress-fill { height: 100%; background: linear-gradient(90deg, #00c853, #00e676); transition: width 0.3s; display: flex; align-items: center; justify-content: center; color: #000; font-weight: bold; font-size: 12px; min-width: 40px; }
        .progress-fill.error { background: linear-gradient(90deg, #c62828, #e53935); color: white; }

        .sub-progress { width: 100%; height: 8px; background: #0d1117; border-radius: 4px; overflow: hidden; margin: 6px 0; }
        .sub-progress-fill { height: 100%; background: linear-gradient(90deg, #00e5ff, #40c4ff); transition: width 0.3s; border-radius: 4px; }

        .stats { display: flex; gap: 12px; flex-wrap: wrap; margin: 15px 0; }
        .stat { background: #0d1117; border: 1px solid #2a2e4a; border-radius: 10px; padding: 12px 16px; text-align: center; min-width: 100px; flex: 1; }
        .stat .num { font-size: 22px; font-weight: bold; }
        .stat .lbl { font-size: 11px; color: #78909c; margin-top: 4px; }
        .c-blue { color: #00e5ff; }
        .c-green { color: #4caf50; }
        .c-red { color: #f44336; }
        .c-orange { color: #ff9800; }
        .c-purple { color: #ce93d8; }
        .c-cyan { color: #00e5ff; }

        #log { background: #0d1117; border: 1px solid #21262d; border-radius: 8px; padding: 12px; max-height: 500px; overflow-y: auto; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; line-height: 1.6; }
        .log-line { padding: 1px 0; }

        .timer { color: #ffd740; font-size: 16px; font-weight: bold; margin-left: 12px; vertical-align: middle; }
        .current-wiki { color: #00e5ff; font-size: 13px; margin-top: 8px; font-family: monospace; }

        .cron-info { background: #1a237e; border: 1px solid #283593; border-radius: 8px; padding: 12px 16px; margin-top: 12px; font-size: 12px; color: #90caf9; }
        .cron-info strong { color: #ffd740; }

        .error-box { background: #b71c1c; border: 1px solid #e53935; border-radius: 8px; padding: 15px; color: white; margin: 10px 0; }

        @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
        .pulsing { animation: pulse 1.5s infinite; }
    </style>
</head>
<body>

<h1>Backup a Wasabi S3</h1>
<p class="subtitle">Copia de seguridad de public_html/* &rarr; <?php echo htmlspecialchars(WASABI_BUCKET); ?> (excluye data/media/, data/cache/, data/tmp/, data/locks/, data/index/)</p>

<div class="info-bar">
    Origen: <?php echo htmlspecialchars(SEARCH_PATH); ?> &nbsp;|&nbsp;
    Bucket: <?php echo htmlspecialchars(WASABI_BUCKET); ?> &nbsp;|&nbsp;
    Region: <?php echo htmlspecialchars(WASABI_REGION); ?> &nbsp;|&nbsp;
    Batch: <?php echo BATCH_SIZE; ?> archivos/request &nbsp;|&nbsp;
    Lock timeout: <?php echo LOCK_TIMEOUT; ?>s
</div>

<?php if ($lastCron): ?>
<div class="cron-info">
    <strong>Ultimo cron:</strong>
    <?php echo htmlspecialchars($lastCron['last_run'] ?? '?'); ?> &mdash;
    <?php echo (int)($lastCron['wikis'] ?? 0); ?> wikis,
    <?php echo (int)($lastCron['files_uploaded'] ?? 0); ?> archivos,
    <?php echo (int)($lastCron['errors'] ?? 0); ?> errores,
    <?php echo htmlspecialchars($lastCron['elapsed'] ?? '?'); ?>s
</div>
<?php endif; ?>

<div class="card">
    <h2>Acciones</h2>
    <button class="btn btn-primary" onclick="doTest()" id="btn-test">Probar Conexion</button>
    <button class="btn btn-primary" onclick="doScan()" id="btn-scan">Escanear Wikis</button>
    <button class="btn btn-success" onclick="doBackup(false)" id="btn-backup" disabled>Iniciar Backup</button>
    <button class="btn btn-warning" onclick="doBackup(true)" id="btn-resync" disabled title="Ignora manifest local, consulta Wasabi">Forzar Resync</button>
    <button class="btn btn-danger" onclick="doCancel()" id="btn-cancel" style="display:none">Cancelar</button>
    <span class="timer" id="timer"></span>
    <div class="current-wiki" id="current-wiki"></div>
    <div id="resume-bar" style="display:none;margin-top:10px;padding:10px;background:#1a237e;border-radius:8px;color:#90caf9;font-size:13px">
        <span id="resume-text"></span>
        <button class="btn btn-success" onclick="doResume()" style="padding:5px 12px;font-size:12px;margin-left:10px">Reanudar</button>
        <button class="btn btn-danger" onclick="clearCheckpoint()" style="padding:5px 12px;font-size:12px;margin-left:4px">Descartar</button>
    </div>
</div>

<div class="card">
    <h2>Progreso General (wikis)</h2>
    <div class="progress-bar"><div class="progress-fill" id="pbar" style="width:0%">0%</div></div>
    <h2>Progreso Wiki Actual (archivos)</h2>
    <div class="sub-progress"><div class="sub-progress-fill" id="sub-pbar" style="width:0%"></div></div>
    <div id="sub-info" style="font-size:11px;color:#78909c;margin-top:4px"></div>
    <div class="stats">
        <div class="stat"><div class="num c-blue" id="s-total">0</div><div class="lbl">Wikis</div></div>
        <div class="stat"><div class="num c-green" id="s-ok">0</div><div class="lbl">Completados</div></div>
        <div class="stat"><div class="num c-purple" id="s-files">0</div><div class="lbl">Subidos</div></div>
        <div class="stat"><div class="num c-cyan" id="s-skipped">0</div><div class="lbl">Sin cambios</div></div>
        <div class="stat"><div class="num c-green" id="s-saved">0</div><div class="lbl">Ahorro gzip</div></div>
        <div class="stat"><div class="num c-orange" id="s-deleted">0</div><div class="lbl">Borrados</div></div>
        <div class="stat"><div class="num c-red" id="s-err">0</div><div class="lbl">Errores</div></div>
    </div>
</div>

<div class="card">
    <h2>Log</h2>
    <button class="btn btn-warning" onclick="clearLog()" style="padding:5px 12px;font-size:11px;margin-bottom:8px">Limpiar Log</button>
    <div id="log"></div>
</div>

<script>
(function() {
    'use strict';

    // ============================================================
    // STATE
    // ============================================================
    var wikis = [];
    var cancelled = false;
    var running = false;
    var timerInterval = null;
    var startTime = 0;
    var forceResync = false;
    var BATCH_SIZE = <?php echo BATCH_SIZE; ?>;
    var CONCURRENCY = <?php echo defined('WIKI_CONCURRENCY') ? WIKI_CONCURRENCY : 3; ?>;
    var counters = { ok: 0, files: 0, skipped: 0, saved: 0, deleted: 0, err: 0 };
    var completedWikiNames = [];
    var activeWikis = [];
    var CHECKPOINT_KEY = 'backup_wasabi_checkpoint';

    // ============================================================
    // UI HELPERS
    // ============================================================
    function log(msg, color) {
        var el = document.getElementById('log');
        var line = document.createElement('div');
        line.className = 'log-line';
        line.style.color = color || '#e0e0e0';
        var ts = new Date().toLocaleTimeString();
        line.textContent = '[' + ts + '] ' + msg;
        el.appendChild(line);
        el.scrollTop = el.scrollHeight;
    }

    window.clearLog = function() {
        document.getElementById('log').innerHTML = '';
    };

    function updateActiveWikis() {
        var text = activeWikis.length > 0 ? 'Procesando: ' + activeWikis.join(', ') : '';
        document.getElementById('current-wiki').textContent = text;
    }

    function updateStats() {
        document.getElementById('s-ok').textContent = counters.ok;
        document.getElementById('s-files').textContent = counters.files;
        document.getElementById('s-skipped').textContent = counters.skipped;
        document.getElementById('s-saved').textContent = formatBytes(counters.saved);
        document.getElementById('s-deleted').textContent = counters.deleted;
        document.getElementById('s-err').textContent = counters.err;
    }

    function updateMainProgress(current, total) {
        var pct = total > 0 ? Math.round((current / total) * 100) : 0;
        var bar = document.getElementById('pbar');
        bar.style.width = pct + '%';
        bar.textContent = pct + '% (' + current + '/' + total + ')';
        document.getElementById('s-total').textContent = total;
        updateStats();
    }

    function updateSubProgress(current, total, label) {
        var pct = total > 0 ? Math.round((current / total) * 100) : 0;
        document.getElementById('sub-pbar').style.width = pct + '%';
        document.getElementById('sub-info').textContent = label || (current + '/' + total);
    }

    function resetSubProgress() {
        document.getElementById('sub-pbar').style.width = '0%';
        document.getElementById('sub-info').textContent = '';
    }

    function startTimer() {
        startTime = Date.now();
        timerInterval = setInterval(function() {
            var elapsed = Math.floor((Date.now() - startTime) / 1000);
            var min = Math.floor(elapsed / 60);
            var sec = elapsed % 60;
            document.getElementById('timer').textContent = min + ':' + (sec < 10 ? '0' : '') + sec;
        }, 1000);
    }

    function stopTimer() {
        if (timerInterval) clearInterval(timerInterval);
    }

    function setButtonsRunning(isRunning) {
        document.getElementById('btn-test').disabled = isRunning;
        document.getElementById('btn-scan').disabled = isRunning;
        document.getElementById('btn-backup').disabled = isRunning;
        document.getElementById('btn-resync').disabled = isRunning;
        document.getElementById('btn-cancel').style.display = isRunning ? '' : 'none';
    }

    function formatBytes(bytes) {
        if (bytes === 0) return '0 B';
        var k = 1024;
        var sizes = ['B', 'KB', 'MB', 'GB'];
        var i = Math.floor(Math.log(bytes) / Math.log(k));
        return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i];
    }

    // ============================================================
    // CHECKPOINT / RESUME
    // ============================================================
    function saveCheckpoint() {
        try {
            localStorage.setItem(CHECKPOINT_KEY, JSON.stringify({
                ts: Date.now(),
                completed: completedWikiNames,
                counters: counters,
                totalWikis: wikis.length,
            }));
        } catch(e) {}
    }

    function loadCheckpoint() {
        try {
            var raw = localStorage.getItem(CHECKPOINT_KEY);
            if (!raw) return null;
            var data = JSON.parse(raw);
            // Expirar despues de 24h
            if (Date.now() - data.ts > 86400000) {
                localStorage.removeItem(CHECKPOINT_KEY);
                return null;
            }
            return data;
        } catch(e) { return null; }
    }

    window.clearCheckpoint = function() {
        localStorage.removeItem(CHECKPOINT_KEY);
        document.getElementById('resume-bar').style.display = 'none';
        log('Checkpoint descartado', '#ff9800');
    };

    // ============================================================
    // AJAX HELPER
    // ============================================================
    function ajax(action, data) {
        var fd = new FormData();
        fd.append('action', action);
        if (data) {
            for (var key in data) {
                if (data.hasOwnProperty(key)) fd.append(key, data[key]);
            }
        }
        return fetch('?action=' + encodeURIComponent(action), {
            method: 'POST',
            body: fd,
        }).then(function(resp) {
            if (!resp.ok) {
                return resp.text().then(function(txt) {
                    var parsed;
                    try { parsed = JSON.parse(txt); } catch(e) { parsed = null; }
                    if (parsed && parsed.error) throw new Error(parsed.error);
                    throw new Error('HTTP ' + resp.status + ': ' + txt.substring(0, 200));
                });
            }
            return resp.json();
        });
    }

    // ============================================================
    // PROCESS ONE WIKI (usado por el pool de concurrencia)
    // ============================================================
    async function processOneWiki(wiki, wikiNum, totalWikis) {
        var label = '[' + wikiNum + '/' + totalWikis + '] ' + wiki.name;

        activeWikis.push(wiki.name);
        updateActiveWikis();

        log(label + ' - listando archivos...', '#42a5f5');

        // PASO 1: Obtener lista de archivos (pre-filtrada)
        var fileList;
        try {
            var listData = { wiki: wiki.name };
            if (forceResync) listData.forceResync = '1';
            fileList = await ajax('list_files', listData);
        } catch(e) {
            counters.err++;
            log(label + ' ERROR listando: ' + e.message, '#f44336');
            activeWikis = activeWikis.filter(function(n){ return n !== wiki.name; });
            updateActiveWikis();
            return;
        }

        if (fileList.status !== 'ok') {
            counters.err++;
            log(label + ' ERROR: ' + (fileList.error || 'desconocido'), '#f44336');
            activeWikis = activeWikis.filter(function(n){ return n !== wiki.name; });
            updateActiveWikis();
            return;
        }

        var files = fileList.files;
        var unchangedKeys = fileList.unchangedKeys || [];
        var needUpload = fileList.needUpload;
        var preSkipped = fileList.skipped;
        var source = fileList.source || 'wasabi';

        log(label + ' - ' + fileList.count + ' archivos (' + formatBytes(fileList.totalSize) + '), ' + preSkipped + ' sin cambios, ' + needUpload + ' a subir [' + source + ']', '#e0e0e0');

        if (needUpload === 0) {
            counters.ok++;
            counters.skipped += preSkipped;
            log(label + ' - todo al dia', '#4caf50');
            // Guardar manifest
            try { await ajax('save_manifest', { wiki: wiki.name }); } catch(e) {}
            completedWikiNames.push(wiki.name);
            saveCheckpoint();
            activeWikis = activeWikis.filter(function(n){ return n !== wiki.name; });
            updateActiveWikis();
            return;
        }

        // PASO 2: Dividir en batches y subir
        var batches = [];
        for (var b = 0; b < files.length; b += BATCH_SIZE) {
            batches.push(files.slice(b, b + BATCH_SIZE));
        }

        var wikiUploaded = 0;
        var wikiSkipped = preSkipped;
        var wikiErrors = 0;
        var wikiSaved = 0;
        var allUploadedKeys = unchangedKeys.slice();
        var wikiCancelled = false;

        for (var bi = 0; bi < batches.length; bi++) {
            if (cancelled) { wikiCancelled = true; break; }

            var batch = batches[bi];
            var batchNum = (bi + 1) + '/' + batches.length;

            try {
                var batchResult = await ajax('upload_batch', {
                    wiki: wiki.name,
                    files: JSON.stringify(batch),
                });

                if (batchResult.status === 'ok' || batchResult.status === 'partial') {
                    wikiUploaded += batchResult.uploaded || 0;
                    wikiErrors += batchResult.errors || 0;
                    wikiSaved += batchResult.bytesSaved || 0;

                    if (batchResult.keys && batchResult.keys.length > 0) {
                        allUploadedKeys = allUploadedKeys.concat(batchResult.keys);
                    }

                    if (batchResult.errors > 0) {
                        log(label + ' batch ' + batchNum + ': ' + batchResult.uploaded + ' OK, ' + batchResult.errors + ' err (' + batchResult.time + 's)', '#ff9800');
                        if (batchResult.errorDetails) {
                            for (var ei = 0; ei < Math.min(batchResult.errorDetails.length, 3); ei++) {
                                log('  -> ' + batchResult.errorDetails[ei], '#ef5350');
                            }
                        }
                    }
                } else {
                    wikiErrors += batch.length;
                    log(label + ' batch ' + batchNum + ' ERROR: ' + (batchResult.error || '?'), '#f44336');
                }
            } catch(e) {
                wikiErrors += batch.length;
                log(label + ' batch ' + batchNum + ' FALLO: ' + e.message, '#f44336');
            }
        }

        counters.files += wikiUploaded;
        counters.skipped += wikiSkipped;
        counters.saved += wikiSaved;
        counters.err += wikiErrors;

        // PASO 3: Cleanup (borrar obsoletos)
        if (!wikiCancelled && wikiErrors === 0 && allUploadedKeys.length > 0) {
            try {
                var cleanResult = await ajax('cleanup', {
                    wiki: wiki.name,
                    keys: JSON.stringify(allUploadedKeys),
                });
                if (cleanResult.status === 'ok' && cleanResult.deleted > 0) {
                    counters.deleted += cleanResult.deleted;
                    log(label + ' cleanup: ' + cleanResult.deleted + ' obsoletos borrados', '#ff9800');
                }
            } catch(e) {
                log(label + ' cleanup error: ' + e.message, '#ef5350');
            }
        }

        // PASO 4: Guardar manifest
        if (!wikiCancelled && wikiErrors === 0) {
            try { await ajax('save_manifest', { wiki: wiki.name }); } catch(e) {}
        }

        if (!wikiCancelled) {
            counters.ok++;
            var statusColor = wikiErrors > 0 ? '#ff9800' : '#4caf50';
            var msg = wikiUploaded + ' subidos, ' + wikiSkipped + ' sin cambios';
            if (wikiSaved > 0) msg += ', ' + formatBytes(wikiSaved) + ' ahorrado gzip';
            if (wikiErrors > 0) msg += ', ' + wikiErrors + ' errores';
            log(label + ' LISTO: ' + msg, statusColor);
            completedWikiNames.push(wiki.name);
            saveCheckpoint();
        }

        activeWikis = activeWikis.filter(function(n){ return n !== wiki.name; });
        updateActiveWikis();
    }

    // ============================================================
    // CONCURRENCY POOL - procesa N wikis en paralelo
    // ============================================================
    async function runPool(wikiList, totalWikis, startIdx) {
        var queue = wikiList.slice();
        var idx = startIdx;

        async function worker() {
            while (queue.length > 0 && !cancelled) {
                var wiki = queue.shift();
                idx++;
                await processOneWiki(wiki, idx, totalWikis);
                updateMainProgress(counters.ok + counters.err, totalWikis);
            }
        }

        var workers = [];
        var numWorkers = Math.min(CONCURRENCY, queue.length);
        for (var w = 0; w < numWorkers; w++) {
            workers.push(worker());
        }
        await Promise.all(workers);
    }

    // ============================================================
    // ACTIONS
    // ============================================================

    window.doTest = function() {
        log('Probando conexion a Wasabi...', '#42a5f5');
        document.getElementById('btn-test').disabled = true;
        ajax('test').then(function(data) {
            if (data.status === 'ok') {
                log('OK: ' + data.mensaje, '#4caf50');
            } else {
                log('ERROR: ' + (data.error || 'desconocido'), '#f44336');
            }
        }).catch(function(e) {
            log('Error de conexion: ' + e.message, '#f44336');
        }).finally(function() {
            document.getElementById('btn-test').disabled = false;
        });
    };

    window.doScan = function() {
        log('Escaneando wikis en el servidor...', '#42a5f5');
        document.getElementById('btn-scan').disabled = true;
        ajax('scan').then(function(data) {
            wikis = data.wikis || [];
            log('Encontrados: ' + wikis.length + ' wikis DokuWiki', '#4caf50');
            if (wikis.length > 0) {
                var names = wikis.map(function(w) { return w.name; }).join(', ');
                log('Wikis: ' + names, '#90a4ae');
            }
            updateMainProgress(0, wikis.length);
            document.getElementById('btn-backup').disabled = (wikis.length === 0);
            document.getElementById('btn-resync').disabled = (wikis.length === 0);

            // Verificar checkpoint
            var cp = loadCheckpoint();
            if (cp && cp.totalWikis === wikis.length && cp.completed.length > 0 && cp.completed.length < wikis.length) {
                document.getElementById('resume-bar').style.display = '';
                document.getElementById('resume-text').textContent =
                    'Backup interrumpido: ' + cp.completed.length + '/' + cp.totalWikis + ' wikis completados. Reanudar?';
            }
        }).catch(function(e) {
            log('Error escaneando: ' + e.message, '#f44336');
        }).finally(function() {
            document.getElementById('btn-scan').disabled = false;
        });
    };

    window.doCancel = function() {
        if (cancelled) return;
        cancelled = true;
        log('Cancelacion solicitada... esperando fin de batches activos', '#ff9800');
        document.getElementById('btn-cancel').disabled = true;
    };

    window.doResume = function() {
        var cp = loadCheckpoint();
        if (!cp) return;
        forceResync = false; // Resume siempre usa manifest, no resync
        document.getElementById('resume-bar').style.display = 'none';
        startBackup(cp.completed, cp.counters);
    };

    window.doBackup = function(isResync) {
        forceResync = !!isResync;
        document.getElementById('resume-bar').style.display = 'none';
        localStorage.removeItem(CHECKPOINT_KEY);
        startBackup([], null);
    };

    async function startBackup(skipWikis, savedCounters) {
        if (wikis.length === 0) {
            log('Primero escanee los wikis', '#f44336');
            return;
        }
        if (running) return;

        running = true;
        cancelled = false;
        completedWikiNames = skipWikis.slice();

        if (savedCounters) {
            counters = JSON.parse(JSON.stringify(savedCounters)); // Clonar para no mutar checkpoint
        } else {
            counters = { ok: 0, files: 0, skipped: 0, saved: 0, deleted: 0, err: 0 };
        }

        setButtonsRunning(true);
        startTimer();

        // Filtrar wikis ya completados (resume)
        var pendingWikis = wikis.filter(function(w) {
            return skipWikis.indexOf(w.name) === -1;
        });
        var startIdx = wikis.length - pendingWikis.length;

        updateMainProgress(startIdx, wikis.length);

        if (skipWikis.length > 0) {
            log('=== REANUDANDO BACKUP: ' + pendingWikis.length + ' wikis pendientes (de ' + wikis.length + '), ' + CONCURRENCY + ' en paralelo ===', '#00e5ff');
        } else {
            var label = forceResync ? 'RESYNC COMPLETO' : 'BACKUP';
            log('=== INICIO ' + label + ': ' + wikis.length + ' wikis, ' + CONCURRENCY + ' en paralelo ===', '#00e5ff');
        }

        // Ejecutar pool de concurrencia
        await runPool(pendingWikis, wikis.length, startIdx);

        // FINALIZACION
        stopTimer();
        var elapsed = document.getElementById('timer').textContent;
        activeWikis = [];
        updateActiveWikis();
        resetSubProgress();
        localStorage.removeItem(CHECKPOINT_KEY);

        var summary = counters.ok + '/' + wikis.length + ' wikis, ' +
                      counters.files + ' subidos, ' + counters.skipped + ' sin cambios, ' +
                      formatBytes(counters.saved) + ' ahorrado gzip, ' +
                      counters.deleted + ' borrados, ' +
                      counters.err + ' errores';
        var summaryColor = counters.err > 0 ? '#ff9800' : '#4caf50';
        log('=== BACKUP FINALIZADO (' + elapsed + '): ' + summary + ' ===', summaryColor);

        if (counters.err > 0) {
            document.getElementById('pbar').className = 'progress-fill error';
        }

        running = false;
        setButtonsRunning(false);
        document.getElementById('btn-backup').disabled = false;
        document.getElementById('btn-resync').disabled = false;
        updateMainProgress(counters.ok, wikis.length);
    }

})();
</script>

</body>
</html>
