"""Skill management commands for CleverAgents CLI.

The ``agents skill`` command group manages skills — reusable, namespaced
collections of tools defined in YAML configuration files.

## Commands

| Command                 | Description                              |
|-------------------------|------------------------------------------|
| ``agents skill add``    | Register skill from YAML config file     |
| ``agents skill remove`` | Remove a registered skill                |
| ``agents skill list``   | List skills with optional filters        |
| ``agents skill show``   | Show full skill details                  |
| ``agents skill tools``  | Show flattened tool list                 |
| ``agents skill refresh``| Recompute flattening and sync MCP skills |

## Config-Only Add

Skills are registered **exclusively** via a YAML configuration file:

```bash
agents skill add --config ./skills/devops-toolkit.yaml
```

## Error Handling

| Error                      | Cause                                    |
|----------------------------|------------------------------------------|
| ``Config file error``      | File not found or not readable           |
| ``Schema validation error``| YAML does not match SkillConfigSchema    |
| ``Already registered``     | Skill exists (without ``--update``)      |
| ``Not registered``         | Skill not found for show/remove/tools    |

Based on specification.md "agents skill" section and implementation_plan.md
task C0.skill.cli.
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Annotated, Any

import typer
from pydantic import ValidationError as PydanticValidationError
from rich.panel import Panel
from rich.table import Table

from cleveragents.application.container import get_container
from cleveragents.application.services.skill_service import SkillService
from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.cli.renderers import _get_console
from cleveragents.domain.models.core.skill import (
    ResolvedToolEntry,
    Skill,
)
from cleveragents.skills.schema import SkillConfigSchema

logger = logging.getLogger(__name__)

# Create sub-app for skill commands
app = typer.Typer(help="Manage skills (reusable, namespaced tool collections).")
console = _get_console()

# Reusable --format option description
_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"

# ---------------------------------------------------------------------------
# Service accessor.  Always delegates to the DI container singleton so
# that the CLI and reactive layers share the same ``SkillService`` instance.
# No module-level cache — the Container's own Singleton provider handles
# caching, and ``reset_container()`` correctly invalidates it.
# ---------------------------------------------------------------------------


def _get_skill_service() -> SkillService:
    """Return the shared SkillService from the DI container.

    Delegates to ``get_container().skill_service()`` on every call so
    that ``reset_container()`` correctly invalidates the cached instance.
    The container provider handles DB-fallback logic.
    """
    return get_container().skill_service()


def _reset_skill_service(
    service: SkillService | None = None,
) -> None:
    """Reset the service (used by tests).

    Args:
        service: Optional ``SkillService`` instance to install.
            When ``None``, the next ``_get_skill_service`` call will
            create a fresh in-memory service (no DB persistence) to
            avoid side-effects during unit testing.

    Note:
        This patches the container's ``skill_service`` provider so
        that ``_get_skill_service()`` (which always delegates to the
        container) picks up the replacement.  When *service* is
        ``None`` a plain in-memory ``SkillService()`` is installed.
    """
    from dependency_injector import providers

    svc = service if service is not None else SkillService()
    container = get_container()
    container.skill_service.override(providers.Object(svc))


# ---------------------------------------------------------------------------
# Helpers — spec dict and rich printing
# ---------------------------------------------------------------------------


def _skill_spec_dict(
    skill: Skill,
    service: SkillService,
) -> dict[str, Any]:
    """Return skill data using spec field names for non-rich formats."""
    sources = service.get_source_types(skill)
    result: dict[str, Any] = {
        "name": skill.name,
        "namespace": skill.namespace,
        "short_name": skill.short_name,
        "description": skill.description,
        "config_path": service.get_config_path(skill.name) or "",
        "tool_refs": list(skill.tool_refs),
        "includes": [inc.name for inc in skill.includes],
        "mcp_servers": [m.server for m in skill.mcp_servers],
        "agent_skills": [a.path for a in skill.agent_skills],
        "sources": sources,
    }
    created = service.get_created_at(skill.name)
    if created:
        result["created"] = created.isoformat()
    updated = service.get_updated_at(skill.name)
    if updated:
        result["updated"] = updated.isoformat()
    return result


def _tool_count(skill: Skill) -> int:
    """Count the total direct tool references in a skill."""
    count = len(skill.tool_refs)
    count += len(skill.anonymous_tools)
    for mcp in skill.mcp_servers:
        if mcp.tools:
            count += len(mcp.tools)
    count += len(skill.agent_skills)
    return count


# ---------------------------------------------------------------------------
# Rich output helpers
# ---------------------------------------------------------------------------


def _print_skill_registered(
    skill: Skill,
    service: SkillService,
    config_path: str | None,
    is_new: bool,
) -> None:
    """Print rich output after skill add."""
    created = service.get_created_at(skill.name)
    updated = service.get_updated_at(skill.name)
    title = "Skill Registered" if is_new else "Skill Updated"
    ts = created if is_new else updated

    details = (
        f"[bold]Name:[/bold] {skill.name}\n"
        f"[bold]Description:[/bold] {skill.description}\n"
        f"[bold]Config:[/bold] {config_path or '(unknown)'}\n"
        f"[bold]{'Created' if is_new else 'Updated'}:[/bold] "
        f"{ts.strftime('%Y-%m-%d %H:%M') if ts else 'unknown'}"
    )
    console.print(Panel(details, title=title, expand=False))

    # Includes panel
    if skill.includes:
        includes_lines: list[str] = []
        for inc in skill.includes:
            status = "registered" if inc.name in service._skills else "not registered"
            includes_lines.append(f"[blue]{inc.name}[/blue] ({status})")
        console.print(Panel("\n".join(includes_lines), title="Includes", expand=False))

    # Tool Sources panel
    sources = service.get_source_types(skill)
    if sources:
        source_table = Table(show_header=True, show_edge=False, pad_edge=False)
        source_table.add_column("Source", style="cyan")
        source_table.add_column("Count", justify="right")
        total = 0
        if "builtin" in sources:
            c = len(skill.tool_refs)
            total += c
            source_table.add_row("builtin", str(c))
        if "mcp" in sources:
            c = sum(len(m.tools) if m.tools else 0 for m in skill.mcp_servers)
            total += c
            source_table.add_row("mcp", str(c))
        if "agent_skill" in sources:
            c = len(skill.agent_skills)
            total += c
            source_table.add_row("agent_skill", str(c))
        if "custom" in sources:
            c = len(skill.anonymous_tools)
            total += c
            source_table.add_row("custom", str(c))
        source_table.add_row("[yellow]Total[/yellow]", str(total))
        console.print(Panel(source_table, title="Tool Sources", expand=False))

    # MCP Servers panel
    if skill.mcp_servers:
        mcp_lines: list[str] = []
        for mcp in skill.mcp_servers:
            tool_count = len(mcp.tools) if mcp.tools else 0
            mcp_lines.append(
                f"[green]{mcp.server}:[/green] validated ({tool_count} tools)"
            )
        console.print(Panel("\n".join(mcp_lines), title="MCP Servers", expand=False))

    total_tools = _tool_count(skill)
    verb = "registered" if is_new else "updated"
    console.print(f"[green]✓ OK[/green] Skill {verb} with {total_tools} tools")


def _print_skill_update_changes(
    old_skill: Skill,
    new_skill: Skill,
    service: SkillService,
) -> None:
    """Print Changes panel when updating a skill."""
    old_tools = set(old_skill.tool_refs)
    new_tools = set(new_skill.tool_refs)
    added = len(new_tools - old_tools)
    removed = len(old_tools - new_tools)
    modified = len(old_tools & new_tools)

    old_includes = {i.name for i in old_skill.includes}
    new_includes = {i.name for i in new_skill.includes}
    includes_changed = old_includes != new_includes

    old_mcp = {m.server for m in old_skill.mcp_servers}
    new_mcp = {m.server for m in new_skill.mcp_servers}
    mcp_changed = old_mcp != new_mcp

    changes = (
        f"[green]Tools Added:[/green] {added}\n"
        f"[yellow]Tools Removed:[/yellow] {removed}\n"
        f"[blue]Tools Modified:[/blue] {modified}\n"
        f"[blue]Includes Changed:[/blue] {'yes' if includes_changed else 'no'}\n"
        f"[blue]MCP Servers Changed:[/blue] {'yes' if mcp_changed else 'no'}"
    )
    console.print(Panel(changes, title="Changes", expand=False))

    # Affected actors (empty in in-memory mode)
    dependents = service.get_dependents(new_skill.name)
    dependent_skills = dependents.get("skills", [])
    if dependent_skills:
        n_deps = len(dependent_skills)
        actor_lines = [f"[yellow]Warning:[/yellow] {n_deps} skills include this skill:"]
        for dep in dependent_skills:
            actor_lines.append(f"- {dep}")
        actor_lines.append("These will pick up changes on next use")
        console.print(
            Panel("\n".join(actor_lines), title="Affected Actors", expand=False)
        )


def _print_skill_detail(skill: Skill, service: SkillService) -> None:
    """Print rich output for skill show."""
    created = service.get_created_at(skill.name)
    updated = service.get_updated_at(skill.name)
    config_path = service.get_config_path(skill.name)

    created_str = created.strftime("%Y-%m-%d %H:%M") if created else "unknown"
    updated_str = updated.strftime("%Y-%m-%d %H:%M") if updated else "unknown"
    details = (
        f"[bold]Name:[/bold] {skill.name}\n"
        f"[bold]Description:[/bold] {skill.description}\n"
        f"[bold]Config:[/bold] {config_path or '(unknown)'}\n"
        f"[bold]Created:[/bold] {created_str}\n"
        f"[bold]Updated:[/bold] {updated_str}"
    )
    console.print(Panel(details, title="Skill Details", expand=False))

    # Includes panel
    if skill.includes:
        inc_lines: list[str] = []
        for inc in skill.includes:
            inc_skill = service._skills.get(inc.name)
            if inc_skill:
                tc = _tool_count(inc_skill)
                src = ", ".join(service.get_source_types(inc_skill))
                inc_lines.append(f"[blue]{inc.name}[/blue] → {tc} tools ({src})")
            else:
                inc_lines.append(f"[blue]{inc.name}[/blue] → (not registered)")
        console.print(
            Panel(
                "\n".join(inc_lines),
                title=f"Includes ({len(skill.includes)})",
                expand=False,
            )
        )

    # Direct Tools table
    direct_tools: list[tuple[str, str, str, str]] = []
    for ref in skill.tool_refs:
        direct_tools.append((ref, "builtin", "—", "—"))
    for mcp in skill.mcp_servers:
        if mcp.tools:
            for tool_name in mcp.tools:
                direct_tools.append((tool_name, f"mcp:{mcp.server}", "—", "—"))
    for agent in skill.agent_skills:
        direct_tools.append((agent.path, "agent_skill", "—", "—"))
    for idx, inline in enumerate(skill.anonymous_tools):
        direct_tools.append(
            (
                f"_anon_{idx}",
                "custom",
                "no" if inline.capability and inline.capability.read_only else "—",
                "—",
            )
        )

    if direct_tools:
        table = Table(
            title=f"Direct Tools ({len(direct_tools)})",
            show_header=True,
        )
        table.add_column("Name", style="cyan")
        table.add_column("Source", style="magenta")
        table.add_column("Writes")
        table.add_column("Checkpoint")
        for name, src, writes, checkpoint in direct_tools:
            table.add_row(name, src, writes, checkpoint)
        console.print(table)

    # MCP Servers panel
    if skill.mcp_servers:
        mcp_lines: list[str] = []
        for mcp in skill.mcp_servers:
            tc = len(mcp.tools) if mcp.tools else 0
            transport = getattr(mcp, "transport", "stdio")
            mcp_lines.append(
                f"[green]{mcp.server}:[/green] {transport}, {tc} tools, connected"
            )
        console.print(
            Panel(
                "\n".join(mcp_lines),
                title=f"MCP Servers ({len(skill.mcp_servers)})",
                expand=False,
            )
        )

    # Capability Summary
    try:
        summary = service.compute_capability_summary(skill.name)
        cap_text = (
            f"[blue]Total Tools:[/blue] {summary.total_tools}\n"
            f"[green]Read-Only:[/green] {summary.read_only_tools}\n"
            f"[yellow]Writes:[/yellow] {summary.write_tools}\n"
            f"[green]Checkpointable:[/green] {summary.checkpointable_tools}\n"
            f"[yellow]Has Side Effects:[/yellow] {summary.has_side_effects}"
        )
        console.print(Panel(cap_text, title="Capability Summary", expand=False))
    except (ValueError, KeyError):
        pass

    # Referenced By panel
    dependents = service.get_dependents(skill.name)
    dep_skills = dependents.get("skills", [])
    dep_actors = dependents.get("actors", [])
    if dep_skills or dep_actors:
        ref_lines: list[str] = []
        if dep_actors:
            ref_lines.append(f"[blue]Actors:[/blue] {', '.join(dep_actors)}")
        if dep_skills:
            ref_lines.append(f"[blue]Skills:[/blue] {', '.join(dep_skills)}")
        console.print(Panel("\n".join(ref_lines), title="Referenced By", expand=False))

    console.print("[green]✓ OK[/green] Skill loaded")


def _print_skill_tools(
    skill: Skill,
    entries: list[ResolvedToolEntry],
    service: SkillService,
) -> None:
    """Print the flattened tool list for a skill."""
    table = Table(
        title=f"Tools for {skill.name}",
        show_header=True,
    )
    table.add_column("Tool", style="cyan")
    table.add_column("Source", style="magenta")
    table.add_column("From Skill")
    table.add_column("Read-Only", justify="center")
    table.add_column("Writes", justify="center")
    table.add_column("Checkpoint")

    from_includes = 0
    direct = 0
    for entry in entries:
        is_direct = entry.source_skill == skill.name
        from_label = "(direct)" if is_direct else entry.source_skill
        if is_direct:
            direct += 1
        else:
            from_includes += 1

        # Source type determination
        source_type = "builtin"
        if entry.name.startswith("mcp:"):
            source_type = entry.name.split("/")[0]
        elif entry.name.startswith("agent_skill:"):
            source_type = "agent_skill"
        elif entry.is_inline:
            source_type = "custom"

        read_only = "—"
        writes = "—"
        checkpoint = "—"
        if entry.is_inline and entry.inline_tool and entry.inline_tool.capability:
            cap = entry.inline_tool.capability
            read_only = "✓" if cap.read_only else "—"
            writes = "✓" if cap.writes else "—"
            checkpoint = "yes" if cap.checkpointable else "no"

        table.add_row(
            entry.name, source_type, from_label, read_only, writes, checkpoint
        )

    console.print(table)

    # Summary panel
    summary = service.compute_capability_summary(skill.name)
    summary_text = (
        f"[blue]Total:[/blue] {summary.total_tools}\n"
        f"[green]From Includes:[/green] {from_includes}\n"
        f"[blue]Direct:[/blue] {direct}\n"
        f"[green]Read-Only:[/green] {summary.read_only_tools}\n"
        f"[yellow]Writes:[/yellow] {summary.write_tools}\n"
        f"[green]Checkpointable:[/green] {summary.checkpointable_tools}"
    )
    console.print(Panel(summary_text, title="Summary", expand=False))
    console.print(f"[green]✓ OK[/green] {summary.total_tools} tools listed")


# ---------------------------------------------------------------------------
# CLI commands
# ---------------------------------------------------------------------------


@app.command("add")
def add(
    config: Annotated[
        Path,
        typer.Option(
            "--config",
            "-c",
            help="Path to the skill YAML configuration file",
            exists=False,
        ),
    ],
    update: Annotated[
        bool,
        typer.Option(
            "--update",
            help="Allow overwriting an existing skill registration",
        ),
    ] = False,
    fmt: Annotated[
        str,
        typer.Option(
            "--format",
            "-f",
            help=_FORMAT_HELP,
        ),
    ] = "rich",
) -> None:
    """Register a new skill from a YAML configuration file.

    Skills are defined entirely by their YAML config. The ``name`` field in the
    config determines the skill's registered name.

    Examples:
        agents skill add --config ./skills/devops-toolkit.yaml
        agents skill add --config ./skills/devops-toolkit-v2.yaml --update
    """
    try:
        schema = SkillConfigSchema.from_yaml_file(config)

        service = _get_skill_service()

        # Capture old skill for diff if updating
        old_skill: Skill | None = None
        if update and schema.name in service._skills:
            old_skill = service._skills[schema.name]

        skill, is_new = service.add_skill(
            schema,
            config_path=str(config),
            update=update,
        )

        if fmt != OutputFormat.RICH.value:
            data = _skill_spec_dict(skill, service)
            data["is_new"] = is_new
            typer.echo(format_output(data, fmt))
            return

        _print_skill_registered(skill, service, str(config), is_new)

        if not is_new and old_skill is not None:
            _print_skill_update_changes(old_skill, skill, service)

    except FileNotFoundError as e:
        console.print(f"[red]Config file error:[/red] {e}")
        raise typer.Abort() from e
    except PydanticValidationError as e:
        console.print(f"[red]Schema validation error:[/red] {e}")
        raise typer.Abort() from e
    except ValueError as e:
        error_msg = str(e)
        console.print(f"[red]✗ Error:[/red] {error_msg}")
        if "already registered" in error_msg:
            console.print(
                "\nTo overwrite, re-run with"
                " [cyan]--update[/cyan]:\n\n"
                f"  agents skill add --config"
                f" {config} --update"
            )
        raise typer.Abort() from e


@app.command("remove")
def remove(
    name: Annotated[
        str,
        typer.Argument(help="Namespaced skill name to remove"),
    ],
    yes: Annotated[
        bool,
        typer.Option(
            "--yes",
            "-y",
            help="Skip confirmation prompt",
        ),
    ] = False,
    fmt: Annotated[
        str,
        typer.Option(
            "--format",
            "-f",
            help=_FORMAT_HELP,
        ),
    ] = "rich",
) -> None:
    """Remove a registered skill.

    The skill is removed from the registry. A confirmation prompt is shown
    unless ``--yes`` is provided. Dependency information is displayed.

    Examples:
        agents skill remove local/devops-toolkit
        agents skill remove local/devops-toolkit --yes
    """
    try:
        service = _get_skill_service()

        # Pre-check existence
        skill = service.get_skill(name)

        if not yes:
            confirm = typer.confirm(f"Remove skill {name}?", default=False)
            if not confirm:
                console.print("[yellow]Aborted.[/yellow]")
                raise typer.Abort()

        # Compute dependency info before removal
        dependents = service.get_dependents(name)
        tool_c = _tool_count(skill)
        mcp_c = len(skill.mcp_servers)

        service.remove_skill(name)

        if fmt != OutputFormat.RICH.value:
            data = {"name": name, "removed": True, "tools_removed": tool_c}
            typer.echo(format_output(data, fmt))
            return

        # Rich output
        removed_details = (
            f"[bold]Name:[/bold] {name}\n"
            f"[yellow]Tools:[/yellow] {tool_c} removed from registry\n"
            f"[blue]MCP Servers:[/blue] {mcp_c} connection closed"
        )
        console.print(Panel(removed_details, title="Skill Removed", expand=False))

        dep_skills = dependents.get("skills", [])
        dep_actors = dependents.get("actors", [])
        if dep_skills or dep_actors:
            dep_lines: list[str] = []
            if dep_skills:
                ns = len(dep_skills)
                dep_lines.append(
                    f"[yellow]Warning:[/yellow] {ns} skill(s) include this skill:"
                )
                for d in dep_skills:
                    dep_lines.append(f"- {d}")
            if dep_actors:
                na = len(dep_actors)
                dep_lines.append(
                    f"[yellow]Warning:[/yellow] {na} actor(s) reference this skill:"
                )
                for a in dep_actors:
                    dep_lines.append(f"- {a}")
            console.print(
                Panel("\n".join(dep_lines), title="Dependency Check", expand=False)
            )

        console.print("[green]✓ OK[/green] Skill removed")

    except KeyError as exc:
        console.print(f"[red]Skill not found:[/red] {name}")
        raise typer.Abort() from exc


@app.command("list")
def list_skills(
    namespace: Annotated[
        str | None,
        typer.Option(
            "--namespace",
            "-n",
            help="Filter by namespace",
        ),
    ] = None,
    source: Annotated[
        str | None,
        typer.Option(
            "--source",
            "-s",
            help="Filter by tool source type (mcp, agent_skill, builtin, custom)",
        ),
    ] = None,
    fmt: Annotated[
        str,
        typer.Option(
            "--format",
            "-f",
            help=_FORMAT_HELP,
        ),
    ] = "rich",
) -> None:
    """List all registered skills.

    Optionally filter by namespace and/or tool source type.

    Examples:
        agents skill list
        agents skill list --namespace local
        agents skill list --source mcp
        agents skill list --format json
    """
    service = _get_skill_service()
    skills = service.list_skills(namespace=namespace, source=source)

    if not skills:
        console.print("[yellow]No skills found.[/yellow]")
        console.print("Register one with 'agents skill add --config <file>'")
        return

    if fmt != OutputFormat.RICH.value:
        data = []
        for s in skills:
            spec_dict = _skill_spec_dict(s, service)
            # Add capability summary fields
            try:
                summary = service.compute_capability_summary(s.name)
                spec_dict["tool_count"] = summary.total_tools
                spec_dict["capability_summary"] = {
                    "total_tools": summary.total_tools,
                    "read_only_tools": summary.read_only_tools,
                    "write_tools": summary.write_tools,
                    "checkpointable_tools": summary.checkpointable_tools,
                    "has_side_effects": summary.has_side_effects,
                }
            except (ValueError, KeyError):
                spec_dict["tool_count"] = _tool_count(s)
                spec_dict["capability_summary"] = None
            data.append(spec_dict)
        typer.echo(format_output(data, fmt))
        return

    # Rich table
    table = Table(title=f"Skills ({len(skills)} total)", show_header=True)
    table.add_column("Name", style="cyan")
    table.add_column("Description")
    table.add_column("Tools", justify="right")
    table.add_column("Includes", justify="right")
    table.add_column("Sources")

    total_tools = 0
    local_count = 0
    server_count = 0

    for skill in skills:
        tc = _tool_count(skill)
        total_tools += tc
        sources = service.get_source_types(skill)
        if skill.namespace == "local":
            local_count += 1
        else:
            server_count += 1

        # Truncate long descriptions
        desc = (
            skill.description[:50] + "..."
            if len(skill.description) > 50
            else skill.description
        )

        table.add_row(
            skill.name,
            desc,
            str(tc),
            str(len(skill.includes)),
            ", ".join(sources) if sources else "—",
        )

    console.print(table)

    # Summary panel
    summary_text = (
        f"[yellow]Total:[/yellow] {len(skills)}\n"
        f"[blue]Local:[/blue] {local_count}\n"
        f"[blue]Server:[/blue] {server_count}\n"
        f"[blue]Total Tools:[/blue] {total_tools}"
    )
    console.print(Panel(summary_text, title="Summary", expand=False))
    console.print(f"[green]✓ OK[/green] {len(skills)} skills listed")


@app.command("show")
def show(
    name: Annotated[
        str,
        typer.Argument(help="Namespaced skill name to show"),
    ],
    fmt: Annotated[
        str,
        typer.Option(
            "--format",
            "-f",
            help=_FORMAT_HELP,
        ),
    ] = "rich",
) -> None:
    """Show full details for a registered skill.

    Displays skill metadata, includes, direct tools, MCP servers,
    capability summary, and referenced-by information.

    Examples:
        agents skill show local/devops-toolkit
        agents skill show local/devops-toolkit --format json
    """
    try:
        service = _get_skill_service()
        skill = service.get_skill(name)

        if fmt != OutputFormat.RICH.value:
            data = _skill_spec_dict(skill, service)
            # Add capability summary
            try:
                summary = service.compute_capability_summary(name)
                data["tool_count"] = summary.total_tools
                data["capability_summary"] = {
                    "total_tools": summary.total_tools,
                    "read_only_tools": summary.read_only_tools,
                    "write_tools": summary.write_tools,
                    "checkpointable_tools": summary.checkpointable_tools,
                    "has_side_effects": summary.has_side_effects,
                }
            except (ValueError, KeyError):
                data["tool_count"] = _tool_count(skill)
                data["capability_summary"] = None
            typer.echo(format_output(data, fmt))
            return

        _print_skill_detail(skill, service)

    except KeyError as exc:
        console.print(f"[red]Skill not found:[/red] {name}")
        raise typer.Abort() from exc


@app.command("tools")
def tools(
    name: Annotated[
        str,
        typer.Argument(help="Namespaced skill name to resolve tools for"),
    ],
    refresh: Annotated[
        bool,
        typer.Option(
            "--refresh",
            help="Re-scan Agent Skills paths before resolving tools",
        ),
    ] = False,
    fmt: Annotated[
        str,
        typer.Option(
            "--format",
            "-f",
            help=_FORMAT_HELP,
        ),
    ] = "rich",
) -> None:
    """Show the flattened tool list for a skill.

    Includes tools inherited from included skills. Displays tool source,
    origin skill, read-only/write/checkpoint flags, and a summary.

    Use ``--refresh`` to re-scan Agent Skills discovery paths before
    resolving tools (picks up new or changed Agent Skills folders).

    Examples:
        agents skill tools local/devops-toolkit
        agents skill tools local/devops-toolkit --refresh
        agents skill tools local/devops-toolkit --format json
    """
    try:
        service = _get_skill_service()

        if refresh:
            from cleveragents.application.services.config_service import (
                ConfigService,
            )

            config_svc = ConfigService()
            resolved = config_svc.resolve("skills.agent_skills_paths")
            raw_paths: str = str(resolved.value) if resolved.value else ""
            if raw_paths:
                from cleveragents.skills.discovery import (
                    discover_agent_skills,
                    parse_agent_skills_paths,
                )

                paths = parse_agent_skills_paths(raw_paths)
                result = discover_agent_skills(paths)
                if result.discovered:
                    console.print(
                        f"[blue]Refreshed:[/blue] Found "
                        f"{len(result.discovered)} agent skill(s)"
                    )
                if result.errors:
                    for err in result.errors:
                        console.print(f"[yellow]Warning:[/yellow] {err}")

        skill, entries = service.resolve_tools(name)

        if fmt != OutputFormat.RICH.value:
            # Compute capability summary for structured output
            summary = service.compute_capability_summary(name)

            data: list[dict[str, Any]] = []
            for entry in entries:
                # Determine source type for metadata
                source_type = "builtin"
                if entry.name.startswith("mcp:"):
                    source_type = "mcp"
                elif entry.name.startswith("agent_skill:"):
                    source_type = "agent_skills"
                elif entry.is_inline:
                    source_type = "custom"

                data.append(
                    {
                        "name": entry.name,
                        "source_skill": entry.source_skill,
                        "source": source_type,
                        "is_direct": entry.source_skill == skill.name,
                        "is_inline": entry.is_inline,
                    }
                )

            # Include capability summary in output
            output = {
                "skill_name": skill.name,
                "tools": data,
                "capability_summary": {
                    "total_tools": summary.total_tools,
                    "read_only_tools": summary.read_only_tools,
                    "write_tools": summary.write_tools,
                    "checkpointable_tools": summary.checkpointable_tools,
                    "has_side_effects": summary.has_side_effects,
                },
            }
            typer.echo(format_output(output, fmt))
            return

        _print_skill_tools(skill, entries, service)

    except KeyError as exc:
        console.print(f"[red]Skill not found:[/red] {name}")
        raise typer.Abort() from exc
    except ValueError as e:
        console.print(f"[red]Resolution error:[/red] {e}")
        raise typer.Abort() from e


@app.command("refresh")
def refresh(
    name: Annotated[
        str | None,
        typer.Argument(help="Namespaced skill name to refresh (omit with --all)"),
    ] = None,
    all_skills: Annotated[
        bool,
        typer.Option(
            "--all",
            help="Refresh all registered skills",
        ),
    ] = False,
    fmt: Annotated[
        str,
        typer.Option(
            "--format",
            "-f",
            help=_FORMAT_HELP,
        ),
    ] = "rich",
) -> None:
    """Recompute tool flattening and sync MCP-backed skills.

    Refreshes a skill's computed tool set by re-running the flattening
    algorithm. For MCP-backed skills, re-enumerates available tools from
    MCP servers. For Agent Skills, re-scans configured discovery paths.

    Either specify a skill name OR use ``--all`` to refresh all skills.

    Examples:
        agents skill refresh local/devops-toolkit
        agents skill refresh --all
        agents skill refresh local/linear-tracker --format json
    """
    try:
        service = _get_skill_service()

        # Validate mutually exclusive arguments
        if name and all_skills:
            console.print("[red]Error:[/red] Cannot specify both <name> and --all")
            raise typer.Abort()

        if not name and not all_skills:
            console.print("[red]Error:[/red] Must specify either <name> or --all")
            raise typer.Abort()

        # Determine which skills to refresh
        if all_skills:
            skills_to_refresh = service.list_skills()
            if not skills_to_refresh:
                console.print("[yellow]No skills registered.[/yellow]")
                return
        else:
            # Single skill - name guaranteed str at this point
            if name is None:
                console.print("[red]Error:[/red] Must specify either <name> or --all")
                raise typer.Abort()
            skill = service.get_skill(name)
            skills_to_refresh = [skill]

        # Perform refresh for Agent Skills discovery if configured
        agent_skills_refreshed = False
        from cleveragents.application.services.config_service import (
            ConfigService,
        )

        config_svc = ConfigService()
        resolved = config_svc.resolve("skills.agent_skills_paths")
        raw_paths: str = str(resolved.value) if resolved.value else ""

        if raw_paths:
            from cleveragents.skills.discovery import (
                discover_agent_skills,
                parse_agent_skills_paths,
            )

            paths = parse_agent_skills_paths(raw_paths)
            result = discover_agent_skills(paths)
            agent_skills_refreshed = True

            if result.discovered and fmt == OutputFormat.RICH.value:
                console.print(
                    f"[blue]Agent Skills:[/blue] Discovered "
                    f"{len(result.discovered)} skill(s)"
                )
            if result.errors and fmt == OutputFormat.RICH.value:
                for err in result.errors:
                    console.print(f"[yellow]Warning:[/yellow] {err}")

        # Refresh each skill (recompute flattening)
        refreshed_data: list[dict[str, Any]] = []
        mcp_errors: list[str] = []

        for skill in skills_to_refresh:
            try:
                # Resolve tools to trigger recomputation
                _, _entries = service.resolve_tools(skill.name)
                summary = service.compute_capability_summary(skill.name)

                # Check for MCP servers
                has_mcp = len(skill.mcp_servers) > 0
                mcp_status = "synced" if has_mcp else "n/a"

                # For MCP skills, we would sync here if MCP adapter was available
                # Currently, we just mark as refreshed
                if has_mcp:
                    # TODO: When MCP adapter integration is available, call
                    # mcp_adapter.refresh_tools() for each server
                    pass

                refreshed_data.append(
                    {
                        "name": skill.name,
                        "total_tools": summary.total_tools,
                        "includes": len(skill.includes),
                        "mcp_servers": len(skill.mcp_servers),
                        "mcp_status": mcp_status,
                        "agent_skills": len(skill.agent_skills),
                        "read_only_tools": summary.read_only_tools,
                        "write_tools": summary.write_tools,
                        "checkpointable_tools": summary.checkpointable_tools,
                    }
                )

            except ValueError as e:
                err_msg = f"{skill.name}: {e}"
                mcp_errors.append(err_msg)
                refreshed_data.append(
                    {
                        "name": skill.name,
                        "error": str(e),
                        "mcp_status": "failed",
                    }
                )

        # Output results
        if fmt != OutputFormat.RICH.value:
            output: dict[str, Any] = {
                "refreshed": len(refreshed_data),
                "agent_skills_refreshed": agent_skills_refreshed,
                "skills": refreshed_data,
            }
            if mcp_errors:
                output["errors"] = mcp_errors
            typer.echo(format_output(output, fmt))
            return

        # Rich output
        if len(skills_to_refresh) == 1:
            # Single skill detail
            skill_data = refreshed_data[0]
            if "error" in skill_data:
                console.print(f"[red]Refresh failed:[/red] {skill_data['error']}")
                raise typer.Abort()

            details = (
                f"[bold]Name:[/bold] {skill_data['name']}\n"
                f"[blue]Total Tools:[/blue] {skill_data['total_tools']}\n"
                f"[blue]Includes:[/blue] {skill_data['includes']}\n"
                f"[blue]MCP Servers:[/blue] {skill_data['mcp_servers']} "
                f"({skill_data['mcp_status']})\n"
                f"[blue]Agent Skills:[/blue] {skill_data['agent_skills']}\n"
                f"[green]Read-Only:[/green] {skill_data['read_only_tools']}\n"
                f"[yellow]Writes:[/yellow] {skill_data['write_tools']}\n"
                f"[green]Checkpointable:[/green] "
                f"{skill_data['checkpointable_tools']}"
            )
            console.print(Panel(details, title="Skill Refreshed", expand=False))
            console.print(
                f"[green]✓ OK[/green] Skill refreshed with "
                f"{skill_data['total_tools']} tools"
            )
        else:
            # Multiple skills table
            table = Table(
                title=f"Skills Refreshed ({len(refreshed_data)})",
                show_header=True,
            )
            table.add_column("Name", style="cyan")
            table.add_column("Tools", justify="right")
            table.add_column("Includes", justify="right")
            table.add_column("MCP", justify="center")
            table.add_column("Status")

            for skill_data in refreshed_data:
                status = "✓" if "error" not in skill_data else "✗"
                status_style = "green" if "error" not in skill_data else "red"
                mcp_count = skill_data.get("mcp_servers", 0)
                table.add_row(
                    skill_data["name"],
                    str(skill_data.get("total_tools", "—")),
                    str(skill_data.get("includes", "—")),
                    str(mcp_count) if mcp_count > 0 else "—",
                    f"[{status_style}]{status}[/{status_style}]",
                )

            console.print(table)

            if mcp_errors:
                error_panel = "\n".join(f"[red]•[/red] {err}" for err in mcp_errors)
                console.print(Panel(error_panel, title="Errors", expand=False))
                console.print(
                    f"[yellow]⚠ Warning[/yellow] {len(skills_to_refresh)} "
                    f"skills refreshed with {len(mcp_errors)} error(s)"
                )
            else:
                console.print(
                    f"[green]✓ OK[/green] {len(skills_to_refresh)} skills refreshed"
                )

    except KeyError as exc:
        console.print(f"[red]Skill not found:[/red] {name}")
        raise typer.Abort() from exc
    except Exception as e:
        console.print(f"[red]Refresh error:[/red] {e}")
        raise typer.Abort() from e
