"""Primitive matchers — equality, ordering, set membership, regex, range.

See: ``DESIGN.md`` §3.1.5 — table row "Equality / range".

Matchers are constructed via the lowercase factory functions exported
here (``equals(x)``, ``gt(0)`` etc.). The class objects backing them
are intentionally underscore-prefixed; consumers compose via the
factories + the ``&`` / ``|`` / ``~`` / ``.when_present()`` operators
on :class:`mgf.apiprobe.core.matcher.Matcher`.
"""

from __future__ import annotations

import re
from typing import Any

from mgf.apiprobe.core.matcher import Matcher
from mgf.apiprobe.exceptions import MatcherError

# ---------------------------------------------------------------------------
# Equality / non-equality.
# ---------------------------------------------------------------------------


class _Equals(Matcher):
    def __init__(self, expected: Any) -> None:
        self.expected = expected

    def matches(self, value: Any) -> bool:
        return bool(value == self.expected)

    def describe(self) -> str:
        return f"== {self.expected!r}"


def equals(expected: Any) -> Matcher:
    """Match if the value equals ``expected`` (``==``)."""
    return _Equals(expected)


class _NotEquals(Matcher):
    def __init__(self, expected: Any) -> None:
        self.expected = expected

    def matches(self, value: Any) -> bool:
        return bool(value != self.expected)

    def describe(self) -> str:
        return f"!= {self.expected!r}"


def not_equals(expected: Any) -> Matcher:
    """Match if the value does not equal ``expected``."""
    return _NotEquals(expected)


# ---------------------------------------------------------------------------
# Ordering. Raise MatcherError on type mismatch — comparing strings to
# ints (etc.) is a structural bug in the suite, not a finding.
# ---------------------------------------------------------------------------


def _compare(value: Any, threshold: Any, op: str) -> bool:
    try:
        if op == ">":
            return bool(value > threshold)
        if op == ">=":
            return bool(value >= threshold)
        if op == "<":
            return bool(value < threshold)
        if op == "<=":
            return bool(value <= threshold)
    except TypeError as exc:
        raise MatcherError(f"cannot compare {value!r} {op} {threshold!r}: {exc}") from exc
    raise AssertionError(f"unknown op {op!r}")  # pragma: no cover


class _Gt(Matcher):
    def __init__(self, threshold: Any) -> None:
        self.threshold = threshold

    def matches(self, value: Any) -> bool:
        return _compare(value, self.threshold, ">")

    def describe(self) -> str:
        return f"> {self.threshold!r}"


def gt(threshold: Any) -> Matcher:
    """Match if the value is strictly greater than ``threshold``."""
    return _Gt(threshold)


class _Gte(Matcher):
    def __init__(self, threshold: Any) -> None:
        self.threshold = threshold

    def matches(self, value: Any) -> bool:
        return _compare(value, self.threshold, ">=")

    def describe(self) -> str:
        return f">= {self.threshold!r}"


def gte(threshold: Any) -> Matcher:
    """Match if the value is greater than or equal to ``threshold``."""
    return _Gte(threshold)


class _Lt(Matcher):
    def __init__(self, threshold: Any) -> None:
        self.threshold = threshold

    def matches(self, value: Any) -> bool:
        return _compare(value, self.threshold, "<")

    def describe(self) -> str:
        return f"< {self.threshold!r}"


def lt(threshold: Any) -> Matcher:
    """Match if the value is strictly less than ``threshold``."""
    return _Lt(threshold)


class _Lte(Matcher):
    def __init__(self, threshold: Any) -> None:
        self.threshold = threshold

    def matches(self, value: Any) -> bool:
        return _compare(value, self.threshold, "<=")

    def describe(self) -> str:
        return f"<= {self.threshold!r}"


def lte(threshold: Any) -> Matcher:
    """Match if the value is less than or equal to ``threshold``."""
    return _Lte(threshold)


class _Between(Matcher):
    def __init__(self, low: Any, high: Any) -> None:
        if low > high:
            raise MatcherError(f"between({low!r}, {high!r}): low > high")
        self.low = low
        self.high = high

    def matches(self, value: Any) -> bool:
        return _compare(value, self.low, ">=") and _compare(value, self.high, "<=")

    def describe(self) -> str:
        return f"in [{self.low!r}, {self.high!r}]"


def between(low: Any, high: Any) -> Matcher:
    """Match if ``low <= value <= high`` (inclusive both sides).

    Raises :class:`MatcherError` at construction time if ``low > high``
    — that's a probe-construction bug, not a runtime finding.
    """
    return _Between(low, high)


# ---------------------------------------------------------------------------
# Set membership.
# ---------------------------------------------------------------------------


class _In(Matcher):
    def __init__(self, allowed: tuple[Any, ...]) -> None:
        self.allowed = allowed

    def matches(self, value: Any) -> bool:
        return value in self.allowed

    def describe(self) -> str:
        return f"in {list(self.allowed)!r}"


def in_(*allowed: Any) -> Matcher:
    """Match if the value is in the given set.

    Use as ``in_(200, 304)`` — variadic for ergonomic call sites.
    """
    return _In(allowed)


class _NotIn(Matcher):
    def __init__(self, forbidden: tuple[Any, ...]) -> None:
        self.forbidden = forbidden

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

    def describe(self) -> str:
        return f"not in {list(self.forbidden)!r}"


def not_in(*forbidden: Any) -> Matcher:
    """Match if the value is NOT in the given set."""
    return _NotIn(forbidden)


# ---------------------------------------------------------------------------
# Regex.
# ---------------------------------------------------------------------------


class _Matches(Matcher):
    def __init__(self, pattern: str | re.Pattern[str]) -> None:
        self.pattern = pattern if isinstance(pattern, re.Pattern) else re.compile(pattern)

    def matches(self, value: Any) -> bool:
        if not isinstance(value, str):
            return False
        return self.pattern.search(value) is not None

    def describe(self) -> str:
        return f"matches {self.pattern.pattern!r}"


def matches(pattern: str | re.Pattern[str]) -> Matcher:
    """Match if the value is a string that the regex finds.

    Uses :func:`re.search` — anchor with ``^...$`` for whole-match
    semantics. Non-string values fail the match (return ``False``);
    they do NOT raise. Cross-type comparisons are intentional findings,
    not exceptions.
    """
    return _Matches(pattern)


__all__ = [
    "between",
    "equals",
    "gt",
    "gte",
    "in_",
    "lt",
    "lte",
    "matches",
    "not_equals",
    "not_in",
]
