"""Entrypoint de la commande `stt` (client)."""

from __future__ import annotations

import argparse
import asyncio
import os
import sys


def _cmd_setup() -> int:
    from stt.config import MODEL_PRESETS, write_default_config

    path = write_default_config()
    print(f"✓ Config : {path}")
    print("\nUtilisation :")
    print("  stt --text                 # chat texte simple (sans micro ni HUD)")
    print("  stt                        # voix + HUD")
    print(f"  stt --model claude         # modèles : {', '.join(MODEL_PRESETS)}")
    print("\nÀ configurer si besoin :")
    print(f"  • URL du STT-server dans {path} ([server] url)")
    print("  • Pour la voix : Piper + voix → ~/.local/share/piper/<voix>.onnx")
    return 0


SERVICE_NAME = "stt.service"


def _service_unit(stt_bin: str) -> str:
    """Contenu du unit systemd --user : lance `stt` (HUD + voix) avec la session graphique."""
    return f"""[Unit]
Description=STT — Assistant vocal Jarvis (HUD + voix)
After=graphical-session.target
PartOf=graphical-session.target

[Service]
Type=simple
ExecStart={stt_bin}
Restart=on-failure
RestartSec=5
Environment=PYTHONUNBUFFERED=1
StandardOutput=journal
StandardError=journal
SyslogIdentifier=stt

[Install]
WantedBy=graphical-session.target
"""


def _cmd_install_service() -> int:
    """Installe et démarre le service systemd --user (auto-start au login + kiosk)."""
    import shutil
    import subprocess
    from pathlib import Path

    stt_bin = shutil.which("stt") or os.path.realpath(sys.argv[0])
    unit_dir = Path.home() / ".config" / "systemd" / "user"
    unit_dir.mkdir(parents=True, exist_ok=True)
    unit_path = unit_dir / SERVICE_NAME
    unit_path.write_text(_service_unit(stt_bin))
    print(f"✓ Unit écrit : {unit_path}")
    print(f"  ExecStart = {stt_bin}")

    def sc(*args):
        subprocess.run(["systemctl", "--user", *args], check=False)

    # Rendre DISPLAY/WAYLAND dispo au gestionnaire systemd --user (pour le kiosk)
    subprocess.run(
        ["systemctl", "--user", "import-environment",
         "DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY"],
        check=False,
    )
    sc("daemon-reload")
    sc("enable", SERVICE_NAME)
    sc("start", SERVICE_NAME)

    print("\n✓ Service activé et démarré (systemd --user).")
    print("\nCommandes utiles :")
    print("  systemctl --user status stt")
    print("  journalctl --user -u stt -f")
    print("  systemctl --user restart stt")
    print("  stt --uninstall-service")
    print("\nDémarrer dès le boot (kiosk dédié, sans login manuel) :")
    print('  sudo loginctl enable-linger "$USER"   # + auto-login du bureau')
    print("\nLe HUD s'ouvre en kiosk si [ui].kiosk = true (défaut).")
    print("Si l'écran ne s'ouvre pas : voir le caveat DISPLAY dans admin/ia/stt.md.")
    return 0


def _cmd_uninstall_service() -> int:
    """Arrête, désactive et supprime le service systemd --user."""
    import subprocess
    from pathlib import Path

    def sc(*args):
        subprocess.run(["systemctl", "--user", *args], check=False)

    sc("stop", SERVICE_NAME)
    sc("disable", SERVICE_NAME)
    unit_path = Path.home() / ".config" / "systemd" / "user" / SERVICE_NAME
    if unit_path.exists():
        unit_path.unlink()
        print(f"✓ Unit supprimé : {unit_path}")
    else:
        print("(unit déjà absent)")
    sc("daemon-reload")
    print("✓ Service STT désinstallé.")
    return 0


def _service_installed() -> bool:
    """Le unit systemd --user stt.service est-il installé ?"""
    import subprocess

    return subprocess.run(
        ["systemctl", "--user", "cat", SERVICE_NAME],
        capture_output=True, text=True,
    ).returncode == 0


def _cmd_start() -> int:
    """Démarre le service STT (systemd --user)."""
    import subprocess

    if not _service_installed():
        print("Service non installé — lance d'abord : stt --install-service")
        print("(ou simplement `stt` pour un lancement direct, sans service)")
        return 1
    subprocess.run(["systemctl", "--user", "start", SERVICE_NAME], check=False)
    state = subprocess.run(
        ["systemctl", "--user", "is-active", SERVICE_NAME],
        capture_output=True, text=True,
    ).stdout.strip()
    print(f"✓ Service STT démarré (état : {state}).")
    return 0


def _cmd_restart() -> int:
    """Redémarre le service STT — recharge la config (modèle ASR, wake word…)."""
    import subprocess

    if not _service_installed():
        print("Service non installé — lance d'abord : stt --install-service")
        return 1
    # systemd possède le job : même si ce process est tué (cas restart depuis le
    # HUD, qui tourne DANS le service), le redémarrage va à son terme.
    subprocess.run(["systemctl", "--user", "restart", SERVICE_NAME], check=False)
    print("✓ Service STT redémarré.")
    return 0


def _running_stt_pids() -> list[int]:
    """PID des instances `stt` (service voix/HUD) en cours, hors la commande courante.

    Lit /proc (Linux) : on cible les process dont argv[0] se nomme `stt` et qui ne
    portent pas un flag utilitaire (--stop, --update, …) → uniquement le service vocal.
    """
    me = os.getpid()
    utilitaires = {
        "--stop", "--start", "--restart", "--update", "--version", "--setup", "--text",
        "--install-service", "--uninstall-service",
        "--install-desktop", "--uninstall-desktop", "--install-kokoro",
        "--voices", "--install-voice",
    }
    pids: list[int] = []
    try:
        entries = os.listdir("/proc")
    except OSError:
        return pids
    for entry in entries:
        if not entry.isdigit():
            continue
        pid = int(entry)
        if pid == me:
            continue
        try:
            with open(f"/proc/{pid}/cmdline", "rb") as f:
                args = [a.decode("utf-8", "ignore") for a in f.read().split(b"\0") if a]
        except OSError:
            continue
        if not args:
            continue
        # Le chemin du bin `stt` peut être argv[0] (exec direct via shebang) ou un
        # token plus loin (lancement pipx : `python -E /…/bin/stt --window app` →
        # le `-E` décale le chemin). On cherche un token qui est un *chemin* vers
        # `stt` (slash + basename `stt`) — ça écarte le `stt` nu d'un
        # `journalctl -u stt` — et on ignore les sous-commandes utilitaires.
        is_stt = any("/" in a and os.path.basename(a) == "stt" for a in args)
        if is_stt and not (utilitaires & set(args)):
            pids.append(pid)
    return pids


def _cmd_stop() -> int:
    """Éteint le service STT : unit systemd --user en priorité, sinon le process vocal."""
    import signal
    import subprocess

    # 1) Service systemd --user (cas auto-start) — chemin propre
    active = subprocess.run(
        ["systemctl", "--user", "is-active", SERVICE_NAME],
        capture_output=True, text=True,
    ).stdout.strip()
    if active == "active":
        subprocess.run(["systemctl", "--user", "stop", SERVICE_NAME], check=False)
        print("✓ Service STT arrêté (systemctl --user stop stt).")
        return 0

    # 2) Lancé à la main (terminal / .desktop) → SIGTERM au(x) process vocal(aux)
    pids = _running_stt_pids()
    if not pids:
        print("Aucun service ni instance STT en cours.")
        return 0
    for pid in pids:
        try:
            os.kill(pid, signal.SIGTERM)
        except OSError:
            pass
    mot = "instance" if len(pids) == 1 else "instances"
    print(f"✓ {len(pids)} {mot} STT arrêtée(s) (PID {', '.join(map(str, pids))}).")
    return 0


def _desktop_entry(stt_bin: str, icon_path: str) -> str:
    """Contenu du lanceur .desktop : STT comme application de bureau (fenêtre app)."""
    return f"""[Desktop Entry]
Type=Application
Name=STT — Assistant vocal
GenericName=Assistant vocal
Comment=Assistant vocal Jarvis du homelab Funk
Exec={stt_bin} --window app
Icon={icon_path}
Terminal=false
Categories=Utility;AudioVideo;
Keywords=jarvis;assistant;voix;hermes;
StartupNotify=true
StartupWMClass=STT-Funk
"""


def _cmd_install_desktop() -> int:
    """Crée l'entrée de menu (.desktop) + icône → STT lançable comme une appli (type Discord)."""
    import shutil
    import subprocess
    from pathlib import Path

    stt_bin = shutil.which("stt") or os.path.realpath(sys.argv[0])
    icon_path = Path(__file__).resolve().parent / "hud" / "icon.svg"
    apps_dir = Path.home() / ".local" / "share" / "applications"
    apps_dir.mkdir(parents=True, exist_ok=True)
    entry = apps_dir / "stt.desktop"
    entry.write_text(_desktop_entry(stt_bin, str(icon_path)))
    entry.chmod(0o755)
    subprocess.run(["update-desktop-database", str(apps_dir)], check=False)

    print(f"✓ Lanceur : {entry}")
    print(f"  Icône    : {icon_path}")
    print(f"  Exec     : {stt_bin} --window app")
    print("\nSTT apparaît dans le menu des applications (épinglable au dock).")
    print("Il s'ouvre en fenêtre « app » (sans onglets), comme Discord.")
    print("Retirer : stt --uninstall-desktop")
    return 0


def _cmd_uninstall_desktop() -> int:
    """Supprime le lanceur de bureau."""
    import subprocess
    from pathlib import Path

    entry = Path.home() / ".local" / "share" / "applications" / "stt.desktop"
    if entry.exists():
        entry.unlink()
        print(f"✓ Lanceur supprimé : {entry}")
    else:
        print("(lanceur déjà absent)")
    subprocess.run(["update-desktop-database", str(entry.parent)], check=False)
    return 0


def _installed_version() -> str | None:
    from importlib.metadata import PackageNotFoundError, version

    try:
        return version("stt")
    except PackageNotFoundError:
        return None


def _cmd_version() -> int:
    print(_installed_version() or "version inconnue (pas installé via pip/pipx)")
    return 0


def _cmd_update() -> int:
    """Met à jour STT depuis sa source git, sans désinstall/réinstall manuel.

    Utilise `pipx upgrade` (non destructif : conserve l'install actuelle si l'upgrade échoue),
    contrairement à `pipx reinstall` qui désinstalle d'abord et peut laisser sans rien.
    """
    import json
    import shutil
    import subprocess

    pipx = shutil.which("pipx")
    if not pipx:
        print("pipx introuvable — mise à jour automatique impossible.")
        return 1

    # Source d'installation (pour la commande de secours en cas d'échec)
    spec = None
    try:
        listing = subprocess.run(
            [pipx, "list", "--json"], capture_output=True, text=True
        ).stdout or "{}"
        spec = json.loads(listing)["venvs"]["stt"]["metadata"]["main_package"]["package_or_url"]
    except Exception:  # noqa: BLE001
        pass

    before = _installed_version()
    if before:
        print(f"Version installée : {before}")
    print("Mise à jour depuis git (pipx upgrade)…\n")
    res = subprocess.run([pipx, "upgrade", "stt"], capture_output=True, text=True)
    out = ((res.stdout or "") + (res.stderr or "")).strip()
    if out:
        print(out)

    print()
    if res.returncode != 0 or shutil.which("stt") is None:
        print("❌ Échec de la mise à jour — ton install précédente est conservée.")
        print("   Réessaie (souvent réseau/SSH), ou en dernier recours :")
        print("     uv cache clean")
        if spec:
            print(f'     pipx install --force "{spec}"')
        return res.returncode or 1
    print("✓ Mise à jour terminée — vérifie avec : stt --version")
    return 0


def _make_client(args):
    from stt.api import ServerClient
    from stt.config import load_config, resolve_model

    cfg = load_config()
    if args.server:
        cfg["server"]["url"] = args.server
    client = ServerClient(cfg["server"])
    if args.model:
        client.model = resolve_model(args.model)
    # Jeton Ghostfolio (source unique côté client) → transmis au serveur par requête
    gf_token = (cfg.get("ghostfolio") or {}).get("access_token")
    if gf_token:
        client.secrets["ghostfolio_token"] = gf_token
    return cfg, client


def _cmd_text(args) -> int:
    """Chat texte simple : tape, le serveur répond. Pas de micro ni HUD."""
    _, client = _make_client(args)
    print(f"STT (texte) — serveur {client.url} — modèle '{client.model}'")
    if not client.healthy():
        print("  ⚠️  serveur injoignable — vérifie [server] url et le réseau")
    print("  commandes : /model <nom>, /models, /reset, /quit\n")

    from stt.config import resolve_model

    while True:
        try:
            line = input("vous> ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\n👋")
            return 0
        if not line:
            continue
        if line in ("/quit", "/exit", "/q"):
            return 0
        if line == "/reset":
            client.reset()
            print("  → mémoire de session effacée\n")
            continue
        if line == "/models":
            try:
                m = client.models()
                print(f"  défaut: {m['default']} — dispo: {', '.join(m['available'])}\n")
            except Exception as e:  # noqa: BLE001
                print(f"  ❌ {e}\n")
            continue
        if line.startswith("/model "):
            client.model = resolve_model(line.split(maxsplit=1)[1].strip())
            print(f"  → modèle : {client.model}\n")
            continue
        try:
            reply = client.ask(line)
        except Exception as e:  # noqa: BLE001
            print(f"  ❌ {e}\n")
            continue
        print(f"hermes [{client.model}]> {reply}\n")


def _cmd_voice(args) -> int:
    from stt.config import load_config  # noqa: F401  (cfg via _make_client)
    from stt.memory import LocalMemory
    from stt.ui import serve
    from stt.voice import VoiceEngine

    cfg, client = _make_client(args)
    # mode d'ouverture du HUD : --window > config window_mode > kiosk (compat) > none
    ui = cfg["ui"]
    ui["window_mode"] = (
        args.window or ui.get("window_mode") or ("kiosk" if ui.get("kiosk") else "none")
    )
    # --window PERSISTE le choix → `stt --window app` rend le HUD fenêtré durablement
    # (y compris pour le service systemd, relu au prochain `stt --restart`).
    if args.window:
        _set_config("ui", {"window_mode": args.window})
    print(f"STT-server : {client.url} — modèle '{client.model}'")
    if not client.healthy():
        print("  ⚠️  serveur injoignable — vérifie [server] url et le réseau")

    memory = None
    if cfg["memory"].get("enabled"):
        memory = LocalMemory(cfg["memory"]["local_db"])

    def make_engine(emit):
        return VoiceEngine(
            cfg["voice"], client.ask, emit, memory=memory, no_tts=args.no_tts,
            model_label=lambda: client.model,
        )

    try:
        asyncio.run(serve(cfg, make_engine, client))
    except KeyboardInterrupt:
        print("\n👋 Au revoir")
    finally:
        if memory:
            memory.close()
    return 0


def _cmd_install_kokoro() -> int:
    """Télécharge le modèle Kokoro + le pack de voix (TTS naturel, ONNX) en local."""
    import urllib.request
    from pathlib import Path

    base = "https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0"
    dest = Path.home() / ".local/share/kokoro"
    dest.mkdir(parents=True, exist_ok=True)

    def _download(url: str, target: Path) -> None:
        def hook(blocks: int, bsize: int, total: int) -> None:
            if total > 0:
                pct = min(100, blocks * bsize * 100 // total)
                print(f"\r  {pct}%", end="", flush=True)
        urllib.request.urlretrieve(url, target, hook)  # noqa: S310 — URL fixe (GitHub release)
        print()

    for name, size in (("kokoro-v1.0.onnx", "~310 Mo"), ("voices-v1.0.bin", "~26 Mo")):
        target = dest / name
        if target.exists() and target.stat().st_size > 0:
            print(f"✓ {name} déjà présent")
            continue
        print(f"⬇ {name} ({size})…")
        try:
            _download(f"{base}/{name}", target)
        except Exception as e:  # noqa: BLE001
            print(f"❌ Échec du téléchargement de {name} : {e}")
            target.unlink(missing_ok=True)
            return 1

    print(f"\n✓ Modèle Kokoro installé dans {dest}")
    print("Étapes restantes pour activer la voix Kokoro :")
    print("  1) pipx inject stt kokoro-onnx        # moteur Python (onnxruntime)")
    print("  2) sudo dnf install espeak-ng         # phonémisation FR (si absent)")
    print('  3) ~/.config/stt/stt.toml → [voice] : tts_engine = "kokoro"')
    print("  4) stt --restart")
    return 0


# Voix Piper FR (vérifiées sur HuggingFace rhasspy/piper-voices). Nom → description.
_PIPER_FR_VOICES = {
    "fr_FR-upmc-medium": "femme (UPMC) — défaut",
    "fr_FR-siwis-medium": "femme (SIWIS)",
    "fr_FR-tom-medium": "homme (Tom)",
    "fr_FR-gilles-low": "homme (Gilles)",
    "fr_FR-mls-medium": "multi-locuteurs (MLS)",
}


def _cmd_voices() -> int:
    print("Voix Piper FR — installe avec : stt --install-voice <nom>\n")
    for name, desc in _PIPER_FR_VOICES.items():
        print(f"  • {name:22}  {desc}")
    print("\nVoix Kokoro (plus naturelle, FR : ff_siwis) → stt --install-kokoro")
    print("Après installation d'une voix : stt --restart")
    return 0


def _set_config(section: str, updates: dict) -> None:
    """Met à jour les clés d'une section de ~/.config/stt/stt.toml (préserve le reste)."""
    import tomllib

    import tomli_w
    from stt.config import CONFIG_DIR, CONFIG_PATH

    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    cfg: dict = {}
    if CONFIG_PATH.exists():
        with CONFIG_PATH.open("rb") as f:
            cfg = tomllib.load(f)
    cfg.setdefault(section, {}).update(updates)
    with CONFIG_PATH.open("wb") as f:
        tomli_w.dump(cfg, f)


def _set_voice_config(updates: dict) -> None:
    _set_config("voice", updates)


def _cmd_install_voice(name: str) -> int:
    """Télécharge une voix Piper FR (.onnx + .onnx.json) et la sélectionne dans la config."""
    import urllib.error
    import urllib.request
    from pathlib import Path

    parts = name.split("-")
    if len(parts) != 3 or "_" not in parts[0]:
        print(f"Nom invalide : {name!r} (attendu ex. fr_FR-tom-medium — voir `stt --voices`).")
        return 1
    lc, speaker, quality = parts
    lang = lc.split("_")[0]
    base = ("https://huggingface.co/rhasspy/piper-voices/resolve/main/"
            f"{lang}/{lc}/{speaker}/{quality}/{name}")
    dest = Path.home() / ".local/share/piper"
    dest.mkdir(parents=True, exist_ok=True)

    def hook(blocks: int, bsize: int, total: int) -> None:
        if total > 0:
            print(f"\r  {min(100, blocks * bsize * 100 // total)}%", end="", flush=True)

    for ext in (".onnx", ".onnx.json"):
        target = dest / f"{name}{ext}"
        if target.exists() and target.stat().st_size > 0:
            print(f"✓ {name}{ext} déjà présent")
            continue
        print(f"⬇ {name}{ext}…")
        try:
            urllib.request.urlretrieve(f"{base}{ext}", target, hook)  # noqa: S310
            print()
        except urllib.error.HTTPError as e:
            print(f"\n❌ {name}{ext} : HTTP {e.code} (voix introuvable ? `stt --voices`)")
            target.unlink(missing_ok=True)
            return 1
        except Exception as e:  # noqa: BLE001
            print(f"\n❌ {name}{ext} : {e}")
            target.unlink(missing_ok=True)
            return 1

    _set_voice_config({"tts_engine": "piper", "piper_voice": name})
    print(f"\n✓ Voix « {name} » installée et sélectionnée (tts_engine=piper).")
    print("  Applique : stt --restart")
    return 0


def main(argv: list[str] | None = None) -> int:
    p = argparse.ArgumentParser(prog="stt", description="Client vocal Jarvis de Funk")
    p.add_argument("--setup", action="store_true", help="Écrit la config par défaut")
    p.add_argument("--text", action="store_true", help="Mode chat texte (sans micro ni HUD)")
    p.add_argument("--model", help="Modèle : hermes, claude, qwen, opus (ou alias LiteLLM)")
    p.add_argument("--server", help="Force l'URL du STT-server (sinon config)")
    p.add_argument("--no-tts", action="store_true", help="Réponses texte seulement (voix)")
    p.add_argument("--window", choices=["app", "kiosk", "none"],
                   help="Mode d'ouverture du HUD (PERSISTÉ) : app (fenêtre déplaçable/redimensionnable, "
                        "type Discord) | kiosk (plein écran) | none")
    p.add_argument("--start", action="store_true",
                   help="Démarre le service STT (systemd --user)")
    p.add_argument("--restart", action="store_true",
                   help="Redémarre le service STT (recharge la config : ASR, wake word…)")
    p.add_argument("--stop", action="store_true",
                   help="Éteint le service STT en cours (systemd --user ou process vocal)")
    p.add_argument("--install-service", action="store_true",
                   help="Installe le service systemd --user (auto-start au login + kiosk)")
    p.add_argument("--uninstall-service", action="store_true",
                   help="Désinstalle le service systemd --user")
    p.add_argument("--install-desktop", action="store_true",
                   help="Crée le lanceur de bureau (.desktop + icône) — appli type Discord")
    p.add_argument("--uninstall-desktop", action="store_true",
                   help="Supprime le lanceur de bureau")
    p.add_argument("--update", action="store_true",
                   help="Met à jour STT depuis git (pipx reinstall)")
    p.add_argument("--install-kokoro", action="store_true",
                   help="Télécharge le modèle Kokoro (TTS naturel) dans ~/.local/share/kokoro")
    p.add_argument("--voices", action="store_true",
                   help="Liste les voix Piper FR installables (homme/femme)")
    p.add_argument("--install-voice", metavar="NOM",
                   help="Télécharge une voix Piper FR et la sélectionne (ex. fr_FR-tom-medium)")
    p.add_argument("--version", action="store_true", help="Affiche la version installée")
    args = p.parse_args(argv)

    if args.version:
        return _cmd_version()
    if args.setup:
        return _cmd_setup()
    if args.update:
        return _cmd_update()
    if args.install_kokoro:
        return _cmd_install_kokoro()
    if args.voices:
        return _cmd_voices()
    if args.install_voice:
        return _cmd_install_voice(args.install_voice)
    if args.start:
        return _cmd_start()
    if args.restart:
        return _cmd_restart()
    if args.stop:
        return _cmd_stop()
    if args.install_service:
        return _cmd_install_service()
    if args.uninstall_service:
        return _cmd_uninstall_service()
    if args.install_desktop:
        return _cmd_install_desktop()
    if args.uninstall_desktop:
        return _cmd_uninstall_desktop()
    if args.text:
        return _cmd_text(args)
    return _cmd_voice(args)


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