"""CLI entry point for kollab command."""

import argparse
import asyncio
import logging
import re
import shlex
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Optional

if TYPE_CHECKING:
    from .commands.registry import SlashCommandRegistry

# Fix encoding for Windows to support Unicode characters
if sys.platform == "win32":
    # Set UTF-8 mode for stdin/stdout/stderr
    if hasattr(sys.stdout, "reconfigure"):
        sys.stdout.reconfigure(encoding="utf-8", errors="replace")
    if hasattr(sys.stderr, "reconfigure"):
        sys.stderr.reconfigure(encoding="utf-8", errors="replace")
    if hasattr(sys.stdin, "reconfigure") and sys.stdin.isatty():
        sys.stdin.reconfigure(encoding="utf-8", errors="replace")

    # Also set console output code page to UTF-8
    try:
        import ctypes

        kernel32 = ctypes.windll.kernel32
        kernel32.SetConsoleOutputCP(65001)  # UTF-8
        kernel32.SetConsoleCP(65001)  # UTF-8 for input too
    except Exception:
        pass  # Ignore if this fails

# Import from the same directory
from .application import TerminalLLMChat
from .logging import setup_bootstrap_logging
from .version import __version__


def parse_timeout(timeout_str: str) -> int:
    """Parse timeout string into seconds.

    Args:
        timeout_str: Timeout string like "30s", "2min", "1h"

    Returns:
        Timeout in seconds

    Raises:
        ValueError: If timeout format is invalid
    """
    timeout_str = timeout_str.strip().lower()

    # Match pattern like "30s", "2min", "1h"
    match = re.match(
        r"^(\d+(?:\.\d+)?)(s|sec|second|seconds|m|min|minute|minutes|h|hour|hours)$",
        timeout_str,
    )
    if not match:
        raise ValueError(
            f"Invalid timeout format: {timeout_str}. Use format like '30s', '2min', or '1h'"
        )

    value = float(match.group(1))
    unit = match.group(2)

    # Convert to seconds
    if unit in ("s", "sec", "second", "seconds"):
        return int(value)
    elif unit in ("m", "min", "minute", "minutes"):
        return int(value * 60)
    elif unit in ("h", "hour", "hours"):
        return int(value * 3600)
    else:
        raise ValueError(f"Unknown time unit: {unit}")


def build_cli_help_text(command_registry: "SlashCommandRegistry") -> str:
    """Build help text showing CLI-invocable commands.

    Args:
        command_registry: Registry with all registered commands.

    Returns:
        Formatted help text for epilog.
    """
    # Get all non-hidden CLI commands
    cli_commands = []
    for cmd_def in command_registry.get_all_commands():
        if not cmd_def.cli_hidden:
            cli_commands.append(cmd_def)

    if not cli_commands:
        return ""

    # Build help text
    lines = ["\nslash commands (use as --command args...):"]

    # Limit to 50 commands to prevent OOM
    shown = cli_commands[:50]
    for cmd in shown:
        # Build flag list
        if cmd.aliases:
            flags = f"  --{cmd.name}, --{', --'.join(cmd.aliases[:2])}"
        else:
            flags = f"  --{cmd.name}"

        # Format help
        desc = (
            cmd.description[:60] + "..."
            if len(cmd.description) > 60
            else cmd.description
        )
        if len(flags) > 24:
            lines.append(flags)
            lines.append(f"{'':24}{desc}")
        else:
            lines.append(f"{flags:24}{desc}")

    if len(cli_commands) > 50:
        lines.append(f"\n  ... and {len(cli_commands) - 50} more commands")

    return "\n".join(lines)


def print_full_help(
    parser, command_registry: "SlashCommandRegistry", start_time: float = None
) -> None:
    """Print full help including plugin commands.

    Args:
        parser: The argparse parser instance.
        command_registry: Registry with all registered commands.
        start_time: Optional start time for elapsed time display.
    """
    # Print standard argparse help
    parser.print_help()

    # Print plugin commands section
    cli_commands = []
    for cmd_def in command_registry.get_all_commands():
        if not cmd_def.cli_hidden:
            cli_commands.append(cmd_def)

    if cli_commands:
        print("\nPlugin Commands (invoke as kollab --<command>):")
        for cmd in sorted(cli_commands, key=lambda c: c.name):
            if cmd.aliases:
                aliases = f" ({', '.join(cmd.aliases[:2])})"
            else:
                aliases = ""
            name_part = f"--{cmd.name}{aliases}"
            desc = (
                cmd.description[:50] + "..."
                if len(cmd.description) > 50
                else cmd.description
            )
            print(f"  {name_part:36} {desc}")

    # Print render time
    if start_time is not None:
        import time

        elapsed = (time.perf_counter() - start_time) * 1000
        print(f"\n({elapsed:.0f}ms)")


def discover_plugin_args() -> tuple:
    """Discover plugins and collect their CLI arg registrations.

    Returns:
        Tuple of (plugin_classes, discovery) where:
        - plugin_classes: List of plugin class types that may register CLI args
        - discovery: PluginDiscovery object for reuse in app initialization
    """
    from pathlib import Path

    from kollabor_plugins.discovery import PluginDiscovery

    # Determine plugins directory (same logic as application.py)
    package_dir = Path(__file__).parent.parent
    plugins_dir = package_dir / "plugins"
    if not plugins_dir.exists():
        plugins_dir = Path.cwd() / "plugins"

    discovery = PluginDiscovery(plugins_dir)
    plugin_classes = discovery.discover_classes_only()

    return plugin_classes, discovery


def parse_arguments(
    plugin_classes: list = None,
    argv: list[str] = None,
    command_registry: Optional["SlashCommandRegistry"] = None,
):
    """Parse command-line arguments including plugin-registered args.

    Args:
        plugin_classes: Optional list of plugin classes for CLI arg registration.
        argv: Optional list of argument strings to parse (defaults to sys.argv).
        command_registry: Optional command registry for CLI slash command support.

    Returns:
        Parsed arguments namespace
    """
    # Check for -h/--help early - we'll handle it after plugins are initialized
    # so we can show plugin commands in help output
    args_to_check = argv if argv is not None else sys.argv[1:]
    help_requested = "-h" in args_to_check or "--help" in args_to_check

    # Remove help flags so argparse doesn't handle them immediately
    if help_requested:
        args_to_check = [a for a in args_to_check if a not in ("-h", "--help")]
        argv = args_to_check

    # Build CLI help epilog if registry available
    cli_help = ""
    if command_registry:
        cli_help = build_cli_help_text(command_registry)

    parser = argparse.ArgumentParser(
        description="Kollab - Terminal-based LLM chat interface",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        add_help=False,  # We handle -h ourselves to show plugin commands
        epilog=f"""
Examples:
  kollab                                    # Start interactive mode
  kollab "what is 1+1?"                     # Interactive mode with initial message
  kollab -p "what is 1+1?"                  # Pipe mode: process and exit
  kollab --timeout 30s -p "complex query"   # Pipe mode with custom timeout
  kollab --timeout 5min -p "long task"      # Pipe mode with custom timeout
  echo "hello" | kollab -p                  # Pipe mode from stdin
  cat file.txt | kollab -p --timeout 1h     # Process file with 1 hour timeout
  cat code.py | kollab "find issues" -p     # Combine stdin + query (stdin as context)
  git diff | kollab "write commit msg" -p   # Pipe git diff with instruction
  kollab --system-prompt my-prompt.md       # Use custom system prompt
  kollab --agent lint-editor               # Use specific agent
  kollab -a lint-editor                    # Short form for agent
  kollab --profile claude                  # Use specific LLM profile
  kollab -a myagent -s coding -s review    # Agent with multiple skills
  kollab --agent myagent --skill coding    # Agent with skill (long form)
  kollab --reset-config                    # Reset configs to defaults with updated profiles
  kollab --sub list                         # Execute /sub list and exit
  kollab --sub list --stay                  # Execute /sub list, then interactive mode
{cli_help}
        """,
    )

    # Manual help argument (we handle it after plugins are initialized)
    parser.add_argument(
        "-h",
        "--help",
        action="store_true",
        dest="show_help",
        help="Show this help message and exit",
    )

    parser.add_argument(
        "-v",
        "--version",
        action="version",
        version=f"%(prog)s {__version__}",
        help="Show version number and exit",
    )

    parser.add_argument(
        "query",
        nargs="?",
        help=(
            "Message to send (interactive mode) or instruction (pipe mode). "
            "In pipe mode, combines with stdin content."
        ),
    )

    parser.add_argument(
        "-p",
        "--pipe",
        action="store_true",
        help="Pipe mode: process input and exit (requires -p flag or stdin redirect)",
    )

    parser.add_argument(
        "--timeout",
        type=str,
        default="2min",
        help="Timeout for pipe mode processing (e.g., 30s, 2min, 1h). Default: 2min",
    )

    parser.add_argument(
        "--system-prompt",
        type=str,
        default=None,
        metavar="FILE",
        help="Use a custom system prompt file (e.g., --system-prompt my-prompt.md)",
    )

    parser.add_argument(
        "-a",
        "--agent",
        type=str,
        default=None,
        metavar="AGENT",
        help="Use a specific agent (e.g., --agent lint-editor)",
    )

    parser.add_argument(
        "--profile",
        type=str,
        default=None,
        metavar="PROFILE",
        help="Use a specific LLM profile (e.g., --profile claude, --profile openai)",
    )

    parser.add_argument(
        "--save",
        action="store_true",
        default=False,
        help="Save auto-created profile to global config (use with --profile for env-var profiles)",
    )

    parser.add_argument(
        "--local",
        action="store_true",
        default=False,
        help="With --save, save profile to local project config instead of global",
    )

    parser.add_argument(
        "--simple",
        action="store_true",
        default=False,
        help="Use simple text output (no fancy boxes or colors)",
    )

    parser.add_argument(
        "-s",
        "--skill",
        type=str,
        action="append",
        default=None,
        metavar="SKILL",
        help="Load a skill for the active agent (can be used multiple times: -s skill1 -s skill2)",
    )

    parser.add_argument(
        "--font-dir",
        action="store_true",
        help="Print path to bundled Nerd Fonts directory and exit (for use with agg)",
    )

    parser.add_argument(
        "--reset-config",
        action="store_true",
        help="Reset global and local config.json to defaults with updated profiles",
    )

    parser.add_argument(
        "--login",
        type=str,
        choices=["openai"],
        default=None,
        metavar="PROVIDER",
        help="Login via OAuth (e.g., --login openai)",
    )

    # CLI slash command support
    parser.add_argument(
        "--stay",
        action="store_true",
        help="Stay in interactive mode after CLI command executes",
    )

    # Register plugin CLI arguments
    if plugin_classes:
        registered_args = set()  # Track registered argument names to detect conflicts
        for plugin_class in plugin_classes:
            if hasattr(plugin_class, "register_cli_args"):
                try:
                    # Track arguments before registration
                    before_count = len(parser._actions)
                    plugin_class.register_cli_args(parser)
                    # Check for conflicts by detecting new argument names
                    for action in parser._actions[before_count:]:
                        for arg_str in action.option_strings:
                            if arg_str in registered_args:
                                logging.warning(
                                    f"Plugin {plugin_class.__name__} argument conflict: "
                                    f"{arg_str} already registered by another plugin"
                                )
                            registered_args.add(arg_str)
                except Exception as e:
                    logging.warning(
                        f"Plugin {plugin_class.__name__} arg registration failed: {e}"
                    )

    # Parse known args, capture unknown as potential CLI commands
    args, unknown = parser.parse_known_args(argv)

    # Store help request flag and parser for deferred help generation
    args._help_pending = help_requested or getattr(args, "show_help", False)
    args._parser = parser

    # Initialize CLI command storage
    args.cli_command = None
    args._unknown_args = []

    # Process unknown args as potential CLI commands
    if unknown:
        if unknown[0].startswith("--") and len(unknown[0]) > 2:
            cmd_name = unknown[0][2:]  # Strip '--'

            # Validate command name (prevent injection)
            if not cmd_name.replace("-", "").replace("_", "").isalnum():
                parser.error(f"Invalid command name: {cmd_name}")

            # IMPORTANT: Handle positional args that got split between query and unknown
            # due to argparse's greedy positional matching.
            #
            # Case 1: `kollab --permission trust`
            #   -> unknown=['--permission'], query='trust'
            #   -> CLI arg is 'trust', no message
            #
            # Case 2: `kollab "hello" --permission trust`
            #   -> unknown=['--permission', 'trust'], query='hello'
            #   -> CLI arg is 'trust', message is 'hello'
            #
            # Case 3: `kollab --permission trust --stay "hello"`
            #   -> unknown=['--permission', 'hello'], query='trust'
            #   -> CLI arg is 'trust', message is 'hello'
            #   -> Need to detect this by checking argv order!

            cmd_flag = unknown[0]
            non_flag_unknown = [a for a in unknown[1:] if not a.startswith("-")]

            if args.query and len(unknown) == 1:
                # Case 1: Just CLI flag, query is the CLI arg
                unknown.append(args.query)
                args.query = None
            elif args.query and non_flag_unknown:
                # Cases 2 or 3: Both query and extra positionals exist
                # Check original argv order to determine which is which
                argv_to_check = argv if argv is not None else sys.argv[1:]
                try:
                    cmd_pos = argv_to_check.index(cmd_flag)
                    query_pos = argv_to_check.index(args.query)

                    if query_pos > cmd_pos:
                        # Case 3: Query appears AFTER CLI flag in argv
                        # Collect contiguous positional args after cmd_flag from argv
                        # (these are all command args, not user messages)
                        cli_args = []
                        for i in range(cmd_pos + 1, len(argv_to_check)):
                            if argv_to_check[i].startswith("-"):
                                break
                            cli_args.append(argv_to_check[i])

                        unknown = [cmd_flag] + cli_args

                        # Any non-flag unknowns NOT in the contiguous block
                        # are user messages (e.g. after --stay)
                        cli_arg_set = set(cli_args)
                        leftover = [a for a in non_flag_unknown if a not in cli_arg_set]
                        args.query = leftover[0] if leftover else None
                    # else: Case 2 - query is BEFORE CLI flag, it's the user's message
                    # No changes needed, unknown already has the CLI arg
                except ValueError:
                    # Couldn't find positions, leave as-is
                    pass

            if command_registry:
                # Full processing with registry
                cmd_def = command_registry.get_command(cmd_name)

                if cmd_def:
                    if cmd_def.cli_hidden:
                        parser.error(
                            f"Command /{cmd_name} is not available from command line.\n"
                            f"Start kollabor in interactive mode and use /{cmd_name} instead."
                        )

                    # Sanitize arguments
                    sanitized_args = [shlex.quote(arg) for arg in unknown[1:]]

                    args.cli_command = {
                        "name": cmd_def.name,  # Use canonical name
                        "args": sanitized_args,
                        "raw": f"/{cmd_def.name} {' '.join(sanitized_args)}".strip(),
                        "requires_interactive": cmd_def.requires_interactive,
                        "command_def": cmd_def,
                    }
                else:
                    # Unknown command - show available commands
                    available = [
                        c.name
                        for c in command_registry.get_all_commands()
                        if not c.cli_hidden
                    ]
                    available_flags = [f"--{c}" for c in sorted(available[:10])]
                    parser.error(
                        f"Unknown command: --{cmd_name}\n"
                        f"Available commands: {', '.join(available_flags)}\n"
                        f"Run 'kollab -h' to see all"
                    )
            else:
                # No registry yet - store for later processing in application.py
                args._unknown_args = unknown
        else:
            # Unknown args that don't look like CLI commands
            parser.error(f"Unrecognized arguments: {' '.join(unknown)}")

    return args


def process_cli_command(
    args: argparse.Namespace, command_registry: "SlashCommandRegistry"
) -> Optional[dict]:
    """Process CLI command with command registry.

    Called from application.py once command registry is available.

    Args:
        args: Parsed arguments with potential _unknown_args.
        command_registry: Initialized command registry.

    Returns:
        CLI command dict if valid command found, None otherwise.

    Raises:
        ValueError: If command is invalid or not CLI-invocable.
    """
    # Check for stored unknown args
    unknown = getattr(args, "_unknown_args", [])
    if not unknown:
        return args.cli_command  # May already be set

    if not unknown[0].startswith("--") or len(unknown[0]) <= 2:
        raise ValueError(f"Unrecognized arguments: {' '.join(unknown)}")

    cmd_name = unknown[0][2:]  # Strip '--'

    # Validate command name
    if not cmd_name.replace("-", "").replace("_", "").isalnum():
        raise ValueError(f"Invalid command name: {cmd_name}")

    # NOTE: Positional arg fixing already happened in parse_arguments()
    # when _unknown_args was stored. Don't re-apply the fix here.

    # Look up in registry
    cmd_def = command_registry.get_command(cmd_name)

    if not cmd_def:
        available = [
            c.name for c in command_registry.get_all_commands() if not c.cli_hidden
        ]
        available_flags = [f"--{c}" for c in sorted(available[:10])]
        raise ValueError(
            f"Unknown command: --{cmd_name}\n"
            f"Available commands: {', '.join(available_flags)}\n"
            f"Run 'kollab -h' to see all"
        )

    if cmd_def.cli_hidden:
        raise ValueError(
            f"Command /{cmd_name} is not available from command line.\n"
            f"Start kollabor in interactive mode and use /{cmd_name} instead."
        )

    # Sanitize arguments
    sanitized_args = [shlex.quote(arg) for arg in unknown[1:]]

    return {
        "name": cmd_def.name,  # Use canonical name
        "args": sanitized_args,
        "raw": f"/{cmd_def.name} {' '.join(sanitized_args)}".strip(),
        "requires_interactive": cmd_def.requires_interactive,
        "command_def": cmd_def,
    }


def handle_early_plugin_args(args, plugin_classes: list) -> bool:
    """Let plugins handle args that should exit early.

    Args:
        args: Parsed command-line arguments.
        plugin_classes: List of plugin classes.

    Returns:
        True if a plugin requested early exit, False otherwise.
    """
    should_exit = False

    for plugin_class in plugin_classes:
        if hasattr(plugin_class, "handle_early_args"):
            try:
                result = plugin_class.handle_early_args(args)
                # Handle both old-style (bool) and new-style (tuple) returns
                if isinstance(result, tuple):
                    plugin_should_exit, output_message = result
                    if plugin_should_exit and output_message:
                        print(output_message)
                    should_exit = should_exit or plugin_should_exit
                elif result:  # Old-style bool return
                    should_exit = True
            except Exception as e:
                logging.error(
                    f"Plugin {plugin_class.__name__} early arg handler failed: {e}"
                )

    return should_exit


async def async_main() -> None:
    """Main async entry point for the application with proper error handling."""
    import time

    _start_time = time.perf_counter()

    # Check for help flag - use null logging to avoid file creation
    help_mode = "-h" in sys.argv or "--help" in sys.argv

    if help_mode:
        # Use null logging for help mode - no files created
        logging.basicConfig(level=logging.WARNING, handlers=[logging.NullHandler()])
    else:
        # Setup bootstrap logging before application starts
        setup_bootstrap_logging()

    logger = logging.getLogger(__name__)

    # Early plugin discovery for CLI args
    plugin_classes, discovery = discover_plugin_args()

    # Parse all args (core + plugin)
    args = parse_arguments(plugin_classes)
    args._start_time = _start_time  # For timing display in help

    # Handle early-exit args from plugins (like --capture)
    if handle_early_plugin_args(args, plugin_classes):
        return  # Plugin handled it and requested exit

    # Handle --reset-config: reset configs and exit
    if args.reset_config:
        from kollabor_config.config_utils import initialize_config

        global_config = Path.home() / ".kollabor-cli" / "config.json"
        local_config = Path.cwd() / ".kollabor-cli" / "config.json"

        print("Resetting configuration files...")
        initialize_config(force=True)
        print("Configuration reset complete!")
        print(f"  - Global config: {global_config}")
        print(f"  - Local config:  {local_config}")
        return

    # Handle --font-dir: print font directory and exit
    if args.font_dir:
        try:
            from fonts import get_font_dir

            print(get_font_dir())
        except ImportError:
            # Fallback for development mode
            font_dir = Path(__file__).parent.parent / "fonts"
            if font_dir.exists():
                print(font_dir)
            else:
                print("Error: fonts directory not found", file=sys.stderr)
                sys.exit(1)
        return

    # Handle --login: run OAuth flow and exit (or continue to interactive)
    if args.login:
        if not sys.stdin.isatty():
            print("Error: --login requires an interactive terminal", file=sys.stderr)
            sys.exit(1)

        await _handle_cli_login(args.login)
        return

    # Check if we have a CLI command or --help pending
    # These should bypass pipe mode detection
    has_cli_command = (
        getattr(args, "cli_command", None) is not None
        or getattr(args, "_unknown_args", [])
        or getattr(args, "_help_pending", False)
    )

    # Determine if we're in pipe mode and what the input is
    # Pipe mode ONLY when:
    # 1. Explicit -p/--pipe flag is set, OR
    # 2. stdin is not a tty (redirected/piped input) AND no CLI command
    pipe_mode_active = args.pipe or (not sys.stdin.isatty() and not has_cli_command)

    piped_input = None
    initial_message = None

    if pipe_mode_active:
        # Pipe mode: read from query arg and/or stdin, process and exit
        stdin_content = None
        query_text = None

        # Check for piped input from stdin
        if not sys.stdin.isatty():
            stdin_content = sys.stdin.read().strip()

        # Check for query argument
        if args.query:
            query_text = args.query.strip()

        # Combine both if present (stdin as context, query as instruction)
        if stdin_content and query_text:
            piped_input = f"{stdin_content}\n\n{query_text}"
        elif stdin_content:
            piped_input = stdin_content
        elif query_text:
            piped_input = query_text
        else:
            print("Error: No input received from pipe", file=sys.stderr)
            sys.exit(1)
    else:
        # Interactive mode: query arg becomes initial message
        if args.query:
            initial_message = args.query.strip()

    app = None
    try:
        logger.info("Creating application instance...")

        # Create plugin registry once to avoid double discovery
        # Reuse the discovery object from CLI arg parsing
        from kollabor_plugins import PluginRegistry

        plugin_registry = PluginRegistry(discovery.plugins_dir)
        plugin_registry.discovery = discovery  # Reuse discovery from CLI args

        app = TerminalLLMChat(
            args=args,
            system_prompt_file=args.system_prompt,
            agent_name=args.agent,
            profile_name=args.profile,
            save_profile=args.save,
            save_local=args.local,
            skill_names=args.skill,
            plugin_registry=plugin_registry,  # Pass pre-initialized registry
        )
        logger.info("Starting application...")

        if pipe_mode_active and piped_input:
            # Parse timeout for pipe mode
            try:
                timeout_seconds = parse_timeout(args.timeout)
            except ValueError as e:
                print(f"Error: {e}", file=sys.stderr)
                sys.exit(1)

            # Pipe mode: send input and exit after response
            await app.start_pipe_mode(piped_input, timeout=timeout_seconds)
        else:
            # Interactive mode (with optional initial message)
            await app.start(initial_message=initial_message)
    except KeyboardInterrupt:
        # print("\n\nApplication interrupted by user")
        logger.info("Application interrupted by user")
    except Exception as e:
        print(f"\n\nApplication failed to start: {e}")
        logger.error(f"Application startup failed: {type(e).__name__}: {e}")
        # Print helpful error message for common issues
        if "permission" in str(e).lower():
            print(
                "\nTip: Check file permissions and try running with appropriate privileges"
            )
        elif "already in use" in str(e).lower():
            print(
                "\nTip: Another instance may be running. Try closing other applications."
            )
        elif "not found" in str(e).lower():
            print("\nTip: Check that all required dependencies are installed.")
        raise  # Re-raise for full traceback in debug mode
    finally:
        # Ensure cleanup happens even if startup fails (skip if in pipe mode and already cleaned up)
        pipe_mode = getattr(app, "pipe_mode", False) if app else False
        if app and not app._startup_complete and not pipe_mode:
            logger.info("Performing emergency cleanup after startup failure...")
            try:
                await app.cleanup()
            except Exception as cleanup_error:
                logger.error(f"Emergency cleanup failed: {cleanup_error}")
                # print("Warning: Some resources may not have been cleaned up properly")


async def _handle_cli_login(provider: str) -> None:
    """Handle --login flag: run OAuth flow from CLI.

    Args:
        provider: Provider to authenticate with (e.g. "openai").
    """
    if provider != "openai":
        print(f"Error: unsupported login provider: {provider}", file=sys.stderr)
        sys.exit(1)

    try:
        from kollabor_ai.oauth import OAuthTokenStorage, OpenAIOAuthClient
        from kollabor_ai.oauth.openai_oauth import DEVICE_VERIFY_URL

        client = OpenAIOAuthClient()
        storage = OAuthTokenStorage()

        print("\n  prerequisite: enable device code auth in ChatGPT")
        print("    1. go to chatgpt.com")
        print("    2. settings > security (or data controls)")
        print("    3. enable 'device code authorization' / Codex toggle")
        print()

        try:
            # Step 1: Request device code
            device = await client._request_device_code()

            print(f"  open: {DEVICE_VERIFY_URL}")
            print(f"  code: {device.user_code}\n")
            print("  waiting for browser authorization...")

            # Open browser
            import webbrowser

            try:
                webbrowser.open(DEVICE_VERIFY_URL)
            except Exception:
                print("  (could not open browser, use the URL above)")

            # Step 3: Poll for authorization_code
            auth_resp = await client._poll_for_auth_code(device)

            # Step 4: Exchange via PKCE for real tokens
            tokens = await client._exchange_code(
                auth_resp.authorization_code,
                auth_resp.code_verifier,
            )
        finally:
            await client.close()

        # Store tokens
        await storage.store_tokens("openai", tokens)

        # Query available models and pick the best one
        from kollabor_ai.oauth.openai_oauth import (
            pick_best_model,
            query_codex_models,
        )

        print("\n  querying available models...")
        available_models = await query_codex_models(
            tokens.access_token, tokens.account_id
        )
        model = pick_best_model(available_models)

        import time

        remaining = tokens.expires_at - time.time()
        if remaining > 86400:
            exp_str = f"{remaining / 86400:.1f}d"
        else:
            exp_str = f"{remaining / 3600:.1f}h"

        print("\n  logged in via OpenAI OAuth")
        print(f"  model:   {model}")
        if available_models:
            print(f"  available: {', '.join(available_models)}")
        print(f"  expires: {exp_str}")
        print("  run 'kollab' to start chatting\n")

    except Exception as e:
        print(f"\nError: {e}", file=sys.stderr)
        sys.exit(1)


def cli_main() -> None:
    """Synchronous entry point for pip-installed CLI command."""
    try:
        asyncio.run(async_main())
    except KeyboardInterrupt:
        # print("\n\nExited cleanly!")
        pass
    except Exception as e:
        print(f"\n\nFatal error: {e}")
        # Exit with error code for scripts that depend on it
        sys.exit(1)
