"""Semantic matchers — validating IBAN, IMEI, E.164 phone numbers.

See: ``DESIGN.md`` §3.1.5 + §3.7 — the semantic field validator
library is the differentiator vs. schema fuzzing. Each matcher does
**real validation**, not just regex shape:

  - ``is_iban``  — MOD-97 checksum (IBAN ISO 13616)
  - ``is_imei``  — Luhn checksum on 15 digits (3GPP TS 23.003)
  - ``is_e164``  — E.164 shape: ``+`` then 1-15 digits

These catch provider bugs schema fuzzers miss: a provider returning
"a value that looks like an IBAN" but with corrupted check digits, a
test fixture with a sequential IMEI that fails Luhn, etc.

Domain-specific telecom matchers (``is_teltonika_imei``,
``is_mapTiler_tile_url``, etc.) live in plugins per DESIGN.md §16
Phase 6 — out of v0.1 scope.
"""

from __future__ import annotations

import re
from typing import Any

from mgf.apiprobe.core.matcher import Matcher

# ---------------------------------------------------------------------------
# IBAN — International Bank Account Number, ISO 13616.
#
# Validation:
#   1. 15-34 alphanumerics; first 2 country code (letters), next 2
#      check digits (digits); rest is the BBAN.
#   2. Move first 4 chars to the end.
#   3. Replace each letter with two digits (A=10..Z=35).
#   4. Resulting integer mod 97 must equal 1.
# ---------------------------------------------------------------------------


_IBAN_SHAPE_RE = re.compile(r"^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$")


def _iban_checksum_ok(iban: str) -> bool:
    rearranged = iban[4:] + iban[:4]
    digits = []
    for char in rearranged:
        if char.isdigit():
            digits.append(char)
        else:
            digits.append(str(ord(char) - ord("A") + 10))
    return int("".join(digits)) % 97 == 1


class _IsIban(Matcher):
    def matches(self, value: Any) -> bool:
        if not isinstance(value, str):
            return False
        normalised = value.replace(" ", "").upper()
        if not _IBAN_SHAPE_RE.match(normalised):
            return False
        return _iban_checksum_ok(normalised)

    def describe(self) -> str:
        return "is iban (ISO 13616, MOD-97 checksum)"


def is_iban() -> Matcher:
    """Match if the value is a valid IBAN (shape + MOD-97 checksum).

    Whitespace is tolerated (banks print IBANs with spaces); case is
    normalised to upper before validation. The checksum step catches
    typos that pass shape-only validation.
    """
    return _IsIban()


# ---------------------------------------------------------------------------
# IMEI — International Mobile Equipment Identity, 3GPP TS 23.003.
#
# 15 decimal digits. Last digit is the Luhn checksum over the first 14.
# (IMEISV exists with 16 digits + no Luhn — out of scope; consumers
# can use a regex-based matcher for that.)
# ---------------------------------------------------------------------------


_IMEI_SHAPE_RE = re.compile(r"^\d{15}$")


def _luhn_ok(digits: str) -> bool:
    """Return True if ``digits`` (decimal-only, length 2+) has valid Luhn checksum."""
    total = 0
    # Process right-to-left; double every second digit starting from
    # the rightmost (which is the check digit itself, NOT doubled).
    for i, ch in enumerate(reversed(digits)):
        n = int(ch)
        if i % 2 == 1:
            n *= 2
            if n > 9:
                n -= 9
        total += n
    return total % 10 == 0


class _IsImei(Matcher):
    def matches(self, value: Any) -> bool:
        if not isinstance(value, str):
            return False
        if not _IMEI_SHAPE_RE.match(value):
            return False
        return _luhn_ok(value)

    def describe(self) -> str:
        return "is imei (15 digits + Luhn)"


def is_imei() -> Matcher:
    """Match if the value is a valid IMEI (15 digits + Luhn checksum)."""
    return _IsImei()


# ---------------------------------------------------------------------------
# E.164 — international phone number format.
#
# ``+`` followed by 1-15 digits. The leading digit must be 1-9 (no
# leading zeros). Subscriber numbers vary by country code; we don't
# validate against country-code databases here (out of scope; consumer
# plugins can add ``is_e164_for_country("DE")`` etc.).
# ---------------------------------------------------------------------------


_E164_RE = re.compile(r"^\+[1-9]\d{0,14}$")


class _IsE164(Matcher):
    def matches(self, value: Any) -> bool:
        if not isinstance(value, str):
            return False
        return _E164_RE.match(value) is not None

    def describe(self) -> str:
        return "is e.164 phone number"


def is_e164() -> Matcher:
    """Match if the value is in E.164 format: ``+`` then 1-15 digits.

    Rejects leading zero after ``+`` (no country code starts with 0).
    Does NOT validate the country code is real or the subscriber
    number length matches that country — those are domain matchers.
    """
    return _IsE164()


__all__ = [
    "is_e164",
    "is_iban",
    "is_imei",
]
