#
# Strict Jinja2 renderer for values that may contain Jinja markers.
#
# Why:
# - In some Ansible lookup evaluation contexts, templating can leave "{{ ... }}"
#   untouched without failing.
# - We want hard-fail semantics to avoid leaking Jinja into generated configs.
#
# Additionally:
# - Support nested Jinja-in-vars by rendering multiple passes with a depth limit.
#   Example:
#     CA_TRUST.cert_host = "{{ CA_ROOT.cert_host }}"
#     CA_ROOT.cert_host  = "/etc/{{ SOFTWARE_DOMAIN }}/ca/root-ca.crt"
#   We want the final result without any "{{ ... }}" left.

from __future__ import annotations

from typing import Any

from ansible.errors import AnsibleError


def as_str(v: Any) -> str:
    return "" if v is None else str(v).strip()


def looks_like_jinja(s: str) -> bool:
    return ("{{" in s) or ("{%" in s)


def build_render_context(variables: dict[str, Any]) -> tuple[dict, str]:
    """
    Build a robust context for rendering Jinja-in-strings.

    We intentionally do NOT rely on Ansible's Templar here because in some
    lookup evaluation contexts it can silently keep Jinja markers untouched.

    Context priority:
      1) variables dict
      2) hostvars[inventory_hostname] overlays variables (wins on conflicts)
    """
    ctx: dict = {}
    if isinstance(variables, dict):
        ctx.update(variables)

    inv_host = as_str(variables.get("inventory_hostname", "")).strip()

    hostvars = variables.get("hostvars") if isinstance(variables, dict) else None
    if inv_host and isinstance(hostvars, dict):
        hv = hostvars.get(inv_host)
        if isinstance(hv, dict):
            ctx.update(hv)

    return ctx, inv_host


def _jinja_env():
    try:
        from jinja2 import Environment, StrictUndefined, select_autoescape
    except Exception as exc:
        raise AnsibleError(f"jinja_strict: cannot import jinja2. Error: {exc}") from exc
    return Environment(
        undefined=StrictUndefined,
        autoescape=select_autoescape(["html", "xml"]),
    )


def render_jinja2_strict_once(
    raw: str, *, ctx: dict, inv_host: str, var_name: str, err_prefix: str
) -> str:
    """
    Render ONE pass using plain Jinja2 with StrictUndefined.
    Missing vars fail hard.
    """
    if not looks_like_jinja(raw):
        return raw

    env = _jinja_env()
    try:
        rendered = env.from_string(raw).render(ctx)
    except Exception as exc:
        raise AnsibleError(
            f"{err_prefix}: failed to render {var_name} via strict Jinja2. "
            f"inventory_hostname='{inv_host}'. Raw: {raw}. Error: {exc}"
        ) from exc

    return as_str(rendered)


def render_jinja2_strict_recursive(
    raw: str,
    *,
    ctx: dict,
    inv_host: str,
    var_name: str,
    err_prefix: str,
    max_passes: int = 5,
) -> str:
    """
    Render using strict Jinja2 repeatedly until:
      - no Jinja markers remain, OR
      - output stops changing, OR
      - max_passes is reached

    Hard-fail if markers remain after max_passes / stabilization.
    """
    if not looks_like_jinja(raw):
        return raw

    current = raw
    for i in range(max_passes):
        rendered = render_jinja2_strict_once(
            current,
            ctx=ctx,
            inv_host=inv_host,
            var_name=var_name,
            err_prefix=err_prefix,
        )

        if not looks_like_jinja(rendered):
            return rendered

        if rendered == current:
            raise AnsibleError(
                f"{err_prefix}: {var_name} did not fully expand (still contains Jinja markers) "
                f"after pass {i + 1}/{max_passes}. inventory_hostname='{inv_host}'. "
                f"Value: {rendered}."
            )

        current = rendered

    raise AnsibleError(
        f"{err_prefix}: {var_name} did not fully expand (still contains Jinja markers) "
        f"after {max_passes} passes. inventory_hostname='{inv_host}'. "
        f"Raw: {raw} | Rendered: {current}."
    )


def render_strict(
    value: Any,
    *,
    variables: dict[str, Any],
    var_name: str,
    err_prefix: str,
    max_passes: int = 5,
) -> str:
    """
    Convenience wrapper:
    - converts to string
    - if Jinja markers exist => render via strict Jinja2 using a robust context
      (recursive with depth limit)
    - hard-fail if Jinja markers remain
    """
    raw = as_str(value)
    if not raw:
        return raw
    if not looks_like_jinja(raw):
        return raw

    ctx, inv_host = build_render_context(variables)
    rendered = render_jinja2_strict_recursive(
        raw,
        ctx=ctx,
        inv_host=inv_host,
        var_name=var_name,
        err_prefix=err_prefix,
        max_passes=max_passes,
    )

    if looks_like_jinja(rendered):
        raise AnsibleError(
            f"{err_prefix}: {var_name} did not fully expand (still contains Jinja markers). "
            f"inventory_hostname='{inv_host}'. Raw: {raw} | Rendered: {rendered}."
        )

    return rendered
