"""Jinja environment and the context every template receives."""

from __future__ import annotations

from pathlib import Path
from typing import Any

from fastapi import Request
from fastapi.templating import Jinja2Templates

from lembas import __version__
from lembas.config import settings
from lembas.db.models import User
from lembas.services import metrics as metrics_service
from lembas.services import steps as steps_service
from lembas.services import tool_labels
from lembas.services.markdown import highlight_tokens
from lembas.services.reasoning import format_duration

TEMPLATE_DIR = Path(__file__).parent / "templates"
STATIC_DIR = Path(__file__).parent / "static"

templates = Jinja2Templates(directory=str(TEMPLATE_DIR))
templates.env.trim_blocks = True
templates.env.lstrip_blocks = True

# {{ message.reasoning_ms | duration }} -> "8 seconds"
templates.env.filters["duration"] = format_duration

# {{ message.usage_json | metrics }} -> a Metrics, so the finished bubble reads
# its numbers through the same object the live frames are built from.
templates.env.filters["metrics"] = metrics_service.from_message


def stable_hue(value: str) -> int:
    """A deterministic 0-359 hue for a string.

    Used for generated model avatars so each model gets its own colour without
    anyone choosing one, and the same model looks the same on every page and
    after every restart. Python's hash() is salted per process, hence md5.
    """
    import hashlib

    digest = hashlib.md5(value.encode("utf-8"), usedforsecurity=False).digest()
    return int.from_bytes(digest[:2], "big") % 360


templates.env.filters["stable_hue"] = stable_hue

# A user's own message: escaped here and marked up, so `@mentions` read as
# references rather than as punctuation. A filter rather than a context value
# because the message templates are included from four different handlers and
# every one of them would otherwise have to remember to pass it.
templates.env.filters["tokens"] = highlight_tokens

# What a tool call is called and what it looks like. Globals rather than
# context values because a message bubble is rendered from four different
# handlers -- pages, post_message, regenerate and the SSE follower -- and every
# one of them would otherwise have to remember to pass them. That is the exact
# trap `audio_service.template_flags` fell into.
templates.env.globals["tool_label"] = tool_labels.label_for
templates.env.globals["tool_icon"] = tool_labels.icon_for

# A finished reply as the sequence of steps it was. A global for exactly the
# reason the two above are, and it is why turning the bubble into a sequence
# needed no change in `pages.py`, `post_message`, `regenerate` or the `done`
# frame -- all four of which render `chat/_message.html`.
templates.env.globals["message_steps"] = steps_service.for_message


class _Brand:
    """Whose instance this is, as a Jinja global.

    A **global** and not a context value, because `render()` has no database
    session and four render paths never reach it at all -- the login page, the
    error pages, the offline page and the SSE fragments. Threading it through
    every one of those would still leave the ones that bypass `render()`.

    A proxy rather than the snapshot itself, because a global is bound once at
    import and the snapshot changes when an administrator saves. Every attribute
    goes through `branding.snapshot()`, which is a process-level cache: one
    query per process, and one after each save.
    """

    def __getattr__(self, name: str):
        from lembas.services import branding

        return getattr(branding.snapshot(), name)


templates.env.globals["brand"] = _Brand()


def resolve_theme(user: User | None) -> str:
    """Theme to render with on the server.

    Only ever a first guess: the inline script in base.html corrects it from
    localStorage before first paint. Getting it close server-side is what stops
    a signed-in user seeing a flash of the wrong theme on every navigation.

    Validated against the themes that actually exist rather than against a
    hard-coded pair, or a custom theme would be stored on the account, refused
    here, and rendered as Moria on every page load until localStorage corrected
    it -- a flash on every navigation, which is what this function exists to
    prevent.
    """
    from lembas.services import branding

    if user is not None:
        chosen = (user.settings_json or {}).get("theme")
        if chosen in branding.snapshot().theme_ids:
            return chosen
    return settings.default_theme


def resolve_layout(user: User | None) -> str:
    """Stored panel widths as a `style` value for <html>, or "".

    Same shape as the theme and for the same reason: a first guess, corrected
    from localStorage before first paint. This is what carries a dragged width
    to a second browser, where localStorage has nothing to say.

    Re-clamped on the way out rather than trusted from the column. The bounds
    could have tightened since it was stored, and a width outside them is a
    panel somebody cannot see well enough to drag back.
    """
    if user is None:
        return ""
    from lembas.api.preferences import LAYOUT_BOUNDS

    parts = []
    for name, raw in ((user.settings_json or {}).get("layout") or {}).items():
        bounds = LAYOUT_BOUNDS.get(str(name))
        if bounds is None:
            continue
        try:
            value = min(max(int(float(raw)), bounds[0]), bounds[1])
        except (TypeError, ValueError):
            continue
        parts.append(f"{name}:{value}px")
    return ";".join(parts)


def render(
    request: Request,
    template: str,
    context: dict[str, Any] | None = None,
    **kwargs: Any,
):
    """Render a template with the globals every page expects.

    Using this instead of templates.TemplateResponse directly is what
    guarantees `user` and `theme` are always defined, so templates never need
    to guard against a missing variable.
    """
    user = getattr(request.state, "user", None)
    payload: dict[str, Any] = {
        "request": request,
        "user": user,
        "theme": resolve_theme(user),
        "layout": resolve_layout(user),
        "version": __version__,
        "allow_signup": settings.allow_signup,
    }
    payload.update(context or {})
    return templates.TemplateResponse(request, template, payload, **kwargs)
