from __future__ import annotations

import os
import re
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Mapping, Optional

# Supports:
#   {{ var }}
#   {{ var | lower }}
#   {{ var | default("x") }}
# And env passthrough:
#   ${ENV_NAME}

_VAR_RE = re.compile(r"\{\{\s*(?P<expr>[^}]+?)\s*\}\}")
_ENV_RE = re.compile(r"\$\{(?P<name>[A-Za-z_][A-Za-z0-9_]*)\}")


FilterFn = Callable[..., str]


@dataclass
class TemplateEngine:
    filters: Dict[str, FilterFn]

    @staticmethod
    def default() -> "TemplateEngine":
        eng = TemplateEngine(filters={})
        eng.register_filter("lower", lambda s: str(s).lower())
        eng.register_filter("upper", lambda s: str(s).upper())
        eng.register_filter("trim", lambda s: str(s).strip())
        eng.register_filter("default", _filter_default)
        eng.register_filter("replace", _filter_replace)
        return eng

    def register_filter(self, name: str, fn: FilterFn) -> None:
        # Only allow explicit registration
        self.filters[name] = fn

    def render_str(
        self, s: str, *, vars_map: Mapping[str, Any], env_map: Mapping[str, str]
    ) -> str:
        def repl_env(m: re.Match) -> str:
            return env_map.get(m.group("name"), "")

        def repl_var(m: re.Match) -> str:
            expr = m.group("expr").strip()
            return self._eval_expr(expr, vars_map=vars_map)

        s = _ENV_RE.sub(repl_env, s)
        s = _VAR_RE.sub(repl_var, s)
        return s

    def render_obj(
        self, obj: Any, *, vars_map: Mapping[str, Any], env_map: Mapping[str, str]
    ) -> Any:
        if obj is None:
            return None
        if isinstance(obj, str):
            return self.render_str(obj, vars_map=vars_map, env_map=env_map)
        if isinstance(obj, list):
            return [self.render_obj(x, vars_map=vars_map, env_map=env_map) for x in obj]
        if isinstance(obj, dict):
            return {
                str(k): self.render_obj(v, vars_map=vars_map, env_map=env_map)
                for k, v in obj.items()
            }
        return obj

    def _eval_expr(self, expr: str, *, vars_map: Mapping[str, Any]) -> str:
        # expr grammar:   var | filter(args) | filter ...
        parts = [p.strip() for p in expr.split("|")]
        if (
            not parts
        ):  # pragma: no cover - split() on str always yields at least one item
            return ""

        # base: a bare identifier
        base = parts[0]
        if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", base):
            # keep it super strict: only identifiers
            return ""

        val: Any = vars_map.get(base, "")

        for filt in parts[1:]:
            name, args = _parse_filter_call(filt)
            fn = self.filters.get(name)
            if fn is None:
                # unknown filters render as empty (or could error)
                val = ""
                continue
            val = fn(val, *args)

        return "" if val is None else str(val)


def _parse_filter_call(s: str) -> tuple[str, List[str]]:
    # name or name("a","b")
    s = s.strip()
    m = re.match(r"^(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*(?:\((?P<args>.*)\))?$", s)
    if not m:
        return ("", [])
    name = m.group("name")
    args_raw = m.group("args")
    if not args_raw:
        return (name, [])
    # very small args parser: comma-separated quoted strings OR bare words
    args: List[str] = []
    for part in _split_args(args_raw):
        part = part.strip()
        if (part.startswith('"') and part.endswith('"')) or (
            part.startswith("'") and part.endswith("'")
        ):
            args.append(part[1:-1])
        else:
            args.append(part)
    return (name, args)


def _split_args(s: str) -> List[str]:
    # tiny CSV-ish splitter for quotes
    out: List[str] = []
    cur: List[str] = []
    quote: Optional[str] = None
    for ch in s:
        if quote:
            cur.append(ch)
            if ch == quote:
                quote = None
            continue
        if ch in ("'", '"'):
            quote = ch
            cur.append(ch)
            continue
        if ch == ",":
            out.append("".join(cur).strip())
            cur = []
            continue
        cur.append(ch)
    if cur:
        out.append("".join(cur).strip())
    return [x for x in out if x]


def _filter_default(val: Any, default: str = "") -> str:
    s = "" if val is None else str(val)
    return s if s != "" else default


def _filter_replace(val: Any, old: str, new: str = "") -> str:
    return ("" if val is None else str(val)).replace(old, new)
