"""RFC 6570 URI Templates with bidirectional support.

Provides both expansion (template + variables → URI) and matching
(URI → variables). RFC 6570 only specifies expansion; matching is the
inverse operation needed by MCP servers to route ``resources/read``
requests to handlers.

Supports Levels 1-3 fully, plus Level 4 explode modifier for path-like
operators (``{/var*}``, ``{.var*}``, ``{;var*}``). The Level 4 prefix
modifier (``{var:N}``) and query-explode (``{?var*}``) are not supported.

Matching semantics
------------------

Matching is not specified by RFC 6570 (§1.4 explicitly defers to regex
languages). This implementation uses a two-ended scan that never
backtracks: match time is O(n·v) where n is URI length and v is the
number of template variables. Realistic templates have v < 10, making
this effectively linear; there is no input that produces
superpolynomial time.

A template may contain **at most one multi-segment variable** —
``{+var}``, ``{#var}``, or an explode-modified variable (``{/var*}``,
``{.var*}``, ``{;var*}``). This variable greedily consumes whatever the
surrounding bounded variables and literals do not. Two such variables
in one template are inherently ambiguous (which one gets the extra
segment?) and are rejected at parse time. So are any two variables
adjacent with no literal between them — including a variable adjacent
to the multi-segment variable: the scan has nothing to anchor the
boundary on. Operators that emit their own lead character supply that
literal themselves, so ``{+path}{.ext}`` and ``{a}{.b}`` are fine
while ``{+path}{ext}`` and ``{a}{b}`` are not.

Bounded variables before the multi-segment variable match **lazily**
(first occurrence of the following literal); those after match
**greedily** (last occurrence of the preceding literal). Templates
without a multi-segment variable match greedily throughout, identical
to regex semantics.

Reserved expansion ``{+var}`` leaves ``?`` and ``#`` unencoded, but
the scan stops at those characters so ``{+path}{?q}`` can separate path
from query. A value containing a literal ``?`` or ``#`` expands fine
but will not round-trip through ``match()``.
"""

from __future__ import annotations

import re
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from typing import Literal, TypeAlias, cast
from urllib.parse import quote, unquote

__all__ = [
    "DEFAULT_MAX_TEMPLATE_LENGTH",
    "DEFAULT_MAX_VARIABLES",
    "DEFAULT_MAX_URI_LENGTH",
    "InvalidUriTemplate",
    "Operator",
    "UriTemplate",
    "Variable",
]

Operator = Literal["", "+", "#", ".", "/", ";", "?", "&"]

_OPERATORS: frozenset[str] = frozenset({"+", "#", ".", "/", ";", "?", "&"})

# RFC 6570 §2.3: varname = varchar *(["."] varchar), varchar = ALPHA / DIGIT / "_"
# Dots appear only between varchar groups — not consecutive, not trailing.
# (Percent-encoded varchars are technically allowed but unseen in practice.)
_VARNAME_RE = re.compile(r"^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*$")

DEFAULT_MAX_TEMPLATE_LENGTH = 8_192
DEFAULT_MAX_VARIABLES = 256
DEFAULT_MAX_URI_LENGTH = 65_536

# RFC 3986 reserved characters, kept unencoded by {+var} and {#var}.
_RESERVED = ":/?#[]@!$&'()*+,;="


@dataclass(frozen=True)
class _OperatorSpec:
    """Expansion behavior for a single operator (RFC 6570 §3.2, Table in §A)."""

    prefix: str
    """Leading character emitted before the first variable."""
    separator: str
    """Character between variables (and between exploded list items)."""
    named: bool
    """Emit ``name=value`` pairs (query/path-param style) rather than bare values."""
    allow_reserved: bool
    """Keep reserved characters unencoded ({+var}, {#var})."""
    ifemp: str
    """Suffix after a named variable whose expanded value is empty (RFC §A): '' for ;, '=' for ?/&."""


_OPERATOR_SPECS: dict[Operator, _OperatorSpec] = {
    "": _OperatorSpec(prefix="", separator=",", named=False, allow_reserved=False, ifemp=""),
    "+": _OperatorSpec(prefix="", separator=",", named=False, allow_reserved=True, ifemp=""),
    "#": _OperatorSpec(prefix="#", separator=",", named=False, allow_reserved=True, ifemp=""),
    ".": _OperatorSpec(prefix=".", separator=".", named=False, allow_reserved=False, ifemp=""),
    "/": _OperatorSpec(prefix="/", separator="/", named=False, allow_reserved=False, ifemp=""),
    ";": _OperatorSpec(prefix=";", separator=";", named=True, allow_reserved=False, ifemp=""),
    "?": _OperatorSpec(prefix="?", separator="&", named=True, allow_reserved=False, ifemp="="),
    "&": _OperatorSpec(prefix="&", separator="&", named=True, allow_reserved=False, ifemp="="),
}

# Per-operator stop characters for the linear scan. A bounded variable's
# value ends at the first occurrence of any character in its stop set,
# mirroring the character-class boundaries a regex would use but without
# the backtracking.
_STOP_CHARS: dict[Operator, str] = {
    "": "/?#&,",  # simple: everything structural is pct-encoded
    "+": "?#",  # reserved: / allowed, stop at query/fragment
    "#": "",  # fragment: tail of URI, nothing stops it
    ".": "./?#",  # label: stop at next .
    "/": "/?#",  # path segment: stop at next /
    ";": ";/?#",  # path-param value (may be empty: ;name)
    "?": "&#",  # query value (may be empty: ?name=)
    "&": "&#",  # query-cont value
}


class InvalidUriTemplate(ValueError):
    """Raised when a URI template string is malformed or unsupported.

    Attributes:
        template: The template string that failed to parse.
        position: Character offset where the error was detected, or None
            if the error is not tied to a specific position.
    """

    def __init__(self, message: str, *, template: str, position: int | None = None) -> None:
        super().__init__(message)
        self.template = template
        self.position = position


@dataclass(frozen=True)
class Variable:
    """A single variable within a URI template expression."""

    name: str
    operator: Operator
    explode: bool = False


@dataclass
class _Expression:
    """A parsed ``{...}`` expression: one operator, one or more variables."""

    operator: Operator
    variables: list[Variable]


_Part = str | _Expression


@dataclass(frozen=True)
class _Lit:
    """A literal run in the flattened match-atom sequence."""

    text: str


@dataclass(frozen=True)
class _Cap:
    """A single-variable capture in the flattened match-atom sequence.

    ``ifemp`` marks the ``;`` operator's optional-equals quirk: ``{;id}``
    expands to ``;id=value`` or bare ``;id`` when the value is empty, so
    the scan must accept both forms.
    """

    var: Variable
    ifemp: bool = False


_Atom: TypeAlias = _Lit | _Cap


def _is_greedy(var: Variable) -> bool:
    """Return True if this variable can span multiple path segments.

    Reserved/fragment expansion and explode variables are the only
    constructs whose match range is not bounded by a single structural
    delimiter. A template may contain at most one such variable.
    """
    return var.explode or var.operator in ("+", "#")


def _is_str_sequence(value: object) -> bool:
    """Check if value is a non-string sequence whose items are all strings."""
    if isinstance(value, str) or not isinstance(value, Sequence):
        return False
    seq = cast(Sequence[object], value)
    return all(isinstance(item, str) for item in seq)


_PCT_TRIPLET_RE = re.compile(r"%[0-9A-Fa-f]{2}")


def _encode(value: str, *, allow_reserved: bool) -> str:
    """Percent-encode a value per RFC 6570 §3.2.1.

    Simple expansion encodes everything except unreserved characters.
    Reserved expansion (``{+var}``, ``{#var}``) additionally keeps
    RFC 3986 reserved characters intact and passes through existing
    ``%XX`` pct-triplets unchanged (RFC 6570 §3.2.3). A bare ``%`` not
    followed by two hex digits is still encoded to ``%25``.
    """
    if not allow_reserved:
        return quote(value, safe="")

    # Reserved expansion: walk the string, pass through triplets as-is,
    # quote the gaps between them. A bare % with no triplet lands in a
    # gap and gets encoded normally.
    out: list[str] = []
    last = 0
    for m in _PCT_TRIPLET_RE.finditer(value):
        out.append(quote(value[last : m.start()], safe=_RESERVED))
        out.append(m.group())
        last = m.end()
    out.append(quote(value[last:], safe=_RESERVED))
    return "".join(out)


def _expand_expression(expr: _Expression, variables: Mapping[str, str | Sequence[str]]) -> str:
    """Expand a single ``{...}`` expression into its URI fragment.

    Walks the expression's variables, encoding and joining defined ones
    according to the operator's spec. Undefined variables are skipped
    (RFC 6570 §2.3); if all are undefined, the expression contributes
    nothing (no prefix is emitted).
    """
    spec = _OPERATOR_SPECS[expr.operator]
    rendered: list[str] = []

    for var in expr.variables:
        if var.name not in variables:
            # Undefined: skip entirely, no placeholder.
            continue

        value = variables[var.name]

        # Explicit type guard: reject non-str scalars with a clear message
        # rather than a confusing "not iterable" from the sequence branch.
        if not isinstance(value, str) and not _is_str_sequence(value):
            raise TypeError(f"Variable {var.name!r} must be str or a sequence of str, got {type(value).__name__}")

        if isinstance(value, str):
            encoded = _encode(value, allow_reserved=spec.allow_reserved)
            if spec.named:
                rendered.append(f"{var.name}{spec.ifemp}" if value == "" else f"{var.name}={encoded}")
            else:
                rendered.append(encoded)
        else:
            # Sequence value.
            items = [_encode(v, allow_reserved=spec.allow_reserved) for v in value]
            if not items:
                continue
            if var.explode:
                # Each item gets the operator's separator; named ops repeat the key.
                if spec.named:
                    rendered.append(
                        spec.separator.join(f"{var.name}{spec.ifemp}" if v == "" else f"{var.name}={v}" for v in items)
                    )
                else:
                    rendered.append(spec.separator.join(items))
            else:
                # Non-explode: comma-join into a single value, then apply
                # ifemp to the joined result (RFC §3.2.1: behaves as if the
                # value were the joined string).
                joined = ",".join(items)
                if spec.named:
                    rendered.append(f"{var.name}{spec.ifemp}" if joined == "" else f"{var.name}={joined}")
                else:
                    rendered.append(joined)

    if not rendered:
        return ""
    return spec.prefix + spec.separator.join(rendered)


@dataclass(frozen=True)
class UriTemplate:
    """A parsed RFC 6570 URI template.

    Construct via :meth:`parse`. Instances are immutable and hashable;
    equality is based on the template string alone.
    """

    template: str
    _parts: list[_Part] = field(repr=False, compare=False)
    _variables: list[Variable] = field(repr=False, compare=False)
    _prefix: list[_Atom] = field(repr=False, compare=False)
    _greedy: Variable | None = field(repr=False, compare=False)
    _suffix: list[_Atom] = field(repr=False, compare=False)
    _query_variables: list[Variable] = field(repr=False, compare=False)

    @staticmethod
    def is_template(value: str) -> bool:
        """Check whether a string contains URI template expressions.

        A cheap heuristic for distinguishing concrete URIs from templates
        without the cost of full parsing. Returns ``True`` if the string
        contains at least one ``{...}`` pair.

        Example::

            >>> UriTemplate.is_template("file://docs/{name}")
            True
            >>> UriTemplate.is_template("file://docs/readme.txt")
            False

        Note:
            This does not validate the template. A ``True`` result does
            not guarantee :meth:`parse` will succeed.
        """
        open_i = value.find("{")
        return open_i != -1 and value.find("}", open_i) != -1

    @classmethod
    def parse(
        cls,
        template: str,
        *,
        max_length: int = DEFAULT_MAX_TEMPLATE_LENGTH,
        max_variables: int = DEFAULT_MAX_VARIABLES,
    ) -> UriTemplate:
        """Parse a URI template string.

        Args:
            template: An RFC 6570 URI template.
            max_length: Maximum permitted length of the template string.
                Guards against resource exhaustion.
            max_variables: Maximum number of variables permitted across
                all expressions. Counting variables rather than
                ``{...}`` expressions closes the gap where a single
                ``{v0,v1,...,vN}`` expression packs arbitrarily many
                variables under one expression count.

        Raises:
            InvalidUriTemplate: If the template is malformed, exceeds the
                size limits, or uses unsupported RFC 6570 features.
        """
        if len(template) > max_length:
            raise InvalidUriTemplate(
                f"Template exceeds maximum length of {max_length}",
                template=template,
            )

        parts, variables = _parse(template, max_variables=max_variables)

        # Trailing {?...}/{&...} expressions are split off and matched as
        # a query string (order-agnostic, partial, extras ignored) rather
        # than via the linear scan.
        path_parts, query_vars = _split_query_tail(parts)
        atoms = _flatten(path_parts)
        prefix, greedy, suffix = _partition_greedy(atoms, template)

        return cls(
            template=template,
            _parts=parts,
            _variables=variables,
            _prefix=prefix,
            _greedy=greedy,
            _suffix=suffix,
            _query_variables=query_vars,
        )

    @property
    def variables(self) -> list[Variable]:
        """All variables in the template, in order of appearance."""
        return list(self._variables)

    @property
    def variable_names(self) -> list[str]:
        """All variable names in the template, in order of appearance."""
        return [v.name for v in self._variables]

    @property
    def query_variable_names(self) -> frozenset[str]:
        """Names of variables that :meth:`match` treats as optional query parameters.

        These are the variables in a trailing run of ``{?...}``/``{&...}``
        expressions, which are matched leniently: a URI that omits some
        (or all) of them still matches, and the omitted names are simply
        absent from the result. Any value bound to such a name therefore
        needs a fallback for the omitted case.

        Every other variable is bound on every successful :meth:`match`
        (possibly to an empty string) and is *not* in this set. That
        includes a ``{&...}`` expression with no preceding ``{?...}``: it
        never emits the ``?`` the lenient query split keys on, so it is
        matched strictly.
        """
        return frozenset(v.name for v in self._query_variables)

    def expand(self, variables: Mapping[str, str | Sequence[str]]) -> str:
        """Expand the template by substituting variable values.

        String values are percent-encoded according to their operator:
        simple ``{var}`` encodes reserved characters; ``{+var}`` and
        ``{#var}`` leave them intact. Sequence values are joined with
        commas for non-explode variables, or with the operator's
        separator for explode variables.

        Example::

            >>> t = UriTemplate.parse("file://docs/{name}")
            >>> t.expand({"name": "hello world.txt"})
            'file://docs/hello%20world.txt'

            >>> t = UriTemplate.parse("file://docs/{+path}")
            >>> t.expand({"path": "src/main.py"})
            'file://docs/src/main.py'

            >>> t = UriTemplate.parse("/search{?q,lang}")
            >>> t.expand({"q": "mcp", "lang": "en"})
            '/search?q=mcp&lang=en'

            >>> t = UriTemplate.parse("/files{/path*}")
            >>> t.expand({"path": ["a", "b", "c"]})
            '/files/a/b/c'

        Args:
            variables: Values for each template variable. Keys must be
                strings; values must be ``str`` or a sequence of ``str``.

        Returns:
            The expanded URI string.

        Note:
            Per RFC 6570, variables absent from the mapping are
            **silently omitted**. This is the correct behavior for
            optional query parameters (``{?page}`` with no page yields
            no ``?page=``), but for required path segments it produces
            a structurally incomplete URI. If you need all variables
            present, validate before calling::

                missing = set(t.variable_names) - variables.keys()
                if missing:
                    raise ValueError(f"Missing: {missing}")

        Raises:
            TypeError: If a value is neither ``str`` nor an iterable of
                ``str``. Non-string scalars (``int``, ``None``) are not
                coerced.
        """
        out: list[str] = []
        for part in self._parts:
            if isinstance(part, str):
                out.append(part)
            else:
                out.append(_expand_expression(part, variables))
        return "".join(out)

    def match(self, uri: str, *, max_uri_length: int = DEFAULT_MAX_URI_LENGTH) -> dict[str, str | list[str]] | None:
        """Match a concrete URI against this template and extract variables.

        This is the inverse of :meth:`expand`. The URI is matched via a
        linear scan of the template and captured values are
        percent-decoded. The round-trip ``match(expand({k: v})) == {k: v}``
        holds when ``v`` does not contain its operator's separator
        unencoded: ``{.ext}`` with ``ext="tar.gz"`` expands to
        ``.tar.gz`` but does not match — the scan stops ``ext`` at the
        first ``.`` and the trailing ``.gz`` has nothing to consume it.
        RFC 6570 §1.4 notes this is an inherent reversal limitation.

        Matching is structural at the URI level only: a simple ``{name}``
        will not match across a literal ``/`` in the URI (the scan stops
        there), but a percent-encoded ``%2F`` that decodes to ``/`` is
        accepted as part of the value. Path-safety validation belongs at
        a higher layer; see :mod:`mcp.shared.path_security`.

        Example::

            >>> t = UriTemplate.parse("file://docs/{name}")
            >>> t.match("file://docs/readme.txt")
            {'name': 'readme.txt'}
            >>> t.match("file://docs/hello%20world.txt")
            {'name': 'hello world.txt'}

            >>> t = UriTemplate.parse("file://docs/{+path}")
            >>> t.match("file://docs/src/main.py")
            {'path': 'src/main.py'}

            >>> t = UriTemplate.parse("/files{/path*}")
            >>> t.match("/files/a/b/c")
            {'path': ['a', 'b', 'c']}

        **Query parameters** (``{?q,lang}`` at the end of a template)
        are matched leniently: order-agnostic, partial, and unrecognized
        params are ignored. Absent params are omitted from the result so
        downstream function defaults can apply::

            >>> t = UriTemplate.parse("logs://{service}{?since,level}")
            >>> t.match("logs://api")
            {'service': 'api'}
            >>> t.match("logs://api?level=error")
            {'service': 'api', 'level': 'error'}
            >>> t.match("logs://api?level=error&since=5m&utm=x")
            {'service': 'api', 'since': '5m', 'level': 'error'}

        Args:
            uri: A concrete URI string.
            max_uri_length: Maximum permitted length of the input URI.
                Oversized inputs return ``None`` without scanning,
                guarding against resource exhaustion.

        Returns:
            A mapping from variable names to decoded values (``str`` for
            scalar variables, ``list[str]`` for explode variables), or
            ``None`` if the URI does not match the template or exceeds
            ``max_uri_length``.
        """
        if len(uri) > max_uri_length:
            return None

        if self._query_variables:
            # Two-phase: scan matches the path, the query is split and
            # decoded manually. Query params may be partial, reordered,
            # or include extras; absent params stay absent so downstream
            # defaults can apply. Fragment is stripped first since the
            # template's {?...} tail never describes a fragment.
            before_fragment, _, _ = uri.partition("#")
            path, _, query = before_fragment.partition("?")
            result = self._scan(path)
            if result is None:
                return None
            if query:
                parsed = _parse_query(query)
                for var in self._query_variables:
                    if var.name in parsed:
                        result[var.name] = parsed[var.name]
            return result

        return self._scan(uri)

    def _scan(self, uri: str) -> dict[str, str | list[str]] | None:
        """Run the two-ended linear scan against the path portion of a URI."""
        n = len(uri)

        if self._greedy is None:
            # No greedy var: the suffix IS the whole template, scanned
            # right-to-left and anchored so atoms[0] matches at position 0.
            suffix = _scan_suffix(self._suffix, uri, n, anchored=True)
            if suffix is None:
                return None
            suffix_result, suffix_start = suffix
            return suffix_result if suffix_start == 0 else None

        # Greedy var present. The parser rejects a capture adjacent to
        # the greedy slot, so a non-empty suffix begins with a _Lit whose
        # rfind-derived anchor does not depend on how far the prefix
        # scans. Scan the suffix first, then give the prefix that exact
        # position as its ceiling so it cannot consume past the anchor.
        suffix = _scan_suffix(self._suffix, uri, n, anchored=False)
        if suffix is None:
            return None
        suffix_result, suffix_start = suffix
        prefix = _scan_prefix(self._prefix, uri, 0, suffix_start)
        if prefix is None:
            return None
        prefix_result, prefix_end = prefix

        # Prefix consumed [0, prefix_end); suffix consumed [suffix_start, n);
        # the greedy var takes the gap. The prefix scan is bounded by
        # suffix_start, so this holds by construction; guard explicitly
        # rather than asserting so a future regression surfaces as a
        # non-match, not an exception.
        if suffix_start < prefix_end:
            return None  # pragma: no cover - unreachable while bounds hold
        middle = uri[prefix_end:suffix_start]
        greedy_value = _extract_greedy(self._greedy, middle)
        if greedy_value is None:
            return None

        return {**prefix_result, self._greedy.name: greedy_value, **suffix_result}

    def __str__(self) -> str:
        return self.template


def _parse_query(query: str) -> dict[str, str]:
    """Parse a query string into a name→value mapping.

    Unlike ``urllib.parse.parse_qs``, this follows RFC 3986 semantics:
    ``+`` is a literal sub-delim, not a space. Form-urlencoding treats
    ``+`` as space for HTML form submissions, but RFC 6570 and MCP
    resource URIs follow RFC 3986 where only ``%20`` encodes a space.

    Parameter names are **not** percent-decoded. RFC 6570 expansion
    never encodes variable names, so a legitimate match will always
    have the name in literal form. Decoding names would let
    ``%74oken=evil&token=real`` shadow the real ``token`` parameter
    via first-wins.

    Duplicate keys keep the first value. Pairs without ``=`` are
    treated as empty-valued.
    """
    result: dict[str, str] = {}
    for pair in query.split("&"):
        name, _, value = pair.partition("=")
        if name and name not in result:
            result[name] = unquote(value)
    return result


def _extract_greedy(var: Variable, raw: str) -> str | list[str] | None:
    """Decode the greedy variable's isolated middle span.

    For scalar greedy (``{+var}``, ``{#var}``) this is a stop-char
    validation and a single ``unquote``. For explode variables the span
    is a run of separator-delimited segments (``/a/b/c`` or
    ``;keys=a;keys=b``) that is split, validated, and decoded per item.
    """
    spec = _OPERATOR_SPECS[var.operator]
    stops = _STOP_CHARS[var.operator]

    if not var.explode:
        if any(c in stops for c in raw):
            return None
        return unquote(raw)

    sep = spec.separator
    if not raw:
        return []
    # A non-empty explode span must begin with the separator: {/a*}
    # expands to "/x/y", never "x/y". The scan does not consume the
    # separator itself, so it must be the first character here.
    if raw[0] != sep:
        return None
    # Segments must not contain the operator's non-separator stop
    # characters (e.g. {/path*} segments may contain neither ? nor #).
    body_stops = set(stops) - {sep}
    if any(c in body_stops for c in raw):
        return None

    segments: list[str] = []
    prefix = f"{var.name}="
    # split()[0] is always "" because raw starts with the separator;
    # subsequent empties are legitimate values ({/path*} with
    # ["a","","c"] expands to /a//c).
    for seg in raw.split(sep)[1:]:
        if spec.named:
            # Named explode emits name=value per item (or bare name
            # under ; with empty value). Validate the name and strip
            # the prefix before decoding.
            if seg.startswith(prefix):
                seg = seg[len(prefix) :]
            elif seg == var.name:
                seg = ""
            else:
                return None
        segments.append(unquote(seg))
    return segments


def _split_query_tail(parts: list[_Part]) -> tuple[list[_Part], list[Variable]]:
    """Separate trailing ``?``/``&`` expressions from the path portion.

    Lenient query matching (order-agnostic, partial, ignores extras)
    applies when a template ends with one or more consecutive ``?``/``&``
    expressions and the preceding path portion contains no literal
    ``?``. If the path has a literal ``?`` (e.g., ``?fixed=1{&page}``),
    the URI's ``?`` split won't align with the template's expression
    boundary, so the strict scan is used instead.

    Returns:
        A pair ``(path_parts, query_vars)``. If lenient matching does
        not apply, ``query_vars`` is empty and ``path_parts`` is the
        full input.
    """
    split = len(parts)
    for i in range(len(parts) - 1, -1, -1):
        part = parts[i]
        if isinstance(part, _Expression) and part.operator in ("?", "&"):
            split = i
        else:
            break

    if split == len(parts):
        return parts, []

    # The tail must start with a {?...} expression so that expand()
    # emits a ? the URI can split on. A standalone {&page} expands
    # with an & prefix, which partition("?") won't find.
    first = parts[split]
    assert isinstance(first, _Expression)
    if first.operator != "?":
        return parts, []

    # If the path portion contains a literal ?/# or a {?...}/{#...}
    # expression, lenient matching's partition("#") then partition("?")
    # would strip content the path scan expects to see. Fall back to
    # the strict scan.
    for part in parts[:split]:
        if isinstance(part, str):
            if "?" in part or "#" in part:
                return parts, []
        elif part.operator in ("?", "#"):
            return parts, []

    query_vars: list[Variable] = []
    for part in parts[split:]:
        assert isinstance(part, _Expression)
        query_vars.extend(part.variables)

    return parts[:split], query_vars


def _parse(template: str, *, max_variables: int) -> tuple[list[_Part], list[Variable]]:
    """Split a template into an ordered sequence of literals and expressions.

    Walks the string, alternating between collecting literal runs and
    parsing ``{...}`` expressions. The resulting ``parts`` sequence
    preserves positional interleaving so ``match()`` and ``expand()`` can
    walk it in order.

    Raises:
        InvalidUriTemplate: On unclosed braces, too many expressions, or
            any error surfaced by :func:`_parse_expression`.
    """
    parts: list[_Part] = []
    variables: list[Variable] = []
    i = 0
    n = len(template)

    while i < n:
        # Find the next expression opener from the current cursor.
        brace = template.find("{", i)

        if brace == -1:
            # No more expressions; everything left is a trailing literal.
            parts.append(template[i:])
            break

        if brace > i:
            # Literal text between cursor and the brace.
            parts.append(template[i:brace])

        end = template.find("}", brace)
        if end == -1:
            raise InvalidUriTemplate(
                f"Unclosed expression at position {brace}",
                template=template,
                position=brace,
            )

        # Delegate body (between braces, exclusive) to the expression parser.
        expr = _parse_expression(template, template[brace + 1 : end], brace)
        parts.append(expr)
        variables.extend(expr.variables)

        if len(variables) > max_variables:
            raise InvalidUriTemplate(
                f"Template exceeds maximum of {max_variables} variables",
                template=template,
            )

        # Advance past the closing brace.
        i = end + 1

    _check_duplicate_variables(template, variables)
    _check_single_query_expression(template, parts)
    return parts, variables


def _parse_expression(template: str, body: str, pos: int) -> _Expression:
    """Parse the body of a single ``{...}`` expression.

    The body is everything between the braces. It consists of an optional
    leading operator character followed by one or more comma-separated
    variable specifiers. Each specifier is a name with an optional
    trailing ``*`` (explode modifier).

    Args:
        template: The full template string, for error reporting.
        body: The expression body, braces excluded.
        pos: Character offset of the opening brace, for error reporting.

    Raises:
        InvalidUriTemplate: On empty body, invalid variable names, or
            unsupported modifiers.
    """
    if not body:
        raise InvalidUriTemplate(f"Empty expression at position {pos}", template=template, position=pos)

    # Peel off the operator, if any. Membership check justifies the cast.
    operator: Operator = ""
    if body[0] in _OPERATORS:
        operator = cast(Operator, body[0])
        body = body[1:]
        if not body:
            raise InvalidUriTemplate(
                f"Expression has operator but no variables at position {pos}",
                template=template,
                position=pos,
            )

    # Remaining body is comma-separated variable specs: name[*]
    variables: list[Variable] = []
    for spec in body.split(","):
        if ":" in spec:
            raise InvalidUriTemplate(
                f"Prefix modifier {{var:N}} is not supported (in {spec!r} at position {pos})",
                template=template,
                position=pos,
            )

        explode = spec.endswith("*")
        name = spec[:-1] if explode else spec

        if not _VARNAME_RE.fullmatch(name):
            raise InvalidUriTemplate(
                f"Invalid variable name {name!r} at position {pos}",
                template=template,
                position=pos,
            )

        # Explode only makes sense for operators that repeat a separator.
        # Simple/reserved/fragment have no per-item separator; query-explode
        # needs order-agnostic dict matching which we don't support yet.
        if explode and operator in ("", "+", "#", "?", "&"):
            raise InvalidUriTemplate(
                f"Explode modifier on {{{operator}{name}*}} is not supported for matching",
                template=template,
                position=pos,
            )

        variables.append(Variable(name=name, operator=operator, explode=explode))

    return _Expression(operator=operator, variables=variables)


def _check_duplicate_variables(template: str, variables: list[Variable]) -> None:
    """Reject templates that use the same variable name more than once.

    RFC 6570 requires repeated variables to expand to the same value,
    which would require backreference matching with potentially
    exponential cost. Rather than silently returning only the last
    captured value, we reject at parse time.

    Raises:
        InvalidUriTemplate: If any variable name appears more than once.
    """
    seen: set[str] = set()
    for var in variables:
        if var.name in seen:
            raise InvalidUriTemplate(
                f"Variable {var.name!r} appears more than once; repeated variables are not supported",
                template=template,
            )
        seen.add(var.name)


def _check_single_query_expression(template: str, parts: list[_Part]) -> None:
    """Reject templates with more than one ``{?...}`` expression.

    The ``?`` operator emits a leading ``?``, so two such expressions
    expand to a URI with two ``?`` characters — malformed per RFC 3986
    §3.4. Use ``{?a,b}`` or ``{?a}{&b}`` for multiple query parameters.
    """
    seen = False
    for part in parts:
        if isinstance(part, _Expression) and part.operator == "?":
            if seen:
                raise InvalidUriTemplate(
                    "Template contains more than one {?...} expression; "
                    "use {?a,b} or {?a}{&b} for multiple query parameters",
                    template=template,
                )
            seen = True


def _flatten(parts: list[_Part]) -> list[_Atom]:
    """Lower expressions into a flat sequence of literals and single-variable captures.

    Operator prefixes and separators become explicit ``_Lit`` atoms so
    the scan only ever sees two atom kinds. Adjacent literals are
    coalesced so that anchor-finding (``find``/``rfind``) operates on
    the longest possible literal, reducing false matches.

    Explode variables emit no lead literal: the explode capture
    includes its own separator-prefixed repetitions (``{/a*}`` →
    ``/x/y/z``, not ``/`` then ``x/y/z``).
    """
    atoms: list[_Atom] = []

    def push_lit(text: str) -> None:
        if not text:
            return
        if atoms and isinstance(atoms[-1], _Lit):
            atoms[-1] = _Lit(atoms[-1].text + text)
        else:
            atoms.append(_Lit(text))

    for part in parts:
        if isinstance(part, str):
            push_lit(part)
            continue
        spec = _OPERATOR_SPECS[part.operator]
        for i, var in enumerate(part.variables):
            lead = spec.prefix if i == 0 else spec.separator
            if var.explode:
                atoms.append(_Cap(var))
            elif spec.named:
                # ; uses ifemp (bare name when empty); ? and & always
                # emit name= so the equals is part of the literal.
                if part.operator == ";":
                    push_lit(f"{lead}{var.name}")
                    atoms.append(_Cap(var, ifemp=True))
                else:
                    push_lit(f"{lead}{var.name}=")
                    atoms.append(_Cap(var))
            else:
                push_lit(lead)
                atoms.append(_Cap(var))
    return atoms


def _partition_greedy(atoms: list[_Atom], template: str) -> tuple[list[_Atom], Variable | None, list[_Atom]]:
    """Split atoms at the single greedy variable, if any.

    Returns ``(prefix, greedy_var, suffix)``. If there is no greedy
    variable the entire atom list is returned as the suffix so that
    the right-to-left scan (which matches regex-greedy semantics)
    handles it.

    Raises:
        InvalidUriTemplate: If two variables are adjacent with no
            literal between them — whether or not one is the
            multi-segment variable, the scan has nothing to anchor the
            boundary on — or if more than one multi-segment variable
            is present (two are inherently ambiguous: there is no
            principled way to decide which one absorbs an extra
            segment).
    """
    greedy_idx: int | None = None
    prev: _Atom | None = None
    for i, atom in enumerate(atoms):
        if isinstance(atom, _Cap):
            if isinstance(prev, _Cap):
                raise InvalidUriTemplate(
                    f"Variables {prev.var.name!r} and {atom.var.name!r} are adjacent "
                    "with no literal separator; matching cannot determine where one "
                    "ends and the other begins. Add a literal between them or use a "
                    "single variable.",
                    template=template,
                )
            if _is_greedy(atom.var):
                if greedy_idx is not None:
                    raise InvalidUriTemplate(
                        "Template contains more than one multi-segment variable "
                        "({+var}, {#var}, or explode modifier); matching would be ambiguous",
                        template=template,
                    )
                greedy_idx = i
        prev = atom
    if greedy_idx is None:
        return [], None, atoms
    greedy = atoms[greedy_idx]
    assert isinstance(greedy, _Cap)
    return atoms[:greedy_idx], greedy.var, atoms[greedy_idx + 1 :]


def _scan_suffix(
    atoms: Sequence[_Atom], uri: str, end: int, *, anchored: bool
) -> tuple[dict[str, str | list[str]], int] | None:
    """Scan atoms right-to-left from ``end``, returning captures and start position.

    Each bounded variable takes the minimum span that lets its
    preceding literal match (found via ``rfind``), which makes the
    *first* variable in template order greedy — identical to Python
    regex semantics for a sequence of greedy groups.

    When ``anchored`` is true the atom sequence is the entire template
    (no greedy variable), so ``atoms[0]`` must match at URI position 0
    rather than at its rightmost occurrence.
    """
    result: dict[str, str | list[str]] = {}
    pos = end
    i = len(atoms) - 1
    while i >= 0:
        atom = atoms[i]
        if isinstance(atom, _Lit):
            n = len(atom.text)
            if pos < n or uri[pos - n : pos] != atom.text:
                return None
            pos -= n
            i -= 1
            continue

        var = atom.var
        stops = _STOP_CHARS[var.operator]
        prev = atoms[i - 1] if i > 0 else None

        if atom.ifemp:
            # ;name or ;name=value. The preceding _Lit is ";name".
            # Try empty first: if the lit ends at pos the value is
            # absent (RFC ifemp). Otherwise require =value.
            assert isinstance(prev, _Lit)
            if uri.endswith(prev.text, 0, pos):
                result[var.name] = ""
                i -= 1
                continue
            earliest = pos
            while earliest > 0 and uri[earliest - 1] not in stops:
                earliest -= 1
            eq = uri.find("=", earliest, pos)
            if eq == -1:
                return None
            result[var.name] = unquote(uri[eq + 1 : pos])
            pos = eq
            i -= 1
            continue

        # Earliest valid start: the var cannot extend left past any
        # stop-char, so scan backward to find that boundary.
        earliest = pos
        while earliest > 0 and uri[earliest - 1] not in stops:
            earliest -= 1

        if prev is None:
            start = earliest
        else:
            # prev is a _Lit: the parser rejects two adjacent captures,
            # so the only possible neighbour kind is a literal.
            assert isinstance(prev, _Lit)
            if anchored and i - 1 == 0:
                # First atom of the whole template: positionally fixed at
                # 0, not rightmost occurrence. rfind would land inside the
                # value when the literal repeats there (e.g. "prefix-{id}"
                # against "prefix-prefix-123").
                start = len(prev.text)
                if start < earliest or start > pos:
                    return None
            else:
                # Rightmost occurrence of the preceding literal whose end
                # falls within the var's valid range.
                idx = uri.rfind(prev.text, 0, pos)
                if idx == -1 or idx + len(prev.text) < earliest:
                    return None
                start = idx + len(prev.text)

        result[var.name] = unquote(uri[start:pos])
        pos = start
        i -= 1
    return result, pos


def _scan_prefix(
    atoms: Sequence[_Atom], uri: str, start: int, limit: int
) -> tuple[dict[str, str | list[str]], int] | None:
    """Scan atoms left-to-right from ``start``, not exceeding ``limit``.

    Each bounded variable takes the minimum span that lets its
    following literal match (found via ``find``), leaving the
    greedy variable as much of the URI as possible.
    """
    result: dict[str, str | list[str]] = {}
    pos = start
    for i, atom in enumerate(atoms):
        if isinstance(atom, _Lit):
            end = pos + len(atom.text)
            if end > limit or uri[pos:end] != atom.text:
                return None
            pos = end
            continue

        var = atom.var
        stops = _STOP_CHARS[var.operator]
        # Every capture here is followed by a literal: the parser rejects
        # two adjacent captures, and a capture at the END of the prefix
        # would be adjacent to the greedy variable.
        nxt = atoms[i + 1]
        assert isinstance(nxt, _Lit)

        if atom.ifemp:
            # RFC §3.2.7 ifemp: ;name=val for non-empty, bare ;name for
            # empty. Decide which form is present without falling through
            # to the stop-char scan when the value is empty.
            if uri.startswith(nxt.text, pos):
                # Following literal begins immediately: value is empty.
                # Checked before '=' so a literal that itself starts
                # with '=' is not mistaken for the ifemp separator.
                result[var.name] = ""
                continue
            if pos < limit and uri[pos] == "=":
                pos += 1  # value follows; fall through to the scan
            else:
                # The following literal does not start here and there is
                # no '=': the URI's name continued past the template's
                # (e.g. ;keys vs ;key) — no parse.
                return None

        # Latest valid end: the var stops at the first stop-char or
        # the scan limit, whichever comes first.
        latest = pos
        while latest < limit and uri[latest] not in stops:
            latest += 1

        # First occurrence of the following literal: the capture takes
        # the minimum span, leaving the greedy variable as much of the
        # URI as possible. The search window's upper bound already
        # forces any hit to start at or before ``latest``, so the var
        # never extends past a stop-char.
        end = uri.find(nxt.text, pos, latest + len(nxt.text))
        if end == -1:
            return None

        result[var.name] = unquote(uri[pos:end])
        pos = end
    return result, pos
