<?php

declare(strict_types=1);

namespace PpPhp\Modules\Settings\Controllers;

use PpPhp\Core\Config;
use PpPhp\Core\ModuleManager;
use PpPhp\Core\Packaging\PlatformPackager;
use PpPhp\Core\Security;
use PpPhp\Core\Update\BaselineStore;
use PpPhp\Core\Update\DeltaBuilder;
use PpPhp\Core\Update\UpdateReleases;
use PpPhp\Core\Version;

/**
 * App Bundle download — OWNER ONLY (admin-ppradhan).
 *
 * Lets the platform owner generate a clean, secret-free ZIP of the PPPHP
 * platform (via \PpPhp\Core\Packaging\PlatformPackager) and download it. The
 * bundle is built to a temp file, streamed to the browser, then DELETED from
 * the server — nothing sensitive is left behind and no artifact persists.
 *
 * Not available to superadmin, developer, or anyone else.
 */
class PlatformBundleController
{
    public function __construct(
        private Config $config,
        private ModuleManager $moduleManager,
    ) {}

    /** Hard owner gate — bounce anyone else back to Projects. */
    private function gateOwner(): void
    {
        Security::RequireAuth();
        if (!Security::IsOwner()) {
            redirect('/projects');
        }
    }

    /** The owner-only page with the generate/download action. */
    public function index(): string
    {
        $this->gateOwner();

        $baseUrl   = $this->config->get('base_url');
        $assetsUrl = $this->config->get('assets_url');
        $user      = Security::CurrentUserData();
        $menu      = $this->moduleManager->collectMenu();
        $ownerName = Security::ownerUsername();

        // Recorded baselines an "Updates only" patch can diff against, per edition.
        // The JS filters this by the edition the owner selects.
        $baselines = [
            'client' => $this->baselinesFor('client'),
            'server' => $this->baselinesFor('server'),
        ];

        ob_start();
        include __DIR__ . '/../Views/app_bundle.php';
        return (string) ob_get_clean();
    }

    /**
     * Versions available as a delta baseline for an edition — every full bundle
     * built here (BaselineStore), plus OTA-published releases (which carry the
     * same manifest). Client edition additionally sees the OTA catalogue, since
     * that is what customer installs actually run.
     *
     * @return array<int,array{version:string,builtAt:string,source:string}>
     */
    private function baselinesFor(string $edition): array
    {
        $seen = [];
        $out  = [];
        foreach ((new BaselineStore(constant('BASE_PATH')))->list($edition) as $b) {
            $v = (string) $b['version'];
            if (isset($seen[$v])) continue;
            $seen[$v] = true;
            $out[] = ['version' => $v, 'builtAt' => (string) $b['builtAt'], 'source' => 'built'];
        }
        if ($edition === 'client') {
            foreach (UpdateReleases::all('stable') as $r) {
                $v = Version::normalise((string) $r['version']);
                if ($v === '' || isset($seen[$v]) || empty($r['manifest_json'])) continue;
                $seen[$v] = true;
                $out[] = ['version' => $v, 'builtAt' => (string) ($r['published_at'] ?? ''), 'source' => 'published'];
            }
        }
        usort($out, static fn ($a, $b) => version_compare($b['version'], $a['version']));
        return $out;
    }

    /** Build → stream → delete. Void: streams a file and exits. */
    public function download(): void
    {
        $this->gateOwner();

        @set_time_limit(0);
        ignore_user_abort(true);           // finish + clean up even if the client disconnects

        // Server edition = everything (licence authority, bundle builder).
        // Client edition = what a customer runs, with those cut out.
        $variant = ($_GET['variant'] ?? '') === PlatformPackager::VARIANT_CLIENT
            ? PlatformPackager::VARIANT_CLIENT
            : PlatformPackager::VARIANT_SERVER;

        $zipPath = rtrim(sys_get_temp_dir(), '/\\') . '/ppphp-bundle-' . bin2hex(random_bytes(8)) . '.zip';
        $packager = (new PlatformPackager(constant('BASE_PATH')))->variant($variant);

        // Optional release version for this bundle. Stamped into release.json +
        // the shipped Version.php so the far side recognises it as a new release
        // instead of "same version". Blank keeps the code's current version.
        $relver = trim((string) ($_GET['relver'] ?? ''));
        if ($relver !== '') {
            $packager->releaseVersion($relver);
            // Opt-in: keep THIS development copy in step with the release we just
            // stamped, so the dev build and the deployed servers all report the
            // same version. Rewrites the Version::PLATFORM constant on disk and
            // records it in platform settings. Best-effort — a failure here must
            // never block the download.
            if (($_GET['bumpsrc'] ?? '') === '1') {
                $this->bumpSourceVersion(\PpPhp\Core\Version::normalise($relver));
            }
        }

        // Server bundles can be a MAIN licence server or a BACKUP bound to a main.
        if ($variant === PlatformPackager::VARIANT_SERVER && ($_GET['role'] ?? '') === 'backup') {
            $packager->serverRole('backup', (string) ($_GET['main'] ?? ''));
        }

        // PROTECTED client bundle: the build engine ships encrypted and is opened
        // with a key that only a valid lease carries. Offered for CLIENT bundles
        // alone — sealing our own servers' build engine would leave us unable to
        // cut the next release if a lease ever lapsed.
        if ($variant === PlatformPackager::VARIANT_CLIENT && ($_GET['protect'] ?? '') === '1') {
            $packager->protect(true);
        }

        try {
            $packager->build($zipPath);
        } catch (\Throwable $e) {
            @unlink($zipPath);
            http_response_code(500);
            header('Content-Type: text/plain; charset=utf-8');
            echo 'Bundle generation failed: ' . $e->getMessage();
            exit;
        }

        // The built bundle's true version + manifest, from its own release.json.
        $newManifest = DeltaBuilder::manifestOfBundle($zipPath);
        $newVersion  = Version::normalise((string) ($newManifest['__version__']
            ?? ($relver !== '' ? $relver : Version::PLATFORM)));

        // Two bundle types: a FULL installer (fresh installs), or an UPDATES-only
        // delta (a small patch for existing installs / OTA hosting) that carries
        // only the files changed since a chosen baseline. Either way, this build's
        // manifest is recorded so a future patch can diff against it.
        $type  = ($_GET['type'] ?? 'full') === 'updates' ? 'updates' : 'full';
        $store = new BaselineStore(constant('BASE_PATH'));

        $streamPath = $zipPath;
        $filename   = $packager->suggestedFilename();

        if ($type === 'updates') {
            $from = Version::normalise((string) ($_GET['from'] ?? ''));
            $base = $this->baselineManifest($variant, $from);
            if ($from === '' || $base === []) {
                @unlink($zipPath);
                http_response_code(400);
                header('Content-Type: text/plain; charset=utf-8');
                echo 'No recorded baseline for version "' . htmlspecialchars($from) . '" (' . $variant . ' edition). '
                    . 'Build a Full Installer of that version first — then a patch can diff against it.';
                exit;
            }
            $deltaPath = rtrim(sys_get_temp_dir(), '/\\') . '/ppphp-update-' . bin2hex(random_bytes(6)) . '.zip';
            try {
                (new DeltaBuilder())->build($base, $zipPath, $deltaPath);
            } catch (\Throwable $e) {
                @unlink($zipPath); @unlink($deltaPath);
                http_response_code(500);
                header('Content-Type: text/plain; charset=utf-8');
                echo 'Patch generation failed: ' . $e->getMessage();
                exit;
            }
            @unlink($zipPath);                 // the full build was only the means to the delta
            $streamPath = $deltaPath;
            $filename   = 'ppphp-update-' . $from . '-to-' . $newVersion . '-' . $variant . '.zip';
        }

        // Remember this build so future "Updates only" patches can start from it.
        if (!empty($newManifest)) {
            $store->record($variant, $newVersion, $newManifest);
        }

        // Signal the client (cookie handshake) that the download is starting so
        // the UI can dismiss its "preparing…" spinner.
        $token = (string) preg_replace('/[^a-zA-Z0-9]/', '', (string) ($_GET['token'] ?? ''));
        if ($token !== '') setcookie('pp_bundle_ready', $token, time() + 120, '/');

        while (ob_get_level() > 0) ob_end_clean();   // no stray output before the binary

        header('Content-Type: application/zip');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        header('Content-Length: ' . (string) filesize($streamPath));
        header('X-Content-Type-Options: nosniff');
        header('Cache-Control: no-store');
        readfile($streamPath);

        @unlink($streamPath);              // ← delete the artifact from the server after streaming
        exit;
    }

    /**
     * The baseline manifest a patch diffs against: a build recorded here first,
     * else (client edition) the matching OTA-published release's manifest.
     *
     * @return array<string,string>  {rel: sha256} + a __version__ label, or []
     */
    private function baselineManifest(string $edition, string $version): array
    {
        $m = (new BaselineStore(constant('BASE_PATH')))->manifest($edition, $version);
        if ($m !== []) return $m;
        if ($edition === 'client') {
            $files = UpdateReleases::manifest($version, 'stable');
            if ($files !== []) { $files['__version__'] = Version::normalise($version); return $files; }
        }
        return [];
    }

    /**
     * Rewrite the Version::PLATFORM constant on disk and record the version in
     * platform settings, so the dev copy reports the same version we just
     * stamped into a release bundle. Best-effort: any failure is swallowed so
     * the bundle download is never blocked.
     */
    private function bumpSourceVersion(string $version): void
    {
        if (!preg_match('/^\d+\.\d+\.\d+$/', $version)) return;

        $vf  = constant('BASE_PATH') . '/core/Version.php';
        $src = @file_get_contents($vf);
        if ($src === false || $src === '') return;

        $new = preg_replace(
            "/(const\s+PLATFORM\s*=\s*')[^']*(';)/",
            '${1}' . $version . '${2}',
            $src,
            1,
            $count
        );
        if ($count !== 1 || $new === null) return;

        @file_put_contents($vf, $new, LOCK_EX);
        if (class_exists(\PpPhp\Core\PlatformSettings::class)) {
            try { \PpPhp\Core\PlatformSettings::setVersion($version); } catch (\Throwable $e) {}
        }
    }
}
