#!/usr/bin/env python3
"""Genereert de platform-woordenlijsten uit de canonieke bron.

Bron:    dictionary/proat-dictionary.json  (enige plek om iets toe te voegen)
Doelen:  VoiceInk/Services/DictionaryPack.generated.swift   (macOS, seedpack)
         proat-ios/Proat/ProatDictionary.json               (iOS, gebundeld)
         <proat-windows>/DictionaryPack.generated.cs         (Windows, seedpack)

Gebruik:
    python3 dictionary/generate.py
    python3 dictionary/generate.py --check          # alleen verifieren, niets schrijven
    python3 dictionary/generate.py --windows-path ~/Documents/proat-windows

Exit-code 1 bij --check als een doel niet meer overeenkomt met de bron.
"""
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

REPO = Path(__file__).resolve().parent.parent
SOURCE = REPO / "dictionary" / "proat-dictionary.json"
SWIFT_OUT = REPO / "VoiceInk" / "Services" / "DictionaryPack.generated.swift"
IOS_OUT = REPO / "proat-ios" / "Proat" / "ProatDictionary.json"
WINDOWS_DEFAULT = Path.home() / "Documents" / "proat-windows"

BANNER_LINES = [
    "GEGENEREERD BESTAND, niet met de hand aanpassen.",
    "Bron: dictionary/proat-dictionary.json",
    "Regenereren: python3 dictionary/generate.py",
]


def load_source() -> dict:
    doc = json.loads(SOURCE.read_text())
    for key in ("packVersion", "vocabulary", "replacements", "regexTriggers", "biasBudgetChars"):
        if key not in doc:
            sys.exit(f"Bron mist verplichte sleutel '{key}': {SOURCE}")
    validate(doc)
    return doc


def validate(doc: dict) -> None:
    """Vangt de fouten die stil verkeerd gedrag geven in plaats van een crash."""
    problems: list[str] = []

    seen: dict[str, str] = {}
    for word in doc["vocabulary"]:
        if not word.strip():
            problems.append("leeg vocabulary-woord")
        if word.lower() in seen:
            problems.append(f"vocabulary-dubbel: {word!r} naast {seen[word.lower()]!r}")
        seen[word.lower()] = word

    targets: dict[str, str] = {}
    for rule in doc["replacements"]:
        frm, to = rule["from"].strip(), rule["to"].strip()
        if not frm or not to:
            problems.append(f"lege vervangregel: {rule!r}")
            continue
        if frm.startswith("/"):
            if not (frm.endswith("/") or frm.endswith("/i")):
                problems.append(f"regex-trigger zonder sluit-slash: {frm!r}")
            continue
        # Twee letterlijke regels naar hetzelfde doel vechten om de sorteer-orde.
        if to.lower() in targets:
            problems.append(f"twee letterlijke regels naar {to!r}; voeg ze samen")
        targets[to.lower()] = to
        variants = [v.strip() for v in frm.split(",")]
        if any(not v for v in variants):
            problems.append(f"lege variant in {frm!r}")

    # De biasing-prompt kapt stil af zodra het budget vol is.
    cost = sum(len(w) + 2 for w in doc["vocabulary"])
    if cost > doc["biasBudgetChars"]:
        problems.append(
            f"vocabulary kost {cost} tekens, budget is {doc['biasBudgetChars']}: "
            "termen aan de staart worden stil weggelaten"
        )

    # Een regex-trigger moet de hele from-waarde zijn. De engine detecteert dat
    # vóór de komma-split, dus een quantifier als {1,6} mag wel een komma bevatten.
    for trigger in doc["regexTriggers"].values():
        if not (trigger.startswith("/") and trigger.endswith("/")):
            problems.append(f"regexTrigger niet in /slashes/: {trigger!r}")
        if not any(r["from"].strip() == trigger for r in doc["replacements"]):
            problems.append(f"regexTrigger staat niet als vervangregel in de lijst: {trigger!r}")

    if problems:
        for p in problems:
            print(f"FOUT: {p}", file=sys.stderr)
        sys.exit(1)


# ------------------------------------------------------------------ generators

def swift_literal(text: str) -> str:
    """Raw string voor patronen met backslashes, anders een gewone string."""
    if "\\" in text:
        assert '"#' not in text
        return f'#"{text}"#'
    escaped = text.replace("\\", "\\\\").replace('"', '\\"')
    return f'"{escaped}"'


def render_swift(doc: dict) -> str:
    lines = [f"//  {line}" for line in BANNER_LINES]
    lines += [
        "//",
        f"//  Pack v{doc['packVersion']}, bijgewerkt {doc['updated']}: "
        f"{len(doc['vocabulary'])} woorden, {len(doc['replacements'])} vervangregels.",
        "",
        "import Foundation",
        "",
        "/// De gecureerde woordenlijst van Proat, gedeeld met iOS en Windows.",
        "enum DictionaryPack {",
        "",
        f"    static let packVersion = {doc['packVersion']}",
        "",
        "    /// Tekenbudget voor de Voxtral-biasingprompt (zie MistralProvider).",
        f"    static let biasBudgetChars = {doc['biasBudgetChars']}",
        "",
    ]

    for name, trigger in doc["regexTriggers"].items():
        lines.append(f"    static let {name}RegexTrigger = {swift_literal(trigger)}")
    lines.append("")

    lines.append("    static let vocabulary: [String] = [")
    for word in doc["vocabulary"]:
        lines.append(f"        {swift_literal(word)},")
    lines.append("    ]")
    lines.append("")

    lines.append("    static let replacements: [(original: String, replacement: String)] = [")
    for rule in doc["replacements"]:
        lines.append(f"        ({swift_literal(rule['from'])}, {swift_literal(rule['to'])}),")
    lines.append("    ]")
    lines.append("}")
    lines.append("")
    return "\n".join(lines)


def render_ios(doc: dict) -> str:
    payload = {
        "vocabulary": doc["vocabulary"],
        "replacements": [{"from": r["from"], "to": r["to"]} for r in doc["replacements"]],
        "biasBudgetChars": doc["biasBudgetChars"],
        "exported": doc["updated"],
        "source": f"dictionary/proat-dictionary.json (pack v{doc['packVersion']})",
    }
    return json.dumps(payload, ensure_ascii=False, indent=1) + "\n"


def cs_literal(text: str) -> str:
    escaped = text.replace("\\", "\\\\").replace('"', '\\"')
    return f'"{escaped}"'


def render_csharp(doc: dict) -> str:
    lines = [f"// {line}" for line in BANNER_LINES]
    lines += [
        "//",
        f"// Pack v{doc['packVersion']}, bijgewerkt {doc['updated']}: "
        f"{len(doc['vocabulary'])} woorden, {len(doc['replacements'])} vervangregels.",
        "",
        "namespace Proat;",
        "",
        "/// <summary>De gecureerde woordenlijst van Proat, gedeeld met macOS en iOS.</summary>",
        "internal static class DictionaryPack",
        "{",
        f"    public const int PackVersion = {doc['packVersion']};",
        "",
        "    /// <summary>Tekenbudget voor de Voxtral-biasingprompt.</summary>",
        f"    public const int BiasBudgetChars = {doc['biasBudgetChars']};",
        "",
        "    public static readonly string[] Vocabulary =",
        "    {",
    ]
    for word in doc["vocabulary"]:
        lines.append(f"        {cs_literal(word)},")
    lines += [
        "    };",
        "",
        "    public static readonly (string Original, string Replacement)[] Replacements =",
        "    {",
    ]
    for rule in doc["replacements"]:
        lines.append(f"        ({cs_literal(rule['from'])}, {cs_literal(rule['to'])}),")
    lines += ["    };", "}", ""]
    return "\n".join(lines)


# ----------------------------------------------------------------------- main

def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--check", action="store_true",
                        help="niets schrijven, alleen melden of de doelen achterlopen")
    parser.add_argument("--windows-path", type=Path, default=WINDOWS_DEFAULT,
                        help="pad naar de proat-windows repo (overslaan als die er niet is)")
    args = parser.parse_args()

    doc = load_source()

    targets: list[tuple[Path, str]] = [
        (SWIFT_OUT, render_swift(doc)),
        (IOS_OUT, render_ios(doc)),
    ]
    if args.windows_path.expanduser().is_dir():
        targets.append((args.windows_path.expanduser() / "DictionaryPack.generated.cs",
                        render_csharp(doc)))
    else:
        print(f"proat-windows niet gevonden op {args.windows_path}, C#-doel overgeslagen")

    stale = 0
    for path, content in targets:
        current = path.read_text() if path.exists() else None
        if current == content:
            print(f"gelijk    {path}")
            continue
        stale += 1
        if args.check:
            print(f"VEROUDERD {path}")
        else:
            path.parent.mkdir(parents=True, exist_ok=True)
            path.write_text(content)
            print(f"geschreven {path}")

    cost = sum(len(w) + 2 for w in doc["vocabulary"])
    print(f"\npack v{doc['packVersion']}: {len(doc['vocabulary'])} woorden "
          f"({cost}/{doc['biasBudgetChars']} tekens biasing), "
          f"{len(doc['replacements'])} vervangregels")

    if args.check and stale:
        print("\nDoelen lopen achter op de bron. Draai: python3 dictionary/generate.py",
              file=sys.stderr)
        return 1
    return 0


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