"""Normalisation et validation du domaine d'un site (spec 2026-08-31 §4).

Fonction pure, sans I/O. La forme stockée est l'hôte ASCII (punycode) ; l'affichage
Unicode est l'affaire de `web/lib/format-domain.ts`.
"""

import ipaddress
from urllib.parse import urlsplit

import idna

MAX_DOMAIN_LENGTH = 253


class DomainError(ValueError):
    """Domaine invalide — mappée en 422 Problem Details par les routes qui l'attrapent."""


def normalize_domain(raw: str) -> str:
    value = raw.strip()
    if not value:
        raise DomainError("Domaine vide")
    # Un hôte nu passe par le même parsing qu'une URL complète : chemin, query et fragment
    # sont ignorés (l'utilisateur colle souvent une URL profonde).
    if "://" not in value:
        value = f"http://{value}"
    # urlsplit lève un ValueError brut sur les crochets déséquilibrés (« [::1 », « exemple].fr ») :
    # sans ce garde-fou, la route répondrait 500 au lieu de 422.
    try:
        parts = urlsplit(value)
    except ValueError as exc:
        raise DomainError("Entrée illisible comme URL") from exc
    if parts.scheme not in ("http", "https"):
        raise DomainError(f"Schéma « {parts.scheme} » refusé (http ou https uniquement)")
    if "@" in parts.netloc:
        raise DomainError("Identifiants dans l'URL refusés")
    try:
        port = parts.port
    except ValueError as exc:
        raise DomainError("Port illisible") from exc
    if port is not None:
        raise DomainError("Port explicite refusé")
    host = parts.hostname
    if not host:
        raise DomainError("Aucun hôte dans l'entrée")
    if host.endswith("."):
        raise DomainError("Point final refusé")
    try:
        ipaddress.ip_address(host)
    except ValueError:
        pass
    else:
        raise DomainError("Adresse IP refusée : entrer un nom de domaine")
    host = host.lower()
    if host == "localhost" or "." not in host:
        raise DomainError("Hôte sans TLD refusé")
    # L'encodage IDNA valide au passage les labels (longueur, caractères, tirets).
    try:
        ascii_host = idna.encode(host, uts46=True).decode("ascii")
    except idna.IDNAError as exc:
        raise DomainError(f"Domaine invalide : {exc}") from exc
    # `ip_address()` ne reconnaît que les formes strictes : « 127.1 », « 0x7f.1 » ou
    # « 0x7f.0.0.1 » lui échappent alors que `getaddrinfo` — comme les navigateurs et curl —
    # les résout en 127.0.0.1. Un TLD réel n'est jamais numérique ni hexadécimal (RFC 3696),
    # donc exiger un TLD alphabétique élimine ces notations inet_aton d'un seul coup.
    # La garde s'applique à `ascii_host` et non à `host` : sur la forme Unicode, `isalpha()`
    # est faux pour les marques combinantes (Mn/Mc) et rejetterait à tort les ccTLD IDN
    # délégués (.भारत, .বাংলা, .ලංකා, .சிங்கப்பூர்…). Après encodage, le TLD est ASCII par
    # construction — alphabétique, ou « xn-- » pour les IDN.
    tld = ascii_host.rpartition(".")[2]
    if not (tld.startswith("xn--") or tld.isalpha()):
        raise DomainError(f"TLD « {tld} » invalide : attendu alphabétique ou « xn-- »")
    if len(ascii_host) > MAX_DOMAIN_LENGTH:
        raise DomainError(f"Domaine trop long (> {MAX_DOMAIN_LENGTH} caractères)")
    return ascii_host
