<?php
/**
 * CLI: Sube archivos locales restantes (con contenido) a Wasabi y los trunca.
 *
 * Proceso:
 *   1. Recorre todos los wikis buscando archivos con contenido en data/media/
 *   2. Verifica si ya existe en Wasabi (via listObjects)
 *   3. Si no existe, lo sube a Wasabi
 *   4. Registra en la SQLite del wiki
 *   5. Trunca el archivo local a 0 bytes
 *
 * Uso:
 *   screen -S wasabi_remaining php cli_upload_remaining.php
 *
 * @author NICC
 * @date   2026-03-22
 */

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

// =====================================================================
//  CONFIGURACIÓN
// =====================================================================

$BASE_DIR = '/home/systeng3/public_html';

// Leer credenciales Wasabi
$deployerDb = $BASE_DIR . '/prueba/lib/plugins/siedeployer/diagnosticos/migracion_wasabi_estado.sqlite3';
$WASABI_CREDS = null;

if (file_exists($deployerDb)) {
    try {
        $pdo = new PDO('sqlite:' . $deployerDb);
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $creds = [];
        $q = $pdo->query("SELECT key, value FROM migration_config WHERE key LIKE 'wasabi_%'");
        while ($r = $q->fetch(PDO::FETCH_ASSOC)) {
            $k = str_replace('wasabi_', '', $r['key']);
            $creds[$k] = $r['value'];
        }
        if (!empty($creds['key']) && !empty($creds['secret'])) {
            $WASABI_CREDS = [
                'key'      => $creds['key'],
                'secret'   => $creds['secret'],
                'bucket'   => $creds['bucket'] ?? 'nicc-archivos',
                'region'   => $creds['region'] ?? 'us-east-1',
                'endpoint' => $creds['endpoint'] ?? 's3.wasabisys.com',
            ];
        }
    } catch (Exception $e) {
        // silencio
    }
}

if (!$WASABI_CREDS) {
    fwrite(STDERR, "ERROR: No se pudieron leer credenciales Wasabi.\n");
    exit(1);
}

// MIME map
$MIME_MAP = [
    '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',
    'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
    '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',  'odt'  => 'application/vnd.oasis.opendocument.text',
    '7z'   => 'application/x-7z-compressed',
    'tar'  => 'application/x-tar',
    'rtf'  => 'application/rtf',
];

// =====================================================================
//  CLIENTE S3
// =====================================================================

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

    public function __construct($key, $secret, $bucket, $region, $endpoint) {
        $this->key      = $key;
        $this->secret   = $secret;
        $this->bucket   = $bucket;
        $this->region   = $region;
        $this->endpoint = $endpoint;
    }

    public function putObject($localPath, $wasabiKey, $mimeType = 'application/octet-stream') {
        if (!file_exists($localPath) || !is_readable($localPath)) {
            return ['ok' => false, 'error' => 'No se pudo leer archivo local'];
        }

        $fileSize = filesize($localPath);
        $contentHash = hash_file('sha256', $localPath);
        if ($contentHash === false) {
            return ['ok' => false, 'error' => 'No se pudo calcular SHA256'];
        }

        $host = $this->bucket . '.' . $this->endpoint;
        $encodedKey = implode('/', array_map('rawurlencode', explode('/', $wasabiKey)));
        $url  = 'https://' . $host . '/' . $encodedKey;
        $date = gmdate('Ymd\THis\Z');
        $dateShort = gmdate('Ymd');

        $canonicalUri = '/' . implode('/', array_map('rawurlencode', explode('/', $wasabiKey)));

        $headers = [
            'Content-Length'       => $fileSize,
            'Content-Type'         => $mimeType,
            'Host'                 => $host,
            'x-amz-content-sha256' => $contentHash,
            'x-amz-date'           => $date,
        ];

        $authHeader = $this->signV4('PUT', $canonicalUri, '', $headers, $contentHash, $dateShort, $date);
        $headers['Authorization'] = $authHeader;

        $curlHeaders = [];
        foreach ($headers as $k => $v) $curlHeaders[] = "$k: $v";

        $fh = fopen($localPath, 'rb');
        if (!$fh) {
            return ['ok' => false, 'error' => 'No se pudo abrir archivo'];
        }

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_PUT            => true,
            CURLOPT_INFILE         => $fh,
            CURLOPT_INFILESIZE     => $fileSize,
            CURLOPT_HTTPHEADER     => $curlHeaders,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 600,
            CURLOPT_SSL_VERIFYPEER => false,
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);
        fclose($fh);

        if ($curlError) return ['ok' => false, 'error' => 'curl: ' . $curlError];
        if ($httpCode >= 200 && $httpCode < 300) return ['ok' => true, 'error' => ''];

        $err = '';
        if (preg_match('/<Message>(.*?)<\/Message>/s', $response, $m)) $err = $m[1];
        return ['ok' => false, 'error' => "HTTP $httpCode: $err"];
    }

    public function headObject($key) {
        $host = $this->bucket . '.' . $this->endpoint;
        $encodedKey = implode('/', array_map('rawurlencode', explode('/', $key)));
        $url = 'https://' . $host . '/' . $encodedKey;
        $date = gmdate('Ymd\THis\Z');
        $dateShort = gmdate('Ymd');
        $contentHash = hash('sha256', '');

        $canonicalUri = '/' . implode('/', array_map('rawurlencode', explode('/', $key)));

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

        $authHeader = $this->signV4('HEAD', $canonicalUri, '', $headers, $contentHash, $dateShort, $date);
        $headers['Authorization'] = $authHeader;

        $curlHeaders = [];
        foreach ($headers as $k => $v) $curlHeaders[] = "$k: $v";

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST  => 'HEAD',
            CURLOPT_HTTPHEADER     => $curlHeaders,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_NOBODY         => true,
            CURLOPT_HEADER         => true,
            CURLOPT_TIMEOUT        => 15,
            CURLOPT_SSL_VERIFYPEER => false,
        ]);

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

        if ($httpCode === 200) {
            return ['exists' => true, 'size' => ($size !== false && $size >= 0) ? (int)$size : 0];
        }
        return ['exists' => false, 'size' => 0];
    }

    private function signV4($method, $uri, $queryString, $headers, $payloadHash, $dateShort, $dateLong) {
        $scope = "$dateShort/{$this->region}/s3/aws4_request";
        $canonicalHeaders = '';
        $signedHeadersList = [];
        $sorted = [];
        foreach ($headers as $k => $v) $sorted[strtolower($k)] = trim($v);
        ksort($sorted);
        foreach ($sorted as $k => $v) {
            $canonicalHeaders .= "$k:$v\n";
            $signedHeadersList[] = $k;
        }
        $signedHeaders = implode(';', $signedHeadersList);

        $canonicalRequest = "$method\n$uri\n$queryString\n$canonicalHeaders\n$signedHeaders\n$payloadHash";
        $stringToSign = "AWS4-HMAC-SHA256\n$dateLong\n$scope\n" . hash('sha256', $canonicalRequest);

        $kDate    = hash_hmac('sha256', $dateShort, 'AWS4' . $this->secret, true);
        $kRegion  = hash_hmac('sha256', $this->region, $kDate, true);
        $kService = hash_hmac('sha256', 's3', $kRegion, true);
        $kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);

        $signature = hash_hmac('sha256', $stringToSign, $kSigning);
        return "AWS4-HMAC-SHA256 Credential={$this->key}/$scope, SignedHeaders=$signedHeaders, Signature=$signature";
    }
}

// =====================================================================
//  FUNCIONES
// =====================================================================

function detectWikis($baseDir) {
    $wikis = [];
    $baseDir = rtrim($baseDir, '/');
    $handle = @opendir($baseDir);
    if (!$handle) return $wikis;
    while (($entry = readdir($handle)) !== false) {
        if ($entry[0] === '.') continue;
        $path = $baseDir . '/' . $entry;
        if (!is_dir($path)) continue;
        if (file_exists($path . '/doku.php')) {
            $wikis[] = ['id' => $entry, 'path' => $path];
        }
    }
    closedir($handle);
    sort($wikis);
    return $wikis;
}

function getWikiSqlite($wikiPath) {
    $dbFile = $wikiPath . '/data/meta/wasabisync.sqlite3';
    if (!file_exists($dbFile)) return null;
    try {
        $pdo = new PDO('sqlite:' . $dbFile);
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        return $pdo;
    } catch (Exception $e) {
        return null;
    }
}

function scanMediaWithContent($dir, $ns = '') {
    $files = [];
    $dir = rtrim($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, scanMediaWithContent($fullPath, $subNs));
        } elseif (is_file($fullPath)) {
            $size = filesize($fullPath);
            if ($size > 0) {
                $mediaId = $ns ? ($ns . ':' . $entry) : $entry;
                $files[] = ['path' => $fullPath, 'media_id' => $mediaId, 'size' => $size];
            }
        }
    }
    closedir($handle);
    return $files;
}

function formatSize($bytes) {
    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';
}

// =====================================================================
//  MAIN
// =====================================================================

$wikis = detectWikis($BASE_DIR);
$s3 = new WasabiUploader(
    $WASABI_CREDS['key'], $WASABI_CREDS['secret'],
    $WASABI_CREDS['bucket'], $WASABI_CREDS['region'], $WASABI_CREDS['endpoint']
);

echo "╔══════════════════════════════════════════════════╗\n";
echo "║  SUBIR ARCHIVOS RESTANTES A WASABI + TRUNCAR     ║\n";
echo "╠══════════════════════════════════════════════════╣\n";
echo "║  Wikis:  " . count($wikis) . "\n";
echo "║  Fecha:  " . date('Y-m-d H:i:s') . "\n";
echo "╚══════════════════════════════════════════════════╝\n\n";

$grandUploaded = 0;
$grandTruncated = 0;
$grandSkipped = 0;
$grandAlready = 0;
$grandFailed = 0;
$grandFreed = 0;
$startTime = microtime(true);

foreach ($wikis as $wi => $wiki) {
    $wikiId = $wiki['id'];
    $mediaDir = $wiki['path'] . '/data/media';

    if (!is_dir($mediaDir)) continue;

    // Escanear archivos con contenido
    $files = scanMediaWithContent($mediaDir);
    if (empty($files)) continue;

    $pdo = getWikiSqlite($wiki['path']);
    if (!$pdo) {
        echo sprintf("[%s] [%d/%d] %s — ERROR: no SQLite\n", date('H:i:s'), $wi + 1, count($wikis), $wikiId);
        continue;
    }

    echo sprintf("[%s] [%d/%d] %s — %d archivos con contenido... ",
        date('H:i:s'), $wi + 1, count($wikis), $wikiId, count($files));

    $uploaded = 0;
    $truncated = 0;
    $skipped = 0;
    $already = 0;
    $failed = 0;
    $freed = 0;

    foreach ($files as $f) {
        $mediaId = $f['media_id'];
        $wasabiKey = $wikiId . '/' . str_replace(':', '/', $mediaId);

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

        // Verificar si ya existe en Wasabi (HEAD)
        $head = $s3->headObject($wasabiKey);
        if ($head['exists'] && $head['size'] == $f['size']) {
            // Ya existe con mismo tamaño, solo registrar y truncar
            $already++;
        } else if ($head['exists'] && $head['size'] != $f['size']) {
            // Existe pero con tamaño diferente — re-subir
            $result = $s3->putObject($f['path'], $wasabiKey, $mimeType);
            if (!$result['ok']) {
                $failed++;
                fwrite(STDERR, "  [FAIL] $mediaId: " . $result['error'] . "\n");
                continue;
            }
            $uploaded++;
        } else {
            // No existe — subir
            $result = $s3->putObject($f['path'], $wasabiKey, $mimeType);
            if (!$result['ok']) {
                $failed++;
                fwrite(STDERR, "  [FAIL] $mediaId: " . $result['error'] . "\n");
                continue;
            }
            $uploaded++;
        }

        // Verificar con HEAD que realmente llego
        $verify = $s3->headObject($wasabiKey);
        if (!$verify['exists']) {
            $failed++;
            fwrite(STDERR, "  [VERIFY FAIL] $mediaId\n");
            continue;
        }

        // Registrar en SQLite
        $stmt = $pdo->prepare('INSERT OR REPLACE INTO wasabi_files
            (media_id, wasabi_key, file_size, file_mtime, mime_type, uploaded_at)
            VALUES (?, ?, ?, ?, ?, ?)');
        $stmt->execute([$mediaId, $wasabiKey, $f['size'], filemtime($f['path']), $mimeType, date('c')]);

        // 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'];
            }
        }
    }

    echo sprintf("%d subidos, %d ya existian, %d truncados (%s liberados), %d fallos\n",
        $uploaded, $already, $truncated, formatSize($freed), $failed);

    $grandUploaded += $uploaded;
    $grandAlready += $already;
    $grandTruncated += $truncated;
    $grandFreed += $freed;
    $grandFailed += $failed;
}

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

echo "\n╔══════════════════════════════════════════════════╗\n";
echo "║  RESULTADO GLOBAL                                ║\n";
echo "╠══════════════════════════════════════════════════╣\n";
echo "║  Subidos:      " . str_pad(number_format($grandUploaded), 10) . "                     ║\n";
echo "║  Ya existian:  " . str_pad(number_format($grandAlready), 10) . "                     ║\n";
echo "║  Truncados:    " . str_pad(number_format($grandTruncated), 10) . "                     ║\n";
echo "║  Liberado:     " . str_pad(formatSize($grandFreed), 10) . "                     ║\n";
echo "║  Fallos:       " . str_pad(number_format($grandFailed), 10) . "                     ║\n";
echo "║  Tiempo:       " . str_pad($elapsed . 's', 10) . "                     ║\n";
echo "╚══════════════════════════════════════════════════╝\n";
