<?php
namespace Core;

use Exception;

class View
{
    private string $viewName;
    private array $data = [];
    private ?string $layout = null;
    private bool $minify = true;
    private bool $retunRaw = false;

    public static array $headAssets = [];
    public static array $bodyScripts = [];
    public static array $globalData = [];

    protected static array $compiledScripts = [];

    /**
     * Renders a view file and returns its content as a string, without a layout.
     * This is ideal for generating HTML content for emails or API responses.
     *
     * @param string $viewName The name of the view file (e.g., 'emails/welcome').
     * @param array $data The data to be passed to the view.
     * @return string The rendered HTML content.
     */
    public static function renderForEmail(string $viewName, array $data = []): string
    {
        $instance = new self($viewName);
        $instance->with($data);
        $instance->minify = false;

        // Email view file
        $viewFile = FS_APP_PATH . '/Views/Email/' . $viewName . '.email.php';
        if (!file_exists($viewFile)) {
            throw new Exception("View file not found: {$viewFile}");
        }

        // 1️⃣ Read RAW template (do NOT execute yet)
        $rawContent = file_get_contents($viewFile);

        // 2️⃣ Compile template ({{ }} → PHP, @if, @foreach, etc)
        $compiledContent = $instance->compile($rawContent);

        // 3️⃣ Execute compiled template with variables
        ob_start();
        extract($instance->data, EXTR_SKIP);

        // safest execution method
        eval('?>' . $compiledContent);

        $finalHtml = ob_get_clean();

        return $finalHtml;
    }


    public static function addHead(string $assets): void
    {
        if (!in_array($assets, self::$headAssets)) {
            self::$headAssets[] = $assets;
        }
    }

    public static function addScript(string $script): void
    {
        if (!in_array($script, self::$bodyScripts)) {
            self::$bodyScripts[] = $script;
        }
    }

    public static function renderHead(): string
    {
        return implode("\n", self::$headAssets);
    }

    public static function renderScripts(): string
    {
        return implode("\n", self::$bodyScripts);
    }

    public static function setGlobalData(array $data): void
    {
        self::$globalData = $data;
    }

    /**
     * Default globally available components
     */
    private array $defaultComponents = [
        'Button' => \App\Components\Button::class,
        'StatsCard' => \App\Components\StatsCard::class,
        'DataTable'=> \App\Components\DataTable::class,
        'FormBuilder'=> \App\Components\FormBuilder::class,
        'Field'=> \App\Components\Field::class,
        'Ajax'=> \App\Components\Ajax::class,
        'SweetAjax'=> \App\Components\SweetAjax::class,
        'Modal'=> \App\Components\Modal::class,
        'NotificationDropdown'=> \App\Components\NotificationDropdown::class,
    ];

    public function __construct(string $viewName)
    {
        $this->viewName = $viewName;

        // Merge global data on construction
        $this->data = array_merge($this->data, self::$globalData);

        // Inject default components on construct
        // foreach ($this->defaultComponents as $alias => $class) {
        //     // Inject as alias class shortcut
        //     class_alias($class, $alias);
        // }
    }

    public static function make(string $viewName): self
    {
        return new static($viewName);
    }

    public function setLayout(string $layout): self
    {
        $this->layout = $layout;
        return $this;
    }

    public function with(array $data): self
    {
        $this->data = array_merge($this->data, $data);
        return $this;
    }

    /**
     * Add rare/extra components for a specific view
     */
    public function withComponents(array $components): self
    {
        foreach ($components as $alias => $class) {
            $this->data[$alias] = $class;
        }
        return $this;
    }

    public function dontMinify(): self
    {
        $this->minify = false;
        return $this;
    }

    public function retunRaw(): self
    {
        $this->retunRaw = true;
        return $this;
    }

    public function render(): void
    {
        // Prepare data for extraction
        $data = $this->data;
        if (!empty($data)) {
            extract($data, EXTR_SKIP);
        }

        $viewFile = FS_APP_PATH . '/Views/' . $this->viewName . '.view.php';
        if (!file_exists($viewFile)) {
            throw new Exception("View file not found: {$viewFile}");
        }

        $layoutFile = null;
        if ($this->layout) {
            $layoutFile = FS_APP_PATH . '/layouts/' . $this->layout . '.php';
            if (!file_exists($layoutFile)) {
                throw new Exception("Layout file not found: {$layoutFile}");
            }
        }

        // Use a new .compiled.php extension to force invalidation of old buggy cached files
        $compiledPath = FS_STORAGE_PATH . '/views/compiled/' . $this->viewName . '.compiled.php';

        // Advanced Cache Check
        $needsRecompile = true;
        if (file_exists($compiledPath)) {
            $compiledTime = filemtime($compiledPath);
            $isViewFresh = $compiledTime >= filemtime($viewFile);
            $isLayoutFresh = $layoutFile ? ($compiledTime >= filemtime($layoutFile)) : true;
            $isEngineFresh = $compiledTime >= filemtime(__FILE__); // Forces recompile if View.php changes

            if ($isViewFresh && $isLayoutFresh && $isEngineFresh) {
                $needsRecompile = false;
            }
        }

        if ($needsRecompile) {
            // Safely read template as string (do NOT execute PHP yet)
            $viewContent = file_get_contents($viewFile);

            if ($layoutFile) {
                $rawLayoutContent = file_get_contents($layoutFile);
                $finalContent = str_replace('@yield("content")', $viewContent, $rawLayoutContent);
            } else {
                $finalContent = $viewContent;
            }

            if (!is_dir(dirname($compiledPath))) {
                mkdir(dirname($compiledPath), 0777, true);
            }

            $compiledContent = $this->compile($finalContent);
            
            if ($this->minify) {
                $compiledContent = $this->minifyPhpTemplate($compiledContent);
            }
            
            file_put_contents($compiledPath, $compiledContent);
        }

        // Execute the safely compiled PHP
        ob_start();
        require $compiledPath;
        $finalContent = ob_get_clean();

        // Final output handling
        if ($this->retunRaw) {
            echo "<pre>" . htmlentities($finalContent) . "</pre>";
            exit;
        }

        if ($this->minify) {
            $finalContent = $this->minifyHtml($finalContent);
        }

        echo $finalContent;
    }

    protected function compile(string $content): string
    {
        // Process other directives and variables
        $content = $this->replaceDirectives($content);
        $content = $this->replaceVariables($content);

        // Collect all @addScript ... @endAddScript blocks
        $content = preg_replace_callback(
            '/@addScript\s*(<script.*?<\/script>)\s*@endAddScript/s',
            function ($matches) {
                self::$compiledScripts[] = $matches[1]; // store the script
                return ''; // remove from original location
            },
            $content
        );

        // Inject collected scripts before </body>
        if (!empty(self::$compiledScripts)) {
            $content = str_replace('</body>', implode("\n", self::$compiledScripts) . "\n</body>", $content);
        }

        return $content;
    }

    protected function replaceDirectives(string $content): string
    {
        // 1. @forelse (Processed first to avoid collisions with @foreach or @if)
        $content = preg_replace_callback(
            '/@forelse\s*(\((?:[^()]+|(?1))*\))/',
            function ($matches) {
                $inner = substr($matches[1], 1, -1); // Remove outer parentheses
                $parts = preg_split('/\s+as\s+/', $inner, 2);
                $array = isset($parts[0]) ? trim($parts[0]) : '';
                return "<?php if(!empty({$array})): foreach({$inner}): ?>";
            },
            $content
        );
        $content = preg_replace('/@empty\b/', '<?php endforeach; else: ?>', $content);
        $content = preg_replace('/@endforelse\b/', '<?php endif; ?>', $content);

        // 2. @foreach (Uses recursive pattern to match properly balanced nested parentheses)
        $content = preg_replace('/@foreach\s*(\((?:[^()]+|(?1))*\))/', '<?php foreach $1: ?>', $content);
        $content = preg_replace('/@endforeach\b/', '<?php endforeach; ?>', $content);

        // 3. @for
        $content = preg_replace('/@for\s*(\((?:[^()]+|(?1))*\))/', '<?php for $1: ?>', $content);
        $content = preg_replace('/@endfor\b/', '<?php endfor; ?>', $content);

        // 4. @if, @elseif, @else, @endif
        $content = preg_replace('/@if\s*(\((?:[^()]+|(?1))*\))/', '<?php if $1: ?>', $content);
        $content = preg_replace('/@elseif\s*(\((?:[^()]+|(?1))*\))/', '<?php elseif $1: ?>', $content);
        $content = preg_replace('/@else\b/', '<?php else: ?>', $content);
        $content = preg_replace('/@endif\b/', '<?php endif; ?>', $content);

        // 5. @component
        $content = preg_replace_callback(
            '/@component\s*\(\{\s*((?:.|\n)*?)\s*\}\)/',
            fn($matches) => "<?=" . $matches[1] . "?>",
            $content
        );

        // 6. new: @php ... @endphp
        $content = preg_replace_callback(
            '/@php\s*((?:.|\n)*?)\s*@endphp/',
            fn($m) => '<?php ' . $m[1] . ' ?>',
            $content
        );

        return $content;
    }

    protected function replaceVariables(string $content): string
    {
        // raw {!! $var !!}
        $content = preg_replace_callback('/\{!!\s*(.+?)\s*!!\}/s', function ($matches) {
            return "<?=" . $matches[1] . "?>";
        }, $content);

        // escaped {{ $var }}
        $content = preg_replace_callback('/\{\{\s*(.+?)\s*\}\}/s', function ($matches) {
            return "<?= htmlspecialchars(" . $matches[1] . ", ENT_QUOTES, 'UTF-8') ?>";
        }, $content);

        return $content;
    }

    private function minifyPhpTemplate(string $code): string
    {
        $code = preg_replace('/^\s*\/\/.*$/m', '', $code);
        $code = preg_replace('/^\s*#.*$/m', '', $code);
        $code = preg_replace('!/\*[^*][\s\S]*?\*/!', '', $code);

        $code = preg_replace_callback('/<\?php(.*?)\?>/s', function ($matches) {
            $php = $matches[1];
            $php = preg_replace('/\s+/', ' ', $php);
            return "<?php " . trim($php) . " ?>";
        }, $code);

        $code = preg_replace('/\?>\s+<\?php/', '?><?php', $code);

        return trim($code);
    }

    private function minifyHtml(string $html): string
    {
        $html = preg_replace('/<!--(?!\[if).*?-->/', '', $html);

        $html = preg_replace_callback('/<style\b[^>]*>(.*?)<\/style>/is', function ($matches) {
            $css = $matches[1];
            $css = preg_replace('!/\*.*?\*/!s', '', $css);
            $css = preg_replace('/\s+/', ' ', $css);
            $css = preg_replace('/\s*([{}|:;,])\s*/', '$1', $css);
            $css = trim($css);
            return '<style>' . $css . '</style>';
        }, $html);

        preg_match_all('/<script\b[^>]*>.*?<\/script>/is', $html, $scripts);
        $placeholders = [];
        foreach ($scripts[0] as $i => $script) {
            $placeholder = "%%SCRIPT_PLACEHOLDER_{$i}%%";
            $html = str_replace($script, $placeholder, $html);
            $placeholders[$placeholder] = $script;
        }

        $html = preg_replace('/\s+/', ' ', $html);
        $html = preg_replace('/>\s+</', '><', $html);
        $html = trim($html);

        foreach ($placeholders as $placeholder => $script) {
            $html = str_replace($placeholder, $script, $html);
        }

        return $html;
    }
}