<?php
declare(strict_types=1);

require_once dirname(__DIR__) . '/common/config.php';
require_once dirname(__DIR__) . '/common/plugin_store.php';

function cli_err(string $message): void
{
    fwrite(STDERR, $message . PHP_EOL);
}

function cli_out(string $message): void
{
    fwrite(STDOUT, $message . PHP_EOL);
}

function cli_json_canonicalize($value)
{
    if (is_array($value)) {
        $isList = array_keys($value) === range(0, count($value) - 1);
        if ($isList) {
            foreach ($value as $idx => $item) {
                $value[$idx] = cli_json_canonicalize($item);
            }
            return $value;
        }

        ksort($value, SORT_STRING);
        foreach ($value as $key => $item) {
            $value[$key] = cli_json_canonicalize($item);
        }
    }

    return $value;
}

function cli_package_normalize_content(string $relativePath, string $content): ?string
{
    $relativePath = str_replace('\\', '/', ltrim($relativePath, '/'));

    if (basename($relativePath) === '.htaccess') {
        return null;
    }

    if ($relativePath === 'plugin.json') {
        $data = json_decode($content, true);
        if (is_array($data)) {
            unset($data['version'], $data['published_at']);
            $data = cli_json_canonicalize($data);
            return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }
    }

    if (preg_match('~^apps/[^/]+/app\.json$~', $relativePath)) {
        $data = json_decode($content, true);
        if (is_array($data)) {
            unset($data['version'], $data['updated_at']);
            $data = cli_json_canonicalize($data);
            return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }
    }

    if ($relativePath === 'README.md') {
        return (string) preg_replace('/\(\d+\.\d+\.\d+\)/', '(__VERSION__)', $content);
    }

    if (preg_match('~^install/(?:install|installer|uninstall|uninstaller)\.php$~', $relativePath)) {
        return (string) preg_replace('/("version"\s*:\s*")([^"]+)(")/', '$1__VERSION__$3', $content);
    }

    return $content;
}

function cli_package_root_dir(string $dir): string
{
    $dir = rtrim($dir, '/\\');
    if (is_file($dir . '/plugin.json')) {
        return $dir;
    }

    $items = @scandir($dir) ?: [];
    $dirs = [];
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') {
            continue;
        }

        $full = $dir . DIRECTORY_SEPARATOR . $item;
        if (is_dir($full)) {
            $dirs[] = $full;
        }
    }

    if (count($dirs) === 1 && is_file($dirs[0] . '/plugin.json')) {
        return $dirs[0];
    }

    return $dir;
}

function cli_package_fingerprint(string $rootDir, array &$errors): ?array
{
    if (!is_dir($rootDir)) {
        $errors[] = 'No existe el directorio del paquete a comparar.';
        return null;
    }

    $rootDir = cli_package_root_dir($rootDir);
    if (!is_file($rootDir . '/plugin.json')) {
        $errors[] = 'El paquete a comparar no contiene plugin.json.';
        return null;
    }

    $files = [];
    $rii = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($rootDir, FilesystemIterator::SKIP_DOTS)
    );

    foreach ($rii as $file) {
        if (!$file->isFile()) {
            continue;
        }

        $path = $file->getPathname();
        $rel = str_replace('\\', '/', substr($path, strlen($rootDir) + 1));
        $raw = @file_get_contents($path);
        if ($raw === false) {
            $errors[] = 'No se pudo leer para comparar: ' . $rel;
            return null;
        }

        $normalized = cli_package_normalize_content($rel, $raw);
        if ($normalized === null) {
            continue;
        }

        $files[$rel] = hash('sha256', $normalized);
    }

    ksort($files, SORT_STRING);

    return [
        'hash' => hash('sha256', json_encode($files, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)),
        'files' => $files,
    ];
}

function cli_remote_store_plugin(string $pluginId, array $store): ?array
{
    $list = $store['plugins'] ?? $store;
    if (!is_array($list)) {
        return null;
    }

    foreach ($list as $item) {
        if (is_array($item) && (string) ($item['id'] ?? '') === $pluginId) {
            return $item;
        }
    }

    return null;
}

function cli_store_same_package(string $pluginId, string $localRoot, array $store, array &$errors): ?array
{
    $remote = cli_remote_store_plugin($pluginId, $store);
    if (!is_array($remote)) {
        return null;
    }

    $zipUrl = trim((string) ($remote['zip'] ?? ''));
    if ($zipUrl === '') {
        return null;
    }

    $zipUrl = plugin_store_join_url(plugin_store_base_url(), $zipUrl);
    $sha256 = (string) ($remote['sha256'] ?? '');
    $tmpZip = cli_download_remote_to_tmp($zipUrl, $sha256, $errors);
    if (!$tmpZip) {
        return null;
    }

    $tmpDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'plugin_store_cmp_' . bin2hex(random_bytes(6));

    try {
        if (!cli_extract_zip($tmpZip, $tmpDir, $errors)) {
            return null;
        }

        $localFp = cli_package_fingerprint($localRoot, $errors);
        $remoteFp = cli_package_fingerprint($tmpDir, $errors);
        if (!is_array($localFp) || !is_array($remoteFp)) {
            return null;
        }

        return [
            'same' => hash_equals((string) $localFp['hash'], (string) $remoteFp['hash']),
            'remote' => $remote,
            'local_hash' => (string) $localFp['hash'],
            'remote_hash' => (string) $remoteFp['hash'],
        ];
    } finally {
        @unlink($tmpZip);
        plugin_rrmdir($tmpDir);
    }
}

function cli_publish_base_url(): string
{
    if (defined('PLUGIN_STORE_BASE_URL') && PLUGIN_STORE_BASE_URL) {
        return rtrim((string) PLUGIN_STORE_BASE_URL, '/') . '/';
    }

    if (defined('PLUGIN_STORE_PUBLISH_URL') && PLUGIN_STORE_PUBLISH_URL) {
        $url = (string) PLUGIN_STORE_PUBLISH_URL;
        $suffix = '/api/publish-core-update.php';
        if (str_ends_with($url, $suffix)) {
            return substr($url, 0, -strlen($suffix)) . '/';
        }
    }

    return plugin_store_base_url();
}

function cli_publish_token(): string
{
    if (defined('PLUGIN_STORE_PUBLISH_TOKEN') && PLUGIN_STORE_PUBLISH_TOKEN) {
        return (string) PLUGIN_STORE_PUBLISH_TOKEN;
    }

    return '';
}

function cli_shell_exec_available(): bool
{
    return function_exists('shell_exec') && stripos((string) ini_get('disable_functions'), 'shell_exec') === false;
}

function cli_powershell_quote(string $value): string
{
    return "'" . str_replace("'", "''", $value) . "'";
}

function cli_http_download_via_fopen(string $url, string $destPath, int $timeout, array &$errors): bool
{
    $scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
    if (!ini_get('allow_url_fopen')) {
        return false;
    }
    if ($scheme === 'https' && !extension_loaded('openssl')) {
        return false;
    }

    $ctx = stream_context_create([
        'http' => [
            'timeout' => $timeout,
            'user_agent' => 'CSIF-Intranet-PluginManager/1.1.1',
            'follow_location' => 1,
        ],
        'ssl' => [
            'verify_peer' => true,
            'verify_peer_name' => true,
        ],
    ]);

    $raw = @file_get_contents($url, false, $ctx);
    if ($raw === false || $raw === '') {
        return false;
    }

    if (@file_put_contents($destPath, $raw) === false) {
        $errors[] = 'No se pudo escribir el fichero temporal: ' . $destPath;
        return false;
    }

    return true;
}

function cli_http_download_via_curl_binary(string $url, string $destPath, int $timeout, array &$errors): bool
{
    if (!cli_shell_exec_available()) {
        return false;
    }

    $version = @shell_exec('curl.exe --version 2>NUL');
    if (!is_string($version) || trim($version) === '') {
        return false;
    }

    $cmd = 'curl.exe --fail --silent --show-error --location --max-time ' . (int) $timeout
        . ' --output ' . escapeshellarg($destPath)
        . ' ' . escapeshellarg($url);
    exec($cmd, $out, $rc);

    if ($rc !== 0 || !is_file($destPath) || filesize($destPath) <= 0) {
        @unlink($destPath);
        $errors[] = 'No se pudo descargar mediante curl.exe.';
        return false;
    }

    return true;
}

function cli_http_download_via_powershell(string $url, string $destPath, int $timeout, array &$errors): bool
{
    if (DIRECTORY_SEPARATOR !== '\\' || !cli_shell_exec_available()) {
        return false;
    }

    $probe = @shell_exec('powershell -NoProfile -Command "$PSVersionTable.PSVersion.ToString()"');
    if (!is_string($probe) || trim($probe) === '') {
        return false;
    }

    $psCommand = "\$ProgressPreference = 'SilentlyContinue';"
        . ' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;'
        . ' Invoke-WebRequest -UseBasicParsing -TimeoutSec ' . (int) $timeout
        . ' -Uri ' . cli_powershell_quote($url)
        . ' -OutFile ' . cli_powershell_quote($destPath) . ';';

    $cmd = 'powershell -NoProfile -Command ' . escapeshellarg($psCommand);
    exec($cmd, $out, $rc);

    if ($rc !== 0 || !is_file($destPath) || filesize($destPath) <= 0) {
        @unlink($destPath);
        $errors[] = 'No se pudo descargar mediante PowerShell.';
        return false;
    }

    return true;
}

function cli_http_download(string $url, string $destPath, int $timeout, array &$errors): bool
{
    if (cli_http_download_via_fopen($url, $destPath, $timeout, $errors)) {
        return true;
    }
    if (cli_http_download_via_curl_binary($url, $destPath, $timeout, $errors)) {
        return true;
    }
    if (cli_http_download_via_powershell($url, $destPath, $timeout, $errors)) {
        return true;
    }

    return false;
}

function cli_fetch_store_index(array &$errors): ?array
{
    $url = rtrim(plugin_store_base_url(), '/') . '/index.php?format=json';
    $tmp = tempnam(sys_get_temp_dir(), 'plugin_idx_');
    if (!$tmp) {
        $errors[] = 'No se pudo crear fichero temporal para el catalogo del store.';
        return null;
    }

    $downloadErrors = [];
    $ok = cli_http_download($url, $tmp, 12, $downloadErrors);
    $raw = $ok ? (string) @file_get_contents($tmp) : '';
    @unlink($tmp);

    if (!$ok || trim($raw) === '') {
        foreach ($downloadErrors as $error) {
            $errors[] = $error;
        }
        $errors[] = 'No se pudo leer el catalogo del store: ' . $url;
        return null;
    }

    $data = json_decode($raw, true);
    if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
        $errors[] = 'El catalogo del store no es JSON valido: ' . $url;
        return null;
    }

    return $data;
}

function cli_download_remote_to_tmp(string $url, string $sha256Expected, array &$errors, bool $allowShaMismatch = true): ?string
{
    $tmpBase = tempnam(sys_get_temp_dir(), 'plugin_zip_');
    if (!$tmpBase) {
        $errors[] = 'No se pudo crear fichero temporal.';
        return null;
    }
    $tmp = $tmpBase . '.zip';
    @unlink($tmp);
    if (!@rename($tmpBase, $tmp)) {
        $tmp = $tmpBase;
    }

    $downloadErrors = [];
    if (!cli_http_download($url, $tmp, 30, $downloadErrors)) {
        @unlink($tmp);
        foreach ($downloadErrors as $error) {
            $errors[] = $error;
        }
        $errors[] = 'No se pudo descargar el ZIP: ' . $url;
        return null;
    }

    if ($sha256Expected !== '') {
        if (!is_file($tmp)) {
            $errors[] = 'El ZIP descargado no existe tras la descarga: ' . $tmp;
            return null;
        }
        $hash = strtolower((string) hash_file('sha256', $tmp));
        $expected = strtolower(trim($sha256Expected));
        $hashMatches = hash_equals($expected, $hash);

        if (
            !$hashMatches
            && preg_match('/^[0-9a-f]{63}$/', $expected)
            && strlen($hash) === 64
            && hash_equals($expected, substr($hash, 1))
        ) {
            $hashMatches = true;
        }

        if (!$hashMatches && $allowShaMismatch) {
            $errors[] = 'Aviso: SHA256 remoto inconsistente para ' . $url . '. Se continua solo para comparacion.';
        }

        if (!$hashMatches && !$allowShaMismatch) {
            @unlink($tmp);
            $errors[] = 'SHA256 no coincide para ' . $url;
            return null;
        }
    }

    return $tmp;
}

function cli_extract_zip(string $zipPath, string $destDir, array &$errors): bool
{
    if (class_exists('ZipArchive')) {
        $zip = new ZipArchive();
        if ($zip->open($zipPath) !== true) {
            $errors[] = 'No se pudo abrir ZIP.';
            return false;
        }
        @mkdir($destDir, 0775, true);
        $ok = $zip->extractTo($destDir);
        $zip->close();
        if (!$ok) {
            $errors[] = 'No se pudo descomprimir el ZIP con ZipArchive.';
            return false;
        }
        return true;
    }

    if (DIRECTORY_SEPARATOR === '\\' && cli_shell_exec_available()) {
        $probe = @shell_exec('powershell -NoProfile -Command "$PSVersionTable.PSVersion.ToString()"');
        if (is_string($probe) && trim($probe) !== '') {
            @mkdir($destDir, 0775, true);
            $psCommand = 'Expand-Archive -LiteralPath ' . cli_powershell_quote($zipPath)
                . ' -DestinationPath ' . cli_powershell_quote($destDir) . ' -Force;';
            $cmd = 'powershell -NoProfile -Command ' . escapeshellarg($psCommand);
            exec($cmd, $out, $rc);
            if ($rc === 0) {
                return true;
            }
        }
    }

    if (cli_shell_exec_available()) {
        $probe = @shell_exec('tar --version 2>NUL');
        if (is_string($probe) && trim($probe) !== '') {
            @mkdir($destDir, 0775, true);
            $cmd = 'tar -xf ' . escapeshellarg($zipPath) . ' -C ' . escapeshellarg($destDir);
            exec($cmd, $out, $rc);
            if ($rc === 0) {
                return true;
            }
        }
    }

    $errors[] = 'No hay ningun extractor ZIP disponible en este entorno.';
    return false;
}

function cli_zip_dir(string $srcDir, string $zipFile, array &$errors): bool
{
    if (class_exists('ZipArchive')) {
        $zip = new ZipArchive();
        if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            $errors[] = 'No se pudo crear ZIP: ' . $zipFile;
            return false;
        }

        $srcDir = rtrim($srcDir, '/\\');
        $rii = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($srcDir, FilesystemIterator::SKIP_DOTS)
        );

        foreach ($rii as $file) {
            if ($file->isDir()) {
                continue;
            }

            $path = $file->getPathname();
            $rel = substr($path, strlen($srcDir) + 1);
            $rel = str_replace('\\', '/', $rel);
            if (strpos($rel, '..') !== false) {
                continue;
            }
            if (basename($rel) === '.htaccess') {
                continue;
            }
            $zip->addFile($path, $rel);
        }

        $zip->close();
        return true;
    }

    if (DIRECTORY_SEPARATOR === '\\' && cli_shell_exec_available()) {
        $probe = @shell_exec('powershell -NoProfile -Command "$PSVersionTable.PSVersion.ToString()"');
        if (is_string($probe) && trim($probe) !== '') {
            @unlink($zipFile);
            $psCommand = 'Compress-Archive -Path ' . cli_powershell_quote(rtrim($srcDir, '/\\') . DIRECTORY_SEPARATOR . '*')
                . ' -DestinationPath ' . cli_powershell_quote($zipFile) . ' -Force;';
            $cmd = 'powershell -NoProfile -Command ' . escapeshellarg($psCommand);
            exec($cmd, $out, $rc);
            if ($rc === 0 && is_file($zipFile) && filesize($zipFile) > 0) {
                return true;
            }
        }
    }

    if (cli_shell_exec_available()) {
        $probe = @shell_exec('tar --version 2>NUL');
        if (is_string($probe) && trim($probe) !== '') {
            @unlink($zipFile);
            $cmd = 'tar -a -c -f ' . escapeshellarg($zipFile)
                . ' -C ' . escapeshellarg($srcDir) . ' .';
            exec($cmd, $out, $rc);
            if ($rc === 0 && is_file($zipFile) && filesize($zipFile) > 0) {
                return true;
            }
        }
    }

    $errors[] = 'No se pudo crear ZIP en este entorno.';
    return false;
}

function cli_publish_zip(string $zipFile, array &$errors): ?array
{
    $token = cli_publish_token();
    if ($token === '') {
        $errors[] = 'Falta PLUGIN_STORE_PUBLISH_TOKEN en common/config.php, config/secrets.local.php o variables de entorno.';
        return null;
    }

    if (!is_file($zipFile) || filesize($zipFile) < 10) {
        $errors[] = 'ZIP no encontrado o vacio: ' . $zipFile;
        return null;
    }

    $url = cli_publish_base_url() . 'api/publish.php';
    if (DIRECTORY_SEPARATOR === '\\' && cli_shell_exec_available()) {
        $version = @shell_exec('curl.exe --version 2>NUL');
        if (is_string($version) && trim($version) !== '') {
            $cmd = 'curl.exe --fail --silent --show-error --location --max-time 180'
                . ' -H ' . escapeshellarg('X-Plugin-Token: ' . $token)
                . ' -H ' . escapeshellarg('Expect:')
                . ' -F ' . escapeshellarg('file=@' . $zipFile . ';type=application/zip')
                . ' -F ' . escapeshellarg('force=1')
                . ' ' . escapeshellarg($url) . ' 2>&1';
            exec($cmd, $lines, $rc);
            $resp = implode("\n", $lines);
            $http = $rc === 0 ? 200 : 0;
            if ($rc !== 0 || trim($resp) === '') {
                $errors[] = 'Error curl.exe al publicar: ' . ($resp !== '' ? $resp : 'sin respuesta');
                return null;
            }
        } elseif (function_exists('curl_init')) {
            $ch = curl_init($url);

            $post = [
                'file' => new CURLFile($zipFile, 'application/zip', basename($zipFile)),
                'force' => '1',
            ];
            $headers = [
                'X-Plugin-Token: ' . $token,
                'Expect:',
            ];

            curl_setopt_array($ch, [
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => $post,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => 180,
                CURLOPT_HTTPHEADER => $headers,
                CURLOPT_SSL_VERIFYPEER => true,
                CURLOPT_SSL_VERIFYHOST => 2,
                CURLOPT_SAFE_UPLOAD => true,
            ]);

            $resp = curl_exec($ch);
            $http = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $err = curl_error($ch);
            curl_close($ch);

            if ($resp === false) {
                $errors[] = 'Error cURL: ' . $err;
                return null;
            }
        } else {
            $errors[] = 'Falta PHP cURL y no hay curl.exe disponible.';
            return null;
        }
    } elseif (function_exists('curl_init')) {
        $ch = curl_init($url);

        $post = [
            'file' => new CURLFile($zipFile, 'application/zip', basename($zipFile)),
            'force' => '1',
        ];
        $headers = [
            'X-Plugin-Token: ' . $token,
            'Expect:',
        ];

        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $post,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 180,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_SAFE_UPLOAD => true,
        ]);

        $resp = curl_exec($ch);
        $http = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err = curl_error($ch);
        curl_close($ch);

        if ($resp === false) {
            $errors[] = 'Error cURL: ' . $err;
            return null;
        }
    } else {
        $errors[] = 'Falta PHP cURL y no hay shell_exec disponible para usar curl.exe.';
        return null;
    }

    $data = json_decode((string) $resp, true);
    if ($http !== 200 || !is_array($data) || !($data['ok'] ?? false)) {
        $errors[] = 'Publish fallo (HTTP ' . $http . '): ' . substr((string) $resp, 0, 400);
        return null;
    }

    return $data;
}

function cli_plugin_dirs(string $pluginsRoot): array
{
    $items = @scandir($pluginsRoot) ?: [];
    $dirs = [];

    foreach ($items as $item) {
        if ($item === '.' || $item === '..') {
            continue;
        }

        $dir = $pluginsRoot . DIRECTORY_SEPARATOR . $item;
        if (is_dir($dir) && is_file($dir . DIRECTORY_SEPARATOR . 'plugin.json')) {
            $dirs[] = $dir;
        }
    }

    sort($dirs, SORT_NATURAL | SORT_FLAG_CASE);
    return $dirs;
}

function cli_read_plugin_manifest(string $pluginDir): array
{
    $path = $pluginDir . DIRECTORY_SEPARATOR . 'plugin.json';
    $raw = (string) @file_get_contents($path);
    $data = json_decode($raw, true);
    return is_array($data) ? $data : [];
}

function cli_usage(): void
{
    cli_out('Uso: php tools/publish_changed_plugins.php [--publish] [--only=plugin1,plugin2] [--json]');
}

$publish = false;
$json = false;
$only = [];

foreach (array_slice($argv, 1) as $arg) {
    if ($arg === '--publish') {
        $publish = true;
        continue;
    }

    if ($arg === '--json') {
        $json = true;
        continue;
    }

    if ($arg === '--help' || $arg === '-h') {
        cli_usage();
        exit(0);
    }

    if (str_starts_with($arg, '--only=')) {
        $rawOnly = substr($arg, 7);
        $only = array_values(array_filter(array_map('trim', explode(',', $rawOnly))));
        continue;
    }

    cli_err('Argumento no reconocido: ' . $arg);
    cli_usage();
    exit(1);
}

$pluginsRoot = dirname(__DIR__) . '/plugins';
$pluginDirs = cli_plugin_dirs($pluginsRoot);
$storeErrors = [];
$store = cli_fetch_store_index($storeErrors);

if (!is_array($store)) {
    foreach ($storeErrors as $error) {
        cli_err($error);
    }
    exit(2);
}

$results = [];
foreach ($pluginDirs as $pluginDir) {
    $manifest = cli_read_plugin_manifest($pluginDir);
    $pluginId = (string) ($manifest['id'] ?? basename($pluginDir));
    if ($only && !in_array($pluginId, $only, true)) {
        continue;
    }

    $version = (string) ($manifest['version'] ?? '');
    $errors = [];
    $sameCheck = cli_store_same_package($pluginId, $pluginDir, $store, $errors);
    $warnings = array_values(array_filter($errors, static function (string $message): bool {
        return strncmp($message, 'Aviso:', 6) === 0;
    }));
    $fatalErrors = array_values(array_filter($errors, static function (string $message): bool {
        return strncmp($message, 'Aviso:', 6) !== 0;
    }));
    $remote = cli_remote_store_plugin($pluginId, $store);
    $remoteVersion = is_array($remote) ? (string) ($remote['version'] ?? '') : '';

    $status = 'changed';
    if ($sameCheck !== null && !empty($sameCheck['same'])) {
        $status = 'unchanged';
    } elseif ($remote === null) {
        $status = 'new';
    } elseif ($fatalErrors) {
        $status = 'error';
    }

    $results[] = [
        'id' => $pluginId,
        'version' => $version,
        'remote_version' => $remoteVersion,
        'status' => $status,
        'path' => $pluginDir,
        'errors' => $fatalErrors,
        'warnings' => $warnings,
    ];
}

if ($json) {
    cli_out(json_encode([
        'publish_requested' => $publish,
        'publish_token_available' => cli_publish_token() !== '',
        'results' => $results,
    ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
} else {
    cli_out('Plugins analizados: ' . count($results));
    cli_out('Token de publicacion disponible: ' . (cli_publish_token() !== '' ? 'SI' : 'NO'));
    foreach ($results as $row) {
        $line = sprintf(
            '- %s | local=%s | store=%s | estado=%s',
            $row['id'],
            ($row['version'] !== '' ? $row['version'] : 'sin version'),
            ($row['remote_version'] !== '' ? $row['remote_version'] : 'sin publicar'),
            $row['status']
        );
        cli_out($line);
        foreach ($row['errors'] as $error) {
            cli_out('  error: ' . $error);
        }
        foreach (($row['warnings'] ?? []) as $warning) {
            cli_out('  aviso: ' . $warning);
        }
    }
}

if (!$publish) {
    exit(0);
}

$publishable = array_values(array_filter($results, static function (array $row): bool {
    return in_array($row['status'], ['changed', 'new'], true);
}));

if (empty($publishable)) {
    cli_out('No hay plugins pendientes de publicar.');
    exit(0);
}

if (cli_publish_token() === '') {
    cli_err('No se puede publicar: falta PLUGIN_STORE_PUBLISH_TOKEN.');
    exit(3);
}

$exitCode = 0;
foreach ($publishable as $row) {
    $zipFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $row['id'] . '-' . ($row['version'] ?: '0.0.0') . '.zip';
    $errors = [];

    cli_out('Publicando ' . $row['id'] . '...');
    @unlink($zipFile);
    if (!cli_zip_dir($row['path'], $zipFile, $errors)) {
        $exitCode = 4;
    } else {
        $pub = cli_publish_zip($zipFile, $errors);
        if (is_array($pub)) {
            cli_out('  ok: ' . $row['id'] . ' -> version ' . ((string) ($pub['version'] ?? $row['version'])));
        } else {
            $exitCode = 4;
        }
    }

    foreach ($errors as $error) {
        cli_err('  error: ' . $error);
    }

    @unlink($zipFile);
}

exit($exitCode);
