"""``Matcher`` base — composable, typed response assertions.

See: ``DESIGN.md`` §3.1.5 (Matchers DSL) and §3.7 (Matcher protocol).

A ``Matcher`` is a callable that takes a value and returns ``True`` if
the value satisfies the matcher's predicate. Matchers compose with
``&`` (both must match) and ``|`` (either must match), and invert with
``~``::

    is_uuid() & matches(r"^[0-9a-f-]+$")    # UUID and lowercase
    equals(200) | equals(304)                # 200 OR 304
    ~is_empty()                              # not empty (when collection matchers ship)

Every matcher carries a ``describe()`` string used in :class:`Finding`
evidence. The description is part of the user-facing surface — keep it
short and readable ("> 0", "is uuid").

The Matcher DSL today is the small set required by the default
~18-check catalogue. Collection matchers, cross-field matchers,
and the conditional ``when_status`` modifier are future adds.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any


class Matcher(ABC):
    """Abstract base for every matcher.

    Concrete matchers implement :meth:`matches` (the predicate) and
    :meth:`describe` (the string). Composition operators are inherited.
    """

    @abstractmethod
    def matches(self, value: Any) -> bool:
        """Return True iff ``value`` satisfies this matcher."""

    @abstractmethod
    def describe(self) -> str:
        """Return a short human-readable description of the predicate."""

    def __call__(self, value: Any) -> bool:
        """Allow ``matcher(value)`` as a convenience for ``matcher.matches(value)``."""
        return self.matches(value)

    def __and__(self, other: Matcher) -> Matcher:
        return _AndMatcher(self, other)

    def __or__(self, other: Matcher) -> Matcher:
        return _OrMatcher(self, other)

    def __invert__(self) -> Matcher:
        return _NotMatcher(self)

    def when_present(self) -> Matcher:
        """Wrap this matcher so it tolerates a missing value.

        A matcher returned by :meth:`when_present` returns ``True`` when
        the value is :data:`MISSING` (the sentinel the check engine
        passes when a JSONPath / header lookup misses); otherwise it
        delegates to the underlying matcher. Useful for optional
        response fields::

            "$.archived_at": is_iso8601_z().when_present()
        """
        return _WhenPresentMatcher(self)


class _MISSING:
    """Sentinel for a value the check engine couldn't locate.

    Distinct from ``None`` — ``None`` can be a real JSON value, while
    a missing key is structural absence.
    """

    _instance: _MISSING | None = None

    def __new__(cls) -> _MISSING:
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __repr__(self) -> str:
        return "MISSING"


MISSING = _MISSING()
"""Sentinel passed to matchers when a JSONPath / header lookup misses.

Matchers do NOT match ``MISSING`` by default; use
:meth:`Matcher.when_present` to opt in.
"""


# ---------------------------------------------------------------------------
# Composition implementations. Hidden behind underscore — consumers
# don't construct these directly; they emerge from `&` / `|` / `~`.
# ---------------------------------------------------------------------------


class _AndMatcher(Matcher):
    def __init__(self, left: Matcher, right: Matcher) -> None:
        self.left = left
        self.right = right

    def matches(self, value: Any) -> bool:
        return self.left.matches(value) and self.right.matches(value)

    def describe(self) -> str:
        return f"({self.left.describe()}) AND ({self.right.describe()})"


class _OrMatcher(Matcher):
    def __init__(self, left: Matcher, right: Matcher) -> None:
        self.left = left
        self.right = right

    def matches(self, value: Any) -> bool:
        return self.left.matches(value) or self.right.matches(value)

    def describe(self) -> str:
        return f"({self.left.describe()}) OR ({self.right.describe()})"


class _NotMatcher(Matcher):
    def __init__(self, inner: Matcher) -> None:
        self.inner = inner

    def matches(self, value: Any) -> bool:
        return not self.inner.matches(value)

    def describe(self) -> str:
        return f"NOT ({self.inner.describe()})"


class _WhenPresentMatcher(Matcher):
    def __init__(self, inner: Matcher) -> None:
        self.inner = inner

    def matches(self, value: Any) -> bool:
        if value is MISSING:
            return True
        return self.inner.matches(value)

    def describe(self) -> str:
        return f"{self.inner.describe()} (when present)"


__all__ = ["MISSING", "Matcher"]
