#!/usr/bin/env -S uv run --script
#MISE silent=true
#MISE description="Start the development environment for a project"
# /// script
# dependencies = ["rich"]
# ///
import argparse
import signal
import subprocess
import sys
import threading
import time
import json
from pathlib import Path
from typing import Optional, List, Dict

from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.live import Live
from rich.layout import Layout
from rich.status import Status
from rich.prompt import Prompt

console = Console()

# Configuration
DEFAULT_FRONTEND_CMD = ["pnpm", "run", "dev"]
DEFAULT_BACKEND_CMD = ["uv", "run", "uvicorn", "api.server:app", "--reload"]
DEFAULT_FRONTEND_PORT = 3000
DEFAULT_BACKEND_PORT = 8000

class ProcessManager:
    def __init__(self):
        self.processes: Dict[str, subprocess.Popen] = {}
        self.running = False
        self.logs: Dict[str, List[str]] = {"frontend": [], "backend": []}

    def start_process(self, name: str, cmd: List[str], cwd: Optional[str] = None) -> bool:
        """Start a process and track it."""
        try:
            console.print(f"[dim]Starting {name} in {cwd or 'current directory'}: {' '.join(cmd)}[/dim]")

            process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                bufsize=1,
                universal_newlines=True,
                cwd=cwd
            )
            self.processes[name] = process

            # Start thread to capture output
            thread = threading.Thread(
                target=self._capture_output,
                args=(name, process),
                daemon=True
            )
            thread.start()

            return True
        except Exception as e:
            console.print(f"[red]Failed to start {name}: {e}[/red]")
            return False

    def _capture_output(self, name: str, process: subprocess.Popen):
        """Capture process output in a separate thread."""
        while True:
            stdout = process.stdout
            assert stdout is not None
            output = stdout.readline()
            if output == '' and process.poll() is not None:
                break
            if output:
                # Keep only last 50 lines
                self.logs[name].append(output.strip())
                if len(self.logs[name]) > 50:
                    self.logs[name].pop(0)

    def stop_all(self):
        """Stop all processes."""
        self.running = False
        for name, process in self.processes.items():
            try:
                process.terminate()
                process.wait(timeout=5)
            except subprocess.TimeoutExpired:
                process.kill()
            except Exception:
                pass

    def is_running(self) -> bool:
        """Check if any process is still running."""
        return any(p.poll() is None for p in self.processes.values())

    def get_status(self) -> Dict[str, str]:
        """Get status of all processes."""
        status = {}
        for name, process in self.processes.items():
            if process.poll() is None:
                status[name] = "🟢 Running"
            else:
                status[name] = f"🔴 Stopped (exit code: {process.poll()})"
        return status

def find_project_root() -> Optional[Path]:
    """Find the project root directory by looking for package.json."""
    current_dir = Path(__file__).resolve().parent

    # Start from the script directory and go up
    while current_dir != current_dir.parent:
        if (current_dir / "package.json").exists():
            return current_dir
        current_dir = current_dir.parent

    return None

def get_available_apps(project_root: Path) -> List[str]:
    """Get list of available apps in the monorepo."""
    apps_dir = project_root / "apps"
    if not apps_dir.exists():
        return []

    apps = []
    for app_dir in apps_dir.iterdir():
        if app_dir.is_dir() and (app_dir / "package.json").exists():
            apps.append(app_dir.name)

    return sorted(apps)

def get_package_name(app_path: Path) -> str:
    """Get the package name from package.json."""
    try:
        with open(app_path / "package.json", 'r') as f:
            package_data = json.load(f)
            return package_data.get("name", "")
    except (FileNotFoundError, json.JSONDecodeError):
        return ""

def check_dependencies(project_root: Path, app_name: str | None = None) -> Dict[str, bool]:
    """Check if required dependencies are available."""
    deps = {}

    # Check pnpm global availability
    try:
        subprocess.run(["pnpm", "--version"], capture_output=True, check=True)
        deps["pnpm"] = True
    except (subprocess.CalledProcessError, FileNotFoundError):
        deps["pnpm"] = False

    # Check uv global availability
    try:
        subprocess.run(["uv", "--version"], capture_output=True, check=True)
        deps["uv"] = True
    except (subprocess.CalledProcessError, FileNotFoundError):
        deps["uv"] = False

    # Check project structure
    deps["package.json"] = (project_root / "package.json").exists()
    deps["apps directory"] = (project_root / "apps").exists()

    # If app is specified, check app-specific dependencies
    if app_name:
        app_path = project_root / "apps" / app_name

        # Check pnpm workspace install for specific app
        if deps["pnpm"] and (app_path / "package.json").exists():
            try:
                # Use pnpm list to check if the workspace package is valid
                subprocess.run(
                    ["pnpm", "list", "--filter", app_name, "--depth", "0"],
                    capture_output=True,
                    check=True,
                    cwd=str(project_root),
                    timeout=15
                )
                deps[f"pnpm install ({app_name})"] = True
            except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
                deps[f"pnpm install ({app_name})"] = False
        else:
            deps[f"pnpm install ({app_name})"] = False

        # Check uv sync for specific app
        if deps["uv"] and (app_path / "api").exists():
            try:
                # Check if we can sync the package (dry-run)
                subprocess.run(
                    ["uv", "sync", "--package", app_name, "--dry-run"],
                    capture_output=True,
                    check=True,
                    cwd=str(project_root),
                    timeout=15
                )
                deps[f"uv sync ({app_name})"] = True
            except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
                deps[f"uv sync ({app_name})"] = False
        else:
            deps[f"uv sync ({app_name})"] = False

    return deps

def display_dependency_status(deps: Dict[str, bool]) -> bool:
    """Display dependency status and return if all are satisfied."""
    table = Table(title="🔍 Dependency Check")
    table.add_column("Dependency", style="cyan")
    table.add_column("Status", style="bold")
    table.add_column("Description", style="dim")

    descriptions = {
        "pnpm": "Node.js package manager",
        "uv": "Python package manager",
        "package.json": "Root project configuration",
        "apps directory": "Apps directory for monorepo"
    }

    all_good = True
    for dep, available in deps.items():
        if available:
            status = "[green]✅ Available[/green]"
        else:
            status = "[red]❌ Missing[/red]"
            all_good = False

        # Generate description for app-specific dependencies
        description = descriptions.get(dep, "")
        if "pnpm install" in dep:
            description = "Frontend dependencies installation"
        elif "uv sync" in dep:
            description = "Backend dependencies synchronization"

        table.add_row(dep, status, description)

    console.print(table)
    return all_good

def display_available_apps(apps: List[str], project_root: Path, show_numbers: bool = False):
    """Display available apps."""
    if not apps:
        console.print("[yellow]No apps found in the monorepo.[/yellow]")
        return

    table = Table(title="📱 Available Apps")

    if show_numbers:
        table.add_column("No.", style="bold yellow", width=4)

    table.add_column("App Name", style="cyan")
    table.add_column("Frontend", style="green")
    table.add_column("Backend", style="blue")

    for i, app in enumerate(apps, 1):
        app_path = project_root / "apps" / app
        has_frontend = (app_path / "package.json").exists()
        has_backend = (app_path / "api").exists()

        frontend_status = "✅" if has_frontend else "❌"
        backend_status = "✅" if has_backend else "❌"

        if show_numbers:
            table.add_row(str(i), app, frontend_status, backend_status)
        else:
            table.add_row(app, frontend_status, backend_status)

    console.print(table)

def interactive_select_app(available_apps: List[str], project_root: Path) -> Optional[str]:
    """Interactively select an app from the available apps."""
    if not available_apps:
        console.print("[red]❌ No apps found in the monorepo.[/red]")
        return None

    console.print("\n[bold]Available apps in monorepo:[/bold]")
    display_available_apps(available_apps, project_root, show_numbers=True)

    # Create choices with numbers
    choices = [str(i) for i in range(1, len(available_apps) + 1)]

    console.print()

    try:
        choice = Prompt.ask(
            "Select an app to start",
            choices=choices,
            default="1"
        )

        selected_app = available_apps[int(choice) - 1]
        console.print(f"[green]✅ Selected app: {selected_app}[/green]")
        return selected_app

    except (ValueError, IndexError, KeyboardInterrupt):
        console.print("[yellow]Selection cancelled.[/yellow]")
        return None

def create_live_dashboard(manager: ProcessManager, app_name: str) -> Layout:
    """Create a live dashboard layout."""
    layout = Layout()

    layout.split_column(
        Layout(name="header", size=3),
        Layout(name="body"),
        Layout(name="footer", size=3)
    )

    layout["body"].split_row(
        Layout(name="frontend"),
        Layout(name="backend")
    )

    return layout

def update_dashboard(layout: Layout, manager: ProcessManager, app_name: str):
    """Update the dashboard with current status."""
    # Header
    layout["header"].update(
        Panel(
            f"[bold blue]🚀 Development Environment - {app_name}[/bold blue]\n"
            "[dim]Press Ctrl+C to stop all services[/dim]",
            border_style="blue"
        )
    )

    # Status
    status = manager.get_status()

    # Frontend panel
    frontend_logs = "\n".join(manager.logs.get("frontend", [])[-10:])  # Last 10 lines
    frontend_status = status.get("frontend", "🔴 Not started")

    layout["frontend"].update(
        Panel(
            f"[bold]Status:[/bold] {frontend_status}\n\n"
            f"[dim]Recent output:[/dim]\n{frontend_logs}",
            title="🎨 Frontend (pnpm)",
            border_style="green" if "Running" in frontend_status else "red"
        )
    )

    # Backend panel
    backend_logs = "\n".join(manager.logs.get("backend", [])[-10:])  # Last 10 lines
    backend_status = status.get("backend", "🔴 Not started")

    layout["backend"].update(
        Panel(
            f"[bold]Status:[/bold] {backend_status}\n\n"
            f"[dim]Recent output:[/dim]\n{backend_logs}",
            title="🔧 Backend (uvicorn)",
            border_style="green" if "Running" in backend_status else "red"
        )
    )

    # Footer
    layout["footer"].update(
        Panel(
            f"[bold]Frontend:[/bold] http://localhost:{DEFAULT_FRONTEND_PORT} | "
            f"[bold]Backend:[/bold] http://localhost:{DEFAULT_BACKEND_PORT}",
            border_style="blue"
        )
    )

def start_development_environment(
    app_name: str,
    frontend_cmd: List[str],
    backend_cmd: List[str],
    show_dashboard: bool = True,
    project_root: Path | None = None
) -> bool:
    """Start the development environment."""
    manager = ProcessManager()

    # Set up signal handler for graceful shutdown
    def signal_handler(sig, frame):
        console.print(f"\n[yellow]Shutting down development environment for {app_name}...[/yellow]")
        manager.stop_all()
        sys.exit(0)

    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, signal_handler)

    assert project_root is not None, "project_root must be provided"

    # Check dependencies first
    console.print("[bold]Checking dependencies...[/bold]")
    deps = check_dependencies(project_root, app_name)

    if not display_dependency_status(deps):
        console.print("\n[red]❌ Some dependencies are missing. Please install them first.[/red]")
        return False

    console.print("\n[green]✅ All dependencies are available![/green]")

    # Set up paths
    app_path = project_root / "apps" / app_name
    frontend_cwd = str(app_path) if (app_path / "package.json").exists() else None
    backend_cwd = str(app_path) if (app_path / "api").exists() else None

    # Show startup configuration
    config_panel = Panel.fit(
        f"[bold]App:[/bold] {app_name}\n"
        f"[bold]Frontend:[/bold] {' '.join(frontend_cmd)} (cwd: {frontend_cwd or 'N/A'})\n"
        f"[bold]Backend:[/bold] {' '.join(backend_cmd)} (cwd: {backend_cwd or 'N/A'})\n"
        f"[bold]Frontend URL:[/bold] http://localhost:{DEFAULT_FRONTEND_PORT}\n"
        f"[bold]Backend URL:[/bold] http://localhost:{DEFAULT_BACKEND_PORT}",
        title="🚀 Development Configuration",
        border_style="green"
    )
    console.print(config_panel)

    # Start processes
    console.print("\n[bold]Starting development servers...[/bold]")

    # Start frontend if available
    if frontend_cwd:
        with Status("Starting frontend server...", spinner="dots"):
            if not manager.start_process("frontend", frontend_cmd, frontend_cwd):
                console.print("[red]Failed to start frontend[/red]")
                return False
            time.sleep(2)  # Give it a moment to start
    else:
        console.print("[yellow]No frontend found for this app[/yellow]")

    # Start backend if available
    if backend_cwd:
        with Status("Starting backend server...", spinner="dots"):
            if not manager.start_process("backend", backend_cmd, backend_cwd):
                console.print("[red]Failed to start backend[/red]")
                return False
            time.sleep(2)  # Give it a moment to start
    else:
        console.print("[yellow]No backend found for this app[/yellow]")

    if not manager.processes:
        console.print("[red]No services to start for this app[/red]")
        return False

    manager.running = True

    if show_dashboard:
        # Live dashboard
        layout = create_live_dashboard(manager, app_name)

        with Live(layout, refresh_per_second=2, screen=True):
            try:
                while manager.is_running():
                    update_dashboard(layout, manager, app_name)
                    time.sleep(0.5)
            except KeyboardInterrupt:
                pass
    else:
        # Simple mode - just wait
        try:
            console.print(f"\n[green]✅ Development environment started for {app_name}![/green]")
            console.print("[dim]Press Ctrl+C to stop all services[/dim]")

            while manager.is_running():
                time.sleep(1)
        except KeyboardInterrupt:
            pass

    # Cleanup
    console.print(f"\n[yellow]Stopping all services for {app_name}...[/yellow]")
    manager.stop_all()
    console.print("[green]✅ Development environment stopped.[/green]")

    return True

def main():
    # Find project root
    project_root = find_project_root()
    if not project_root:
        console.print("[red]❌ Could not find project root (package.json not found)[/red]")
        sys.exit(1)

    console.print(f"[dim]Project root: {project_root}[/dim]")

    # Get available apps
    available_apps = get_available_apps(project_root)

    parser = argparse.ArgumentParser(
        description="Start development environment with frontend and backend servers for a monorepo app",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=f"""
Available apps: {', '.join(available_apps) if available_apps else 'None found'}

Examples:
  python dev.py                                    # Interactive app selection
  python dev.py --app bursatil                    # Start bursatil app directly
  python dev.py --app ocp --no-dashboard          # Start ocp app without dashboard
  python dev.py --list-apps                       # List available apps
  python dev.py --app bursatil --frontend-cmd "pnpm run build" # Custom frontend command
        """
    )

    parser.add_argument(
        "--app",
        choices=available_apps,
        help="App to start (if not provided, you'll be prompted to select one)"
    )

    parser.add_argument(
        "--frontend-cmd",
        default=" ".join(DEFAULT_FRONTEND_CMD),
        help="Frontend command to run (default: pnpm run dev)"
    )

    parser.add_argument(
        "--backend-cmd",
        default=" ".join(DEFAULT_BACKEND_CMD),
        help="Backend command to run (default: uv run uvicorn api.server:app --reload)"
    )

    parser.add_argument(
        "--no-dashboard",
        action="store_true",
        help="Disable the live dashboard"
    )

    parser.add_argument(
        "--check-deps",
        action="store_true",
        help="Only check dependencies and exit"
    )

    parser.add_argument(
        "--list-apps",
        action="store_true",
        help="List available apps and exit"
    )

    args = parser.parse_args()

    # Handle --check-deps
    if args.check_deps:
        console.print("[bold]Checking dependencies...[/bold]")
        deps = check_dependencies(project_root)
        display_dependency_status(deps)
        return

    # Handle --list-apps
    if args.list_apps:
        console.print("[bold]Available apps in monorepo:[/bold]")
        display_available_apps(available_apps, project_root)
        return

    # Handle app selection
    if not args.app:
        if not available_apps:
            console.print("[red]❌ No apps found in the monorepo.[/red]")
            sys.exit(1)

        # Interactive selection
        selected_app = interactive_select_app(available_apps, project_root)
        if not selected_app:
            sys.exit(1)
        args.app = selected_app

    # Parse commands
    frontend_cmd = args.frontend_cmd.split()
    backend_cmd = args.backend_cmd.split()
    show_dashboard = not args.no_dashboard

    # Start development environment
    success = start_development_environment(
        args.app,
        frontend_cmd,
        backend_cmd,
        show_dashboard,
        project_root
    )
    sys.exit(0 if success else 1)

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        console.print("\n[yellow]Operation cancelled by user.[/yellow]")
        sys.exit(1)
    except Exception as e:
        console.print(f"[red]Unexpected error: {e}[/red]")
        sys.exit(1)
