"""Format matchers — well-known string shapes.

See: ``DESIGN.md`` §3.1.5 row "Type / format".

These matchers are stricter than regex — they invoke real parsers
(``uuid.UUID``, ``datetime.fromisoformat``) rather than pattern-only
checks so subtly malformed inputs (12-section UUID, missing TZ on a
date) get caught.

The ``is_email`` / ``is_url`` matchers stay regex-based because there
is no stdlib parser whose strictness matches what consumers expect of
"a sane email" / "a sane URL". The regexes are deliberately
permissive (catch obvious garbage; defer hairy edge cases to the
consumer's domain matchers).
"""

from __future__ import annotations

import re
from datetime import datetime
from typing import Any
from uuid import UUID

from mgf.apiprobe.core.matcher import Matcher

# ---------------------------------------------------------------------------
# UUID. Accepts any RFC 4122 variant (1, 3, 4, 5, 7).
# ---------------------------------------------------------------------------


class _IsUuid(Matcher):
    def matches(self, value: Any) -> bool:
        if not isinstance(value, str):
            return False
        try:
            UUID(value)
        except (ValueError, AttributeError):
            return False
        return True

    def describe(self) -> str:
        return "is uuid"


def is_uuid() -> Matcher:
    """Match if the value parses as a UUID via :class:`uuid.UUID`.

    Accepts any UUID variant — v1, v4, v7, etc. Rejects the bare
    32-hex form without dashes (``UUID()`` accepts it; we don't, since
    that shape is a common provider bug).
    """
    return _IsUuid()


# ---------------------------------------------------------------------------
# ISO 8601 with mandatory ``Z`` (UTC). The most common provider-side
# error is "drops the TZ suffix" — explicitly checking for ``Z`` (or
# ``+00:00``) catches it.
# ---------------------------------------------------------------------------


class _IsIso8601Z(Matcher):
    def matches(self, value: Any) -> bool:
        if not isinstance(value, str):
            return False
        # Must end in Z or have an explicit offset; naive timestamps fail.
        if not (value.endswith("Z") or _has_explicit_offset(value)):
            return False
        try:
            # fromisoformat (3.11+) accepts the Z suffix.
            datetime.fromisoformat(value)
        except ValueError:
            return False
        return True

    def describe(self) -> str:
        return "is iso8601 with explicit timezone"


_OFFSET_RE = re.compile(r"[+-]\d{2}:\d{2}$")


def _has_explicit_offset(value: str) -> bool:
    return bool(_OFFSET_RE.search(value))


def is_iso8601_z() -> Matcher:
    """Match if the value parses as an ISO 8601 timestamp with explicit TZ.

    Accepts trailing ``Z`` (UTC) or an explicit ``±HH:MM`` offset.
    Rejects naive timestamps (no offset) — those are the most common
    source of cross-system date bugs and the matcher is calibrated to
    catch them.
    """
    return _IsIso8601Z()


# ---------------------------------------------------------------------------
# URL — basic shape. http/https only by default. Stricter than the
# stdlib's ``urlparse`` (which accepts everything).
# ---------------------------------------------------------------------------


_URL_RE = re.compile(
    r"^https?://"  # scheme
    r"[a-zA-Z0-9.\-_]+"  # hostname
    r"(?::\d+)?"  # optional port
    r"(?:/[^\s]*)?$"  # optional path + query (no whitespace)
)


class _IsUrl(Matcher):
    def matches(self, value: Any) -> bool:
        return isinstance(value, str) and _URL_RE.match(value) is not None

    def describe(self) -> str:
        return "is url (http/https)"


def is_url() -> Matcher:
    """Match if the value is an http(s) URL with a host component.

    Deliberately permissive: catches obvious non-URLs but does NOT
    validate hostname DNS shape, query syntax, or fragment well-formed-
    ness. Use a regex via :func:`mgf.apiprobe.matches` if you need a
    domain-specific shape.
    """
    return _IsUrl()


# ---------------------------------------------------------------------------
# Email — basic shape. Per RFC 5321 a "complete" regex is impractical
# (literal addresses, escaped quotes, etc.); the calibration target
# here is "catches obvious typos like 'foo@', 'foo @bar', 'foobar'".
# ---------------------------------------------------------------------------


_EMAIL_RE = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")


class _IsEmail(Matcher):
    def matches(self, value: Any) -> bool:
        return isinstance(value, str) and _EMAIL_RE.match(value) is not None

    def describe(self) -> str:
        return "is email"


def is_email() -> Matcher:
    """Match if the value looks like an email (``a@b.c`` shape).

    Permissive on purpose — see module docstring. Domain-specific
    formats (e.g. corporate email allow-lists) belong in consumer
    matchers, not here.
    """
    return _IsEmail()


__all__ = [
    "is_email",
    "is_iso8601_z",
    "is_url",
    "is_uuid",
]
