<?php

namespace App\Http\Controllers\Admin;

use App\Core\Controller;
use App\Core\RateLimiter;
use App\Core\Request;
use App\Core\Response;
use App\Models\MarketplaceInstalled;
use App\Models\Setting;
use App\Services\ActivityLogger;
use App\Services\Marketplace\DependencyChecker;
use App\Services\Marketplace\Exceptions\DependencyException;
use App\Services\Marketplace\Exceptions\RegistryPolicyException;
use App\Services\Marketplace\Exceptions\SignatureException;
use App\Services\Marketplace\MarketplaceAuditLogger;
use App\Services\Marketplace\MarketplaceService;
use App\Services\Marketplace\PackagePreview;
use App\Services\Marketplace\RegistryPolicy;
use App\Services\Marketplace\RollbackManager;
use App\Services\Marketplace\SignatureVerifier;

/**
 * ITSWEBER CMS – Marketplace Controller (Client)
 *
 * Browse, install, and update themes/plugins from the marketplace.
 */
class MarketplaceController extends Controller
{
    // ─────────────────────────────────────────────────────────
    // Browse
    // ─────────────────────────────────────────────────────────

    /**
     * GET /admin/marketplace – browse marketplace packages.
     */
    public function index(Request $request, array $params = []): void
    {
        $service = $this->marketplaceService();

        $packages  = [];
        $error     = null;
        $fromCache = false;

        if ($service->isConfigured()) {
            $result    = $service->fetchRegistry();
            $packages  = $result['packages'];
            $fromCache = $result['from_cache'];
            $error     = $result['error'];

            // Merge installed status
            $installed = MarketplaceInstalled::getInstalledMap();
            foreach ($packages as &$pkg) {
                $slug = $pkg['slug'] ?? '';
                if (isset($installed[$slug])) {
                    $pkg['_installed']         = true;
                    $pkg['_installed_version'] = $installed[$slug]['version'] ?? '';
                    $pkg['_has_update']        = version_compare(
                        $pkg['version'] ?? '0', $installed[$slug]['version'] ?? '0', '>'
                    );
                } else {
                    $pkg['_installed']  = false;
                    $pkg['_has_update'] = false;
                }
            }
            unset($pkg);
        }

        // Filter
        $typeFilter   = $request->query('type', '');
        $searchFilter = trim($request->query('search', ''));

        if ($typeFilter !== '' && in_array($typeFilter, ['theme', 'plugin'], true)) {
            $packages = array_filter($packages, fn($p) => ($p['type'] ?? '') === $typeFilter);
        }
        if ($searchFilter !== '') {
            $q = mb_strtolower($searchFilter);
            $packages = array_filter($packages, fn($p) =>
                str_contains(mb_strtolower($p['name'] ?? ''), $q)
                || str_contains(mb_strtolower($p['description'] ?? ''), $q)
                || str_contains(mb_strtolower($p['slug'] ?? ''), $q)
            );
        }

        $this->renderAdmin('marketplace/index', [
            'pageTitle'     => __('admin.marketplace.title'),
            'packages'      => array_values($packages),
            'configured'    => $service->isConfigured(),
            'marketplaceUrl'=> Setting::get('marketplace_url', ''),
            'fromCache'     => $fromCache,
            'error'         => $error,
            'typeFilter'    => $typeFilter,
            'searchFilter'  => $searchFilter,
            'auth'          => $this->auth(),
            'csrf'          => $this->csrf(),
        ]);
    }

    /**
     * GET /admin/marketplace/detail/{slug} – package detail.
     */
    public function detail(Request $request, array $params = []): void
    {
        $slug = $params['slug'] ?? '';
        if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $slug)) {
            set_flash('error', __('admin.marketplace.invalid_slug'));
            Response::redirect(admin_url('marketplace'));
            return;
        }

        $service = $this->marketplaceService();
        $package = $service->fetchPackageDetail($slug);

        if ($package === null) {
            set_flash('error', __('admin.marketplace.package_not_found'));
            Response::redirect(admin_url('marketplace'));
            return;
        }

        $installed = MarketplaceInstalled::findBySlug($slug);
        $package['_installed']         = $installed !== null;
        $package['_installed_version'] = $installed['version'] ?? '';
        $package['_license_key']       = $installed['license_key'] ?? '';
        $package['_has_update']        = $installed !== null
            && version_compare($package['version'] ?? '0', $installed['version'] ?? '0', '>');

        // Check CMS version compatibility
        $minCms = $package['min_cms_version'] ?? '0';
        $package['_cms_compatible'] = version_compare(cms_version(), $minCms, '>=');

        $this->renderAdmin('marketplace/detail', [
            'pageTitle' => __('admin.marketplace.detail', [':name' => e($package['name'] ?? $slug)]),
            'package'   => $package,
            'auth'      => $this->auth(),
            'csrf'      => $this->csrf(),
        ]);
    }

    // ─────────────────────────────────────────────────────────
    // Install / Update
    // ─────────────────────────────────────────────────────────

    /**
     * POST /admin/marketplace/install – install a package from marketplace.
     */
    public function install(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        $userId = $this->auth()['id'] ?? 0;
        $limiter = new RateLimiter($this->appBasePath() . '/storage/cache', 5, 300, 300);
        if ($limiter->isLocked('marketplace:install:' . $userId)) {
            set_flash('error', __('admin.marketplace.rate_limited'));
            Response::redirect(admin_url('marketplace'));
            return;
        }

        $slug       = $request->input('slug', '');
        $version    = $request->input('version', '');
        $licenseKey = trim($request->input('license_key', ''));

        if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $slug)) {
            set_flash('error', __('admin.marketplace.invalid_slug'));
            Response::redirect(admin_url('marketplace'));
            return;
        }

        // Prevent double install
        if (MarketplaceInstalled::isInstalled($slug)) {
            set_flash('error', __('admin.marketplace.already_installed'));
            Response::redirect(admin_url('marketplace/detail/' . $slug));
            return;
        }

        $service = $this->marketplaceService();

        // ── 1. Registry policy ────────────────────────────────
        $registryUrl = $service->getRegistryUrl();
        $policy      = new RegistryPolicy($this->db());
        try {
            $policy->assertAllowed($registryUrl);
        } catch (RegistryPolicyException $e) {
            set_flash('error', $e->getMessage());
            Response::redirect(admin_url('marketplace'));
            return;
        }

        // ── 2. Fetch package detail ───────────────────────────
        $package = $service->fetchPackageDetail($slug);
        if ($package === null) {
            set_flash('error', __('admin.marketplace.package_not_found'));
            Response::redirect(admin_url('marketplace'));
            return;
        }

        $type = $package['type'] ?? 'plugin';

        // Premium packages require license key
        $isPremium = !empty($package['is_premium']);
        if ($isPremium && $licenseKey === '') {
            set_flash('error', __('admin.marketplace.requires_license'));
            Response::redirect(admin_url('marketplace/detail/' . $slug));
            return;
        }

        // ── 3. Dependency check (before download) ─────────────
        try {
            (new DependencyChecker())->checkCmsVersion($package);
        } catch (DependencyException $e) {
            set_flash('error', $e->getMessage());
            Response::redirect(admin_url('marketplace/detail/' . $slug));
            return;
        }

        // ── 4. Download ───────────────────────────────────────
        $download = $service->downloadPackage($slug, $version, $licenseKey);
        if (!$download['success']) {
            set_flash('error', __('admin.marketplace.download_failed', [':error' => $download['error']]));
            Response::redirect(admin_url('marketplace/detail/' . $slug));
            return;
        }

        $zipPath = $download['path'];

        // ── 5. Signature + SHA256 verification ───────────────
        $verifyResult = ['sha256_verified' => false, 'sig_verified' => false, 'key_id' => $package['key_id'] ?? ''];
        try {
            $trustedKeys  = $policy->getTrustedPublicKeys($registryUrl);
            $publicKeyB64 = $this->resolvePublicKey($trustedKeys, $package['key_id'] ?? 'primary');
            $verifyResult = (new SignatureVerifier())->verifyPackage($zipPath, $package, $publicKeyB64);
        } catch (SignatureException | RegistryPolicyException $e) {
            $limiter->recordFailure('marketplace:install:' . $userId);
            MarketplaceAuditLogger::record([
                'slug'            => $slug,
                'type'            => $type,
                'action'          => 'install',
                'version_target'  => $version,
                'registry_url'    => $registryUrl,
                'key_id'          => $package['key_id'] ?? '',
                'sha256_verified' => false,
                'sig_verified'    => false,
                'result'          => 'failed',
                'error_message'   => $e->getMessage(),
            ]);
            @unlink($zipPath);
            set_flash('error', $e->getMessage());
            Response::redirect(admin_url('marketplace/detail/' . $slug));
            return;
        }

        // ── 6. Pre-install snapshot ───────────────────────────
        $rollback  = new RollbackManager($this->appBasePath());
        $backupZip = $rollback->snapshot($type, $slug);

        // ── 7. Install with auto-rollback on failure ──────────
        $result = ['success' => false, 'name' => '', 'version' => '', 'error' => 'unknown'];
        try {
            if ($type === 'theme') {
                $result = $this->installThemeFromZip($zipPath, $slug);
            } else {
                $result = $this->installPluginFromZip($zipPath, $slug);
            }

            if (!$result['success']) {
                throw new \RuntimeException($result['error'] ?? 'install_failed');
            }
        } catch (\Throwable $e) {
            if ($backupZip !== '') {
                try { $rollback->restore($backupZip, $type, $slug); } catch (\Throwable $ignored) {}
            }
            MarketplaceAuditLogger::record([
                'slug'            => $slug,
                'type'            => $type,
                'action'          => 'install',
                'version_target'  => $version,
                'registry_url'    => $registryUrl,
                'key_id'          => $verifyResult['key_id'],
                'sha256_verified' => $verifyResult['sha256_verified'],
                'sig_verified'    => $verifyResult['sig_verified'],
                'result'          => 'failed',
                'error_message'   => $e->getMessage(),
                'backup_path'     => $backupZip ?: null,
            ]);
            @unlink($zipPath);
            set_flash('error', __('admin.marketplace.install_failed', [':error' => $e->getMessage()]));
            Response::redirect(admin_url('marketplace/detail/' . $slug));
            return;
        } finally {
            @unlink($zipPath);
        }

        // ── 8. Cleanup old backups ────────────────────────────
        $rollback->cleanupOldBackups($type, $slug, (int) Setting::get('marketplace_backup_keep', '3'));

        // Record in marketplace_installed
        MarketplaceInstalled::create([
            'slug'        => $slug,
            'type'        => $type,
            'name'        => $result['name'] ?? $package['name'] ?? $slug,
            'version'     => $result['version'] ?? $version,
            'license_key' => $licenseKey ?: null,
        ]);

        MarketplaceAuditLogger::record([
            'slug'            => $slug,
            'type'            => $type,
            'action'          => 'install',
            'version_target'  => $result['version'] ?? $version,
            'registry_url'    => $registryUrl,
            'key_id'          => $verifyResult['key_id'],
            'sha256_verified' => $verifyResult['sha256_verified'],
            'sig_verified'    => $verifyResult['sig_verified'],
            'result'          => 'success',
            'backup_path'     => $backupZip ?: null,
        ]);
        ActivityLogger::log('install', 'marketplace', null, $slug);
        $this->clearPageCache();
        $service->clearCache();

        set_flash('success', __('admin.marketplace.install_success', [
            ':name'    => e($result['name'] ?? $slug),
            ':version' => e($result['version'] ?? $version),
        ]));
        Response::redirect(admin_url('marketplace'));
    }

    /**
     * POST /admin/marketplace/update – update an installed package.
     */
    public function update(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        $userId = $this->auth()['id'] ?? 0;
        $limiter = new RateLimiter($this->appBasePath() . '/storage/cache', 5, 300, 300);
        if ($limiter->isLocked('marketplace:install:' . $userId)) {
            set_flash('error', __('admin.marketplace.rate_limited'));
            Response::redirect(admin_url('marketplace'));
            return;
        }

        $slug    = $request->input('slug', '');
        $version = $request->input('version', '');

        if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $slug)) {
            set_flash('error', __('admin.marketplace.invalid_slug'));
            Response::redirect(admin_url('marketplace'));
            return;
        }

        $existing = MarketplaceInstalled::findBySlug($slug);
        if ($existing === null) {
            set_flash('error', __('admin.marketplace.not_installed'));
            Response::redirect(admin_url('marketplace'));
            return;
        }

        $service    = $this->marketplaceService();
        $licenseKey = $existing['license_key'] ?? '';
        $type       = $existing['type'] ?? 'plugin';

        // ── Registry policy ───────────────────────────────────
        $registryUrl = $service->getRegistryUrl();
        $policy      = new RegistryPolicy($this->db());
        try {
            $policy->assertAllowed($registryUrl);
        } catch (RegistryPolicyException $e) {
            set_flash('error', $e->getMessage());
            Response::redirect(admin_url('marketplace'));
            return;
        }

        // ── Fetch package detail ──────────────────────────────
        $package = $service->fetchPackageDetail($slug);
        if ($package === null) {
            set_flash('error', __('admin.marketplace.package_not_found'));
            Response::redirect(admin_url('marketplace'));
            return;
        }

        // ── Dependency check ──────────────────────────────────
        try {
            (new DependencyChecker())->checkCmsVersion($package);
        } catch (DependencyException $e) {
            set_flash('error', $e->getMessage());
            Response::redirect(admin_url('marketplace/detail/' . $slug));
            return;
        }

        // ── Download ──────────────────────────────────────────
        $download = $service->downloadPackage($slug, $version, $licenseKey);
        if (!$download['success']) {
            set_flash('error', __('admin.marketplace.download_failed', [':error' => $download['error']]));
            Response::redirect(admin_url('marketplace/detail/' . $slug));
            return;
        }

        $zipPath = $download['path'];

        // ── Signature + SHA256 verification ──────────────────
        $verifyResult = ['sha256_verified' => false, 'sig_verified' => false, 'key_id' => $package['key_id'] ?? ''];
        try {
            $trustedKeys  = $policy->getTrustedPublicKeys($registryUrl);
            $publicKeyB64 = $this->resolvePublicKey($trustedKeys, $package['key_id'] ?? 'primary');
            $verifyResult = (new SignatureVerifier())->verifyPackage($zipPath, $package, $publicKeyB64);
        } catch (SignatureException | RegistryPolicyException $e) {
            $limiter->recordFailure('marketplace:install:' . $userId);
            MarketplaceAuditLogger::record([
                'slug'            => $slug,
                'type'            => $type,
                'action'          => 'update',
                'version_target'  => $version,
                'registry_url'    => $registryUrl,
                'key_id'          => $package['key_id'] ?? '',
                'sha256_verified' => false,
                'sig_verified'    => false,
                'result'          => 'failed',
                'error_message'   => $e->getMessage(),
            ]);
            @unlink($zipPath);
            set_flash('error', $e->getMessage());
            Response::redirect(admin_url('marketplace/detail/' . $slug));
            return;
        }

        // ── Pre-update snapshot ───────────────────────────────
        $rollback  = new RollbackManager($this->appBasePath());
        $backupZip = $rollback->snapshot($type, $slug);

        // ── Update with auto-rollback on failure ──────────────
        $result = ['success' => false, 'name' => '', 'version' => '', 'error' => 'unknown'];
        try {
            if ($type === 'theme') {
                $result = $this->installThemeFromZip($zipPath, $slug);
            } else {
                $result = $this->installPluginFromZip($zipPath, $slug);
            }

            if (!$result['success']) {
                throw new \RuntimeException($result['error'] ?? 'install_failed');
            }
        } catch (\Throwable $e) {
            if ($backupZip !== '') {
                try { $rollback->restore($backupZip, $type, $slug); } catch (\Throwable $ignored) {}
            }
            MarketplaceAuditLogger::record([
                'slug'            => $slug,
                'type'            => $type,
                'action'          => 'update',
                'version_target'  => $version,
                'registry_url'    => $registryUrl,
                'key_id'          => $verifyResult['key_id'],
                'sha256_verified' => $verifyResult['sha256_verified'],
                'sig_verified'    => $verifyResult['sig_verified'],
                'result'          => 'failed',
                'error_message'   => $e->getMessage(),
                'backup_path'     => $backupZip ?: null,
            ]);
            @unlink($zipPath);
            set_flash('error', __('admin.marketplace.install_failed', [':error' => $e->getMessage()]));
            Response::redirect(admin_url('marketplace/detail/' . $slug));
            return;
        } finally {
            @unlink($zipPath);
        }

        $rollback->cleanupOldBackups($type, $slug, (int) Setting::get('marketplace_backup_keep', '3'));

        // Update marketplace_installed record
        MarketplaceInstalled::updateById((int) $existing['id'], [
            'version'        => $result['version'] ?? $version,
            'remote_version' => null,
        ]);

        MarketplaceAuditLogger::record([
            'slug'            => $slug,
            'type'            => $type,
            'action'          => 'update',
            'version_target'  => $result['version'] ?? $version,
            'registry_url'    => $registryUrl,
            'key_id'          => $verifyResult['key_id'],
            'sha256_verified' => $verifyResult['sha256_verified'],
            'sig_verified'    => $verifyResult['sig_verified'],
            'result'          => 'success',
            'backup_path'     => $backupZip ?: null,
        ]);
        ActivityLogger::log('update', 'marketplace', null, $slug);
        $this->clearPageCache();
        $service->clearCache();

        set_flash('success', __('admin.marketplace.update_success', [
            ':name'    => e($result['name'] ?? $slug),
            ':version' => e($result['version'] ?? $version),
        ]));
        Response::redirect(admin_url('marketplace'));
    }

    // ─────────────────────────────────────────────────────────
    // Preview
    // ─────────────────────────────────────────────────────────

    /**
     * GET /admin/marketplace/preview/{slug} – pre-install preview page.
     */
    public function preview(Request $request, array $params = []): void
    {
        $slug = $params['slug'] ?? '';
        if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $slug)) {
            set_flash('error', __('admin.marketplace.invalid_slug'));
            Response::redirect(admin_url('marketplace'));
            return;
        }

        $service = $this->marketplaceService();
        $package = $service->fetchPackageDetail($slug);

        if ($package === null) {
            set_flash('error', __('admin.marketplace.package_not_found'));
            Response::redirect(admin_url('marketplace'));
            return;
        }

        // Add registry URL to entry for preview
        $package['_registry_url'] = $service->getRegistryUrl();

        $rollback = new RollbackManager($this->appBasePath());
        $builder  = new PackagePreview(new DependencyChecker(), $rollback);
        $preview  = $builder->build($package);

        $this->renderAdmin('marketplace/preview', [
            'pageTitle' => __('admin.marketplace.preview_title', [':name' => $package['name'] ?? $slug]),
            'preview'   => $preview,
            'auth'      => $this->auth(),
            'csrf'      => $this->csrf(),
        ]);
    }

    // ─────────────────────────────────────────────────────────
    // Rollback
    // ─────────────────────────────────────────────────────────

    /**
     * POST /admin/marketplace/rollback – restore a package from backup.
     */
    public function rollback(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        $userId = $this->auth()['id'] ?? 0;
        $limiter = new RateLimiter($this->appBasePath() . '/storage/cache', 5, 300, 300);
        if ($limiter->isLocked('marketplace:install:' . $userId)) {
            set_flash('error', __('admin.marketplace.rate_limited'));
            Response::redirect(admin_url('marketplace'));
            return;
        }

        $slug       = $request->input('slug', '');
        $backupFile = basename($request->input('backup_file', ''));

        if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $slug)) {
            set_flash('error', __('admin.marketplace.invalid_slug'));
            Response::redirect(admin_url('marketplace'));
            return;
        }

        $existing = MarketplaceInstalled::findBySlug($slug);
        if ($existing === null) {
            set_flash('error', __('admin.marketplace.not_installed'));
            Response::redirect(admin_url('marketplace'));
            return;
        }

        $type     = $existing['type'] ?? 'plugin';
        $rollback = new RollbackManager($this->appBasePath());
        $backups  = $rollback->listBackups($type, $slug);

        // Verify backup file exists in the list (prevents path traversal)
        $backupPath = null;
        foreach ($backups as $b) {
            if ($b['filename'] === $backupFile) {
                $backupPath = $b['path'];
                break;
            }
        }

        if ($backupPath === null) {
            set_flash('error', __('admin.marketplace.backup_not_found'));
            Response::redirect(admin_url('marketplace/backups/' . $slug));
            return;
        }

        try {
            $rollback->restore($backupPath, $type, $slug);
        } catch (\Throwable $e) {
            MarketplaceAuditLogger::record([
                'slug'            => $slug,
                'type'            => $type,
                'action'          => 'rollback',
                'version_target'  => '',
                'registry_url'    => '',
                'key_id'          => '',
                'sha256_verified' => false,
                'sig_verified'    => false,
                'result'          => 'failed',
                'error_message'   => $e->getMessage(),
                'backup_path'     => $backupPath,
            ]);
            set_flash('error', __('admin.marketplace.rollback_failed', [':error' => $e->getMessage()]));
            Response::redirect(admin_url('marketplace/backups/' . $slug));
            return;
        }

        MarketplaceAuditLogger::record([
            'slug'            => $slug,
            'type'            => $type,
            'action'          => 'rollback',
            'version_target'  => '',
            'registry_url'    => '',
            'key_id'          => '',
            'sha256_verified' => false,
            'sig_verified'    => false,
            'result'          => 'rolled_back',
            'backup_path'     => $backupPath,
        ]);
        ActivityLogger::log('rollback', 'marketplace', null, $slug);
        $this->clearPageCache();

        set_flash('success', __('admin.marketplace.rollback_success', [':slug' => e($slug)]));
        Response::redirect(admin_url('marketplace'));
    }

    /**
     * GET /admin/marketplace/backups/{slug} – list backups for a package.
     */
    public function backups(Request $request, array $params = []): void
    {
        $slug = $params['slug'] ?? '';
        if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $slug)) {
            set_flash('error', __('admin.marketplace.invalid_slug'));
            Response::redirect(admin_url('marketplace'));
            return;
        }

        $existing = MarketplaceInstalled::findBySlug($slug);
        if ($existing === null) {
            set_flash('error', __('admin.marketplace.not_installed'));
            Response::redirect(admin_url('marketplace'));
            return;
        }

        $type     = $existing['type'] ?? 'plugin';
        $rollback = new RollbackManager($this->appBasePath());

        $this->renderAdmin('marketplace/backups', [
            'pageTitle' => __('admin.marketplace.backups_title', [':slug' => $slug]),
            'slug'      => $slug,
            'type'      => $type,
            'backups'   => $rollback->listBackups($type, $slug),
            'auth'      => $this->auth(),
            'csrf'      => $this->csrf(),
        ]);
    }

    // ─────────────────────────────────────────────────────────
    // Allowlist management
    // ─────────────────────────────────────────────────────────

    /**
     * POST /admin/marketplace/allowlist/add – add a custom registry to the allowlist.
     */
    public function allowlistAdd(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        $url          = trim($request->input('registry_url', ''));
        $publicKeyB64 = trim($request->input('public_key_b64', ''));
        $keyId        = trim($request->input('key_id', 'primary'));
        $label        = trim($request->input('label', ''));

        if ($url === '' || $publicKeyB64 === '') {
            set_flash('error', __('admin.marketplace.allowlist_missing_fields'));
            Response::redirect(admin_url('settings') . '#marketplace');
            return;
        }

        $policy = new RegistryPolicy($this->db());
        try {
            // Validate HTTPS requirement
            $policy->assertAllowed($url);
        } catch (RegistryPolicyException $e) {
            // Allow adding to allowlist even if not yet in it; only HTTPS check matters here
            if (str_starts_with(strtolower($url), 'http://')) {
                set_flash('error', __('admin.marketplace.allowlist_https_required'));
                Response::redirect(admin_url('settings') . '#marketplace');
                return;
            }
        }

        $db = $this->db();
        $db->query(
            "INSERT INTO iw_marketplace_allowlist (registry_url, public_key_b64, key_id, label, added_by)
             VALUES (:url, :key, :kid, :label, :uid)
             ON DUPLICATE KEY UPDATE public_key_b64 = VALUES(public_key_b64), key_id = VALUES(key_id), label = VALUES(label)",
            [
                'url'   => rtrim($url, '/'),
                'key'   => $publicKeyB64,
                'kid'   => $keyId ?: 'primary',
                'label' => $label ?: null,
                'uid'   => $this->auth()['id'] ?? null,
            ]
        );

        MarketplaceAuditLogger::record([
            'slug'            => rtrim($url, '/'),
            'type'            => 'plugin',
            'action'          => 'allowlist_add',
            'version_target'  => '',
            'registry_url'    => rtrim($url, '/'),
            'key_id'          => $keyId ?: 'primary',
            'sha256_verified' => false,
            'sig_verified'    => false,
            'result'          => 'success',
        ]);
        set_flash('success', __('admin.marketplace.allowlist_added'));
        Response::redirect(admin_url('settings') . '#marketplace');
    }

    /**
     * POST /admin/marketplace/allowlist/remove – remove a custom registry from the allowlist.
     */
    public function allowlistRemove(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        $id = (int) $request->input('id', 0);
        if ($id <= 0) {
            set_flash('error', __('admin.marketplace.allowlist_invalid_id'));
            Response::redirect(admin_url('settings') . '#marketplace');
            return;
        }

        $this->db()->query(
            "DELETE FROM iw_marketplace_allowlist WHERE id = :id",
            ['id' => $id]
        );

        MarketplaceAuditLogger::record([
            'slug'            => 'id:' . $id,
            'type'            => 'plugin',
            'action'          => 'allowlist_remove',
            'version_target'  => '',
            'registry_url'    => '',
            'key_id'          => '',
            'sha256_verified' => false,
            'sig_verified'    => false,
            'result'          => 'success',
        ]);
        set_flash('success', __('admin.marketplace.allowlist_removed'));
        Response::redirect(admin_url('settings') . '#marketplace');
    }

    // ─────────────────────────────────────────────────────────
    // License management
    // ─────────────────────────────────────────────────────────

    /**
     * GET /admin/marketplace/licenses – manage license keys.
     */
    public function licenses(Request $request, array $params = []): void
    {
        $installed = MarketplaceInstalled::all('name', 'ASC');

        $this->renderAdmin('marketplace/licenses', [
            'pageTitle' => __('admin.marketplace.licenses'),
            'installed' => $installed,
            'auth'      => $this->auth(),
            'csrf'      => $this->csrf(),
        ]);
    }

    /**
     * POST /admin/marketplace/licenses – save a license key.
     */
    public function saveLicense(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        $slug       = $request->input('slug', '');
        $licenseKey = trim($request->input('license_key', ''));

        $existing = MarketplaceInstalled::findBySlug($slug);
        if ($existing === null) {
            set_flash('error', __('admin.marketplace.not_installed'));
            Response::redirect(admin_url('marketplace/licenses'));
            return;
        }

        MarketplaceInstalled::updateById((int) $existing['id'], [
            'license_key' => $licenseKey ?: null,
        ]);

        set_flash('success', __('admin.marketplace.license_saved'));
        Response::redirect(admin_url('marketplace/licenses'));
    }

    // ─────────────────────────────────────────────────────────
    // Settings
    // ─────────────────────────────────────────────────────────

    /**
     * POST /admin/marketplace/settings – save marketplace URL.
     */
    public function settings(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        $url = trim($request->input('marketplace_url', ''));
        $url = rtrim($url, '/');

        Setting::set('marketplace_url', $url, 'marketplace');

        // Clear cache when URL changes
        $service = new MarketplaceService($this->appBasePath());
        $service->clearCache();

        set_flash('success', __('admin.marketplace.settings_saved'));
        Response::redirect(admin_url('marketplace'));
    }

    // ─────────────────────────────────────────────────────────
    // Private helpers
    // ─────────────────────────────────────────────────────────

    private function marketplaceService(): MarketplaceService
    {
        return new MarketplaceService($this->appBasePath());
    }

    private function appBasePath(): string
    {
        return dirname(__DIR__, 4);
    }

    /**
     * Pick the best matching public key from the trusted-keys array.
     * Falls back to the first key if no exact key_id match.
     *
     * @param array<array{key_id: string, public_key_b64: string}> $trustedKeys
     * @throws SignatureException when the array is empty.
     */
    private function resolvePublicKey(array $trustedKeys, string $keyId): string
    {
        foreach ($trustedKeys as $k) {
            if ($k['key_id'] === $keyId) {
                return $k['public_key_b64'];
            }
        }

        if (!empty($trustedKeys)) {
            return $trustedKeys[0]['public_key_b64'];
        }

        throw new SignatureException('No trusted public key available for this registry.');
    }

    /**
     * Install a theme from a downloaded ZIP file.
     * Adapts ThemeController ZIP security chain (steps 6–20) for non-upload source.
     *
     * @return array{success: bool, name: string, version: string, error: ?string}
     */
    private function installThemeFromZip(string $zipPath, string $expectedSlug): array
    {
        // ── 6. ZipArchive availability ────────────────────────────────────
        if (!class_exists('ZipArchive')) {
            return ['success' => false, 'name' => '', 'version' => '', 'error' => 'ZipArchive not available'];
        }

        // ── 7. Temp directory ─────────────────────────────────────────────
        $tempDir = sys_get_temp_dir() . '/iw_mp_theme_' . bin2hex(random_bytes(8));
        if (!mkdir($tempDir, 0700, true)) {
            return ['success' => false, 'name' => '', 'version' => '', 'error' => 'temp_dir_failed'];
        }

        try {
            // ── 8. Open ZIP ───────────────────────────────────────────────
            $zip = new \ZipArchive();
            $openResult = $zip->open($zipPath);
            if ($openResult !== true) {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'zip_open_failed'];
            }

            // ── 9. Pre-extraction ZIP-slip check ──────────────────────────
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $entryName = $zip->getNameIndex($i);
                if ($entryName === false) continue;
                if (strpos($entryName, '..') !== false || strpos($entryName, "\0") !== false) {
                    $zip->close();
                    return ['success' => false, 'name' => '', 'version' => '', 'error' => 'zip_path_traversal'];
                }
                if (substr($entryName, 0, 1) === '/' || (strlen($entryName) > 1 && $entryName[1] === ':')) {
                    $zip->close();
                    return ['success' => false, 'name' => '', 'version' => '', 'error' => 'zip_absolute_path'];
                }
            }

            // ── 10. Extract ───────────────────────────────────────────────
            if (!$zip->extractTo($tempDir)) {
                $zip->close();
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'zip_extract_failed'];
            }
            $zip->close();

            // ── 11. Post-extraction realpath verification ─────────────────
            $realTempBase = realpath($tempDir);
            if ($realTempBase === false) {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'zip_resolve_failed'];
            }
            $realTempBase .= DIRECTORY_SEPARATOR;

            foreach ($this->recursiveFileList($tempDir) as $extractedFile) {
                $realExtracted = realpath($extractedFile);
                if ($realExtracted === false || strpos($realExtracted, $realTempBase) !== 0) {
                    return ['success' => false, 'name' => '', 'version' => '', 'error' => 'zip_slip_detected'];
                }
            }

            // ── 12. Locate theme.json ─────────────────────────────────────
            $themeRoot = $this->findManifestRoot($tempDir, 'theme.json');
            if ($themeRoot === null) {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'no_theme_json'];
            }

            // ── 13. Parse and validate ────────────────────────────────────
            $themeJson = json_decode(file_get_contents($themeRoot . '/theme.json'), true);
            if (!is_array($themeJson)) {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'invalid_theme_json'];
            }

            $slug    = trim($themeJson['slug']    ?? '');
            $name    = trim($themeJson['name']    ?? '');
            $version = trim($themeJson['version'] ?? '');

            if ($slug === '' || $name === '' || $version === '') {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'missing_fields'];
            }

            // ── 14. Slug validation ───────────────────────────────────────
            if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $slug)) {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'invalid_slug'];
            }

            // Verify slug matches expected
            if ($slug !== $expectedSlug) {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'slug_mismatch'];
            }

            // ── 15. templates/ required ───────────────────────────────────
            if (!is_dir($themeRoot . '/templates')) {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'templates_missing'];
            }

            // ── 16. Protect core themes ───────────────────────────────────
            $protectedSlugs = ['default'];
            if (in_array($slug, $protectedSlugs, true)) {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'slug_reserved'];
            }

            // ── 17. Target paths ──────────────────────────────────────────
            $basePath       = $this->appBasePath();
            $themeTargetDir = $basePath . '/themes/' . $slug;
            $publicThemeDir = $basePath . '/public/themes/' . $slug;

            // ── 18. Copy theme (skip assets/) ─────────────────────────────
            $this->copyDirectoryExcluding($themeRoot, $themeTargetDir, ['assets', '__public_assets__']);

            // ── 19. Copy assets to public/ ────────────────────────────────
            $sourceAssetsDir = $themeRoot . '/assets';
            $altAssetsDir    = $themeRoot . '/__public_assets__';
            if (is_dir($sourceAssetsDir)) {
                $this->copyDirectory($sourceAssetsDir, $publicThemeDir . '/assets');
            } elseif (is_dir($altAssetsDir)) {
                $this->copyDirectory($altAssetsDir, $publicThemeDir);
            }

            return ['success' => true, 'name' => $name, 'version' => $version, 'error' => null];

        } finally {
            $this->removeDirectory($tempDir);
        }
    }

    /**
     * Install a plugin from a downloaded ZIP file.
     * Adapts PluginController ZIP security chain for non-upload source.
     *
     * @return array{success: bool, name: string, version: string, error: ?string}
     */
    private function installPluginFromZip(string $zipPath, string $expectedSlug): array
    {
        if (!class_exists('ZipArchive')) {
            return ['success' => false, 'name' => '', 'version' => '', 'error' => 'ZipArchive not available'];
        }

        $tempDir = sys_get_temp_dir() . '/iw_mp_plugin_' . bin2hex(random_bytes(8));
        if (!mkdir($tempDir, 0700, true)) {
            return ['success' => false, 'name' => '', 'version' => '', 'error' => 'temp_dir_failed'];
        }

        try {
            $zip = new \ZipArchive();
            $openResult = $zip->open($zipPath);
            if ($openResult !== true) {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'zip_open_failed'];
            }

            // Pre-extraction security checks
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $entryName = $zip->getNameIndex($i);
                if ($entryName === false) continue;
                if (strpos($entryName, '..') !== false || strpos($entryName, "\0") !== false) {
                    $zip->close();
                    return ['success' => false, 'name' => '', 'version' => '', 'error' => 'zip_path_traversal'];
                }
                if (substr($entryName, 0, 1) === '/' || (strlen($entryName) > 1 && $entryName[1] === ':')) {
                    $zip->close();
                    return ['success' => false, 'name' => '', 'version' => '', 'error' => 'zip_absolute_path'];
                }
            }

            if (!$zip->extractTo($tempDir)) {
                $zip->close();
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'zip_extract_failed'];
            }
            $zip->close();

            // Post-extraction realpath verification
            $realTempBase = realpath($tempDir);
            if ($realTempBase === false) {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'zip_resolve_failed'];
            }
            $realTempBase .= DIRECTORY_SEPARATOR;

            foreach ($this->recursiveFileList($tempDir) as $extractedFile) {
                $realExtracted = realpath($extractedFile);
                if ($realExtracted === false || strpos($realExtracted, $realTempBase) !== 0) {
                    return ['success' => false, 'name' => '', 'version' => '', 'error' => 'zip_slip_detected'];
                }
            }

            // Locate plugin.json
            $pluginRoot = $this->findManifestRoot($tempDir, 'plugin.json');
            if ($pluginRoot === null) {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'no_plugin_json'];
            }

            // Parse and validate
            $pluginJson = json_decode(file_get_contents($pluginRoot . '/plugin.json'), true);
            if (!is_array($pluginJson)) {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'invalid_plugin_json'];
            }

            $slug    = trim($pluginJson['slug']    ?? '');
            $name    = trim($pluginJson['name']    ?? '');
            $version = trim($pluginJson['version'] ?? '');

            if ($slug === '' || $name === '' || $version === '') {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'missing_fields'];
            }

            if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $slug)) {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'invalid_slug'];
            }

            if ($slug !== $expectedSlug) {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'slug_mismatch'];
            }

            // plugin.php must exist
            if (!is_file($pluginRoot . '/plugin.php')) {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'plugin_php_missing'];
            }

            // Check min CMS version
            $minCms = $pluginJson['min_cms_version'] ?? '0';
            if (!version_compare(cms_version(), $minCms, '>=')) {
                return ['success' => false, 'name' => '', 'version' => '', 'error' => 'cms_version_too_old'];
            }

            // Target paths
            $basePath        = $this->appBasePath();
            $pluginTargetDir = $basePath . '/plugins/' . $slug;
            $publicPluginDir = $basePath . '/public/plugins/' . $slug;

            // Copy plugin files (skip assets/)
            $this->copyDirectoryExcluding($pluginRoot, $pluginTargetDir, ['assets', '__public_assets__']);

            // Copy assets to public/
            $sourceAssetsDir = $pluginRoot . '/assets';
            $altAssetsDir    = $pluginRoot . '/__public_assets__';
            if (is_dir($sourceAssetsDir)) {
                $this->copyDirectory($sourceAssetsDir, $publicPluginDir . '/assets');
            } elseif (is_dir($altAssetsDir)) {
                $this->copyDirectory($altAssetsDir, $publicPluginDir);
            }

            return ['success' => true, 'name' => $name, 'version' => $version, 'error' => null];

        } finally {
            $this->removeDirectory($tempDir);
        }
    }

    // ─────────────────────────────────────────────────────────
    // File system helpers (adapted from ThemeController)
    // ─────────────────────────────────────────────────────────

    /**
     * Find the directory containing a manifest file (flat or one-level-wrapped).
     */
    private function findManifestRoot(string $tempDir, string $manifestFile): ?string
    {
        if (is_file($tempDir . '/' . $manifestFile)) {
            return $tempDir;
        }
        $subdirs = glob($tempDir . '/*', GLOB_ONLYDIR);
        if ($subdirs !== false && count($subdirs) === 1) {
            if (is_file($subdirs[0] . '/' . $manifestFile)) {
                return $subdirs[0];
            }
        }
        return null;
    }

    private function copyDirectory(string $src, string $dst): void
    {
        if (!is_dir($dst)) {
            mkdir($dst, 0755, true);
        }
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($src, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::SELF_FIRST
        );
        foreach ($iterator as $item) {
            $relative = substr($item->getPathname(), strlen($src));
            $target   = $dst . $relative;
            if ($item->isDir()) {
                if (!is_dir($target)) mkdir($target, 0755, true);
            } else {
                $targetDir = dirname($target);
                if (!is_dir($targetDir)) mkdir($targetDir, 0755, true);
                copy($item->getPathname(), $target);
            }
        }
    }

    private function copyDirectoryExcluding(string $src, string $dst, array $excludeDirs): void
    {
        if (!is_dir($dst)) {
            mkdir($dst, 0755, true);
        }
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($src, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::SELF_FIRST
        );
        foreach ($iterator as $item) {
            $relative = ltrim(substr($item->getPathname(), strlen($src)), '/\\');
            $topDir   = explode(DIRECTORY_SEPARATOR, $relative)[0];
            $topDir   = explode('/', $topDir)[0];
            if (in_array($topDir, $excludeDirs, true)) continue;
            $target = $dst . DIRECTORY_SEPARATOR . $relative;
            if ($item->isDir()) {
                if (!is_dir($target)) mkdir($target, 0755, true);
            } else {
                $targetDir = dirname($target);
                if (!is_dir($targetDir)) mkdir($targetDir, 0755, true);
                copy($item->getPathname(), $target);
            }
        }
    }

    private function removeDirectory(string $dir): void
    {
        if (!is_dir($dir)) return;
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::CHILD_FIRST
        );
        foreach ($iterator as $item) {
            if ($item->isDir()) {
                rmdir($item->getPathname());
            } else {
                unlink($item->getPathname());
            }
        }
        rmdir($dir);
    }

    private function recursiveFileList(string $dir): array
    {
        $files = [];
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)
        );
        foreach ($iterator as $item) {
            if ($item->isFile()) {
                $files[] = $item->getPathname();
            }
        }
        return $files;
    }
}
