"""The five v0.1 built-in factories.

See: ``__init__.py`` for usage. Each factory's output passes its
matcher counterpart in :mod:`mgf.apiprobe.core.matchers`.

Randomness uses :mod:`secrets` rather than :mod:`random` — not for
cryptographic strength (test data doesn't need it) but to dodge ruff's
S311 / S301 family of warnings when consumers eyeball the source. The
performance difference is negligible at probe-construction rates.
"""

from __future__ import annotations

import secrets
import uuid as _uuid_stdlib
from datetime import UTC, datetime


def uuid() -> str:
    """Return a UUID v4 string (canonical 8-4-4-4-12 hex)."""
    return str(_uuid_stdlib.uuid4())


def iso8601_z(*, when: datetime | None = None) -> str:
    """Return an ISO 8601 timestamp string with explicit UTC ``Z``.

    Defaults to "now" in UTC. Pass ``when=`` to anchor on a specific
    moment. Output passes :func:`mgf.apiprobe.is_iso8601_z`.
    """
    moment = when or datetime.now(tz=UTC)
    if moment.tzinfo is None:
        # Treat naive datetime as UTC; better than producing a
        # never-passing-the-matcher timestamp.
        moment = moment.replace(tzinfo=UTC)
    return moment.isoformat().replace("+00:00", "Z")


# ---------------------------------------------------------------------------
# IBAN — generates a valid IBAN with correct MOD-97 check digits.
# ---------------------------------------------------------------------------


# Per-country BBAN length. Spec is per-country in ISO 13616. We support
# a curated set; consumers needing other countries can compose via the
# matcher API directly.
_IBAN_BBAN_LENGTH: dict[str, int] = {
    "DE": 18,
    "GB": 18,
    "FR": 23,
    "ES": 20,
    "IT": 23,
    "NL": 14,
}


def iban(*, country: str = "DE") -> str:
    """Generate a syntactically + checksum-valid IBAN for ``country``.

    Supported countries: DE (Germany), GB (UK), FR (France), ES (Spain),
    IT (Italy), NL (Netherlands). ``country`` is uppercased.

    Output passes :func:`mgf.apiprobe.is_iban`.

    Note: the BBAN portion is random — the IBAN is structurally valid
    but does NOT correspond to any real bank account. Use only in
    tests; routing live transactions to a generated IBAN is at best a
    NoOp and at worst a tax-reporting error.
    """
    country_code = country.upper()
    bban_len = _IBAN_BBAN_LENGTH.get(country_code)
    if bban_len is None:
        raise ValueError(
            f"iban: unsupported country {country!r}; supported: {sorted(_IBAN_BBAN_LENGTH)}"
        )
    # Random BBAN as decimal digits (most countries; we ignore alpha
    # BBANs in v0.1 — they're rare and add complexity).
    bban = "".join(str(secrets.randbelow(10)) for _ in range(bban_len))
    # MOD-97 check digit calculation.
    rearranged = bban + country_code + "00"
    digits = "".join(str(ord(ch) - ord("A") + 10) if ch.isalpha() else ch for ch in rearranged)
    check = 98 - (int(digits) % 97)
    return f"{country_code}{check:02d}{bban}"


# ---------------------------------------------------------------------------
# IMEI — 14 random digits + Luhn check digit.
# ---------------------------------------------------------------------------


def imei() -> str:
    """Generate a valid IMEI: 14 random digits + Luhn check digit (15 total).

    Output passes :func:`mgf.apiprobe.is_imei`.
    """
    body = "".join(str(secrets.randbelow(10)) for _ in range(14))
    check = _luhn_check_digit(body)
    return body + str(check)


def _luhn_check_digit(digits: str) -> int:
    """Compute the Luhn check digit that, appended to ``digits``, yields a valid Luhn number."""
    total = 0
    # We're computing the check for digits PRECEDING the (yet-to-be-
    # added) check digit. From right-to-left, every other digit doubles —
    # but since we'll be appending one digit, the doubling pattern starts
    # at index 0 of the existing sequence (which becomes index 1 after
    # the check digit is appended on the right).
    for i, ch in enumerate(reversed(digits)):
        n = int(ch)
        if i % 2 == 0:  # double the digits that will be in odd positions
            n *= 2
            if n > 9:
                n -= 9
        total += n
    return (10 - total % 10) % 10


# ---------------------------------------------------------------------------
# E.164 — `+` then country_code then random digits.
# ---------------------------------------------------------------------------


def e164(*, country_code: int = 1, length: int = 10) -> str:
    """Generate an E.164-shaped phone number.

    ``country_code`` is the country dialing code as an int (default 1
    for North America). ``length`` is the number of subscriber digits
    (default 10). Total length is ``len(str(country_code)) + length``,
    which must not exceed 15 per the standard.

    Output passes :func:`mgf.apiprobe.is_e164`.
    """
    if country_code < 1 or country_code > 999:
        raise ValueError(f"e164: country_code must be between 1 and 999 (got {country_code})")
    if length < 1:
        raise ValueError(f"e164: length must be ≥ 1 (got {length})")
    cc_digits = len(str(country_code))
    if cc_digits + length > 15:
        raise ValueError(f"e164: country_code ({cc_digits} digits) + length ({length}) > 15")
    subscriber = "".join(str(secrets.randbelow(10)) for _ in range(length))
    return f"+{country_code}{subscriber}"


__all__ = [
    "e164",
    "iban",
    "imei",
    "iso8601_z",
    "uuid",
]
