"""
Caprail View Engine.

Jinja2-based template engine with Blade-inspired directives.

Supported directives:
  @extends('layout')         — template inheritance
  @section('name') / @endsection
  @yield('name')             — slot in layout
  @include('partial')
  @component('name') / @endcomponent
  @slot('name') / @endslot
  @if(...) / @elseif(...) / @else / @endif
  @unless(...) / @endunless
  @foreach(...) / @endforeach
  @for(...) / @endfor
  @while(...) / @endwhile
  @empty / @endempty
  @auth / @endauth
  @guest / @endguest
  @csrf                      — CSRF hidden input
  @method('PUT')             — method spoofing hidden input
  @vite(['resources/js/app.js'])
  {{ expr }}                 — escaped output
  {!! expr !!}               — raw (unescaped) output
  {{-- comment --}}          — Blade comment (removed)
"""
from __future__ import annotations

import re
from pathlib import Path
from typing import Any

from jinja2 import Environment, FileSystemLoader, select_autoescape


_INSTANCE: "ViewEngine | None" = None


class ViewEngine:
    """
    The Caprail view engine.

    Configure once at startup via ``ViewEngine.configure(views_path)``,
    then use ``ViewEngine.instance()`` anywhere.
    """

    def __init__(self, views_path: str | Path, cache_path: str | Path | None = None) -> None:
        self._views_path = Path(views_path)
        self._cache_path = Path(cache_path) if cache_path else None
        self._globals: dict[str, Any] = {}
        self._env = self._build_env()

    @classmethod
    def configure(cls, views_path: str | Path, **kwargs) -> "ViewEngine":
        global _INSTANCE
        _INSTANCE = cls(views_path, **kwargs)
        return _INSTANCE

    @classmethod
    def instance(cls) -> "ViewEngine":
        if _INSTANCE is None:
            raise RuntimeError("ViewEngine not configured. Call ViewEngine.configure() first.")
        return _INSTANCE

    def share(self, key: str, value: Any) -> None:
        """Share a variable with all views."""
        self._globals[key] = value
        self._env.globals[key] = value

    # ------------------------------------------------------------------
    # Rendering
    # ------------------------------------------------------------------

    async def render(self, template: str, data: dict | None = None) -> str:
        name = self._normalise_name(template)
        source = self._load_and_compile(name)
        tpl = self._env.from_string(source)
        ctx = {**self._globals, **(data or {})}
        return tpl.render(**ctx)

    async def render_response(self, template: str, data: dict | None = None, status: int = 200):
        from caprail.http.response import Response
        html = await self.render(template, data)
        return Response(html, status=status)

    def render_sync(self, template: str, data: dict | None = None) -> str:
        name = self._normalise_name(template)
        source = self._load_and_compile(name)
        tpl = self._env.from_string(source)
        ctx = {**self._globals, **(data or {})}
        return tpl.render(**ctx)

    # ------------------------------------------------------------------
    # Internal
    # ------------------------------------------------------------------

    def _build_env(self) -> Environment:
        env = Environment(
            loader=FileSystemLoader(str(self._views_path)),
            autoescape=select_autoescape(["html", "htm"]),
            trim_blocks=True,
            lstrip_blocks=True,
        )
        env.globals.update(self._globals)
        # Helpers available in all templates
        env.globals["csrf_field"] = self._csrf_field
        env.globals["method_field"] = self._method_field
        env.globals["vite"] = self._vite
        env.globals["asset"] = self._asset
        env.globals["route"] = self._route_url
        return env

    def _load_and_compile(self, name: str) -> str:
        path = self._views_path / name
        if not path.exists():
            raise FileNotFoundError(f"View '{name}' not found at {path}")
        source = path.read_text(encoding="utf-8")
        return self._compile_blade(source)

    @staticmethod
    def _normalise_name(name: str) -> str:
        """Convert dot notation to path: 'layouts.app' → 'layouts/app.blade.html'"""
        if name.endswith(".blade.html") or name.endswith(".html") or name.endswith(".jinja2"):
            return name
        return name.replace(".", "/") + ".blade.html"

    # ------------------------------------------------------------------
    # Blade → Jinja2 compiler
    # ------------------------------------------------------------------

    @staticmethod
    def _compile_blade(source: str) -> str:
        s = source

        # Remove Blade comments {{-- ... --}}
        s = re.sub(r"\{\{--.*?--\}\}", "", s, flags=re.DOTALL)

        # Raw output: {!! expr !!} → {{ expr | safe }}
        s = re.sub(r"\{!!\s*(.*?)\s*!!\}", r"{{ \1 | safe }}", s)

        # @extends('name') → {% extends "name" %}
        s = re.sub(
            r"@extends\(['\"]([^'\"]+)['\"]\)",
            lambda m: '{{% extends "{}" %}}'.format(ViewEngine._normalise_name(m.group(1))),
            s,
        )

        # @yield('name') → {% block name %}{% endblock %}
        s = re.sub(r"@yield\(['\"]([^'\"]+)['\"]\)", r"{% block \1 %}{% endblock %}", s)

        # @section('name') ... @endsection → {% block name %} ... {% endblock %}
        s = re.sub(r"@section\(['\"]([^'\"]+)['\"]\)", r"{% block \1 %}", s)
        s = re.sub(r"@endsection", "{% endblock %}", s)

        # @include('name') → {% include "name" %}
        s = re.sub(
            r"@include\(['\"]([^'\"]+)['\"](?:,\s*(\{.*?\}))?\)",
            lambda m: '{{% include "{}" %}}'.format(ViewEngine._normalise_name(m.group(1))),
            s,
            flags=re.DOTALL,
        )

        # @component('name') → {% include "name" %}  (simplified)
        s = re.sub(
            r"@component\(['\"]([^'\"]+)['\"]\)",
            lambda m: '{{% include "{}" %}}'.format(ViewEngine._normalise_name(m.group(1))),
            s,
        )
        s = re.sub(r"@endcomponent", "", s)

        # @slot / @endslot (simplified — treated as blocks)
        s = re.sub(r"@slot\(['\"]([^'\"]+)['\"]\)", r"{% block \1 %}", s)
        s = re.sub(r"@endslot", "{% endblock %}", s)

        # @if(expr) / @elseif(expr) / @else / @endif
        s = re.sub(r"@if\((.*?)\)", r"{% if \1 %}", s)
        s = re.sub(r"@elseif\((.*?)\)", r"{% elif \1 %}", s)
        s = re.sub(r"@else\b", "{% else %}", s)
        s = re.sub(r"@endif\b", "{% endif %}", s)

        # @unless(expr) ... @endunless
        s = re.sub(r"@unless\((.*?)\)", r"{% if not (\1) %}", s)
        s = re.sub(r"@endunless\b", "{% endif %}", s)

        # @foreach(items as item) ... @endforeach
        s = re.sub(r"@foreach\((\S+)\s+as\s+(\S+)\)", r"{% for \2 in \1 %}", s)
        s = re.sub(r"@endforeach\b", "{% endfor %}", s)

        # @for(init; cond; incr) — limited support, map to Jinja2 range
        # Simple form: @for($i = 0; $i < 10; $i++)
        s = re.sub(r"@for\(.*?\)", "{% for _i in range(0, 10) %}", s)  # fallback
        s = re.sub(r"@endfor\b", "{% endfor %}", s)

        # @while(expr) ... @endwhile
        s = re.sub(r"@while\((.*?)\)", r"{% while \1 %}", s)  # Jinja2 doesn't have while, but kept for custom extension
        s = re.sub(r"@endwhile\b", "{% endwhile %}", s)

        # @empty / @endempty (pair with @forelse)
        s = re.sub(r"@forelse\((\S+)\s+as\s+(\S+)\)", r"{% for \2 in \1 %}", s)
        s = re.sub(r"@empty\b", "{% else %}", s)
        s = re.sub(r"@endforelse\b", "{% endfor %}", s)

        # @auth ... @endauth (requires 'auth_user' in context)
        s = re.sub(r"@auth\b", "{% if auth_user %}", s)
        s = re.sub(r"@endauth\b", "{% endif %}", s)

        # @guest ... @endguest
        s = re.sub(r"@guest\b", "{% if not auth_user %}", s)
        s = re.sub(r"@endguest\b", "{% endif %}", s)

        # @csrf → {{ csrf_field() | safe }}
        s = re.sub(r"@csrf\b", "{{ csrf_field() | safe }}", s)

        # @method('PUT') → {{ method_field('PUT') | safe }}
        s = re.sub(r"@method\(['\"](\w+)['\"]\)", r"{{ method_field('\1') | safe }}", s)

        # @vite(['...', '...']) → {{ vite(['...']) | safe }}
        s = re.sub(r"@vite\((\[.*?\])\)", r"{{ vite(\1) | safe }}", s, flags=re.DOTALL)

        # @error('field') ... @enderror
        s = re.sub(r"@error\(['\"]([^'\"]+)['\"]\)", r"{% if errors and '\1' in errors %}", s)
        s = re.sub(r"@enderror\b", "{% endif %}", s)

        # @switch / @case / @break / @default / @endswitch — map to if/elif
        # (simplified)
        s = re.sub(r"@switch\((.*?)\)", r"{% set _switch = \1 %}", s)
        s = re.sub(r"@case\((.*?)\)", r"{% if _switch == \1 %}", s)
        s = re.sub(r"@break\b", "{% endif %}", s)
        s = re.sub(r"@default\b", "{% else %}", s)
        s = re.sub(r"@endswitch\b", "{% endif %}", s)

        return s

    # ------------------------------------------------------------------
    # Global helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _csrf_field() -> str:
        return '<input type="hidden" name="_token" value="csrf-placeholder">'

    @staticmethod
    def _method_field(method: str) -> str:
        return f'<input type="hidden" name="_method" value="{method}">'

    @staticmethod
    def _vite(assets: list[str]) -> str:
        tags = []
        for asset in assets:
            if asset.endswith(".css"):
                tags.append(f'<link rel="stylesheet" href="/build/{asset}">')
            else:
                tags.append(f'<script type="module" src="/build/{asset}"></script>')
        return "\n".join(tags)

    @staticmethod
    def _asset(path: str) -> str:
        return f"/public/{path.lstrip('/')}"

    @staticmethod
    def _route_url(name: str, **params) -> str:
        try:
            from caprail.support.helpers import route as route_helper
            return route_helper(name, **params)
        except Exception:
            return f"/{name}"
