"""
CLI tools for myfy framework.

Provides commands for development and operations:
- myfy run: Start development server
- myfy start: Start production server
- myfy routes: List all routes
- myfy modules: Show loaded modules
- myfy frontend: Frontend commands
"""

import importlib.util
import os
import subprocess
import sys
from pathlib import Path

import klyne
import typer
import uvicorn
from rich.console import Console
from rich.table import Table

from myfy.core import Application
from myfy.core.config import load_settings
from myfy.web.config import WebSettings
from myfy_cli.commands import frontend_app
from myfy_cli.version import __version__

klyne.init(
    api_key="klyne_9Tw9gnQoW8GX4DII8v8WmEZgfsjhgZOFMXo7C9KVhjU",
    project="myfy-cli",
    package_version=__version__,
)

app = typer.Typer(
    name="myfy",
    help="myfy framework CLI",
    add_completion=False,
)
console = Console()

# Register command groups
app.add_typer(frontend_app, name="frontend")


def find_application():
    """
    Discover the Application instance in the current directory.

    Only checks whitelisted files for security:
    - app.py
    - main.py
    - application.py

    Returns:
        tuple: (Application instance, filename, variable_name)
    """
    # Only check explicitly safe files (no glob scanning for security)
    safe_files = ["app.py", "main.py", "application.py"]

    for filename in safe_files:
        file_path = Path(filename)
        if file_path.exists() and file_path.is_file():
            # Validate it's actually a Python file
            if not filename.endswith(".py"):
                continue

            result = _load_app_from_file(str(file_path))
            if result:
                app_instance, var_name = result
                console.print(f"[green]✓ Found application in {filename}[/green]")
                return app_instance, filename, var_name

    console.print("[red]Error: Could not find Application instance[/red]")
    console.print("Create an app.py, main.py, or application.py with an Application instance")
    sys.exit(1)


def _load_app_from_file(filepath: str):
    """
    Load and return Application instance from a Python file.

    Returns:
        tuple: (Application instance, variable_name) or None
    """
    try:
        spec = importlib.util.spec_from_file_location("app_module", filepath)
        if spec and spec.loader:
            module = importlib.util.module_from_spec(spec)
            sys.modules["app_module"] = module
            spec.loader.exec_module(module)

            # Look for Application instance
            for name in dir(module):
                obj = getattr(module, name)
                if isinstance(obj, Application):
                    return obj, name
    except Exception as e:
        console.print(f"[yellow]Warning: Failed to load {filepath}: {e}[/yellow]")

    return None


def _setup_reload_module(filename: str, var_name: str) -> tuple[str, dict[str, str]]:
    """
    Set up environment for reloadable ASGI factory.

    Instead of generating code, we use the asgi_factory module with
    environment variables to configure the application.

    Returns:
        Tuple of (import_path, env_vars)
    """
    # Get the module name from filename (e.g., "app.py" -> "app")
    module_name = filename.replace(".py", "")

    # Environment variables for the factory
    env_vars = {
        "MYFY_APP_MODULE": module_name,
        "MYFY_APP_VAR": var_name,
    }

    # Use the factory function instead of generated code
    return "myfy_cli.asgi_factory:create_app", env_vars


def _resolve_host_and_port(
    host: str | None,
    port: int | None,
    application: Application | None = None,
) -> tuple[str, int]:
    """
    Resolve host and port from CLI args, WebSettings, or defaults.

    Precedence: CLI flags > Environment variables > WebSettings defaults > Hardcoded defaults

    Args:
        host: Host from CLI (None if not provided)
        port: Port from CLI (None if not provided)
        application: Application instance (used to get WebSettings from container)

    Returns:
        Tuple of (host, port)
    """
    # If both provided via CLI, use them
    if host is not None and port is not None:
        return host, port

    # Try to get from WebSettings (respects environment variables)
    if application is not None:
        try:
            web_settings = application.container.get(WebSettings)
            if host is None:
                host = web_settings.host
            if port is None:
                port = web_settings.port
        except Exception:
            pass  # Fall through to defaults

    # If using app_path (no application), try loading WebSettings directly
    if application is None:
        try:
            web_settings = load_settings(WebSettings)
            if host is None:
                host = web_settings.host
            if port is None:
                port = web_settings.port
        except Exception:
            pass  # Fall through to defaults

    # Fall back to hardcoded defaults
    if host is None:
        host = "127.0.0.1"
    if port is None:
        port = 8000

    return host, port


@app.command()
def run(
    host: str | None = typer.Option(None, help="Server host"),
    port: int | None = typer.Option(None, help="Server port"),
    reload: bool = typer.Option(True, help="Enable auto-reload"),
    app_path: str | None = typer.Option(None, help="Path to app (e.g., main:app)"),
):
    """
    Start the development server.

    Runs the ASGI application with uvicorn.
    """
    klyne.track(
        "myfy_run",
        {
            "host": host,
            "port": port,
            "reload": reload,
            "app_path": app_path,
        },
    )
    console.print("🚀 Starting myfy development server...")

    if app_path:
        # Use provided app path
        host, port = _resolve_host_and_port(host, port, application=None)

        uvicorn.run(
            app_path,
            host=host,
            port=port,
            reload=reload,
            log_level="info",
        )
    else:
        # Auto-discover and run
        application, filename, var_name = find_application()

        # Initialize if not already done
        if not application._initialized:
            application.initialize()

        # Get ASGI app from web module
        web_module = None
        for module in application._modules:
            if module.name == "web":
                web_module = module
                break

        if web_module is None:
            console.print("[red]Error: No web module found[/red]")
            console.print("Add WebModule() to your application")
            sys.exit(1)

        # Resolve host and port (respects CLI flags > env vars > WebSettings defaults)
        host, port = _resolve_host_and_port(host, port, application)

        console.print(f"📡 Listening on http://{host}:{port}")
        console.print(f"📦 Loaded {len(application._modules)} module(s)")

        if reload:
            # Set up reloadable module for uvicorn using factory
            import_path, env_vars = _setup_reload_module(filename, var_name)
            console.print("🔄 Reload enabled - watching for file changes")

            # Use subprocess to call uvicorn CLI with environment variables
            # This ensures the worker subprocess has the correct environment
            cmd = [
                "uvicorn",
                import_path,
                "--factory",  # Tell uvicorn to call the function
                "--host",
                host,
                "--port",
                str(port),
                "--reload",
                "--log-level",
                "info",
            ]

            # Merge environment variables
            env = os.environ.copy()
            env.update(env_vars)

            # Run uvicorn via subprocess
            subprocess.run(cmd, env=env, check=True)
        else:
            # When reload is disabled, we can pass the app object directly
            assert web_module is not None  # Already checked above

            # Use centralized lifespan creation
            lifespan = application.create_lifespan()

            asgi_app = web_module.get_asgi_app(application.container, lifespan=lifespan)
            uvicorn.run(
                asgi_app.app,  # Use the underlying Starlette app
                host=host,
                port=port,
                reload=False,
                log_level="info",
            )


@app.command()
def start(
    host: str | None = typer.Option(None, help="Server host"),
    port: int | None = typer.Option(None, help="Server port"),
    workers: int = typer.Option(1, help="Number of worker processes"),
    app_path: str | None = typer.Option(None, help="Path to app (e.g., main:app)"),
):
    """
    Start production server.

    Optimized for production deployments:
    - Automatically sets MYFY_FRONTEND_ENVIRONMENT=production
    - Disables auto-reload
    - Verifies frontend assets are built (if FrontendModule is loaded)
    - Supports multiple worker processes via gunicorn

    Example:
        myfy frontend build
        myfy start --host 0.0.0.0 --port 8000 --workers 4
    """
    klyne.track(
        "myfy_start",
        {
            "host": host,
            "port": port,
            "workers": workers,
            "app_path": app_path,
        },
    )
    console.print("🚀 Starting myfy production server...")

    # Set production environment
    os.environ["MYFY_FRONTEND_ENVIRONMENT"] = "production"

    if app_path:
        # Use provided app path
        host, port = _resolve_host_and_port(host, port, application=None)
    else:
        # Auto-discover application
        application, filename, var_name = find_application()

        # Initialize if not already done
        if not application._initialized:
            application.initialize()

        # Verify frontend assets if FrontendModule is loaded
        _verify_frontend_assets(application)

        # Get ASGI app from web module
        web_module = None
        for module in application._modules:
            if module.name == "web":
                web_module = module
                break

        if web_module is None:
            console.print("[red]Error: No web module found[/red]")
            console.print("Add WebModule() to your application")
            sys.exit(1)

        # Resolve host and port
        host, port = _resolve_host_and_port(host, port, application)

        # Set up app_path for gunicorn/uvicorn
        import_path, env_vars = _setup_reload_module(filename, var_name)
        app_path = import_path

        # Update environment
        os.environ.update(env_vars)

    console.print(f"📡 Listening on http://{host}:{port}")
    console.print(f"👷 Workers: {workers}")

    if workers > 1:
        # Use gunicorn for multiple workers
        _run_with_gunicorn(app_path, host, port, workers)
    else:
        # Use uvicorn for single worker
        _run_with_uvicorn(app_path, host, port)


def _verify_frontend_assets(application: Application):
    """
    Verify that frontend assets are built if FrontendModule is loaded.

    Args:
        application: Application instance to check for FrontendModule

    Raises:
        SystemExit: If FrontendModule is present but assets are not built
    """
    # Check if FrontendModule is loaded
    has_frontend = any(m.name == "frontend" for m in application._modules)

    if not has_frontend:
        return

    # Check for manifest.json
    manifest_path = Path("frontend/static/dist/.vite/manifest.json")

    if not manifest_path.exists():
        console.print("[red]Error: Frontend assets not built[/red]")
        console.print("Run 'myfy frontend build' before starting production server")
        sys.exit(1)

    console.print("[green]✓ Frontend assets verified[/green]")


def _run_with_gunicorn(app_path: str, host: str, port: int, workers: int):
    """
    Run the application with gunicorn for multiple workers.

    Args:
        app_path: Import path to ASGI app (e.g., "myfy_cli.asgi_factory:create_app")
        host: Host to bind to
        port: Port to bind to
        workers: Number of worker processes
    """
    try:
        import gunicorn.app.base  # noqa: F401, PLC0415  # pyright: ignore[reportMissingModuleSource]
    except ImportError:
        console.print("[red]Error: gunicorn not installed[/red]")
        console.print("Install with: pip install gunicorn")
        sys.exit(1)

    # Use subprocess for gunicorn to ensure proper signal handling
    cmd = [
        "gunicorn",
        app_path,
        "--worker-class",
        "uvicorn.workers.UvicornWorker",
        "--workers",
        str(workers),
        "--bind",
        f"{host}:{port}",
        "--access-logfile",
        "-",
        "--error-logfile",
        "-",
        "--log-level",
        "info",
    ]

    console.print(f"🔧 Running: {' '.join(cmd)}")

    try:
        subprocess.run(cmd, check=True)
    except KeyboardInterrupt:
        console.print("\n👋 Shutting down gracefully...")
    except subprocess.CalledProcessError as e:
        console.print(f"[red]Error: Gunicorn failed with exit code {e.returncode}[/red]")
        sys.exit(e.returncode)


def _run_with_uvicorn(app_path: str, host: str, port: int):
    """
    Run the application with uvicorn for a single worker.

    Args:
        app_path: Import path to ASGI app (e.g., "myfy_cli.asgi_factory:create_app")
        host: Host to bind to
        port: Port to bind to
    """
    cmd = [
        "uvicorn",
        app_path,
        "--factory",
        "--host",
        host,
        "--port",
        str(port),
        "--log-level",
        "info",
    ]

    console.print(f"🔧 Running: {' '.join(cmd)}")

    try:
        subprocess.run(cmd, check=True)
    except KeyboardInterrupt:
        console.print("\n👋 Shutting down gracefully...")
    except subprocess.CalledProcessError as e:
        console.print(f"[red]Error: Uvicorn failed with exit code {e.returncode}[/red]")
        sys.exit(e.returncode)


@app.command()
def routes():
    """
    List all registered routes.

    Shows a table of routes with methods, paths, and handler names.
    """
    klyne.track("myfy_routes", {})
    application, _, _ = find_application()

    if not application._initialized:
        application.initialize()

    # Find web module
    web_module = None
    for module in application._modules:
        if module.name == "web":
            web_module = module
            break

    if web_module is None:
        console.print("[yellow]No web module found[/yellow]")
        return

    routes_list = web_module.router.get_routes()

    if not routes_list:
        console.print("[yellow]No routes registered[/yellow]")
        return

    # Create table
    table = Table(title="Registered Routes")
    table.add_column("Method", style="cyan")
    table.add_column("Path", style="magenta")
    table.add_column("Handler", style="green")
    table.add_column("Name", style="yellow")

    for route in routes_list:
        table.add_row(
            route.method.value,
            route.path,
            route.handler.__name__,
            route.name or "-",
        )

    console.print(table)
    console.print(f"\n✨ Total: {len(routes_list)} route(s)")


@app.command()
def modules():
    """
    Show all loaded modules.

    Displays modules and their configuration.
    """
    klyne.track("myfy_modules", {})
    application, _, _ = find_application()

    if not application._initialized:
        application.initialize()

    # Create table
    table = Table(title="Loaded Modules")
    table.add_column("Module", style="cyan")
    table.add_column("Status", style="green")

    for module in application._modules:
        table.add_row(module.name, "loaded")

    console.print(table)
    console.print(f"\n✨ Total: {len(application._modules)} module(s)")


@app.command()
def doctor():
    """
    Validate application configuration.

    Checks for common issues and provides recommendations.
    """
    klyne.track("myfy_doctor", {})
    console.print("🔍 Running myfy doctor...")

    try:
        application, _, _ = find_application()

        # Try to initialize
        application.initialize()

        console.print("[green]✓[/green] Application found and initialized")
        console.print(f"[green]✓[/green] Modules loaded: {len(application._modules)}")

        # Check web module
        has_web = any(m.name == "web" for m in application._modules)
        if has_web:
            console.print("[green]✓[/green] Web module configured")
        else:
            console.print("[yellow]![/yellow] No web module (add WebModule() if you need HTTP)")

        console.print("\n[green]✨ All checks passed![/green]")

    except Exception as e:
        console.print(f"[red]✗ Error: {e}[/red]")
        sys.exit(1)


if __name__ == "__main__":
    app()
