"""CLI tool for Ugly Bot local development.

Commands:
    ugly-bot login     - Authenticate with the server
    ugly-bot dev       - Start dev tunnel for local bot testing
    ugly-bot sync      - Download bot code from server
    ugly-bot publish   - Upload local bot code to server
    ugly-bot logout    - Clear stored credentials
"""

import http.server
import json
import os
import sys
import threading
import time
import webbrowser
from pathlib import Path
from typing import Optional
from urllib.parse import parse_qs, urlparse

from .auth import AuthConfig, clear_auth, get_auth, save_auth

DEFAULT_SERVER_URL = "https://ugly.bot"
CONFIG_FILE = "ugly-bot.json"

# Global verbose flag
_verbose = False


def _log(msg: str) -> None:
    """Print a debug message when verbose mode is enabled."""
    if _verbose:
        print(f"  [DEBUG] {msg}")


def _get_server_url() -> str:
    """Get server URL from env or default."""
    return os.environ.get("UGLY_BOT_SERVER", DEFAULT_SERVER_URL)


def _compare_versions(a: str, b: str) -> int:
    """Compare two version strings. Returns -1, 0, or 1."""
    a_parts = [int(x) for x in a.split(".")]
    b_parts = [int(x) for x in b.split(".")]
    for i in range(max(len(a_parts), len(b_parts))):
        av = a_parts[i] if i < len(a_parts) else 0
        bv = b_parts[i] if i < len(b_parts) else 0
        if av < bv:
            return -1
        if av > bv:
            return 1
    return 0


def _check_version(server_url: str) -> None:
    """Check CLI version against server minimum. Exit if outdated."""
    import requests
    from importlib.metadata import version

    current = version("ugly_bot")
    url = f"{server_url}/api/cli-version"
    _log(f"Version check: v{current}, fetching {url}")
    try:
        resp = requests.get(url, timeout=5)
        data = resp.json()
        min_version = data.get("minVersion", "0.0.0")
        _log(f"Server requires >= v{min_version}")
        if _compare_versions(current, min_version) < 0:
            print(f"ugly-bot v{current} is outdated (minimum: {min_version})")
            print("Upgrade: pip install --upgrade ugly-bot")
            sys.exit(1)
    except Exception as e:
        _log(f"Version check failed: {e}")


def _ws_connect(auth: AuthConfig, session_id: str):
    """Connect to the server via WebSocket. Returns the WebSocket or exits on failure."""
    import websocket as ws_client

    parsed = urlparse(auth.serverUrl)
    ws_scheme = "wss" if parsed.scheme == "https" else "ws"
    ws_url = f"{ws_scheme}://{parsed.netloc}/ws?token={auth.token}&sessionId={session_id}"

    _log(f"Connecting to {ws_scheme}://{parsed.netloc}/ws")

    try:
        ws = ws_client.WebSocket()
        ws.connect(ws_url, timeout=10)
        # After connecting, use a longer timeout for recv (server may take time to respond)
        ws.settimeout(60)
        _log("WebSocket connected")
        return ws
    except ConnectionRefusedError:
        print(f"  Error: Connection refused by {auth.serverUrl}")
        print("  Is the server running?")
        sys.exit(1)
    except Exception as e:
        error_type = type(e).__name__
        print(f"  Error: Could not connect to {auth.serverUrl}")
        print(f"  {error_type}: {e}")
        if "timed out" in str(e).lower() or "timeout" in str(e).lower():
            print("  Check that the server URL is correct and reachable.")
            print(f"  Current server: {auth.serverUrl}")
            print("  To change: ugly-bot login [--local]")
        sys.exit(1)


def _ws_call(ws, op: str, input_data: dict) -> dict:
    """Send a WebSocket RPC call and return the response data. Exits on error."""
    import uuid

    call_id = str(uuid.uuid4())
    _log(f"Calling {op}")

    ws.send(json.dumps({
        "type": "call",
        "id": call_id,
        "data": {
            "op": op,
            "input": input_data,
        },
    }))

    while True:
        raw = ws.recv()
        msg = json.loads(raw)
        if msg.get("type") == "response" and msg.get("id") == call_id:
            data = msg.get("data", {})
            if "error" in data and data["error"]:
                return data
            _log(f"{op} responded OK")
            return data

    return {}


def _get_config() -> Optional[dict]:
    """Read ugly-bot.json from current directory."""
    if os.path.exists(CONFIG_FILE):
        with open(CONFIG_FILE, "r") as f:
            return json.load(f)
    return None


def _save_config(config: dict) -> None:
    """Write ugly-bot.json to current directory."""
    with open(CONFIG_FILE, "w") as f:
        json.dump(config, f, indent=2)
    print(f"  Saved {CONFIG_FILE}")


def _require_auth() -> AuthConfig:
    """Get auth config or exit with error."""
    auth = get_auth()
    if auth is None:
        print("Not logged in. Run 'ugly-bot login' first.")
        sys.exit(1)
    _log(f"Authenticated as {auth.userId}")
    _log(f"Server: {auth.serverUrl}")
    return auth


def cmd_login(local: bool = False) -> None:
    """Authenticate with the server via browser."""
    server_url = "http://localhost:3000" if local else _get_server_url()
    _check_version(server_url)
    print(f"Logging in to {server_url}...")

    # Generate a nonce for security
    import secrets
    nonce = secrets.token_urlsafe(16)

    # Start a temporary local HTTP server to receive the callback
    received = threading.Event()
    result = {}

    class CallbackHandler(http.server.BaseHTTPRequestHandler):
        def do_GET(self):
            nonlocal result
            parsed = urlparse(self.path)
            if parsed.path == "/callback":
                params = parse_qs(parsed.query)
                token = params.get("token", [None])[0]
                user_id = params.get("userId", [None])[0]
                cb_nonce = params.get("nonce", [None])[0]

                if cb_nonce != nonce:
                    self.send_response(400)
                    self.end_headers()
                    self.wfile.write(b"Invalid nonce")
                    return

                if token and user_id:
                    result["token"] = token
                    result["userId"] = user_id
                    self.send_response(200)
                    self.send_header("Content-Type", "text/html")
                    self.end_headers()
                    self.wfile.write(
                        b"<html><body style='background:#111;color:#fff;font-family:sans-serif;"
                        b"display:flex;align-items:center;justify-content:center;height:100vh'>"
                        b"<div style='text-align:center'>"
                        b"<h1 style='color:#4ade80'>Logged in!</h1>"
                        b"<p>You can close this window.</p>"
                        b"</div></body></html>"
                    )
                    received.set()
                else:
                    self.send_response(400)
                    self.end_headers()
                    self.wfile.write(b"Missing token or userId")
            else:
                self.send_response(404)
                self.end_headers()

        def log_message(self, format, *args):
            pass  # Suppress request logs

    # Find an available port
    server = http.server.HTTPServer(("localhost", 0), CallbackHandler)
    port = server.server_address[1]

    # Open browser
    auth_url = f"{server_url}/cli-auth?port={port}&nonce={nonce}"
    _log(f"Auth URL: {auth_url}")
    _log(f"Callback listening on localhost:{port}")
    print(f"  Opening browser for authentication...")
    webbrowser.open(auth_url)
    print(f"  Waiting for login (press Ctrl+C to cancel)...")

    # Wait for callback
    server_thread = threading.Thread(target=server.serve_forever, daemon=True)
    server_thread.start()

    try:
        received.wait(timeout=300)  # 5 minute timeout
    except KeyboardInterrupt:
        print("\n  Login cancelled.")
        server.shutdown()
        sys.exit(1)

    server.shutdown()

    if "token" in result:
        save_auth(
            token=result["token"],
            userId=result["userId"],
            serverUrl=server_url,
        )
        print(f"  Logged in as {result['userId']}")
    else:
        print("  Login failed - no token received.")
        sys.exit(1)


def cmd_logout() -> None:
    """Clear stored credentials."""
    clear_auth()
    print("Logged out.")


def cmd_dev() -> None:
    """Start dev tunnel for local bot testing."""
    auth = _require_auth()
    _check_version(auth.serverUrl)
    config = _get_config()

    if config is None:
        print(f"No {CONFIG_FILE} found in current directory.")
        print("Run 'ugly-bot sync <botId>' first to set up a project.")
        sys.exit(1)

    bot_id = config.get("botId")
    if not bot_id:
        print(f"No botId in {CONFIG_FILE}")
        sys.exit(1)

    project_dir = os.getcwd()

    # Check main.py exists
    if not os.path.exists(os.path.join(project_dir, "main.py")):
        print("No main.py found in current directory.")
        sys.exit(1)

    from .tunnel import DevTunnel

    def on_status(msg: str) -> None:
        print(f"[TUNNEL] {msg}")

    tunnel = DevTunnel(
        server_url=auth.serverUrl,
        token=auth.token,
        bot_id=bot_id,
        project_dir=project_dir,
        on_status=on_status,
    )

    # Start file watcher in background
    watcher_thread = _start_file_watcher(project_dir, tunnel)

    print(f"Starting dev tunnel for bot '{bot_id}'...")
    print(f"  Project: {project_dir}")
    print(f"  Server: {auth.serverUrl}")
    print(f"  Press Ctrl+C to stop\n")

    try:
        tunnel.start()
    except KeyboardInterrupt:
        print("\nStopping...")
        tunnel.stop()


def _start_file_watcher(project_dir: str, tunnel) -> Optional[threading.Thread]:
    """Start watching for Python file changes."""
    try:
        import watchfiles

        def watch_loop():
            for changes in watchfiles.watch(
                project_dir,
                watch_filter=watchfiles.PythonFilter(),
                stop_event=threading.Event(),
            ):
                changed_files = [str(c[1]) for c in changes]
                rel_files = [
                    os.path.relpath(f, project_dir) for f in changed_files
                ]
                print(f"[WATCHER] Files changed: {', '.join(rel_files)}")
                tunnel.reload()

        thread = threading.Thread(target=watch_loop, daemon=True)
        thread.start()
        return thread
    except ImportError:
        print(
            "  (Install 'watchfiles' for auto-reload: pip install watchfiles)"
        )
        return None


def cmd_sync(bot_id: Optional[str] = None, force: bool = False) -> None:
    """Download bot code from server to local machine."""
    auth = _require_auth()
    _check_version(auth.serverUrl)

    # If no bot_id provided, try to read from config
    if bot_id is None:
        config = _get_config()
        if config and config.get("botId"):
            bot_id = config["botId"]
        else:
            print("Usage: ugly-bot sync <botId>")
            print("  Or run from a directory with ugly-bot.json")
            sys.exit(1)

    print(f"Syncing bot '{bot_id}' from {auth.serverUrl}...")

    ws = _ws_connect(auth, "cli-sync")

    # First, get the bot to find its botCodeId
    bot_data = _ws_call(ws, "storeGetDoc", {"collection": "bot", "id": bot_id})
    if "error" in bot_data and bot_data["error"]:
        print(f"  Error: {bot_data['error']}")
        sys.exit(1)
    bot = bot_data.get("data", {}).get("item")
    if not bot:
        print(f"  Bot '{bot_id}' not found.")
        sys.exit(1)

    bot_code_id = bot.get("botCodeId")
    if not bot_code_id:
        print(f"  Bot '{bot_id}' has no code.")
        sys.exit(1)

    # Then, get the bot code (which contains source files)
    code_data = _ws_call(ws, "storeGetDoc", {"collection": "botCode", "id": bot_code_id})
    if "error" in code_data and code_data["error"]:
        print(f"  Error: {code_data['error']}")
        sys.exit(1)
    bot_code = code_data.get("data", {}).get("item")
    if not bot_code:
        print(f"  Bot code not found.")
        sys.exit(1)

    ws.close()

    source = bot_code.get("source", {})
    if not source:
        print("  Bot has no source code.")
        sys.exit(1)

    # Write source files to disk
    file_count = _write_source(source, ".", force)
    print(f"  Wrote {file_count} file(s)")

    # Save config
    _save_config({"botId": bot_id, "botCodeId": bot_code_id})

    # Check for requirements.txt
    if os.path.exists("requirements.txt"):
        print("\n  Tip: Create a virtual environment and install dependencies:")
        print("    python3 -m venv venv")
        print("    source venv/bin/activate")
        print("    pip install -r requirements.txt")

    print(f"\n  Run 'ugly-bot dev' to start testing!")


def _write_source(source: dict, base_path: str, force: bool) -> int:
    """Recursively write BotSource files to disk."""
    count = 0
    for key, value in source.items():
        # Unescape dots in path
        key = key.replace("&dot;", ".")
        file_path = os.path.join(base_path, key)

        if isinstance(value, dict) and "text" in value:
            # This is a file
            if os.path.exists(file_path) and not force:
                # Check if content differs
                with open(file_path, "r") as f:
                    existing = f.read()
                if existing == value["text"]:
                    continue
                print(f"  Overwriting: {file_path}")
            else:
                print(f"  Writing: {file_path}")

            os.makedirs(os.path.dirname(file_path) or ".", exist_ok=True)
            with open(file_path, "w") as f:
                f.write(value["text"])
            count += 1
        elif isinstance(value, dict):
            # This is a directory
            os.makedirs(file_path, exist_ok=True)
            count += _write_source(value, file_path, force)

    return count


def cmd_publish() -> None:
    """Upload local bot code to server."""
    auth = _require_auth()
    _check_version(auth.serverUrl)
    config = _get_config()

    if config is None:
        print(f"No {CONFIG_FILE} found. Run 'ugly-bot sync <botId>' first.")
        sys.exit(1)

    bot_id = config.get("botId")
    if not bot_id:
        print(f"No botId in {CONFIG_FILE}")
        sys.exit(1)

    project_dir = os.getcwd()

    if not os.path.exists(os.path.join(project_dir, "main.py")):
        print("No main.py found in current directory.")
        sys.exit(1)

    print(f"Publishing bot '{bot_id}' to {auth.serverUrl}...")

    # Read local source files
    source = _read_source(project_dir)
    if not source:
        print("  No files to publish.")
        sys.exit(1)

    ws = _ws_connect(auth, "cli-publish")

    # Resolve botCodeId
    bot_code_id = config.get("botCodeId")
    if not bot_code_id:
        bot_data = _ws_call(ws, "storeGetDoc", {"collection": "bot", "id": bot_id})
        if "error" in bot_data and bot_data["error"]:
            print(f"  Error: {bot_data['error']}")
            sys.exit(1)
        bot = bot_data.get("data", {}).get("item")
        if not bot:
            print(f"  Bot '{bot_id}' not found.")
            sys.exit(1)
        bot_code_id = bot.get("botCodeId")
        if not bot_code_id:
            print(f"  Bot '{bot_id}' has no code.")
            sys.exit(1)
        # Cache botCodeId in config
        config["botCodeId"] = bot_code_id
        _save_config(config)

    file_count = 0
    _publish_source(ws, bot_code_id, source, [], file_count)

    ws.close()

    print(f"\n  Published successfully!")


def _read_source(base_path: str) -> dict:
    """Read local files into BotSource format, respecting .gitignore-like patterns."""
    ignore_dirs = {"venv", "__pycache__", ".git", "node_modules", ".DS_Store"}
    ignore_extensions = {".pyc", ".pyo"}
    source = {}

    for entry in sorted(os.listdir(base_path)):
        full_path = os.path.join(base_path, entry)

        if entry in ignore_dirs or entry.startswith("."):
            continue

        if os.path.isfile(full_path):
            _, ext = os.path.splitext(entry)
            if ext in ignore_extensions:
                continue
            if entry == CONFIG_FILE:
                continue

            try:
                with open(full_path, "r") as f:
                    text = f.read()
                # Escape dots in key
                key = entry.replace(".", "&dot;")
                source[key] = {
                    "text": text,
                    "updated": int(os.path.getmtime(full_path) * 1000),
                }
            except (UnicodeDecodeError, PermissionError):
                continue

        elif os.path.isdir(full_path):
            key = entry.replace(".", "&dot;")
            sub = _read_source(full_path)
            if sub:
                source[key] = sub

    return source


def _publish_source(ws, bot_id: str, source: dict, path: list, count: int) -> int:
    """Recursively publish source files via WebSocket."""
    for key, value in source.items():
        if isinstance(value, dict) and "text" in value:
            # File
            file_path = path + [key]
            display_path = "/".join(p.replace("&dot;", ".") for p in file_path)

            data = _ws_call(ws, "botCodeSourceUpdate", {
                "id": bot_id,
                "path": file_path,
                "text": value["text"],
                "updated": value.get("updated", int(time.time() * 1000)),
            })

            if "error" in data and data["error"]:
                print(f"  Error publishing {display_path}: {data['error']}")
            else:
                print(f"  Published: {display_path}")

            count += 1

        elif isinstance(value, dict):
            # Directory
            count = _publish_source(ws, bot_id, value, path + [key], count)

    return count


def main() -> None:
    """CLI entry point."""
    args = sys.argv[1:]

    if not args or args[0] in ("--help", "-h"):
        print("Usage: ugly-bot <command> [options]")
        print()
        print("Commands:")
        print("  login              Sign in to Ugly Bot")
        print("  logout             Clear stored credentials")
        print("  dev                Start dev tunnel for local testing")
        print("  sync [botId]       Download bot code from server")
        print("  publish            Upload local bot code to server")
        print()
        print("Options:")
        print("  --local            Use local dev server (http://localhost:3000)")
        print("  --force, -f        Force overwrite without confirmation")
        print("  --verbose, -v      Show debug output")
        print()
        print("Environment:")
        print("  UGLY_BOT_SERVER    Override server URL")
        sys.exit(0)

    global _verbose
    _verbose = "--verbose" in args or "-v" in args

    command = args[0]

    if command == "login":
        local = "--local" in args
        cmd_login(local=local)
    elif command == "logout":
        cmd_logout()
    elif command == "dev":
        cmd_dev()
    elif command == "sync":
        bot_id = args[1] if len(args) > 1 and not args[1].startswith("-") else None
        force = "--force" in args or "-f" in args
        cmd_sync(bot_id=bot_id, force=force)
    elif command == "publish":
        cmd_publish()
    else:
        print(f"Unknown command: {command}")
        print("Run 'ugly-bot --help' for usage.")
        sys.exit(1)


if __name__ == "__main__":
    main()
