#!/usr/bin/env python3
# verifie_filigrane.py — vérifieur public du Filigrane (FILIGRANE/1)
# https://intranautes.ai/filigrane
#
# Licence MIT
#
# Copyright (c) 2026 Julien Bédouret
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Vérifie une pièce candidate du Filigrane, localement — l'oracle sans
modération : personne à convaincre, rien à soumettre, le calcul répond.

Python 3.10+, bibliothèque standard uniquement. Les paramètres et les
empreintes publiées viennent de filigrane.json (téléchargé à côté de ce
fichier sur https://intranautes.ai/filigrane/filigrane.json — l'empreinte
d'un fragment n'y est publiée que le jour de sa diffusion).

Usage :
  python3 verifie_filigrane.py FRAGMENT "PIÈCE" [--donnees filigrane.json]

La pièce se soumet telle que relevée à l'écran : le run maximal complet
(règle d'extraction de la page). La normalisation est appliquée ici —
majuscules, séparateurs et espaces retirés.

Codes de sortie : 0 = OUI (la pièce répond à l'empreinte du fragment) ·
1 = NON · 2 = vérification impossible (dite, jamais silencieuse).
"""

from __future__ import annotations

import argparse
import hashlib
import json
import re
import sys
import unicodedata
from pathlib import Path

ALPHABET = "23456789BCDFGHJKMNPRSTVX"


def normalise(brut: str) -> str:
    """Majuscules, séparateurs et espaces retirés — la règle de la page."""
    texte = unicodedata.normalize("NFC", brut)
    return re.sub(r"[\s\-._·:/\\]", "", texte).upper()


def verifie(piece: str, fragment: dict, kdf: dict) -> bool:
    candidate = normalise(piece)
    if not candidate:
        raise SystemExit("VÉRIFICATION IMPOSSIBLE : pièce vide après "
                         "normalisation")
    hors = sorted({c for c in candidate if c not in ALPHABET})
    if hors:
        print(f"note : caractères hors alphabet {hors} — une pièce n'en "
              "contient jamais (la réponse sera NON)")
    sel = fragment["sel"].encode("utf-8")
    empreinte = hashlib.scrypt(candidate.encode("utf-8"), salt=sel,
                               n=kdf["n"], r=kdf["r"], p=kdf["p"],
                               maxmem=kdf["maxmem_octets"],
                               dklen=kdf["dklen"]).hex()
    return empreinte == fragment["empreinte"]


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(
        description="Vérifieur local du Filigrane (FILIGRANE/1)")
    ap.add_argument("fragment", type=int, help="numéro du fragment (1–24)")
    ap.add_argument("piece", help="le run maximal relevé à l'écran")
    ap.add_argument("--donnees", type=Path,
                    default=Path(__file__).resolve().parent / "filigrane.json",
                    help="le fichier public filigrane.json (défaut : à côté "
                         "de ce script)")
    args = ap.parse_args(argv)

    if not args.donnees.exists():
        print(f"VÉRIFICATION IMPOSSIBLE : {args.donnees} introuvable — "
              "téléchargez filigrane.json depuis "
              "https://intranautes.ai/filigrane/", file=sys.stderr)
        return 2
    donnees = json.loads(args.donnees.read_text(encoding="utf-8"))
    kdf = donnees["kdf"]
    if kdf.get("type") != "scrypt":
        print("VÉRIFICATION IMPOSSIBLE : KDF inattendu dans les données",
              file=sys.stderr)
        return 2
    # Tous les Python n'exposent pas scrypt : il faut un interpréteur compilé
    # avec OpenSSL 1.1+. Celui que macOS livre ne l'est pas (LibreSSL). Sans
    # cette garde, l'appel plus bas lève une AttributeError et le script sort
    # en code 1 - le code du NON : le visiteur croirait sa pièce fausse alors
    # qu'aucun calcul n'a eu lieu. Une vérification impossible se dit.
    if not hasattr(hashlib, "scrypt"):
        print("VÉRIFICATION IMPOSSIBLE : cet interpréteur Python n'expose "
              "pas hashlib.scrypt - il a été compilé sans OpenSSL 1.1+, ce "
              "qui est le cas du python3 livré avec macOS. Réessayez avec "
              "un Python 3.10+ de python.org ou de Homebrew "
              "(brew install python), en appelant par exemple "
              "python3.12 au lieu de python3.", file=sys.stderr)
        return 2
    fragments = {f["numero"]: f for f in donnees["fragments"]}
    if args.fragment not in fragments:
        print(f"VÉRIFICATION IMPOSSIBLE : fragment {args.fragment} inconnu "
              "(1–24)", file=sys.stderr)
        return 2
    fragment = fragments[args.fragment]
    if "empreinte" not in fragment:
        print(f"VÉRIFICATION IMPOSSIBLE : l'empreinte du fragment "
              f"{args.fragment} n'est pas encore publiée — elle paraît le "
              f"jour de sa diffusion ({fragment.get('diffusion', '?')}).",
              file=sys.stderr)
        return 2

    if verifie(args.piece, fragment, kdf):
        print(f"OUI — c'est la pièce du fragment {args.fragment}.")
        return 0
    print(f"NON — ce n'est pas la pièce du fragment {args.fragment}. "
          "Tout ce qui est étrange n'est pas une pièce.")
    return 1


if __name__ == "__main__":
    sys.exit(main())
