"""
OpenTester CLI Tool

Usage:
    opentester --help
    opentester start                          # Start both API (8000) and MCP (8001)
    opentester start --api-port 8080          # Custom API port
    opentester start --mcp-port 8081          # Custom MCP port
    opentester api                            # Start API server only
    opentester api --port 8080                # Custom port
    opentester mcp                            # Start MCP server only
    opentester mcp --port 8081                # Custom port
    opentester status
    opentester doctor
    opentester stop
"""
import json
import os
import subprocess
import sys
import time
from pathlib import Path
from typing import Optional

import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

# Initialize Typer app with subcommands
app = typer.Typer(
    name="opentester",
    help="OpenTester CLI - Manage testing services",
    no_args_is_help=True,
)
console = Console()

# Constants
PID_DIR = Path.home() / ".opentester" / "pids"
CONFIG_FILE = Path.home() / ".config" / "opentester" / "config.json"
DEFAULT_API_PORT = 8000
DEFAULT_MCP_PORT = 8001


def ensure_dirs():
    """Ensure required directories exist."""
    PID_DIR.mkdir(parents=True, exist_ok=True)
    CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)


def get_pid(service: str) -> Optional[int]:
    """Get PID for a service."""
    pid_file = PID_DIR / f"{service}.pid"
    if pid_file.exists():
        try:
            return int(pid_file.read_text().strip())
        except ValueError:
            return None
    return None


def save_pid(service: str, pid: int):
    """Save PID for a service."""
    ensure_dirs()
    pid_file = PID_DIR / f"{service}.pid"
    pid_file.write_text(str(pid))


def remove_pid(service: str):
    """Remove PID file for a service."""
    pid_file = PID_DIR / f"{service}.pid"
    if pid_file.exists():
        pid_file.unlink()


def is_process_running(pid: int) -> bool:
    """Check if a process is running."""
    try:
        os.kill(pid, 0)
        return True
    except (OSError, ProcessLookupError):
        return False


def get_executable() -> str:
    """Get the executable path (handles both Python and PyInstaller)."""
    return sys.executable


def run_subcommand(cmd: list, env: Optional[dict] = None) -> subprocess.Popen:
    """Run a subcommand using the same executable."""
    # In PyInstaller, sys.executable is the bundle exe
    # We append the subcommand as the first argument
    full_cmd = [get_executable()] + cmd
    return subprocess.Popen(full_cmd, env=env)


# ============== API Server Subcommand ==============

@app.command(name="api")
def api_command(
    host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind to"),
    port: int = typer.Option(8000, "--port", "-p", help="Port to bind to"),
    reload: bool = typer.Option(False, "--reload", help="Enable auto-reload"),
):
    """Start the FastAPI server (Web UI)."""
    import uvicorn
    from opentester.main import app as fastapi_app

    console.print(f"[blue]Starting API server on {host}:{port}...[/blue]")
    uvicorn.run(
        fastapi_app,
        host=host,
        port=port,
        reload=reload,
        log_level="info",
    )


# ============== MCP Server Subcommand ==============

@app.command(name="mcp")
def mcp_command(
    host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind to"),
    port: int = typer.Option(8001, "--port", "-p", help="Port to bind to"),
):
    """Start the MCP server (Claude Code integration)."""
    os.environ["FASTMCP_HOST"] = host
    os.environ["FASTMCP_PORT"] = str(port)

    from opentester.mcp.server import mcp

    console.print(f"[green]Starting MCP server on {host}:{port}...[/green]")
    console.print(f"[dim]Transport: streamable-http[/dim]")
    mcp.run(transport="streamable-http")


# ============== Start Command (Both Servers) ==============

@app.command(name="start")
def start_command(
    api_only: bool = typer.Option(False, "--api", help="Start only API server"),
    mcp_only: bool = typer.Option(False, "--mcp", help="Start only MCP server"),
    api_port: int = typer.Option(8000, "--api-port", help="Port for API server (default: 8000)"),
    mcp_port: int = typer.Option(8001, "--mcp-port", help="Port for MCP server (default: 8001)"),
    daemon: bool = typer.Option(False, "--daemon", "-d", help="Run in background"),
):
    """Start OpenTester services."""
    console.print("=" * 50)
    console.print("[bold]OpenTester Servers[/bold]")
    console.print("=" * 50)
    console.print()

    if not api_only:
        console.print(f"  [green]MCP:[/green] http://localhost:{mcp_port}/mcp")
    if not mcp_only:
        console.print(f"  [blue]API:[/blue] http://localhost:{api_port}")
    console.print()

    processes = []

    # Start MCP if not api-only
    if not api_only:
        console.print(f"[green]Starting MCP server on port {mcp_port}...[/green]")
        mcp_proc = run_subcommand(["mcp", "--port", str(mcp_port)])
        save_pid("mcp", mcp_proc.pid)
        processes.append(("mcp", mcp_proc))
        time.sleep(2)

    # Start API if not mcp-only
    if not mcp_only:
        console.print(f"[blue]Starting API server on port {api_port}...[/blue]")
        api_proc = run_subcommand(["api", "--port", str(api_port)])
        save_pid("api", api_proc.pid)
        processes.append(("api", api_proc))
        time.sleep(2)

    console.print()
    console.print("[yellow]Press Ctrl+C to stop[/yellow]")
    console.print()

    try:
        while True:
            for name, proc in processes:
                if proc.poll() is not None:
                    console.print(f"[red]{name.upper()} server exited[/red]")
                    # Terminate other processes
                    for other_name, other_proc in processes:
                        if other_proc != proc:
                            other_proc.terminate()
                    return
            time.sleep(1)
    except KeyboardInterrupt:
        console.print("\n[yellow]Shutting down...[/yellow]")
        for name, proc in processes:
            proc.terminate()
            proc.wait()
            remove_pid(name)


# ============== Stop Command ==============

@app.command(name="stop")
def stop_command(
    api: bool = typer.Option(False, "--api", help="Stop only API server"),
    mcp: bool = typer.Option(False, "--mcp", help="Stop only MCP server"),
):
    """Stop OpenTester services."""
    services = []
    if api:
        services = ["api"]
    elif mcp:
        services = ["mcp"]
    else:
        services = ["api", "mcp"]

    for service in services:
        pid = get_pid(service)
        if pid and is_process_running(pid):
            try:
                os.kill(pid, 15)  # SIGTERM
                console.print(f"[green]Stopped {service.upper()} server (PID: {pid})[/green]")
            except OSError:
                console.print(f"[red]Failed to stop {service.upper()} server[/red]")
        else:
            console.print(f"[yellow]{service.upper()} server not running[/yellow]")
        remove_pid(service)


# ============== Status Command ==============

@app.command(name="status")
def status_command():
    """Check OpenTester services status."""
    table = Table(title="OpenTester Status")
    table.add_column("Service", style="cyan")
    table.add_column("Status", style="green")
    table.add_column("Info", style="dim")

    for service in ["api", "mcp"]:
        pid = get_pid(service)
        if pid and is_process_running(pid):
            table.add_row(
                service.upper(),
                "[green]running[/green]",
                f"PID: {pid}"
            )
        else:
            table.add_row(service.upper(), "[red]stopped[/red]", "")
            remove_pid(service)

    console.print(table)


# ============== Doctor Command ==============

@app.command(name="doctor")
def doctor_command():
    """Check environment and configuration."""
    table = Table(title="OpenTester Environment Check")
    table.add_column("Check", style="cyan")
    table.add_column("Status")
    table.add_column("Result")

    # Python version
    py_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
    table.add_row("Python version", "[green]OK[/green]", py_version)

    # Data directory
    data_dir = Path.home() / ".opentester"
    if data_dir.exists():
        table.add_row("Data directory", "[green]OK[/green]", str(data_dir))
    else:
        data_dir.mkdir(parents=True, exist_ok=True)
        table.add_row("Data directory", "[green]Created[/green]", str(data_dir))

    # Core dependencies
    try:
        import fastapi, uvicorn, mcp, pydantic
        table.add_row("Core dependencies", "[green]OK[/green]", "Installed")
    except ImportError as e:
        table.add_row("Core dependencies", "[red]FAIL[/red]", str(e))

    # Port availability (check default ports)
    import socket
    for port, name in [(8000, "API (default)"), (8001, "MCP (default)")]:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        result = sock.connect_ex(('127.0.0.1', port))
        sock.close()
        if result != 0:
            table.add_row(f"Port {port} ({name})", "[green]OK[/green]", "Available")
        else:
            table.add_row(f"Port {port} ({name})", "[yellow]BUSY[/yellow]", "In use")

    console.print(table)
    console.print()
    console.print("[green]Environment check complete![/green]")


# ============== Version Command ==============

@app.command(name="version")
def version_command():
    """Show version information."""
    console.print("[bold]OpenTester[/bold] v0.1.0")
    console.print("MCP-First Testing Execution Platform")


# Entry point for PyPI package
def main():
    """CLI entry point."""
    app()


if __name__ == "__main__":
    main()
