<?php
declare(strict_types=1);

namespace Celena\Core\Template;

use Celena\Core\Template\Tags\AsyncTag;
use Celena\Core\Template\Tags\CsrfTag;
use Celena\Core\Template\Tags\ForeachTag;
use Celena\Core\Template\Tags\GdprBannerTag;
use Celena\Core\Template\Tags\IconTag;
use Celena\Core\Template\Tags\IncludeTag;
use Celena\Core\Template\Tags\LangTag;

/**
 * Главный фасад шаблонизатора. Кеширует скомпилированные шаблоны на диск.
 * Текущая активная тема настраивается через setTheme().
 */
final class Engine
{
    private string $theme = 'default';
    private readonly Compiler $compiler;

    public function __construct(
        private readonly string $templatesPath,
        private readonly string $cachePath,
        private readonly TagRegistry $tags,
        private readonly bool $cacheEnabled = true,
    ) {
        if (!is_dir($this->cachePath)) {
            @mkdir($this->cachePath, 0755, true);
        }
        $this->compiler = new Compiler($tags);
    }

    public function setTheme(string $theme): void { $this->theme = $theme; }
    public function getTheme(): string            { return $this->theme; }
    public function tags(): TagRegistry            { return $this->tags; }

    public function registerBuiltinTags(): void
    {
        $this->tags->registerInline('include', new IncludeTag($this));
        $this->tags->registerInline('lang', new LangTag());
        $this->tags->registerInline('async', new AsyncTag());
        $this->tags->registerInline('csrf', new CsrfTag());
        $this->tags->registerInline('icon', new IconTag());
        $this->tags->registerInline('gdpr_banner', new GdprBannerTag());
        $this->tags->registerInline('plugin_assets', function () {
            try {
                $pm = \Celena\Core\Kernel::container()->make(\Celena\Core\Plugin\PluginManager::class);
                $css = $pm->activeAssetUrls();
                $js = $pm->activeScriptUrls();
            } catch (\Throwable) {
                return '';
            }
            $html = '';
            foreach ($css as $u) {
                $html .= '<link rel="stylesheet" href="' . e($u) . '">' . "\n";
            }
            foreach ($js as $u) {
                $html .= '<script src="' . e($u) . '" defer></script>' . "\n";
            }
            return $html;
        });

        $this->tags->registerBlock('foreach', new ForeachTag());
    }

    /**
     * Отрендерить шаблон. Имя — относительно темы, без расширения.
     * Например: render('home', ['title' => 'Hi']) → templates/{theme}/home.tpl
     */
    public function render(string $template, array $vars = []): string
    {
        $vars = \Celena\Core\Plugin\Hook::filter('template.globals', $vars, $template);
        $file = $this->resolve($template);
        $compiled = $this->ensureCompiled($file);
        return $this->renderCompiled($compiled, $vars);
    }

    public function renderInclude(string $template, array $vars): string
    {
        return $this->render($template, $vars);
    }

    /**
     * Скомпилировать произвольный исходник шаблона (например, для предпросмотра)
     * и сразу отрендерить — без записи во папку темы. Кеш не используется,
     * каждый вызов компилирует заново.
     */
    public function renderString(string $source, array $vars = []): string
    {
        $php = $this->compiler->compile($source, '<inline>');
        $tmpFile = $this->cachePath . '/__inline_' . bin2hex(random_bytes(4)) . '.php';
        @file_put_contents($tmpFile, $php, LOCK_EX);
        try {
            return $this->renderCompiled($tmpFile, $vars);
        } finally {
            @unlink($tmpFile);
        }
    }

    public function flush(?string $template = null): void
    {
        if ($template !== null) {
            $file = $this->resolve($template);
            @unlink($this->compiledPath($file));
            return;
        }
        $files = glob($this->cachePath . '/*.php') ?: [];
        foreach ($files as $f) {
            @unlink($f);
        }
    }

    /** Доступ к переменным с поддержкой dot-нотации. */
    public function v(array $vars, string $key): mixed
    {
        if (!str_contains($key, '.')) {
            return $vars[$key] ?? '';
        }
        $segments = explode('.', $key);
        $current = $vars;
        foreach ($segments as $seg) {
            if (is_array($current) && array_key_exists($seg, $current)) {
                $current = $current[$seg];
            } elseif (is_object($current) && isset($current->$seg)) {
                $current = $current->$seg;
            } else {
                return '';
            }
        }
        return $current;
    }

    public function callInline(string $name, array $params, array $vars): string
    {
        return $this->tags->renderInline($name, $params, $vars);
    }

    public function callBlock(string $name, array $params, array $vars, callable $renderBody): string
    {
        $handler = $this->tags->resolveBlockHandler($name);
        if ($handler === null) {
            // Если блочный тег не зарегистрирован — рендерим тело один раз с текущими $vars.
            return (string) $renderBody($vars);
        }
        if (is_string($handler) && class_exists($handler)) {
            $handler = new $handler();
        }
        if (is_callable($handler)) {
            return (string) $handler($params, $vars, $renderBody);
        }
        return '';
    }

    private function resolve(string $template): string
    {
        // защита от выхода за пределы темы
        $template = ltrim(str_replace(['..', '\\'], ['', '/'], $template), '/');
        if (!str_ends_with($template, '.tpl')) {
            $template .= '.tpl';
        }
        // 0) Плагины: plugins/{slug}/{path}
        // Порядок поиска (тема может переопределять плагинные шаблоны):
        //   0a) templates/{active_theme}/plugins/{slug}/{path}.tpl  ← оверрайд активной темы
        //   0b) templates/default/plugins/{slug}/{path}.tpl         ← оверрайд default-темы (fallback)
        //   0c) plugins/{slug}/templates/{path}.tpl                 ← оригинал плагина
        if (str_starts_with($template, 'plugins/')) {
            $parts = explode('/', $template, 3);
            if (count($parts) === 3) {
                [, $slug, $rest] = $parts;
                // 0a — активная тема
                $themedOverride = $this->templatesPath . '/' . $this->theme . '/plugins/' . $slug . '/' . $rest;
                if (is_file($themedOverride)) {
                    return $themedOverride;
                }
                // 0b — default-тема (если активная не default)
                if ($this->theme !== 'default') {
                    $defaultOverride = $this->templatesPath . '/default/plugins/' . $slug . '/' . $rest;
                    if (is_file($defaultOverride)) {
                        return $defaultOverride;
                    }
                }
                // 0c — оригинальный шаблон плагина
                $pluginPath = dirname($this->templatesPath) . '/plugins/' . $slug . '/templates/' . $rest;
                if (is_file($pluginPath)) {
                    return $pluginPath;
                }
            }
        }
        // 1) Тема
        $themed = $this->templatesPath . '/' . $this->theme . '/' . $template;
        if (is_file($themed)) {
            return $themed;
        }
        // 2) Дефолтная тема (fallback)
        $fallback = $this->templatesPath . '/default/' . $template;
        if (is_file($fallback)) {
            return $fallback;
        }
        // 3) Прямой путь (для админки и других «меток»)
        $direct = $this->templatesPath . '/' . $template;
        if (is_file($direct)) {
            return $direct;
        }
        throw new \RuntimeException("Template not found: $template");
    }

    private function ensureCompiled(string $sourceFile): string
    {
        $compiled = $this->compiledPath($sourceFile);
        if ($this->cacheEnabled && is_file($compiled) && filemtime($compiled) >= filemtime($sourceFile)) {
            return $compiled;
        }
        $source = (string) file_get_contents($sourceFile);
        $relative = str_replace($this->templatesPath . '/', '', $sourceFile);
        $php = $this->compiler->compile($source, $relative);
        @file_put_contents($compiled, $php, LOCK_EX);
        return $compiled;
    }

    private function compiledPath(string $sourceFile): string
    {
        $hash = sha1($sourceFile);
        return $this->cachePath . '/' . $hash . '.php';
    }

    private function renderCompiled(string $file, array $vars): string
    {
        $__e = $this;
        ob_start();
        include $file;
        return (string) ob_get_clean();
    }
}
