<?php
/**
 * CLI para Wasabi S3 Sync — ejecutar desde terminal del servidor
 *
 * Uso:
 *   php cli.php rebuild [wiki_path]     → Reconstruye SQLite desde Wasabi (listObjects)
 *   php cli.php stats   [wiki_path]     → Muestra estadísticas de SQLite
 *   php cli.php verify  [wiki_path]     → Verifica integridad (HEAD por archivo con contenido)
 *   php cli.php truncate [wiki_path]    → Trunca archivos locales verificados en Wasabi
 *
 * wiki_path = ruta del DokuWiki (ej: /home/systeng3/public_html/prueba)
 *             Si no se especifica, intenta detectar desde la ubicación del script.
 *
 * Ejemplo:
 *   screen -S wasabi php cli.php rebuild /home/systeng3/public_html/prueba
 *
 * @author NICC
 * @date   2026-03-21
 */

if (php_sapi_name() !== 'cli') {
    die("Solo ejecucion por CLI.\n");
}

// =====================================================================
//  BOOTSTRAP: inicializar DokuWiki desde CLI
// =====================================================================

// Detectar ruta del wiki
$wikiPath = '';
if (isset($argv[2]) && is_dir($argv[2])) {
    $wikiPath = rtrim($argv[2], '/\\');
} else {
    // Subir desde lib/plugins/wasabisync/ hasta la raíz del wiki
    $wikiPath = realpath(__DIR__ . '/../../../..');
}

if (!$wikiPath || !file_exists($wikiPath . '/doku.php')) {
    fwrite(STDERR, "ERROR: No se encontro DokuWiki en: $wikiPath\n");
    fwrite(STDERR, "Uso: php cli.php <comando> [ruta_wiki]\n");
    exit(1);
}

// Definir constantes necesarias antes del bootstrap
if (!defined('DOKU_INC')) define('DOKU_INC', $wikiPath . '/');

// Cargar DokuWiki sin enviar headers HTTP
define('DOKU_DISABLE_GZIP_OUTPUT', 1);
if (!defined('NOSESSION')) define('NOSESSION', 1);

// Suprimir warnings de headers en CLI
@ob_start();
require_once(DOKU_INC . 'inc/init.php');
require_once(DOKU_INC . 'inc/common.php');
require_once(DOKU_INC . 'inc/media.php');
require_once(DOKU_INC . 'inc/auth.php');
@ob_end_clean();

// =====================================================================
//  CARGAR HELPER
// =====================================================================

/** @var helper_plugin_wasabisync $helper */
$helper = plugin_load('helper', 'wasabisync');
if (!$helper) {
    fwrite(STDERR, "ERROR: No se pudo cargar el helper wasabisync.\n");
    fwrite(STDERR, "Verifica que el plugin este instalado en: " . DOKU_INC . "lib/plugins/wasabisync/\n");
    exit(1);
}

$command = $argv[1] ?? '';

switch ($command) {
    case 'rebuild':
        cmdRebuild($helper);
        break;
    case 'stats':
        cmdStats($helper);
        break;
    case 'verify':
        cmdVerify($helper);
        break;
    case 'truncate':
        cmdTruncate($helper);
        break;
    default:
        echo "Wasabi S3 Sync — CLI\n";
        echo "====================\n";
        echo "Uso: php cli.php <comando> [ruta_wiki]\n\n";
        echo "Comandos:\n";
        echo "  rebuild    Reconstruir SQLite desde Wasabi (listObjects)\n";
        echo "  stats      Mostrar estadísticas de SQLite\n";
        echo "  verify     Verificar integridad (HEAD por archivo)\n";
        echo "  truncate   Truncar archivos locales ya en Wasabi\n";
        echo "\nEjemplo:\n";
        echo "  screen -S wasabi php cli.php rebuild /home/systeng3/public_html/prueba\n";
        exit(0);
}

// =====================================================================
//  COMANDOS
// =====================================================================

/**
 * Reconstruye SQLite listando todos los objetos en Wasabi
 */
function cmdRebuild($helper) {
    $wikiId = $helper->getWikiId();
    $prefix = $wikiId . '/';

    echo "╔══════════════════════════════════════════════════╗\n";
    echo "║  REBUILD SQLite desde Wasabi                     ║\n";
    echo "╠══════════════════════════════════════════════════╣\n";
    echo "║  Wiki ID:  $wikiId\n";
    echo "║  Prefijo:  $prefix\n";
    echo "║  Fecha:    " . date('Y-m-d H:i:s') . "\n";
    echo "╚══════════════════════════════════════════════════╝\n\n";

    if (!$helper->isReady()) {
        fwrite(STDERR, "ERROR: Plugin no configurado. Verifique credenciales en conf/local.php\n");
        exit(1);
    }

    // MIME map para detectar tipo desde extensión
    $mimeMap = [
        'pdf'  => 'application/pdf',
        'doc'  => 'application/msword',
        'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'xls'  => 'application/vnd.ms-excel',
        'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        'ppt'  => 'application/vnd.ms-powerpoint',
        'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
        'jpg'  => 'image/jpeg', 'jpeg' => 'image/jpeg',
        'png'  => 'image/png',  'gif'  => 'image/gif',
        'zip'  => 'application/zip',
        'rar'  => 'application/x-rar-compressed',
        'txt'  => 'text/plain', 'csv'  => 'text/csv',
        'html' => 'text/html',  'htm'  => 'text/html',
        'mp4'  => 'video/mp4',  'mp3'  => 'audio/mpeg',
        'webp' => 'image/webp', 'svg'  => 'image/svg+xml',
        'bmp'  => 'image/bmp',  'tiff' => 'image/tiff',
        'odt'  => 'application/vnd.oasis.opendocument.text',
        'ods'  => 'application/vnd.oasis.opendocument.spreadsheet',
        '7z'   => 'application/x-7z-compressed',
    ];

    echo "[" . date('H:i:s') . "] Listando objetos en Wasabi con prefijo '$prefix'...\n";

    $result = $helper->listObjects($prefix);

    if (!$result['ok']) {
        fwrite(STDERR, "ERROR listando Wasabi: " . $result['error'] . "\n");
        exit(1);
    }

    $objects = $result['objects'];
    $total = count($objects);
    echo "[" . date('H:i:s') . "] Encontrados: $total objetos\n\n";

    if ($total === 0) {
        echo "No hay objetos en Wasabi para este wiki. Nada que reconstruir.\n";
        exit(0);
    }

    $registered = 0;
    $skipped = 0;
    $errors = 0;
    $totalSize = 0;
    $startTime = microtime(true);

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

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

        // Convertir key a media_id
        $mediaId = $helper->keyToMediaId($key);
        if (!$mediaId) {
            $skipped++;
            continue;
        }

        // Detectar MIME
        $ext = strtolower(pathinfo($key, PATHINFO_EXTENSION));
        $mimeType = $mimeMap[$ext] ?? 'application/octet-stream';

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

        // Registrar en SQLite
        $ok = $helper->dbRegister($mediaId, $key, $size, $mtime, $mimeType);
        if ($ok) {
            $registered++;
            $totalSize += $size;
        } else {
            $errors++;
            fwrite(STDERR, "  ERROR: $mediaId\n");
        }

        // Progreso cada 1000 archivos
        if (($i + 1) % 1000 === 0 || ($i + 1) === $total) {
            $elapsed = microtime(true) - $startTime;
            $rate = $elapsed > 0 ? round(($i + 1) / $elapsed, 1) : 0;
            echo sprintf("  [%s] %d / %d (%.1f/s) — %s registrados, %s errores\n",
                date('H:i:s'), $i + 1, $total, $rate, number_format($registered), $errors);
        }
    }

    $elapsed = round(microtime(true) - $startTime, 1);

    echo "\n╔══════════════════════════════════════════════════╗\n";
    echo "║  RESULTADO REBUILD                                ║\n";
    echo "╠══════════════════════════════════════════════════╣\n";
    echo "║  Registrados:  " . str_pad(number_format($registered), 12) . "                    ║\n";
    echo "║  Omitidos:     " . str_pad(number_format($skipped), 12) . "                    ║\n";
    echo "║  Errores:      " . str_pad(number_format($errors), 12) . "                    ║\n";
    echo "║  Tamaño total: " . str_pad($helper->formatSize($totalSize), 12) . "                    ║\n";
    echo "║  Tiempo:       " . str_pad($elapsed . 's', 12) . "                    ║\n";
    echo "╚══════════════════════════════════════════════════╝\n";

    // Verificar estado final
    $stats = $helper->dbStats();
    echo "\nSQLite total: " . number_format($stats['count']) . " archivos, " . $helper->formatSize($stats['size']) . "\n";
}

/**
 * Muestra estadísticas de SQLite
 */
function cmdStats($helper) {
    if (!$helper->isReady()) {
        // Aún así mostrar stats de SQLite si la DB existe
        echo "AVISO: Plugin no habilitado o sin credenciales.\n\n";
    }

    $stats = $helper->dbStats();

    echo "╔══════════════════════════════════════════════════╗\n";
    echo "║  WASABISYNC — ESTADÍSTICAS SQLite                ║\n";
    echo "╠══════════════════════════════════════════════════╣\n";
    echo "║  Wiki ID:      " . $helper->getWikiId() . "\n";
    echo "║  Archivos:     " . number_format($stats['count']) . "\n";
    echo "║  Tamaño total: " . $helper->formatSize($stats['size']) . "\n";
    echo "╚══════════════════════════════════════════════════╝\n";

    // Contar archivos locales
    global $conf;
    $mediaDir = $conf['mediadir'] ?? (DOKU_INC . 'data/media');
    if (is_dir($mediaDir)) {
        $local = countLocalMedia($mediaDir);
        echo "\nArchivos locales:\n";
        echo "  Total:         " . number_format($local['total']) . "\n";
        echo "  Con contenido: " . number_format($local['with_content']) . " (" . formatSizeSimple($local['content_size']) . ")\n";
        echo "  Stubs (0 B):   " . number_format($local['stubs']) . "\n";
        echo "  Pendientes:    " . number_format(max(0, $local['with_content'] - $stats['count'])) . "\n";
    }
}

/**
 * Verifica integridad: compara archivos locales con contenido contra Wasabi (HEAD)
 */
function cmdVerify($helper) {
    if (!$helper->isReady()) {
        fwrite(STDERR, "ERROR: Plugin no configurado.\n");
        exit(1);
    }

    global $conf;
    $mediaDir = $conf['mediadir'] ?? (DOKU_INC . 'data/media');

    echo "╔══════════════════════════════════════════════════╗\n";
    echo "║  VERIFICACIÓN DE INTEGRIDAD                      ║\n";
    echo "╠══════════════════════════════════════════════════╣\n";
    echo "║  Wiki ID:  " . $helper->getWikiId() . "\n";
    echo "║  Fecha:    " . date('Y-m-d H:i:s') . "\n";
    echo "╚══════════════════════════════════════════════════╝\n\n";

    echo "Escaneando archivos locales...\n";
    $localFiles = scanMediaRecursive($mediaDir);
    echo "Encontrados: " . count($localFiles) . " archivos\n\n";

    $verified = 0;
    $missingDb = 0;
    $missingS3 = 0;
    $sizeMismatch = 0;
    $errors = 0;
    $stubs = 0;
    $total = count($localFiles);
    $startTime = microtime(true);

    foreach ($localFiles as $i => $f) {
        if ($f['size'] <= 0) {
            $stubs++;
            continue;
        }

        $check = $helper->verifyFileIntegrity($f['media_id'], $f['size']);

        switch ($check['status']) {
            case 'verified':     $verified++; break;
            case 'missing_db':   $missingDb++; break;
            case 'missing_s3':
                $missingS3++;
                fwrite(STDERR, "  [NO-S3] {$f['media_id']}\n");
                break;
            case 'size_mismatch':
                $sizeMismatch++;
                fwrite(STDERR, "  [SIZE!] {$f['media_id']} — {$check['detail']}\n");
                break;
            default:
                $errors++;
                fwrite(STDERR, "  [ERROR] {$f['media_id']} — {$check['detail']}\n");
                break;
        }

        if (($i + 1) % 500 === 0 || ($i + 1) === $total) {
            $elapsed = microtime(true) - $startTime;
            $rate = $elapsed > 0 ? round(($i + 1) / $elapsed, 1) : 0;
            echo sprintf("  [%s] %d / %d (%.1f/s)\n", date('H:i:s'), $i + 1, $total, $rate);
        }
    }

    $safe = ($verified > 0 && $missingS3 === 0 && $sizeMismatch === 0 && $errors === 0);

    echo "\n╔══════════════════════════════════════════════════╗\n";
    echo "║  RESULTADO VERIFICACIÓN                          ║\n";
    echo "╠══════════════════════════════════════════════════╣\n";
    echo "║  Verificados OK:  " . str_pad(number_format($verified), 10) . "                    ║\n";
    echo "║  Stubs (0 B):     " . str_pad(number_format($stubs), 10) . "                    ║\n";
    echo "║  Sin SQLite:      " . str_pad(number_format($missingDb), 10) . "                    ║\n";
    echo "║  NO en Wasabi:    " . str_pad(number_format($missingS3), 10) . "                    ║\n";
    echo "║  Tamaño dif:      " . str_pad(number_format($sizeMismatch), 10) . "                    ║\n";
    echo "║  Errores:         " . str_pad(number_format($errors), 10) . "                    ║\n";
    echo "║                                                    ║\n";
    if ($safe) {
        echo "║  *** SEGURO PARA TRUNCAR ***                     ║\n";
    } else {
        echo "║  *** NO ES SEGURO TRUNCAR ***                    ║\n";
    }
    echo "╚══════════════════════════════════════════════════╝\n";

    exit($safe ? 0 : 1);
}

/**
 * Trunca archivos locales que están verificados en Wasabi + SQLite
 * Cada archivo se verifica individualmente con HEAD antes de truncar
 */
function cmdTruncate($helper) {
    if (!$helper->isReady()) {
        fwrite(STDERR, "ERROR: Plugin no configurado.\n");
        exit(1);
    }

    global $conf;
    $mediaDir = $conf['mediadir'] ?? (DOKU_INC . 'data/media');

    echo "╔══════════════════════════════════════════════════╗\n";
    echo "║  TRUNCAR ARCHIVOS LOCALES (con verificación)     ║\n";
    echo "╠══════════════════════════════════════════════════╣\n";
    echo "║  Wiki ID:  " . $helper->getWikiId() . "\n";
    echo "║  Fecha:    " . date('Y-m-d H:i:s') . "\n";
    echo "╚══════════════════════════════════════════════════╝\n\n";

    echo "Escaneando archivos locales con contenido...\n";
    $localFiles = scanMediaRecursive($mediaDir);
    $withContent = array_filter($localFiles, function($f) { return $f['size'] > 0; });
    echo "Archivos con contenido: " . count($withContent) . "\n\n";

    if (empty($withContent)) {
        echo "No hay archivos para truncar. Todo ya son stubs.\n";
        exit(0);
    }

    $truncated = 0;
    $freed = 0;
    $skippedNoDb = 0;
    $skippedNoS3 = 0;
    $skippedSize = 0;
    $failTruncate = 0;
    $total = count($withContent);
    $startTime = microtime(true);
    $i = 0;

    foreach ($withContent as $f) {
        $i++;

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

        // 2. Verificar en Wasabi con HEAD
        $head = $helper->headObject($record['wasabi_key']);
        if (!$head['exists']) {
            $skippedNoS3++;
            fwrite(STDERR, "  [NO-S3] PROTEGIDO: {$f['media_id']}\n");
            continue;
        }

        // 3. Comparar tamaños
        $dbSize = (int)$record['file_size'];
        $wasabiSize = (int)$head['size'];
        if ($wasabiSize > 0 && $dbSize > 0 && $wasabiSize !== $dbSize) {
            $skippedSize++;
            fwrite(STDERR, sprintf("  [SIZE!] PROTEGIDO: %s (db=%d, wasabi=%d)\n",
                $f['media_id'], $dbSize, $wasabiSize));
            continue;
        }

        // 4. Truncar
        $fh = @fopen($f['path'], 'w');
        if ($fh) {
            ftruncate($fh, 0);
            fclose($fh);
            clearstatcache(true, $f['path']);
            if (filesize($f['path']) === 0) {
                $truncated++;
                $freed += $f['size'];
            } else {
                $failTruncate++;
            }
        } else {
            $failTruncate++;
            fwrite(STDERR, "  [FAIL] No se pudo abrir: {$f['media_id']}\n");
        }

        // Progreso cada 500
        if ($i % 500 === 0 || $i === $total) {
            $elapsed = microtime(true) - $startTime;
            $rate = $elapsed > 0 ? round($i / $elapsed, 1) : 0;
            echo sprintf("  [%s] %d / %d (%.1f/s) — %s truncados, %s liberados\n",
                date('H:i:s'), $i, $total, $rate,
                number_format($truncated), formatSizeSimple($freed));
        }
    }

    $elapsed = round(microtime(true) - $startTime, 1);

    echo "\n╔══════════════════════════════════════════════════╗\n";
    echo "║  RESULTADO TRUNCAR                                ║\n";
    echo "╠══════════════════════════════════════════════════╣\n";
    echo "║  Truncados OK:    " . str_pad(number_format($truncated), 10) . "                    ║\n";
    echo "║  Espacio libre:   " . str_pad(formatSizeSimple($freed), 10) . "                    ║\n";
    echo "║  Sin SQLite:      " . str_pad(number_format($skippedNoDb), 10) . "                    ║\n";
    echo "║  NO en Wasabi:    " . str_pad(number_format($skippedNoS3), 10) . "                    ║\n";
    echo "║  Tamaño dif:      " . str_pad(number_format($skippedSize), 10) . "                    ║\n";
    echo "║  Fallos:          " . str_pad(number_format($failTruncate), 10) . "                    ║\n";
    echo "║  Tiempo:          " . str_pad($elapsed . 's', 10) . "                    ║\n";
    echo "╚══════════════════════════════════════════════════╝\n";

    $problems = $skippedNoS3 + $skippedSize + $failTruncate;
    exit($problems === 0 ? 0 : 1);
}

// =====================================================================
//  UTILIDADES
// =====================================================================

function scanMediaRecursive($dir, $ns = '') {
    $files = [];
    $dir = rtrim(str_replace('\\', '/', $dir), '/');
    $handle = @opendir($dir);
    if (!$handle) return $files;

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

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

        if (is_dir($fullPath)) {
            $subNs = $ns ? ($ns . ':' . $entry) : $entry;
            $files = array_merge($files, scanMediaRecursive($fullPath, $subNs));
        } elseif (is_file($fullPath)) {
            $mediaId = $ns ? ($ns . ':' . $entry) : $entry;
            $files[] = [
                'path'     => $fullPath,
                'media_id' => $mediaId,
                'size'     => filesize($fullPath),
            ];
        }
    }
    closedir($handle);
    return $files;
}

function countLocalMedia($dir) {
    $result = ['total' => 0, 'with_content' => 0, 'stubs' => 0, 'content_size' => 0];
    _countRecursive($dir, $result);
    return $result;
}

function _countRecursive($dir, &$result) {
    $dir = rtrim(str_replace('\\', '/', $dir), '/');
    $handle = @opendir($dir);
    if (!$handle) return;
    while (($entry = readdir($handle)) !== false) {
        if ($entry[0] === '.') continue;
        if ($entry === '_dummy') continue;
        $fullPath = $dir . '/' . $entry;
        if (is_dir($fullPath)) {
            _countRecursive($fullPath, $result);
        } elseif (is_file($fullPath)) {
            $result['total']++;
            $size = filesize($fullPath);
            if ($size > 0) {
                $result['with_content']++;
                $result['content_size'] += $size;
            } else {
                $result['stubs']++;
            }
        }
    }
    closedir($handle);
}

function formatSizeSimple($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';
}
