"""Input validators shared across the platform.

All validators follow the Django convention: raise ``ValueError`` on
invalid input and return the cleaned value on success.  They can be used
standalone (``validate_mobile_number(value)``) or wrapped into a Django
``ValidationError`` at the view/serializer boundary.

No imports from ``simorgh.apps.*`` — pure-Python only.

Usage::

    from simorgh.shared.validators import validate_mobile_number, validate_national_id

    clean_mobile = validate_mobile_number("+989123456789")
    clean_id     = validate_national_id("0012345678", country="IR")
"""
from __future__ import annotations

import re
import unicodedata

__all__ = [
    "validate_mobile_number",
    "validate_iban",
    "validate_national_id",
    "validate_email",
    "validate_slug",
    "validate_positive_integer",
    "ValidationError",
]


class ValidationError(ValueError):
    """Raised when a validator rejects the given value."""

    def __init__(self, message: str, code: str = "invalid") -> None:
        super().__init__(message)
        self.message = message
        self.code = code

    def __str__(self) -> str:  # pragma: no cover
        return self.message


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _digits_only(value: str) -> str:
    """Strip non-digit characters (including Arabic-Indic digits → ASCII)."""
    value = value.translate(str.maketrans("٠١٢٣٤٥٦٧٨٩", "0123456789"))
    value = value.translate(str.maketrans("۰۱۲۳۴۵۶۷۸۹", "0123456789"))
    return re.sub(r"\D", "", value)


# ---------------------------------------------------------------------------
# Mobile number
# ---------------------------------------------------------------------------

# E.164 pattern — +<country><number>, 7–15 digits total after "+"
_E164_RE = re.compile(r"^\+[1-9]\d{6,14}$")

# Country-specific prefixes (extend as needed)
_MOBILE_PREFIXES: dict[str, tuple[str, int]] = {
    "IR": ("98", 12),   # +989xxxxxxxxx  → 12 digits after +
    "AE": ("971", 12),  # +9715xxxxxxxx  → 12 digits after +
    "SA": ("966", 12),  # +9665xxxxxxxx  → 12 digits after +
    "TR": ("90", 12),   # +905xxxxxxxxx  → 12 digits after +
    "AF": ("93", 11),   # +937xxxxxxxx   → 11 digits after +
    "PK": ("92", 12),   # +923xxxxxxxxx  → 12 digits after +
}


def validate_mobile_number(value: str, country_code: str = "IR") -> str:
    """Validate and normalise a mobile number to E.164 format.

    Accepts local formats (``09123456789``), national formats (``9123456789``),
    and international formats (``+989123456789``).

    :returns: E.164 string, e.g. ``"+989123456789"``
    :raises:  :exc:`ValidationError`
    """
    if not isinstance(value, str):
        raise ValidationError("Mobile number must be a string.", code="invalid_type")

    # Strip whitespace, dashes, dots, parentheses
    cleaned = re.sub(r"[\s\-.()\u200c]", "", value.strip())
    # Normalise Farsi/Arabic digits
    cleaned = cleaned.translate(str.maketrans("٠١٢٣٤٥٦٧٨٩", "0123456789"))
    cleaned = cleaned.translate(str.maketrans("۰۱۲۳۴۵۶۷۸۹", "0123456789"))

    country = country_code.upper()

    if country in _MOBILE_PREFIXES:
        prefix, expected_len = _MOBILE_PREFIXES[country]
        # Local format: starts with 0
        if cleaned.startswith("0"):
            cleaned = "+" + prefix + cleaned[1:]
        # National format (no leading 0 or +)
        elif cleaned.startswith(prefix):
            cleaned = "+" + cleaned
        elif not cleaned.startswith("+"):
            cleaned = "+" + prefix + cleaned

    if not _E164_RE.match(cleaned):
        raise ValidationError(
            f"'{value}' is not a valid mobile number.",
            code="invalid_mobile",
        )

    if country in _MOBILE_PREFIXES:
        prefix, expected_len = _MOBILE_PREFIXES[country]
        if not cleaned.startswith("+" + prefix):
            raise ValidationError(
                f"Mobile number does not match country {country}.",
                code="country_mismatch",
            )
        if len(cleaned) != expected_len + 1:  # +1 for leading "+"
            raise ValidationError(
                f"Mobile number has wrong length for {country}.",
                code="invalid_length",
            )

    return cleaned


# ---------------------------------------------------------------------------
# IBAN
# ---------------------------------------------------------------------------

_IBAN_COUNTRY_LENGTHS: dict[str, int] = {
    "IR": 26, "DE": 22, "GB": 22, "FR": 27, "SA": 24, "AE": 23,
    "TR": 26, "PL": 28, "NL": 18, "BE": 16, "CH": 21, "AT": 20,
}

_IBAN_DIGIT_MAP = {chr(ord("A") + i): str(10 + i) for i in range(26)}


def _iban_check_digits(iban: str) -> int:
    rearranged = iban[4:] + iban[:4]
    numeric = "".join(_IBAN_DIGIT_MAP.get(ch, ch) for ch in rearranged)
    return int(numeric) % 97


def validate_iban(value: str) -> str:
    """Validate an IBAN and return it in normalised (no spaces, upper) form.

    :raises: :exc:`ValidationError`
    """
    if not isinstance(value, str):
        raise ValidationError("IBAN must be a string.", code="invalid_type")

    iban = re.sub(r"\s", "", value).upper()

    if len(iban) < 4:
        raise ValidationError("IBAN too short.", code="too_short")

    country = iban[:2]
    if not country.isalpha():
        raise ValidationError("IBAN must start with a country code.", code="invalid_format")

    expected = _IBAN_COUNTRY_LENGTHS.get(country)
    if expected and len(iban) != expected:
        raise ValidationError(
            f"IBAN for {country} must be {expected} characters long.",
            code="invalid_length",
        )

    if not re.match(r"^[A-Z]{2}[0-9]{2}[A-Z0-9]+$", iban):
        raise ValidationError("IBAN contains invalid characters.", code="invalid_chars")

    if _iban_check_digits(iban) != 1:
        raise ValidationError("IBAN checksum failed.", code="bad_checksum")

    return iban


# ---------------------------------------------------------------------------
# National ID
# ---------------------------------------------------------------------------

def _validate_ir_national_id(value: str) -> str:
    """Validate Iranian national code (کد ملی)."""
    digits = _digits_only(value)
    if len(digits) != 10:
        raise ValidationError(
            "Iranian national ID must be exactly 10 digits.",
            code="invalid_length",
        )
    if len(set(digits)) == 1:
        raise ValidationError(
            "Iranian national ID cannot be all identical digits.",
            code="invalid_value",
        )
    total = sum(int(digits[i]) * (10 - i) for i in range(9))
    remainder = total % 11
    check = int(digits[9])
    if remainder < 2 and check != remainder:
        raise ValidationError("Iranian national ID checksum failed.", code="bad_checksum")
    if remainder >= 2 and check != (11 - remainder):
        raise ValidationError("Iranian national ID checksum failed.", code="bad_checksum")
    return digits


def validate_national_id(value: str, country: str = "IR") -> str:
    """Validate a national identification number for *country*.

    :param value:   Raw input string (digits, spaces, dashes accepted).
    :param country: ISO 3166-1 alpha-2 country code.
    :returns:       Cleaned digit-only string.
    :raises:        :exc:`ValidationError`
    """
    country = country.upper()
    if country == "IR":
        return _validate_ir_national_id(value)
    # Fallback: generic non-empty digits check for unsupported countries
    digits = _digits_only(value)
    if not digits:
        raise ValidationError(
            f"National ID for country {country} must contain digits.",
            code="invalid",
        )
    return digits


# ---------------------------------------------------------------------------
# Email
# ---------------------------------------------------------------------------

_EMAIL_RE = re.compile(
    r"^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$"
)


def validate_email(value: str) -> str:
    """Validate a basic email address format.

    :returns: Lowercased, stripped email.
    :raises:  :exc:`ValidationError`
    """
    if not isinstance(value, str):
        raise ValidationError("Email must be a string.", code="invalid_type")
    cleaned = value.strip().lower()
    if not _EMAIL_RE.match(cleaned):
        raise ValidationError(f"'{value}' is not a valid email address.", code="invalid_email")
    if len(cleaned) > 254:  # RFC 5321 max length
        raise ValidationError("Email address is too long.", code="too_long")
    return cleaned


# ---------------------------------------------------------------------------
# Slug
# ---------------------------------------------------------------------------

_SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")


def validate_slug(value: str) -> str:
    """Validate a lowercase URL slug (ASCII, hyphens, no leading/trailing hyphen).

    :raises: :exc:`ValidationError`
    """
    if not isinstance(value, str) or not value:
        raise ValidationError("Slug must be a non-empty string.", code="required")
    if not _SLUG_RE.match(value):
        raise ValidationError(
            f"'{value}' is not a valid slug. Use lowercase letters, numbers, and hyphens.",
            code="invalid_slug",
        )
    return value


# ---------------------------------------------------------------------------
# Positive integer
# ---------------------------------------------------------------------------

def validate_positive_integer(value: Any, field_name: str = "value") -> int:
    """Validate that *value* is a positive integer (> 0).

    :raises: :exc:`ValidationError`
    """
    try:
        v = int(value)
    except (TypeError, ValueError):
        raise ValidationError(f"{field_name} must be an integer.", code="invalid_type")
    if v <= 0:
        raise ValidationError(f"{field_name} must be positive.", code="not_positive")
    return v
