<?php
/**
 * MIGRACION MASIVA WASABI S3 - SUBDOMINIOS SIE
 * ===============================================
 * Parche masivo que automatiza: carga, verificacion, registro DB, activacion
 * y truncamiento de archivos de data/media/ hacia Wasabi S3.
 *
 * 260+ GB / 230+ subdominios — diseñado para ser resumible y seguro.
 *
 * FASES (estrictamente secuenciales):
 *   1. INVENTARIO  - Escanear subdominios, contar archivos/tamaños
 *   2. UPLOAD      - Subir archivos a Wasabi (streaming, resumible)
 *   3. VERIFICAR   - HEAD check cada archivo, comparar tamaños
 *   4. REGISTRAR   - Insertar en SQLite de cada subdominio (wasabi_files)
 *   5. TRUNCAR     - Convertir a 0 bytes (solo si 100% verificado)
 *
 * MODOS WEB:
 *   ?fase=dashboard                 - Vista general (default)
 *   ?fase=inventario[&sub=X]        - Escanear archivos
 *   ?fase=upload[&sub=X][&lote=50]  - Subir archivos pendientes
 *   ?fase=verificar[&sub=X]         - Verificar integridad
 *   ?fase=registrar[&sub=X]         - Registrar en DBs locales
 *   ?fase=truncar[&sub=X]           - Truncar archivos verificados
 *   ?fase=reset                     - Reiniciar tracking DB
 *   &autopilot=1                    - Auto-continue entre lotes (sin clics)
 *
 * MODOS CLI (SSH):
 *   php migracion_masiva_wasabi.php upload_all [sub] [lote] [max_mb]
 *   php migracion_masiva_wasabi.php verificar_all [sub] [lote]
 *   php migracion_masiva_wasabi.php registrar_all [sub] [lote]
 *   php migracion_masiva_wasabi.php truncar_all [sub] [lote]
 *   php migracion_masiva_wasabi.php full [sub]    — 5 fases de corrido
 *   php migracion_masiva_wasabi.php stats         — estadisticas
 *
 *   Background: nohup php migracion_masiva_wasabi.php upload_all "" 500 > upload.log 2>&1 &
 *   Screen:     screen -S wasabi php migracion_masiva_wasabi.php upload_all "" 500
 *
 * SEGURIDAD: Solo lectura hasta fase 5. Logs completos. Resumible.
 */

error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('log_errors', '1');
ini_set('error_log', __DIR__ . '/migracion_wasabi_error.log');

@set_time_limit(0);
@ini_set('max_execution_time', 0);
@ini_set('memory_limit', '512M');

// =====================================================================
//  CONFIGURACION
// =====================================================================

$basePath       = '/home/systeng3/public_html';
$trackingDbPath = __DIR__ . '/migracion_wasabi_estado.sqlite3';
$logPath        = __DIR__ . '/migracion_wasabi.log';

$excluir = ['.', '..', 'cgi-bin', '.well-known', '.trash', '.htaccess',
            'respaldo', 'diagnosticos', 'tmp', 'error_log'];

// Extensiones de archivos a migrar (media files)
$extensionesMigrar = ['pdf','doc','docx','xls','xlsx','ppt','pptx','odt','ods',
                      'zip','rar','7z','gz','tar',
                      'jpg','jpeg','png','gif','bmp','svg','webp','ico',
                      'mp4','avi','mov','wmv','mp3','wav',
                      'txt','csv','xml','json','html','htm',
                      'dwg','dxf','ai','psd','eps','tif','tiff'];

// Limite de tamaño de archivo (en bytes) — archivos mayores se marcan como 'skipped'
// Se puede cambiar via ?max_size_mb=500 (default 500 MB)
$maxFileSizeMB = 500;

// Archivos/carpetas a excluir dentro de data/media/
$excluirMedia = ['.', '..', '.htaccess', 'index.html', 'wiki'];

// =====================================================================
//  PARAMETROS
// =====================================================================

$esCLI = (php_sapi_name() === 'cli');
if (!$esCLI) header('Content-Type: text/html; charset=UTF-8');

$fase      = $esCLI ? ($argv[1] ?? 'dashboard') : ($_POST['fase'] ?? $_GET['fase'] ?? 'dashboard');
$targetSub = $esCLI ? ($argv[2] ?? '')           : ($_POST['sub'] ?? $_GET['sub'] ?? '');
$lote      = $esCLI ? (int)($argv[3] ?? 100)     : (int)($_GET['lote'] ?? $_POST['lote'] ?? 100);

if (!empty($targetSub)) {
    if (preg_match('#^https?://#i', $targetSub)) {
        $parsed = parse_url($targetSub);
        if (isset($parsed['host'])) { $parts = explode('.', $parsed['host']); $targetSub = $parts[0]; }
    }
    $targetSub = basename(preg_replace('/[^a-zA-Z0-9._-]/', '', $targetSub));
}

$lote = max(1, min($lote, 5000));

// Limite de tamaño configurable
$maxFileSizeMB = $esCLI ? (int)($argv[4] ?? $maxFileSizeMB) : (int)($_GET['max_size_mb'] ?? $_POST['max_size_mb'] ?? $maxFileSizeMB);
$maxFileSizeMB = max(10, min($maxFileSizeMB, 5000)); // entre 10 MB y 5 GB
$maxFileSize   = $maxFileSizeMB * 1048576; // a bytes

// =====================================================================
//  CLIENTE S3 EMBEBIDO (AWS Signature V4)
// =====================================================================

class WasabiS3Client {
    private $key, $secret, $bucket, $region, $endpoint;

    public function __construct($key, $secret, $bucket = 'nicc-archivos', $region = 'us-east-1', $endpoint = 's3.wasabisys.com') {
        $this->key      = $key;
        $this->secret   = $secret;
        $this->bucket   = $bucket;
        $this->region   = $region;
        $this->endpoint = $endpoint;
    }

    /** Test de conexion: sube un archivo pequeño, verifica y lo borra */
    public function testConnection() {
        $testKey = '_migration_test_' . time() . '.txt';
        $tmpFile = tempnam(sys_get_temp_dir(), 'wasabi_test_');
        file_put_contents($tmpFile, 'test-' . date('Y-m-d H:i:s'));

        $upload = $this->putObject($tmpFile, $testKey, 'text/plain');
        @unlink($tmpFile);
        if (!$upload['ok']) return ['ok' => false, 'msg' => 'Upload test fallido: ' . $upload['error']];

        $head = $this->headObject($testKey);
        if (!$head['exists']) return ['ok' => false, 'msg' => 'HEAD test fallido: archivo no encontrado despues de subir'];

        $del = $this->deleteObject($testKey);
        if (!$del['ok']) return ['ok' => false, 'msg' => 'Delete test fallido: ' . $del['error']];

        return ['ok' => true, 'msg' => 'Conexion OK — upload, HEAD y delete exitosos'];
    }

    /** Upload streaming (no carga en RAM) */
    public function putObject($localPath, $key, $mimeType = 'application/octet-stream') {
        $fileSize = filesize($localPath);
        // Usar UNSIGNED-PAYLOAD para evitar leer archivo completo en RAM para SHA-256
        $sha256   = 'UNSIGNED-PAYLOAD';
        $host     = $this->bucket . '.' . $this->endpoint;
        $encodedKey = $this->encodeKeyForUrl($key);
        $url      = 'https://' . $host . '/' . $encodedKey;

        $now     = gmdate('Ymd\THis\Z');
        $dateStr = gmdate('Ymd');

        $headers = [
            'host'                 => $host,
            'x-amz-content-sha256' => $sha256,
            'x-amz-date'           => $now,
            'content-type'         => $mimeType,
            'content-length'       => (string)$fileSize,
        ];

        $auth = $this->signV4('PUT', '/' . $encodedKey, '', $headers, $sha256, $now, $dateStr);

        $curlHeaders = [];
        foreach ($headers as $k => $v) $curlHeaders[] = $k . ': ' . $v;
        $curlHeaders[] = 'Authorization: ' . $auth;

        $fp = fopen($localPath, 'r');
        if (!$fp) return ['ok' => false, 'error' => 'No se puede abrir archivo local', 'status' => 0];

        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL            => $url,
            CURLOPT_PUT            => true,
            CURLOPT_INFILE         => $fp,
            CURLOPT_INFILESIZE     => $fileSize,
            CURLOPT_HTTPHEADER     => $curlHeaders,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_TIMEOUT        => 1800, // 30 min para archivos grandes
            CURLOPT_CONNECTTIMEOUT => 30,
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error    = curl_error($ch);
        curl_close($ch);
        fclose($fp);

        if ($httpCode === 200) return ['ok' => true, 'error' => '', 'status' => 200];

        $s3Error = $this->parseS3Error($response);
        return ['ok' => false, 'error' => $s3Error ?: ($error ?: "HTTP {$httpCode}"), 'status' => $httpCode];
    }

    /** HEAD request: verificar existencia y tamaño */
    public function headObject($key) {
        $host = $this->bucket . '.' . $this->endpoint;
        $encodedKey = $this->encodeKeyForUrl($key);
        $url  = 'https://' . $host . '/' . $encodedKey;

        $now = gmdate('Ymd\THis\Z');
        $dateStr = gmdate('Ymd');
        $sha256  = hash('sha256', '');

        $headers = [
            'host'                 => $host,
            'x-amz-content-sha256' => $sha256,
            'x-amz-date'           => $now,
        ];

        $auth = $this->signV4('HEAD', '/' . $encodedKey, '', $headers, $sha256, $now, $dateStr);

        $curlHeaders = [];
        foreach ($headers as $k => $v) $curlHeaders[] = $k . ': ' . $v;
        $curlHeaders[] = 'Authorization: ' . $auth;

        $responseHeaders = [];
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL            => $url,
            CURLOPT_NOBODY         => true,
            CURLOPT_HTTPHEADER     => $curlHeaders,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_TIMEOUT        => 30,
            CURLOPT_HEADERFUNCTION => function($ch, $header) use (&$responseHeaders) {
                $parts = explode(':', $header, 2);
                if (count($parts) === 2) $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
                return strlen($header);
            },
        ]);

        curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error    = curl_error($ch);
        curl_close($ch);

        if ($httpCode === 200) {
            return ['exists' => true, 'size' => (int)($responseHeaders['content-length'] ?? 0), 'error' => ''];
        }
        return ['exists' => false, 'size' => 0, 'error' => $error ?: "HTTP {$httpCode}"];
    }

    /** DELETE object */
    public function deleteObject($key) {
        $host = $this->bucket . '.' . $this->endpoint;
        $encodedKey = $this->encodeKeyForUrl($key);
        $url  = 'https://' . $host . '/' . $encodedKey;

        $now = gmdate('Ymd\THis\Z');
        $dateStr = gmdate('Ymd');
        $sha256  = hash('sha256', '');

        $headers = [
            'host'                 => $host,
            'x-amz-content-sha256' => $sha256,
            'x-amz-date'           => $now,
        ];

        $auth = $this->signV4('DELETE', '/' . $encodedKey, '', $headers, $sha256, $now, $dateStr);

        $curlHeaders = [];
        foreach ($headers as $k => $v) $curlHeaders[] = $k . ': ' . $v;
        $curlHeaders[] = 'Authorization: ' . $auth;

        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL            => $url,
            CURLOPT_CUSTOMREQUEST  => 'DELETE',
            CURLOPT_HTTPHEADER     => $curlHeaders,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_TIMEOUT        => 30,
        ]);

        curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        return ['ok' => ($httpCode === 204 || $httpCode === 200), 'error' => '', 'status' => $httpCode];
    }

    // --- Firma AWS Signature V4 ---

    private function signV4($method, $uri, $queryString, $headers, $payloadHash, $now, $dateStr) {
        ksort($headers);
        $canonicalHeaders = '';
        $signedHeadersList = [];
        foreach ($headers as $k => $v) { $canonicalHeaders .= strtolower($k) . ':' . trim($v) . "\n"; $signedHeadersList[] = strtolower($k); }
        $signedHeaders = implode(';', $signedHeadersList);

        $canonicalRequest = $method . "\n" . $uri . "\n" . $queryString . "\n" . $canonicalHeaders . "\n" . $signedHeaders . "\n" . $payloadHash;

        $scope = $dateStr . '/' . $this->region . '/s3/aws4_request';
        $stringToSign = "AWS4-HMAC-SHA256\n" . $now . "\n" . $scope . "\n" . hash('sha256', $canonicalRequest);

        $signingKey = $this->getSigningKey($dateStr);
        $signature  = hash_hmac('sha256', $stringToSign, $signingKey);

        return "AWS4-HMAC-SHA256 Credential={$this->key}/{$scope}, SignedHeaders={$signedHeaders}, Signature={$signature}";
    }

    private function getSigningKey($dateStr) {
        $kDate    = hash_hmac('sha256', $dateStr, 'AWS4' . $this->secret, true);
        $kRegion  = hash_hmac('sha256', $this->region, $kDate, true);
        $kService = hash_hmac('sha256', 's3', $kRegion, true);
        return hash_hmac('sha256', 'aws4_request', $kService, true);
    }

    private function encodeKeyForUrl($key) {
        return implode('/', array_map(function($s) { return rawurlencode($s); }, explode('/', $key)));
    }

    private function parseS3Error($xml) {
        if (empty($xml)) return '';
        if (preg_match('/<Message>(.*?)<\/Message>/s', $xml, $m)) return $m[1];
        return '';
    }
}

// =====================================================================
//  TRACKING DATABASE (SQLite central)
// =====================================================================

function getTrackingDb($dbPath) {
    $isNew = !file_exists($dbPath);
    $pdo = new PDO('sqlite:' . $dbPath);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->exec('PRAGMA journal_mode = WAL');
    $pdo->exec('PRAGMA synchronous = NORMAL');

    if ($isNew) {
        $pdo->exec("
            CREATE TABLE IF NOT EXISTS migration_config (
                key TEXT PRIMARY KEY,
                value TEXT NOT NULL DEFAULT ''
            );
            CREATE TABLE IF NOT EXISTS subdomain_progress (
                subdomain TEXT PRIMARY KEY,
                total_files INTEGER DEFAULT 0,
                total_size INTEGER DEFAULT 0,
                uploaded INTEGER DEFAULT 0,
                verified INTEGER DEFAULT 0,
                registered INTEGER DEFAULT 0,
                truncated INTEGER DEFAULT 0,
                errors INTEGER DEFAULT 0,
                skipped INTEGER DEFAULT 0,
                last_phase TEXT DEFAULT '',
                last_update TEXT DEFAULT ''
            );
            CREATE TABLE IF NOT EXISTS file_tracking (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                subdomain TEXT NOT NULL,
                rel_path TEXT NOT NULL,
                media_id TEXT NOT NULL,
                wasabi_key TEXT NOT NULL,
                file_size INTEGER NOT NULL DEFAULT 0,
                file_mtime INTEGER NOT NULL DEFAULT 0,
                mime_type TEXT DEFAULT '',
                status TEXT DEFAULT 'pending',
                error_msg TEXT DEFAULT '',
                uploaded_at TEXT DEFAULT '',
                verified_at TEXT DEFAULT '',
                registered_at TEXT DEFAULT '',
                truncated_at TEXT DEFAULT '',
                UNIQUE(subdomain, rel_path)
            );
            CREATE INDEX IF NOT EXISTS idx_ft_subdomain ON file_tracking(subdomain);
            CREATE INDEX IF NOT EXISTS idx_ft_status ON file_tracking(status);
            CREATE INDEX IF NOT EXISTS idx_ft_sub_status ON file_tracking(subdomain, status);
        ");
    }

    return $pdo;
}

function getConfig($pdo, $key, $default = '') {
    $stmt = $pdo->prepare('SELECT value FROM migration_config WHERE key = ?');
    $stmt->execute([$key]);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    return $row ? $row['value'] : $default;
}

function setConfig($pdo, $key, $value) {
    $stmt = $pdo->prepare('INSERT OR REPLACE INTO migration_config (key, value) VALUES (?, ?)');
    $stmt->execute([$key, $value]);
}

// =====================================================================
//  FUNCIONES AUXILIARES
// =====================================================================

function logMsg($logPath, $msg) {
    $line = date('Y-m-d H:i:s') . ' ' . $msg . "\n";
    @file_put_contents($logPath, $line, FILE_APPEND | LOCK_EX);
}

function getMimeType($file) {
    $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
    $mimes = [
        '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',
        'zip'=>'application/zip','rar'=>'application/x-rar-compressed','7z'=>'application/x-7z-compressed',
        'jpg'=>'image/jpeg','jpeg'=>'image/jpeg','png'=>'image/png','gif'=>'image/gif','svg'=>'image/svg+xml','webp'=>'image/webp',
        'mp4'=>'video/mp4','mp3'=>'audio/mpeg','wav'=>'audio/wav',
        'txt'=>'text/plain','csv'=>'text/csv','xml'=>'application/xml','json'=>'application/json','html'=>'text/html',
    ];
    return $mimes[$ext] ?? 'application/octet-stream';
}

/** Convierte path de archivo a media_id de DokuWiki */
function pathToMediaId($relPath) {
    // data/media/compartidos/1/archivo.pdf → compartidos:1:archivo.pdf
    $clean = str_replace('\\', '/', $relPath);
    $clean = preg_replace('#^data/media/#', '', $clean);
    $clean = str_replace('/', ':', $clean);
    return $clean;
}

/** Convierte media_id a wasabi key: subdomain/namespace/file */
function mediaIdToWasabiKey($subdomain, $mediaId) {
    return $subdomain . '/' . str_replace(':', '/', $mediaId);
}

/** Escanea data/media/ recursivamente (solo archivos con contenido, filtrado por extension) */
function scanMediaFiles($mediaDir, $basePath = '', $extensiones = [], $excluirNombres = []) {
    $files = [];
    if (!is_dir($mediaDir)) return $files;

    // Crear lookup de extensiones permitidas
    $extFilter = !empty($extensiones) ? array_flip($extensiones) : [];

    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($mediaDir, RecursiveDirectoryIterator::SKIP_DOTS),
        RecursiveIteratorIterator::LEAVES_ONLY
    );

    foreach ($iterator as $file) {
        if (!$file->isFile()) continue;
        $size = $file->getSize();
        if ($size === 0) continue; // Skip stubs

        $fullPath = $file->getPathname();
        $basename = $file->getBasename();

        // Excluir archivos de sistema
        if ($basename === '.htaccess' || $basename === '_dummy') continue;
        if (!empty($excluirNombres) && in_array($basename, $excluirNombres)) continue;

        // Filtrar por extension si se proporcionaron
        if (!empty($extFilter)) {
            $ext = strtolower(pathinfo($basename, PATHINFO_EXTENSION));
            if (!isset($extFilter[$ext])) continue;
        }

        $relPath = str_replace('\\', '/', substr($fullPath, strlen($basePath) + 1));

        $files[] = [
            'rel_path'  => $relPath,
            'full_path' => $fullPath,
            'size'      => $size,
            'mtime'     => $file->getMTime(),
        ];
    }

    return $files;
}

/** Obtiene credenciales Wasabi desde POST o tracking DB */
function getWasabiCredentials($pdo) {
    // Primero intentar POST
    if (!empty($_POST['wasabi_key']) && !empty($_POST['wasabi_secret'])) {
        $creds = [
            'key'      => trim($_POST['wasabi_key']),
            'secret'   => trim($_POST['wasabi_secret']),
            'bucket'   => trim($_POST['wasabi_bucket'] ?: 'nicc-archivos'),
            'region'   => trim($_POST['wasabi_region'] ?: 'us-east-1'),
            'endpoint' => trim($_POST['wasabi_endpoint'] ?: 's3.wasabisys.com'),
        ];
        // Guardar en tracking DB
        foreach ($creds as $k => $v) setConfig($pdo, 'wasabi_' . $k, $v);
        return $creds;
    }

    // Leer de tracking DB
    $key = getConfig($pdo, 'wasabi_key');
    if (empty($key)) return null;

    return [
        'key'      => $key,
        'secret'   => getConfig($pdo, 'wasabi_secret'),
        'bucket'   => getConfig($pdo, 'wasabi_bucket', 'nicc-archivos'),
        'region'   => getConfig($pdo, 'wasabi_region', 'us-east-1'),
        'endpoint' => getConfig($pdo, 'wasabi_endpoint', 's3.wasabisys.com'),
    ];
}

function formatSize($bytes) {
    if ($bytes >= 1099511627776) return round($bytes / 1099511627776, 2) . ' TB';
    if ($bytes >= 1073741824) return round($bytes / 1073741824, 2) . ' GB';
    if ($bytes >= 1048576) return round($bytes / 1048576, 2) . ' MB';
    if ($bytes >= 1024) return round($bytes / 1024, 1) . ' KB';
    return $bytes . ' B';
}

// =====================================================================
//  FASE 1: INVENTARIO
// =====================================================================

function faseInventario($pdo, $basePath, $excluir, $targetSub, $logPath, $maxFileSize = 0, $extensiones = [], $excluirMedia = []) {
    $carpetas = obtenerCarpetas($basePath, $excluir, $targetSub);
    $totalArchivos = 0;
    $totalSize = 0;

    emitProgress("FASE 1: INVENTARIO — Escaneando " . count($carpetas) . " subdominios...\n");

    foreach ($carpetas as $i => $carpeta) {
        $subPath = $basePath . '/' . $carpeta;
        if (!file_exists($subPath . '/doku.php')) continue;

        $mediaDir = $subPath . '/data/media';
        if (!is_dir($mediaDir)) {
            emitProgress("[" . ($i+1) . "/" . count($carpetas) . "] {$carpeta} — sin data/media/\n");
            continue;
        }

        $files = scanMediaFiles($mediaDir, $subPath, $extensiones, $excluirMedia);
        $subSize = 0;

        // Insertar en tracking DB (upsert)
        $pdo->beginTransaction();
        try {
            // Primero verificar si ya existe con status avanzado
            $stmtCheck = $pdo->prepare('SELECT status FROM file_tracking WHERE subdomain = ? AND rel_path = ?');
            $stmtInsert = $pdo->prepare('INSERT OR REPLACE INTO file_tracking (subdomain, rel_path, media_id, wasabi_key, file_size, file_mtime, mime_type, status, error_msg) VALUES (?,?,?,?,?,?,?,?,?)');

            $skippedCount = 0;
            foreach ($files as $f) {
                $mediaId   = pathToMediaId($f['rel_path']);
                $wasabiKey = mediaIdToWasabiKey($carpeta, $mediaId);
                $mime      = getMimeType($f['rel_path']);

                // Detectar archivos que exceden el limite de tamaño
                $isOversized = ($maxFileSize > 0 && $f['size'] > $maxFileSize);
                $initialStatus = $isOversized ? 'skipped' : 'pending';
                $skipMsg = $isOversized ? 'Excede limite: ' . formatSize($f['size']) . ' > ' . formatSize($maxFileSize) : '';

                // Si ya existe con status avanzado, no sobreescribir
                $stmtCheck->execute([$carpeta, $f['rel_path']]);
                $existing = $stmtCheck->fetchColumn();
                if ($existing && in_array($existing, ['uploaded', 'verified', 'registered', 'truncated'])) {
                    $subSize += $f['size'];
                    continue; // No tocar archivos ya procesados
                }

                $stmtInsert->execute([$carpeta, $f['rel_path'], $mediaId, $wasabiKey, $f['size'], $f['mtime'], $mime, $initialStatus, $skipMsg]);
                $subSize += $f['size'];

                if ($isOversized) {
                    $skippedCount++;
                    emitProgress("  GRANDE: " . basename($f['rel_path']) . " (" . formatSize($f['size']) . ") — marcado como skipped\n");
                    logMsg($logPath, "INVENTARIO SKIP {$carpeta}/" . $f['rel_path'] . " — " . formatSize($f['size']));
                }
            }

            // Actualizar subdomain_progress
            $stmtP = $pdo->prepare('INSERT OR REPLACE INTO subdomain_progress (subdomain, total_files, total_size, skipped, last_phase, last_update) VALUES (?,?,?,?,?,?)');
            $stmtP->execute([$carpeta, count($files), $subSize, $skippedCount, 'inventario', date('Y-m-d H:i:s')]);

            $pdo->commit();
        } catch (\Exception $e) {
            $pdo->rollBack();
            emitProgress("  ERROR DB: " . $e->getMessage() . "\n");
            logMsg($logPath, "INVENTARIO ERROR {$carpeta}: " . $e->getMessage());
            continue;
        }

        $totalArchivos += count($files);
        $totalSize += $subSize;

        $skippedInfo = $skippedCount > 0 ? " ({$skippedCount} grandes omitidos)" : '';
        emitProgress("[" . ($i+1) . "/" . count($carpetas) . "] {$carpeta} — " . count($files) . " archivos, " . formatSize($subSize) . $skippedInfo . "\n");
    }

    // Contar omitidos globalmente
    $totalSkipped = $pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='skipped'")->fetchColumn();
    $totalSkippedSize = $pdo->query("SELECT COALESCE(SUM(file_size),0) FROM file_tracking WHERE status='skipped'")->fetchColumn();

    setConfig($pdo, 'inventario_completado', date('Y-m-d H:i:s'));
    setConfig($pdo, 'total_archivos', $totalArchivos);
    setConfig($pdo, 'total_size', $totalSize);
    setConfig($pdo, 'total_skipped', $totalSkipped);

    logMsg($logPath, "INVENTARIO COMPLETADO: {$totalArchivos} archivos, " . formatSize($totalSize) . ($totalSkipped > 0 ? " ({$totalSkipped} omitidos por tamaño, " . formatSize($totalSkippedSize) . ")" : ''));
    emitProgress("\n=== INVENTARIO COMPLETADO ===\n");
    emitProgress("Total: {$totalArchivos} archivos, " . formatSize($totalSize) . "\n");
    if ($totalSkipped > 0) {
        emitProgress("Omitidos por tamaño (>" . formatSize($maxFileSize) . "): {$totalSkipped} archivos, " . formatSize($totalSkippedSize) . "\n");
        emitProgress("Pendientes de upload: " . ($totalArchivos - $totalSkipped) . " archivos\n");
    }
}

// =====================================================================
//  FASE 2: UPLOAD
// =====================================================================

function faseUpload($pdo, $basePath, $targetSub, $lote, $logPath, $s3, $maxFileSize = 0) {
    $where = $targetSub ? " AND subdomain = " . $pdo->quote($targetSub) : '';

    // Contar pendientes
    $stmt = $pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status = 'pending'" . $where);
    $totalPendientes = (int)$stmt->fetchColumn();

    if ($totalPendientes === 0) {
        emitProgress("No hay archivos pendientes de upload.\n");
        return;
    }

    emitProgress("FASE 2: UPLOAD — {$totalPendientes} archivos pendientes (lote: {$lote})\n\n");

    $stmt = $pdo->prepare("SELECT id, subdomain, rel_path, wasabi_key, file_size, mime_type FROM file_tracking WHERE status = 'pending'" . $where . " ORDER BY subdomain, file_size ASC LIMIT ?");
    $stmt->execute([$lote]);
    $archivos = $stmt->fetchAll(PDO::FETCH_ASSOC);

    $exitos = 0; $errores = 0; $bytesSubidos = 0;
    $currentSub = '';
    $tiempoInicio = microtime(true);

    $stmtUpdate = $pdo->prepare("UPDATE file_tracking SET status = ?, uploaded_at = ?, error_msg = ? WHERE id = ?");
    $stmtProgress = $pdo->prepare("UPDATE subdomain_progress SET uploaded = (SELECT COUNT(*) FROM file_tracking WHERE subdomain = ? AND status IN ('uploaded','verified','registered','truncated')), errors = (SELECT COUNT(*) FROM file_tracking WHERE subdomain = ? AND status = 'error'), last_phase = 'upload', last_update = ? WHERE subdomain = ?");

    foreach ($archivos as $i => $f) {
        if ($f['subdomain'] !== $currentSub) {
            $currentSub = $f['subdomain'];
            emitProgress("\n--- {$currentSub} ---\n");
        }

        $localPath = $basePath . '/' . $f['subdomain'] . '/' . $f['rel_path'];

        if (!file_exists($localPath) || filesize($localPath) === 0) {
            $stmtUpdate->execute(['error', '', 'Archivo local no existe o vacio', $f['id']]);
            $errores++;
            emitProgress("  [" . ($i+1) . "/{$lote}] SKIP " . basename($f['rel_path']) . " — no existe\n");
            continue;
        }

        // Verificar tamaño real del archivo antes de subir
        $realSize = filesize($localPath);
        if ($maxFileSize > 0 && $realSize > $maxFileSize) {
            $stmtUpdate->execute(['skipped', '', 'Excede limite en upload: ' . formatSize($realSize) . ' > ' . formatSize($maxFileSize), $f['id']]);
            $errores++;
            emitProgress("  [" . ($i+1) . "/{$lote}] GRANDE " . basename($f['rel_path']) . " (" . formatSize($realSize) . ") — omitido\n");
            logMsg($logPath, "UPLOAD SKIP (grande) {$f['subdomain']}/{$f['rel_path']}: " . formatSize($realSize));
            continue;
        }

        $result = $s3->putObject($localPath, $f['wasabi_key'], $f['mime_type']);

        if ($result['ok']) {
            $stmtUpdate->execute(['uploaded', date('Y-m-d H:i:s'), '', $f['id']]);
            $exitos++;
            $bytesSubidos += $f['file_size'];
            emitProgress("  [" . ($i+1) . "/{$lote}] OK " . basename($f['rel_path']) . " (" . formatSize($f['file_size']) . ")\n");
        } else {
            // Reintentar una vez
            sleep(2);
            $result2 = $s3->putObject($localPath, $f['wasabi_key'], $f['mime_type']);
            if ($result2['ok']) {
                $stmtUpdate->execute(['uploaded', date('Y-m-d H:i:s'), '', $f['id']]);
                $exitos++;
                $bytesSubidos += $f['file_size'];
                emitProgress("  [" . ($i+1) . "/{$lote}] OK (retry) " . basename($f['rel_path']) . "\n");
            } else {
                $errMsg = substr($result2['error'], 0, 200);
                $stmtUpdate->execute(['error', '', $errMsg, $f['id']]);
                $errores++;
                emitProgress("  [" . ($i+1) . "/{$lote}] ERROR " . basename($f['rel_path']) . " — {$errMsg}\n");
                logMsg($logPath, "UPLOAD ERROR {$f['subdomain']}/{$f['rel_path']}: {$errMsg}");
            }
        }

        // Actualizar progreso del subdominio cada 10 archivos
        if (($i + 1) % 10 === 0 || $i === count($archivos) - 1) {
            $stmtProgress->execute([$f['subdomain'], $f['subdomain'], date('Y-m-d H:i:s'), $f['subdomain']]);
        }
    }

    $duracion = round(microtime(true) - $tiempoInicio, 1);
    $velocidad = $duracion > 0 ? formatSize($bytesSubidos / $duracion) . '/s' : 'N/A';
    $restantes = $totalPendientes - count($archivos);

    emitProgress("\n=== UPLOAD PARCIAL COMPLETADO ===\n");
    emitProgress("Procesados: " . count($archivos) . " | OK: {$exitos} | Errores: {$errores}\n");
    emitProgress("Subidos: " . formatSize($bytesSubidos) . " en {$duracion}s ({$velocidad})\n");
    emitProgress("Restantes: {$restantes}\n");

    logMsg($logPath, "UPLOAD: {$exitos} OK, {$errores} errores, " . formatSize($bytesSubidos) . " en {$duracion}s. Restantes: {$restantes}");
}

// =====================================================================
//  FASE 3: VERIFICAR
// =====================================================================

function faseVerificar($pdo, $basePath, $targetSub, $lote, $logPath, $s3) {
    $where = $targetSub ? " AND subdomain = " . $pdo->quote($targetSub) : '';

    $stmt = $pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status = 'uploaded'" . $where);
    $totalPendientes = (int)$stmt->fetchColumn();

    if ($totalPendientes === 0) {
        emitProgress("No hay archivos pendientes de verificacion.\n");
        return;
    }

    emitProgress("FASE 3: VERIFICAR — {$totalPendientes} archivos (lote: {$lote})\n\n");

    $stmt = $pdo->prepare("SELECT id, subdomain, rel_path, wasabi_key, file_size FROM file_tracking WHERE status = 'uploaded'" . $where . " LIMIT ?");
    $stmt->execute([$lote]);
    $archivos = $stmt->fetchAll(PDO::FETCH_ASSOC);

    $verificados = 0; $errores = 0;
    $stmtUpdate = $pdo->prepare("UPDATE file_tracking SET status = ?, verified_at = ?, error_msg = ? WHERE id = ?");
    $stmtProgress = $pdo->prepare("UPDATE subdomain_progress SET verified = (SELECT COUNT(*) FROM file_tracking WHERE subdomain = ? AND status IN ('verified','registered','truncated')), last_phase = 'verificar', last_update = ? WHERE subdomain = ?");

    foreach ($archivos as $i => $f) {
        $head = $s3->headObject($f['wasabi_key']);

        if (!$head['exists']) {
            $stmtUpdate->execute(['error', '', 'No existe en Wasabi', $f['id']]);
            $errores++;
            emitProgress("  [" . ($i+1) . "/{$lote}] FALTA " . $f['subdomain'] . '/' . basename($f['rel_path']) . "\n");
        } elseif ((int)$head['size'] !== (int)$f['file_size']) {
            $stmtUpdate->execute(['error', '', "Tamaño no coincide: local={$f['file_size']} wasabi={$head['size']}", $f['id']]);
            $errores++;
            emitProgress("  [" . ($i+1) . "/{$lote}] TAMAÑO " . $f['subdomain'] . '/' . basename($f['rel_path']) . " (local:{$f['file_size']} vs wasabi:{$head['size']})\n");
        } else {
            $stmtUpdate->execute(['verified', date('Y-m-d H:i:s'), '', $f['id']]);
            $verificados++;
            if (($i+1) % 50 === 0) {
                emitProgress("  [" . ($i+1) . "/{$lote}] verificados: {$verificados}\n");
            }
        }

        if (($i + 1) % 20 === 0 || $i === count($archivos) - 1) {
            $stmtProgress->execute([$f['subdomain'], date('Y-m-d H:i:s'), $f['subdomain']]);
        }
    }

    $restantes = $totalPendientes - count($archivos);
    emitProgress("\n=== VERIFICACION PARCIAL ===\n");
    emitProgress("Procesados: " . count($archivos) . " | Verificados: {$verificados} | Errores: {$errores} | Restantes: {$restantes}\n");
    logMsg($logPath, "VERIFICAR: {$verificados} OK, {$errores} errores. Restantes: {$restantes}");
}

// =====================================================================
//  FASE 4: REGISTRAR EN SQLite DE CADA SUBDOMINIO
// =====================================================================

function faseRegistrar($pdo, $basePath, $targetSub, $lote, $logPath) {
    $where = $targetSub ? " AND subdomain = " . $pdo->quote($targetSub) : '';

    $stmt = $pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status = 'verified'" . $where);
    $totalPendientes = (int)$stmt->fetchColumn();

    if ($totalPendientes === 0) {
        emitProgress("No hay archivos pendientes de registro.\n");
        return;
    }

    emitProgress("FASE 4: REGISTRAR — {$totalPendientes} archivos en SQLite local (lote: {$lote})\n\n");

    $stmt = $pdo->prepare("SELECT id, subdomain, media_id, wasabi_key, file_size, file_mtime, mime_type FROM file_tracking WHERE status = 'verified'" . $where . " ORDER BY subdomain LIMIT ?");
    $stmt->execute([$lote]);
    $archivos = $stmt->fetchAll(PDO::FETCH_ASSOC);

    $registrados = 0; $errores = 0;
    $currentSub = '';
    $subPdo = null;
    $subStmt = null;

    $stmtUpdate = $pdo->prepare("UPDATE file_tracking SET status = ?, registered_at = ?, error_msg = ? WHERE id = ?");
    $stmtProgress = $pdo->prepare("UPDATE subdomain_progress SET registered = (SELECT COUNT(*) FROM file_tracking WHERE subdomain = ? AND status IN ('registered','truncated')), last_phase = 'registrar', last_update = ? WHERE subdomain = ?");

    foreach ($archivos as $i => $f) {
        if ($f['subdomain'] !== $currentSub) {
            // Commit anterior si hay
            if ($subPdo && $subPdo->inTransaction()) $subPdo->commit();

            $currentSub = $f['subdomain'];
            $dbFile = $basePath . '/' . $currentSub . '/data/meta/wasabisync.sqlite3';

            try {
                // Crear DB si no existe
                $subPdo = new PDO('sqlite:' . $dbFile);
                $subPdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                $subPdo->exec('PRAGMA journal_mode = WAL');

                // Crear tabla si no existe
                $subPdo->exec("CREATE TABLE IF NOT EXISTS wasabi_files (
                    media_id TEXT PRIMARY KEY,
                    wasabi_key TEXT NOT NULL,
                    file_size INTEGER NOT NULL DEFAULT 0,
                    file_mtime INTEGER NOT NULL DEFAULT 0,
                    mime_type TEXT DEFAULT '',
                    uploaded_at TEXT NOT NULL DEFAULT ''
                )");

                $subStmt = $subPdo->prepare("INSERT OR REPLACE INTO wasabi_files (media_id, wasabi_key, file_size, file_mtime, mime_type, uploaded_at) VALUES (?,?,?,?,?,?)");
                $subPdo->beginTransaction();

                emitProgress("  [{$currentSub}] Registrando en SQLite...\n");
            } catch (\Exception $e) {
                emitProgress("  ERROR DB {$currentSub}: " . $e->getMessage() . "\n");
                logMsg($logPath, "REGISTRAR ERROR DB {$currentSub}: " . $e->getMessage());
                $subPdo = null;
                continue;
            }
        }

        if (!$subPdo || !$subStmt) {
            $stmtUpdate->execute(['error', '', 'No se pudo abrir SQLite del subdominio', $f['id']]);
            $errores++;
            continue;
        }

        try {
            $subStmt->execute([$f['media_id'], $f['wasabi_key'], $f['file_size'], $f['file_mtime'], $f['mime_type'], date('c')]);
            $stmtUpdate->execute(['registered', date('Y-m-d H:i:s'), '', $f['id']]);
            $registrados++;
        } catch (\Exception $e) {
            $stmtUpdate->execute(['error', '', 'Error INSERT: ' . $e->getMessage(), $f['id']]);
            $errores++;
        }

        if (($i + 1) % 50 === 0 || $i === count($archivos) - 1) {
            if ($subPdo && $subPdo->inTransaction()) { $subPdo->commit(); $subPdo->beginTransaction(); }
            $stmtProgress->execute([$f['subdomain'], date('Y-m-d H:i:s'), $f['subdomain']]);
        }
    }

    // Commit final
    if ($subPdo && $subPdo->inTransaction()) $subPdo->commit();

    emitProgress("\n=== REGISTRO COMPLETADO ===\n");
    emitProgress("Registrados: {$registrados} | Errores: {$errores}\n");
    logMsg($logPath, "REGISTRAR: {$registrados} OK, {$errores} errores");
}

// =====================================================================
//  FASE 5: TRUNCAR (solo si 100% verificado)
// =====================================================================

function faseTruncar($pdo, $basePath, $targetSub, $lote, $logPath, $forzar = false) {
    $where = $targetSub ? " AND subdomain = " . $pdo->quote($targetSub) : '';
    $whereP = $targetSub ? " WHERE subdomain = " . $pdo->quote($targetSub) : '';

    // Verificar que NO hay archivos en estados intermedios
    $stmtCheck = $pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status IN ('pending','uploaded','verified')" . $where);
    $noListos = (int)$stmtCheck->fetchColumn();

    // Verificar que no hay errores sin resolver
    $stmtErr = $pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status = 'error'" . $where);
    $conErrores = (int)$stmtErr->fetchColumn();

    if ($noListos > 0 && !$forzar) {
        emitProgress("BLOQUEADO: Hay {$noListos} archivos que no han completado todas las fases anteriores.\n");
        emitProgress("Primero complete las fases de upload, verificacion y registro.\n");
        emitProgress("(Use modo CLI con truncar_all para ignorar este bloqueo)\n");
        return;
    } elseif ($noListos > 0 && $forzar) {
        emitProgress("AVISO: {$noListos} archivos en estados intermedios. Se omiten y solo se truncan los registrados.\n");
    }

    if ($conErrores > 0) {
        emitProgress("ADVERTENCIA: Hay {$conErrores} archivos con errores. Estos NO seran truncados.\n");
        emitProgress("Revise los errores antes de continuar.\n\n");
    }

    $stmt = $pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status = 'registered'" . $where);
    $totalPendientes = (int)$stmt->fetchColumn();

    if ($totalPendientes === 0) {
        emitProgress("No hay archivos listos para truncar.\n");
        return;
    }

    emitProgress("FASE 5: TRUNCAR — {$totalPendientes} archivos a 0 bytes (lote: {$lote})\n\n");

    $stmt = $pdo->prepare("SELECT id, subdomain, rel_path, file_size FROM file_tracking WHERE status = 'registered'" . $where . " LIMIT ?");
    $stmt->execute([$lote]);
    $archivos = $stmt->fetchAll(PDO::FETCH_ASSOC);

    $truncados = 0; $errores = 0; $espacioLiberado = 0;
    $stmtUpdate = $pdo->prepare("UPDATE file_tracking SET status = ?, truncated_at = ?, error_msg = ? WHERE id = ?");
    $stmtProgress = $pdo->prepare("UPDATE subdomain_progress SET truncated = (SELECT COUNT(*) FROM file_tracking WHERE subdomain = ? AND status = 'truncated'), last_phase = 'truncar', last_update = ? WHERE subdomain = ?");

    foreach ($archivos as $i => $f) {
        $localPath = $basePath . '/' . $f['subdomain'] . '/' . $f['rel_path'];

        if (!file_exists($localPath)) {
            $stmtUpdate->execute(['truncated', date('Y-m-d H:i:s'), 'Ya no existe (OK)', $f['id']]);
            $truncados++;
            continue;
        }

        $currentSize = filesize($localPath);
        if ($currentSize === 0) {
            $stmtUpdate->execute(['truncated', date('Y-m-d H:i:s'), 'Ya era stub', $f['id']]);
            $truncados++;
            continue;
        }

        // Truncar a 0 bytes
        $fh = @fopen($localPath, 'w');
        if ($fh) {
            ftruncate($fh, 0);
            fclose($fh);

            // Verificar truncamiento
            clearstatcache(true, $localPath);
            if (filesize($localPath) === 0) {
                $stmtUpdate->execute(['truncated', date('Y-m-d H:i:s'), '', $f['id']]);
                $truncados++;
                $espacioLiberado += $f['file_size'];
            } else {
                $stmtUpdate->execute(['error', '', 'ftruncate fallo — archivo sigue con contenido', $f['id']]);
                $errores++;
            }
        } else {
            $stmtUpdate->execute(['error', '', 'No se pudo abrir archivo para truncar', $f['id']]);
            $errores++;
        }

        if (($i+1) % 20 === 0) {
            emitProgress("  [{$f['subdomain']}] " . ($i+1) . "/{$lote} truncados: {$truncados}, liberado: " . formatSize($espacioLiberado) . "\n");
        }

        if (($i + 1) % 20 === 0 || $i === count($archivos) - 1) {
            $stmtProgress->execute([$f['subdomain'], date('Y-m-d H:i:s'), $f['subdomain']]);
        }
    }

    emitProgress("\n=== TRUNCAMIENTO COMPLETADO ===\n");
    emitProgress("Truncados: {$truncados} | Errores: {$errores} | Espacio liberado: " . formatSize($espacioLiberado) . "\n");
    logMsg($logPath, "TRUNCAR: {$truncados} OK, {$errores} errores, liberado " . formatSize($espacioLiberado));
}

// =====================================================================
//  FUNCIONES COMUNES
// =====================================================================

function obtenerCarpetas($basePath, $excluir, $targetSub = '') {
    if (!empty($targetSub)) return [$targetSub];
    $carpetas = [];
    $scan = @scandir($basePath);
    if (!$scan) return [];
    foreach ($scan as $c) {
        if (in_array($c, $excluir)) continue;
        if (!is_dir($basePath . '/' . $c)) continue;
        $carpetas[] = $c;
    }
    sort($carpetas);
    return $carpetas;
}

/** Escanea subdominios DokuWiki validos (tienen doku.php + data/media) */
function scanSubdominiosDokuWiki($basePath, $excluir) {
    $subdominios = [];
    $scan = @scandir($basePath);
    if (!$scan) return $subdominios;
    foreach ($scan as $c) {
        if ($c[0] === '.' || $c[0] === '_') continue;
        if (in_array($c, $excluir)) continue;
        $fullPath = $basePath . '/' . $c;
        if (!is_dir($fullPath)) continue;
        if (is_link($fullPath)) continue;
        if (!file_exists($fullPath . '/doku.php')) continue;
        $hasMedia = is_dir($fullPath . '/data/media');
        $subdominios[] = [
            'name'      => $c,
            'has_media' => $hasMedia,
        ];
    }
    usort($subdominios, function($a, $b) { return strcasecmp($a['name'], $b['name']); });
    return $subdominios;
}

function emitProgress($msg) {
    $isCLI = (php_sapi_name() === 'cli');
    echo $isCLI ? $msg : htmlspecialchars($msg);
    if (ob_get_level()) ob_flush();
    flush();
}

// =====================================================================
//  MODO CLI CONTINUO (sin navegador)
// =====================================================================
// Uso SSH:
//   php migracion_masiva_wasabi.php upload_all [subdominio] [lote] [max_mb]
//   php migracion_masiva_wasabi.php verificar_all [subdominio] [lote]
//   php migracion_masiva_wasabi.php registrar_all [subdominio] [lote]
//   php migracion_masiva_wasabi.php truncar_all [subdominio] [lote]
//   php migracion_masiva_wasabi.php full [subdominio]  — ejecuta las 5 fases completas
//   php migracion_masiva_wasabi.php stats              — muestra estadisticas
//
// Para correr en background:
//   nohup php migracion_masiva_wasabi.php upload_all "" 500 > upload.log 2>&1 &
//   screen -S wasabi php migracion_masiva_wasabi.php upload_all "" 500

if ($esCLI && in_array($fase, ['upload_all', 'verificar_all', 'registrar_all', 'truncar_all', 'full', 'stats'])) {

    // Override emitProgress para CLI (sin htmlspecialchars)
    function emitProgressCLI($msg) {
        echo $msg;
        flush();
    }

    // Reemplazar la funcion emitProgress en modo CLI continuo
    // Nota: usamos un wrapper global
    $GLOBALS['_cli_continuous'] = true;

    $pdo = getTrackingDb($trackingDbPath);
    $creds = getWasabiCredentials($pdo);
    $s3 = null;
    if ($creds) {
        $s3 = new WasabiS3Client($creds['key'], $creds['secret'], $creds['bucket'], $creds['region'], $creds['endpoint']);
    }

    $cliLote = max(100, $lote); // CLI usa lotes mas grandes por default

    if ($fase === 'stats') {
        // Solo mostrar estadisticas
        $st = [
            'total'      => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking")->fetchColumn(),
            'pending'    => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='pending'")->fetchColumn(),
            'uploaded'   => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='uploaded'")->fetchColumn(),
            'verified'   => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='verified'")->fetchColumn(),
            'registered' => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='registered'")->fetchColumn(),
            'truncated'  => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='truncated'")->fetchColumn(),
            'errors'     => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='error'")->fetchColumn(),
            'skipped'    => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='skipped'")->fetchColumn(),
            'total_size' => (int)$pdo->query("SELECT COALESCE(SUM(file_size),0) FROM file_tracking")->fetchColumn(),
        ];
        echo "╔══════════════════════════════════════════════════╗\n";
        echo "║  MIGRACION WASABI S3 — ESTADISTICAS             ║\n";
        echo "╠══════════════════════════════════════════════════╣\n";
        echo "║  Total:       " . str_pad(number_format($st['total']), 36) . "║\n";
        echo "║  Tamaño:      " . str_pad(formatSize($st['total_size']), 36) . "║\n";
        echo "║  Pendientes:  " . str_pad(number_format($st['pending']), 36) . "║\n";
        echo "║  Subidos:     " . str_pad(number_format($st['uploaded']), 36) . "║\n";
        echo "║  Verificados: " . str_pad(number_format($st['verified']), 36) . "║\n";
        echo "║  Registrados: " . str_pad(number_format($st['registered']), 36) . "║\n";
        echo "║  Truncados:   " . str_pad(number_format($st['truncated']), 36) . "║\n";
        echo "║  Errores:     " . str_pad(number_format($st['errors']), 36) . "║\n";
        echo "║  Omitidos:    " . str_pad(number_format($st['skipped']), 36) . "║\n";
        echo "╚══════════════════════════════════════════════════╝\n";
        exit(0);
    }

    if (!$s3) {
        echo "ERROR: No hay credenciales configuradas en la base de datos.\n";
        echo "Ejecute primero el dashboard web para configurar las credenciales.\n";
        exit(1);
    }

    // Test conexion
    echo "[" . date('H:i:s') . "] Test de conexion a Wasabi...\n";
    $testResult = $s3->testConnection();
    if (!$testResult['ok']) {
        echo "ERROR: {$testResult['msg']}\n";
        exit(1);
    }
    echo "[" . date('H:i:s') . "] OK: {$testResult['msg']}\n\n";

    // Mapeo de fase continua a fase base
    $faseMap = [
        'upload_all'    => 'upload',
        'verificar_all' => 'verificar',
        'registrar_all' => 'registrar',
        'truncar_all'   => 'truncar',
        'full'          => 'full',
    ];
    $faseBase = $faseMap[$fase];

    // Determinar que status buscar para cada fase
    $statusMap = [
        'upload'    => 'pending',
        'verificar' => 'uploaded',
        'registrar' => 'verified',
        'truncar'   => 'registered',
    ];

    if ($faseBase === 'full') {
        // Ejecutar las 5 fases secuencialmente
        $fasesOrden = ['inventario', 'upload', 'verificar', 'registrar', 'truncar'];
        foreach ($fasesOrden as $fi => $faseActual) {
            echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
            echo " FASE " . ($fi+1) . "/5: " . strtoupper($faseActual) . "\n";
            echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n";

            if ($faseActual === 'inventario') {
                faseInventario($pdo, $basePath, $excluir, $targetSub, $logPath, $maxFileSize, $extensionesMigrar, $excluirMedia);
            } else {
                // Loop continuo hasta que no queden archivos
                $statusBuscar = $statusMap[$faseActual];
                $where = $targetSub ? " AND subdomain = " . $pdo->quote($targetSub) : '';
                $iteracion = 0;

                while (true) {
                    $pendientes = (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status = '{$statusBuscar}'" . $where)->fetchColumn();
                    if ($pendientes === 0) {
                        echo "[" . date('H:i:s') . "] Fase {$faseActual}: completada (0 pendientes)\n\n";
                        break;
                    }

                    $iteracion++;
                    echo "[" . date('H:i:s') . "] Lote #{$iteracion} — {$pendientes} pendientes\n";

                    switch ($faseActual) {
                        case 'upload':    faseUpload($pdo, $basePath, $targetSub, $cliLote, $logPath, $s3, $maxFileSize); break;
                        case 'verificar': faseVerificar($pdo, $basePath, $targetSub, $cliLote, $logPath, $s3); break;
                        case 'registrar': faseRegistrar($pdo, $basePath, $targetSub, $cliLote, $logPath); break;
                        case 'truncar':   faseTruncar($pdo, $basePath, $targetSub, $cliLote, $logPath, true); break;
                    }

                    echo "\n";
                    // Breve pausa entre lotes para no saturar
                    usleep(500000); // 0.5s
                }
            }
        }

        // Resumen final
        $st = [
            'truncated' => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='truncated'" . ($targetSub ? " AND subdomain=" . $pdo->quote($targetSub) : ''))->fetchColumn(),
            'errors'    => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='error'" . ($targetSub ? " AND subdomain=" . $pdo->quote($targetSub) : ''))->fetchColumn(),
            'total'     => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking" . ($targetSub ? " WHERE subdomain=" . $pdo->quote($targetSub) : ''))->fetchColumn(),
            'bytes'     => (int)$pdo->query("SELECT COALESCE(SUM(file_size),0) FROM file_tracking WHERE status='truncated'" . ($targetSub ? " AND subdomain=" . $pdo->quote($targetSub) : ''))->fetchColumn(),
        ];

        echo "\n╔══════════════════════════════════════════════════╗\n";
        echo "║  MIGRACION COMPLETA                              ║\n";
        echo "╠══════════════════════════════════════════════════╣\n";
        echo "║  Total:     " . str_pad(number_format($st['total']), 37) . "║\n";
        echo "║  Truncados: " . str_pad(number_format($st['truncated']), 37) . "║\n";
        echo "║  Errores:   " . str_pad(number_format($st['errors']), 37) . "║\n";
        echo "║  Liberado:  " . str_pad(formatSize($st['bytes']), 37) . "║\n";
        echo "╚══════════════════════════════════════════════════╝\n";

    } else {
        // Fase individual continua (upload_all, verificar_all, etc.)
        $statusBuscar = $statusMap[$faseBase];
        $where = $targetSub ? " AND subdomain = " . $pdo->quote($targetSub) : '';
        $iteracion = 0;
        $totalProcesados = 0;
        $tiempoGlobal = microtime(true);

        echo "[" . date('H:i:s') . "] Iniciando {$faseBase} continuo (lote: {$cliLote})\n";
        if ($targetSub) echo "[" . date('H:i:s') . "] Subdominio: {$targetSub}\n";
        echo "\n";

        while (true) {
            $pendientes = (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status = '{$statusBuscar}'" . $where)->fetchColumn();
            if ($pendientes === 0) {
                echo "\n[" . date('H:i:s') . "] ===== COMPLETADO =====\n";
                echo "[" . date('H:i:s') . "] Total procesados: {$totalProcesados}\n";
                $dur = round(microtime(true) - $tiempoGlobal, 1);
                echo "[" . date('H:i:s') . "] Tiempo total: {$dur}s\n";
                break;
            }

            $iteracion++;
            echo "[" . date('H:i:s') . "] === Lote #{$iteracion} — {$pendientes} restantes ===\n";

            switch ($faseBase) {
                case 'upload':    faseUpload($pdo, $basePath, $targetSub, $cliLote, $logPath, $s3, $maxFileSize); break;
                case 'verificar': faseVerificar($pdo, $basePath, $targetSub, $cliLote, $logPath, $s3); break;
                case 'registrar': faseRegistrar($pdo, $basePath, $targetSub, $cliLote, $logPath); break;
                case 'truncar':   faseTruncar($pdo, $basePath, $targetSub, $cliLote, $logPath, true); break;
            }

            $totalProcesados += $cliLote;
            echo "\n";
            usleep(500000); // 0.5s entre lotes
        }
    }

    logMsg($logPath, "CLI {$fase}: finalizado" . ($targetSub ? " (sub: {$targetSub})" : ''));
    exit(0);
}

// =====================================================================
//  EJECUCION (WEB)
// =====================================================================

$pdo = getTrackingDb($trackingDbPath);

// Leer credenciales
$creds = getWasabiCredentials($pdo);

// Crear cliente S3 si hay credenciales
$s3 = null;
if ($creds) {
    $s3 = new WasabiS3Client($creds['key'], $creds['secret'], $creds['bucket'], $creds['region'], $creds['endpoint']);
}

// === Ejecutar fases de procesamiento (output streaming) ===
if (in_array($fase, ['inventario', 'upload', 'verificar', 'registrar', 'truncar', 'test_conexion', 'test_completo', 'reset'])) {

    if ($fase === 'reset') {
        @unlink($trackingDbPath);
        header('Location: ?');
        exit;
    }

    // Streaming output
    while (ob_get_level()) ob_end_flush();
    @ini_set('output_buffering', 'off');
    @ini_set('zlib.output_compression', false);

    $faseSafe = htmlspecialchars($fase, ENT_QUOTES, 'UTF-8');
    echo "<!DOCTYPE html><html><head><meta charset='UTF-8'><title>Migracion Wasabi — Fase: {$faseSafe}</title>";
    echo '<style>body{font-family:monospace;background:#0f172a;color:#e2e8f0;padding:20px;line-height:1.7;} ';
    echo 'h1{color:#38bdf8;margin-bottom:8px;} .ok{color:#4ade80;} .err{color:#f87171;} .warn{color:#fbbf24;} ';
    echo 'a{color:#38bdf8;} .btn{display:inline-block;padding:8px 16px;background:#2563eb;color:#fff;border-radius:6px;text-decoration:none;margin:4px;}</style>';
    echo "</head><body>";
    echo "<h1>Migracion Masiva Wasabi S3</h1>";
    echo "<p>Fase: <strong>{$faseSafe}</strong> | " . date('Y-m-d H:i:s');
    if ($targetSub) echo " | Subdominio: <strong>" . htmlspecialchars($targetSub, ENT_QUOTES, 'UTF-8') . "</strong>";
    echo " | Max: <strong>{$maxFileSizeMB} MB</strong>/archivo";
    echo "</p><hr><pre>\n";
    flush();

    if ($fase === 'test_conexion') {
        if (!$s3) { emitProgress("ERROR: No hay credenciales configuradas.\n"); }
        else {
            $r = $s3->testConnection();
            emitProgress($r['ok'] ? "OK: {$r['msg']}\n" : "ERROR: {$r['msg']}\n");
        }
    } elseif ($fase === 'test_completo') {
        // ============================================================
        // PRUEBA COMPLETA: ejecuta las 5 fases en un solo subdominio
        // ============================================================
        if (!$targetSub) {
            emitProgress("ERROR: Debe seleccionar un subdominio para la prueba completa.\n");
        } elseif (!$s3) {
            emitProgress("ERROR: No hay credenciales configuradas.\n");
        } else {
            $abort = false;

            // PASO 0: Test conexion
            emitProgress("╔══════════════════════════════════════════════════╗\n");
            emitProgress("║  PRUEBA COMPLETA: " . str_pad($targetSub, 30) . "║\n");
            emitProgress("╚══════════════════════════════════════════════════╝\n\n");

            emitProgress("━━━ PASO 0: TEST DE CONEXION ━━━\n");
            $r = $s3->testConnection();
            if ($r['ok']) {
                emitProgress("✓ {$r['msg']}\n\n");
            } else {
                emitProgress("✗ {$r['msg']}\n");
                emitProgress("\nABORTADO: La conexion a Wasabi fallo.\n");
                $abort = true;
            }

            // PASO 1: Inventario
            if (!$abort) {
                emitProgress("━━━ PASO 1: INVENTARIO ━━━\n");
                faseInventario($pdo, $basePath, $excluir, $targetSub, $logPath, $maxFileSize, $extensionesMigrar, $excluirMedia);

                $countPending = (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE subdomain = " . $pdo->quote($targetSub) . " AND status = 'pending'")->fetchColumn();
                if ($countPending === 0) {
                    emitProgress("\nNo hay archivos pendientes para este subdominio.\n");
                    $abort = true;
                } else {
                    emitProgress("\n✓ {$countPending} archivos pendientes de upload\n\n");
                }
            }

            // PASO 2: Upload
            if (!$abort) {
                emitProgress("━━━ PASO 2: UPLOAD ━━━\n");
                faseUpload($pdo, $basePath, $targetSub, 50000, $logPath, $s3, $maxFileSize);

                $countUploaded = (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE subdomain = " . $pdo->quote($targetSub) . " AND status = 'uploaded'")->fetchColumn();
                $countErrors = (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE subdomain = " . $pdo->quote($targetSub) . " AND status = 'error'")->fetchColumn();
                if ($countUploaded === 0 && $countErrors > 0) {
                    emitProgress("\nABORTADO: Todos los archivos fallaron en upload.\n");
                    $abort = true;
                } else {
                    emitProgress("\n✓ {$countUploaded} archivos subidos" . ($countErrors > 0 ? ", {$countErrors} errores" : "") . "\n\n");
                }
            }

            // PASO 3: Verificar
            if (!$abort) {
                emitProgress("━━━ PASO 3: VERIFICAR ━━━\n");
                faseVerificar($pdo, $basePath, $targetSub, 50000, $logPath, $s3);

                $countVerified = (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE subdomain = " . $pdo->quote($targetSub) . " AND status = 'verified'")->fetchColumn();
                emitProgress("\n✓ {$countVerified} archivos verificados\n\n");
            }

            // PASO 4: Registrar
            if (!$abort) {
                emitProgress("━━━ PASO 4: REGISTRAR EN SQLite ━━━\n");
                faseRegistrar($pdo, $basePath, $targetSub, 50000, $logPath);

                $countRegistered = (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE subdomain = " . $pdo->quote($targetSub) . " AND status = 'registered'")->fetchColumn();
                emitProgress("\n✓ {$countRegistered} archivos registrados\n\n");
            }

            // PASO 5: Truncar
            if (!$abort) {
                emitProgress("━━━ PASO 5: TRUNCAR ━━━\n");
                faseTruncar($pdo, $basePath, $targetSub, 50000, $logPath);

                $countTruncated = (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE subdomain = " . $pdo->quote($targetSub) . " AND status = 'truncated'")->fetchColumn();
                emitProgress("\n✓ {$countTruncated} archivos truncados\n\n");
            }

            // RESUMEN FINAL
            if (!$abort) {
                $subStats = $pdo->query("SELECT
                    COUNT(*) as total,
                    SUM(CASE WHEN status='truncated' THEN 1 ELSE 0 END) as truncados,
                    SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) as errores,
                    COALESCE(SUM(file_size),0) as bytes
                FROM file_tracking WHERE subdomain = " . $pdo->quote($targetSub))->fetch(PDO::FETCH_ASSOC);

                emitProgress("╔══════════════════════════════════════════════════╗\n");
                emitProgress("║  RESULTADO FINAL                                ║\n");
                emitProgress("╠══════════════════════════════════════════════════╣\n");
                emitProgress("║  Subdominio: " . str_pad($targetSub, 36) . "║\n");
                emitProgress("║  Archivos:   " . str_pad($subStats['total'], 36) . "║\n");
                emitProgress("║  Truncados:  " . str_pad($subStats['truncados'], 36) . "║\n");
                emitProgress("║  Errores:    " . str_pad($subStats['errores'], 36) . "║\n");
                emitProgress("║  Espacio:    " . str_pad(formatSize($subStats['bytes']), 36) . "║\n");
                emitProgress("╚══════════════════════════════════════════════════╝\n");

                if ((int)$subStats['errores'] === 0) {
                    emitProgress("\n✓ PRUEBA COMPLETA EXITOSA — listo para aplicar a todos los subdominios.\n");
                } else {
                    emitProgress("\n⚠ Prueba completada con {$subStats['errores']} errores. Revise antes de aplicar masivamente.\n");
                }
            }

            logMsg($logPath, "TEST COMPLETO {$targetSub}: finalizado");
        }
    } elseif ($fase === 'inventario') {
        faseInventario($pdo, $basePath, $excluir, $targetSub, $logPath, $maxFileSize, $extensionesMigrar, $excluirMedia);
    } elseif ($fase === 'upload') {
        if (!$s3) { emitProgress("ERROR: No hay credenciales configuradas.\n"); }
        else { faseUpload($pdo, $basePath, $targetSub, $lote, $logPath, $s3, $maxFileSize); }
    } elseif ($fase === 'verificar') {
        if (!$s3) { emitProgress("ERROR: No hay credenciales configuradas.\n"); }
        else { faseVerificar($pdo, $basePath, $targetSub, $lote, $logPath, $s3); }
    } elseif ($fase === 'registrar') {
        faseRegistrar($pdo, $basePath, $targetSub, $lote, $logPath);
    } elseif ($fase === 'truncar') {
        faseTruncar($pdo, $basePath, $targetSub, $lote, $logPath);
    }

    echo "</pre><hr>";
    echo "<a href='?' class='btn'>Volver al Dashboard</a> ";
    if ($targetSub) echo "<a href='?sub={$targetSub}' class='btn'>Detalle {$targetSub}</a> ";
    if (in_array($fase, ['upload','verificar','registrar','truncar'])) {
        $nextUrl = "?fase={$fase}&lote={$lote}" . ($targetSub ? "&sub={$targetSub}" : '') . "&max_size_mb={$maxFileSizeMB}";
        echo "<a href='{$nextUrl}' class='btn' style='background:#16a34a;' id='btnContinuar'>Continuar siguiente lote</a> ";

        // Auto-pilot UI
        echo "<button onclick='toggleAutopilot()' class='btn' id='btnAutopilot' style='background:#7c3aed;'>&#9658; Auto-pilot</button> ";
        echo "<button onclick='stopAutopilot()' class='btn' id='btnStop' style='background:#dc2626;display:none;'>&#9724; Detener</button> ";
        echo "<span id='autopilotStatus' style='color:#fbbf24;font-size:0.85em;margin-left:8px;'></span>";

        echo "<script>
var autopilotActive = false;
var autopilotTimer = null;
var delaySeconds = 3;

function toggleAutopilot() {
    autopilotActive = true;
    document.getElementById('btnAutopilot').style.display = 'none';
    document.getElementById('btnStop').style.display = 'inline-block';
    startCountdown();
}

function stopAutopilot() {
    autopilotActive = false;
    clearInterval(autopilotTimer);
    document.getElementById('btnAutopilot').style.display = 'inline-block';
    document.getElementById('btnStop').style.display = 'none';
    document.getElementById('autopilotStatus').textContent = 'Detenido.';
}

function startCountdown() {
    var remaining = delaySeconds;
    document.getElementById('autopilotStatus').textContent = 'Auto-continue en ' + remaining + 's...';
    autopilotTimer = setInterval(function() {
        remaining--;
        if (remaining <= 0) {
            clearInterval(autopilotTimer);
            document.getElementById('autopilotStatus').textContent = 'Redirigiendo...';
            window.location.href = document.getElementById('btnContinuar').href;
        } else {
            document.getElementById('autopilotStatus').textContent = 'Auto-continue en ' + remaining + 's...';
        }
    }, 1000);
}

// Si viene de auto-pilot (parametro URL), activar automaticamente
if (window.location.search.indexOf('autopilot=1') !== -1) {
    // Agregar autopilot=1 al link de continuar
    var link = document.getElementById('btnContinuar');
    link.href += '&autopilot=1';
    toggleAutopilot();
} else {
    // Agregar autopilot=1 al link si se activa manualmente
    var origToggle = toggleAutopilot;
    toggleAutopilot = function() {
        var link = document.getElementById('btnContinuar');
        if (link.href.indexOf('autopilot=1') === -1) link.href += '&autopilot=1';
        origToggle();
    };
}
</script>";
    }
    echo "</body></html>";
    exit;
}

// =====================================================================
//  DASHBOARD (HTML)
// =====================================================================

// Escanear subdominios DokuWiki para el selector
$allSubdominios = scanSubdominiosDokuWiki($basePath, $excluir);

// Obtener estadisticas globales
$stats = [
    'total'      => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking")->fetchColumn(),
    'total_size' => (int)$pdo->query("SELECT COALESCE(SUM(file_size),0) FROM file_tracking")->fetchColumn(),
    'pending'    => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='pending'")->fetchColumn(),
    'uploaded'   => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='uploaded'")->fetchColumn(),
    'verified'   => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='verified'")->fetchColumn(),
    'registered' => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='registered'")->fetchColumn(),
    'truncated'  => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='truncated'")->fetchColumn(),
    'errors'     => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='error'")->fetchColumn(),
    'skipped'    => (int)$pdo->query("SELECT COUNT(*) FROM file_tracking WHERE status='skipped'")->fetchColumn(),
    'skipped_size' => (int)$pdo->query("SELECT COALESCE(SUM(file_size),0) FROM file_tracking WHERE status='skipped'")->fetchColumn(),
];

// Subdominios
$subFilter = $targetSub ? " WHERE subdomain = " . $pdo->quote($targetSub) : '';
$subdominios = $pdo->query("SELECT * FROM subdomain_progress" . $subFilter . " ORDER BY subdomain")->fetchAll(PDO::FETCH_ASSOC);

// Errores recientes
$erroresRecientes = $pdo->query("SELECT subdomain, rel_path, error_msg, status FROM file_tracking WHERE status='error' ORDER BY subdomain LIMIT 50")->fetchAll(PDO::FETCH_ASSOC);

// Archivos grandes omitidos (skipped)
$archivosGrandes = $pdo->query("SELECT subdomain, rel_path, file_size, error_msg FROM file_tracking WHERE status='skipped' ORDER BY file_size DESC LIMIT 100")->fetchAll(PDO::FETCH_ASSOC);

// Calcular porcentajes
$pctUpload = $stats['total'] > 0 ? round(($stats['uploaded'] + $stats['verified'] + $stats['registered'] + $stats['truncated']) / $stats['total'] * 100, 1) : 0;
$pctVerify = $stats['total'] > 0 ? round(($stats['verified'] + $stats['registered'] + $stats['truncated']) / $stats['total'] * 100, 1) : 0;
$pctRegister = $stats['total'] > 0 ? round(($stats['registered'] + $stats['truncated']) / $stats['total'] * 100, 1) : 0;
$pctTruncate = $stats['total'] > 0 ? round($stats['truncated'] / $stats['total'] * 100, 1) : 0;

$inventarioOk = getConfig($pdo, 'inventario_completado');
$tieneCreds   = $creds && !empty($creds['key']);
$puedeUpload  = $inventarioOk && $tieneCreds;
$puedeVerify  = $stats['uploaded'] > 0 || $stats['verified'] > 0;
$puedeRegistrar = $stats['verified'] > 0;
$puedeTruncar = $stats['registered'] > 0 && $stats['pending'] === 0 && $stats['uploaded'] === 0 && $stats['verified'] === 0;

?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Migracion Masiva Wasabi S3 - Dashboard</title>
<style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 20px; }
    .container { max-width: 1400px; margin: 0 auto; }
    h1 { color: #38bdf8; margin-bottom: 4px; font-size: 1.6em; }
    h2 { color: #94a3b8; font-size: 1.1em; margin-bottom: 12px; }
    .subtitle { color: #64748b; margin-bottom: 20px; font-size: 0.9em; }

    .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; margin-bottom: 24px; }
    .card { background: #1e293b; border-radius: 10px; padding: 14px; text-align: center; border: 1px solid #334155; }
    .card .num { font-size: 1.6em; font-weight: 700; }
    .card .label { color: #94a3b8; font-size: 0.78em; margin-top: 4px; }
    .blue { color: #38bdf8; } .green { color: #4ade80; } .yellow { color: #fbbf24; }
    .red { color: #f87171; } .purple { color: #a78bfa; } .cyan { color: #22d3ee; } .white { color: #f1f5f9; }

    .section { background: #1e293b; border-radius: 10px; padding: 20px; margin-bottom: 16px; border: 1px solid #334155; }
    .section h2 { margin-bottom: 14px; }

    .phase-bar { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; }
    .phase { flex: 1; min-width: 160px; background: #334155; border-radius: 8px; padding: 12px; text-align: center; position: relative; overflow: hidden; }
    .phase .pname { font-weight: 700; font-size: 0.85em; margin-bottom: 4px; }
    .phase .pval { font-size: 1.3em; font-weight: 700; }
    .phase .ppct { font-size: 0.75em; color: #94a3b8; }
    .phase .progress { position: absolute; bottom: 0; left: 0; height: 3px; background: #38bdf8; transition: width 0.3s; }
    .phase.active { border: 2px solid #38bdf8; }
    .phase.done { border: 2px solid #4ade80; }
    .phase.done .progress { background: #4ade80; }
    .phase.locked { opacity: 0.5; }

    .form-box { background: #334155; border-radius: 10px; padding: 16px; margin-bottom: 16px; }
    .form-box h3 { color: #38bdf8; margin-bottom: 10px; font-size: 0.95em; }
    .form-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: end; }
    .form-group { flex: 1; min-width: 150px; }
    .form-group label { display: block; font-size: 0.8em; color: #94a3b8; margin-bottom: 4px; font-weight: 600; }
    .form-group input, .form-group select { width: 100%; padding: 8px 10px; border: 1px solid #475569; border-radius: 6px; background: #1e293b; color: #e2e8f0; font-size: 0.9em; }
    .form-group input:focus { border-color: #38bdf8; outline: none; }

    .btn { display: inline-block; padding: 10px 18px; border-radius: 8px; font-weight: 600; font-size: 0.88em;
           text-decoration: none; cursor: pointer; border: none; transition: all 0.2s; color: #fff; }
    .btn-blue { background: #2563eb; } .btn-blue:hover { background: #1d4ed8; }
    .btn-green { background: #16a34a; } .btn-green:hover { background: #15803d; }
    .btn-red { background: #dc2626; } .btn-red:hover { background: #b91c1c; }
    .btn-yellow { background: #d97706; } .btn-yellow:hover { background: #b45309; }
    .btn-purple { background: #7c3aed; }
    .btn-gray { background: #475569; }
    .btn:disabled, .btn.disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }

    .actions { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }

    table { width: 100%; border-collapse: collapse; font-size: 0.82em; }
    th { text-align: left; padding: 8px 6px; border-bottom: 1px solid #334155; color: #94a3b8; font-size: 0.85em; }
    td { padding: 6px; border-bottom: 1px solid #1e293b; }
    tr:hover { background: #334155; }
    .badge { display: inline-block; padding: 2px 7px; border-radius: 4px; font-size: 0.75em; font-weight: 600; }
    .badge-ok { background: #065f46; color: #4ade80; }
    .badge-warn { background: #78350f; color: #fbbf24; }
    .badge-err { background: #7f1d1d; color: #f87171; }
    .badge-info { background: #1e3a5f; color: #38bdf8; }
    .badge-gray { background: #334155; color: #94a3b8; }

    .bar { height: 6px; background: #334155; border-radius: 3px; overflow: hidden; }
    .bar-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }
    .bar-blue { background: #38bdf8; } .bar-green { background: #4ade80; } .bar-yellow { background: #fbbf24; } .bar-red { background: #f87171; }

    .error-row { background: #1c1917; }

    .filter-bar { margin-bottom: 10px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
    .filter-bar input { padding: 6px 10px; border: 1px solid #475569; border-radius: 6px; background: #1e293b; color: #e2e8f0; font-size: 0.85em; width: 180px; }

    .warn-box { background: #422006; border: 1px solid #d97706; border-radius: 10px; padding: 14px; margin-bottom: 16px; color: #fbbf24; }
    .ok-box { background: #052e16; border: 1px solid #16a34a; border-radius: 10px; padding: 14px; margin-bottom: 16px; color: #4ade80; }
</style>
</head>
<body>
<div class="container">

<h1>&#9889; Migracion Masiva Wasabi S3</h1>
<p class="subtitle">
    <?= date('Y-m-d H:i:s') ?>
    <?php if ($targetSub): ?> | Subdominio: <strong style="color:#38bdf8;"><?= htmlspecialchars($targetSub) ?></strong> <a href="?" style="color:#94a3b8;font-size:0.85em;">[ver todos]</a><?php endif; ?>
    <?php if ($inventarioOk): ?> | Inventario: <?= $inventarioOk ?><?php endif; ?>
</p>

<!-- Resumen global -->
<div class="summary">
    <div class="card"><div class="num white"><?= number_format($stats['total']) ?></div><div class="label">Total archivos</div></div>
    <div class="card"><div class="num blue"><?= formatSize($stats['total_size']) ?></div><div class="label">Tamaño total</div></div>
    <div class="card"><div class="num yellow"><?= number_format($stats['pending']) ?></div><div class="label">Pendientes</div></div>
    <div class="card"><div class="num cyan"><?= number_format($stats['uploaded']) ?></div><div class="label">Subidos</div></div>
    <div class="card"><div class="num purple"><?= number_format($stats['verified']) ?></div><div class="label">Verificados</div></div>
    <div class="card"><div class="num blue"><?= number_format($stats['registered']) ?></div><div class="label">Registrados</div></div>
    <div class="card"><div class="num green"><?= number_format($stats['truncated']) ?></div><div class="label">Truncados</div></div>
    <div class="card"><div class="num red"><?= number_format($stats['errors']) ?></div><div class="label">Errores</div></div>
    <?php if ($stats['skipped'] > 0): ?>
    <div class="card" style="border:1px solid #d97706;"><div class="num yellow"><?= number_format($stats['skipped']) ?></div><div class="label">Omitidos (grandes)</div></div>
    <?php endif; ?>
</div>

<!-- Barras de progreso por fase -->
<div class="phase-bar">
    <div class="phase <?= $inventarioOk ? 'done' : 'active' ?>">
        <div class="pname">1. Inventario</div>
        <div class="pval"><?= $inventarioOk ? '&#10003;' : '—' ?></div>
        <div class="ppct"><?= number_format($stats['total']) ?> archivos</div>
        <div class="progress" style="width:<?= $inventarioOk ? 100 : 0 ?>%"></div>
    </div>
    <div class="phase <?= $pctUpload >= 100 ? 'done' : ($puedeUpload ? 'active' : 'locked') ?>">
        <div class="pname">2. Upload</div>
        <div class="pval"><?= $pctUpload ?>%</div>
        <div class="ppct"><?= number_format($stats['uploaded'] + $stats['verified'] + $stats['registered'] + $stats['truncated']) ?> / <?= number_format($stats['total']) ?></div>
        <div class="progress" style="width:<?= $pctUpload ?>%"></div>
    </div>
    <div class="phase <?= $pctVerify >= 100 ? 'done' : ($puedeVerify ? 'active' : 'locked') ?>">
        <div class="pname">3. Verificar</div>
        <div class="pval"><?= $pctVerify ?>%</div>
        <div class="ppct"><?= number_format($stats['verified'] + $stats['registered'] + $stats['truncated']) ?> / <?= number_format($stats['total']) ?></div>
        <div class="progress" style="width:<?= $pctVerify ?>%"></div>
    </div>
    <div class="phase <?= $pctRegister >= 100 ? 'done' : ($puedeRegistrar ? 'active' : 'locked') ?>">
        <div class="pname">4. Registrar DB</div>
        <div class="pval"><?= $pctRegister ?>%</div>
        <div class="ppct"><?= number_format($stats['registered'] + $stats['truncated']) ?> / <?= number_format($stats['total']) ?></div>
        <div class="progress" style="width:<?= $pctRegister ?>%"></div>
    </div>
    <div class="phase <?= $pctTruncate >= 100 ? 'done' : ($puedeTruncar ? 'active' : 'locked') ?>">
        <div class="pname">5. Truncar</div>
        <div class="pval"><?= $pctTruncate ?>%</div>
        <div class="ppct"><?= number_format($stats['truncated']) ?> / <?= number_format($stats['total']) ?></div>
        <div class="progress" style="width:<?= $pctTruncate ?>%"></div>
    </div>
</div>

<!-- Credenciales y controles -->
<div class="section">
    <h2>&#128273; Credenciales Wasabi & Controles</h2>
    <form method="POST" action="">
        <div class="form-row">
            <div class="form-group">
                <label>Access Key *</label>
                <input type="text" name="wasabi_key" value="<?= htmlspecialchars($creds['key'] ?? '') ?>" required />
            </div>
            <div class="form-group">
                <label>Secret Key *</label>
                <input type="password" name="wasabi_secret" value="<?= htmlspecialchars($creds['secret'] ?? '') ?>" required />
            </div>
            <div class="form-group" style="max-width:150px;">
                <label>Bucket</label>
                <input type="text" name="wasabi_bucket" value="<?= htmlspecialchars($creds['bucket'] ?? 'nicc-archivos') ?>" />
            </div>
            <div class="form-group" style="max-width:120px;">
                <label>Region</label>
                <input type="text" name="wasabi_region" value="<?= htmlspecialchars($creds['region'] ?? 'us-east-1') ?>" />
            </div>
            <div class="form-group" style="max-width:180px;">
                <label>Endpoint</label>
                <input type="text" name="wasabi_endpoint" value="<?= htmlspecialchars($creds['endpoint'] ?? 's3.wasabisys.com') ?>" />
            </div>
            <div class="form-group" style="max-width:100px;">
                <label>Lote</label>
                <input type="number" name="lote" value="<?= $lote ?>" min="1" max="5000" />
            </div>
            <div class="form-group" style="max-width:120px;">
                <label>Max MB/archivo</label>
                <input type="number" name="max_size_mb" value="<?= $maxFileSizeMB ?>" min="10" max="5000" title="Archivos mayores se omiten" />
            </div>
        </div>
        <div style="margin-top:6px;">
            <label style="font-size:0.8em;color:#94a3b8;">Subdominio (vacio = todos) — <?= count($allSubdominios) ?> sistemas detectados</label>
            <div class="form-row" style="margin-top:4px;">
                <div class="form-group" style="max-width:260px;">
                    <select name="sub" id="subSelector" style="padding:8px 10px;border:1px solid #475569;border-radius:6px;background:#1e293b;color:#e2e8f0;font-size:0.9em;width:100%;">
                        <option value="">— Todos los subdominios —</option>
                        <?php foreach ($allSubdominios as $sd): ?>
                        <option value="<?= htmlspecialchars($sd['name']) ?>" <?= ($targetSub === $sd['name']) ? 'selected' : '' ?>><?= htmlspecialchars($sd['name']) ?><?= $sd['has_media'] ? '' : ' (sin media)' ?></option>
                        <?php endforeach; ?>
                    </select>
                </div>
                <input type="hidden" name="fase" id="faseInput" value="dashboard" />
                <button type="submit" class="btn btn-blue" onclick="document.getElementById('faseInput').value='test_conexion'">&#128268; Test Conexion</button>
                <button type="submit" class="btn btn-yellow" onclick="document.getElementById('faseInput').value='inventario'">1. Inventario</button>
                <button type="submit" class="btn btn-blue" onclick="document.getElementById('faseInput').value='upload'" <?= !$puedeUpload?'disabled':'' ?>>2. Upload</button>
                <button type="submit" class="btn btn-purple" onclick="document.getElementById('faseInput').value='verificar'" <?= !$puedeVerify?'disabled':'' ?>>3. Verificar</button>
                <button type="submit" class="btn btn-green" onclick="document.getElementById('faseInput').value='registrar'" <?= !$puedeRegistrar?'disabled':'' ?>>4. Registrar</button>
                <button type="submit" class="btn btn-red" onclick="if(!confirm('ATENCION: Esto truncara archivos a 0 bytes.\\n\\nSolo proceda si ha verificado el 100% de integridad.\\n\\nContinuar?')){return false;}document.getElementById('faseInput').value='truncar'" <?= !$puedeTruncar?'disabled':'' ?>>5. Truncar</button>
            </div>
            <div style="margin-top:10px;padding-top:10px;border-top:1px solid #475569;">
                <label style="font-size:0.85em;color:#fbbf24;font-weight:600;">&#9889; Prueba completa en un dominio (ejecuta las 5 fases secuencialmente)</label>
                <div style="margin-top:6px;">
                    <button type="submit" class="btn" style="background:linear-gradient(135deg,#2563eb,#7c3aed);font-size:0.95em;padding:12px 24px;" onclick="var sel=document.getElementById('subSelector');if(!sel.value){alert('Seleccione un subdominio primero.');return false;}if(!confirm('Ejecutar PRUEBA COMPLETA en: '+sel.value+'?\\n\\nEsto ejecutara:\\n1. Inventario\\n2. Upload\\n3. Verificar\\n4. Registrar DB\\n5. Truncar\\n\\nSolo afecta al subdominio seleccionado.\\n\\nContinuar?')){return false;}document.getElementById('faseInput').value='test_completo'">&#128640; Prueba completa (dominio seleccionado)</button>
                </div>
            </div>
        </div>
    </form>
    <div style="margin-top:10px;padding-top:10px;border-top:1px solid #475569;">
        <label style="font-size:0.85em;color:#a78bfa;font-weight:600;">&#9658; Auto-pilot (ejecuta lotes automaticamente sin parar)</label>
        <div style="margin-top:6px;display:flex;gap:8px;flex-wrap:wrap;">
            <?php
            $apSub = $targetSub ? '&sub=' . urlencode($targetSub) : '';
            $apBase = "?lote={$lote}&max_size_mb={$maxFileSizeMB}{$apSub}&autopilot=1";
            ?>
            <a href="<?= $apBase ?>&fase=upload" class="btn" style="background:linear-gradient(135deg,#7c3aed,#2563eb);" <?= !$puedeUpload?'onclick="return false;" style="opacity:0.4;pointer-events:none;"':'' ?>>&#9658; Auto-pilot Upload</a>
            <a href="<?= $apBase ?>&fase=verificar" class="btn" style="background:linear-gradient(135deg,#7c3aed,#a855f7);" <?= !$puedeVerify?'onclick="return false;" style="opacity:0.4;pointer-events:none;"':'' ?>>&#9658; Auto-pilot Verificar</a>
            <a href="<?= $apBase ?>&fase=registrar" class="btn" style="background:linear-gradient(135deg,#7c3aed,#16a34a);" <?= !$puedeRegistrar?'onclick="return false;" style="opacity:0.4;pointer-events:none;"':'' ?>>&#9658; Auto-pilot Registrar</a>
            <span style="color:#94a3b8;font-size:0.8em;align-self:center;">Cada lote se ejecuta automaticamente. Puede detener en cualquier momento.</span>
        </div>
    </div>
    <div style="margin-top:10px;">
        <a href="?fase=reset" class="btn btn-gray" style="font-size:0.78em;" onclick="return confirm('Reiniciar TODA la base de datos de tracking?\\n\\nEsto NO afecta archivos ni Wasabi.')">Reiniciar tracking DB</a>
    </div>
</div>

<?php if (!$tieneCreds && $stats['total'] === 0): ?>
<div class="warn-box">
    <strong>Paso 1:</strong> Ingrese las credenciales Wasabi arriba y haga click en "Test Conexion" para verificar.<br>
    <strong>Paso 2:</strong> Ejecute "1. Inventario" para escanear todos los subdominios (o uno especifico en el campo "Subdominio").<br>
    <strong>Paso 3:</strong> Ejecute cada fase en orden. Puede ejecutar lotes parciales y continuar.
</div>
<?php endif; ?>

<?php if ($puedeTruncar && $stats['errors'] === 0): ?>
<div class="ok-box">
    <strong>&#9989; LISTO PARA TRUNCAR:</strong> Todos los archivos han sido subidos, verificados y registrados. Puede proceder con la fase 5 (truncar) de forma segura.
</div>
<?php elseif ($puedeTruncar && $stats['errors'] > 0): ?>
<div class="warn-box">
    <strong>&#9888; ADVERTENCIA:</strong> Hay <?= $stats['errors'] ?> archivos con errores. Los archivos con errores NO seran truncados. Revise los errores antes de proceder.
</div>
<?php endif; ?>

<!-- Tabla de subdominios -->
<?php if (!empty($subdominios)): ?>
<div class="section">
    <h2>&#128203; Progreso por subdominio (<?= count($subdominios) ?>)</h2>
    <div class="filter-bar">
        <input type="text" id="filtro" placeholder="Buscar subdominio..." oninput="filtrar()">
    </div>
    <table id="tabla">
        <thead>
            <tr>
                <th>#</th>
                <th>Subdominio</th>
                <th>Archivos</th>
                <th>Tamaño</th>
                <th>Subidos</th>
                <th>Verificados</th>
                <th>Registrados</th>
                <th>Truncados</th>
                <th>Errores</th>
                <th>Progreso</th>
                <th>Acciones</th>
            </tr>
        </thead>
        <tbody>
        <?php $n = 0; foreach ($subdominios as $sp): $n++;
            $subPct = $sp['total_files'] > 0 ? round($sp['truncated'] / $sp['total_files'] * 100, 0) : 0;
            $uploadPct = $sp['total_files'] > 0 ? round($sp['uploaded'] / $sp['total_files'] * 100, 0) : 0;
        ?>
            <tr data-name="<?= htmlspecialchars(strtolower($sp['subdomain'])) ?>">
                <td><?= $n ?></td>
                <td><strong><a href="?sub=<?= urlencode($sp['subdomain']) ?>" style="color:#38bdf8;text-decoration:none;"><?= htmlspecialchars($sp['subdomain']) ?></a></strong></td>
                <td><?= number_format($sp['total_files']) ?></td>
                <td><?= formatSize($sp['total_size']) ?></td>
                <td><?= $sp['uploaded'] ?></td>
                <td><?= $sp['verified'] ?></td>
                <td><?= $sp['registered'] ?></td>
                <td><?= $sp['truncated'] ?></td>
                <td><?= $sp['errors'] > 0 ? '<span class="red">' . $sp['errors'] . '</span>' : '0' ?></td>
                <td style="min-width:100px;">
                    <div class="bar"><div class="bar-fill bar-green" style="width:<?= $subPct ?>%"></div></div>
                    <span style="font-size:0.75em;color:#94a3b8;"><?= $subPct ?>%</span>
                </td>
                <td>
                    <a href="?sub=<?= urlencode($sp['subdomain']) ?>" style="font-size:0.8em;color:#38bdf8;">detalle</a>
                </td>
            </tr>
        <?php endforeach; ?>
        </tbody>
    </table>
</div>
<?php endif; ?>

<!-- Archivos grandes omitidos -->
<?php if (!empty($archivosGrandes)): ?>
<div class="section">
    <h2 style="color:#fbbf24;">&#128230; Archivos grandes omitidos (<?= count($archivosGrandes) ?><?= $stats['skipped'] > 100 ? ' de ' . $stats['skipped'] : '' ?>) — <?= formatSize($stats['skipped_size']) ?></h2>
    <p style="color:#94a3b8;font-size:0.82em;margin-bottom:10px;">Estos archivos exceden el limite de <?= $maxFileSizeMB ?> MB y no seran subidos a Wasabi. Puede aumentar el limite en "Max MB/archivo" y re-ejecutar el inventario.</p>
    <table>
        <thead><tr><th>Subdominio</th><th>Archivo</th><th>Tamaño</th><th>Motivo</th></tr></thead>
        <tbody>
        <?php foreach ($archivosGrandes as $ag): ?>
            <tr style="background:#1c1917;">
                <td><?= htmlspecialchars($ag['subdomain']) ?></td>
                <td style="word-break:break-all;max-width:300px;"><?= htmlspecialchars(basename($ag['rel_path'])) ?></td>
                <td style="color:#fbbf24;font-weight:600;"><?= formatSize($ag['file_size']) ?></td>
                <td style="font-size:0.82em;color:#94a3b8;"><?= htmlspecialchars($ag['error_msg']) ?></td>
            </tr>
        <?php endforeach; ?>
        </tbody>
    </table>
</div>
<?php endif; ?>

<!-- Errores recientes -->
<?php if (!empty($erroresRecientes)): ?>
<div class="section">
    <h2 style="color:#f87171;">&#9888; Errores recientes (<?= count($erroresRecientes) ?>)</h2>
    <table>
        <thead><tr><th>Subdominio</th><th>Archivo</th><th>Error</th></tr></thead>
        <tbody>
        <?php foreach ($erroresRecientes as $e): ?>
            <tr class="error-row">
                <td><?= htmlspecialchars($e['subdomain']) ?></td>
                <td style="word-break:break-all;max-width:300px;"><?= htmlspecialchars(basename($e['rel_path'])) ?></td>
                <td style="color:#f87171;font-size:0.82em;"><?= htmlspecialchars($e['error_msg']) ?></td>
            </tr>
        <?php endforeach; ?>
        </tbody>
    </table>
</div>
<?php endif; ?>

</div><!-- /container -->

<script>
function filtrar() {
    var texto = document.getElementById('filtro').value.toLowerCase();
    document.querySelectorAll('#tabla tbody tr').forEach(function(row) {
        var name = row.dataset.name || '';
        row.style.display = (!texto || name.indexOf(texto) !== -1) ? '' : 'none';
    });
}
</script>
</body>
</html>
