"""CLI entry point for the isard Gencat authentication tool."""

from __future__ import annotations

import os
import subprocess
import sys
from difflib import get_close_matches
from pathlib import Path

import typer
from dotenv import load_dotenv
from rich.console import Console
from rich.table import Table

from isard import IsardError, IsardVdiClient, get_desktops, login_to_gencat
from isard import (
    add_ssh_key,
    get_bastion,
    set_bastion,
    start_desktop,
    stop_desktop,
    wait_desktop,
)
from isard.errors import BookingRequiredError, HttpError
from isard.session_cache import (
    clear_session,
    load_isardvdi_session,
    load_session,
    save_isardvdi_session,
    save_session,
)
from isard.types import AuthSession, Desktop, IsardVdiSession

load_dotenv()

app = typer.Typer(help="Authenticate against the Gencat educational platform.")
console = Console()
err_console = Console(stderr=True)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _credentials(username: str | None, password: str | None) -> tuple[str, str]:
    """Resolve credentials from flags → env vars → interactive prompt."""
    resolved_username: str = (
        username or os.environ.get("GENCAT_USERNAME") or typer.prompt("Username")
    )
    resolved_password: str = (
        password
        or os.environ.get("GENCAT_PASSWORD")
        or typer.prompt("Password", hide_input=True)
    )
    return resolved_username, resolved_password


def _get_auth_session(
    username: str | None, password: str | None, force: bool = False
) -> AuthSession:
    """Get or create auth session."""
    if not force:
        session = load_session()
        if session:
            return session

    resolved_username, resolved_password = _credentials(username, password)
    return login_to_gencat(resolved_username, resolved_password)


def _refresh_isardvdi_session(
    session: AuthSession,
    username: str | None = None,
    password: str | None = None,
) -> IsardVdiSession:
    """Get or refresh IsardVDI session, handling 401 by re-authenticating once."""
    isardvdi_session = load_isardvdi_session()
    if isardvdi_session:
        try:
            # Test the token by making a simple API call with the session
            get_desktops(isardvdi_session)
            return isardvdi_session
        except HttpError as e:
            if e.status_code == 401:
                console.print(
                    "[yellow]Session expired, refreshing...[/yellow]", err=True
                )
                # Clear the invalid session
                isardvdi_path = Path.home() / ".config" / "isard" / "isardvdi.json"
                isardvdi_path.unlink(missing_ok=True)
            else:
                raise

    # Create new IsardVDI session
    client = IsardVdiClient.login(session.user_info.username, "password-not-needed")
    new_isardvdi_session = IsardVdiSession(
        token=client._session.token, username=session.user_info.username
    )
    save_isardvdi_session(new_isardvdi_session)
    return new_isardvdi_session


def _resolve_desktop(desktops: list[Desktop], name: str) -> Desktop:
    """Resolve desktop by name with fuzzy matching."""
    # Exact match (case-insensitive)
    for desktop in desktops:
        if desktop.name.lower() == name.lower():
            return desktop

    # Fuzzy match
    desktop_names = [d.name for d in desktops]
    matches = get_close_matches(name, desktop_names, n=1, cutoff=0.5)
    if matches:
        match_name = matches[0]
        for desktop in desktops:
            if desktop.name == match_name:
                console.print(f"[yellow]Using desktop: {desktop.name}[/yellow]")
                return desktop

    # Substring match (case-insensitive)
    for desktop in desktops:
        if name.lower() in desktop.name.lower():
            console.print(f"[yellow]Using desktop: {desktop.name}[/yellow]")
            return desktop

    # No match found
    available = ", ".join(f'"{d.name}"' for d in desktops)
    typer.echo(f"Desktop '{name}' not found. Available: {available}", err=True)
    raise typer.Exit(1)


# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------


@app.command()
def login(
    username: str | None = typer.Option(None, "--username", "-u", help="Username"),
    password: str | None = typer.Option(None, "--password", "-p", help="Password"),
    force: bool = typer.Option(False, "--force", "-f", help="Force re-authentication"),
) -> None:
    """Authenticate against the Gencat platform."""
    try:
        session = _get_auth_session(username, password, force)
        save_session(session)

        isardvdi_session = _refresh_isardvdi_session(session, username, password)

        console.print(
            f"[green]Authenticated as:[/green] {session.user_info.display_name}"
        )
        console.print(f"[green]Email:[/green] {session.user_info.email}")
        console.print(f"[green]Session expires:[/green] {session.expires_at}")
        console.print("[green]IsardVDI token:[/green] Active")

    except IsardError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)


@app.command()
def logout() -> None:
    """Clear cached authentication sessions."""
    clear_session()
    console.print("[green]Logged out successfully[/green]")


@app.command(name="list")
def list_desktops(
    username: str | None = typer.Option(None, "--username", "-u", help="Username"),
    password: str | None = typer.Option(None, "--password", "-p", help="Password"),
    force: bool = typer.Option(False, "--force", "-f", help="Force re-authentication"),
    hardware: bool = typer.Option(False, "--hardware", "-H", help="Show hardware info"),
) -> None:
    """List virtual desktops."""
    try:
        session = _get_auth_session(username, password, force)
        save_session(session)

        isardvdi_session = _refresh_isardvdi_session(session, username, password)

        desktops = get_desktops(isardvdi_session)

        if hardware:
            with IsardVdiClient(isardvdi_session) as client:
                client.enrich_hardware(desktops)

        if not desktops:
            console.print("[yellow]No desktops found[/yellow]")
            return

        table = Table(title="Virtual Desktops")
        table.add_column("Name", style="bold")
        table.add_column("State")
        table.add_column("IP Address")
        table.add_column("Operating System")

        if hardware:
            table.add_column("CPU")
            table.add_column("Memory")

        for desktop in desktops:
            # Color-code state
            state_color = {
                "Started": "green",
                "Stopped": "red",
                "Starting": "yellow",
                "Stopping": "yellow",
            }.get(desktop.state, "white")

            state_display = f"[{state_color}]{desktop.state}[/{state_color}]"
            ip_display = desktop.ip or "-"
            os_display = desktop.os or "-"

            row = [desktop.name, state_display, ip_display, os_display]

            if hardware:
                cpu_display = f"{desktop.vcpus}" if desktop.vcpus else "-"
                memory_display = f"{desktop.memory_mb} MB" if desktop.memory_mb else "-"
                row.extend([cpu_display, memory_display])

            table.add_row(*row)

        console.print(table)

    except IsardError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)


@app.command()
def start(
    name: str = typer.Argument(..., help="Desktop name"),
    username: str | None = typer.Option(None, "--username", "-u", help="Username"),
    password: str | None = typer.Option(None, "--password", "-p", help="Password"),
    force: bool = typer.Option(False, "--force", "-f", help="Force re-authentication"),
    wait: bool = typer.Option(False, "--wait", "-w", help="Wait for desktop to start"),
    timeout: int = typer.Option(300, help="Timeout in seconds for --wait"),
) -> None:
    """Start a virtual desktop."""
    try:
        session = _get_auth_session(username, password, force)
        save_session(session)

        isardvdi_session = _refresh_isardvdi_session(session, username, password)

        desktops = get_desktops(isardvdi_session)
        desktop = _resolve_desktop(desktops, name)

        if desktop.state == "Started":
            console.print(
                f"[yellow]Desktop '{desktop.name}' is already started[/yellow]"
            )
            return

        console.print(f"[blue]Starting desktop:[/blue] {desktop.name}")

        try:
            new_state = start_desktop(isardvdi_session, desktop.id)
            console.print(f"[green]Desktop state:[/green] {new_state}")
        except BookingRequiredError:
            err_console.print(
                f"[red]Error:[/red] Desktop '{desktop.name}' requires a booking to start"
            )
            raise typer.Exit(1)

        if wait:
            console.print(
                f"[blue]Waiting for desktop to start (timeout: {timeout}s)...[/blue]"
            )
            final_state = wait_desktop(
                isardvdi_session, desktop.id, "Started", timeout, poll_interval=5
            )
            console.print(f"[green]Final state:[/green] {final_state}")

    except IsardError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)


@app.command()
def stop(
    name: str = typer.Argument(..., help="Desktop name"),
    username: str | None = typer.Option(None, "--username", "-u", help="Username"),
    password: str | None = typer.Option(None, "--password", "-p", help="Password"),
    force: bool = typer.Option(False, "--force", "-f", help="Force re-authentication"),
    wait: bool = typer.Option(False, "--wait", "-w", help="Wait for desktop to stop"),
    timeout: int = typer.Option(300, help="Timeout in seconds for --wait"),
) -> None:
    """Stop a virtual desktop."""
    try:
        session = _get_auth_session(username, password, force)
        save_session(session)

        isardvdi_session = _refresh_isardvdi_session(session, username, password)

        desktops = get_desktops(isardvdi_session)
        desktop = _resolve_desktop(desktops, name)

        if desktop.state == "Stopped":
            console.print(
                f"[yellow]Desktop '{desktop.name}' is already stopped[/yellow]"
            )
            return

        console.print(f"[blue]Stopping desktop:[/blue] {desktop.name}")
        new_state = stop_desktop(isardvdi_session, desktop.id)
        console.print(f"[green]Desktop state:[/green] {new_state}")

        if wait:
            console.print(
                f"[blue]Waiting for desktop to stop (timeout: {timeout}s)...[/blue]"
            )
            final_state = wait_desktop(
                isardvdi_session, desktop.id, "Stopped", timeout, poll_interval=5
            )
            console.print(f"[green]Final state:[/green] {final_state}")

    except IsardError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)


@app.command()
def ssh(
    name: str = typer.Argument(..., help="Desktop name"),
    username: str | None = typer.Option(None, "--username", "-u", help="Username"),
    password: str | None = typer.Option(None, "--password", "-p", help="Password"),
    force: bool = typer.Option(False, "--force", "-f", help="Force re-authentication"),
    start_if_stopped: bool = typer.Option(
        False, "--start", help="Start desktop if stopped"
    ),
    ssh_user: str = typer.Option("user", "--user", "-l", help="SSH username"),
    identity: str | None = typer.Option(
        None, "--identity", "-i", help="SSH private key path"
    ),
    dry_run: bool = typer.Option(
        False, "--dry-run", help="Print SSH command without executing"
    ),
) -> None:
    """SSH into a virtual desktop via bastion."""
    try:
        session = _get_auth_session(username, password, force)
        save_session(session)

        isardvdi_session = _refresh_isardvdi_session(session, username, password)

        desktops = get_desktops(isardvdi_session)
        desktop = _resolve_desktop(desktops, name)

        # Check if we need to start the desktop
        if desktop.state != "Started":
            if start_if_stopped:
                console.print(f"[blue]Starting desktop:[/blue] {desktop.name}")
                try:
                    new_state = start_desktop(isardvdi_session, desktop.id)
                    console.print(f"[green]Desktop started:[/green] {new_state}")

                    # Wait for it to be fully started
                    final_state = wait_desktop(
                        isardvdi_session,
                        desktop.id,
                        "Started",
                        timeout=300,
                        poll_interval=5,
                    )
                    console.print(f"[green]Desktop ready:[/green] {final_state}")
                except BookingRequiredError:
                    err_console.print(
                        f"[red]Error:[/red] Desktop '{desktop.name}' requires a booking to start"
                    )
                    raise typer.Exit(1)
            else:
                err_console.print(
                    f"[red]Error:[/red] Desktop '{desktop.name}' is not started. "
                    "Use --start to start it automatically."
                )
                raise typer.Exit(1)

        # Check if bastion is allowed and build SSH command
        with IsardVdiClient(isardvdi_session) as client:
            if not client.is_bastion_allowed():
                err_console.print(
                    "[red]Error:[/red] Bastion access not allowed for your account"
                )
                raise typer.Exit(1)

            # Resolve SSH key
            ssh_key_path: Path
            if identity:
                ssh_key_path = Path(identity)
                if not ssh_key_path.exists():
                    err_console.print(
                        f"[red]Error:[/red] SSH key not found: {identity}"
                    )
                    raise typer.Exit(1)
            else:
                # Try common locations
                home = Path.home()
                for key_file in [".ssh/id_ed25519", ".ssh/id_rsa"]:
                    candidate = home / key_file
                    if candidate.exists():
                        ssh_key_path = candidate
                        break
                else:
                    # Generate new ed25519 key
                    ssh_dir = home / ".ssh"
                    ssh_dir.mkdir(exist_ok=True)
                    ssh_key_path = ssh_dir / "id_ed25519"

                    console.print("[blue]Generating new SSH key...[/blue]")
                    result = subprocess.run(
                        [
                            "ssh-keygen",
                            "-t",
                            "ed25519",
                            "-f",
                            str(ssh_key_path),
                            "-N",
                            "",
                        ],
                        capture_output=True,
                    )

                    if result.returncode != 0:
                        err_console.print(
                            f"[red]Error:[/red] Failed to generate SSH key"
                        )
                        raise typer.Exit(1)

                    console.print(f"[green]Generated SSH key:[/green] {ssh_key_path}")

            # Read public key
            pub_key_path = ssh_key_path.with_suffix(ssh_key_path.suffix + ".pub")
            if not pub_key_path.exists():
                err_console.print(
                    f"[red]Error:[/red] Public key not found: {pub_key_path}"
                )
                raise typer.Exit(1)

            public_key = pub_key_path.read_text().strip()

            # Register public key with bastion
            console.print("[blue]Registering SSH key with bastion...[/blue]")
            bastion_target = add_ssh_key(isardvdi_session, desktop.id, public_key)

            # Build SSH command
            ssh_command = client.ssh_command(bastion_target, ssh_user)

            if dry_run:
                console.print("[blue]SSH command:[/blue]")
                console.print(" ".join(ssh_command))
                return

            # Execute SSH
            console.print(f"[green]Connecting to {desktop.name}...[/green]")
            os.execvp("ssh", ssh_command)

    except IsardError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)


def main() -> None:
    """Main CLI entry point."""
    app()


if __name__ == "__main__":
    main()
