"""Command-line interface for Arc CLI."""

import asyncio
import json
import os
import shlex
import sys
import time
from contextlib import suppress

import click
import yaml
from dotenv import load_dotenv

from arc.core import ArcAgent, SettingsManager
from arc.database import DatabaseError, DatabaseManager, QueryValidationError
from arc.database.services import ServiceContainer
from arc.ml.runtime import MLRuntime
from arc.ui.console import InteractiveInterface
from arc.utils import ConfirmationService
from arc.utils.cli_parsing import OptionParsingError, parse_options
from arc.utils.report import (
    build_issue_url,
    compose_issue_body,
    open_in_browser,
)

# Load environment variables
load_dotenv()


@click.group()
@click.version_option(version="0.1.0")
def cli():
    """Arc CLI - A conversational AI agent for file editing and system operations."""
    pass


@cli.command()
@click.option("-d", "--directory", default=None, help="Set working directory")
@click.option(
    "-k", "--api-key", default=None, help="Arc API key (or set ARC_API_KEY env var)"
)
@click.option(
    "-u",
    "--base-url",
    default=None,
    help="Arc API base URL (or set ARC_BASE_URL env var)",
)
@click.option(
    "-m", "--model", default=None, help="AI model to use (e.g., gpt-4, claude-3-sonnet)"
)
@click.option(
    "--max-tool-rounds", default=400, help="Maximum number of tool execution rounds"
)
def chat(
    directory: str | None,
    api_key: str | None,
    base_url: str | None,
    model: str | None,
    max_tool_rounds: int,
):
    """Start an interactive chat session with Arc CLI."""
    ui = InteractiveInterface()

    # Change directory if specified
    if directory:
        try:
            os.chdir(directory)
        except OSError as e:
            ui.show_system_error(f"Error changing directory to {directory}: {e}")
            sys.exit(1)

    # Get configuration
    settings_manager = SettingsManager()

    # Check raw settings to detect missing values before applying defaults
    raw_settings = settings_manager.load_user_settings()
    missing_model = "model" not in raw_settings and not os.getenv("ARC_MODEL")
    missing_base_url = "baseURL" not in raw_settings and not os.getenv("ARC_BASE_URL")

    api_key = api_key or settings_manager.get_api_key()
    base_url = base_url or settings_manager.get_base_url()
    model = model or settings_manager.get_current_model()

    # Show warnings if using default values
    if missing_model and not click.get_current_context().params.get("model"):
        ui.show_warning(
            "Model not configured. Using default 'gpt-4o'. "
            "Set a model using /config or ARC_MODEL environment variable."
        )

    if missing_base_url and not click.get_current_context().params.get("base_url"):
        ui.show_warning(
            "Base URL not configured. Using default 'https://api.openai.com/v1'. "
            "Set baseURL using /config or ARC_BASE_URL environment variable."
        )

    # Save command line settings if provided
    if api_key and click.get_current_context().params.get("api_key"):
        settings_manager.update_user_setting("apiKey", api_key)
        ui.show_system_success("API key saved to ~/.arc/user-settings.json")

    if base_url and click.get_current_context().params.get("base_url"):
        settings_manager.update_user_setting("baseURL", base_url)
        ui.show_system_success("Base URL saved to ~/.arc/user-settings.json")

    # Initialize database services
    system_db_path = settings_manager.get_system_database_path()
    user_db_path = settings_manager.get_user_database_path()
    db_manager = DatabaseManager(system_db_path, user_db_path)
    services = ServiceContainer(db_manager, artifacts_dir=".arc/artifacts")

    # Run interactive mode
    asyncio.run(
        run_interactive_mode(api_key, base_url, model, max_tool_rounds, services)
    )


async def handle_sql_command(
    query_service, ui, user_input: str, current_database: str
) -> str:
    """Handle SQL command execution using InteractiveQueryService.

    Args:
        query_service: InteractiveQueryService instance
        ui: InteractiveInterface instance
        user_input: Raw user input starting with /sql
        current_database: Current database context ("system" or "user")

    Returns:
        Updated current_database after processing the command
    """
    # Parse the command: /sql use [system|user] OR /sql <query>
    parts = user_input.split(" ", 2)

    if len(parts) < 2:
        ui.show_system_error(
            "SQL command requires arguments. "
            "Usage: /sql use [system|user] OR /sql <query>"
        )
        return current_database

    # Check if this is a database switch command
    if len(parts) >= 3 and parts[1].lower() == "use":
        db_name = parts[2].lower().rstrip(";")  # Support trailing semicolon
        if db_name in ["system", "user"]:
            # Use section printer for indented output matching SQL query style
            db_label = "System DB" if db_name == "system" else "User DB"
            with ui._printer.section(color="blue") as p:
                p.print(f"SQL Query ({db_label})")
                p.print(f"[dim]✓ Switched to {db_name} database[/dim]")
            return db_name
        else:
            ui.show_system_error("Invalid database. Use 'system' or 'user'")
            return current_database

    # Otherwise, treat as a query to execute against current database
    query = " ".join(parts[1:]).strip()

    if not query:
        ui.show_system_error("❌ Empty SQL query provided.")
        return current_database

    try:
        # Execute the query using the current database context
        result = query_service.execute_query(query, current_database)

        # Display results using the UI formatter
        execution_time = getattr(result, "query_execution_time", result.execution_time)
        ui.show_sql_result(result, current_database, execution_time)

    except QueryValidationError as e:
        ui.show_system_error(f"Query Error: {str(e)}")
    except DatabaseError as e:
        ui.show_system_error(f"Database Error: {str(e)}")
    except Exception as e:
        ui.show_system_error(f"Unexpected error executing SQL: {str(e)}")

    return current_database


class CommandError(Exception):
    """Raised when ML command parsing or validation fails."""


def _parse_options(
    args: list[str], spec: dict[str, bool], command_name: str = ""
) -> dict[str, str | bool]:
    """Parse command-line options and convert errors to CommandError.

    This is a wrapper around parse_options that converts OptionParsingError
    to CommandError for consistency with existing error handling.

    Args:
        args: List of argument tokens
        spec: Dict mapping option names to whether they expect values
        command_name: Command name for better error messages

    Returns:
        Dict of parsed options

    Raises:
        CommandError: If parsing fails
    """
    try:
        return parse_options(args, spec, command_name)
    except OptionParsingError as e:
        raise CommandError(str(e)) from e


def _get_ml_tool_config() -> tuple[str, str, str | None]:
    """Get configuration required for ML tools.

    Returns:
        Tuple of (api_key, base_url, model) where:
        - api_key: API key (can be empty for enterprise gateway mode)
        - base_url: Base URL for API
        - model: Model name (optional, may be None)

    Raises:
        CommandError: If neither API key nor base URL is configured

    Note:
        In enterprise gateway environments, only base_url is required.
        The gateway handles authentication, so api_key can be empty.
    """
    settings = SettingsManager()
    api_key = settings.get_api_key() or ""
    base_url = settings.get_base_url()
    model = settings.get_current_model()

    # Require at least one of API key or base URL to be configured
    # Empty API key is allowed if base URL is set (enterprise gateway mode)
    if not api_key and not base_url:
        raise CommandError(
            "API configuration required. Either:\n"
            "  1. Set API key (ARC_API_KEY environment variable or /config)\n"
            "  2. Configure base URL for enterprise gateway (use /config)"
        )

    return api_key, base_url, model


async def handle_ml_command(
    user_input: str,
    ui: InteractiveInterface,
    runtime: "MLRuntime",
    agent: "ArcAgent | None" = None,
) -> None:
    try:
        tokens = shlex.split(user_input)
    except ValueError as e:
        ui.show_system_error(f"Failed to parse command: {e}")
        return

    if not tokens or tokens[0] != "/ml":
        ui.show_system_error("Invalid ML command. Use /ml <subcommand> ...")
        return

    if len(tokens) < 2:
        ui.show_system_error("Usage: /ml <data|model|evaluate|jobs> ...")
        return

    subcommand = tokens[1]
    args = tokens[2:]

    try:
        if subcommand == "jobs":
            _ml_jobs(args, ui, runtime)
        elif subcommand == "model":
            await _ml_model(args, ui, runtime, agent)
        elif subcommand == "evaluate":
            await _ml_evaluate(args, ui, runtime)
        elif subcommand == "data":
            await _ml_data_processing(args, ui, runtime)
        else:
            raise CommandError(f"Unknown ML command: {subcommand}")
    except CommandError as e:
        ui.show_system_error(str(e))
    except Exception as e:
        ui.show_system_error(f"ML command failed: {e}")


def _ml_jobs(args: list[str], ui: InteractiveInterface, runtime: MLRuntime) -> None:
    if not args:
        raise CommandError("Usage: /ml jobs <list|status <job_id>>")

    sub = args[0]
    if sub == "list":
        jobs = runtime.job_service.list_jobs(limit=20)
        rows = []
        for job in jobs:
            job_type = job.type.value if hasattr(job.type, "value") else str(job.type)
            status = (
                job.status.value if hasattr(job.status, "value") else str(job.status)
            )
            rows.append(
                [
                    job.job_id,
                    job_type,
                    status,
                    job.message or "",
                    job.updated_at.isoformat()
                    if hasattr(job.updated_at, "isoformat")
                    else str(job.updated_at),
                ]
            )
        ui.show_table(
            title="ML Jobs",
            columns=["Job ID", "Type", "Status", "Message", "Updated"],
            rows=rows,
        )
    elif sub == "status":
        if len(args) < 2:
            raise CommandError("Usage: /ml jobs status <job_id>")
        job_id = args[1]
        job = runtime.job_service.get_job_by_id(job_id)
        if job is None:
            raise CommandError(f"Job '{job_id}' not found")
        job_type = job.type.value if hasattr(job.type, "value") else str(job.type)
        status = job.status.value if hasattr(job.status, "value") else str(job.status)
        rows = [
            ["Job ID", job.job_id],
            ["Type", job_type],
            ["Status", status],
            ["Message", job.message or ""],
            [
                "Created",
                job.created_at.isoformat()
                if hasattr(job.created_at, "isoformat")
                else str(job.created_at),
            ],
            [
                "Updated",
                job.updated_at.isoformat()
                if hasattr(job.updated_at, "isoformat")
                else str(job.updated_at),
            ],
        ]

        # If this is a training job, show training metrics and TensorBoard info
        if job_type == "train_model" and runtime.services.training_tracking:
            tracking_service = runtime.services.training_tracking

            # Get training run by job_id
            training_run = tracking_service.get_run_by_job_id(job_id)

            if training_run:
                rows.append(["", ""])  # Separator
                rows.append(["Training Run ID", training_run.run_id])
                if training_run.model_id:
                    rows.append(["Model", training_run.model_id])

                # Show TensorBoard info
                if training_run.tensorboard_enabled:
                    rows.append(["TensorBoard", "Enabled"])
                    if training_run.tensorboard_log_dir:
                        rows.append(
                            ["  Log Directory", training_run.tensorboard_log_dir]
                        )
                        logdir = training_run.tensorboard_log_dir
                        rows.append(["  Command", f"tensorboard --logdir {logdir}"])

                # Get latest metrics
                metrics = tracking_service.get_metrics(training_run.run_id, limit=10)

                if metrics:
                    rows.append(["", ""])  # Separator
                    rows.append(["Recent Metrics", f"(latest {len(metrics)})"])

                    # Display all metrics (they're already sorted by most recent first)
                    for metric in metrics:
                        metric_label = (
                            f"{metric.metric_name} ({metric.metric_type.value})"
                        )
                        metric_value = (
                            f"{metric.value:.6f} "
                            f"(epoch {metric.epoch}, step {metric.step})"
                        )
                        rows.append([f"  {metric_label}", metric_value])

        # If this is an evaluation job, show evaluation results and metrics
        if job_type == "evaluate_model":
            # Import here: Only needed when displaying evaluation job status
            from arc.database.services import EvaluationTrackingService

            eval_tracking = EvaluationTrackingService(
                runtime.services.models.db_manager
            )

            # Find evaluation run by job_id
            eval_runs = eval_tracking.list_runs(limit=100)
            eval_run = next((r for r in eval_runs if r.job_id == job_id), None)

            if eval_run:
                rows.append(["", ""])  # Separator
                rows.append(["Evaluation Run ID", eval_run.run_id])
                if eval_run.evaluator_id:
                    rows.append(["Evaluator", eval_run.evaluator_id])
                if eval_run.model_id:
                    rows.append(["Model", eval_run.model_id])
                if eval_run.dataset:
                    rows.append(["Dataset", eval_run.dataset])
                if eval_run.target_column:
                    rows.append(["Target Column", eval_run.target_column])
                if eval_run.prediction_table:
                    rows.append(["Predictions Table", eval_run.prediction_table])

                # Show evaluation metrics
                if eval_run.metrics_result:
                    try:
                        metrics = json.loads(eval_run.metrics_result)
                        rows.append(["", ""])  # Separator
                        rows.append(["Evaluation Metrics", ""])
                        for metric_name, metric_value in metrics.items():
                            if isinstance(metric_value, (int, float)):
                                rows.append([f"  {metric_name}", f"{metric_value:.6f}"])
                            else:
                                rows.append([f"  {metric_name}", str(metric_value)])
                    except Exception:  # noqa: S110
                        rows.append(["Metrics", eval_run.metrics_result])

                # Show error if failed
                if eval_run.error_message:
                    rows.append(["", ""])  # Separator
                    rows.append(["Error", eval_run.error_message])

        ui.show_key_values("Job Status", rows)
    else:
        raise CommandError(f"Unknown jobs subcommand: {sub}")


async def _ml_model(
    args: list[str],
    ui: InteractiveInterface,
    runtime: "MLRuntime",
    agent: "ArcAgent | None" = None,  # noqa: ARG001
) -> None:
    """Handle model specification generation command."""
    options = _parse_options(
        args,
        {
            "name": True,
            "instruction": True,
            "data-table": True,
            "target-column": True,  # Target column for task-aware generation
            "plan-id": True,  # ML plan ID to use for guidance
        },
        command_name="/ml model",
    )

    name = options.get("name")
    instruction = options.get("instruction")
    data_table = options.get("data-table")
    target_column = options.get("target-column")
    plan_id = options.get("plan-id")

    # If plan-id is provided, fetch the plan from database
    ml_plan = None
    if plan_id:
        try:
            # Fetch plan from database
            db_plan = runtime.services.ml_plans.get_plan_by_id(str(plan_id))
            if not db_plan:
                raise CommandError(f"Plan '{plan_id}' not found in database")

            # Parse YAML to dict for the tool
            ml_plan = yaml.safe_load(db_plan.plan_yaml)
            ml_plan["plan_id"] = db_plan.plan_id  # Ensure plan_id is in the dict

            ui.show_info(f"Using ML plan: {plan_id}")
        except Exception as e:
            raise CommandError(f"Failed to load plan '{plan_id}': {e}") from e

    # Validate required parameters
    if not name:
        raise CommandError("/ml model requires --name")

    if not data_table:
        raise CommandError("/ml model requires --data-table")

    # instruction is optional when using a plan
    if not ml_plan and not instruction:
        raise CommandError("/ml model requires --instruction when not using --plan-id")

    try:
        # Import here: MLModelTool only needed for /ml model command
        from arc.tools.ml import MLModelTool

        # Get settings for tool initialization
        api_key, base_url, model = _get_ml_tool_config()

        # Initialize TensorBoard manager for the tool
        tensorboard_manager = None
        try:
            from arc.ml import TensorBoardManager

            tensorboard_manager = TensorBoardManager()
        except Exception:
            tensorboard_manager = None

        # Create the tool with proper dependencies
        tool = MLModelTool(
            runtime.services,
            runtime,
            api_key,
            base_url,
            model,
            ui,
            tensorboard_manager,
        )

        # Execute the tool with confirmation workflow
        result = await tool.execute(
            name=name,
            instruction=instruction,
            data_table=data_table,
            target_column=target_column,
            plan_id=plan_id if ml_plan else None,  # Pass plan ID if available
        )

        if not result.success:
            raise CommandError(f"Model generation failed: {result.error}")

    except Exception as exc:
        raise CommandError(f"Unexpected error during model generation: {exc}") from exc


async def _ml_evaluate(
    args: list[str], ui: InteractiveInterface, runtime: "MLRuntime"
) -> None:
    """Handle model evaluation command."""
    options = _parse_options(
        args,
        {
            "model-id": True,
            "data-table": True,
            "metrics": True,  # Optional comma-separated list
            "output-table": True,  # Optional table for predictions
        },
    )

    model_id = options.get("model-id")
    data_table = options.get("data-table")
    metrics_str = options.get("metrics")
    output_table = options.get("output-table")

    if not model_id or not data_table:
        raise CommandError("/ml evaluate requires --model-id and --data-table")

    # Parse metrics if provided
    metrics = None
    if metrics_str:
        metrics = [m.strip() for m in str(metrics_str).split(",")]

    tensorboard_manager = None
    evaluation_succeeded = False
    try:
        # Import here: MLEvaluateTool only needed for /ml evaluate command
        from arc.tools.ml import MLEvaluateTool

        # Initialize TensorBoard manager for the tool
        try:
            # Import here: TensorBoardManager is optional and heavy dependency
            from arc.ml import TensorBoardManager

            tensorboard_manager = TensorBoardManager()
        except Exception:
            tensorboard_manager = None

        # Create the tool with proper dependencies (no API key needed for evaluation)
        tool = MLEvaluateTool(runtime.services, runtime, ui, tensorboard_manager)

        # Execute the tool: create evaluator and run evaluation
        result = await tool.execute(
            model_id=model_id,
            data_table=data_table,
            metrics=metrics,
            output_table=output_table,
        )

        if not result.success:
            ui.show_system_error(result.error or "Evaluation failed")
        else:
            # Mark success to keep TensorBoard running
            evaluation_succeeded = True

    except Exception as exc:
        raise CommandError(f"Unexpected error during evaluation: {exc}") from exc
    finally:
        # Clean up TensorBoard on failure (keep running on success for user access)
        if tensorboard_manager and not evaluation_succeeded:
            try:
                stopped_count = tensorboard_manager.stop_all()
                if stopped_count > 0:
                    ui.show_warning(
                        f"Stopped {stopped_count} TensorBoard process(es) "
                        "due to failure"
                    )
            except Exception:
                # Don't fail cleanup if TensorBoard stop fails
                pass


async def _ml_data_processing(
    args: list[str], ui: InteractiveInterface, runtime: "MLRuntime"
) -> None:
    """Handle data processing pipeline generation command.

    Generates a new data processor from natural language and registers it to the
    data_processors system table. The processor is then automatically executed.
    """
    options = _parse_options(
        args,
        {
            "name": True,
            "instruction": True,
            "data-source-type": True,
            "data-sources": True,
            "plan-id": True,
            "target-db": True,
        },
        command_name="/ml data",
    )

    name = options.get("name")
    instruction = options.get("instruction")
    data_source_type = options.get("data-source-type")
    data_sources_str = options.get("data-sources")
    plan_id = options.get("plan-id")
    database = options.get("target-db", "user")  # Keep CLI option as --target-db

    # Validate required parameters
    if not name:
        raise CommandError("/ml data requires --name")

    if not instruction:
        raise CommandError("/ml data requires --instruction")

    if not data_source_type:
        raise CommandError(
            "/ml data requires --data-source-type "
            "('csv', 'parquet', 'json', or 'table')"
        )

    valid_types = ["csv", "parquet", "json", "table"]
    if data_source_type not in valid_types:
        raise CommandError(
            f"Invalid data-source-type: {data_source_type}. "
            f"Must be one of: {', '.join(valid_types)}"
        )

    if not data_sources_str:
        hints = {
            "csv": "CSV file paths or URLs (e.g., 'data.csv,https://example.com/data.csv')",
            "parquet": "Parquet file paths or URLs (e.g., 'data.parquet')",
            "json": "JSON file paths or URLs (e.g., 'data.json')",
            "table": "table names (e.g., 'users,transactions')",
        }
        hint = hints.get(data_source_type, "data sources")
        raise CommandError(
            f"/ml data requires --data-sources to narrow the scope. "
            f"For data-source-type='{data_source_type}', provide {hint}"
        )

    # Validate database
    if database not in ["system", "user"]:
        raise CommandError(
            "Invalid database. Use --target-db system or --target-db user"
        )

    # Parse data sources (comma-separated list)
    data_sources = [s.strip() for s in str(data_sources_str).split(",")]

    # If plan-id is provided, fetch the plan from database
    ml_plan = None
    if plan_id:
        try:
            # Fetch plan from database
            db_plan = runtime.services.ml_plans.get_plan_by_id(str(plan_id))
            if not db_plan:
                raise CommandError(f"Plan '{plan_id}' not found in database")

            # Parse YAML to dict for the tool
            ml_plan = yaml.safe_load(db_plan.plan_yaml)
            ml_plan["plan_id"] = db_plan.plan_id

            ui.show_info(f"Using ML plan: {plan_id}")
        except Exception as e:
            raise CommandError(f"Failed to load plan '{plan_id}': {e}") from e

    try:
        # Use the MLDataTool for generation with confirmation workflow
        from arc.tools.ml_data import MLDataTool

        # Get settings for tool initialization
        api_key, base_url, model = _get_ml_tool_config()

        # Create the tool with proper dependencies
        tool = MLDataTool(runtime.services, api_key, base_url, model, ui)

        # Execute the tool with confirmation workflow
        # This will generate YAML, register to database, and execute the pipeline
        result = await tool.execute(
            name=name,
            instruction=instruction,
            data_source_type=data_source_type,
            data_sources=data_sources,
            database=database,
            plan_id=plan_id if ml_plan else None,
        )

        if not result.success:
            raise CommandError(f"Data processor generation failed: {result.error}")

    except Exception as exc:
        raise CommandError(
            f"Unexpected error during data processor generation: {exc}"
        ) from exc


async def run_interactive_mode(
    api_key: str | None,
    base_url: str,
    model: str,
    max_tool_rounds: int,
    services: ServiceContainer,
):
    """Run in interactive mode with enhanced UX."""
    try:
        ui = InteractiveInterface()
        settings_manager = SettingsManager()

        # Explicitly prompt to configure if no settings file exists
        if not settings_manager.settings_file.exists():
            ui.show_warning("No configuration found at ~/.arc/user-settings.json")
            resp = (
                (
                    await ui.get_user_input_async(
                        "Configure API key, Base URL, and Model now? (Y/n): "
                    )
                )
                .strip()
                .lower()
            )
            if not resp or resp.startswith("y"):
                # Collect values; allow skip
                new_api = await ui.get_user_input_async(
                    "API key (leave blank to skip): "
                )
                if new_api.strip():
                    settings_manager.update_user_setting("apiKey", new_api.strip())
                    api_key = new_api.strip()
                    ui.show_system_success("API key saved to ~/.arc/user-settings.json")
                new_url = await ui.get_user_input_async(f"Base URL [{base_url}]: ")
                if new_url.strip():
                    settings_manager.update_user_setting("baseURL", new_url.strip())
                    base_url = new_url.strip()
                    ui.show_system_success(
                        "Base URL saved to ~/.arc/user-settings.json"
                    )
                new_model = await ui.get_user_input_async(f"Model [{model}]: ")
                if new_model.strip():
                    settings_manager.update_user_setting("model", new_model.strip())
                    model = new_model.strip()
                    ui.show_system_success("Model saved to ~/.arc/user-settings.json")

        # Initialize agent if API key or base URL is configured
        # (for enterprise gateway mode where only base_url is needed)
        agent: ArcAgent | None = None
        if api_key or base_url:
            # Pass empty string for api_key if None (gateway mode)
            agent = ArcAgent(
                api_key or "", services, base_url, model, max_tool_rounds, ui
            )

        with suppress(Exception):
            ConfirmationService.get_instance().set_ui(ui)

        # Database context for SQL commands - defaults to user database
        current_database = "user"

        # Show enhanced welcome screen
        # Provide welcome even if agent is not yet initialized
        current_model_name = (
            agent.get_current_model() if agent else (model or "Not set")
        )
        current_dir = agent.get_current_directory() if agent else os.getcwd()
        ui.show_welcome(current_model_name, current_dir)

        while True:
            try:
                # Get user input with styled prompt
                user_input = await ui.get_user_input_async()

                # Display the user message in chat history with different coloring
                ui.show_user_message(user_input)

                # Handle system commands (only with / prefix)
                if user_input.startswith("/"):
                    if user_input.startswith("/ml"):
                        await handle_ml_command(
                            user_input, ui, services.ml_runtime, agent
                        )
                        continue

                    cmd = user_input[
                        1:
                    ].lower()  # Remove the / prefix and convert to lowercase

                    if cmd == "exit":
                        ui.show_goodbye()
                        break
                    elif cmd == "help":
                        ui.show_commands()
                        continue

                    elif cmd == "clear":
                        ui.clear_screen()
                        continue
                    elif cmd == "config":
                        # Show and optionally edit configuration
                        settings_manager = SettingsManager()
                        current_api_key = settings_manager.get_api_key()
                        current_base_url = settings_manager.get_base_url()
                        current_model = settings_manager.get_current_model()

                        config_text = (
                            f"API Key: {'*' * 8 if current_api_key else 'Not set'}\n"
                            f"Base URL: {current_base_url or 'Not set'}\n"
                            f"Model: {current_model or 'Not set'}\n"
                            f"Max Tool Rounds: {max_tool_rounds}"
                        )
                        ui.show_config_panel(config_text)

                        # Offer inline editing of baseURL, model, and apiKey
                        # Print prompt with section indentation to match config output
                        edit_resp = (
                            (
                                await ui.get_user_input_async(
                                    "  Edit configuration values now? (y/N): "
                                )
                            )
                            .strip()
                            .lower()
                        )

                        if edit_resp.startswith("y"):
                            # Note: environment variables override settings at runtime.
                            # Editing here updates ~/.arc/user-settings.json.
                            new_api = await ui.get_user_input_async(
                                "API key (leave blank to keep current): "
                            )
                            if new_api.strip():
                                settings_manager.update_user_setting(
                                    "apiKey", new_api.strip()
                                )
                                ui.show_system_success(
                                    "API key saved to ~/.arc/user-settings.json"
                                )

                            new_url = await ui.get_user_input_async(
                                f"Base URL [{current_base_url or ''}]: "
                            )
                            if new_url.strip():
                                settings_manager.update_user_setting(
                                    "baseURL", new_url.strip()
                                )
                                ui.show_system_success(
                                    "Base URL saved to ~/.arc/user-settings.json"
                                )

                            new_model = await ui.get_user_input_async(
                                f"Model [{current_model or ''}]: "
                            )
                            if new_model.strip():
                                settings_manager.update_user_setting(
                                    "model", new_model.strip()
                                )
                                ui.show_system_success(
                                    "Model saved to ~/.arc/user-settings.json"
                                )

                            # Refresh and show the updated configuration
                            updated_api = settings_manager.get_api_key()
                            updated_base = settings_manager.get_base_url()
                            current_model_setting = settings_manager.get_current_model()
                            updated_model = current_model_setting
                            updated_text = (
                                f"API Key: {'*' * 8 if updated_api else 'Not set'}\n"
                                f"Base URL: {updated_base or 'Not set'}\n"
                                f"Model: {updated_model or 'Not set'}\n"
                                f"Max Tool Rounds: {max_tool_rounds}"
                            )
                            ui.show_config_panel(updated_text)

                            # Initialize agent if missing and API key set
                            if agent is None and updated_api:
                                try:
                                    nonlocal_agent = ArcAgent(
                                        updated_api,
                                        services,
                                        updated_base,
                                        updated_model,
                                        max_tool_rounds,
                                        ui,
                                    )
                                    agent = nonlocal_agent
                                    ui.show_system_success(
                                        "AI chat is now enabled with the configured "
                                        "settings."
                                    )
                                except Exception as init_exc:
                                    ui.show_system_error(
                                        f"Failed to initialize agent: {init_exc}"
                                    )

                        # Add separator before returning to prompt
                        ui._printer.add_separator()
                        continue
                    elif cmd.startswith("sql"):
                        # Handle SQL queries and update current database context
                        current_database = await handle_sql_command(
                            services.query, ui, user_input, current_database
                        )
                        continue
                    elif cmd.startswith("report"):
                        # LLM-assisted bug report generation
                        import termios

                        from rich.markdown import Markdown
                        from rich.padding import Padding
                        from rich.panel import Panel

                        from arc.utils.bug_report_prompt import (
                            format_bug_report_for_display,
                            format_bug_report_for_editing,
                            generate_bug_report,
                            parse_bug_report_from_markdown,
                        )
                        from arc.utils.yaml_workflow import YamlEditorHelper

                        # Check if we can use LLM assistance
                        can_use_llm = (
                            agent
                            and hasattr(agent, "chat_history")
                            and agent.chat_history
                            and len(agent.chat_history) > 0
                        )

                        title = ""
                        desc = ""

                        # Keep entire /report flow within one section for proper nesting
                        with ui._printer.section(shape="ℹ") as p:
                            p.print("Create Bug Report")
                            p.print(
                                "[dim]I'll help you create a detailed bug report based on our conversation.[/dim]"  # noqa: E501
                            )

                            if not can_use_llm:
                                # Show why auto-detect isn't available
                                p.print()
                                if agent:
                                    p.print(
                                        "▸ [dim]Auto-detect unavailable: No conversation history yet[/dim]"  # noqa: E501
                                    )
                                else:
                                    p.print(
                                        "▸ [dim]Auto-detect unavailable: No AI agent initialized[/dim]"  # noqa: E501
                                    )
                                p.print(
                                    "  [dim]Have a conversation first, then use /report to analyze issues[/dim]"  # noqa: E501
                                )
                                # Manual entry for no-LLM case
                                title = await ui.get_user_input_async("  Title: ")
                                desc = await ui.get_user_input_async("  Description: ")
                            else:
                                # Auto-detect from conversation using LLM
                                # First, prompt for issue context
                                p.print()
                                issue_hint = await ui.get_user_input_async(
                                    "  What issue would you like to report? "
                                )

                                if not issue_hint.strip():
                                    p.print()
                                    p.print(
                                        "[dim]Bug report cancelled (no issue provided)[/dim]"  # noqa: E501
                                    )
                                    continue

                                p.print()
                                p.print("▸ [dim]Analyzing conversation...[/dim]")

                                report = await generate_bug_report(
                                    agent, max_messages=20, issue_hint=issue_hint
                                )

                                if report and report.get("title"):
                                    # Edit loop - allow multiple iterations
                                    while True:
                                        # Display generated report in markdown panel
                                        formatted = format_bug_report_for_display(
                                            report
                                        )

                                        # Use Rich's Markdown renderer with panel
                                        md = Markdown(formatted, justify="left")
                                        panel = Panel(
                                            md, border_style="color(245)", expand=False
                                        )
                                        # Add 2-space left padding to nest under section
                                        padded_panel = Padding(panel, (0, 0, 0, 2))
                                        ui._printer.console.print(padded_panel)
                                        ui._printer.console.print()  # Blank line

                                        # Flush terminal to prevent race condition
                                        ui._printer.console.file.flush()
                                        try:
                                            if hasattr(
                                                ui._printer.console.file, "fileno"
                                            ):
                                                termios.tcdrain(
                                                    ui._printer.console.file.fileno()
                                                )
                                        except (OSError, AttributeError):
                                            pass

                                        # Use arrow key selection for confirmation
                                        options = [
                                            ("accept", "Accept and create issue"),
                                            ("edit", "Edit in system editor"),
                                            ("cancel", "Cancel"),
                                        ]

                                        choice = await ui._printer.get_choice_async(
                                            options, default="accept"
                                        )

                                        # Reset prompt state after choice
                                        ui._printer.reset_prompt_session()

                                        if choice == "accept":
                                            title = report["title"]
                                            # Compose full description from sections
                                            desc_parts = []
                                            if report["description"]:
                                                desc_parts.append(report["description"])
                                            if report["steps"]:
                                                desc_parts.append(
                                                    f"\n\n**Steps to Reproduce:**\n{report['steps']}"  # noqa: E501
                                                )
                                            if report["expected"]:
                                                desc_parts.append(
                                                    f"\n\n**Expected Behavior:**\n{report['expected']}"  # noqa: E501
                                                )
                                            if report["actual"]:
                                                desc_parts.append(
                                                    f"\n\n**Actual Behavior:**\n{report['actual']}"  # noqa: E501
                                                )
                                            if report["context"]:
                                                desc_parts.append(
                                                    f"\n\n**Context:**\n{report['context']}"
                                                )
                                            desc = "".join(desc_parts)
                                            break  # Exit edit loop
                                        elif choice == "edit":
                                            # Launch system editor
                                            markdown_content = (
                                                format_bug_report_for_editing(report)
                                            )
                                            header = "# Edit bug report and save to confirm\n\n"  # noqa: E501

                                            edited_content = await YamlEditorHelper.edit_with_system_editor(  # noqa: E501
                                                markdown_content,
                                                header_comment=header,
                                                yaml_suffix=".md",
                                            )

                                            if edited_content:
                                                # Parse edited markdown back to report dict # noqa: E501
                                                report = parse_bug_report_from_markdown(
                                                    edited_content
                                                )
                                                p.print()
                                                p.print("[dim]✓ Report updated[/dim]")
                                                # Loop back to preview
                                            else:
                                                p.print()
                                                p.print(
                                                    "[yellow]⚠ Edit cancelled or failed[/yellow]"  # noqa: E501
                                                )
                                                # Loop back to preview with unchanged report # noqa: E501
                                        else:
                                            # User cancelled
                                            p.print()
                                            p.print("[dim]Bug report cancelled[/dim]")
                                            title = None  # Signal cancellation
                                            break  # Exit edit loop

                                    # If cancelled, skip to next command
                                    if title is None:
                                        continue
                                else:
                                    # LLM couldn't generate report, use manual entry
                                    p.print()
                                    p.print(
                                        "[yellow]Could not auto-detect issue from conversation[/yellow]"  # noqa: E501
                                    )
                                    title = await ui.get_user_input_async("  Title: ")
                                    desc = await ui.get_user_input_async(
                                        "  Description: "
                                    )

                            # Build issue URL
                            try:
                                model_name = (
                                    agent.get_current_model() if agent else None
                                )
                            except Exception:
                                model_name = None
                            body = compose_issue_body(desc, model=model_name)
                            issue_url = build_issue_url(title, body)

                            # Ask to open browser using arrow key selection
                            p.print()
                            open_options = [
                                ("yes", "Open browser"),
                                ("no", "Show URL only"),
                            ]

                            open_choice = await ui._printer.get_choice_async(
                                open_options, default="yes"
                            )
                            ui._printer.reset_prompt_session()

                            p.print()
                            if open_choice == "yes":
                                opened = open_in_browser(issue_url)
                                if opened:
                                    p.print(
                                        "[dim]✓ Opened browser to GitHub issues page[/dim]"  # noqa: E501
                                    )
                                else:
                                    p.print("[dim]Could not open browser[/dim]")
                                    p.print(f"[dim]URL: {issue_url}[/dim]")
                            else:
                                p.print("[dim]You can create the issue at:[/dim]")
                                p.print(f"[dim][cyan]{issue_url}[/cyan][/dim]")

                        continue
                    else:
                        ui.show_system_error(f"Unknown system command: /{cmd}")
                        continue

                # No special exit without slash; only /exit is supported

                if not user_input:
                    continue

                # If no agent and user typed free text, require configuration
                if not user_input.startswith("/") and agent is None:
                    ui.show_system_error(
                        "API key not configured. Use /config to set apiKey, "
                        "baseURL, and model."
                    )
                    continue

                # Process streaming response with clean context management
                start_time = time.time()

                with ui.escape_watcher() as esc:
                    interrupted = False
                    with ui.stream_response(start_time) as handler:
                        agen = agent.process_user_message_stream(user_input).__aiter__()
                        esc_task = asyncio.create_task(esc.event.wait())
                        try:
                            while True:
                                next_task = asyncio.create_task(agen.__anext__())
                                done, _ = await asyncio.wait(
                                    {esc_task, next_task},
                                    return_when=asyncio.FIRST_COMPLETED,
                                )

                                if esc_task in done:
                                    interrupted = True
                                    next_task.cancel()
                                    # Wait for cancelled task to complete
                                    with suppress(asyncio.CancelledError):
                                        await next_task
                                    # Close generator (triggers GeneratorExit handler)
                                    # This adds the cancellation message to chat history
                                    with suppress(Exception):
                                        await agen.aclose()
                                    break

                                # Otherwise, next_task completed
                                chunk = next_task.result()
                                handler.handle_chunk(chunk)
                        except StopAsyncIteration:
                            pass
                        finally:
                            if not esc_task.done():
                                esc_task.cancel()

                if interrupted:
                    # Generator added cancellation message to chat history
                    # Display it through the stream handler for consistency
                    from arc.core.agent import StreamingChunk

                    if agent and agent.chat_history:
                        last_entry = agent.chat_history[-1]
                        if last_entry.type == "assistant" and last_entry.content:
                            # Create a content chunk and flow it through the handler
                            chunk = StreamingChunk(
                                type="content", content=last_entry.content
                            )
                            handler.handle_chunk(chunk)
                            chunk = StreamingChunk(type="done")
                            handler.handle_chunk(chunk)

            except KeyboardInterrupt:
                ui.show_goodbye()
                break
            except EOFError:
                ui.show_goodbye()
                break
            except Exception as e:
                ui.show_system_error(str(e))

    except Exception as e:
        ui = InteractiveInterface()
        ui.show_system_error(f"Error initializing Arc CLI: {str(e)}")
        sys.exit(1)
    finally:
        # Clean up resources
        with suppress(Exception):
            if agent:
                agent.cleanup()
        with suppress(Exception):
            services.shutdown()

        # Clean up TensorBoard processes
        with suppress(Exception):
            # Import here: TensorBoardManager is optional and heavy dependency
            from arc.ml import TensorBoardManager

            tb_manager = TensorBoardManager()
            tb_manager.stop_all()


if __name__ == "__main__":
    cli()
