<?php
declare(strict_types=1);

namespace App\Core;

final class Template
{
    private string $basePath;
    private array $sections = [];
    private array $sectionStack = [];
    private ?string $layoutName = null;
    private array $layoutData = [];
    private array $data = [];

    public function __construct()
    {
        $this->basePath = dirname(__DIR__, 2);
    }

    public function render(string $viewPath, array $data = []): string
    {
        $this->data = $data;
        $this->sections = [];
        $this->sectionStack = [];
        $this->layoutName = null;

        $file = $this->resolvePath($viewPath);
        if (!file_exists($file)) {
            throw new \RuntimeException("View not found: {$viewPath} (looked in: {$file})");
        }

        // Render the view (which may call layout() and section())
        $viewContent = $this->renderFile($file, $data);

        // If no layout was specified, return view content directly
        if ($this->layoutName === null) {
            return $viewContent;
        }

        // Render the layout with sections
        $layoutFile = $this->resolvePath($this->layoutName);
        if (!file_exists($layoutFile)) {
            throw new \RuntimeException("Layout not found: {$this->layoutName} (looked in: {$layoutFile})");
        }

        // Store any non-sectioned content as 'content' if not already set
        $trimmed = trim($viewContent);
        if ($trimmed !== '' && !isset($this->sections['content'])) {
            $this->sections['content'] = $viewContent;
        }

        return $this->renderFile($layoutFile, $data);
    }

    private function renderFile(string $file, array $data = []): string
    {
        $__template = $this;
        extract($data, EXTR_SKIP);

        ob_start();
        try {
            require $file;
        } catch (\Throwable $e) {
            ob_end_clean();
            throw $e;
        }
        return ob_get_clean();
    }

    /**
     * Called by views to specify which layout to use.
     */
    public function layout(string $name): void
    {
        $this->layoutName = $name;
    }

    /**
     * Start a named section.
     */
    public function section(string $name): void
    {
        $this->sectionStack[] = $name;
        ob_start();
    }

    /**
     * End the current section.
     */
    public function endSection(): void
    {
        if (empty($this->sectionStack)) {
            throw new \RuntimeException('Cannot end section — no section started.');
        }

        $name = array_pop($this->sectionStack);
        $content = ob_get_clean();
        $this->sections[$name] = $content;
    }

    /**
     * Output a section's content (called by layouts).
     * Returns the section content or default.
     */
    public function yield(string $name, string $default = ''): string
    {
        return $this->sections[$name] ?? $default;
    }

    /**
     * Include a partial/component.
     * Supports both module paths and shared paths.
     */
    public function include(string $path, array $data = []): void
    {
        $file = $this->resolvePath($path);
        if (!file_exists($file)) {
            echo "<!-- Include not found: {$path} → {$file} -->";
            return;
        }

        // Merge parent data with include-specific data
        $mergedData = array_merge($this->data, $data);

        $__template = $this;
        extract($mergedData, EXTR_SKIP);

        require $file;
    }

    /**
     * Include only if the file exists.
     */
    public function includeIf(string $path, array $data = []): void
    {
        $file = $this->resolvePath($path);
        if (file_exists($file)) {
            $this->include($path, $data);
        }
    }

    /**
     * Output data as JSON for embedding in script tags.
     */
    public function json($data): string
    {
        return htmlspecialchars(
            json_encode($data, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT),
            ENT_QUOTES,
            'UTF-8'
        );
    }

    /**
     * Escape HTML.
     */
    public function e(?string $value): string
    {
        return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8');
    }

    /**
     * Resolve a dot-notation path to an actual file path.
     *
     * Resolution order:
     *   1. "Layout.main"              → app/Shared/Layout/main.php
     *   2. "Shared.Components.sidebar" → app/Shared/Components/sidebar.php
     *   3. "Members.Views.index"      → app/Modules/Members/Views/index.php
     *   4. "Members.Views._partials.family-tab" → app/Modules/Members/Views/_partials/family-tab.php
     */
    private function resolvePath(string $dotPath): string
    {
        $parts = explode('.', $dotPath);
        $relativePath = implode(DIRECTORY_SEPARATOR, $parts) . '.php';

        // Check 1: Does it start with "Layout" → Shared/Layout
        if ($parts[0] === 'Layout') {
            $file = $this->basePath . '/app/Shared/' . $relativePath;
            if (file_exists($file)) {
                return $file;
            }
        }

        // Check 2: Does it start with "Shared" → app/Shared/
        if ($parts[0] === 'Shared') {
            $file = $this->basePath . '/app/' . $relativePath;
            if (file_exists($file)) {
                return $file;
            }
        }

        // Check 3: Module path → app/Modules/
        $file = $this->basePath . '/app/Modules/' . $relativePath;
        if (file_exists($file)) {
            return $file;
        }

        // Check 4: Direct path from app/
        $file = $this->basePath . '/app/' . $relativePath;
        if (file_exists($file)) {
            return $file;
        }

        // Check 5: Try from base path
        $file = $this->basePath . '/' . $relativePath;
        if (file_exists($file)) {
            return $file;
        }

        // Return the most likely path for error message
        return $this->basePath . '/app/Modules/' . $relativePath;
    }
}