#!/usr/bin/env python3
from __future__ import annotations

import os
import platform
import queue
import shutil
import subprocess
import threading
import webbrowser
from pathlib import Path
import tkinter as tk
from tkinter import filedialog, messagebox, ttk


ROOT = Path(__file__).resolve().parent
GENERATED_DIR = ROOT / "installer" / "generated"
COMPOSE_PATH = GENERATED_DIR / "docker-compose.guided.yml"
DOCKER_DOCS = "https://docs.docker.com/engine/install/"
OPEN_WEBUI_DOCS = "https://docs.openwebui.com/features/audio/"


MODEL_EXPLANATIONS = {
    "clone": (
        "Voice Clone",
        "Uses recorded reference voices from the Voice Library. This is the everyday server for exported voices and Open WebUI.",
        "faster-qwen3-tts",
        "8020",
        "/models/Qwen3-TTS",
    ),
    "design": (
        "Voice Design",
        "Generates a voice from a written description. Great for prototyping characters without a recording.",
        "faster-qwen3-tts-voicedesign",
        "8021",
        "/models/Qwen3-TTS-VoiceDesign",
    ),
    "custom": (
        "Custom Voice",
        "Uses Qwen3 CustomVoice speaker IDs and optional instructions. Useful when you already maintain named model speakers.",
        "faster-qwen3-tts-customvoice",
        "8022",
        "/models/Qwen3-TTS-CustomVoice",
    ),
}


class GuidedInstaller(tk.Tk):
    def __init__(self) -> None:
        super().__init__()
        self.title("TTS Voice Creator Guided Installer")
        self.geometry("1120x780")
        self.minsize(920, 680)
        self.log_queue: queue.Queue[str] = queue.Queue()
        self._build_vars()
        self._build_ui()
        self.after(120, self._drain_log)
        self.check_environment()

    def _build_vars(self) -> None:
        home = Path.home()
        self.voice_dir = tk.StringVar(value=str(home / "TTS_Voices"))
        self.model_dir = tk.StringVar(value=str(home / "TTS_Models"))
        self.creator_port = tk.StringVar(value="7860")
        self.network_name = tk.StringVar(value="tts_net")
        self.tts_image = tk.StringVar(value="faster-qwen3-tts-dgx-spark:v4")
        self.install_portainer = tk.BooleanVar(value=False)
        self.model_enabled = {key: tk.BooleanVar(value=(key == "clone")) for key in MODEL_EXPLANATIONS}
        self.model_ports = {key: tk.StringVar(value=meta[3]) for key, meta in MODEL_EXPLANATIONS.items()}
        self.model_paths = {key: tk.StringVar(value=str(Path(self.model_dir.get()) / Path(meta[4]).name)) for key, meta in MODEL_EXPLANATIONS.items()}
        self.extra_tts_name = tk.StringVar(value="")
        self.extra_tts_url = tk.StringVar(value="")

    def _build_ui(self) -> None:
        self.columnconfigure(0, weight=0)
        self.columnconfigure(1, weight=1)
        self.rowconfigure(0, weight=1)

        left = ttk.Frame(self, padding=14)
        left.grid(row=0, column=0, sticky="ns")
        right = ttk.Frame(self, padding=14)
        right.grid(row=0, column=1, sticky="nsew")
        right.columnconfigure(0, weight=1)
        right.rowconfigure(1, weight=1)

        ttk.Label(left, text="Install Checks", font=("", 15, "bold")).pack(anchor="w")
        self.status = tk.Text(left, width=38, height=15, wrap="word")
        self.status.pack(fill="x", pady=(8, 12))
        ttk.Button(left, text="Re-check this computer", command=self.check_environment).pack(fill="x", pady=3)
        ttk.Button(left, text="Open Docker install guide", command=lambda: webbrowser.open(DOCKER_DOCS)).pack(fill="x", pady=3)
        ttk.Button(left, text="Install Docker on Linux", command=self.install_docker_linux).pack(fill="x", pady=3)
        ttk.Button(left, text="Install Portainer", command=self.install_portainer_now).pack(fill="x", pady=3)
        ttk.Button(left, text="Generate compose file", command=self.generate_compose).pack(fill="x", pady=(14, 3))
        ttk.Button(left, text="Install / update selected stack", command=self.install_stack).pack(fill="x", pady=3)
        ttk.Button(left, text="Open Open WebUI audio guide", command=lambda: webbrowser.open(OPEN_WEBUI_DOCS)).pack(fill="x", pady=(14, 3))

        nb = ttk.Notebook(right)
        nb.grid(row=0, column=0, sticky="nsew")
        right.rowconfigure(0, weight=1)

        self._setup_tab(nb)
        self._models_tab(nb)
        self._openwebui_tab(nb)
        self._log_tab(nb)

    def _setup_tab(self, nb: ttk.Notebook) -> None:
        tab = ttk.Frame(nb, padding=12)
        tab.columnconfigure(1, weight=1)
        nb.add(tab, text="1. Basics")
        rows = [
            ("Voice library folder", self.voice_dir, True),
            ("Model cache / LLM folder", self.model_dir, True),
            ("Creator app port", self.creator_port, False),
            ("Docker network", self.network_name, False),
            ("Qwen TTS Docker image", self.tts_image, False),
        ]
        for row, (label, var, browse) in enumerate(rows):
            ttk.Label(tab, text=label).grid(row=row, column=0, sticky="w", pady=6)
            ttk.Entry(tab, textvariable=var).grid(row=row, column=1, sticky="ew", pady=6, padx=8)
            if browse:
                ttk.Button(tab, text="Browse", command=lambda v=var: self.pick_dir(v)).grid(row=row, column=2, pady=6)
        ttk.Checkbutton(tab, text="Also install Portainer web UI", variable=self.install_portainer).grid(row=len(rows), column=1, sticky="w", pady=8)
        text = (
            "What these pieces do:\n\n"
            "Docker runs the app and TTS servers in containers.\n"
            "Portainer is optional. It gives non-command-line users a web dashboard for containers.\n"
            "The voice folder stores cloned voices and metadata.\n"
            "The model folder stores large Qwen model files, ideally on a fast disk with enough space."
        )
        ttk.Label(tab, text=text, wraplength=680, justify="left").grid(row=len(rows)+1, column=0, columnspan=3, sticky="ew", pady=16)

    def _models_tab(self, nb: ttk.Notebook) -> None:
        tab = ttk.Frame(nb, padding=12)
        tab.columnconfigure(2, weight=1)
        nb.add(tab, text="2. Qwen Models")
        ttk.Label(tab, text="Install", font=("", 10, "bold")).grid(row=0, column=0, sticky="w")
        ttk.Label(tab, text="Model", font=("", 10, "bold")).grid(row=0, column=1, sticky="w")
        ttk.Label(tab, text="Model location", font=("", 10, "bold")).grid(row=0, column=2, sticky="w")
        ttk.Label(tab, text="Port", font=("", 10, "bold")).grid(row=0, column=3, sticky="w")
        for i, (key, meta) in enumerate(MODEL_EXPLANATIONS.items(), start=1):
            title, desc, _container, _port, _path = meta
            ttk.Checkbutton(tab, variable=self.model_enabled[key]).grid(row=i, column=0, sticky="nw", pady=8)
            ttk.Label(tab, text=f"{title}\n{desc}", wraplength=260, justify="left").grid(row=i, column=1, sticky="w", pady=8, padx=8)
            ttk.Entry(tab, textvariable=self.model_paths[key]).grid(row=i, column=2, sticky="ew", pady=8, padx=8)
            ttk.Entry(tab, textvariable=self.model_ports[key], width=8).grid(row=i, column=3, sticky="w", pady=8)

        ttk.Label(tab, text="Optional external TTS endpoint", font=("", 11, "bold")).grid(row=5, column=0, columnspan=4, sticky="w", pady=(22, 6))
        ttk.Label(tab, text="Name").grid(row=6, column=0, sticky="w")
        ttk.Entry(tab, textvariable=self.extra_tts_name).grid(row=6, column=1, sticky="ew", padx=8)
        ttk.Label(tab, text="Base URL").grid(row=6, column=2, sticky="e")
        ttk.Entry(tab, textvariable=self.extra_tts_url).grid(row=6, column=3, sticky="ew")
        ttk.Label(tab, text="Use this if you already have another OpenAI-compatible TTS server. The creator can route to it later by setting the TTS API URL in Settings.", wraplength=760).grid(row=7, column=0, columnspan=4, sticky="w", pady=10)

    def _openwebui_tab(self, nb: ttk.Notebook) -> None:
        tab = ttk.Frame(nb, padding=12)
        tab.columnconfigure(0, weight=1)
        nb.add(tab, text="3. Open WebUI")
        self.openwebui_text = tk.Text(tab, height=24, wrap="word")
        self.openwebui_text.grid(row=0, column=0, sticky="nsew")
        ttk.Button(tab, text="Refresh instructions", command=self.refresh_openwebui_text).grid(row=1, column=0, sticky="e", pady=8)
        self.refresh_openwebui_text()

    def _log_tab(self, nb: ttk.Notebook) -> None:
        tab = ttk.Frame(nb, padding=12)
        tab.rowconfigure(0, weight=1)
        tab.columnconfigure(0, weight=1)
        nb.add(tab, text="Log")
        self.log = tk.Text(tab, wrap="word")
        self.log.grid(row=0, column=0, sticky="nsew")

    def pick_dir(self, var: tk.StringVar) -> None:
        chosen = filedialog.askdirectory(initialdir=var.get() or str(Path.home()))
        if chosen:
            var.set(chosen)

    def log_line(self, msg: str) -> None:
        self.log_queue.put(msg.rstrip() + "\n")

    def _drain_log(self) -> None:
        while True:
            try:
                msg = self.log_queue.get_nowait()
            except queue.Empty:
                break
            self.log.insert("end", msg)
            self.log.see("end")
        self.after(120, self._drain_log)

    def run_threaded(self, title: str, commands: list[list[str]]) -> None:
        def worker():
            self.log_line(f"\n== {title} ==")
            for cmd in commands:
                self.log_line("$ " + " ".join(cmd))
                try:
                    proc = subprocess.run(cmd, cwd=ROOT, text=True, capture_output=True)
                    if proc.stdout:
                        self.log_line(proc.stdout)
                    if proc.stderr:
                        self.log_line(proc.stderr)
                    if proc.returncode:
                        self.log_line(f"Command exited with {proc.returncode}")
                        break
                except Exception as exc:
                    self.log_line(f"Failed: {exc}")
                    break
        threading.Thread(target=worker, daemon=True).start()

    def command_ok(self, cmd: list[str]) -> bool:
        try:
            return subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=6).returncode == 0
        except Exception:
            return False

    def docker_names(self) -> set[str]:
        try:
            out = subprocess.check_output(["docker", "ps", "-a", "--format", "{{.Names}}"], text=True, timeout=8)
            return {line.strip() for line in out.splitlines() if line.strip()}
        except Exception:
            return set()

    def check_environment(self) -> None:
        docker_bin = shutil.which("docker")
        compose_ok = self.command_ok(["docker", "compose", "version"]) if docker_bin else False
        daemon_ok = self.command_ok(["docker", "info"]) if docker_bin else False
        names = self.docker_names() if daemon_ok else set()
        lines = [
            f"System: {platform.system()} {platform.release()}",
            f"Docker command: {'found' if docker_bin else 'missing'}",
            f"Docker daemon: {'running' if daemon_ok else 'not reachable'}",
            f"Docker Compose plugin: {'found' if compose_ok else 'missing'}",
            f"Portainer: {'installed' if any('portainer' in n for n in names) else 'not found'}",
            f"Creator app: {'installed' if 'tts-voice-creator-clone-and-design' in names or 'tts-voice-creator' in names or 'voice-clone-factory' in names else 'not found'}",
            f"Qwen clone TTS: {'installed' if 'faster-qwen3-tts' in names else 'not found'}",
            f"Qwen Voice Design: {'installed' if 'faster-qwen3-tts-voicedesign' in names else 'not found'}",
            f"Qwen Custom Voice: {'installed' if 'faster-qwen3-tts-customvoice' in names else 'not found'}",
        ]
        self.status.delete("1.0", "end")
        self.status.insert("end", "\n".join(lines))

    def install_portainer_now(self) -> None:
        if not messagebox.askyesno("Install Portainer", "Install or update Portainer CE with Docker?"):
            return
        self.run_threaded("Install Portainer", [
            ["docker", "volume", "create", "portainer_data"],
            [
                "docker", "run", "-d", "--name", "portainer", "--restart=always",
                "-p", "9000:9000", "-p", "9443:9443",
                "-v", "/var/run/docker.sock:/var/run/docker.sock",
                "-v", "portainer_data:/data",
                "portainer/portainer-ce:latest",
            ],
        ])

    def install_docker_linux(self) -> None:
        if platform.system().lower() != "linux":
            messagebox.showinfo("Docker installer", "This one-click installer is only for Linux. Use the Docker install guide for this operating system.")
            return
        if not messagebox.askyesno(
            "Install Docker",
            "Run Docker's official Linux convenience installer?\n\nThis may ask for sudo privileges and needs internet access.",
        ):
            return
        self.run_threaded("Install Docker", [
            ["curl", "-fsSL", "https://get.docker.com", "-o", "/tmp/get-docker.sh"],
            ["sh", "/tmp/get-docker.sh"],
            ["docker", "version"],
        ])

    def generate_compose(self) -> None:
        GENERATED_DIR.mkdir(parents=True, exist_ok=True)
        content = self.compose_text()
        COMPOSE_PATH.write_text(content)
        self.log_line(f"Generated {COMPOSE_PATH}")
        messagebox.showinfo("Compose generated", f"Generated:\n{COMPOSE_PATH}")

    def install_stack(self) -> None:
        self.generate_compose()
        if self.install_portainer.get():
            self.install_portainer_now()
        if not messagebox.askyesno("Install stack", "Run docker compose up -d for the generated stack?"):
            return
        self.run_threaded("Install selected stack", [
            ["docker", "network", "create", self.network_name.get()],
            ["docker", "compose", "-f", str(COMPOSE_PATH), "up", "-d", "--build"],
        ])

    def compose_text(self) -> str:
        network = self.network_name.get().strip() or "tts_net"
        voice_dir = self.voice_dir.get().strip() or "./voices"
        model_dir = self.model_dir.get().strip() or "./models"
        image = self.tts_image.get().strip() or "faster-qwen3-tts-dgx-spark:v4"
        creator_port = self.creator_port.get().strip() or "7860"
        services = [
            "services:",
            "  tts-voice-creator-clone-and-design:",
            "    build:",
            "      context: ../..",
            "      dockerfile: Dockerfile",
            "    image: tts-voice-creator-clone-and-design:latest",
            "    container_name: tts-voice-creator-clone-and-design",
            "    restart: unless-stopped",
            "    ports:",
            f"      - \"{creator_port}:7860\"",
            "    volumes:",
            f"      - {voice_dir}:/voices:rw",
            "      - tts-voice-creator-clone-and-design:/home/app/.config/tts-voice-creator",
            "    environment:",
            "      - PYTHONUNBUFFERED=1",
            "      - VOICES_DIR=/voices",
            "      - OUTPUT_DIR=/voices/active_voices",
            "      - TTS_CONTAINER_NAME=faster-qwen3-tts",
            "    extra_hosts:",
            "      - \"host.docker.internal:host-gateway\"",
            "    networks:",
            f"      - {network}",
            "",
        ]
        for key, meta in MODEL_EXPLANATIONS.items():
            if not self.model_enabled[key].get():
                continue
            _title, _desc, container, _default_port, _default_model = meta
            host_port = self.model_ports[key].get().strip() or _default_port
            model_path = self.model_paths[key].get().strip() or str(Path(model_dir) / Path(_default_model).name)
            services.extend(self.tts_service_yaml(key, container, image, host_port, model_path, network))

        services.extend([
            "volumes:",
            "  tts-voice-creator-clone-and-design:",
            "",
            "networks:",
            f"  {network}:",
            "    external: true",
            "",
        ])
        return "\n".join(services)

    def tts_service_yaml(self, key: str, container: str, image: str, host_port: str, model_path: str, network: str) -> list[str]:
        lines = [
            f"  {container}:",
            f"    image: {image}",
            f"    container_name: {container}",
            "    restart: unless-stopped",
            "    ports:",
            f"      - \"{host_port}:8000\"",
            "    volumes:",
            f"      - {model_path}:/models/{Path(model_path).name}:ro",
            f"      - {ROOT / 'support' / 'faster-qwen3-tts-config'}:/config:ro",
            "    environment:",
            "      - PYTHONUNBUFFERED=1",
            "    networks:",
            f"      - {network}",
        ]
        if key == "design":
            lines.extend([
                "    command: python /config/run_voicedesign_server.py --host 0.0.0.0 --port 8000 --model /models/" + Path(model_path).name,
            ])
        elif key == "custom":
            lines.extend([
                "    command: python /config/run_customvoice_server.py --host 0.0.0.0 --port 8000 --model /models/" + Path(model_path).name,
            ])
        lines.append("")
        return lines

    def refresh_openwebui_text(self) -> None:
        creator = f"http://YOUR_HOST:{self.creator_port.get() or '7860'}/v1"
        text = f"""Open WebUI setup

Without routing:
1. Open Open WebUI Admin Settings -> Audio.
2. Choose OpenAI-compatible TTS.
3. Base URL: http://YOUR_HOST:{self.model_ports['clone'].get() or '8020'}/v1
4. API key: dummy
5. Model: qwen3-tts or tts-1
6. Voice: one real active voice, such as EN_F_Anna.

With routing:
1. Open Open WebUI Admin Settings -> Audio.
2. Choose OpenAI-compatible TTS.
3. Base URL: {creator}
4. API key: dummy
5. Model: tts-1
6. Voice: default
7. In TTS Voice Creator -> Routing, map:
   Open WebUI + default + EN -> EN_F_YourEnglishVoice
   Open WebUI + default + DE -> DE_M_YourGermanVoice

What to tell users:
- Voice Clone is the normal production path for recorded voices.
- Voice Design is for creating voices from descriptions.
- Custom Voice is for named Qwen speakers/custom voice model workflows.
- Portainer is optional. Use it when you want a browser dashboard for containers.
"""
        self.openwebui_text.delete("1.0", "end")
        self.openwebui_text.insert("end", text)


if __name__ == "__main__":
    GuidedInstaller().mainloop()
