"""CLI commands for hdx agent — scaffold, wire, and run agents."""

from __future__ import annotations

import asyncio
import json
import time
from pathlib import Path

import click
import yaml

from hatchdx.agent.config import AgentConfigError, load_agent_config
from hatchdx.agent.runtime.tools import (
    CollectedTools,
    ToolDefinition,
    apply_tool_filters,
    collect_and_filter_tools,
)
from hatchdx.agent.scaffolder import (
    AGENT_TEMPLATES,
    DEFAULT_MODELS,
    PROVIDERS,
    AgentScaffoldContext,
    scaffold_agent,
)
from hatchdx.utils import console


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


def _validate_agent_name(name: str) -> tuple[bool, str]:
    """Validate an agent name (lowercase, hyphens, starts with letter)."""
    import re

    if not name:
        return False, "Name cannot be empty"
    if not re.match(r"^[a-z][a-z0-9-]*$", name):
        return False, "Must be lowercase letters, numbers, and hyphens (start with a letter)"
    return True, ""


def _prompt_agent_name() -> str:
    """Prompt for a valid agent name."""
    while True:
        name = click.prompt("Agent name (e.g., weather-agent)")
        valid, reason = _validate_agent_name(name)
        if valid:
            return name
        click.echo(f"  Invalid: {reason}")


def _collect_agent_prompts() -> AgentScaffoldContext:
    """Run interactive prompts and return an AgentScaffoldContext."""
    click.echo()
    name = _prompt_agent_name()
    description = click.prompt("Description", default="An AI assistant")
    template = click.prompt(
        "Template",
        type=click.Choice(AGENT_TEMPLATES, case_sensitive=False),
        default="single-agent",
    )
    provider = click.prompt(
        "Model provider",
        type=click.Choice(PROVIDERS, case_sensitive=False),
        default="anthropic",
    )
    default_model = DEFAULT_MODELS.get(provider, "")
    model = click.prompt("Model", default=default_model)

    return AgentScaffoldContext.from_user_input(
        name=name,
        description=description,
        template=template,
        provider=provider,
        model=model,
    )


def _find_agents_dir(path: str | None = None) -> Path:
    """Resolve agents directory."""
    return Path(path) if path else Path.cwd() / "agents"


def _find_agent_dir(name: str, agents_dir: Path) -> Path | None:
    """Find an agent directory by name. Returns None if not found."""
    agent_dir = agents_dir / name
    if agent_dir.is_dir() and (agent_dir / "agent.yaml").exists():
        return agent_dir
    return None


def _list_agent_dirs(agents_dir: Path) -> list[Path]:
    """List all agent directories that contain agent.yaml."""
    if not agents_dir.is_dir():
        return []
    return sorted(
        d for d in agents_dir.iterdir()
        if d.is_dir() and (d / "agent.yaml").exists()
    )


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


@click.group("agent")
def agent_cmd():
    """Create and manage AI agents with MCP servers."""


@agent_cmd.command("create")
@click.option("--name", "-n", help="Agent name (lowercase, hyphens)")
@click.option("--description", "-d", help="One-line description")
@click.option(
    "--template",
    "-t",
    type=click.Choice(AGENT_TEMPLATES, case_sensitive=False),
    help="Agent template",
)
@click.option(
    "--provider",
    "-p",
    type=click.Choice(PROVIDERS, case_sensitive=False),
    help="Model provider",
)
@click.option("--model", "-m", help="Model identifier")
@click.option("--output-dir", "-o", type=click.Path(), help="Output directory")
def create_cmd(name, description, template, provider, model, output_dir):
    """Create a new agent project."""
    # If all required flags provided, skip interactive prompts
    if name and description and template and provider:
        valid, reason = _validate_agent_name(name)
        if not valid:
            console.error(f"Invalid agent name: {reason}")
            raise SystemExit(1)

        context = AgentScaffoldContext.from_user_input(
            name=name,
            description=description,
            template=template,
            provider=provider,
            model=model,
        )
    elif name:
        # Partial flags — fill in defaults for missing values
        valid, reason = _validate_agent_name(name)
        if not valid:
            console.error(f"Invalid agent name: {reason}")
            raise SystemExit(1)

        context = AgentScaffoldContext.from_user_input(
            name=name,
            description=description or "An AI assistant",
            template=template or "single-agent",
            provider=provider or "anthropic",
            model=model,
        )
    else:
        context = _collect_agent_prompts()

    console.step(f"Creating {context.name}...")

    if output_dir:
        out_dir = Path(output_dir)
    else:
        from hatchdx.utils.workspace import detect_output_dir
        out_dir = detect_output_dir("agent")

    try:
        agent_dir = scaffold_agent(context, output_dir=out_dir)
    except FileExistsError:
        console.error(f"Agent already exists: {context.name}")
        raise SystemExit(1)
    except FileNotFoundError as e:
        console.error(str(e))
        raise SystemExit(1)

    console.success(f"Agent created at {agent_dir}/")
    click.echo()
    click.echo("Created files:")
    click.echo(f"  {agent_dir}/agent.yaml")
    click.echo(f"  {agent_dir}/prompts/system.md")
    click.echo(f"  {agent_dir}/evals/")
    click.echo()
    click.echo("Next steps:")
    click.echo(f"  hdx agent add-server {context.name} --server ./servers/my-server")
    click.echo(f"  hdx agent chat {context.name}")


@agent_cmd.command("list")
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
@click.option("--path", "-p", type=click.Path(), help="Agents directory")
def list_cmd(as_json, path):
    """List all agents in the project."""
    agents_dir = _find_agents_dir(path)
    agent_dirs = _list_agent_dirs(agents_dir)

    if not agent_dirs:
        if as_json:
            click.echo("[]")
        else:
            console.warning(f"No agents found in {agents_dir}/")
            click.echo()
            click.echo("Create your first agent:")
            click.echo("  hdx agent create")
        return

    agents = []
    for agent_dir in agent_dirs:
        try:
            config = load_agent_config(agent_dir / "agent.yaml")
            agents.append({
                "name": config.name,
                "description": config.description,
                "servers": len(config.servers),
                "model": config.model.model,
                "version": config.version,
            })
        except (AgentConfigError, FileNotFoundError):
            agents.append({
                "name": agent_dir.name,
                "description": "(invalid config)",
                "servers": 0,
                "model": "?",
                "version": "?",
            })

    if as_json:
        click.echo(json.dumps(agents, indent=2))
        return

    click.echo()
    click.echo("Agents:")
    click.echo()
    for agent in agents:
        click.echo(f"  {agent['name']}")
        click.echo(f"    {agent['description']}")
        click.echo(f"    Model: {agent['model']} | Servers: {agent['servers']}")
        click.echo()


@agent_cmd.command("inspect")
@click.argument("name")
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
@click.option("--tools", "show_tools", is_flag=True, help="Only show tools")
@click.option("--path", "-p", type=click.Path(), help="Agents directory")
def inspect_cmd(name, as_json, show_tools, path):
    """Show detailed information about an agent."""
    agents_dir = _find_agents_dir(path)
    agent_dir = _find_agent_dir(name, agents_dir)

    if agent_dir is None:
        console.error(f"Agent '{name}' not found.")
        available = _list_agent_dirs(agents_dir)
        if available:
            click.echo()
            click.echo("Available agents:")
            for d in available:
                click.echo(f"  {d.name}")
        click.echo()
        click.echo("Or create a new agent:")
        click.echo(f"  hdx agent create --name {name}")
        raise SystemExit(1)

    try:
        config = load_agent_config(agent_dir / "agent.yaml")
    except AgentConfigError as e:
        console.error(f"Invalid agent config: {e}")
        raise SystemExit(1)

    if as_json:
        data = {
            "name": config.name,
            "description": config.description,
            "version": config.version,
            "model": {
                "provider": config.model.provider,
                "model": config.model.model,
                "max_tokens": config.model.max_tokens,
                "temperature": config.model.temperature,
            },
            "servers": [
                {
                    "name": s.name,
                    "source": s.path or s.command or s.url,
                    "transport": "stdio" if (s.path or s.command) else "http",
                }
                for s in config.servers
            ],
            "tool_filter": {
                server_name: {
                    "include": f.include,
                    "exclude": f.exclude,
                }
                for server_name, f in config.tool_filter.items()
            },
            "settings": {
                "max_tool_calls": config.settings.max_tool_calls,
                "tool_call_timeout": config.settings.tool_call_timeout,
                "retry_on_error": config.settings.retry_on_error,
                "max_retries": config.settings.max_retries,
                "tool_namespacing": config.settings.tool_namespacing,
            },
            "config_path": str(agent_dir / "agent.yaml"),
        }
        click.echo(json.dumps(data, indent=2))
        return

    if show_tools:
        _print_tools_only(config)
        return

    _print_full_inspect(config, agent_dir)


def _print_tools_only(config):
    """Print just the tool filter information."""
    click.echo()
    if not config.tool_filter:
        click.echo("No tool filters configured. All server tools will be exposed.")
        return

    click.echo("Tool Filters:")
    for server_name, f in config.tool_filter.items():
        if f.include:
            click.echo(f"  {server_name}: include {f.include}")
        elif f.exclude:
            click.echo(f"  {server_name}: exclude {f.exclude}")


def _print_full_inspect(config, agent_dir):
    """Print full agent inspection output."""
    click.echo()
    click.echo(f"Agent: {config.name}")
    click.echo()
    click.echo(f"  Description: {config.description}")
    click.echo(f"  Model: {config.model.model} ({config.model.provider})")
    click.echo(f"  Config: {agent_dir / 'agent.yaml'}")

    if config.servers:
        click.echo()
        click.echo(f"  Servers ({len(config.servers)}):")
        for server in config.servers:
            source = server.path or server.command or server.url
            transport = "stdio (local)" if (server.path or server.command) else "http (remote)"
            click.echo(f"    {server.name}: {source} [{transport}]")

    if config.tool_filter:
        click.echo()
        click.echo("  Tool Filters:")
        for server_name, f in config.tool_filter.items():
            if f.include:
                tools_str = ", ".join(f.include)
                click.echo(f"    {server_name}: include [{tools_str}]")
            elif f.exclude:
                tools_str = ", ".join(f.exclude)
                click.echo(f"    {server_name}: exclude [{tools_str}]")

    click.echo()
    click.echo("  Settings:")
    click.echo(f"    max_tool_calls: {config.settings.max_tool_calls}")
    click.echo(f"    tool_call_timeout: {config.settings.tool_call_timeout}")
    click.echo(f"    retry_on_error: {config.settings.retry_on_error}")
    click.echo(f"    tool_namespacing: {config.settings.tool_namespacing}")

    prompt_source = config.system_prompt_file or "(inline)"
    click.echo(f"    system_prompt: {prompt_source}")
    click.echo()


@agent_cmd.command("add-server")
@click.argument("name")
@click.option("--server", "-s", type=click.Path(), help="Path to local MCP server directory")
@click.option("--url", "-u", help="URL for remote HTTP server")
@click.option("--command", "-c", help="Direct command to start server")
@click.option("--server-name", help="Override server name (auto-detected from path)")
@click.option("--env", "-e", multiple=True, help="Environment variables (KEY=VALUE, repeatable)")
@click.option("--filter-tools", is_flag=True, help="Interactively select which tools to expose")
@click.option("--include-tools", multiple=True, help="Tools to include (whitelist)")
@click.option("--exclude-tools", multiple=True, help="Tools to exclude (blacklist)")
@click.option("--path", "-p", type=click.Path(), help="Agents directory")
def add_server_cmd(name, server, url, command, server_name, env, filter_tools, include_tools, exclude_tools, path):
    """Add an MCP server to an existing agent."""
    agents_dir = _find_agents_dir(path)
    agent_dir = _find_agent_dir(name, agents_dir)

    if agent_dir is None:
        console.error(f"Agent '{name}' not found in {agents_dir}/")
        raise SystemExit(1)

    # Validate exactly one source
    sources = [s for s in (server, url, command) if s is not None]
    if len(sources) == 0:
        console.error("Must specify one of --server, --url, or --command")
        raise SystemExit(1)
    if len(sources) > 1:
        console.error("Specify only one of --server, --url, or --command")
        raise SystemExit(1)

    # Cannot specify both include and exclude
    if include_tools and exclude_tools:
        console.error("Cannot specify both --include-tools and --exclude-tools")
        raise SystemExit(1)

    # Determine server name
    if not server_name:
        if server:
            server_name = Path(server).name
        elif url:
            server_name = "remote-server"
        elif command:
            # Use first word of command
            server_name = command.split()[0].split("/")[-1].replace(".", "-")

    # Load existing config to check for duplicates
    config_path = agent_dir / "agent.yaml"
    try:
        config = load_agent_config(config_path)
    except AgentConfigError as e:
        console.error(f"Invalid agent config: {e}")
        raise SystemExit(1)

    existing_names = {s.name for s in config.servers}
    if server_name in existing_names:
        console.error(f"Server '{server_name}' already exists in agent '{name}'")
        raise SystemExit(1)

    # Parse env vars
    env_dict = {}
    for e_str in env:
        if "=" not in e_str:
            console.error(f"Invalid env format: '{e_str}' (expected KEY=VALUE)")
            raise SystemExit(1)
        key, value = e_str.split("=", 1)
        env_dict[key] = value

    # Build the server entry
    server_entry: dict = {"name": server_name}
    if server:
        server_entry["path"] = server
    elif url:
        server_entry["url"] = url
    elif command:
        server_entry["command"] = command
    if env_dict:
        server_entry["env"] = env_dict

    # Update agent.yaml
    raw = yaml.safe_load(config_path.read_text())
    if "servers" not in raw or raw["servers"] is None:
        raw["servers"] = []
    raw["servers"].append(server_entry)

    # Handle tool filtering flags
    if include_tools or exclude_tools:
        if "tool_filter" not in raw or raw["tool_filter"] is None:
            raw["tool_filter"] = {}
        if include_tools:
            raw["tool_filter"][server_name] = {"include": list(include_tools)}
        elif exclude_tools:
            raw["tool_filter"][server_name] = {"exclude": list(exclude_tools)}

    config_path.write_text(yaml.dump(raw, default_flow_style=False, sort_keys=False))

    console.success(f"Added server '{server_name}' to agent '{name}'")

    source = server or url or command
    click.echo()
    click.echo(f"  Server: {server_name}")
    click.echo(f"  Source: {source}")
    if include_tools:
        click.echo(f"  Include tools: {', '.join(include_tools)}")
    if exclude_tools:
        click.echo(f"  Exclude tools: {', '.join(exclude_tools)}")
    click.echo()
    click.echo(f"  hdx agent inspect {name}")


# ---------------------------------------------------------------------------
# Chat command
# ---------------------------------------------------------------------------


def _format_args(arguments: dict) -> str:
    """Format tool call arguments for inline display."""
    if not arguments:
        return ""
    parts = []
    for key, value in arguments.items():
        if isinstance(value, str) and len(value) > 30:
            value = value[:27] + "..."
        parts.append(f"{key}={value}")
    return " ".join(parts)


def _format_result_brief(result: str) -> str:
    """Truncate a tool result for inline display."""
    result = result.replace("\n", " ").strip()
    if len(result) > 60:
        return result[:57] + "..."
    return result


def _format_cost(cost: float) -> str:
    """Format a cost value for display."""
    if cost < 0.001:
        return f"${cost:.4f}"
    return f"${cost:.3f}"


def _format_tokens(n: int) -> str:
    """Format token count with comma separators."""
    return f"{n:,}"


async def _run_chat_interactive(engine, agent_name: str, verbose: bool) -> None:
    """Run an interactive chat session."""
    from hatchdx.agent.engine import estimate_cost_for_provider
    from hatchdx.agent.runtime.loop import LoopTurn

    # Session tracking
    session_start = time.monotonic()
    total_messages = 0
    total_tool_calls = 0
    total_input_tokens = 0
    total_output_tokens = 0
    total_cost = 0.0
    conversation_history: list[dict] = []

    click.echo()
    click.echo(f"  Type your message, or /help for commands.")
    click.echo(f"  Press Ctrl+C to exit.")
    click.echo()

    def _handle_session_command(cmd: str) -> bool:
        """Handle /commands. Returns True if handled."""
        nonlocal conversation_history
        cmd = cmd.strip().lower()

        if cmd in ("/help", "help"):
            click.echo()
            click.echo("  Commands:")
            click.echo("    /tools      List all available tools")
            click.echo("    /servers    Show connected servers")
            click.echo("    /history    Show conversation history")
            click.echo("    /clear      Clear conversation history")
            click.echo("    /system     Show system prompt")
            click.echo("    /cost       Show session cost")
            click.echo("    /quit       Exit chat")
            click.echo()
            return True

        if cmd == "/tools":
            click.echo()
            tools_by_server = engine.collected_tools.tools_by_server()
            for server_name, tools in tools_by_server.items():
                click.echo(f"  {server_name}:")
                for tool in tools:
                    click.echo(f"    {tool.name}  {tool.description}")
            click.echo()
            return True

        if cmd == "/servers":
            click.echo()
            for server in engine.servers:
                status = "running" if server.is_running else "stopped"
                tool_count = len(server.tools)
                source = server.config.path or server.config.command or server.config.url
                click.echo(f"  {server.config.name}: {source} [{status}] ({tool_count} tools)")
            click.echo()
            return True

        if cmd == "/history":
            click.echo()
            if not conversation_history:
                click.echo("  (no messages yet)")
            else:
                for entry in conversation_history:
                    role = entry["role"]
                    content = entry["content"]
                    if role == "user":
                        click.echo(f"  You: {content}")
                    elif role == "assistant":
                        preview = content[:80] + "..." if len(content) > 80 else content
                        click.echo(f"  Agent: {preview}")
            click.echo()
            return True

        if cmd == "/clear":
            conversation_history.clear()
            console.success("Conversation history cleared.")
            click.echo()
            return True

        if cmd == "/system":
            click.echo()
            click.echo(engine.config.system_prompt)
            click.echo()
            return True

        if cmd == "/cost":
            click.echo()
            cost = total_cost
            click.echo(f"  Messages: {total_messages}")
            click.echo(f"  Tool calls: {total_tool_calls}")
            click.echo(f"  Tokens: {_format_tokens(total_input_tokens)} in / {_format_tokens(total_output_tokens)} out")
            click.echo(f"  Cost: {_format_cost(cost)}")
            click.echo()
            return True

        if cmd in ("/quit", "/exit", "quit", "exit"):
            raise KeyboardInterrupt

        return False

    try:
        while True:
            try:
                user_input = click.prompt(f"  {agent_name}", prompt_suffix="> ")
            except (EOFError, click.Abort):
                raise KeyboardInterrupt

            if not user_input.strip():
                continue

            # Check for session commands
            if user_input.strip().startswith("/") or user_input.strip().lower() in ("help", "quit", "exit"):
                if _handle_session_command(user_input.strip()):
                    continue

            total_messages += 1
            conversation_history.append({"role": "user", "content": user_input})

            try:
                result = await engine.chat(user_input)
            except Exception as e:
                console.error(f"Error: {e}")
                click.echo()
                continue

            # Display tool call events
            for turn in result.turns:
                for event in turn.tool_events:
                    args_str = _format_args(event.arguments)
                    result_brief = _format_result_brief(event.result)
                    latency = f"{event.latency_ms:.0f}ms"

                    if event.is_error:
                        icon = "[red]x[/red]"
                    elif event.was_retry:
                        icon = "[yellow]~[/yellow]"
                    else:
                        icon = "[green]![/green]"

                    console.console.print(
                        f"  {icon} {event.server_name}:{event.tool_name} {args_str} -> {result_brief} ({latency})"
                    )

            # Display final response
            if result.final_text:
                click.echo()
                click.echo(f"  {result.final_text}")
                conversation_history.append({"role": "assistant", "content": result.final_text})

            # Update session stats
            total_tool_calls += result.total_tool_calls
            total_input_tokens += result.total_input_tokens
            total_output_tokens += result.total_output_tokens
            total_cost += estimate_cost_for_provider(
                engine.config.model.provider,
                engine.config.model.model,
                result.total_input_tokens,
                result.total_output_tokens,
            )

            # Per-message stats
            click.echo()
            click.echo(
                f"  Tokens: {_format_tokens(result.total_input_tokens)} in / "
                f"{_format_tokens(result.total_output_tokens)} out | "
                f"Tools: {result.total_tool_calls} calls | "
                f"Cost: {_format_cost(estimate_cost_for_provider(engine.config.model.provider, engine.config.model.model, result.total_input_tokens, result.total_output_tokens))}"
            )
            click.echo()

    except KeyboardInterrupt:
        pass

    # Session summary
    duration = time.monotonic() - session_start
    minutes = int(duration // 60)
    seconds = int(duration % 60)

    click.echo()
    click.echo("  Session Summary")
    click.echo(f"    Messages:    {total_messages}")
    click.echo(f"    Tool calls:  {total_tool_calls}")
    click.echo(f"    Tokens:      {_format_tokens(total_input_tokens)} in / {_format_tokens(total_output_tokens)} out")
    click.echo(f"    Cost:        {_format_cost(total_cost)}")
    click.echo(f"    Duration:    {minutes}m {seconds}s")
    click.echo()
    click.echo("  Goodbye!")
    click.echo()


async def _run_chat_single(engine, message: str, as_json: bool) -> None:
    """Run a single-message chat and exit."""
    from hatchdx.agent.engine import estimate_cost_for_provider

    result = await engine.chat(message)

    cost = estimate_cost_for_provider(
        engine.config.model.provider,
        engine.config.model.model,
        result.total_input_tokens,
        result.total_output_tokens,
    )

    if as_json:
        output = {
            "response": result.final_text,
            "tool_calls": result.total_tool_calls,
            "input_tokens": result.total_input_tokens,
            "output_tokens": result.total_output_tokens,
            "cost": round(cost, 6),
            "tools_used": [
                {
                    "name": e.tool_name,
                    "server": e.server_name,
                    "latency_ms": round(e.latency_ms, 1),
                    "is_error": e.is_error,
                }
                for e in result.all_tool_events
            ],
        }
        click.echo(json.dumps(output, indent=2))
    else:
        # Show tool calls
        for event in result.all_tool_events:
            args_str = _format_args(event.arguments)
            result_brief = _format_result_brief(event.result)
            latency = f"{event.latency_ms:.0f}ms"
            icon = "x" if event.is_error else "!"
            click.echo(f"  {icon} {event.server_name}:{event.tool_name} {args_str} -> {result_brief} ({latency})")

        if result.final_text:
            click.echo()
            click.echo(result.final_text)


async def _run_chat(agent_name: str, agents_dir: Path, message: str | None, verbose: bool, as_json: bool) -> None:
    """Core async chat runner."""
    from hatchdx.agent.engine import AgentEngine
    from hatchdx.agent.providers.base import ProviderAuthError
    from hatchdx.agent.runtime.servers import ServerConnectionError

    agent_dir = _find_agent_dir(agent_name, agents_dir)
    if agent_dir is None:
        console.error(f"Agent '{agent_name}' not found.")
        available = _list_agent_dirs(agents_dir)
        if available:
            click.echo()
            click.echo("Available agents:")
            for d in available:
                click.echo(f"  {d.name}")
        click.echo()
        click.echo("Or create a new agent:")
        click.echo(f"  hdx agent create --name {agent_name}")
        raise SystemExit(1)

    try:
        config = load_agent_config(agent_dir / "agent.yaml")
    except AgentConfigError as e:
        console.error(f"Invalid agent config: {e}")
        raise SystemExit(1)

    engine = AgentEngine(config, config_dir=agent_dir)

    if not message:
        click.echo()
        click.echo(f"  Agent Chat: {agent_name}")
        click.echo()

    try:
        # Start servers
        if not message:
            console.step("Starting servers...")

        try:
            servers = await engine.start()
        except ProviderAuthError as e:
            console.error(str(e))
            raise SystemExit(1)
        except ServerConnectionError as e:
            console.error(str(e))
            raise SystemExit(1)

        if not message:
            for server in servers:
                tool_count = len(server.tools)
                console.success(f"{server.config.name} ({tool_count} tools)")

            click.echo()
            click.echo(f"  Model: {config.model.model} ({config.model.provider})")
            click.echo(f"  Tools: {engine.collected_tools.tool_count} available")

        if message:
            await _run_chat_single(engine, message, as_json)
        else:
            await _run_chat_interactive(engine, agent_name, verbose)

    finally:
        await engine.shutdown()


@agent_cmd.command("chat")
@click.argument("name")
@click.option("--message", "-m", help="Send a single message (non-interactive)")
@click.option("--verbose", "-v", is_flag=True, help="Show full tool call payloads")
@click.option("--json", "as_json", is_flag=True, help="Output as JSON (with -m)")
@click.option("--path", "-p", type=click.Path(), help="Agents directory")
def chat_cmd(name, message, verbose, as_json, path):
    """Start an interactive chat session with an agent."""
    agents_dir = _find_agents_dir(path)
    asyncio.run(_run_chat(name, agents_dir, message, verbose, as_json))


# ---------------------------------------------------------------------------
# Eval command
# ---------------------------------------------------------------------------


def _find_eval_suite(agent_dir: Path, suite_name: str) -> Path | None:
    """Find an eval suite YAML file in the agent's evals/ directory."""
    evals_dir = agent_dir / "evals"
    if not evals_dir.is_dir():
        return None

    # Try exact name, then with .yaml/.yml extension
    for candidate in [
        evals_dir / suite_name,
        evals_dir / f"{suite_name}.yaml",
        evals_dir / f"{suite_name}.yml",
    ]:
        if candidate.exists():
            return candidate
    return None


def _list_eval_suites(agent_dir: Path) -> list[str]:
    """List available eval suites for an agent."""
    evals_dir = agent_dir / "evals"
    if not evals_dir.is_dir():
        return []
    return sorted(
        p.stem for p in evals_dir.iterdir()
        if p.suffix in (".yaml", ".yml")
    )


async def _run_eval(
    agent_name: str,
    agents_dir: Path,
    suite_name: str,
    runs: int,
    output_format: str,
    save: bool,
    compare: bool,
    tag: str | None,
    models: tuple[str, ...] = (),
    yes: bool = False,
) -> int:
    """Core async eval runner. Returns exit code."""
    from hatchdx.agent.engine import AgentEngine
    from hatchdx.agent.eval.models import EvalConfigError, load_eval_suite
    from hatchdx.agent.eval.reporter import (
        report_cross_model_json,
        report_cross_model_table,
        report_html,
        report_json,
        report_multi_run_json,
        report_multi_run_table,
        report_table,
    )
    from hatchdx.agent.eval.runner import run_eval_cross_model, run_eval_suite, run_eval_suite_multi
    from hatchdx.agent.eval.storage import EvalStorage
    from hatchdx.agent.providers.base import ProviderAuthError
    from hatchdx.agent.runtime.servers import ServerConnectionError

    agent_dir = _find_agent_dir(agent_name, agents_dir)
    if agent_dir is None:
        console.error(f"Agent '{agent_name}' not found.")
        return 2

    # Find eval suite
    suite_path = _find_eval_suite(agent_dir, suite_name)
    if suite_path is None:
        console.error(f"Eval suite '{suite_name}' not found.")
        available = _list_eval_suites(agent_dir)
        if available:
            click.echo()
            click.echo("Available suites:")
            for s in available:
                click.echo(f"  {s}")
        else:
            click.echo()
            click.echo(f"No eval suites found in {agent_dir / 'evals'}/")
            click.echo("Create a YAML file in the evals/ directory.")
        return 2

    # Load suite
    try:
        suite = load_eval_suite(suite_path)
    except EvalConfigError as e:
        console.error(f"Invalid eval suite: {e}")
        return 2

    # Cost warning — agent evals make real LLM API calls
    if not yes:
        num_cases = len(suite.cases)
        if tag:
            num_cases = sum(1 for c in suite.cases if tag in c.tags)
        num_models = max(len(models), 1)
        total_calls = num_cases * runs * num_models
        click.echo()
        if not click.confirm(
            f"This will make ~{total_calls} LLM API call{'s' if total_calls != 1 else ''} "
            f"(costs real money). Continue?",
            default=True,
        ):
            return 0

    # Load agent config and start engine
    try:
        config = load_agent_config(agent_dir / "agent.yaml")
    except AgentConfigError as e:
        console.error(f"Invalid agent config: {e}")
        return 2

    engine = AgentEngine(config, config_dir=agent_dir)

    try:
        console.step("Starting servers...")
        try:
            servers = await engine.start()
        except ProviderAuthError as e:
            console.error(str(e))
            return 2
        except ServerConnectionError as e:
            console.error(str(e))
            return 2

        for server in servers:
            tool_count = len(server.tools)
            console.success(f"{server.config.name} ({tool_count} tools)")

        # Progress callback
        case_count = [0]
        total_cases = len(suite.cases)
        if tag:
            total_cases = sum(1 for c in suite.cases if tag in c.tags)

        async def on_case(case_result):
            case_count[0] += 1
            status = "PASS" if case_result.passed else "FAIL"
            click.echo(
                f"  [{case_count[0]}/{total_cases}] {case_result.case_name}: {status} "
                f"({case_result.latency_ms:.0f}ms, ${case_result.cost_usd:.4f})"
            )

        click.echo()

        # Cross-model evaluation
        if models:
            model_list = list(models)
            click.echo(f"Cross-model eval: {suite.name} ({total_cases} cases, {len(model_list)} models)")
            click.echo()

            async def on_model(model_name):
                click.echo(f"  Model: {model_name}")

            run_results = await run_eval_cross_model(
                suite, engine, model_list,
                tag_filter=tag, on_case_complete=on_case, on_model_start=on_model,
            )

            # Always persist cross-model results
            db_path = Path.cwd() / ".hdx" / "agent_evals.db"
            storage = EvalStorage(db_path)
            for rr in run_results:
                storage.save_run(rr)
            storage.close()
            console.success(f"Results saved to {db_path}")

            if output_format == "json":
                click.echo(report_cross_model_json(run_results))
            else:
                click.echo(report_cross_model_table(run_results))

            all_passed = all(rr.failed == 0 for rr in run_results)
            return 0 if all_passed else 1

        click.echo(f"Running eval: {suite.name} ({total_cases} cases, {runs} run{'s' if runs > 1 else ''})")
        click.echo()

        # Run eval
        if runs > 1:
            run_results = await run_eval_suite_multi(
                suite, engine, runs=runs, tag_filter=tag, on_case_complete=on_case
            )
        else:
            run_result = await run_eval_suite(
                suite, engine, tag_filter=tag, on_case_complete=on_case
            )
            run_results = [run_result]

        # Storage — always persist results
        if save:
            console.warning("--save is deprecated (results are now always saved). You can remove this flag.")

        db_path = Path.cwd() / ".hdx" / "agent_evals.db"
        storage = EvalStorage(db_path)
        comparison = None
        comparison_cases = None

        for rr in run_results:
            storage.save_run(rr)
        console.success(f"Results saved to {db_path}")

        if compare:
            latest = storage.get_latest_run(agent_name, suite.name)
            if latest:
                comparison = latest
                comparison_cases = storage.get_run_cases(latest["id"])

        # Output
        if runs > 1:
            if output_format == "json":
                click.echo(report_multi_run_json(run_results))
            else:
                click.echo(report_multi_run_table(run_results))
        else:
            rr = run_results[0]
            if output_format == "json":
                click.echo(report_json(rr))
            elif output_format == "html":
                html = report_html(rr)
                html_path = agent_dir / "evals" / f"{suite.name}-report.html"
                html_path.write_text(html)
                console.success(f"HTML report saved to {html_path}")
            else:
                click.echo(report_table(rr, comparison=comparison, comparison_cases=comparison_cases))

        if storage:
            storage.close()

        # Exit code: 0 if all passed, 1 if any failed
        all_passed = all(rr.failed == 0 for rr in run_results)
        return 0 if all_passed else 1

    finally:
        await engine.shutdown()


@agent_cmd.command("eval")
@click.argument("name")
@click.option("--suite", "-s", required=True, help="Eval suite name (from evals/ dir)")
@click.option("--runs", "-r", default=1, type=int, help="Number of runs (default: 1)")
@click.option("--model", "-m", multiple=True, help="Model(s) to test (repeatable, e.g., --model gpt-4o --model claude-sonnet-4-20250514)")
@click.option("--output", "-o", "output_format", default="table", type=click.Choice(["table", "json", "html"]), help="Output format")
@click.option("--save", is_flag=True, hidden=True, help="Deprecated: results are now always saved")
@click.option("--compare", is_flag=True, help="Compare with previous run")
@click.option("--tag", "-t", help="Only run cases with this tag")
@click.option("--yes", "-y", is_flag=True, help="Skip cost confirmation (for CI)")
@click.option("--path", "-p", type=click.Path(), help="Agents directory")
def eval_cmd(name, suite, runs, model, output_format, save, compare, tag, yes, path):
    """Run eval suites against an agent."""
    agents_dir = _find_agents_dir(path)
    exit_code = asyncio.run(_run_eval(
        name, agents_dir, suite, runs, output_format, save, compare, tag, models=model, yes=yes
    ))
    raise SystemExit(exit_code)
