"""Lokales Wan2GP (AMD/NVIDIA) – Pfade, LAN, Prompt-Export."""

from __future__ import annotations

import json
import os
import socket
import subprocess
import sys
import time
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
CONFIG_PATH = ROOT / "data" / "wan2gp-config.json"

WAN2GP_PORT = 7860
STUDIO_PORT = 8794
PINOKIO_REF = "pinokio://127.0.0.1:42000/api/wan2gp-amd"
DEFAULT_PINOKIO_APP = Path("C:/pinokio/api/wan2gp-amd/app")


def _load_config() -> dict:
    if CONFIG_PATH.is_file():
        try:
            return json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError):
            pass
    return {
        "gpu_vendor": "amd",
        "gpu_model": "RX 6900 XT",
        "vram_gb": 16,
        "pinokio_script": "wan2gp-amd",
        "rocm_arch": "gfx103X-dgpu",
        "wan2gp_port": WAN2GP_PORT,
        "recommended_model": "Wan 2.2 T2V 14B",
        "fast_model": "LTX Video 13B Distilled",
        "aspect_ratio": "9:16",
        "ratio_runway": "720:1280",
        "duration_sec": 8,
        "resolution": "1080p",
        "launch_args": "--listen --server-name 0.0.0.0 --server-port 7860",
    }


def save_config(cfg: dict) -> None:
    CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
    CONFIG_PATH.write_text(json.dumps(cfg, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def wan2gp_dir() -> Path:
    try:
        from jam.jampulse_desktop import studio_root

        p = studio_root() / "Wan2GP"
    except ImportError:
        p = Path.home() / "Desktop" / "VidPuls" / "Wan2GP"
    p.mkdir(parents=True, exist_ok=True)
    return p


def videos_local_dir() -> Path:
    p = wan2gp_dir() / "Videos"
    p.mkdir(parents=True, exist_ok=True)
    return p


def local_ip() -> str:
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
            s.connect(("8.8.8.8", 80))
            return s.getsockname()[0]
    except OSError:
        return "127.0.0.1"


def port_open(port: int, host: str = "127.0.0.1") -> bool:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(0.5)
        return s.connect_ex((host, port)) == 0


def wan2gp_running() -> bool:
    return port_open(WAN2GP_PORT)


def pinokio_app_dir() -> Path:
    """Wan2GP-App unter Pinokio (Python + wgp.py)."""
    if DEFAULT_PINOKIO_APP.is_dir():
        return DEFAULT_PINOKIO_APP
    try:
        out = subprocess.check_output(
            _pterm_cmd("home"),
            stderr=subprocess.DEVNULL,
            text=True,
            encoding="utf-8",
            errors="replace",
            timeout=15,
        ).strip()
        if out:
            p = Path(out) / "api" / "wan2gp-amd" / "app"
            if p.is_dir():
                return p
    except (subprocess.SubprocessError, OSError, FileNotFoundError):
        pass
    return DEFAULT_PINOKIO_APP


def wan2gp_python() -> Path:
    app = pinokio_app_dir()
    py = app / "env" / "Scripts" / "python.exe"
    return py if py.is_file() else Path(sys.executable)


def read_prompt_file(path: Path | None = None) -> str:
    """Prompt aus jami-prompt.txt (Kommentarzeilen # werden entfernt)."""
    p = path or (wan2gp_dir() / "jami-prompt.txt")
    if not p.is_file():
        raise FileNotFoundError(f"Prompt-Datei fehlt: {p}")
    lines = []
    for line in p.read_text(encoding="utf-8").splitlines():
        s = line.strip()
        if not s or s.startswith("#"):
            continue
        lines.append(line.rstrip())
    text = "\n".join(lines).strip()
    if not text:
        raise ValueError(f"Kein Prompt in {p}")
    return text


def build_generation_settings(
    prompt: str,
    *,
    model_type: str = "t2v",
    resolution: str = "480x832",
    video_length: int = 49,
    num_inference_steps: int = 20,
) -> dict:
    """WanGP CLI/API-Settings (Wan2.1 T2V 14B, 9:16 – sparsam für 16 GB VRAM)."""
    return {
        "model_type": model_type,
        "prompt": prompt,
        "resolution": resolution,
        "video_length": int(video_length),
        "num_inference_steps": int(num_inference_steps),
        "video_prompt_type": "",
    }


def write_generation_job(settings: dict, name: str = "jami-generate.json") -> Path:
    out = wan2gp_dir() / name
    out.write_text(json.dumps(settings, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    return out


def _pterm_cmd(*args: str) -> list[str]:
    """pterm ist unter Windows oft ein npm-PowerShell-Skript."""
    ps1 = Path.home() / "AppData" / "Roaming" / "npm" / "pterm.ps1"
    if ps1.is_file():
        return [
            "powershell",
            "-NoProfile",
            "-ExecutionPolicy",
            "Bypass",
            "-File",
            str(ps1),
            *args,
        ]
    return ["pterm", *args]


def _run_cmd(cmd: list[str], *, timeout: float = 30) -> tuple[int, str]:
    try:
        r = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            encoding="utf-8",
            errors="replace",
            timeout=timeout,
            shell=False,
        )
        out = ((r.stdout or "") + (r.stderr or "")).strip()
        return r.returncode, out
    except subprocess.TimeoutExpired:
        return 124, "Timeout"
    except (subprocess.SubprocessError, OSError, FileNotFoundError) as e:
        return 127, str(e)


def stop_wan2gp_ui() -> tuple[bool, str]:
    if not wan2gp_running():
        return True, "Wan2GP UI laeuft nicht."
    _run_cmd(_pterm_cmd("stop", "--ref", PINOKIO_REF), timeout=20)
    if not wan2gp_running():
        return True, "Wan2GP gestoppt."
    ok, kill_msg = _kill_port_listeners(WAN2GP_PORT)
    if ok:
        return True, kill_msg
    return False, kill_msg


def _kill_port_listeners(port: int) -> tuple[bool, str]:
    code, out = _run_cmd(
        [
            "powershell",
            "-NoProfile",
            "-Command",
            (
                f"$c = Get-NetTCPConnection -LocalPort {port} -State Listen -ErrorAction SilentlyContinue; "
                "if (-not $c) { exit 0 }; "
                "$pids = $c.OwningProcess | Sort-Object -Unique; "
                "foreach ($procId in $pids) { Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue }; "
                "exit 0"
            ),
        ],
        timeout=30,
    )
    if code != 0:
        return False, out or f"Port {port} freigeben fehlgeschlagen"
    for _ in range(15):
        if not port_open(port):
            return True, f"Port {port} frei."
        time.sleep(1)
    return False, f"Port {port} noch belegt."


def repair_wan2gp_checkpoints() -> dict:
    """Hängengebliebene HuggingFace-Downloads in ckpts/ reparieren."""
    import shutil

    app = pinokio_app_dir()
    ckpts = app / "ckpts"
    if not ckpts.is_dir():
        return {"ok": False, "error": f"ckpts fehlt: {ckpts}", "fixed": [], "errors": []}

    fixed: list[str] = []
    errors: list[str] = []
    min_bytes = 50 * 1024 * 1024  # 50 MB – kleinere Dateien ignorieren

    temp_dirs = sorted({p for p in ckpts.rglob("_temp*") if p.is_dir()}, key=lambda p: len(p.parts), reverse=True)
    for temp_dir in temp_dirs:
        for sf in temp_dir.rglob("*.safetensors"):
            try:
                if sf.stat().st_size < min_bytes:
                    continue
                rel = sf.relative_to(temp_dir)
                if len(rel.parts) >= 2:
                    target_dir = ckpts / rel.parts[0]
                else:
                    target_dir = ckpts
                target = target_dir / sf.name
                target_dir.mkdir(parents=True, exist_ok=True)
                if target.is_file():
                    if target.stat().st_size >= sf.stat().st_size:
                        fixed.append(f"bereits ok: {target.name}")
                        continue
                    target.unlink()
                shutil.move(str(sf), str(target))
                fixed.append(f"verschoben: {target.relative_to(ckpts)}")
            except OSError as e:
                errors.append(f"{sf.name}: {e}")

    for temp_dir in sorted({p for p in ckpts.rglob("_temp*") if p.is_dir()}, key=lambda p: len(p.parts), reverse=True):
        try:
            shutil.rmtree(temp_dir)
            fixed.append(f"temp entfernt: {temp_dir.relative_to(ckpts)}")
        except OSError as e:
            errors.append(f"{temp_dir.name}: {e}")

    patch_ok, patch_msg = patch_wan2gp_download()
    if patch_ok:
        fixed.append(patch_msg)
    elif patch_msg:
        errors.append(patch_msg)

    umt5 = ckpts / "umt5-xxl" / "models_t5_umt5-xxl-enc-quanto_int8.safetensors"
    critical_errors = [e for e in errors if "verschoben" not in e and umt5.is_file() is False]
    return {
        "ok": umt5.is_file() and not critical_errors,
        "umt5_ready": umt5.is_file(),
        "umt5_path": str(umt5),
        "fixed": fixed,
        "errors": errors,
    }


def patch_wan2gp_download() -> tuple[bool, str]:
    """Wan2GP download.py: Abbruch vermeiden wenn Zieldatei schon existiert."""
    dl = pinokio_app_dir() / "shared" / "utils" / "download.py"
    if not dl.is_file():
        return False, "download.py nicht gefunden"
    text = dl.read_text(encoding="utf-8")
    needle = "            shutil.move(os.path.join(temp_full_path, onefile), tgt)"
    if "final_path = os.path.join(tgt, onefile)" in text:
        return True, "download.py bereits gepatcht"
    if needle not in text:
        return False, "download.py: unbekanntes Format"
    patched = text.replace(
        needle,
        "            final_path = os.path.join(tgt, onefile)\n"
        "            if os.path.isfile(final_path):\n"
        "                shutil.rmtree(temp_dir_path)\n"
        "                return\n"
        "            shutil.move(os.path.join(temp_full_path, onefile), tgt)",
    )
    dl.write_text(patched, encoding="utf-8")
    return True, "download.py gepatcht (existing-file skip)"


def run_cli_generation(
    settings_path: Path,
    *,
    output_dir: Path | None = None,
    log_path: Path | None = None,
    stop_ui: bool = True,
    verbose: int = 1,
) -> dict:
    """Headless: Modell laden, ggf. downloaden, Video rendern."""
    app = pinokio_app_dir()
    py = wan2gp_python()
    wgp = app / "wgp.py"
    if not wgp.is_file():
        return {"ok": False, "error": f"wgp.py nicht gefunden: {wgp}"}

    out = output_dir or videos_local_dir()
    out.mkdir(parents=True, exist_ok=True)
    log = log_path or (wan2gp_dir() / "generate.log")

    stop_msg = ""
    if stop_ui:
        ok, stop_msg = stop_wan2gp_ui()
        if not ok:
            return {"ok": False, "error": f"Wan2GP UI stoppen fehlgeschlagen: {stop_msg}"}

    repair = repair_wan2gp_checkpoints()
    if not repair.get("umt5_ready"):
        return {
            "ok": False,
            "error": "UMT5-Encoder fehlt nach Reparatur – Wan2GP in Pinokio erneut starten.",
            "repair": repair,
        }

    cmd = [
        str(py),
        str(wgp),
        "--process",
        str(settings_path.resolve()),
        "--output-dir",
        str(out.resolve()),
        "--verbose",
        str(verbose),
    ]
    log.parent.mkdir(parents=True, exist_ok=True)
    with log.open("a", encoding="utf-8") as lf:
        lf.write(f"\n--- CLI start {subprocess.list2cmdline(cmd)} ---\n")
        lf.flush()
        proc = subprocess.Popen(
            cmd,
            cwd=str(app),
            stdout=lf,
            stderr=subprocess.STDOUT,
            env={**os.environ, "PYTHONIOENCODING": "utf-8", "PYTHONUNBUFFERED": "1"},
        )
    return {
        "ok": True,
        "pid": proc.pid,
        "log": str(log),
        "output_dir": str(out),
        "settings": str(settings_path),
        "stop_msg": stop_msg,
        "cmd": cmd,
    }


KNOWN_VRAM_GB = {
    "6900 xt": 16,
    "6800 xt": 16,
    "7900 xtx": 24,
    "7900 xt": 20,
    "7800 xt": 16,
    "4070": 12,
    "4080": 16,
    "4090": 24,
    "3060": 12,
}


def _guess_vram_gb(name: str) -> float:
    low = name.lower()
    for key, gb in KNOWN_VRAM_GB.items():
        if key in low:
            return float(gb)
    return 0.0


def detect_gpu() -> dict:
    """GPU-Info per wmic (Windows)."""
    info = {"name": "", "vendor": "", "vram_mb": 0}
    try:
        out = subprocess.check_output(
            ["wmic", "path", "win32_VideoController", "get", "Name,AdapterRAM", "/format:csv"],
            stderr=subprocess.DEVNULL,
            text=True,
            encoding="utf-8",
            errors="replace",
            timeout=15,
        )
        for line in out.splitlines():
            if not line.strip() or line.startswith("Node"):
                continue
            parts = [p.strip() for p in line.split(",") if p.strip()]
            if len(parts) >= 3:
                try:
                    vram = int(parts[1])
                except ValueError:
                    vram = 0
                name = parts[2]
                if vram >= info["vram_mb"]:
                    info["vram_mb"] = vram
                    info["name"] = name
                    low = name.lower()
                    if "amd" in low or "radeon" in low:
                        info["vendor"] = "amd"
                    elif "nvidia" in low or "geforce" in low or "rtx" in low:
                        info["vendor"] = "nvidia"
    except (subprocess.SubprocessError, OSError, FileNotFoundError):
        pass
    guessed = _guess_vram_gb(info.get("name") or "")
    if guessed:
        info["vram_gb"] = guessed
    elif info["vram_mb"] > 0:
        raw_gb = info["vram_mb"] / (1024**3)
        info["vram_gb"] = round(raw_gb, 1) if raw_gb < 32 else guessed or 16
    else:
        info["vram_gb"] = 0
    return info


def pinokio_script_for_gpu(gpu: dict) -> str:
    vendor = (gpu.get("vendor") or "").lower()
    if vendor == "nvidia":
        return "wan2gp"
    return "wan2gp-amd"


def export_jami_prompt(
    dialogue: str = "",
    *,
    use_weekend_date: bool = False,
    file_path: Path | None = None,
) -> tuple[str, Path]:
    from jam.ai_video_prompt import build_video_prompt, load_dialogue_text

    text = load_dialogue_text(
        text=dialogue,
        file_path=file_path,
        use_weekend_date=use_weekend_date,
    )
    prompt = build_video_prompt(text)
    out = wan2gp_dir() / "jami-prompt.txt"
    cfg = _load_config()
    out.write_text(
        "# Jami → Wan2GP / LTX (lokal, 0 € Cloud)\n"
        f"# Modell-Empfehlung: {cfg.get('recommended_model', 'Wan 2.2')}\n"
        f"# Format: {cfg.get('aspect_ratio', '9:16')} · {cfg.get('duration_sec', 8)}s\n"
        "# Wan2GP → Text-to-Video → Prompt einfügen → Generate\n\n"
        + prompt
        + "\n",
        encoding="utf-8",
    )
    (wan2gp_dir() / "jami-dialog.txt").write_text(text + "\n", encoding="utf-8")
    return prompt, out


def _anleitung_text(cfg: dict, gpu: dict) -> str:
    script = cfg.get("pinokio_script") or pinokio_script_for_gpu(gpu)
    ip = local_ip()
    return f"""VidPuls – Lokales KI-Video (Wan2GP / LTX)
{'=' * 48}

DEINE GPU: {gpu.get('name') or 'unbekannt'} ({gpu.get('vram_gb', '?')} GB)
PINOKIO-SCRIPT: {script}
  → AMD (RX 6900 XT etc.): wan2gp-amd
  → NVIDIA: wan2gp

SCHRITT 1 – Pinokio installieren
  https://pinokio.computer
  Download → installieren → starten

SCHRITT 2 – Wan2GP installieren
  Pinokio → Discover → Suche: {script}
  Install klicken (20–60 Min., Modelle werden geladen)
  Beim ersten Start: Modell wählen:
    • {cfg.get('recommended_model', 'Wan 2.2 T2V 14B')} (beste Qualität)
    • {cfg.get('fast_model', 'LTX 13B Distilled')} (schneller)

SCHRITT 3 – Wan2GP LAN-fähig starten
  In Pinokio bei {script} → Settings / Launch Args:
    {cfg.get('launch_args', '--listen --server-name 0.0.0.0 --server-port 7860')}

SCHRITT 4 – Jami-Prompt einfügen
  Doppelklick: Wan Prompt exportieren.bat
  Oder: Wan2GP Studio starten.bat (Browser-UI)
  Datei: Wan2GP\\jami-prompt.txt → in Wan2GP kopieren

SCHRITT 5 – Video generieren
  Wan2GP → Text-to-Video
  Seitenverhältnis: {cfg.get('aspect_ratio', '9:16')}
  Dauer: {cfg.get('duration_sec', 8)} Sekunden
  Generate → MP4 speichern nach Wan2GP\\Videos\\

ANDERE IM WLAN (kein Cloud-Server)
  Dieser PC: http://127.0.0.1:{cfg.get('wan2gp_port', 7860)}
  Handy/Laptop: http://{ip}:{cfg.get('wan2gp_port', 7860)}
  Firewall: Wan LAN freigeben.bat (einmalig)

VIDPULS STUDIO (Jami-Text → Prompt)
  http://127.0.0.1:{STUDIO_PORT}

STIMmen (optional)
  Dialog Studio starten.bat → ElevenLabs Sarah/Laura
  Video + Audio in CapCut/DaVinci zusammenfügen

KOSTEN: 0 € API · nur Strom · alles auf deinem PC
"""


def enable_firewall_rule(port: int = WAN2GP_PORT) -> tuple[bool, str]:
    name = "VidPuls Wan2GP LAN"
    try:
        subprocess.run(
            [
                "netsh", "advfirewall", "firewall", "delete", "rule",
                f"name={name}",
            ],
            capture_output=True,
            timeout=30,
        )
        r = subprocess.run(
            [
                "netsh", "advfirewall", "firewall", "add", "rule",
                f"name={name}",
                "dir=in", "action=allow", "protocol=TCP",
                f"localport={port}",
            ],
            capture_output=True,
            text=True,
            timeout=30,
        )
        if r.returncode == 0:
            return True, f"Firewall-Regel für Port {port} erstellt."
        return False, (r.stderr or r.stdout or "netsh fehlgeschlagen")[:300]
    except (subprocess.SubprocessError, OSError) as e:
        return False, str(e)


def setup_wan2gp(*, firewall: bool = True, export_prompt: bool = True) -> dict:
    """Desktop-Ordner, Config, Anleitung, Prompt – alles vorbereiten."""
    gpu = detect_gpu()
    cfg = _load_config()
    cfg["gpu_detected"] = gpu.get("name") or ""
    cfg["gpu_vendor"] = gpu.get("vendor") or cfg.get("gpu_vendor", "amd")
    if gpu.get("vram_gb"):
        cfg["vram_gb"] = gpu["vram_gb"]
    cfg["pinokio_script"] = pinokio_script_for_gpu(gpu)
    save_config(cfg)

    wdir = wan2gp_dir()
    videos_local_dir()
    (wdir / "pinokio-launch-args.txt").write_text(
        cfg.get("launch_args", "--listen --server-name 0.0.0.0 --server-port 7860") + "\n",
        encoding="utf-8",
    )
    (wdir / "ANLEITUNG-WAN2GP.txt").write_text(_anleitung_text(cfg, gpu), encoding="utf-8")

    prompt_path = None
    if export_prompt:
        _, prompt_path = export_jami_prompt(use_weekend_date=True)

    fw_ok, fw_msg = (False, "")
    if firewall:
        fw_ok, fw_msg = enable_firewall_rule(int(cfg.get("wan2gp_port", WAN2GP_PORT)))

    return {
        "ok": True,
        "gpu": gpu,
        "config": cfg,
        "wan2gp_dir": str(wdir),
        "prompt": str(prompt_path) if prompt_path else "",
        "firewall_ok": fw_ok,
        "firewall_msg": fw_msg,
        "lan_url": f"http://{local_ip()}:{cfg.get('wan2gp_port', WAN2GP_PORT)}",
        "local_url": f"http://127.0.0.1:{cfg.get('wan2gp_port', WAN2GP_PORT)}",
        "studio_url": f"http://127.0.0.1:{STUDIO_PORT}",
        "pinokio_script": cfg["pinokio_script"],
    }
