# Copyright 2025 Softwell S.r.l. - SPDX-License-Identifier: Apache-2.0
"""CLI for running TextualApp applications.

Usage:
    pygui run examples/basic/hello_world.py
    pygui run examples/basic/hello_world.py -c   # run and connect
    pygui run examples/basic/hello_world.py -r   # run with autoreload
    pygui list
    pygui connect hello_world
"""

from __future__ import annotations

import argparse
import importlib.util
import os
import shutil
import subprocess
import sys

from genro_textual.registry import (
    find_free_port,
    get_app_info,
    list_apps,
    register_app,
    unregister_app,
)


def _run_with_tmux(file_path: str, app_name: str) -> None:
    """Run app in tmux: TUI on top pane, REPL on bottom pane."""
    if shutil.which("tmux") is None:
        print("Error: tmux not found. Install it or use two terminals:")
        print(f"  Terminal 1: pygui run {file_path}")
        print(f"  Terminal 2: pygui connect {app_name}")
        sys.exit(1)

    session = f"pygui-{app_name}"
    python = sys.executable
    file_abs = os.path.abspath(file_path)

    # Command for top pane: run the TUI app, then kill session on exit
    run_cmd = f"{python} -m genro_textual.cli run {file_abs}; tmux kill-session -t {session}"
    # Command for bottom pane: wait for app to register, then connect
    connect_cmd = (
        f"sleep 1 && {python} -m genro_textual.cli connect {app_name}"
    )

    # Create tmux session with the TUI in the first pane
    subprocess.run(
        ["tmux", "new-session", "-d", "-s", session, "-x", "200", "-y", "50", run_cmd],
        check=True,
    )
    # Split horizontally: REPL on bottom (30% height)
    subprocess.run(
        ["tmux", "split-window", "-v", "-t", session, "-p", "30", connect_cmd],
        check=True,
    )
    # Enable mouse support — click on a pane to focus it
    subprocess.run(
        ["tmux", "set-option", "-t", session, "mouse", "on"],
        check=True,
    )
    # Focus on the TUI pane (top)
    subprocess.run(
        ["tmux", "select-pane", "-t", session + ":.0"],
        check=True,
    )
    # Attach to the session
    os.execvp("tmux", ["tmux", "attach-session", "-t", session])


def _run_with_reload(file_path: str) -> None:
    """Run app with autoreload using watchfiles."""
    try:
        from watchfiles import run_process
    except ImportError:
        print("Error: watchfiles not installed. Run: pip install watchfiles")
        sys.exit(1)

    def target():
        run_app(file_path, connect=False, reload=False)

    watch_dir = os.path.dirname(os.path.abspath(file_path)) or "."
    run_process(watch_dir, target=target)


def run_app(file_path: str, connect: bool = False, reload: bool = False) -> None:
    """Run a TextualApp from file path. Expects class Application."""
    if not os.path.isfile(file_path):
        print(f"Error: file not found: {file_path}")
        sys.exit(1)

    app_name = os.path.splitext(os.path.basename(file_path))[0]

    if reload:
        _run_with_reload(file_path)
        return

    if connect:
        _run_with_tmux(file_path, app_name)
        return

    # Carica il modulo dal file
    spec = importlib.util.spec_from_file_location(app_name, file_path)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)

    # Cerca classe Main
    app_class = getattr(module, "Application", None)
    if app_class is None:
        print(f"Error: {file_path} must have a class named 'Application'")
        sys.exit(1)

    port = find_free_port()

    # Create app first to get token from remote server
    app = app_class(remote_port=port)

    # Get token from remote server (created during app init)
    token = ""
    if app._remote_server is not None:
        token = app._remote_server.token

    register_app(app_name, port, token)
    print(f"Starting {app_name} on port {port}")

    try:
        app.run()
    finally:
        unregister_app(app_name)


def _is_alive(port: int) -> bool:
    """Check if something is listening on the given port."""
    import socket

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(0.3)
    try:
        sock.connect(("localhost", port))
        sock.close()
        return True
    except (ConnectionRefusedError, OSError):
        return False


def list_running() -> None:
    """List all registered apps, checking if they are alive."""
    apps = list_apps()
    if not apps:
        print("No apps registered")
        return
    dead = []
    for app_name, info in apps.items():
        port = info["port"] if isinstance(info, dict) else info
        alive = _is_alive(port)
        status = "alive" if alive else "dead"
        print(f"  {app_name}: port {port} [{status}]")
        if not alive:
            dead.append(app_name)
    if dead:
        for name in dead:
            unregister_app(name)
        print(f"  Cleaned up {len(dead)} dead app(s)")


def _repl_help(name: str, port: int) -> None:
    """Print REPL help."""
    print(f"""
pygui REPL — connected to '{name}' on port {port}

Slash commands:
  /help             — show this help
  /keys             — list page node keys
  /quit             — quit the REPL

Add widgets:
  app.page.static("Hello!")
  app.page.button("Click me", variant="primary")
  app.page.input(placeholder="Type here")
  app.page.css(".title {{ color: red; }}")
  app.page.binding(key="d", action="toggle_dark", description="Dark mode")

Containers:
  v = app.page.vertical()
  v.static("Inside vertical")
  v.button("OK")

Inspect:
  app.page.keys()          — list node keys
  app.page["static_0"]     — get a node value
""")


def connect_repl(name: str) -> None:
    """Start a REPL connected to an app."""
    import code

    info = get_app_info(name)
    if info is None:
        print(f"App '{name}' not found")
        sys.exit(1)

    from genro_textual.remote import connect

    app = connect(name=name)
    port = info["port"]

    slash_commands = {
        "/help": lambda: _repl_help(name, port),
        "/keys": lambda: print(app.page.keys()),
        "/quit": lambda: (_ for _ in ()).throw(SystemExit(0)),
    }

    class SlashConsole(code.InteractiveConsole):
        """Python REPL that intercepts /commands."""

        def runsource(self, source, filename="<input>", symbol="single"):
            stripped = source.strip()
            if stripped.startswith("/"):
                cmd = stripped.split()[0]
                handler = slash_commands.get(cmd)
                if handler:
                    handler()
                    return False
                print(f"Unknown command: {cmd}. Type /help for available commands.")
                return False
            return super().runsource(source, filename, symbol)

    # Enable readline for history (arrow keys) and tab completion
    try:
        import readline  # noqa: F401
    except ImportError:
        pass

    print(f"Connected to {name} on port {port}")
    print("Type /help for available commands")

    console = SlashConsole(locals={"app": app})
    console.interact(banner="", exitmsg="")


def stop_app(name: str) -> None:
    """Stop a running app and kill its tmux session if any."""
    info = get_app_info(name)
    if info is None:
        print(f"App '{name}' not found")
        sys.exit(1)

    # Try graceful shutdown via remote protocol
    from genro_textual.remote import connect

    try:
        app = connect(name=name)
        app._send(("__quit__",))
        print(f"Stopped {name}")
    except Exception:
        print(f"App {name} not responding, cleaning up")

    # Kill tmux session if it exists
    session = f"pygui-{name}"
    subprocess.run(
        ["tmux", "kill-session", "-t", session],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )

    unregister_app(name)
    print(f"Unregistered {name}")


def _list_app_names() -> list[str]:
    """Return registered app names for completion."""
    return list(list_apps().keys())


def _generate_zsh_completion() -> str:
    """Generate zsh completion script for pygui."""
    return '''#compdef pygui

_pygui_app_names() {
    local -a apps
    apps=($(pygui _complete_apps 2>/dev/null))
    _describe 'app name' apps
}

_pygui() {
    local -a commands
    commands=(
        'run:Run a TextualApp'
        'list:List running apps'
        'connect:Connect REPL to an app'
        'stop:Stop a running app'
        'completions:Generate shell completions'
    )

    _arguments -C \\
        '1:command:->command' \\
        '*::arg:->args'

    case $state in
        command)
            _describe 'command' commands
            ;;
        args)
            case $words[1] in
                run)
                    _arguments \\
                        '-c[Run with REPL in tmux]' \\
                        '-r[Run with autoreload]' \\
                        '1:file:_files -g "*(/) *.py"'
                    ;;
                connect|stop)
                    _pygui_app_names
                    ;;
            esac
            ;;
    esac
}

_pygui "$@"
'''


def print_completions(shell: str) -> None:
    """Print shell completion script."""
    if shell == "zsh":
        print(_generate_zsh_completion())
        print("# Add to ~/.zshrc:")
        print("#   eval \"$(pygui completions zsh)\"")
    else:
        print(f"Unsupported shell: {shell}. Only 'zsh' is supported.")
        sys.exit(1)


def main() -> None:
    """CLI entry point."""
    parser = argparse.ArgumentParser(prog="pygui", description="TextualApp CLI")
    subparsers = parser.add_subparsers(dest="command", required=True)

    # run command
    run_parser = subparsers.add_parser("run", help="Run a TextualApp")
    run_parser.add_argument("file", help="path/to/file.py (must have class Application)")
    run_parser.add_argument(
        "-c", "--connect", action="store_true", help="Run in background and connect REPL"
    )
    run_parser.add_argument(
        "-r", "--reload", action="store_true", help="Run with autoreload on file changes"
    )

    # list command
    subparsers.add_parser("list", help="List running apps")

    # connect command
    connect_parser = subparsers.add_parser("connect", help="Connect to an app")
    connect_parser.add_argument("name", help="App name")

    # stop command
    stop_parser = subparsers.add_parser("stop", help="Stop a running app")
    stop_parser.add_argument("name", help="App name")

    # completions command
    comp_parser = subparsers.add_parser("completions", help="Generate shell completions")
    comp_parser.add_argument("shell", nargs="?", default="zsh", help="Shell type (default: zsh)")

    # Hidden: emit app names for completion
    subparsers.add_parser("_complete_apps", help=argparse.SUPPRESS)

    args = parser.parse_args()

    if args.command == "run":
        run_app(args.file, connect=args.connect, reload=args.reload)
    elif args.command == "list":
        list_running()
    elif args.command == "connect":
        connect_repl(args.name)
    elif args.command == "stop":
        stop_app(args.name)
    elif args.command == "completions":
        print_completions(args.shell)
    elif args.command == "_complete_apps":
        for name in _list_app_names():
            print(name)


if __name__ == "__main__":
    main()
