<?php
declare(strict_types=1);

namespace Celena\Core;

use Celena\Core\Database\Connection;
use Celena\Core\Http\Request;
use Celena\Core\Http\Response;
use Celena\Core\Http\Router;
use Celena\Core\Plugin\PluginManager;
use Celena\Core\Security\Csrf;
use Celena\Core\Template\Engine;
use Celena\Core\Template\TagRegistry;
use Throwable;

/**
 * Точка bootstrap всего приложения.
 * Инициализирует автозагрузчик (если ещё не), контейнер, конфиг, ошибки.
 */
final class Kernel
{
    private static ?Container $container = null;
    private static string $rootPath = '';

    public function __construct(string $rootPath)
    {
        self::$rootPath = rtrim($rootPath, '/\\');
    }

    public static function container(): Container
    {
        if (self::$container === null) {
            throw new \RuntimeException('Kernel not booted.');
        }
        return self::$container;
    }

    public static function container_available(): bool
    {
        return self::$container !== null;
    }

    private static ?self $instance = null;

    public static function bootedInstance(): self
    {
        if (self::$instance === null) {
            throw new \RuntimeException('Kernel not booted.');
        }
        return self::$instance;
    }

    public static function rootPath(): string
    {
        return self::$rootPath;
    }

    public function boot(): self
    {
        // Глобальные хелперы
        require_once self::$rootPath . '/core/Functions.php';

        self::$instance = $this;

        // Контейнер. Регистрируем сам себя — auto-resolve в конструкторах
        // для параметра типа Container будет возвращать тот же экземпляр.
        self::$container = new Container();
        self::$container->instance(Container::class, self::$container);

        // Конфиг
        $config = new Config(
            configDir: self::$rootPath . '/config',
            envFile: self::$rootPath . '/.env',
        );
        self::$container->instance(Config::class, $config);

        // Логгер
        $logger = new Logger(
            logDir: self::$rootPath . '/storage/logs',
            minLevel: (string) ($config->get('app.log_level') ?? env('LOG_LEVEL', 'warning')),
        );
        self::$container->instance(Logger::class, $logger);

        // Обработчики ошибок.
        // В production трейс наружу не отдаём независимо от значения APP_DEBUG —
        // защита от случая, когда оператор оставил debug=true при выкатке.
        $env = strtolower((string) ($config->get('app.env') ?? env('APP_ENV', 'production')));
        $debugCfg = (bool) ($config->get('app.debug') ?? false);
        $debug = $env === 'production' ? false : $debugCfg;
        $this->registerErrorHandlers($logger, $debug);
        if ($debugCfg && !$debug) {
            $logger->warning('APP_DEBUG=true в production-окружении — игнорируется ради безопасности.');
        }

        // Установить дефолтную таймзону
        date_default_timezone_set((string) ($config->get('app.timezone') ?? 'UTC'));

        // Сессии (только если не CLI и не в установщике)
        if (PHP_SAPI !== 'cli' && session_status() === PHP_SESSION_NONE && !headers_sent()) {
            $this->configureSession($config);
            session_start();
        }

        // Соединение с БД (ленивое — singleton)
        self::$container->singleton(Connection::class, function (): Connection {
            /** @var Config $cfg */
            $cfg = self::$container->make(Config::class);
            return new Connection((array) $cfg->get('database', []));
        });

        // Шаблонизатор
        self::$container->singleton(TagRegistry::class, fn (): TagRegistry => new TagRegistry());
        self::$container->singleton(Engine::class, function (): Engine {
            /** @var Config $cfg */
            $cfg = self::$container->make(Config::class);
            $tags = self::$container->make(TagRegistry::class);
            $engine = new Engine(
                templatesPath: self::$rootPath . '/templates',
                cachePath: self::$rootPath . '/storage/cache/tpl',
                tags: $tags,
                cacheEnabled: (bool) ($cfg->get('cache.templates') ?? true),
            );
            $engine->registerBuiltinTags();
            return $engine;
        });

        // Роутер
        self::$container->singleton(Router::class, fn (): Router => new Router(self::$container));

        // PluginManager (lazy)
        self::$container->singleton(PluginManager::class, function (): PluginManager {
            return new PluginManager(self::$rootPath . '/plugins', self::$container);
        });

        // Csrf — статический фасад, инициализируем токен
        Csrf::init();

        // I18n: переводчик + локаль. Доступные языки — по файлам core/languages/*.json.
        self::$container->singleton(\Celena\Core\I18n\Locale::class, function () use ($config): \Celena\Core\I18n\Locale {
            $available = [];
            foreach (glob(self::$rootPath . '/core/languages/*.json') ?: [] as $f) {
                $code = basename($f, '.json');
                if (preg_match('/^[a-z]{2}(-[A-Z]{2})?$/', $code)) $available[] = $code;
            }
            if ($available === []) $available = ['ru'];
            return new \Celena\Core\I18n\Locale(
                default: (string) ($config->get('app.locale') ?? 'ru'),
                available: $available,
            );
        });
        self::$container->singleton(\Celena\Core\I18n\Translator::class, function () use ($config): \Celena\Core\I18n\Translator {
            $locale = (string) ($config->get('app.locale') ?? 'ru');
            $t = new \Celena\Core\I18n\Translator($locale);
            $dirs = [self::$rootPath . '/core/languages'];
            foreach (glob(self::$rootPath . '/modules/*/languages', GLOB_ONLYDIR) ?: [] as $d) $dirs[] = $d;
            foreach (glob(self::$rootPath . '/plugins/*/languages', GLOB_ONLYDIR) ?: [] as $d) $dirs[] = $d;
            foreach ($dirs as $dir) {
                foreach (glob($dir . '/*.json') ?: [] as $file) {
                    $t->loadFile($file, basename($file, '.json'));
                }
            }
            return $t;
        });

        return $this;
    }

    public function handle(Request $request): Response
    {
        try {
            /** @var Router $router */
            $router = self::$container->make(Router::class);
            return $router->dispatch($request);
        } catch (Throwable $e) {
            return $this->renderException($e);
        }
    }

    public function send(Response $response): void
    {
        $response->send();
    }

    private function renderException(Throwable $e): Response
    {
        /** @var Logger $logger */
        $logger = self::$container->make(Logger::class);
        $logger->error($e->getMessage(), [
            'exception' => get_class($e),
            'file' => $e->getFile(),
            'line' => $e->getLine(),
        ]);

        /** @var Config $config */
        $config = self::$container->make(Config::class);
        $env = strtolower((string) ($config->get('app.env') ?? env('APP_ENV', 'production')));
        $debug = $env !== 'production' && (bool) ($config->get('app.debug') ?? false);
        if ($debug) {
            $body = '<h1>500 Server Error</h1><pre style="background:#111;color:#eee;padding:12px;">'
                . htmlspecialchars($e::class . ': ' . $e->getMessage(), ENT_QUOTES, 'UTF-8')
                . "\nat " . htmlspecialchars($e->getFile() . ':' . $e->getLine(), ENT_QUOTES, 'UTF-8')
                . "\n\n" . htmlspecialchars($e->getTraceAsString(), ENT_QUOTES, 'UTF-8')
                . '</pre>';
            return Response::html($body, 500);
        }
        return Response::error(500, 'Internal Server Error');
    }

    private function registerErrorHandlers(Logger $logger, bool $debug): void
    {
        ini_set('display_errors', $debug ? '1' : '0');
        error_reporting(E_ALL);

        set_error_handler(function (int $severity, string $message, string $file, int $line) {
            if (!(error_reporting() & $severity)) {
                return false;
            }
            throw new \ErrorException($message, 0, $severity, $file, $line);
        });

        set_exception_handler(function (Throwable $e) use ($logger) {
            $logger->critical($e->getMessage(), [
                'exception' => get_class($e),
                'file' => $e->getFile() . ':' . $e->getLine(),
            ]);
            if (PHP_SAPI === 'cli') {
                fwrite(STDERR, $e::class . ': ' . $e->getMessage() . PHP_EOL);
                fwrite(STDERR, $e->getTraceAsString() . PHP_EOL);
            } else {
                http_response_code(500);
                echo '<h1>500 Server Error</h1>';
            }
        });
    }

    private function configureSession(Config $config): void
    {
        $name = (string) ($config->get('app.session.name') ?? 'celena_sess');
        session_name($name);

        $lifetime = (int) ($config->get('app.session.lifetime') ?? 86400);

        // Изолированное хранилище сессий внутри проекта. На общем save_path (OSPanel
        // держит один каталог сессий на все сайты) сборщик мусора чужого сайта чистит
        // файлы по СВОЕМУ gc_maxlifetime и может удалить наши свежие сессии. Своя папка
        // оставляет уборку только нашим процессам — с нашим gc_maxlifetime ниже.
        $sessionPath = self::$rootPath . '/storage/sessions';
        if (is_dir($sessionPath) || @mkdir($sessionPath, 0775, true)) {
            session_save_path($sessionPath);
        }

        // Серверный файл сессии должен жить не меньше cookie. Иначе сборщик мусора удалит
        // сессию (вместе с CSRF-токеном) раньше, чем истечёт кука: «полежавшая» форма логина
        // отправится со старым токеном в новую пустую сессию → «CSRF-токен недействителен».
        if ($lifetime > 0 && (int) ini_get('session.gc_maxlifetime') < $lifetime) {
            ini_set('session.gc_maxlifetime', (string) $lifetime);
        }

        // Отвергать неизвестный session id: защита от session fixation и «самоисцеление»
        // застрявшей в браузере старой/битой куки — сервер выдаст свежую сессию с Set-Cookie,
        // вместо того чтобы молча принять чужой id и работать с пустой сессией.
        ini_set('session.use_strict_mode', '1');

        // Secure-флаг включаем автоматически когда APP_URL начинается с https://
        // или текущий запрос уже под TLS. На http-локалке — false (иначе браузер не примет куку).
        $appUrl = (string) ($config->get('app.url') ?? '');
        $secure = str_starts_with($appUrl, 'https://')
            || (($_SERVER['HTTPS'] ?? 'off') !== 'off')
            || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https')
            || ((int) ($_SERVER['SERVER_PORT'] ?? 0) === 443);
        session_set_cookie_params([
            'lifetime' => $lifetime,
            'path' => '/',
            'domain' => '',
            'secure' => $secure,
            'httponly' => true,
            'samesite' => 'Lax',
        ]);
    }
}
