<?php
declare(strict_types=1);

namespace Celena\Module\Marketplace;

use Celena\Core\Config;
use Celena\Core\Database\Connection;
use Celena\Core\Http\Request;
use Celena\Core\Http\Response;
use Celena\Core\Kernel;
use Celena\Core\Logger;
use Celena\Core\Plugin\Installer;
use Celena\Core\Plugin\Marketplace;
use Celena\Core\Plugin\PackageSigner;
use Celena\Core\Plugin\PluginManager;
use Celena\Core\Security\Auth;
use Celena\Core\Security\Csrf;
use Celena\Core\Template\Engine;
use Celena\Core\Template\ThemeManager;
use Celena\Module\AdminBase\AdminController;
use Celena\Module\Settings\Options;

/**
 * Клиент маркетплейса в админке celena.io: каталог, страница пакета, установка
 * в один клик и проверка обновлений. Переиспользует core/Plugin/Marketplace + Installer.
 */
final class MarketplaceController extends AdminController
{
    /** Текущая версия CMS для проверки requires_celena (синхронно с core/bootstrap.php). */
    private const CMS_VERSION = '3.5';

    public function __construct(
        Engine $engine,
        Auth $auth,
        Options $options,
        private readonly Config $config,
        private readonly Connection $conn,
        private readonly Logger $logger,
        private readonly PluginManager $plugins,
        private readonly ThemeManager $themes,
    ) {
        parent::__construct($engine, $auth, $options);
    }

    public function index(Request $request): Response
    {
        if (($g = $this->guard()) !== null) {
            return $g;
        }
        $type = (string) $request->query('type', 'plugin');
        if ($type !== 'plugin' && $type !== 'theme') {
            $type = 'plugin';
        }
        $q = trim((string) $request->query('q', ''));
        $category = trim((string) $request->query('category', ''));
        $page = max(1, (int) $request->query('page', 1));

        $error = '';
        $res = ['items' => [], 'page' => 1, 'pages' => 1, 'total' => 0, 'categories' => []];
        try {
            $res = $this->mp($request)->list($type, $page, $q, $category);
        } catch (\Throwable $e) {
            $error = 'Маркетплейс недоступен: ' . $e->getMessage();
        }

        $cats = [];
        foreach ((array) ($res['categories'] ?? []) as $c) {
            $cats[] = ['name' => (string) $c, 'active' => ((string) $c === $category) ? '1' : ''];
        }

        return $this->view('admin/marketplace/index', [
            'items'       => $this->markInstalled((array) ($res['items'] ?? []), $type),
            'type'        => $type,
            'q'           => $q,
            'category'    => $category,
            'categories'  => $cats,
            'page'        => (int) ($res['page'] ?? 1),
            'pages'       => (int) ($res['pages'] ?? 1),
            'total'       => (int) ($res['total'] ?? 0),
            'mp_error'    => $error,
            'mp_url'      => $this->baseUrl(),
            'updates'     => [],
        ], 'marketplace', 'Маркетплейс');
    }

    public function item(Request $request, array $params): Response
    {
        if (($g = $this->guard()) !== null) {
            return $g;
        }
        $slug = (string) ($params['slug'] ?? '');
        try {
            $pkg = $this->mp($request)->item($slug);
        } catch (\Throwable $e) {
            return $this->redirect('/admin/marketplace', 'Не удалось загрузить пакет: ' . $e->getMessage(), 'error');
        }
        $type = (string) ($pkg['type'] ?? 'plugin');
        $installed = $this->isInstalled($slug, $type);
        $paid = (bool) ($pkg['is_paid'] ?? false);
        return $this->view('admin/marketplace/item', [
            'pkg'          => $pkg,
            'installed'    => $installed ? '1' : '',
            'is_paid'      => $paid ? '1' : '',
            'free_install' => (!$installed && !$paid) ? '1' : '',
            'paid_buy'     => (!$installed && $paid) ? '1' : '',
            'license'      => $this->licenseFor($slug),
            'price_label'  => $paid
                ? number_format((float) ($pkg['price'] ?? 0), 2) . ' ' . (string) ($pkg['currency'] ?? 'USD')
                : '',
            'buy_url'      => $this->baseUrl() . '/buy/' . rawurlencode($slug),
        ], 'marketplace', (string) ($pkg['name'] ?? $slug));
    }

    public function install(Request $request): Response
    {
        if (($g = $this->guard()) !== null) {
            return $g;
        }
        if (!Csrf::validate((string) $request->input('_csrf'))) {
            return $this->redirect('/admin/marketplace', 'Недействительный CSRF-токен.', 'error');
        }
        $slug = (string) $request->input('slug', '');
        $type = (string) $request->input('type', 'plugin') === 'theme' ? 'theme' : 'plugin';
        $version = (string) $request->input('version', 'latest');
        if ($slug === '') {
            return $this->redirect('/admin/marketplace', 'Не указан пакет.', 'error');
        }
        // Если введён лицензионный ключ — запоминаем его для этого пакета.
        $postedLicense = trim((string) $request->input('license_key', ''));
        if ($postedLicense !== '') {
            $this->setLicense($slug, $postedLicense);
        }

        $root = Kernel::rootPath();
        $tmp = tempnam(sys_get_temp_dir(), 'mpz');
        if ($tmp === false) {
            return $this->redirect('/admin/marketplace', 'Не удалось создать временный файл.', 'error');
        }
        try {
            $info = $this->mp($request)->installUrl($slug, $version, $this->licenseFor($slug));
            $zipUrl = (string) ($info['zip_url'] ?? '');
            if ($zipUrl === '') {
                throw new \RuntimeException('сервер не вернул ссылку на архив.');
            }

            // Проверка совместимости до скачивания.
            $reqPhp = (string) ($info['requires_php'] ?? '');
            $reqCelena = (string) ($info['requires_celena'] ?? '');
            if ($reqPhp !== '' && !$this->meets($reqPhp, PHP_VERSION)) {
                @unlink($tmp);
                return $this->redirect('/admin/marketplace', 'Пакет требует PHP ' . $reqPhp . ', а на сервере ' . PHP_VERSION . '.', 'error');
            }
            if ($reqCelena !== '' && !$this->meets($reqCelena, self::CMS_VERSION)) {
                @unlink($tmp);
                return $this->redirect('/admin/marketplace', 'Пакет требует Celena ' . $reqCelena . ', а установлена ' . self::CMS_VERSION . '.', 'error');
            }

            $this->mp($request)->download($zipUrl, $tmp, (string) ($info['sha256'] ?? ''));

            // Supply-chain: если в config задан публичный ключ реестра — подпись обязательна.
            // Проверяем Ed25519-подпись фактически скачанных байт (slug|version|sha256).
            $pubkey = trim((string) ($this->config->get('marketplace.pubkey') ?? ''));
            if ($pubkey !== '') {
                if (!PackageSigner::available()) {
                    @unlink($tmp);
                    return $this->redirect('/admin/marketplace', 'Проверка подписи невозможна: на сервере нет libsodium. Установка отменена.', 'error');
                }
                $sig = (string) ($info['signature'] ?? '');
                $resolvedVer = (string) ($info['version'] ?? $version);
                $actualSha = (string) hash_file('sha256', $tmp);
                if ($sig === '' || !PackageSigner::verify($pubkey, $slug, $resolvedVer, $actualSha, $sig)) {
                    @unlink($tmp);
                    return $this->redirect('/admin/marketplace', 'Подпись пакета не прошла проверку — установка отменена (supply-chain).', 'error');
                }
            }

            $installer = new Installer($root . '/plugins', $this->conn, $this->logger);
            if ($type === 'theme') {
                $installer->installFromZip($tmp, $root . '/templates', 'theme.json');
                $msg = 'Тема «' . $slug . '» установлена. Активируйте её в разделе «Шаблоны».';
            } else {
                $installer->installFromZip($tmp);
                $this->enablePlugin($slug);
                $msg = 'Плагин «' . $slug . '» установлен и включён.';
            }
        } catch (\Throwable $e) {
            @unlink($tmp);
            return $this->redirect('/admin/marketplace/item/' . rawurlencode($slug), 'Ошибка установки: ' . $this->humanizeError($e->getMessage()), 'error');
        }
        @unlink($tmp);
        return $this->redirect('/admin/marketplace?type=' . $type, $msg);
    }

    public function updates(Request $request): Response
    {
        if (($g = $this->guard()) !== null) {
            return $g;
        }
        $installed = [];
        $this->plugins->discover();
        foreach ($this->plugins->all() as $slug => $manifest) {
            $installed[(string) $slug] = (string) ($manifest['version'] ?? '0.0.0');
        }

        $updates = [];
        $error = '';
        try {
            $resp = $this->mp($request)->updates($installed, $this->storedLicenses());
            foreach ((array) ($resp['updates'] ?? []) as $slug => $info) {
                $updates[] = [
                    'slug'      => (string) $slug,
                    'version'   => (string) ($info['version'] ?? ''),
                    'changelog' => (string) ($info['changelog'] ?? ''),
                ];
            }
        } catch (\Throwable $e) {
            $error = 'Маркетплейс недоступен: ' . $e->getMessage();
        }

        return $this->view('admin/marketplace/index', [
            'items'      => [],
            'type'       => 'plugin',
            'q'          => '',
            'category'   => '',
            'categories' => [],
            'page'       => 1,
            'pages'      => 1,
            'total'      => 0,
            'mp_error'   => $error,
            'mp_url'     => $this->baseUrl(),
            'updates'    => $updates,
        ], 'marketplace', 'Маркетплейс — обновления');
    }

    // ───────── helpers ─────────

    private function mp(Request $request): Marketplace
    {
        $domain = $request->host();
        $license = (string) $this->options->get('marketplace_license', '');
        return new Marketplace($this->baseUrl(), $license, $domain);
    }

    private function baseUrl(): string
    {
        $url = (string) $this->options->get('marketplace_url', '');
        if ($url === '') {
            $url = (string) ($this->config->get('marketplace.url') ?? 'https://marketplace.celena.io');
        }
        return rtrim($url, '/');
    }

    private function enablePlugin(string $slug): void
    {
        $active = json_decode((string) $this->options->get('active_plugins', '[]'), true) ?: [];
        if (!is_array($active)) {
            $active = [];
        }
        if (!in_array($slug, $active, true)) {
            $active[] = $slug;
            $this->options->set('active_plugins', json_encode(array_values($active), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        }
    }

    /** @return array<string,string> slug => license_key (опция marketplace_licenses) */
    private function storedLicenses(): array
    {
        $raw = json_decode((string) $this->options->get('marketplace_licenses', '[]'), true);
        if (!is_array($raw)) {
            return [];
        }
        $out = [];
        foreach ($raw as $slug => $key) {
            $out[(string) $slug] = (string) $key;
        }
        return $out;
    }

    private function licenseFor(string $slug): string
    {
        return $this->storedLicenses()[$slug] ?? '';
    }

    private function setLicense(string $slug, string $key): void
    {
        $map = $this->storedLicenses();
        $map[$slug] = $key;
        $this->options->set('marketplace_licenses', json_encode($map, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    }

    /** Перевод кодов ошибок API маркетплейса в понятные сообщения. */
    private function humanizeError(string $raw): string
    {
        $map = [
            'license_required'        => 'этот пакет платный — введите лицензионный ключ.',
            'license_invalid'         => 'лицензионный ключ не найден или неверен.',
            'license_domain_mismatch' => 'ключ уже привязан к другому домену.',
            'license_revoked'         => 'лицензия отозвана.',
            'license_refunded'        => 'оплата возвращена, лицензия недействительна.',
            'license_wrong_package'   => 'ключ выдан для другого пакета.',
            'domain_required'         => 'не удалось определить домен сайта.',
            'Подпись пакета'          => 'подпись пакета не прошла проверку (supply-chain).',
            'SHA256 mismatch'         => 'контрольная сумма архива не совпала — файл повреждён или подменён.',
        ];
        foreach ($map as $code => $human) {
            if (str_contains($raw, $code)) {
                return $human;
            }
        }
        return $raw;
    }

    private function isInstalled(string $slug, string $type): bool
    {
        if ($type === 'theme') {
            return $this->themes->exists($slug);
        }
        $this->plugins->discover();
        return array_key_exists($slug, $this->plugins->all());
    }

    /** @param array<int,array<string,mixed>> $items */
    private function markInstalled(array $items, string $type): array
    {
        $this->plugins->discover();
        $pluginSlugs = array_keys($this->plugins->all());
        foreach ($items as &$it) {
            $slug = (string) ($it['slug'] ?? '');
            $isTheme = (string) ($it['type'] ?? $type) === 'theme';
            $it['installed'] = ($isTheme ? $this->themes->exists($slug) : in_array($slug, $pluginSlugs, true)) ? '1' : '';
            $it['price_label'] = (bool) ($it['is_paid'] ?? false)
                ? number_format((float) ($it['price'] ?? 0), 2) . ' ' . (string) ($it['currency'] ?? 'USD')
                : 'Бесплатно';
        }
        unset($it);
        return $items;
    }

    /** Удовлетворяет ли $current требованию вида ">=8.3" / "^1.0" / "1.2". Не распарсили → не блокируем. */
    private function meets(string $req, string $current): bool
    {
        if (!preg_match('/^\s*(>=|<=|>|<|=|\^|~)?\s*([0-9][0-9.]*)/', $req, $m)) {
            return true;
        }
        $op = $m[1] ?: '>=';
        if ($op === '^' || $op === '~' || $op === '=') {
            $op = $op === '=' ? '==' : '>=';
        }
        return version_compare($current, $m[2], $op);
    }

    private function guard(): ?Response
    {
        $user = $this->auth->user();
        if ($user === null) {
            return Response::redirect('/admin/login');
        }
        if ((string) ($user['role'] ?? '') !== 'admin') {
            return Response::html('<h1>403</h1><p>Доступ только для администратора.</p>', 403);
        }
        return null;
    }
}
