#!/usr/bin/env python3
"""vidimus-map — een hele map in één keer vastleggen bij Vidimus, zonder één bestand te uploaden.

Elk bestand wordt op deze computer gehasht (SHA-256). Naar Vidimus gaat alleen het manifest: pad + vingerafdruk per bestand,
als één registratie. Alleen de standaardbibliotheek van Python 3.8+ is nodig.

  export VIDIMUS_API_KEY=…                    (nooit op de opdrachtregel zelf)
  python3 vidimus_map.py vastleggen ./Dossier-2025 --stroom str_… --ondertekenaar sig_… [--ref JANSSENS-2025]
  python3 vidimus_map.py vastleggen ./Dossier-2025 --droog                 (alleen het manifest maken, niets versturen)
  python3 vidimus_map.py controleer Dossier-2025.manifest.json ./Dossier-2025          (hele map, offline)
  python3 vidimus_map.py controleer Dossier-2025.manifest.json ./Dossier-2025/F-001.pdf (één bestand, offline)
"""
from __future__ import annotations

import argparse
import base64
import hashlib
import json
import os
import sys
import unicodedata
import urllib.error
import urllib.request
from pathlib import Path
from typing import Dict, List

MANIFEST_TYPE = "vidimus-manifest"
MANIFEST_VERSIE = 1
PAYLOAD_TYPE = "map-manifest"
MAX_BESTANDEN = 20000
OVERSLAAN = {".DS_Store", "Thumbs.db", "desktop.ini"}
STUK = 8 * 1024 * 1024


def sha256_bestand(pad: Path) -> str:
    h = hashlib.sha256()
    with pad.open("rb") as f:
        for stuk in iter(lambda: f.read(STUK), b""):
            h.update(stuk)
    return h.hexdigest()


def lees_map(map_pad: Path) -> List[dict]:
    rijen = []
    for p in sorted(map_pad.rglob("*")):
        if not p.is_file() or p.is_symlink() or p.name in OVERSLAAN:
            continue
        rel = unicodedata.normalize("NFC", p.relative_to(map_pad).as_posix())
        rijen.append({"pad": rel, "sha256": sha256_bestand(p), "bytes": p.stat().st_size})
        if len(rijen) > MAX_BESTANDEN:
            raise SystemExit(f"meer dan {MAX_BESTANDEN} bestanden: splits de map")
    if not rijen:
        raise SystemExit("geen bestanden in deze map")
    return rijen


def bouw(map_naam: str, bestanden: List[dict]) -> bytes:
    """Zelfde bytes als de server (vidimus/manifest.py) en de browser (map.js) voor dezelfde map."""
    rijen = sorted(bestanden, key=lambda f: f["pad"])
    m = {"type": MANIFEST_TYPE, "versie": MANIFEST_VERSIE, "map": map_naam[:200], "aantal": len(rijen),
         "bytes": sum(f["bytes"] for f in rijen), "bestanden": rijen}
    return json.dumps(m, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")


def vergelijk(manifest: dict, huidig: List[dict], hele_map: bool) -> Dict[str, list]:
    op_pad = {f["pad"]: f for f in manifest["bestanden"]}
    op_sha: Dict[str, List[str]] = {}
    for f in manifest["bestanden"]:
        op_sha.setdefault(f["sha256"], []).append(f["pad"])
    uit: Dict[str, list] = {"identiek": [], "verplaatst": [], "gewijzigd": [], "nieuw": [], "ontbreekt": []}
    if not hele_map:
        for h in huidig:
            (uit["identiek"] if h["sha256"] in op_sha else uit["nieuw"]).append(h["pad"])
        return uit
    gezien = set()
    for h in huidig:
        vast = op_pad.get(h["pad"])
        if vast:
            uit["identiek" if vast["sha256"] == h["sha256"] else "gewijzigd"].append(h["pad"]); gezien.add(h["pad"])
        elif h["sha256"] in op_sha:
            uit["verplaatst"].append(f'{h["pad"]} (was {op_sha[h["sha256"]][0]})'); gezien.update(op_sha[h["sha256"]])
        else:
            uit["nieuw"].append(h["pad"])
    uit["ontbreekt"] = [p for p in op_pad if p not in gezien]
    return uit


def cmd_vastleggen(a: argparse.Namespace) -> int:
    map_pad = Path(a.map).resolve()
    if not map_pad.is_dir():
        raise SystemExit(f"geen map: {map_pad}")
    raw = bouw(map_pad.name, lees_map(map_pad))
    sha = hashlib.sha256(raw).hexdigest()
    uit = Path(a.manifest) if a.manifest else map_pad.parent / f"{map_pad.name}.manifest.json"   # náást de map: erin zou de map veranderen
    uit.write_bytes(raw)
    m = json.loads(raw)
    sys.stdout.write(f"{m['aantal']} bestanden · {m['bytes']} bytes\nmanifest: {uit}\nsha-256:  {sha}\n")
    if a.droog:
        return 0
    sleutel = os.environ.get("VIDIMUS_API_KEY")
    if not sleutel or not a.stroom or not a.ondertekenaar:
        raise SystemExit("vastleggen vraagt VIDIMUS_API_KEY (omgeving), --stroom en --ondertekenaar; of gebruik --droog")
    payload = {"type": PAYLOAD_TYPE, "map": m["map"], "aantal": m["aantal"], "bytes": m["bytes"], "document_sha256": sha}
    body = {"stream_id": a.stroom, "signer_id": a.ondertekenaar, "payload": payload, "idempotency_key": f"map-{sha[:32]}",
            "attachments": {"manifest.json": base64.b64encode(raw).decode()}}
    if a.ref:
        body["ref"] = a.ref
    req = urllib.request.Request(a.url.rstrip("/") + "/ingest", data=json.dumps(body).encode(), method="POST",
                                 headers={"content-type": "application/json", "x-api-key": sleutel})
    try:
        with urllib.request.urlopen(req, timeout=120) as r:
            reg = json.loads(r.read())
    except urllib.error.HTTPError as e:
        raise SystemExit(f"Vidimus weigerde ({e.code}): {e.read().decode('utf-8', 'replace')[:400]}")
    except urllib.error.URLError as e:
        raise SystemExit(f"geen verbinding met {a.url}: {e.reason}")
    sys.stdout.write(f"vastgelegd: {reg['id']}{' (bestond al)' if reg.get('duplicate') else ''}\nvingerafdruk van de registratie: {reg['hash']}\n")
    return 0


def cmd_controleer(a: argparse.Namespace) -> int:
    try:
        manifest = json.loads(Path(a.manifest).read_bytes())
    except (OSError, ValueError) as e:
        raise SystemExit(f"manifest onleesbaar: {e}")
    if manifest.get("type") != MANIFEST_TYPE:
        raise SystemExit("dit is geen Vidimus-manifest")
    doel = Path(a.pad).resolve()
    if doel.is_dir():
        d = vergelijk(manifest, lees_map(doel), hele_map=True)
    elif doel.is_file():
        d = vergelijk(manifest, [{"pad": doel.name, "sha256": sha256_bestand(doel)}], hele_map=False)
    else:
        raise SystemExit(f"niet gevonden: {doel}")
    sys.stdout.write(f"manifest sha-256: {hashlib.sha256(Path(a.manifest).read_bytes()).hexdigest()}\n")
    for soort in ("gewijzigd", "ontbreekt", "nieuw", "verplaatst"):
        for p in d[soort]:
            sys.stdout.write(f"  {soort.upper():<10} {p}\n")
    afwijkend = len(d["gewijzigd"]) + len(d["ontbreekt"]) + len(d["nieuw"])
    sys.stdout.write(f"{len(d['identiek'])} identiek · {len(d['verplaatst'])} verplaatst · {len(d['gewijzigd'])} gewijzigd · "
                     f"{len(d['nieuw'])} niet in het manifest · {len(d['ontbreekt'])} ontbreekt\n{'AFWIJKING' if afwijkend else 'IDENTIEK'}\n")
    return 1 if afwijkend else 0


def main(argv: List[str] | None = None) -> int:
    p = argparse.ArgumentParser(prog="vidimus-map", description="Een hele map in één keer vastleggen: alleen vingerafdrukken verlaten deze computer.")
    sub = p.add_subparsers(dest="cmd", required=True)
    v = sub.add_parser("vastleggen", help="map hashen en als één registratie vastleggen")
    v.add_argument("map"); v.add_argument("--stroom"); v.add_argument("--ondertekenaar"); v.add_argument("--ref")
    v.add_argument("--url", default=os.environ.get("VIDIMUS_URL", "https://app.vidimus.be")); v.add_argument("--manifest", help="waar het manifest bewaard wordt")
    v.add_argument("--droog", action="store_true", help="alleen het manifest maken, niets versturen"); v.set_defaults(func=cmd_vastleggen)
    c = sub.add_parser("controleer", help="een map of één bestand naast een manifest leggen (offline)")
    c.add_argument("manifest"); c.add_argument("pad"); c.set_defaults(func=cmd_controleer)
    a = p.parse_args(argv)
    return a.func(a)


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