"""Main CLI entry point for Navari CLI."""

import asyncio
import contextlib
import json
import traceback
from pathlib import Path
from typing import Annotated

import httpx
import typer
from rich.console import Console
from rich.markdown import Markdown
from rich.table import Table

import navari_cli
from navari_cli.api_client import NavariAPIClient
from navari_cli.auth import acquire_token_silent, browser_oauth_flow, device_code_flow, password_flow
from navari_cli.chat import ChatSession, _print_todo_summary
from navari_cli.config import NavariConfig
from navari_cli.generated.navari_pydantic_ai_server_client import Client
from navari_cli.generated.navari_pydantic_ai_server_client.api.authentication import get_auth_config

# Create main app
app = typer.Typer(
    name="navari",
    help="Navari CLI - Command-line interface for Navari AI Agent",
    no_args_is_help=True,
    rich_markup_mode="rich",
)


def _apply_options(
    ctx: typer.Context,
    server: Annotated[str | None, typer.Option(help="Server URL override")] = None,
    debug: Annotated[bool, typer.Option(help="Enable debug logging")] = False,
    format: Annotated[str | None, typer.Option(help="Output format (plain/table/json)")] = None,
    no_color: Annotated[bool, typer.Option(help="Disable color output")] = False,
):
    """Shared callback for the main app and all sub-command groups.

    At the top level (ctx.parent is None) it resets state so that sequential
    CLI invocations in the same process (e.g. tests) don't leak options.
    """
    if ctx.parent is None:
        # Top-level invocation: reset to defaults before applying
        global_options.server = None
        global_options.debug = False
        global_options.format = "plain"
        global_options.no_color = False
        console.no_color = False
    if server is not None:
        global_options.server = server
    if debug:
        global_options.debug = debug
    if format is not None:
        global_options.format = format
    if no_color:
        global_options.no_color = no_color
        console.no_color = True


conversations_app = typer.Typer(no_args_is_help=True, callback=_apply_options)
files_app = typer.Typer(help="Manage files", no_args_is_help=True, callback=_apply_options)
runs_app = typer.Typer(help="Manage conversation runs", no_args_is_help=True, callback=_apply_options)

app.add_typer(conversations_app, name="conversations", help="Manage conversations (alias: `conv`)")
app.add_typer(conversations_app, name="conv", help="Alias for `conversations`")
app.add_typer(files_app, name="files")
app.add_typer(runs_app, name="runs")

console = Console()


# Global state for options
class GlobalOptions:
    """Global CLI options."""

    server: str | None = None
    debug: bool = False
    format: str = "plain"
    no_color: bool = False


global_options = GlobalOptions()


def _title_from_message(message: str, max_words: int = 6, max_length: int = 50) -> str:
    """Derive a conversation title from the first message (consistent with frontend)."""
    title = " ".join(message.strip().split()[:max_words])
    if len(title) > max_length:
        title = title[: max_length - 3] + "..."
    return title


def _resolve_server() -> str:
    """Resolve server URL from --server flag or last used server."""
    server = global_options.server or NavariConfig.get_last_used_server()
    if not server:
        console.print("[yellow]No server configured.[/yellow] Run [bold]navari login <url>[/bold] first.")
        raise typer.Exit(code=1) from None
    return server


def _make_client(server_url: str, token: str) -> NavariAPIClient:
    """Create API client, using stored verify_ssl setting from login."""
    verify_ssl = NavariConfig.get_server_option(server_url, "verify_ssl", True)
    return NavariAPIClient(server_url, token, verify_ssl=verify_ssl)


async def _resolve_token(server_url: str) -> tuple[str, str]:
    """Load credentials with silent token refresh.

    Tries to refresh the access token using MSAL's cached refresh token.
    If refresh succeeds, updates the stored credentials. Falls back to
    the stored token if refresh fails (e.g. no cached auth config).

    Returns:
        Tuple of (token, user)
    """
    token, user = NavariConfig.load_credentials(server_url)
    auth_config = NavariConfig.load_auth_config(server_url)

    if auth_config:
        cache = NavariConfig.load_msal_cache()
        refreshed = await acquire_token_silent(auth_config, cache, username=user)
        if refreshed:
            token = refreshed
            NavariConfig.save_credentials(server_url, token, user, auth_config)
            NavariConfig.save_msal_cache(cache)

    return token, user


@contextlib.asynccontextmanager
async def _authenticated_client():
    """Resolve server, refresh token, and yield an authenticated API client.

    Handles all credential resolution and error formatting so commands
    can focus on their specific logic.

    Usage:
        async with _authenticated_client() as (client, server_url, user):
            result = await client.list_conversations()
    """
    server_url = _resolve_server()
    token, user = await _resolve_token(server_url)
    async with _make_client(server_url, token) as client:
        yield client, server_url, user


def _handle_command_error(e: Exception) -> None:
    """Print a formatted error and exit. Used by command error handlers."""
    detail = ""
    if isinstance(e, httpx.HTTPStatusError) and hasattr(e.response, "content"):
        try:
            body = json.loads(e.response.content)
            detail = (body.get("error") or body.get("detail") or "") if isinstance(body, dict) else ""
            if not isinstance(detail, str):
                detail = ""
        except (json.JSONDecodeError, TypeError, AttributeError):
            pass
    if detail:
        console.print(f"[red]✗[/red] {e}\n  {detail}")
    else:
        console.print(f"[red]✗[/red] {e}")
    if global_options.debug:
        traceback.print_exc()
    raise typer.Exit(code=1) from None


def _run(coro):
    """Run an async coroutine with standard error handling.

    Bridges async command logic to Typer's sync interface via asyncio.run(),
    catching all exceptions (except typer.Exit/Abort) and formatting them
    via _handle_command_error.
    """
    try:
        return asyncio.run(coro)
    except (typer.Exit, typer.Abort):
        raise
    except Exception as e:
        _handle_command_error(e)


async def _fetch_auth_config(server_url: str, verify_ssl: bool = True) -> dict:
    """Fetch OAuth configuration from the server using the generated client."""
    unauthenticated = Client(
        base_url=server_url,
        verify_ssl=verify_ssl,
        timeout=httpx.Timeout(10),
    )
    response = await get_auth_config.asyncio_detailed(client=unauthenticated)
    if response.status_code != 200:
        raise ConnectionError(
            f"Failed to fetch auth config from {server_url} (HTTP {response.status_code}). Is the server URL correct?"
        )
    return response.parsed.to_dict()


app.callback()(_apply_options)


@app.command()
def version():
    """Show CLI version."""
    console.print(f"Navari CLI v{navari_cli.__version__}")


@app.command()
def login(
    server_url: Annotated[str | None, typer.Argument(help="Server URL (e.g. https://navari.example.com)")] = None,
    device_code: Annotated[bool, typer.Option("--device-code", help="Use device code flow")] = False,
    token: Annotated[str | None, typer.Option("--token", help="Login with a pre-obtained JWT token")] = None,
    username: Annotated[str | None, typer.Option("--username", "-u", help="Username for password login")] = None,
    password: Annotated[str | None, typer.Option("--password", "-p", help="Password for password login")] = None,
    no_verify: Annotated[
        bool, typer.Option("--no-verify", help="Skip SSL verification (remembered for this server)")
    ] = False,
):
    """
    Login to a Navari server.

    Examples:
      navari login https://navari.example.com
      navari login https://navari.example.com --device-code
      navari login https://navari.example.com --token <jwt>
      navari login https://navari.example.com -u user@example.com -p secret
      navari login                              - Re-login to last used server
    """

    async def _impl():
        nonlocal server_url
        # Validate mutually exclusive options
        if sum(bool(x) for x in [device_code, token, username]) > 1:
            console.print("[red]Error:[/red] Use only one of --device-code, --token, or --username/--password")
            raise typer.Exit(code=1) from None
        if (username and not password) or (password and not username):
            console.print("[red]Error:[/red] Both --username and --password are required")
            raise typer.Exit(code=1) from None

        # Resolve server URL — track whether user provided it explicitly
        url_explicit = bool(server_url)
        if not server_url:
            server_url = global_options.server or NavariConfig.get_last_used_server()
        if not server_url:
            console.print("[red]Error:[/red] Server URL required.")
            console.print("Usage: [bold]navari login https://navari.example.com[/bold]")
            raise typer.Exit(code=1) from None

        # Strip trailing slash
        server_url = server_url.rstrip("/")

        console.print(f"[bold]Logging in to {server_url}...[/bold]")

        # Update verify_ssl only when URL is explicit (deliberate login choice).
        # Plain `navari login` (re-login) preserves existing setting.
        if url_explicit or no_verify:
            NavariConfig.set_server_option(server_url, "verify_ssl", not no_verify)

        try:
            if token:
                # Direct token login — skip OAuth, just validate against server
                access_token = token
                auth_config = None
                cache = None
            else:
                # Fetch auth config from server (needed for all OAuth flows)
                auth_config = await _fetch_auth_config(server_url, verify_ssl=not no_verify)
                cache = NavariConfig.load_msal_cache()

                if global_options.debug:
                    console.print(f"[dim]Auth config: {auth_config}[/dim]")

                # Choose authentication flow (all flows populate the MSAL cache)
                if username and password:
                    access_token, _ = await password_flow(auth_config, username, password, cache)
                elif device_code:
                    access_token, _ = await device_code_flow(auth_config, cache)
                else:
                    try:
                        access_token, _ = await browser_oauth_flow(auth_config, cache, server_url=server_url)
                    except Exception as e:
                        console.print(f"[yellow]Browser login failed: {e}[/yellow]")
                        console.print("[yellow]Falling back to device code flow...[/yellow]\n")
                        access_token, _ = await device_code_flow(auth_config, cache)

            # Validate token and get user profile
            async with _make_client(server_url, access_token) as client:
                try:
                    profile = await client.get_user_profile()
                    user_data = profile.get("user", {})
                    user_email = user_data.get("username") or user_data.get("email", "unknown")
                except Exception as e:
                    console.print(f"[yellow]Warning: Could not fetch user profile: {e}[/yellow]")
                    user_email = "authenticated-user"

            # Save credentials, auth config, and MSAL token cache
            NavariConfig.save_credentials(server_url, access_token, user_email, auth_config)
            if cache:
                NavariConfig.save_msal_cache(cache)
            NavariConfig.set_last_used_server(server_url)
            console.print(f"\n[green]✓[/green] Logged in as [bold]{user_email}[/bold]")

        except Exception as e:
            console.print(f"[red]✗[/red] Login failed: {e}")
            if global_options.debug:
                traceback.print_exc()
            raise typer.Exit(code=1) from None

    asyncio.run(_impl())


@app.command()
def logout(
    server_url: Annotated[
        str | None, typer.Argument(help="Server URL to logout from (or all if not specified)")
    ] = None,
):
    """
    Logout from a Navari server.

    Examples:
      navari logout <url>  - Logout from specific server
      navari logout        - Logout from all servers
    """
    try:
        if server_url:
            NavariConfig.delete_credentials(server_url)
            if NavariConfig.get_last_used_server() == server_url:
                NavariConfig.set_last_used_server(None)
            console.print(f"[green]✓[/green] Logged out from {server_url}")
        else:
            NavariConfig.delete_credentials(None)
            NavariConfig.set_last_used_server(None)
            console.print("[green]✓[/green] Logged out from all servers")
    except Exception as e:
        console.print(f"[red]✗[/red] Logout failed: {e}")
        raise typer.Exit(code=1) from None


@app.command()
def whoami(
    server_url: Annotated[str | None, typer.Argument(help="Server URL (uses last used if not specified)")] = None,
):
    """
    Show current logged-in user.

    Examples:
      navari whoami        - Show user for last used server
      navari whoami <url>  - Show user for specific server
    """
    try:
        if server_url is None:
            server_url = global_options.server or NavariConfig.get_last_used_server()
        if not server_url:
            console.print("[yellow]No server configured.[/yellow] Run [bold]navari login <url>[/bold] first.")
            raise typer.Exit(code=1) from None

        token, user = NavariConfig.load_credentials(server_url)

        console.print(f"[bold]Server:[/bold] {server_url}")
        console.print(f"[bold]User:[/bold] {user}")

    except FileNotFoundError:
        console.print("[yellow]Not logged in.[/yellow] Run [bold]navari login <url>[/bold] first.")
        raise typer.Exit(code=1) from None
    except KeyError as e:
        console.print(f"[yellow]{e}[/yellow]")
        raise typer.Exit(code=1) from None


@app.command()
def config(
    server_url: Annotated[str | None, typer.Argument(help="Switch active server to this URL")] = None,
):
    """
    Show or change CLI configuration.

    Examples:
      navari config                                  - Show current config
      navari config https://other.example.com        - Switch active server
    """
    if server_url:
        server_url = server_url.rstrip("/")
        servers = NavariConfig.list_saved_servers()
        if server_url not in servers:
            console.print(f"[red]✗[/red] No credentials for '{server_url}'")
            if servers:
                console.print("[dim]Saved servers:[/dim]")
                for s in servers:
                    console.print(f"  {s}")
            console.print(f"\nRun [bold]navari login {server_url}[/bold] first.")
            raise typer.Exit(code=1) from None

        NavariConfig.set_last_used_server(server_url)
        console.print(f"[green]✓[/green] Switched to [bold]{server_url}[/bold]")

    else:
        console.print(f"[bold]Config directory:[/bold] {NavariConfig.CONFIG_DIR}")

        last_server = NavariConfig.get_last_used_server()
        if last_server:
            console.print(f"[bold]Current server:[/bold]  {last_server}")
        else:
            console.print("[bold]Current server:[/bold]  [dim]not set[/dim]")

        servers = NavariConfig.list_saved_servers()
        if servers:
            console.print(f"\n[bold]Saved servers ({len(servers)}):[/bold]")
            for s in servers:
                marker = " [green]*[/green]" if s == last_server else ""
                console.print(f"  {s}{marker}")
        else:
            console.print("\n[dim]No saved servers. Run [bold]navari login <url>[/bold] to get started.[/dim]")


# ============================================================================
# Chat Command
# ============================================================================


@app.command()
def chat(
    conversation_id: Annotated[str | None, typer.Argument(help="Conversation ID to chat in")] = None,
    new: Annotated[bool, typer.Option("--new", "-n", help="Create a new conversation")] = False,
    title: Annotated[str, typer.Option("--title", "-t", help="Title for new conversation")] = "New Conversation",
    message: Annotated[
        str | None, typer.Option("--message", "-m", help="Send a single message (non-interactive)")
    ] = None,
    file: Annotated[list[str] | None, typer.Option("--file", "-f", help="Attach file(s) to the first message")] = None,
):
    """
    Start an interactive chat session with Navari agent.

    Examples:
      navari chat                                - Resume last conversation or create new
      navari chat <conversation-id>              - Chat in existing conversation
      navari chat --new                          - Start new conversation
      navari chat --new --title "My Task"        - Start new conversation with title
      navari chat -m "Hello"                     - Send a single message
      navari chat -n -f report.pdf -m "Analyze"  - Attach file and send message
      navari chat -n -f a.csv -f b.csv           - Attach multiple files, then chat
    """

    async def _impl():
        nonlocal conversation_id, new, title
        try:
            server_url = _resolve_server()
            token, user = await _resolve_token(server_url)

            # Determine conversation to use
            if new or conversation_id is None:
                if not new and conversation_id is None:
                    # Try to use last conversation, otherwise create new
                    try:
                        async with _make_client(server_url, token) as client:
                            convs = await client.list_conversations(1, 0)
                        if convs:
                            conversation_id = convs[0]["id"]
                            console.print(f"[dim]Resuming: {convs[0]['title']} ({conversation_id})[/dim]")
                        else:
                            new = True
                    except Exception:
                        new = True

                if new:
                    # Derive title from message when using -m (consistent with frontend)
                    if title == "New Conversation" and message:
                        title = _title_from_message(message) or title
                    async with _make_client(server_url, token) as client:
                        conv = await client.create_conversation(title, user_id=user)
                    conversation_id = conv["id"]
                    console.print(
                        f"[green]✓[/green] Created conversation: [bold]{conv['title']}[/bold] ({conversation_id})"
                    )

            else:
                # Explicit conversation ID — fetch title for confirmation
                try:
                    async with _make_client(server_url, token) as client:
                        conv = await client.get_conversation(conversation_id)
                    console.print(f"[dim]Joining: {conv['title']} ({conversation_id})[/dim]")
                except Exception:
                    console.print(f"[dim]Joining: ({conversation_id})[/dim]")

            # Upload any attached files
            file_ids = []
            if file:
                async with _make_client(server_url, token) as client:
                    for f in file:
                        path = Path(f)
                        if not path.exists():
                            console.print(f"[red]Error:[/red] File not found: {f}")
                            raise typer.Exit(code=1) from None
                        result = await client.upload_file(conversation_id, path)
                        fid = result.get("id", result.get("file_id"))
                        file_ids.append(fid)
                        console.print(f"[green]✓[/green] Attached: [bold]{path.name}[/bold]")

            verify_ssl = NavariConfig.get_server_option(server_url, "verify_ssl", True)

            async def _auto_rename(first_message: str):
                """Rename untitled conversations from first message (consistent with frontend)."""
                async with _make_client(server_url, token) as client:
                    conv = await client.get_conversation(conversation_id)
                    if conv.get("title") == "New Conversation":
                        auto_title = _title_from_message(first_message)
                        if auto_title:
                            await client.update_conversation(conversation_id, auto_title)

            session = ChatSession(
                base_url=server_url,
                conversation_id=conversation_id,
                token=token,
                verify_ssl=verify_ssl,
                debug=global_options.debug,
                on_first_message=_auto_rename,
            )

            if message:
                # Non-interactive: send a single message with attachments
                await session._send_query(message, file_ids=file_ids or None)
                try:
                    await _auto_rename(message)
                except Exception:
                    pass  # Non-critical
            else:
                # Interactive REPL - pre-stage any uploaded files
                session.pending_file_ids.extend(file_ids)
                await session.run()

        except FileNotFoundError:
            console.print("[yellow]Not logged in.[/yellow] Run [bold]navari login[/bold] first.")
            raise typer.Exit(code=1) from None
        except KeyError as e:
            console.print(f"[yellow]{e}[/yellow]")
            raise typer.Exit(code=1) from None
        except typer.Exit:
            raise
        except Exception as e:
            console.print(f"[red]Error:[/red] {e}")
            if global_options.debug:
                traceback.print_exc()
            raise typer.Exit(code=1) from None

    asyncio.run(_impl())


# ============================================================================
# Conversation Commands
# ============================================================================


@conversations_app.command("list")
def conversations_list(
    limit: Annotated[int, typer.Option(help="Number of conversations")] = 20,
    offset: Annotated[int, typer.Option(help="Offset for pagination")] = 0,
    format: Annotated[str | None, typer.Option(help="Output format (plain/table/json)")] = None,
):
    """List all conversations."""
    if format is not None:
        global_options.format = format

    async def _impl():
        async with _authenticated_client() as (client, server_url, _):
            conversations = await client.list_conversations(limit, offset)

        # Format output
        if global_options.format == "json":
            console.print_json(json.dumps(conversations))
        elif global_options.format == "table":
            table = Table(title="Conversations")
            table.add_column("ID", style="cyan", no_wrap=True)
            table.add_column("Title", style="white")
            table.add_column("Created", style="dim")
            table.add_column("Status", style="green")

            for conv in conversations:
                table.add_row(
                    conv["id"],
                    conv["title"],
                    conv.get("created_at", "")[:19],
                    conv.get("status", "idle"),
                )
            console.print(table)
        else:  # plain
            for conv in conversations:
                console.print(f"{conv['id']}: {conv['title']}")

    _run(_impl())


@conversations_app.command("create")
def conversations_create(
    title: Annotated[str, typer.Option(help="Conversation title")] = "New Conversation",
    format: Annotated[str | None, typer.Option(help="Output format (plain/table/json)")] = None,
):
    """Create a new conversation."""
    if format is not None:
        global_options.format = format

    async def _impl():
        async with _authenticated_client() as (client, server_url, user):
            conv = await client.create_conversation(title, user_id=user)

        if global_options.format == "json":
            console.print_json(json.dumps(conv))
        else:
            console.print(f"[green]✓[/green] Created conversation: [bold]{conv['title']}[/bold] ({conv['id']})")
            console.print(f"  Title: {conv['title']}")

    _run(_impl())


@conversations_app.command("get")
def conversations_get(
    conversation_id: Annotated[str, typer.Argument(help="Conversation ID")],
    format: Annotated[str | None, typer.Option(help="Output format (plain/table/json)")] = None,
):
    """Get conversation details."""
    if format is not None:
        global_options.format = format

    async def _impl():
        async with _authenticated_client() as (client, server_url, _):
            conv = await client.get_conversation(conversation_id)

        if global_options.format == "json":
            console.print_json(json.dumps(conv))
        else:
            console.print(f"[bold]Conversation:[/bold] {conv['id']}")
            console.print(f"  Title: {conv['title']}")
            console.print(f"  Created: {conv.get('created_at', 'N/A')}")
            console.print(f"  Status: {conv.get('status', 'idle')}")

    _run(_impl())


@conversations_app.command("delete")
def conversations_delete(
    conversation_id: Annotated[str, typer.Argument(help="Conversation ID")],
    yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation")] = False,
):
    """Delete a conversation."""

    async def _impl():
        if not yes:
            confirmed = typer.confirm(f"Delete conversation {conversation_id}?", default=False)
            if not confirmed:
                console.print("[yellow]Cancelled[/yellow]")
                raise typer.Abort()

        async with _authenticated_client() as (client, server_url, _):
            await client.delete_conversation(conversation_id)

        console.print(f"[green]✓[/green] Deleted conversation {conversation_id}")

    _run(_impl())


@conversations_app.command("messages")
def conversations_messages(
    conversation_id: Annotated[str, typer.Argument(help="Conversation ID")],
    limit: Annotated[int, typer.Option(help="Max events to fetch")] = 10000,
    format: Annotated[str | None, typer.Option(help="Output format (plain/json)")] = None,
):
    """Show conversation messages."""
    if format is not None:
        global_options.format = format

    async def _impl():
        """Display conversation messages consistently with the live chat view.
        Renders tool cards, agent messages, todo lists, ask prompts, and results
        the same way the interactive chat session does.
        """
        async with _authenticated_client() as (client, server_url, _):
            events = await client.get_messages(conversation_id, limit=limit)

        if global_options.format == "json":
            console.print_json(json.dumps(events))
            return

        if not events:
            console.print("[dim]No messages in this conversation.[/dim]")
            return

        MESSAGE_TOOLS = {"send_message_to_user"}
        ASK_TOOLS = {"ask_user", "ask_user_with_examples", "ask_user_for_confirmation"}

        for event in events:
            event_kind = event.get("event_kind", "")

            if event_kind == "processing_started":
                query = event.get("query", "")
                if query:
                    console.print(f"\n[bold green]You>[/bold green] {query}")

            elif event_kind == "function_tool_call":
                part = event.get("part", {})
                tool_name = part.get("tool_name", "")
                args_str = part.get("args", "")

                if isinstance(args_str, dict):
                    args = args_str
                elif args_str:
                    try:
                        args = json.loads(args_str)
                    except (json.JSONDecodeError, TypeError):
                        args = {}
                else:
                    args = {}

                if tool_name in MESSAGE_TOOLS:
                    message = args.get("message", "")
                    if message:
                        console.print("\n[bold cyan]Navari>[/bold cyan]")
                        console.print(Markdown(message))

                elif tool_name in ASK_TOOLS:
                    console.print(f'  [dim](use tool "{tool_name}")[/dim]')
                    question = args.get("question", "")
                    if question:
                        console.print(f"\n[bold cyan]Navari>[/bold cyan] {question}")
                    examples = args.get("examples", [])
                    for i, example in enumerate(examples, 1):
                        console.print(f"  [dim]{i}. {example}[/dim]")

                elif tool_name == "write_to_do_file":
                    console.print(f'  [dim](use tool "{tool_name}")[/dim]')
                    content = args.get("content", "")
                    if content:
                        _print_todo_summary(content)

                elif tool_name == "report_file_created":
                    fp = args.get("file_path", "")
                    name = fp.rsplit("/", 1)[-1] if fp else ""
                    console.print(f"  [dim](file created: {name or fp})[/dim]")

                else:
                    console.print(f'  [dim](use tool "{tool_name}")[/dim]')

            elif event_kind == "function_tool_result":
                result = event.get("result", {})
                tool_name = result.get("tool_name", "")
                content = result.get("content", "")
                if tool_name in ASK_TOOLS and content and isinstance(content, str):
                    console.print(f"[bold green]You>[/bold green] {content}")

            elif event_kind == "agent_result":
                summary = event.get("summary", "")
                if summary:
                    console.print(f"\n[bold cyan]Navari>[/bold cyan] {summary}")

        console.print()

    _run(_impl())


# ============================================================================
# File Commands
# ============================================================================


@files_app.command("list")
def files_list(
    conversation_id: Annotated[
        str | None, typer.Option("--conversation", "-c", help="Filter by conversation ID")
    ] = None,
    format: Annotated[str | None, typer.Option(help="Output format (plain/table/json)")] = None,
):
    """List files."""
    if format is not None:
        global_options.format = format

    async def _impl():
        async with _authenticated_client() as (client, server_url, _):
            files = await client.list_files(conversation_id)

        if global_options.format == "json":
            console.print_json(json.dumps(files))
        elif not files:
            console.print("[dim]No files found.[/dim]")
        elif global_options.format == "table":
            table = Table(title="Files")
            table.add_column("ID", style="cyan", no_wrap=True)
            table.add_column("Name", style="white")
            table.add_column("Size", style="dim", justify="right")
            table.add_column("Type", style="dim")

            for f in files:
                size = f.get("size", 0)
                size_str = f"{size:,}" if isinstance(size, int) else str(size)
                table.add_row(
                    str(f.get("id", "")),
                    f.get("filename", f.get("name", "unknown")),
                    size_str,
                    f.get("content_type", f.get("mime_type", "")),
                )
            console.print(table)
        else:
            for f in files:
                name = f.get("filename", f.get("name", "unknown"))
                console.print(f"{f.get('id', '')}: {name}")

    _run(_impl())


@files_app.command("upload")
def files_upload(
    file_path: Annotated[str, typer.Argument(help="Local file path to upload")],
    conversation_id: Annotated[str, typer.Option("--conversation", "-c", help="Conversation ID")],
    format: Annotated[str | None, typer.Option(help="Output format (plain/json)")] = None,
):
    """Upload a file to a conversation."""
    if format is not None:
        global_options.format = format

    async def _impl():
        path = Path(file_path)
        if not path.exists():
            console.print(f"[red]✗[/red] File not found: {file_path}")
            raise typer.Exit(code=1) from None

        async with _authenticated_client() as (client, server_url, _):
            result = await client.upload_file(conversation_id, path)

        if global_options.format == "json":
            console.print_json(json.dumps(result))
        else:
            console.print(f"[green]✓[/green] Uploaded: [bold]{path.name}[/bold]")
            console.print(f"  File ID: {result.get('id', result.get('file_id', 'unknown'))}")

    _run(_impl())


@files_app.command("download")
def files_download(
    file_id: Annotated[str, typer.Argument(help="File ID to download")],
    output: Annotated[str | None, typer.Option("--output", "-o", help="Output file path")] = None,
):
    """Download a file by ID."""

    async def _impl():
        async with _authenticated_client() as (client, server_url, _):
            # Get metadata to determine filename
            metadata = await client.get_file_metadata(file_id)
            filename = metadata.get("filename", metadata.get("name", f"{file_id}"))
            content = await client.download_file(file_id)

        out_path = Path(output) if output else Path(filename)
        out_path.write_bytes(content)
        console.print(f"[green]✓[/green] Downloaded: [bold]{out_path}[/bold] ({len(content):,} bytes)")

    _run(_impl())


@files_app.command("delete")
def files_delete(
    file_id: Annotated[str, typer.Argument(help="File ID to delete")],
    yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation")] = False,
):
    """Delete a file by ID."""

    async def _impl():
        if not yes:
            confirmed = typer.confirm(f"Delete file {file_id}?", default=False)
            if not confirmed:
                console.print("[yellow]Cancelled[/yellow]")
                raise typer.Abort()

        async with _authenticated_client() as (client, server_url, _):
            await client.delete_file(file_id)

        console.print(f"[green]✓[/green] Deleted file {file_id}")

    _run(_impl())


@files_app.command("workspace")
def files_workspace(
    conversation_id: Annotated[str, typer.Argument(help="Conversation ID")],
    output: Annotated[str | None, typer.Option("--output", "-o", help="Output zip file path")] = None,
):
    """Download all workspace files for a conversation as a zip."""

    async def _impl():
        async with _authenticated_client() as (client, server_url, _):
            content = await client.download_workspace(conversation_id)

        out_path = Path(output) if output else Path(f"workspace_{conversation_id[:12]}.zip")
        out_path.write_bytes(content)
        console.print(f"[green]✓[/green] Downloaded workspace: [bold]{out_path}[/bold] ({len(content):,} bytes)")

    _run(_impl())


# ============================================================================
# Run Commands
# ============================================================================


@runs_app.command("list")
def runs_list(
    format: Annotated[str | None, typer.Option(help="Output format (plain/table/json)")] = None,
):
    """List all runs."""
    if format is not None:
        global_options.format = format

    async def _impl():
        async with _authenticated_client() as (client, server_url, _):
            runs = await client.list_runs()

        if global_options.format == "json":
            console.print_json(json.dumps(runs))
        else:
            if isinstance(runs, dict):
                runs_list = runs.get("runs", [runs])
            elif isinstance(runs, list):
                runs_list = runs
            else:
                runs_list = [runs]

            if not runs_list:
                console.print("[dim]No runs found.[/dim]")
                return

            if global_options.format == "table":
                table = Table(title="Runs")
                table.add_column("Conversation ID", style="cyan", no_wrap=True)
                table.add_column("State", style="green")

                for run in runs_list:
                    conv_id = run.get("conversation_id", "")
                    state = run.get("state", run.get("status", run.get("run_state", "")))
                    table.add_row(conv_id, state)
                console.print(table)
            else:
                for run in runs_list:
                    conv_id = run.get("conversation_id", "")
                    state = run.get("state", run.get("status", run.get("run_state", "")))
                    console.print(f"{conv_id}: {state}")

    _run(_impl())


@runs_app.command("status")
def runs_status(
    conversation_id: Annotated[str, typer.Argument(help="Conversation ID")],
    format: Annotated[str | None, typer.Option(help="Output format (plain/json)")] = None,
):
    """Get run status for a conversation."""
    if format is not None:
        global_options.format = format

    async def _impl():
        async with _authenticated_client() as (client, server_url, _):
            status = await client.get_run_status(conversation_id)

        if global_options.format == "json":
            console.print_json(json.dumps(status))
        else:
            console.print(f"[bold]Conversation:[/bold] {conversation_id}")
            for key, value in status.items():
                console.print(f"  {key}: {value}")

    _run(_impl())


@runs_app.command("cancel")
def runs_cancel(
    conversation_id: Annotated[str, typer.Argument(help="Conversation ID")],
):
    """Cancel an active run for a conversation."""

    async def _impl():
        async with _authenticated_client() as (client, server_url, _):
            await client.cancel_run(conversation_id)

        console.print(f"[green]✓[/green] Cancelled run for conversation {conversation_id}")

    _run(_impl())


# Entry point for console script
if __name__ == "__main__":
    app()
