#!/usr/bin/env python3
"""Vérifie la structure des contenus, le vocabulaire et les signes."""

import re
import sys
import tomllib
import unicodedata
from datetime import date
from pathlib import Path
from typing import Any

from _schema import KNOWN_SECTIONS, VOCABULARY_FIELDS

ROOT_DIR = Path(__file__).resolve().parent.parent
CONTENT_DIR = ROOT_DIR / "src" / "content"

MZL_FILENAME_RE = re.compile(r"mzl-([0-9]{3})\.md")
MZL_VALUE_RE = re.compile(r"[0-9]{3}")


class Validator:
    def __init__(self) -> None:
        self.errors = 0

    def error(self, path: Path | None, message: str) -> None:
        if path is None:
            label = ""
        else:
            label = f"{path.relative_to(ROOT_DIR).as_posix()} : "
        print(f"ERREUR : {label}{message}", file=sys.stderr)
        self.errors += 1

    def finish(self) -> int:
        print(f"\nRésultat : {self.errors} erreur(s).")
        return 1 if self.errors else 0


def is_non_empty_string(value: Any) -> bool:
    return isinstance(value, str) and bool(value.strip())


def read_front_matter(path: Path, validator: Validator) -> dict[str, Any] | None:
    try:
        text = path.read_text(encoding="utf-8")
    except UnicodeDecodeError as exc:
        validator.error(path, f"fichier non valide en UTF-8 ({exc})")
        return None
    except OSError as exc:
        validator.error(path, f"lecture impossible ({exc})")
        return None

    if not unicodedata.is_normalized("NFC", text):
        validator.error(path, "contenu non normalisé en Unicode NFC")

    lines = text.splitlines()
    if not lines or lines[0] != "+++":
        validator.error(path, "front matter TOML initial absent")
        return None

    try:
        closing_index = lines.index("+++", 1)
    except ValueError:
        validator.error(path, "délimiteur final +++ absent")
        return None

    try:
        return tomllib.loads("\n".join(lines[1:closing_index]))
    except tomllib.TOMLDecodeError as exc:
        validator.error(path, f"front matter TOML invalide ({exc})")
        return None


def require_table(
    metadata: dict[str, Any],
    key: str,
    path: Path,
    validator: Validator,
) -> dict[str, Any] | None:
    value = metadata.get(key)
    if not isinstance(value, dict):
        validator.error(path, f"{key} doit être une table TOML")
        return None
    return value


def require_string(
    metadata: dict[str, Any],
    key: str,
    path: Path,
    validator: Validator,
) -> None:
    if not is_non_empty_string(metadata.get(key)):
        validator.error(path, f"{key} doit être une chaîne non vide")


def require_date(
    metadata: dict[str, Any],
    path: Path,
    validator: Validator,
) -> None:
    if not isinstance(metadata.get("date"), date):
        validator.error(path, "date doit être une date TOML valide")


def check_structure(
    path: Path,
    metadata: dict[str, Any],
    validator: Validator,
) -> tuple[str, bool] | None:
    relative_path = path.relative_to(CONTENT_DIR)
    section = relative_path.parts[0] if len(relative_path.parts) > 1 else ""

    if not unicodedata.is_normalized("NFC", relative_path.as_posix()):
        validator.error(path, "chemin non normalisé en Unicode NFC")

    if section and section not in KNOWN_SECTIONS:
        validator.error(path, f"section inconnue « {section} »")

    if path.name == "_index.md":
        require_string(metadata, "title", path, validator)
        return None

    draft = metadata.get("draft")
    if not isinstance(draft, bool):
        validator.error(path, "draft doit être un booléen")
        return None

    if draft:
        return section, True

    require_string(metadata, "title", path, validator)
    if section:
        require_date(metadata, path, validator)
    return section, False


def vocabulary_nature(
    metadata: dict[str, Any],
    path: Path,
    validator: Validator,
) -> str | None:
    taxonomies = require_table(metadata, "taxonomies", path, validator)
    if taxonomies is None:
        return None

    natures = taxonomies.get("natures")
    if not isinstance(natures, list) or len(natures) != 1:
        validator.error(path, "taxonomies.natures doit contenir exactement une nature")
        return None

    nature = natures[0]
    if not isinstance(nature, str):
        validator.error(path, "taxonomies.natures doit contenir une chaîne")
        return None

    if nature not in VOCABULARY_FIELDS:
        validator.error(path, f"nature grammaticale inconnue « {nature} »")
        return None

    return nature


def check_vocabulary(
    path: Path,
    metadata: dict[str, Any],
    validator: Validator,
) -> None:
    nature = vocabulary_nature(metadata, path, validator)
    extra = require_table(metadata, "extra", path, validator)
    if nature is None or extra is None:
        return

    expected_fields = VOCABULARY_FIELDS[nature]
    expected_keys = set(expected_fields)
    actual_keys = set(extra)

    missing_fields = [field for field in expected_fields if field not in actual_keys]
    if missing_fields:
        validator.error(
            path,
            "champs extra manquants pour "
            f"la nature « {nature} » : {', '.join(missing_fields)}",
        )

    unexpected_fields = sorted(actual_keys - expected_keys)
    if unexpected_fields:
        validator.error(
            path,
            "champs extra inattendus pour "
            f"la nature « {nature} » : {', '.join(unexpected_fields)}",
        )

    for field in expected_fields:
        if field not in extra:
            continue

        value = extra[field]
        if field == "logograms":
            if not isinstance(value, list) or not all(
                is_non_empty_string(item) for item in value
            ):
                validator.error(
                    path,
                    "extra.logograms doit être un tableau de chaînes non vides",
                )
        elif not isinstance(value, str):
            validator.error(path, f"extra.{field} doit être une chaîne")

    required_fields = ("meaning", "stem") if nature == "verbe" else ("meaning",)
    for field in required_fields:
        value = extra.get(field)
        if isinstance(value, str) and not value.strip():
            validator.error(path, f"extra.{field} doit être une chaîne non vide")


def filename_mzl(path: Path, validator: Validator) -> str | None:
    match = MZL_FILENAME_RE.fullmatch(path.name)
    if match is None:
        validator.error(path, "le nom du fichier doit suivre le format mzl-XXX.md")
        return None

    value = match.group(1)
    if int(value) == 0:
        validator.error(path, "le numéro MZL doit être compris entre 001 et 999")
        return None
    return value


def check_sign(
    path: Path,
    metadata: dict[str, Any],
    expected_mzl: str | None,
    validator: Validator,
) -> None:
    extra = require_table(metadata, "extra", path, validator)
    if extra is None:
        return

    require_string(extra, "sign", path, validator)

    declared_mzl = extra.get("mzl")
    if not isinstance(declared_mzl, str) or MZL_VALUE_RE.fullmatch(declared_mzl) is None:
        validator.error(path, "extra.mzl doit être une chaîne de trois chiffres ASCII")
        return
    if int(declared_mzl) == 0:
        validator.error(path, "extra.mzl doit être compris entre 001 et 999")
        return
    if expected_mzl is not None and declared_mzl != expected_mzl:
        validator.error(
            path,
            f'extra.mzl vaut "{declared_mzl}", mais le fichier indique MZL {expected_mzl}',
        )


def main() -> int:
    validator = Validator()

    if not CONTENT_DIR.is_dir():
        validator.error(None, f"répertoire introuvable : {CONTENT_DIR}")
        return validator.finish()

    print("Vérification des contenus…", flush=True)
    for path in sorted(CONTENT_DIR.rglob("*.md")):
        metadata = read_front_matter(path, validator)
        if metadata is None:
            continue

        structure = check_structure(path, metadata, validator)
        if structure is None:
            continue

        section, is_draft = structure
        expected_mzl = filename_mzl(path, validator) if section == "signes" else None
        if is_draft:
            continue

        if section == "vocabulaire":
            check_vocabulary(path, metadata, validator)
        elif section == "signes":
            check_sign(path, metadata, expected_mzl, validator)

    return validator.finish()


if __name__ == "__main__":
    raise SystemExit(main())
