"""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

try:
    from importlib.metadata import version as get_package_version
except ImportError:
    from importlib_metadata import version as get_package_version

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

# Import from the public library API
from isard import (
    IsardError,
    IsardVdiClient,
    create_desktop,
    get_desktops,
    get_templates,
    login_to_gencat,
    add_ssh_key,
    get_bastion,
    set_bastion,
    start_desktop,
    stop_desktop,
    wait_desktop,
)
from isard.errors import BookingRequiredError, HttpError
from isard.installer import (
    InstallerError,
    UnsupportedPlatformError,
    install_virt_viewer,
    check_virt_viewer_status,
    launch_virt_viewer,
)
from isard.session_cache import (
    clear_session,
    load_isardvdi_session,
    load_session,
    save_isardvdi_session,
    save_session,
)
from isard.types import AuthSession, Desktop, IsardVdiSession, Template

load_dotenv()

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


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


def _normalize_username(username: str) -> str:
    """Normalize a username by adding @edu.gencat.cat domain if not present.

    Args:
        username: The raw username input

    Returns:
        Username with @edu.gencat.cat domain if it wasn't already present
    """
    if "@" not in username:
        return f"{username}@edu.gencat.cat"
    return username


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")
    )
    # Auto-complete username with @edu.gencat.cat domain if needed
    resolved_username = _normalize_username(resolved_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_gencat_session(
    username: str | None, password: str | None, force: bool = False
) -> tuple[AuthSession, bool]:
    """Return a valid Gencat SAML session, reusing the cache when possible.

    Returns (session, was_fresh).
    """
    if not force:
        cached = load_session()
        if cached is not None:
            return cached, False

    username, password = _credentials(username, password)
    with console.status("Authenticating with Gencat..."):
        try:
            session = login_to_gencat(username, password)
        except IsardError as exc:
            err_console.print(f"[red]Gencat authentication failed:[/red] {exc}")
            raise typer.Exit(code=1) from exc

    path = save_session(session)
    console.print(f"[dim]Gencat session cached at {path}[/dim]")
    return session, True


def _get_isardvdi_session(
    username: str | None, password: str | None, force: bool = False
) -> tuple[IsardVdiSession, bool]:
    """Return a valid IsardVDI JWT session, reusing the cache when possible.

    Returns (session, was_fresh).
    """
    if not force:
        cached = load_isardvdi_session()
        if cached is not None:
            return cached, False

    username, password = _credentials(username, password)
    with console.status("Authenticating with IsardVDI..."):
        try:
            client = IsardVdiClient.login(username, password)
        except IsardError as exc:
            err_console.print(f"[red]IsardVDI authentication failed:[/red] {exc}")
            raise typer.Exit(code=1) from exc

    session = client.isardvdi_session
    path = save_isardvdi_session(session)
    console.print(f"[dim]IsardVDI session cached at {path}[/dim]")
    return session, True


def _refresh_isardvdi_session(
    username: str | None, password: str | None
) -> IsardVdiSession:
    """Clear the cached IsardVDI session and re-authenticate.

    Used when a cached token is rejected with HTTP 401.
    """
    from isard.session_cache import _isardvdi_session_path

    p = _isardvdi_session_path()
    if p.exists():
        p.unlink()
    console.print("[dim]Cached IsardVDI session expired, re-authenticating...[/dim]")
    session, _ = _get_isardvdi_session(username, password, force=True)
    return session


def _resolve_desktop(
    isardvdi_session: IsardVdiSession,
    name: str,
    *,
    username: str | None,
    password: str | None,
    was_fresh: bool = False,
) -> tuple[Desktop, IsardVdiSession]:
    """Resolve desktop by name with fuzzy matching and session refresh handling.

    Returns (desktop, isardvdi_session) tuple. The session may be refreshed
    if the initial desktop fetch fails with HTTP 401.
    """

    def _get_desktop_by_name(session: IsardVdiSession, target_name: str) -> Desktop:
        """Get desktop by name from the API with fuzzy matching."""
        with console.status("Fetching desktops..."):
            try:
                desktops = get_desktops(session)
            except HttpError as exc:
                if exc.status_code == 401 and not was_fresh:
                    # Session expired, refresh and retry once
                    refreshed_session = _refresh_isardvdi_session(username, password)
                    desktops = get_desktops(refreshed_session)
                    # Update the session reference for the caller
                    nonlocal isardvdi_session
                    isardvdi_session = refreshed_session
                else:
                    raise

        if not desktops:
            err_console.print("[red]No desktops found.[/red]")
            raise typer.Exit(code=1)

        # Exact match (case-insensitive)
        for desktop in desktops:
            if desktop.name.lower() == target_name.lower():
                return desktop

        # Fuzzy match
        desktop_names = [d.name for d in desktops]
        matches = get_close_matches(target_name, desktop_names, n=1, cutoff=0.5)
        if matches:
            match_name = matches[0]
            for desktop in desktops:
                if desktop.name == match_name:
                    return desktop

        # Substring match (case-insensitive)
        for desktop in desktops:
            if target_name.lower() in desktop.name.lower():
                return desktop

        # No match found
        available = ", ".join(f'"{d.name}"' for d in desktops)
        err_console.print(
            f"[red]Desktop '{target_name}' not found.[/red] Available: {available}"
        )
        raise typer.Exit(code=1)

    desktop = _get_desktop_by_name(isardvdi_session, name)
    return desktop, isardvdi_session


def _handle_template_error(exc: IsardError) -> None:
    """Handle template-related errors with user-friendly messages."""
    if isinstance(exc, HttpError):
        if exc.status_code == 401:
            err_console.print(
                "[red]Access Denied:[/red] Your account doesn't have permission to view templates."
            )
            err_console.print(
                "[yellow]Contact your IsardVDI administrator to request template access.[/yellow]"
            )
        elif exc.status_code == 404:
            err_console.print(
                "[red]Templates Not Available:[/red] The templates feature is not accessible."
            )
            err_console.print("[yellow]Possible reasons:[/yellow]")
            err_console.print("  • Your user role doesn't include template access")
            err_console.print(
                "  • Templates are not enabled in this IsardVDI installation"
            )
        else:
            err_console.print(
                f"[red]Failed to fetch templates:[/red] HTTP {exc.status_code}"
            )
    else:
        err_console.print(f"[red]Failed to fetch templates:[/red] {exc}")
    err_console.print(
        "[dim]You can still use other commands like 'list', 'start', 'stop', 'ssh'[/dim]"
    )


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


@app.command()
def login(
    username: str = typer.Option(
        None,
        "--username",
        "-u",
        help="Gencat username (domain auto-added if omitted). Falls back to GENCAT_USERNAME env var.",
    ),
    password: str = typer.Option(
        None,
        "--password",
        "-p",
        help="Gencat password. Falls back to GENCAT_PASSWORD env var.",
        hide_input=True,
    ),
    force: bool = typer.Option(
        False,
        "--force",
        "-f",
        help="Force a fresh login even if a valid cached session exists.",
    ),
) -> None:
    """Log in to Gencat (SAML) and IsardVDI, display session info."""
    gencat_session, gencat_fresh = _get_gencat_session(username, password, force=force)
    isardvdi_session, isardvdi_fresh = _get_isardvdi_session(
        username, password, force=force
    )

    if not gencat_fresh:
        console.print("[dim]Using cached Gencat session.[/dim]")
    if not isardvdi_fresh:
        console.print("[dim]Using cached IsardVDI session.[/dim]")

    ui = gencat_session.user_info

    table = Table(title="Gencat Session", show_header=False, min_width=40)
    table.add_column("Field", style="bold cyan")
    table.add_column("Value")

    table.add_row("Username", ui.username)
    table.add_row("Display name", ui.display_name)
    table.add_row("Email", ui.email)
    if ui.given_name:
        table.add_row("Given name", ui.given_name)
    if ui.family_name:
        table.add_row("Family name", ui.family_name)
    if ui.organization:
        table.add_row("Organization", ui.organization)
    if ui.role:
        table.add_row("Role", ui.role)
    table.add_row("Session ID", gencat_session.session_id)
    table.add_row(
        "Expires at",
        gencat_session.expires_at.strftime("%Y-%m-%d %H:%M:%S UTC"),
    )
    table.add_row(
        "Expires in",
        str(gencat_session.time_until_expiry()).split(".")[0],
    )
    table.add_row(
        "Gencat valid",
        "[green]Yes[/green]" if gencat_session.is_valid() else "[red]No[/red]",
    )
    table.add_row(
        "IsardVDI valid",
        "[green]Yes[/green]" if isardvdi_session.is_valid() else "[red]No[/red]",
    )

    console.print(table)


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


@app.command()
def version() -> None:
    """Show the version of the isard CLI tool."""
    try:
        pkg_version = get_package_version("isard")
        console.print(f"isard [bold]{pkg_version}[/bold]")
    except Exception:
        console.print("isard [yellow]version unknown[/yellow]")


@app.command(name="list")
def desktops(
    username: str = typer.Option(
        None,
        "--username",
        "-u",
        help="Gencat username (domain auto-added if omitted). Falls back to GENCAT_USERNAME env var.",
    ),
    password: str = typer.Option(
        None,
        "--password",
        "-p",
        help="Gencat password. Falls back to GENCAT_PASSWORD env var.",
        hide_input=True,
    ),
    name: str = typer.Option(
        None,
        "--name",
        "-n",
        help="Filter by desktop name.",
    ),
    force: bool = typer.Option(
        False,
        "--force",
        "-f",
        help="Force a fresh login even if a valid cached session exists.",
    ),
    hardware: bool = typer.Option(
        False,
        "--hardware",
        "-H",
        help="Show CPU and memory columns (requires extra API calls).",
    ),
) -> None:
    """List IsardVDI desktops for the authenticated user."""
    isardvdi_session, was_fresh = _get_isardvdi_session(username, password, force=force)

    if not was_fresh:
        console.print("[dim]Using cached IsardVDI session.[/dim]")

    with console.status("Fetching desktops..."):
        try:
            vms = get_desktops(isardvdi_session, name=name)
        except HttpError as exc:
            if exc.status_code == 401 and not was_fresh:
                isardvdi_session = _refresh_isardvdi_session(username, password)
                try:
                    vms = get_desktops(isardvdi_session, name=name)
                except IsardError as exc2:
                    err_console.print(f"[red]Failed to fetch desktops:[/red] {exc2}")
                    raise typer.Exit(code=1) from exc2
            else:
                err_console.print(f"[red]Failed to fetch desktops:[/red] {exc}")
                raise typer.Exit(code=1) from exc
        except IsardError as exc:
            err_console.print(f"[red]Failed to fetch desktops:[/red] {exc}")
            raise typer.Exit(code=1) from exc

    if hardware and vms:
        with console.status("Fetching hardware info..."):
            try:
                IsardVdiClient(isardvdi_session).enrich_hardware(vms)
            except IsardError as exc:
                err_console.print(
                    f"[yellow]Could not fetch hardware info:[/yellow] {exc}"
                )

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

    table = Table(title="Desktops", show_lines=False)
    table.add_column("Name", style="bold")
    table.add_column("State")
    table.add_column("IP")
    table.add_column("OS")
    if hardware:
        table.add_column("CPU", justify="right")
        table.add_column("Memory", justify="right")

    state_colours = {
        "Started": "green",
        "Stopped": "red",
        "Shutting-down": "yellow",
        "WaitingIP": "yellow",
    }

    for vm in vms:
        colour = state_colours.get(vm.state, "white")
        row = [
            vm.name,
            f"[{colour}]{vm.state}[/{colour}]",
            vm.ip or "-",
            vm.os or "-",
        ]
        if hardware:
            row.append(f"{vm.vcpus} vCPU" if vm.vcpus is not None else "-")
            mem = f"{vm.memory_mb} MB" if vm.memory_mb is not None else "-"
            row.append(mem)
        table.add_row(*row)

    console.print(table)


@app.command()
def stop(
    name: str = typer.Argument(..., help="Name of the desktop to stop."),
    username: str = typer.Option(None, "--username", "-u"),
    password: str = typer.Option(None, "--password", "-p", hide_input=True),
    force: bool = typer.Option(False, "--force", "-f", help="Force a fresh login."),
    wait: bool = typer.Option(
        False, "--wait", "-w", help="Wait until the desktop reaches Stopped state."
    ),
    timeout: float = typer.Option(
        120.0,
        "--timeout",
        help="Seconds to wait when --wait is used.",
        show_default=True,
    ),
) -> None:
    """Stop a running desktop."""
    isardvdi_session, was_fresh = _get_isardvdi_session(username, password, force=force)
    desktop, isardvdi_session = _resolve_desktop(
        isardvdi_session,
        name,
        username=username,
        password=password,
        was_fresh=was_fresh,
    )
    if desktop.name != name:
        console.print(f"[dim]Matched:[/dim] {desktop.name!r}")
    with console.status(f"Stopping [bold]{desktop.name}[/bold]..."):
        try:
            new_state = stop_desktop(isardvdi_session, desktop.id)
        except IsardError as exc:
            err_console.print(f"[red]Failed to stop desktop:[/red] {exc}")
            raise typer.Exit(code=1) from exc
    console.print(f"[green]{desktop.name}[/green] → {new_state or 'Stopping'}")

    if wait:
        with console.status(
            f"Waiting for [bold]{desktop.name}[/bold] to reach [bold]Stopped[/bold]..."
        ):
            try:
                final_state = wait_desktop(
                    isardvdi_session, desktop.id, "Stopped", timeout=timeout
                )
            except TimeoutError as exc:
                err_console.print(f"[yellow]Timed out waiting:[/yellow] {exc}")
                raise typer.Exit(code=1) from exc
            except IsardError as exc:
                err_console.print(f"[red]Error while waiting:[/red] {exc}")
                raise typer.Exit(code=1) from exc
        console.print(
            f"[green]{desktop.name}[/green] is now [bold]{final_state}[/bold]"
        )


@app.command()
def start(
    name: str = typer.Argument(..., help="Name of the desktop to start."),
    username: str = typer.Option(None, "--username", "-u"),
    password: str = typer.Option(None, "--password", "-p", hide_input=True),
    force: bool = typer.Option(False, "--force", "-f", help="Force a fresh login."),
    wait: bool = typer.Option(
        False, "--wait", "-w", help="Wait until the desktop reaches Started state."
    ),
    timeout: float = typer.Option(
        120.0,
        "--timeout",
        help="Seconds to wait when --wait is used.",
        show_default=True,
    ),
) -> None:
    """Start a stopped desktop."""
    isardvdi_session, was_fresh = _get_isardvdi_session(username, password, force=force)
    desktop, isardvdi_session = _resolve_desktop(
        isardvdi_session,
        name,
        username=username,
        password=password,
        was_fresh=was_fresh,
    )
    if desktop.name != name:
        console.print(f"[dim]Matched:[/dim] {desktop.name!r}")
    with console.status(f"Starting [bold]{desktop.name}[/bold]..."):
        try:
            new_state = start_desktop(isardvdi_session, desktop.id)
        except BookingRequiredError:
            err_console.print(
                f"[yellow]Cannot start[/yellow] [bold]{desktop.name}[/bold][yellow]:"
                " a booking is required before this desktop can be started.[/yellow]\n"
                "[dim]Visit the IsardVDI web interface to make a booking.[/dim]"
            )
            raise typer.Exit(code=1)
        except IsardError as exc:
            err_console.print(f"[red]Failed to start desktop:[/red] {exc}")
            raise typer.Exit(code=1) from exc
    console.print(f"[green]{desktop.name}[/green] → {new_state or 'Starting'}")

    if wait:
        with console.status(
            f"Waiting for [bold]{desktop.name}[/bold] to reach [bold]Started[/bold]..."
        ):
            try:
                final_state = wait_desktop(
                    isardvdi_session, desktop.id, "Started", timeout=timeout
                )
            except TimeoutError as exc:
                err_console.print(f"[yellow]Timed out waiting:[/yellow] {exc}")
                raise typer.Exit(code=1) from exc
            except IsardError as exc:
                err_console.print(f"[red]Error while waiting:[/red] {exc}")
                raise typer.Exit(code=1) from exc
        console.print(
            f"[green]{desktop.name}[/green] is now [bold]{final_state}[/bold]"
        )


@app.command()
def ssh(
    name: str = typer.Argument(..., help="Name of the desktop to connect to."),
    username: str = typer.Option(None, "--username", "-u"),
    password: str = typer.Option(None, "--password", "-p", hide_input=True),
    force: bool = typer.Option(False, "--force", "-f", help="Force a fresh login."),
    ssh_user: str = typer.Option(
        "user",
        "--user",
        "-l",
        help="Linux username inside the VM.",
        show_default=True,
    ),
    identity: str = typer.Option(
        None,
        "--identity",
        "-i",
        help="Path to SSH private key. Defaults to ~/.ssh/id_ed25519 or ~/.ssh/id_rsa.",
    ),
    auto_start: bool = typer.Option(
        False,
        "--start",
        help="Automatically start the desktop if it is not running.",
    ),
    wait_timeout: float = typer.Option(
        120.0,
        "--timeout",
        help="Seconds to wait for the desktop to start when --start is used.",
        show_default=True,
    ),
    dry_run: bool = typer.Option(
        False,
        "--dry-run",
        help="Print the SSH command without executing it.",
    ),
) -> None:
    """Open an SSH session to a desktop via the IsardVDI bastion."""
    isardvdi_session, was_fresh = _get_isardvdi_session(username, password, force=force)
    desktop, isardvdi_session = _resolve_desktop(
        isardvdi_session,
        name,
        username=username,
        password=password,
        was_fresh=was_fresh,
    )
    if desktop.name != name:
        console.print(f"[dim]Matched:[/dim] {desktop.name!r}")

    # ── Resolve SSH key ───────────────────────────────────────────────────────
    key_path: Path | None = None
    if identity:
        key_path = Path(identity).expanduser()
        if not key_path.exists():
            err_console.print(f"[red]SSH key not found:[/red] {key_path}")
            raise typer.Exit(code=1)
    else:
        for candidate in [Path("~/.ssh/id_ed25519"), Path("~/.ssh/id_rsa")]:
            expanded = candidate.expanduser()
            if expanded.exists():
                key_path = expanded
                break

        if key_path is None:
            # Generate a new ed25519 key.
            default_key = Path("~/.ssh/id_ed25519").expanduser()
            console.print(
                "[dim]No SSH key found — generating ~/.ssh/id_ed25519...[/dim]"
            )
            default_key.parent.mkdir(mode=0o700, exist_ok=True)
            result = subprocess.run(
                [
                    "ssh-keygen",
                    "-t",
                    "ed25519",
                    "-f",
                    str(default_key),
                    "-N",
                    "",
                    "-C",
                    f"{os.environ.get('USER', 'user')}@isard",
                ],
                capture_output=True,
                text=True,
            )
            if result.returncode != 0:
                err_console.print(
                    f"[red]ssh-keygen failed:[/red] {result.stderr.strip()}"
                )
                raise typer.Exit(code=1)
            key_path = default_key
            console.print(f"[green]Generated SSH key:[/green] {key_path}")

    pub_key_path = key_path.with_suffix(".pub")
    if not pub_key_path.exists():
        err_console.print(f"[red]SSH public key not found:[/red] {pub_key_path}")
        raise typer.Exit(code=1)

    public_key = pub_key_path.read_text().strip()

    # ── Ensure desktop is started ─────────────────────────────────────────────
    if desktop.state != "Started":
        if not auto_start:
            err_console.print(
                f"[yellow]Desktop[/yellow] [bold]{desktop.name}[/bold] "
                f"[yellow]is[/yellow] {desktop.state}[yellow]."
                " Use --start to start it automatically.[/yellow]"
            )
            raise typer.Exit(code=1)

        console.print(
            f"[dim]Desktop {desktop.name!r} is {desktop.state} — starting...[/dim]"
        )
        try:
            start_desktop(isardvdi_session, desktop.id)
        except BookingRequiredError:
            err_console.print(
                f"[yellow]Cannot start[/yellow] [bold]{desktop.name}[/bold][yellow]:"
                " a booking is required before this desktop can be started.[/yellow]\n"
                "[dim]Visit the IsardVDI web interface to make a booking.[/dim]"
            )
            raise typer.Exit(code=1)
        except IsardError as exc:
            err_console.print(f"[red]Failed to start desktop:[/red] {exc}")
            raise typer.Exit(code=1) from exc

        with console.status(
            f"Waiting for [bold]{desktop.name}[/bold] to reach [bold]Started[/bold]..."
        ):
            try:
                final_state = wait_desktop(
                    isardvdi_session, desktop.id, "Started", timeout=wait_timeout
                )
            except TimeoutError as exc:
                err_console.print(f"[yellow]Timed out waiting:[/yellow] {exc}")
                raise typer.Exit(code=1) from exc
            except IsardError as exc:
                err_console.print(f"[red]Error while waiting:[/red] {exc}")
                raise typer.Exit(code=1) from exc
        console.print(
            f"[green]{desktop.name}[/green] is now [bold]{final_state}[/bold]"
        )

    # ── Configure bastion ─────────────────────────────────────────────────────
    client = IsardVdiClient(isardvdi_session)

    with console.status(f"Configuring SSH bastion for [bold]{desktop.name}[/bold]..."):
        try:
            target = add_ssh_key(isardvdi_session, desktop.id, public_key)
            if not target.ssh.enabled:
                target = set_bastion(
                    isardvdi_session,
                    desktop.id,
                    ssh_enabled=True,
                    ssh_port=target.ssh.port,
                    authorized_keys=target.ssh.authorized_keys,
                )
        except IsardError as exc:
            err_console.print(f"[red]Failed to configure bastion:[/red] {exc}")
            raise typer.Exit(code=1) from exc

    # ── Build SSH command ─────────────────────────────────────────────────────
    cmd = client.ssh_command(target, ssh_user=ssh_user)
    cmd += ["-i", str(key_path)]

    console.print(f"[dim]Connecting:[/dim] {' '.join(cmd)}")

    if dry_run:
        return

    # Platform-specific SSH execution:
    # - Windows: Use subprocess.call() for proper terminal I/O
    # - UNIX: Use os.execvp() for efficient process replacement
    import platform
    import sys
    if platform.system() == "Windows":
        sys.exit(subprocess.call(cmd))
    else:
        os.execvp(cmd[0], cmd)


@app.command(name="template")
def templates(
    username: str = typer.Option(
        None,
        "--username",
        "-u",
        help="Gencat username (domain auto-added if omitted). Falls back to GENCAT_USERNAME env var.",
    ),
    password: str = typer.Option(
        None,
        "--password",
        "-p",
        help="Gencat password. Falls back to GENCAT_PASSWORD env var.",
        hide_input=True,
    ),
    force: bool = typer.Option(
        False,
        "--force",
        "-f",
        help="Force a fresh login even if a valid cached session exists.",
    ),
    category: str = typer.Option(
        None,
        "--category",
        "-c",
        help="Category UUID to filter templates. Uses default if not provided.",
    ),
    hardware: bool = typer.Option(
        False,
        "--hardware",
        "-H",
        help="Show CPU, memory, and disk columns.",
    ),
    filter_name: str = typer.Option(
        None,
        "--filter",
        help="Filter templates by partial name match (case insensitive).",
    ),
) -> None:
    """List available IsardVDI templates/images for creating desktops."""
    isardvdi_session, was_fresh = _get_isardvdi_session(username, password, force=force)

    if not was_fresh:
        console.print("[dim]Using cached IsardVDI session.[/dim]")

    with console.status("Fetching available templates..."):
        try:
            templates = get_templates(isardvdi_session, category_id=category)
        except HttpError as exc:
            if exc.status_code == 401 and not was_fresh:
                isardvdi_session = _refresh_isardvdi_session(username, password)
                try:
                    templates = get_templates(isardvdi_session, category_id=category)
                except IsardError as exc2:
                    _handle_template_error(exc2)
                    raise typer.Exit(code=1) from exc2
            else:
                _handle_template_error(exc)
                raise typer.Exit(code=1) from exc
        except IsardError as exc:
            _handle_template_error(exc)
            raise typer.Exit(code=1) from exc

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

    # Apply name filter if provided
    if filter_name:
        filter_lower = filter_name.lower()
        filtered_templates = [
            template for template in templates if filter_lower in template.name.lower()
        ]
        if not filtered_templates:
            console.print(
                f"[yellow]No templates found matching filter '{filter_name}'.[/yellow]"
            )
            return
        templates = filtered_templates

    # Set table title based on whether filter is applied
    title = (
        f"Available Templates (filtered by '{filter_name}')"
        if filter_name
        else "Available Templates"
    )
    table = Table(title=title, show_lines=False)
    table.add_column("Name", style="bold")
    table.add_column("Description")
    table.add_column("OS")
    if hardware:
        table.add_column("CPU", justify="right")
        table.add_column("Memory", justify="right")
        table.add_column("Disk", justify="right")

    for template in templates:
        if not template.enabled:
            continue  # Skip disabled templates

        row = [
            template.name,
            template.description or "-",
            template.os or "-",
        ]

        if hardware:
            row.append(f"{template.vcpus} vCPU" if template.vcpus is not None else "-")
            row.append(
                f"{template.memory_mb} MB" if template.memory_mb is not None else "-"
            )
            row.append(
                f"{template.disk_size_gb} GB"
                if template.disk_size_gb is not None
                else "-"
            )

        table.add_row(*row)

    console.print(table)


@app.command()
def create(
    name: str = typer.Argument(..., help="Name for the new desktop"),
    username: str = typer.Option(
        None,
        "--username",
        "-u",
        help="Gencat username (domain auto-added if omitted). Falls back to GENCAT_USERNAME env var.",
    ),
    password: str = typer.Option(
        None,
        "--password",
        "-p",
        help="Gencat password. Falls back to GENCAT_PASSWORD env var.",
        hide_input=True,
    ),
    force: bool = typer.Option(
        False,
        "--force",
        "-f",
        help="Force a fresh login even if a valid cached session exists.",
    ),
    template: str = typer.Option(
        None,
        "--template",
        "-t",
        help="Template name or ID to use. If not specified, will show interactive selection.",
    ),
    description: str = typer.Option(
        None,
        "--description",
        "-d",
        help="Optional description for the new desktop.",
    ),
    category: str = typer.Option(
        None,
        "--category",
        "-c",
        help="Category UUID to filter templates. Uses default if not provided.",
    ),
    filter_name: str = typer.Option(
        None,
        "--filter",
        help="Filter templates by partial name match during interactive selection (case insensitive).",
    ),
) -> None:
    """Create a new desktop from a template."""
    from .types import Template

    # Validate desktop name (IsardVDI requires minimum 4 characters)
    if len(name.strip()) < 4:
        err_console.print(
            "[red]Error:[/red] Desktop name must be at least 4 characters long.\n"
            f"[dim]Provided name '{name}' is only {len(name.strip())} characters.[/dim]\n"
            f"[dim]This is a requirement of the IsardVDI server.[/dim]"
        )
        raise typer.Exit(code=1)

    # Ensure name doesn't contain invalid characters (basic validation)
    invalid_chars = set(name) - set(
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
    )
    if invalid_chars:
        err_console.print(
            "[red]Error:[/red] Desktop name contains invalid characters.\n"
            f"[dim]Invalid characters: {', '.join(sorted(invalid_chars))}[/dim]\n"
            f"[dim]Allowed: letters, numbers, hyphens (-), and underscores (_)[/dim]"
        )
        raise typer.Exit(code=1)

    isardvdi_session, was_fresh = _get_isardvdi_session(username, password, force=force)

    if not was_fresh:
        console.print("[dim]Using cached IsardVDI session.[/dim]")

    # Get available templates
    with console.status("Fetching available templates..."):
        try:
            templates = get_templates(isardvdi_session, category_id=category)
        except HttpError as exc:
            if exc.status_code == 401 and not was_fresh:
                isardvdi_session = _refresh_isardvdi_session(username, password)
                try:
                    templates = get_templates(isardvdi_session, category_id=category)
                except IsardError as exc2:
                    _handle_template_error(exc2)
                    raise typer.Exit(code=1) from exc2
            else:
                _handle_template_error(exc)
                raise typer.Exit(code=1) from exc
        except IsardError as exc:
            _handle_template_error(exc)
            raise typer.Exit(code=1) from exc

    # Filter enabled templates
    enabled_templates = [t for t in templates if t.enabled]

    if not enabled_templates:
        console.print(
            "[yellow]No enabled templates available for desktop creation.[/yellow]"
        )
        raise typer.Exit(code=1)

    # Apply name filter if provided (for interactive selection)
    if filter_name and not template:
        filter_lower = filter_name.lower()
        filtered_templates = [
            t for t in enabled_templates if filter_lower in t.name.lower()
        ]
        if not filtered_templates:
            console.print(
                f"[yellow]No templates found matching filter '{filter_name}'.[/yellow]"
            )
            console.print("\nAll available templates:")
            for t in enabled_templates:
                console.print(f"  • {t.name}")
            raise typer.Exit(code=1)
        enabled_templates = filtered_templates
        console.print(
            f"[dim]Showing templates matching '{filter_name}' ({len(enabled_templates)} found)[/dim]"
        )

    # Template selection logic
    selected_template: Template | None = None

    if template:
        # Try to find template by name or ID (search in original enabled list, not filtered)
        search_list = [
            t for t in templates if t.enabled
        ]  # Use full list for direct template search
        template_lower = template.lower()
        for t in search_list:
            if (
                t.id == template
                or t.name.lower() == template_lower
                or template_lower in t.name.lower()
            ):
                selected_template = t
                break

        if not selected_template:
            console.print(f"[red]Template '{template}' not found.[/red]")
            console.print("\nAvailable templates:")
            for t in search_list:
                console.print(f"  • {t.name} (ID: {t.id})")
            raise typer.Exit(code=1)
    else:
        # Interactive template selection (uses filtered list if filter was applied)
        title = "Available templates"
        if filter_name:
            title += f" (filtered by '{filter_name}')"
        console.print(f"{title}:")
        for i, t in enumerate(enabled_templates, 1):
            desc = f" - {t.description}" if t.description else ""
            os_info = f" [{t.os}]" if t.os else ""
            console.print(f"  {i}. {t.name}{os_info}{desc}")

        while True:
            try:
                choice = typer.prompt(
                    f"Select template (1-{len(enabled_templates)})", type=int, default=1
                )
                if 1 <= choice <= len(enabled_templates):
                    selected_template = enabled_templates[choice - 1]
                    break
                else:
                    console.print(
                        f"[red]Please enter a number between 1 and {len(enabled_templates)}[/red]"
                    )
            except ValueError, EOFError, KeyboardInterrupt:
                console.print("\n[yellow]Desktop creation cancelled.[/yellow]")
                raise typer.Exit(code=1)

    if not selected_template:
        console.print("[red]No template selected.[/red]")
        raise typer.Exit(code=1)

    # Confirm creation
    console.print(f"\n[bold]Creating desktop:[/bold]")
    console.print(f"  Name: {name}")
    console.print(f"  Template: {selected_template.name}")
    console.print(f"  Template ID: {selected_template.id}")
    if description:
        console.print(f"  Description: {description}")

    if not typer.confirm("Proceed with desktop creation?", default=True):
        console.print("[yellow]Desktop creation cancelled.[/yellow]")
        raise typer.Exit(code=1)

    # Create the desktop
    with console.status("Creating desktop..."):
        try:
            new_desktop = create_desktop(
                isardvdi_session, selected_template.id, name, description
            )
        except HttpError as exc:
            if exc.status_code == 401 and not was_fresh:
                # Try refreshing session once
                isardvdi_session = _refresh_isardvdi_session(username, password)
                try:
                    new_desktop = create_desktop(
                        isardvdi_session, selected_template.id, name, description
                    )
                except HttpError as exc2:
                    console.print(
                        f"[red]Failed to create desktop:[/red] HTTP {exc2.status_code}"
                    )
                    if exc2.status_code == 409:
                        console.print(
                            "[yellow]Desktop with this name may already exist.[/yellow]"
                        )
                    elif exc2.status_code == 403:
                        console.print(
                            "[yellow]Quota exceeded or insufficient permissions.[/yellow]"
                        )
                    raise typer.Exit(code=1) from exc2
            else:
                console.print(
                    f"[red]Failed to create desktop:[/red] HTTP {exc.status_code}"
                )
                if exc.status_code == 409:
                    console.print(
                        "[yellow]Desktop with this name may already exist.[/yellow]"
                    )
                elif exc.status_code == 403:
                    console.print(
                        "[yellow]Quota exceeded or insufficient permissions.[/yellow]"
                    )
                elif exc.status_code == 404:
                    console.print(
                        "[yellow]Template not found or no longer available.[/yellow]"
                    )
                raise typer.Exit(code=1) from exc
        except IsardError as exc:
            console.print(f"[red]Failed to create desktop:[/red] {exc}")
            raise typer.Exit(code=1) from exc

    # Show success message
    console.print(
        f"\n[green]✓ Desktop '{new_desktop.name}' created successfully![/green]"
    )
    console.print(f"  ID: {new_desktop.id}")
    console.print(f"  State: {new_desktop.state}")

    # Optionally start the desktop
    if new_desktop.state in ("Stopped", "Ready") and typer.confirm(
        "Start the desktop now?", default=True
    ):
        with console.status("Starting desktop..."):
            try:
                start_desktop(isardvdi_session, new_desktop.id)
                console.print("[green]Desktop is starting...[/green]")
                console.print(
                    "[dim]You can check the status with: uv run python main.py list[/dim]"
                )
            except Exception as exc:
                console.print(
                    f"[yellow]Desktop created but failed to start: {exc}[/yellow]"
                )
                console.print(
                    "[dim]You can start it manually with: uv run python main.py start <name>[/dim]"
                )


@app.command()
def delete(
    name: str = typer.Argument(..., help="Name of the desktop to delete"),
    username: str = typer.Option(
        None,
        "--username",
        "-u",
        help="Gencat username (domain auto-added if omitted). Falls back to GENCAT_USERNAME env var.",
    ),
    password: str = typer.Option(
        None,
        "--password",
        "-p",
        help="Gencat password. Falls back to GENCAT_PASSWORD env var.",
        hide_input=True,
    ),
    force: bool = typer.Option(
        False,
        "--force",
        "-f",
        help="Force a fresh login even if a valid cached session exists.",
    ),
    yes: bool = typer.Option(
        False,
        "--yes",
        "-y",
        help="Skip confirmation prompt.",
    ),
    permanent: bool = typer.Option(
        False,
        "--permanent",
        help="Permanently delete the desktop (cannot be undone). WARNING: This is irreversible!",
    ),
) -> None:
    """Delete a desktop (move to trash or permanent deletion).

    By default, this moves the desktop to the IsardVDI trash where it can be recovered.
    Use --permanent for irreversible deletion that cannot be undone.
    """
    from isard import delete_desktop, delete_desktop_permanent

    isardvdi_session, was_fresh = _get_isardvdi_session(username, password, force=force)
    if not was_fresh:
        console.print("[dim]Using cached IsardVDI session.[/dim]")

    # Resolve desktop name to get the desktop object
    desktop, isardvdi_session = _resolve_desktop(
        isardvdi_session,
        name,
        username=username,
        password=password,
        was_fresh=was_fresh,
    )

    # Safety confirmation unless --yes is used
    if not yes:
        console.print()
        if permanent:
            console.print(
                f"[red][bold]⚠ DANGER: You are about to PERMANENTLY DELETE:[/bold][/red]"
            )
            console.print(f"  Desktop: {desktop.name}")
            console.print(f"  ID: {desktop.id}")
            console.print(f"  State: {desktop.state}")
            console.print()
            console.print("[red][bold]THIS OPERATION CANNOT BE UNDONE![/bold][/red]")

            if not typer.confirm(
                "Are you absolutely certain you want to PERMANENTLY DELETE this desktop?",
                default=False,
            ):
                console.print("[dim]Permanent deletion cancelled.[/dim]")
                raise typer.Exit(code=0)
        else:
            console.print(
                f"[yellow][bold]⚠ WARNING: You are about to move to trash:[/bold][/yellow]"
            )
            console.print(f"  Desktop: {desktop.name}")
            console.print(f"  ID: {desktop.id}")
            console.print(f"  State: {desktop.state}")
            console.print()
            console.print(
                "[yellow]This will move the desktop to trash. Use --permanent for immediate permanent deletion.[/yellow]"
            )

            if not typer.confirm(
                "Do you want to move this desktop to trash?", default=False
            ):
                console.print("[dim]Move to trash cancelled.[/dim]")
                raise typer.Exit(code=0)

    # Perform the deletion
    try:
        if permanent:
            with console.status(f"Permanently deleting desktop '{desktop.name}'..."):
                delete_desktop_permanent(isardvdi_session, desktop.id)
                console.print(
                    f"[green]✓ Desktop '{desktop.name}' has been permanently deleted.[/green]"
                )
        else:
            with console.status(f"Moving desktop '{desktop.name}' to trash..."):
                delete_desktop(isardvdi_session, desktop.id)
                console.print(
                    f"[green]✓ Desktop '{desktop.name}' has been moved to trash.[/green]"
                )
    except HttpError as exc:
        if exc.status_code == 401 and not was_fresh:
            # Session expired, refresh and retry once
            isardvdi_session = _refresh_isardvdi_session(username, password)
            try:
                if permanent:
                    delete_desktop_permanent(isardvdi_session, desktop.id)
                    console.print(
                        f"[green]✓ Desktop '{desktop.name}' has been permanently deleted.[/green]"
                    )
                else:
                    delete_desktop(isardvdi_session, desktop.id)
                    console.print(
                        f"[green]✓ Desktop '{desktop.name}' has been moved to trash.[/green]"
                    )
            except IsardError as retry_exc:
                operation = "permanently delete" if permanent else "move to trash"
                err_console.print(
                    f"[red]Failed to {operation} desktop:[/red] {retry_exc}"
                )
                raise typer.Exit(code=1) from retry_exc
        else:
            operation = "permanently delete" if permanent else "move to trash"
            err_console.print(
                f"[red]Failed to {operation} desktop:[/red] HTTP {exc.status_code}"
            )
            raise typer.Exit(code=1) from exc
    except IsardError as exc:
        operation = "permanently delete" if permanent else "move to trash"
        err_console.print(f"[red]Failed to {operation} desktop:[/red] {exc}")
        raise typer.Exit(code=1) from exc


@app.command()
def view(
    desktop_name: str = typer.Argument(..., help="Name of the desktop to view"),
    username: str | None = typer.Option(
        None, "--username", "-u", help="Gencat username"
    ),
    password: str | None = typer.Option(
        None, "--password", "-p", help="Gencat password"
    ),
    force: bool = typer.Option(False, "--force", "-f", help="Force re-authentication"),
    start: bool = typer.Option(
        False, "--start", help="Auto-start desktop if not running"
    ),
    install: bool = typer.Option(
        True, "--install/--no-install", help="Auto-install remote-viewer if missing"
    ),
    debug: bool = typer.Option(
        False, "--debug/--no-debug", help="Show debug output from remote-viewer"
    ),
) -> None:
    """Open a desktop in remote-viewer using SPICE connection.

    Downloads the SPICE connection file for the specified desktop and launches
    remote-viewer to provide a remote desktop connection. Automatically installs
    the virt-viewer package (which includes remote-viewer) if not available.
    """
    # Get IsardVDI session
    isardvdi_session, was_fresh = _get_isardvdi_session(username, password, force)

    try:
        # Find the desktop
        desktop, isardvdi_session = _resolve_desktop(
            isardvdi_session,
            desktop_name,
            username=username,
            password=password,
            was_fresh=was_fresh,
        )
        if desktop.name != desktop_name:
            console.print(f"[dim]Matched:[/dim] {desktop.name!r}")

        console.print(f"Found desktop: {desktop.name} (State: {desktop.state})")

        # Check if desktop is running
        if desktop.state != "Started":
            if start:
                console.print("Desktop is not running. Starting...")
                try:
                    with console.status(f"Starting desktop '{desktop.name}'..."):
                        start_desktop(isardvdi_session, desktop.id)

                    # Wait for desktop to start
                    console.print("Waiting for desktop to start...")
                    wait_desktop(isardvdi_session, desktop.id, "Started", timeout=120.0)
                    console.print("✅ Desktop started successfully")
                except BookingRequiredError:
                    err_console.print(
                        f"[yellow]Cannot start[/yellow] [bold]{desktop.name}[/bold][yellow]:"
                        " a booking is required before this desktop can be started.[/yellow]\n"
                        "[dim]Visit the IsardVDI web interface to make a booking.[/dim]"
                    )
                    raise typer.Exit(code=1)
                except IsardError as exc:
                    err_console.print(f"[red]Failed to start desktop:[/red] {exc}")
                    raise typer.Exit(code=1) from exc
            else:
                err_console.print(
                    f"[red]Desktop is not running (state: {desktop.state}). "
                    f"Use --start to automatically start it, or start it manually first.[/red]"
                )
                raise typer.Exit(code=1)

        # Check if remote-viewer is installed
        if install:
            try:
                status = check_virt_viewer_status()
                if not status.get("installed") and not status.get("error"):
                    console.print("[yellow]remote-viewer is not installed.[/yellow]")
                    if typer.confirm(
                        "Would you like to install remote-viewer now?", default=True
                    ):
                        install_virt_viewer(force=False, silent=True)
                    else:
                        console.print(
                            "Please install remote-viewer manually to use this feature."
                        )
                        raise typer.Exit(code=1)
            except UnsupportedPlatformError:
                # Unsupported platform, assume remote-viewer is available
                pass

        # Get SPICE file
        console.print("Retrieving SPICE connection file...")
        try:
            with IsardVdiClient(isardvdi_session) as client:
                spice_content = client.get_spice_file(desktop.id)
        except HttpError as exc:
            if exc.status_code == 401 and not was_fresh:
                # Session expired, refresh and retry once
                isardvdi_session = _refresh_isardvdi_session(username, password)
                try:
                    with IsardVdiClient(isardvdi_session) as client:
                        spice_content = client.get_spice_file(desktop.id)
                except IsardError as retry_exc:
                    err_console.print(
                        f"[red]Failed to get SPICE file:[/red] {retry_exc}"
                    )
                    raise typer.Exit(code=1) from retry_exc
            else:
                err_console.print(
                    f"[red]Failed to get SPICE file:[/red] HTTP {exc.status_code}"
                )
                raise typer.Exit(code=1) from exc

        # Launch remote-viewer
        try:
            launch_virt_viewer(spice_content, desktop.name, debug=debug)
        except UnsupportedPlatformError as exc:
            err_console.print(f"[red]Platform not supported:[/red] {exc}")
            # On unsupported platforms, save SPICE file and show instructions
            spice_file = Path(f"{desktop.name}.vv")
            spice_file.write_text(spice_content)
            console.print(f"SPICE file saved as: {spice_file}")
            console.print("Run: remote-viewer " + str(spice_file))
            raise typer.Exit(code=1) from exc
        except InstallerError as exc:
            err_console.print(f"[red]remote-viewer error:[/red] {exc}")
            raise typer.Exit(code=1) from exc

    except HttpError as exc:
        err_console.print(f"[red]API error:[/red] HTTP {exc.status_code}")
        raise typer.Exit(code=1) from exc
    except IsardError as exc:
        err_console.print(f"[red]Error:[/red] {exc}")
        raise typer.Exit(code=1) from exc


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


if __name__ == "__main__":
    main()
