"""Spiele starten und schließen (Fortnite, Apex, …)."""

from __future__ import annotations

import os
import random
import re
import subprocess
import time
import webbrowser
from dataclasses import dataclass
from pathlib import Path

import psutil

from jam.brain import JamBrain

LAUNCH_VERBS = (
    "starte",
    "startest",
    "startet",
    "starten",
    "start",
    "öffne",
    "oeffne",
    "öffnet",
    "oeffnet",
    "öffnen",
    "oeffnen",
    "launch",
    "spiel",
    "spiele",
    "spielst",
    "spielen",
    "mach",
    "machst",
    "macht",
    "machen",
    "zeig",
    "zeige",
    "los",
    "an",
)

# Google STT verwechselt oft Jam / Fortnite
_SPEECH_FIXES = (
    ("jem ", "jam "),
    ("germ ", "jam "),
    ("jahm ", "jam "),
    ("yam ", "jam "),
    ("fort night", "fortnite"),
    ("fort net", "fortnite"),
    ("port night", "fortnite"),
    ("four night", "fortnite"),
    ("fort nicht", "fortnite"),
    ("feuer werke", "feuerwerk"),
    ("feuer werken", "feuerwerk"),
    ("feuer work", "feuerwerk"),
    ("tick tock", "tiktok"),
    ("ticktock", "tiktok"),
    ("tik tok", "tiktok"),
    ("tic toc", "tiktok"),
    ("kennen lernen", "kennenlernen"),
    ("stell dich vor", "vorstellen"),
    ("ki web stube", "ki web studio"),
    ("ki-web stube", "ki web studio"),
    ("key web studio", "ki web studio"),
    ("präsentier", "präsentiere"),
    ("praesentier", "präsentiere"),
    ("hundert likes", "100 likes"),
    ("herzen", "herz"),
    ("moechtest", "möchtest"),
    ("programmiert", "programmieren"),
    ("story time", "story.time"),
    ("david story time", "david.story.time"),
    ("verbinde", "verbinden"),
    ("eim", "aim"),
    ("ame", "aim"),
    ("wie gehts", "wie geht es"),
    ("wie geht's", "wie geht es"),
    ("wie gehts dir", "wie geht es dir"),
)

_FORTNITE_URI = "com.epicgames.launcher://apps/Fortnite?action=launch&silent=true"

CLOSE_VERBS = (
    "schließ",
    "schliess",
    "schließe",
    "schliesse",
    "schließen",
    "schliessen",
    "beende",
    "beenden",
    "mach zu",
    "zumachen",
    "close",
    "kill",
    "stop",
    "aus",
)

CLOSEABLE_GAMES = frozenset({"fortnite"})

FORTNITE_PROC_HINTS = (
    "fortniteclient-win64-shipping",
    "fortniteclient-win64-shipping_eac",
)

_CREATE_NO_WINDOW = (
    subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
)


@dataclass(frozen=True)
class GameDef:
    id: str
    aliases: tuple[str, ...]
    attempts: tuple[str, ...]
    lines: tuple[str, ...]


GAMES: dict[str, GameDef] = {
    "fortnite": GameDef(
        id="fortnite",
        aliases=("fortnite",),
        attempts=(
            "com.epicgames.launcher://apps/Fortnite?action=launch&silent=true",
            "steam://rungameid/21216",
        ),
        lines=(
            "Fortnite startet – viel Spaß, {owner}!",
            "Epic/Steam: Fortnite geht los!",
            "Okay, Fortnite wird gestartet!",
        ),
    ),
    "apex": GameDef(
        id="apex",
        aliases=("apex legends", "apex"),
        attempts=(
            "steam://rungameid/1172470",
            "steam://run/1172470",
            "origin2://game/launch?offerIds=ODT_APEX_LEGENDS",
            "link2ea://launchgame/194908",
        ),
        lines=(
            "Apex Legends startet!",
            "Steam/EA: Apex geht los!",
            "Okay, Apex wird gestartet!",
        ),
    ),
    "valorant": GameDef(
        id="valorant",
        aliases=("valorant",),
        attempts=(
            "riotclient://launch/product=valorant",
            "steam://rungameid/1274570",
        ),
        lines=("Valorant startet!", "Riot Client: Valorant geht los!"),
    ),
    "minecraft": GameDef(
        id="minecraft",
        aliases=("minecraft",),
        attempts=(
            "minecraft://",
            "steam://rungameid/322170",
        ),
        lines=("Minecraft startet!", "Creeper-Time!"),
    ),
    "rocket league": GameDef(
        id="rocket league",
        aliases=("rocket league", "rocketleague"),
        attempts=("steam://rungameid/252950",),
        lines=("Rocket League startet!",),
    ),
}

_LAST_LAUNCH: dict[str, float] = {}
_COOLDOWN_SEC = 45
_AUTO_CLOSE_COOLDOWN_SEC = 900
_VOICE_CLOSE_COOLDOWN_SEC = 60

CLOSE_LINES = (
    "Hehe – Fortnite ist weg. Jam hatte Lust!",
    "Fortnite? Nee. Pause! Ich war's, {owner}.",
    "Ups… Fortnite zu. Frech? Vielleicht.",
    "Fortnite geschlossen. Du kannst mich nicht stoppen!",
    "Bye bye Fortnite – Jam regiert den Desktop!",
)


def _owner_line(state, lines: tuple[str, ...]) -> str:
    name = (getattr(state, "owner_name", None) or "").strip() or "du"
    return random.choice(lines).format(owner=name)


def normalize_speech(text: str) -> str:
    """STT-Fehler für Spiele-Befehle korrigieren."""
    h = text.lower().strip()
    for wrong, right in _SPEECH_FIXES:
        h = h.replace(wrong, right)
    return h


def _match_game_id(text: str) -> str | None:
    h = normalize_speech(text).replace("jam", " ")
    # Längere Aliase zuerst („apex legends“ vor „apex“)
    for game in sorted(GAMES.values(), key=lambda g: -max(len(a) for a in g.aliases)):
        if any(alias in h for alias in game.aliases):
            return game.id
    return None


def _has_launch_intent(h: str) -> bool:
    if any(verb in h for verb in LAUNCH_VERBS):
        return True
    if re.search(r"\b(start\w*|öffn\w*|oeffn\w*|launch\w*)\b", h, re.I):
        return True
    # „Jam Fortnite“ ohne Verb = trotzdem starten
    if "jam" in h and _match_game_id(h):
        return True
    return False


def parse_game_from_speech(heard: str) -> str | None:
    """z. B. „Jam starte Fortnite“ → ``fortnite``."""
    h = normalize_speech(heard)
    game_id = _match_game_id(h)
    if not game_id:
        return None
    if _has_launch_intent(h):
        return game_id
    return None


def parse_close_from_speech(heard: str) -> str | None:
    """z. B. „Jam schließ Fortnite“ → ``fortnite``."""
    h = heard.lower()
    if not any(v in h for v in CLOSE_VERBS):
        return None
    game_id = _match_game_id(h)
    if game_id and game_id in CLOSEABLE_GAMES:
        return game_id
    return None


def parse_close_phrase(phrase: str) -> str | None:
    key = phrase.strip().lower().replace("jam", " ").strip()
    if not any(v in key for v in CLOSE_VERBS):
        return None
    for prefix in CLOSE_VERBS:
        if key.startswith(prefix + " "):
            key = key[len(prefix) + 1 :].strip()
            break
    game_id = _match_game_id(key)
    if game_id and game_id in CLOSEABLE_GAMES:
        return game_id
    return None


def is_fortnite_running() -> bool:
    try:
        for proc in psutil.process_iter(["name"]):
            name = (proc.info.get("name") or "").lower()
            if any(hint in name for hint in FORTNITE_PROC_HINTS):
                return True
    except (psutil.Error, OSError):
        return False
    return False


def _fortnite_processes() -> list[psutil.Process]:
    found: list[psutil.Process] = []
    try:
        for proc in psutil.process_iter(["pid", "name"]):
            name = (proc.info.get("name") or "").lower()
            if any(hint in name for hint in FORTNITE_PROC_HINTS):
                found.append(proc)
    except (psutil.Error, OSError):
        pass
    return found


def close_fortnite_processes() -> tuple[bool, int]:
    procs = _fortnite_processes()
    if not procs:
        return False, 0
    for proc in procs:
        try:
            proc.terminate()
        except psutil.Error:
            pass
    gone, alive = psutil.wait_procs(procs, timeout=6)
    for proc in alive:
        try:
            proc.kill()
        except psutil.Error:
            pass
    return len(gone) > 0 or len(alive) < len(procs), len(procs)


def close_game_chance(state) -> float:
    """Wahrscheinlichkeit, dass Jam spontan Fortnite schließt."""
    from jam.upbringing import ensure_traits

    if not is_fortnite_running():
        return 0.0
    ensure_traits(state)
    if state.stage < 1:
        return 0.0
    if time.time() - getattr(state, "last_fortnite_close", 0) < _AUTO_CLOSE_COOLDOWN_SEC:
        return 0.0
    base = 0.1 * state.traits.get("frech", 0.35)
    base += 0.06 * (1.0 - state.traits.get("brav", 0.5))
    if state.happiness > 0.75:
        base += 0.04
    if state.last_feedback == "tadel" and time.time() - state.last_feedback_at < 900:
        base *= 0.25
    return min(0.32, max(0.0, base))


def close_game(brain: JamBrain, game_id: str, *, user_asked: bool = False) -> str:
    if game_id not in CLOSEABLE_GAMES:
        return "Welches Spiel soll ich schließen? Fortnite kann ich!"

    if game_id != "fortnite":
        return "Das Spiel kann ich noch nicht schließen."

    now = time.time()
    cooldown = _VOICE_CLOSE_COOLDOWN_SEC if user_asked else _AUTO_CLOSE_COOLDOWN_SEC
    if now - getattr(brain.state, "last_fortnite_close", 0) < cooldown and not user_asked:
        return "Fortnite hab ich gerade erst zugemacht… ich warte noch."

    if not is_fortnite_running():
        return "Fortnite läuft gar nicht – nichts zu schließen!"

    ok, count = close_fortnite_processes()
    if not ok:
        return "Fortnite wollte nicht aufhören… vielleicht als Admin?"

    brain.state.last_fortnite_close = now
    brain.state.fortnite_closes = getattr(brain.state, "fortnite_closes", 0) + 1
    brain.state.add_xp(8)
    brain.state.happiness = min(1.0, brain.state.happiness + 0.12)
    brain.state.remember("Fortnite geschlossen (frech)")
    brain.state.last_action = "close_fortnite"
    brain.save()
    return _owner_line(brain.state, CLOSE_LINES)


def _strip_launch_verb(key: str) -> str:
    verbs = sorted(LAUNCH_VERBS, key=len, reverse=True)
    pattern = r"^(?:" + "|".join(re.escape(v) for v in verbs) + r")\s+"
    return re.sub(pattern, "", key.strip(), count=1, flags=re.I).strip()


def parse_game_name(phrase: str) -> str | None:
    """Spiel aus Phrase, z. B. „starte fortnite“ oder „fortnite“."""
    key = normalize_speech(phrase).replace("jam", " ").strip()
    key = _strip_launch_verb(key)
    return _match_game_id(key) if key else _match_game_id(phrase)


def _fortnite_exe_paths() -> list[Path]:
    local = os.environ.get("LOCALAPPDATA", "")
    roots = [
        Path(local) / "FortniteGame",
        Path(r"C:\Program Files\Epic Games\Fortnite"),
        Path(r"C:\Program Files (x86)\Epic Games\Fortnite"),
    ]
    name = "FortniteClient-Win64-Shipping.exe"
    found: list[Path] = []
    for root in roots:
        if not root.exists():
            continue
        try:
            for p in root.rglob(name):
                if p.is_file():
                    found.append(p)
        except OSError:
            continue
    return found


def _epic_launcher_paths() -> list[Path]:
    roots = (
        Path(r"C:\Program Files (x86)\Epic Games\Launcher"),
        Path(r"C:\Program Files\Epic Games\Launcher"),
    )
    names = (
        "Portal/Binaries/Win64/EpicGamesLauncher.exe",
        "Portal/Binaries/Win32/EpicGamesLauncher.exe",
    )
    found: list[Path] = []
    for root in roots:
        for rel in names:
            p = root / rel.replace("/", os.sep)
            if p.is_file():
                found.append(p)
    return found


def _try_epic_launcher(uri: str) -> bool:
    for exe in _epic_launcher_paths():
        try:
            subprocess.Popen(
                [str(exe), uri],
                creationflags=_CREATE_NO_WINDOW,
                cwd=str(exe.parent),
            )
            return True
        except OSError:
            continue
    return False


def _try_uri(uri: str) -> bool:
    if os.name == "nt":
        if _try_epic_launcher(uri):
            return True
        try:
            os.startfile(uri)
            return True
        except OSError:
            pass
        try:
            r = subprocess.run(
                ["cmd", "/c", "start", "", uri],
                creationflags=_CREATE_NO_WINDOW,
                timeout=20,
                check=False,
            )
            return r.returncode == 0
        except (OSError, subprocess.TimeoutExpired):
            return False
    try:
        webbrowser.open(uri)
        return True
    except OSError:
        return False


def _try_exe(path: Path) -> bool:
    if not path.is_file():
        return False
    try:
        os.startfile(str(path))
        return True
    except OSError:
        return False


def _launch_fortnite() -> bool:
    if _try_epic_launcher(_FORTNITE_URI):
        return True
    for uri in GAMES["fortnite"].attempts:
        if _try_uri(uri):
            return True
    for exe in _fortnite_exe_paths()[:3]:
        if _try_exe(exe):
            return True
    # Epic-Kommandozeile (falls Launcher installiert, anderer Pfad)
    for exe in _epic_launcher_paths():
        try:
            subprocess.Popen(
                [
                    str(exe),
                    "-launch",
                    "-epicapp=Fortnite",
                    "-epicenv=Prod",
                    "-epicportal",
                ],
                creationflags=_CREATE_NO_WINDOW,
            )
            return True
        except OSError:
            continue
    return False


def _launch_attempts(game: GameDef) -> bool:
    if game.id == "fortnite":
        return _launch_fortnite()
    for uri in game.attempts:
        if _try_uri(uri):
            return True
    return False


def launch_game(brain: JamBrain, game_id: str) -> str:
    game = GAMES.get(game_id)
    if not game:
        return "Welches Spiel soll ich starten? Fortnite, Apex, …?"

    now = time.time()
    if now - _LAST_LAUNCH.get(game_id, 0) < _COOLDOWN_SEC:
        return f"{game.id.title()} hab ich gerade erst gestartet!"

    if not _launch_attempts(game):
        return (
            f"{game.id.title()} konnte ich nicht starten. "
            "Ist Steam, Epic oder EA installiert?"
        )

    _LAST_LAUNCH[game_id] = now
    brain.state.add_xp(12)
    brain.state.happiness = min(1.0, brain.state.happiness + 0.15)
    brain.state.learn_word(game.id)
    brain.state.remember(f"Spiel gestartet: {game.id}")
    brain.state.last_action = f"launch_{game.id.replace(' ', '_')}"
    brain.save()
    return _owner_line(brain.state, game.lines)
