<?php
declare(strict_types=1);

/**
 * Общий bootstrap: инициализация Kernel, регистрация встроенных модулей и маршрутов.
 *
 * @return \Celena\Core\Kernel
 */

use Celena\Core\Container;
use Celena\Core\Database\Connection;
use Celena\Core\Http\Router;
use Celena\Core\Kernel;
use Celena\Core\Plugin\PluginManager;
use Celena\Core\Security\Auth;
use Celena\Core\Security\RateLimiter;
use Celena\Core\Template\BlockRegistry;
use Celena\Core\Template\ThemeManager;
use Celena\Module\Settings\Options;

$root = dirname(__DIR__);

require_once $root . '/core/Autoloader.php';
if (!class_exists('Celena\Core\Kernel', false) || !Kernel::container_available()) {
    \Celena\Core\Autoloader::register($root);
}

$kernel = Kernel::container_available() ? Kernel::bootedInstance() : (new Kernel($root))->boot();
$container = Kernel::container();

// ---------- Сервисы ----------
$container->singleton(Options::class, fn (): Options => new Options($container->make(Connection::class)));
$container->singleton(Auth::class, fn (): Auth => new Auth($container->make(Connection::class)));
$container->singleton(RateLimiter::class, fn (): RateLimiter => new RateLimiter($root . '/storage/cache/data'));
$container->singleton(BlockRegistry::class, fn (): BlockRegistry => new BlockRegistry());
$container->singleton(ThemeManager::class, fn (): ThemeManager => new ThemeManager($root . '/templates'));
$container->singleton(\Celena\Module\Translations\TranslationsController::class, fn () => new \Celena\Module\Translations\TranslationsController(
    $container->make(\Celena\Core\Template\Engine::class),
    $container->make(Auth::class),
    $container->make(Options::class),
    $root,
));
$container->singleton(\Celena\Core\Ai\AiManager::class, fn () => new \Celena\Core\Ai\AiManager($container->make(Options::class)));
$container->singleton(\Celena\Core\Queue\QueueManager::class, fn () => new \Celena\Core\Queue\QueueManager(
    $container->make(Connection::class),
    $container->make(\Celena\Core\Logger::class),
    $container,
));
$container->singleton(\Celena\Core\Mail\Mailer::class, fn () => new \Celena\Core\Mail\Mailer(
    $container->make(Options::class),
    $container->make(\Celena\Core\Logger::class),
    $root . '/storage/logs',
));

// Webhooks: авто-подписка на хуки при активной установке
if (is_file($root . '/install.lock')) {
    try {
        $dispatcher = new \Celena\Module\Webhooks\WebhookDispatcher(
            $container->make(Connection::class),
            $container->make(\Celena\Core\Queue\QueueManager::class),
        );
        $dispatcher->registerHooks();
    } catch (\Throwable $e) {
        // Если таблица webhooks ещё не создана — молча пропускаем
    }

    // Источник уведомлений по умолчанию: комментарии на модерации
    \Celena\Core\Plugin\Hook::addFilter('admin.notifications', function (array $items) use ($container): array {
        try {
            $conn = $container->make(Connection::class);
            $pending = $conn->builder('celena_comments')->where('status', '=', 'pending')->count();
            if ($pending > 0) {
                $items[] = [
                    'title' => 'Комментарии на модерации',
                    'text'  => $pending . ' шт. ждут проверки',
                    'url'   => '/admin/comments?status=pending',
                    'time'  => '',
                ];
            }
        } catch (\Throwable $e) {
        }
        return $items;
    });
}

// Активация локали на основе текущего запроса (web).
if (PHP_SAPI !== 'cli') {
    $request0 = \Celena\Core\Http\Request::fromGlobals();
    /** @var \Celena\Core\I18n\Locale $loc */
    $loc = $container->make(\Celena\Core\I18n\Locale::class);
    /** @var \Celena\Core\I18n\Translator $tr */
    $tr = $container->make(\Celena\Core\I18n\Translator::class);
    $active = $loc->detect($request0);
    $tr->setLocale($active);
    if ($request0->query('lang') !== null && $request0->query('lang') === $active) {
        $loc->remember($active);
    }
}

/** @var Router $router */
$router = $container->make(Router::class);

// Кастомная 404-страница: для фронта рендерим 404.tpl из активной темы.
$router->setNotFound(function (\Celena\Core\Http\Request $req) use ($container, $root) {
    // Для /admin/* отдаём простую 404 (админка ждёт auth).
    if (str_starts_with($req->path, '/admin')) {
        return \Celena\Core\Http\Response::notFound();
    }
    if ($req->wantsJson()) {
        return \Celena\Core\Http\Response::json(['error' => 'not_found'], 404);
    }
    try {
        /** @var Options $opt */
        $opt = $container->make(Options::class);
        /** @var \Celena\Core\Template\Engine $eng */
        $eng = $container->make(\Celena\Core\Template\Engine::class);
        $eng->setTheme((string) $opt->get('theme', 'default'));
        $html = $eng->render('404', [
            'site' => ['name' => (string) $opt->get('site_name', 'Celena')],
            'page' => ['title' => '404', 'seo_title' => '404', 'seo_description' => '', 'og_image' => ''],
        ]);
        return \Celena\Core\Http\Response::html($html, 404);
    } catch (\Throwable) {
        return \Celena\Core\Http\Response::notFound();
    }
});

// ---------- Публичные маршруты фронта ----------
$router->get('/', \Celena\Module\Dashboard\HomeController::class . '@index');

$router->get('/news',                       \Celena\Module\News\NewsController::class . '@publicList');
$router->get('/news/{slug:[^/]+}',    \Celena\Module\News\NewsController::class . '@publicShow');
$router->post('/news/{slug:[^/]+}/comments', \Celena\Module\Comments\PublicCommentsController::class . '@submit');
$router->get('/category/{slug:[^/]+}', \Celena\Module\Categories\PublicCategoriesController::class . '@show');
$router->get('/search',                     \Celena\Module\Search\SearchController::class . '@search');
$router->get('/p/{slug:[^/]+}',       \Celena\Module\Pages\PagesController::class . '@show');

// SEO
$router->get('/sitemap.xml', \Celena\Module\Seo\SeoController::class . '@sitemap');
$router->get('/robots.txt',  \Celena\Module\Seo\SeoController::class . '@robots');
$router->get('/rss.xml',     \Celena\Module\Seo\SeoController::class . '@rss');

// API: async-блоки
$router->get('/api/blocks/{name:[a-z0-9_\-]+}',  \Celena\Module\Dashboard\BlockController::class . '@show');
$router->post('/api/blocks/{name:[a-z0-9_\-]+}', \Celena\Module\Dashboard\BlockController::class . '@show');

// ---------- Личный кабинет посетителя (/account) ----------
$account      = \Celena\Module\Account\AccountController::class;
$customerAuth = \Celena\Module\Account\CustomerAuthController::class;
// Публичные (для гостя)
$router->get('/account/login',     $customerAuth . '@showLogin');
$router->post('/account/login',    $customerAuth . '@login');
$router->get('/account/register',  $customerAuth . '@showRegister');
$router->post('/account/register', $customerAuth . '@register');
// Защищённые кабинетом (гость → /account/login). Плагины (магазин) добавляют свои
// /account/* роуты в такой же группе с CustomerAuthMiddleware.
$router->group(['prefix' => '/account', 'middleware' => [\Celena\Core\Http\Middleware\CustomerAuthMiddleware::class]], function (Router $r) use ($account, $customerAuth) {
    $r->get('/',         $account . '@overview');
    $r->get('/profile',  $account . '@profile');
    $r->post('/profile', $account . '@updateProfile');
    $r->get('/password', $account . '@password');
    $r->post('/password', $account . '@updatePassword');
    $r->post('/logout',  $customerAuth . '@logout');
});

// ---------- Админка ----------
$authCtrl  = \Celena\Module\Users\AuthController::class;
$dashboard = \Celena\Module\Dashboard\AdminController::class;
$users     = \Celena\Module\Users\UsersController::class;
$settings  = \Celena\Module\Settings\SettingsController::class;
$cats      = \Celena\Module\Categories\CategoriesController::class;
$pages     = \Celena\Module\Pages\PagesController::class;
$news      = \Celena\Module\News\NewsController::class;
$media     = \Celena\Module\Media\MediaController::class;
$cfields   = \Celena\Module\CustomFields\CustomFieldsController::class;
$comments  = \Celena\Module\Comments\CommentsController::class;
$seo       = \Celena\Module\Seo\SeoController::class;
$plugins   = \Celena\Module\Plugins\PluginsController::class;

// Публичные (не требуют auth)
$router->get('/admin/login',  $authCtrl . '@showLogin');
$router->post('/admin/login', $authCtrl . '@login');
$router->post('/admin/logout', $authCtrl . '@logout');

// Всё остальное под /admin защищено AuthMiddleware
$router->group(['prefix' => '/admin', 'middleware' => [\Celena\Core\Http\Middleware\AuthMiddleware::class]], function (Router $r) use (
    $dashboard, $users, $settings, $cats, $pages, $news, $media, $cfields, $comments, $seo, $plugins
) {
    // Дашборд
    $r->get('/', $dashboard . '@index');

    // Уведомления (страница «все уведомления»)
    $r->get('/notifications', \Celena\Module\AdminBase\NotificationsController::class . '@index');

    // Пользователи
    $r->get('/users',                         $users . '@index');
    $r->get('/users/create',                  $users . '@create');
    $r->post('/users/create',                 $users . '@store');
    $r->get('/users/{id:\d+}/edit',           $users . '@edit');
    $r->post('/users/{id:\d+}/update',        $users . '@update');
    $r->post('/users/{id:\d+}/delete',        $users . '@delete');

    // Настройки
    $r->get('/settings',                      $settings . '@index');
    $r->post('/settings',                     $settings . '@save');
    $r->post('/settings/clear-cache',         $settings . '@clearCache');

    // Категории
    $r->get('/categories',                    $cats . '@index');
    $r->get('/categories/create',             $cats . '@create');
    $r->post('/categories/create',            $cats . '@store');
    $r->get('/categories/{id:\d+}/edit',      $cats . '@edit');
    $r->post('/categories/{id:\d+}/update',   $cats . '@update');
    $r->post('/categories/{id:\d+}/delete',   $cats . '@delete');

    // Страницы
    $r->get('/pages',                         $pages . '@index');
    $r->get('/pages/create',                  $pages . '@create');
    $r->post('/pages/create',                 $pages . '@store');
    $r->get('/pages/{id:\d+}/edit',           $pages . '@edit');
    $r->post('/pages/{id:\d+}/update',        $pages . '@update');
    $r->post('/pages/{id:\d+}/delete',        $pages . '@delete');

    // Новости
    $r->get('/news',                          $news . '@index');
    $r->get('/news/create',                   $news . '@create');
    $r->post('/news/create',                  $news . '@store');
    $r->get('/news/{id:\d+}/edit',            $news . '@edit');
    $r->post('/news/{id:\d+}/update',         $news . '@update');
    $r->post('/news/{id:\d+}/delete',         $news . '@delete');

    // Медиа
    $r->get('/media',                         $media . '@index');
    $r->get('/media/pick',                    $media . '@pick');
    $r->post('/media/upload',                 $media . '@upload');
    $r->post('/media/upload-images',          $media . '@uploadImages');
    $r->post('/media/{id:\d+}/delete',        $media . '@delete');

    // Кастомные поля
    $r->get('/custom-fields',                 $cfields . '@index');
    $r->get('/custom-fields/create',          $cfields . '@create');
    $r->post('/custom-fields/create',         $cfields . '@store');
    $r->get('/custom-fields/{id:\d+}/edit',   $cfields . '@edit');
    $r->post('/custom-fields/{id:\d+}/update',$cfields . '@update');
    $r->post('/custom-fields/{id:\d+}/delete',$cfields . '@delete');

    // Комментарии
    $r->get('/comments',                      $comments . '@index');
    $r->post('/comments/{id:\d+}/status',     $comments . '@setStatus');
    $r->post('/comments/{id:\d+}/delete',     $comments . '@delete');

    // SEO
    $r->get('/seo',                           $seo . '@index');

    // Плагины / Шаблоны
    $r->get('/plugins',                       $plugins . '@index');
    $r->post('/plugins/upload',               $plugins . '@upload');
    $r->post('/plugins/{slug:[a-z0-9_\-]+}/toggle', $plugins . '@toggle');
    $r->post('/plugins/{slug:[a-z0-9_\-]+}/delete', $plugins . '@delete');
    $r->get('/templates',                     $plugins . '@templates');
    $r->get('/templates/{theme:[a-z0-9_\-]+}/edit',     \Celena\Module\Plugins\TemplateEditorController::class . '@edit');
    $r->post('/templates/{theme:[a-z0-9_\-]+}/save',    \Celena\Module\Plugins\TemplateEditorController::class . '@save');
    $r->post('/templates/{theme:[a-z0-9_\-]+}/preview', \Celena\Module\Plugins\TemplateEditorController::class . '@preview');

    // ИИ
    $r->get('/ai-settings',                   \Celena\Module\Ai\AiSettingsController::class . '@index');
    $r->post('/ai-settings',                  \Celena\Module\Ai\AiSettingsController::class . '@save');
    $r->post('/api/ai/run',                   \Celena\Module\Ai\AiController::class . '@run');

    // Webhooks
    $r->get('/webhooks',                      \Celena\Module\Webhooks\WebhooksController::class . '@index');
    $r->post('/webhooks/create',              \Celena\Module\Webhooks\WebhooksController::class . '@store');
    $r->post('/webhooks/{id:\d+}/toggle',     \Celena\Module\Webhooks\WebhooksController::class . '@toggle');
    $r->post('/webhooks/{id:\d+}/delete',     \Celena\Module\Webhooks\WebhooksController::class . '@delete');

    // Баннеры
    $r->get('/banners',                          \Celena\Module\Banners\BannersController::class . '@index');
    $r->get('/banners/create',                   \Celena\Module\Banners\BannersController::class . '@create');
    $r->post('/banners/create',                  \Celena\Module\Banners\BannersController::class . '@store');
    $r->get('/banners/{id:\d+}/edit',            \Celena\Module\Banners\BannersController::class . '@edit');
    $r->post('/banners/{id:\d+}/update',         \Celena\Module\Banners\BannersController::class . '@update');
    $r->post('/banners/{id:\d+}/delete',         \Celena\Module\Banners\BannersController::class . '@delete');

    // Меню сайта
    $r->get('/menus',                            \Celena\Module\Menus\MenusController::class . '@index');
    $r->get('/menus/create',                     \Celena\Module\Menus\MenusController::class . '@create');
    $r->post('/menus/create',                    \Celena\Module\Menus\MenusController::class . '@store');
    $r->get('/menus/{id:\d+}/edit',              \Celena\Module\Menus\MenusController::class . '@edit');
    $r->post('/menus/{id:\d+}/delete',           \Celena\Module\Menus\MenusController::class . '@delete');
    $r->post('/menus/{id:\d+}/items/create',     \Celena\Module\Menus\MenusController::class . '@itemStore');
    $r->post('/menus/items/{itemId:\d+}/delete', \Celena\Module\Menus\MenusController::class . '@itemDelete');

    // Переводы
    $r->get('/translations',                     \Celena\Module\Translations\TranslationsController::class . '@index');
    $r->post('/translations',                    \Celena\Module\Translations\TranslationsController::class . '@save');

    // Города
    $r->get('/cities',                           \Celena\Module\Cities\CitiesController::class . '@index');
    $r->get('/cities/create',                    \Celena\Module\Cities\CitiesController::class . '@create');
    $r->post('/cities/create',                   \Celena\Module\Cities\CitiesController::class . '@store');
    $r->get('/cities/{id:\d+}/edit',             \Celena\Module\Cities\CitiesController::class . '@edit');
    $r->post('/cities/{id:\d+}/update',          \Celena\Module\Cities\CitiesController::class . '@update');
    $r->post('/cities/{id:\d+}/delete',          \Celena\Module\Cities\CitiesController::class . '@delete');

    // Профиль
    $r->get('/profile',                       \Celena\Module\Users\ProfileController::class . '@index');
    $r->post('/profile',                      \Celena\Module\Users\ProfileController::class . '@save');

    // Документация (справочник тегов и плагинов)
    $r->get('/docs/tags',                     \Celena\Module\Docs\DocsController::class . '@tags');
    $r->get('/docs/tags-panel',               \Celena\Module\Docs\DocsController::class . '@tagsPanel');
    $r->get('/docs/plugin-panel',             \Celena\Module\Docs\DocsController::class . '@pluginDocsPanel');

    // Маркетплейс (клиент — установка плагинов/тем из реестра)
    $mp = \Celena\Module\Marketplace\MarketplaceController::class;
    $r->get('/marketplace',                                       $mp . '@index');
    $r->get('/marketplace/updates',                               $mp . '@updates');
    $r->get('/marketplace/item/{slug:[a-z0-9][a-z0-9_\-]*}',      $mp . '@item');
    $r->post('/marketplace/install',                              $mp . '@install');
});

// ---------- Глобальные переменные шаблонов ----------
// Дефолты, которые шаблоны темы могут проверять без падения, когда соответствующего
// плагина нет. Плагины подписываются на тот же фильтр с приоритетом > 0 и
// перезаписывают значения (например, shop включает shop.enabled='1').
\Celena\Core\Plugin\Hook::addFilter('template.globals', function (array $vars): array {
    if (!isset($vars['shop']) || !is_array($vars['shop'])) {
        $vars['shop'] = ['enabled' => '0'];
    } elseif (!array_key_exists('enabled', $vars['shop'])) {
        $vars['shop']['enabled'] = '0';
    }
    return $vars;
}, 0);

// Версия CMS — выводится в шапке админки ({cms_version}). Единая точка правки.
\Celena\Core\Plugin\Hook::addFilter('template.globals', function (array $vars): array {
    $vars['cms_version'] = '3.5';
    return $vars;
}, 0);

// Пользовательский код сайта ({custom_head_html}/{custom_foot_html}) — счётчики,
// чат-виджеты и т.п. Значение хранится в БД конкретной установки (опции
// custom_head_html / custom_foot_html), а НЕ в коде — поэтому при распространении
// CMS чужие скрипты не попадают на другие сайты. Пусто по умолчанию.
\Celena\Core\Plugin\Hook::addFilter('template.globals', function (array $vars) use ($container): array {
    $vars['custom_head_html'] = '';
    $vars['custom_foot_html'] = '';
    try {
        $opt = $container->make(Options::class);
        $vars['custom_head_html'] = (string) $opt->get('custom_head_html', '');
        $vars['custom_foot_html'] = (string) $opt->get('custom_foot_html', '');
    } catch (\Throwable) {
    }
    return $vars;
}, 0);

// Глобаль `account` — текущий посетитель (для шапки темы и кабинета). Доступно во всех
// шаблонах: {account.is_guest}, {account.name}. Контроллер может задать своё значение —
// тогда не перетираем.
\Celena\Core\Plugin\Hook::addFilter('template.globals', function (array $vars): array {
    if (isset($vars['account']) && is_array($vars['account'])) {
        return $vars;
    }
    try {
        $auth = \Celena\Core\Kernel::container()->make(\Celena\Core\Security\Auth::class);
        $u = $auth->user();
    } catch (\Throwable) {
        $u = null;
    }
    $vars['account'] = [
        'is_guest' => $u === null ? '1' : '0',
        'id'       => (int) ($u['id'] ?? 0),
        'name'     => (string) ($u['name'] ?? ''),
        'email'    => (string) ($u['email'] ?? ''),
    ];
    return $vars;
}, 0);

// SEO-глобали: текущая локаль (для <html lang>) и самоссылающийся canonical для каждого языка.
\Celena\Core\Plugin\Hook::addFilter('template.globals', function (array $vars): array {
    try {
        $c   = \Celena\Core\Kernel::container();
        $loc = $c->make(\Celena\Core\I18n\Locale::class);
        $cur = $loc->detect(\Celena\Core\Http\Request::fromGlobals());
        $def = $loc->default();
        $host   = (string) ($_SERVER['HTTP_HOST'] ?? (parse_url((string) env('APP_URL', ''), PHP_URL_HOST) ?: 'localhost'));
        $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
        $path   = (string) (parse_url((string) ($_SERVER['REQUEST_URI'] ?? '/'), PHP_URL_PATH) ?: '/');
        $vars['locale'] = $cur;
        $vars['canonical_self'] = $scheme . '://' . $host . $path . ($cur !== $def ? '?lang=' . $cur : '');
    } catch (\Throwable) {
        $vars['locale'] = $vars['locale'] ?? 'en';
        $vars['canonical_self'] = $vars['canonical_self'] ?? '/';
    }
    return $vars;
}, 0);

// Пункт сайдбара админки «Маркетплейс» (клиент установки плагинов/тем).
// Показывается только когда адрес маркетплейса настроен (опция marketplace_url
// или env MARKETPLACE_URL) — иначе на проде до поднятия реестра пункт не висит.
\Celena\Core\Plugin\Hook::addFilter('admin.sidebar', function (array $items) use ($container): array {
    try {
        $opt = $container->make(Options::class);
        $url = (string) $opt->get('marketplace_url', '');
        if ($url === '') {
            $cfg = $container->make(\Celena\Core\Config::class);
            $url = (string) ($cfg->get('marketplace.url') ?? '');
            // env-дефолт не считаем «настроенным», если он не задан явно в .env
            if ($url === 'https://marketplace.celena.io' && (string) env('MARKETPLACE_URL', '') === '') {
                $url = '';
            }
        }
        if ($url === '') {
            return $items;
        }
    } catch (\Throwable) {
        return $items;
    }
    $icon = new \Celena\Core\Template\Tags\IconTag();
    $items[] = [
        'svg'   => $icon(['name' => 'plugins', 'size' => 22, 'class' => 'sb-ico']),
        'label' => 'Маркетплейс',
        'url'   => '/admin/marketplace',
        'slug'  => 'marketplace',
    ];
    return $items;
});

// ---------- Контентные блок-теги [news], [categories], [products], … ----------
// Универсальный блок-тег для списков. Источник предоставляет fetcher (модуль или плагин).
/** @var \Celena\Core\Template\TagRegistry $tags */
$tags = $container->make(\Celena\Core\Template\TagRegistry::class);
$tags->registerBlock('news', new \Celena\Core\Template\Tags\ContentListTag(
    function (array $params) use ($container): array {
        return (new \Celena\Module\News\NewsContentSource($container->make(Connection::class)))->fetch($params);
    },
    ['limit' => 10, 'sort' => 'published_at', 'order' => 'desc', 'status' => 'published'],
));
$tags->registerBlock('categories', new \Celena\Core\Template\Tags\ContentListTag(
    function (array $params) use ($container): array {
        return (new \Celena\Module\Categories\CategoriesContentSource($container->make(Connection::class)))->fetch($params);
    },
    ['limit' => 10, 'sort' => 'news_count', 'order' => 'desc', 'entity' => 'news'],
));
$tags->registerBlock('banners', new \Celena\Core\Template\Tags\ContentListTag(
    function (array $params) use ($container): array {
        return (new \Celena\Module\Banners\BannersContentSource($container->make(Connection::class)))->fetch($params);
    },
    ['limit' => 10, 'sort' => 'sort_order', 'order' => 'asc', 'active' => '1'],
));

// {menu name="header" class="..."} — навигационное меню по slug-у из admin/menus.
// MenuTag сам ходит в БД; кеш — на уровне процесса (один SELECT на slug за запрос).
$tags->registerInline('menu', new \Celena\Core\Template\Tags\MenuTag(
    $container->make(Connection::class)
));

// ---------- Async-блоки ----------
/** @var BlockRegistry $blocks */
$blocks = $container->make(BlockRegistry::class);

$blocks->register('latest_news', function (Container $c, array $params): array {
    /** @var Connection $conn */
    $conn = $c->make(Connection::class);
    $news = $conn->builder('celena_news')
        ->where('status', '=', 'published')
        ->orderBy('published_at', 'desc')
        ->limit(5)
        ->get();
    foreach ($news as &$n) {
        $n['url'] = '/news/' . $n['slug'];
        $n['date'] = (string) ($n['published_at'] ?? '');
    }
    unset($n);
    return [
        'template' => 'async/latest_news',
        'vars' => ['news' => $news, 'news_empty' => $news === [] ? '1' : '0'],
    ];
});

$blocks->register('recent_activity', function (Container $c, array $params): array {
    return [
        'template' => 'admin/async/recent_activity',
        'vars' => ['items' => [
            ['time' => date('H:i'), 'text' => 'Система работает.'],
        ]],
    ];
});

// ---------- Догон системных миграций ----------
// Раннер идемпотентен (хранит применённое в celena_migrations), новые файлы
// применятся, существующие пропустятся. Один SELECT за запрос — дёшево, зато
// разработчик видит свою миграцию без отдельной CLI-команды.
if (is_file($root . '/install.lock')) {
    try {
        $migrationsDir = $root . '/core/Database/migrations';
        if (is_dir($migrationsDir)) {
            $coreMigrationRunner = new \Celena\Core\Database\MigrationRunner(
                $container->make(\Celena\Core\Database\Connection::class),
                $container->make(\Celena\Core\Logger::class),
            );
            $coreMigrationRunner->run($migrationsDir);
        }
    } catch (\Throwable) {
    }
}

// ---------- Плагины ----------
if (is_file($root . '/install.lock')) {
    /** @var Options $options */
    $options = $container->make(Options::class);
    $active = json_decode((string) $options->get('active_plugins', '[]'), true) ?: [];
    if (is_array($active) && $active !== []) {
        /** @var PluginManager $pm */
        $pm = $container->make(PluginManager::class);
        $pm->discover();
        $pm->load($active);
    }
}

return $kernel;
