"""Resource management commands for CleverAgents CLI.

The ``agents resource`` command group manages resource types and resource
instances in the CleverAgents resource registry.

## Resource Type Commands

| Command                        | Description                           |
|--------------------------------|---------------------------------------|
| ``agents resource type add``   | Register custom type from YAML config |
| ``agents resource type remove``| Remove a custom type                  |
| ``agents resource type list``  | List all registered types             |
| ``agents resource type show``  | Show type details                     |

## Resource Instance Commands

| Command                          | Description                               |
|----------------------------------|-------------------------------------------|
| ``agents resource add``          | Add a resource instance of a given type   |
| ``agents resource list``         | List resources with optional type filter  |
| ``agents resource show``         | Show resource details (by name or ULID)   |
| ``agents resource remove``       | Remove a resource from the registry       |
| ``agents resource tree``         | Display resource DAG as a tree            |
| ``agents resource inspect``      | Show detailed resource info               |
| ``agents resource link-child``   | Create DAG edge between resources         |
| ``agents resource unlink-child`` | Remove DAG edge between resources         |

## Example Usage

```bash
# Add a git-checkout resource
agents resource add git-checkout local/my-repo --path /home/user/repo

# List all resources
agents resource list

# Show details
agents resource show local/my-repo

# Display resource tree
agents resource tree local/my-repo --depth 3

# Inspect a resource
agents resource inspect local/my-repo --tree

# Link resources in DAG
agents resource link-child local/parent local/child
agents resource unlink-child local/parent local/child
```

Based on ``implementation_plan.md`` -- Tasks B0.cli.resources, B1.cli.
"""

from __future__ import annotations

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

import typer
from rich.panel import Panel
from rich.table import Table

from cleveragents.application.container import get_container
from cleveragents.application.services.resource_registry_service import (
    ResourceRegistryService,
)
from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.cli.renderers import _get_console
from cleveragents.core.exceptions import (
    CleverAgentsError,
    NotFoundError,
    ValidationError,
)
from cleveragents.domain.models.core.container_lifecycle import (
    ContainerLifecycleState,
)
from cleveragents.resource.handlers.devcontainer import (
    get_lifecycle_tracker,
    rebuild_container,
    stop_container,
)
from cleveragents.resource.inheritance import ResourceTypeParentRemovalError

logger = logging.getLogger(__name__)

# Main resource app
app = typer.Typer(
    help="Manage resources and resource types in the resource registry.",
)

# Sub-app for ``agents resource type ...``
type_app = typer.Typer(
    help="Manage resource types (built-in and custom).",
)
app.add_typer(type_app, name="type")

console = _get_console()

_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"

# Resource types that support --mount and --clone-into flags.
_CONTAINER_TYPES = frozenset({"container-instance", "devcontainer-instance"})


def _get_registry_service() -> ResourceRegistryService:
    """Get the ResourceRegistryService from the DI container."""
    container = get_container()
    service: ResourceRegistryService = container.resource_registry_service()
    return service


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


def _resource_type_dict(spec: Any) -> dict[str, object]:
    """Convert a ResourceTypeSpec to a plain dict for output formatting."""
    cli_args_list: list[dict[str, object]] = []
    for arg in getattr(spec, "cli_args", []) or []:
        cli_args_list.append(
            {
                "name": arg.name,
                "type": arg.type,
                "required": arg.required,
                "description": arg.description,
                "default": arg.default,
            }
        )
    result: dict[str, object] = {
        "name": spec.name,
        "description": spec.description or "",
        "resource_kind": str(spec.resource_kind),
        "sandbox_strategy": str(spec.sandbox_strategy),
        "user_addable": spec.user_addable,
        "built_in": spec.built_in,
        "inherits": getattr(spec, "inherits", None),
        "cli_args": cli_args_list,
        "parent_types": spec.parent_types,
        "child_types": spec.child_types,
        "handler": spec.handler,
        "capabilities": spec.capabilities,
        "equivalence": getattr(spec, "equivalence", None),
    }
    return result


def _get_lifecycle_state_str(resource: Any) -> str | None:
    """Return the lifecycle state string for container resources, or None.

    For ``devcontainer-instance`` and ``container-instance`` resources,
    queries the lifecycle tracker and returns the current state as a
    human-readable string.  Returns ``None`` for all other resource types.

    Args:
        resource: A Resource domain object.

    Returns:
        Lifecycle state string (e.g. ``"detected (not built)"``, ``"running"``)
        or ``None`` if the resource type has no lifecycle state.
    """
    if resource.resource_type_name not in _CONTAINER_TYPES:
        return None
    try:
        tracker = get_lifecycle_tracker(resource.resource_id)
        state = tracker.current_state
        if state == ContainerLifecycleState.DETECTED:
            return "detected (not built)"
        return str(state)
    except (ValueError, KeyError):
        logger.warning(
            "Could not retrieve lifecycle tracker for resource %s",
            resource.resource_id,
        )
        return None


def _resource_dict(resource: Any) -> dict[str, object]:
    """Convert a Resource domain object to a plain dict for output formatting."""
    return {
        "resource_id": resource.resource_id,
        "name": resource.name,
        "type": resource.resource_type_name,
        "classification": str(resource.classification),
        "description": resource.description,
        "location": resource.location,
        "properties": resource.properties,
        "lifecycle_state": _get_lifecycle_state_str(resource),
        "created_at": resource.created_at.isoformat()
        if hasattr(resource.created_at, "isoformat")
        else str(resource.created_at),
        "updated_at": resource.updated_at.isoformat()
        if hasattr(resource.updated_at, "isoformat")
        else str(resource.updated_at),
    }


# ---------------------------------------------------------------------------
# Resource Type commands
# ---------------------------------------------------------------------------


@type_app.command("add")
def type_add(
    config: Annotated[
        Path,
        typer.Option(
            "--config",
            "-c",
            help="Path to the resource type YAML configuration file",
            exists=False,
        ),
    ],
    update: Annotated[
        bool,
        typer.Option("--update", help="Update if type already exists"),
    ] = False,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Register a custom resource type from a YAML configuration file.

    Examples:
        agents resource type add --config ./resource-types/my-type.yaml
        agents resource type add --config ./resource-types/my-type.yaml --update
    """
    try:
        service = _get_registry_service()

        if update:
            # Try to register; if it already exists, show a message
            try:
                spec = service.register_type(config)
            except ValidationError as exc:
                if "already exists" in str(exc):
                    console.print(
                        "[yellow]Resource type already exists. "
                        "Update mode is not yet fully supported.[/yellow]"
                    )
                    raise typer.Abort() from exc
                raise
        else:
            spec = service.register_type(config)

        if fmt != OutputFormat.RICH.value:
            data = _resource_type_dict(spec)
            console.print(format_output(data, fmt))
            return

        console.print(f"[green]Registered resource type:[/green] {spec.name}")
        _print_type_panel(spec)

    except FileNotFoundError as exc:
        console.print(f"[red]Config file not found:[/red] {exc}")
        raise typer.Abort() from exc
    except ValidationError as exc:
        console.print(f"[red]Validation error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except Exception as exc:
        if isinstance(exc, (typer.Abort, typer.Exit)):
            raise
        console.print(f"[red]Unexpected error:[/red] {exc}")
        raise typer.Abort() from exc


@type_app.command("remove")
def type_remove(
    name: Annotated[
        str,
        typer.Argument(help="Name of the resource type to remove"),
    ],
    yes: Annotated[
        bool,
        typer.Option("--yes", "-y", help="Skip confirmation prompt"),
    ] = False,
) -> None:
    """Remove a custom resource type. Built-in types cannot be removed.

    Examples:
        agents resource type remove local/my-type
        agents resource type remove --yes local/my-type
    """
    try:
        service = _get_registry_service()

        # Pre-check so we can show a friendly message before prompting
        spec = service.show_type(name)
        if spec.built_in:
            console.print(f"[red]Cannot remove built-in resource type:[/red] {name}")
            raise typer.Abort()

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

        service.remove_type(name)
        console.print(f"[green]Removed resource type:[/green] {name}")

    except NotFoundError as exc:
        console.print(f"[red]Resource type not found:[/red] {name}")
        raise typer.Abort() from exc
    except ResourceTypeParentRemovalError as exc:
        console.print(f"[red]{exc}[/red]")
        raise typer.Abort() from exc
    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except Exception as exc:
        if isinstance(exc, (typer.Abort, typer.Exit)):
            raise
        console.print(f"[red]Unexpected error:[/red] {exc}")
        raise typer.Abort() from exc


@type_app.command("list")
def type_list(
    regex: Annotated[
        str | None,
        typer.Argument(
            help="Optional regex to filter type names",
        ),
    ] = None,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """List all registered resource types (built-in and custom).

    An optional positional REGEX argument filters type names.

    Examples:
        agents resource type list
        agents resource type list "git.*"
        agents resource type list --format json
    """
    try:
        service = _get_registry_service()
        types = service.list_types()

        # Apply optional regex filter on type names.
        if regex is not None:
            try:
                pattern = re.compile(regex)
            except re.error as re_exc:
                console.print(f"[red]Invalid regex:[/red] {re_exc}")
                raise typer.Abort() from re_exc
            types = [t for t in types if pattern.search(t.name)]

        if not types:
            console.print("[yellow]No resource types registered.[/yellow]")
            return

        if fmt != OutputFormat.RICH.value:
            data = [_resource_type_dict(t) for t in types]
            console.print(format_output(data, fmt))
            return

        table = Table(title=f"Resource Types ({len(types)} total)")
        table.add_column("Name", style="cyan")
        table.add_column("Kind", style="blue")
        table.add_column("Sandbox", style="magenta")
        table.add_column("Inherits", style="green")
        table.add_column("Built-in", justify="center")
        table.add_column("User-addable", justify="center")
        table.add_column("Description", style="dim")

        for spec in types:
            desc = spec.description or ""
            if len(desc) > 50:
                desc = desc[:47] + "..."
            inherits = getattr(spec, "inherits", None) or ""
            table.add_row(
                spec.name,
                str(spec.resource_kind),
                str(spec.sandbox_strategy),
                inherits,
                "yes" if spec.built_in else "no",
                "yes" if spec.user_addable else "no",
                desc,
            )

        console.print(table)

    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except Exception as exc:
        if isinstance(exc, (typer.Abort, typer.Exit)):
            raise
        console.print(f"[red]Unexpected error:[/red] {exc}")
        raise typer.Abort() from exc


@type_app.command("show")
def type_show(
    name: Annotated[
        str,
        typer.Argument(help="Name of the resource type to show"),
    ],
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Show details of a registered resource type.

    Examples:
        agents resource type show git-checkout
        agents resource type show --format json local/my-type
    """
    try:
        service = _get_registry_service()
        spec = service.show_type(name)

        if fmt != OutputFormat.RICH.value:
            data = _resource_type_dict(spec)
            console.print(format_output(data, fmt))
            return

        _print_type_panel(spec)

    except NotFoundError as exc:
        console.print(f"[red]Resource type not found:[/red] {name}")
        raise typer.Abort() from exc
    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except Exception as exc:
        if isinstance(exc, (typer.Abort, typer.Exit)):
            raise
        console.print(f"[red]Unexpected error:[/red] {exc}")
        raise typer.Abort() from exc


def _print_type_panel(spec: Any) -> None:
    """Print a rich panel for a resource type."""
    args_display = ""
    cli_args = getattr(spec, "cli_args", []) or []
    if cli_args:
        lines: list[str] = []
        for arg in cli_args:
            req = "required" if arg.required else "optional"
            lines.append(f"  --{arg.name} ({arg.type}, {req})")
            if arg.description:
                lines.append(f"    {arg.description}")
        args_display = "\n".join(lines)
    else:
        args_display = "  (none)"

    inherits = getattr(spec, "inherits", None)
    inherits_display = inherits or "(none)"

    # Attempt to resolve the full inheritance chain for display.
    chain_display = "(root type)"
    if inherits is not None:
        try:
            service = _get_registry_service()
            chain = service.resolve_type_inheritance_chain(spec.name)
            chain_display = " -> ".join(chain)
        except Exception as chain_exc:  # CLI display; crash is worse than partial info
            logger.debug("Failed to resolve chain for %s: %s", spec.name, chain_exc)
            chain_display = f"{spec.name} -> {inherits} -> ..."

    details = (
        f"[bold]Name:[/bold] {spec.name}\n"
        f"[bold]Description:[/bold] {spec.description or '(none)'}\n"
        f"[bold]Inherits:[/bold] {inherits_display}\n"
        f"[bold]Inheritance Chain:[/bold] {chain_display}\n"
        f"[bold]Kind:[/bold] {spec.resource_kind}\n"
        f"[bold]Sandbox Strategy:[/bold] {spec.sandbox_strategy}\n"
        f"[bold]Built-in:[/bold] {'yes' if spec.built_in else 'no'}\n"
        f"[bold]User-addable:[/bold] {'yes' if spec.user_addable else 'no'}\n"
        f"[bold]Handler:[/bold] {spec.handler or '(none)'}\n"
        f"[bold]CLI Arguments:[/bold]\n{args_display}\n"
        f"[bold]Parent Types:[/bold] {', '.join(spec.parent_types) or '(none)'}\n"
        f"[bold]Child Types:[/bold] {', '.join(spec.child_types) or '(none)'}"
    )

    equivalence = getattr(spec, "equivalence", None)
    if equivalence is not None:
        criteria = equivalence.get("criteria", [])
        equiv_desc = equivalence.get("description", "")
        details += (
            f"\n[bold]Equivalence Criteria:[/bold] {', '.join(criteria)}"
            f"\n[bold]Equivalence Description:[/bold] {equiv_desc}"
        )

    console.print(Panel(details, title="Resource Type", expand=False))


# ---------------------------------------------------------------------------
# Resource instance commands
# ---------------------------------------------------------------------------


def _format_properties(
    properties: dict[str, Any] | None,
) -> str:
    """Format resource properties for rich display, with mount rendering."""
    if not properties:
        return "  (none)"
    lines: list[str] = []
    for k, v in properties.items():
        if k == "mounts":
            try:
                mounts = json.loads(str(v))
                for m in mounts:
                    mode_str = f" ({m['mode']})" if m.get("mode") else ""
                    lines.append(f"  mount: {m['source']}:{m['target']}{mode_str}")
            except (json.JSONDecodeError, TypeError):
                lines.append(f"  {k}: {v}")
        else:
            lines.append(f"  {k}: {v}")
    return "\n".join(lines) if lines else "  (none)"


def _parse_mount_spec(spec: str) -> dict[str, str]:
    """Parse a ``--mount`` flag value into a mount descriptor dict.

    Supported formats:
        - ``<resource-name>:<container-path>`` — resource-reference mount (rw)
        - ``<host-path>:<container-path>:<mode>`` — host-path mount (mode=rw|ro)

    Returns:
        Dict with keys: ``source``, ``target``, ``mode``, ``type``.
    """
    parts = spec.split(":")
    if len(parts) == 2:
        # Distinguish host-path (starts with /) from resource-reference.
        # Resource names use namespace/name (e.g. local/api-repo) but
        # never start with /.
        mount_type = "bind" if parts[0].startswith("/") else "resource"
        return {
            "source": parts[0],
            "target": parts[1],
            "mode": "rw",
            "type": mount_type,
        }
    if len(parts) == 3:
        # host-path:container-path:mode (host-path mount)
        mode = parts[2].lower()
        if mode not in ("rw", "ro"):
            raise typer.BadParameter(
                f"Invalid mount mode '{parts[2]}'. Must be 'rw' or 'ro'."
            )
        return {
            "source": parts[0],
            "target": parts[1],
            "mode": mode,
            "type": "bind",
        }
    raise typer.BadParameter(
        f"Invalid mount spec '{spec}'. Expected "
        "<source>:<target> or <source>:<target>:<mode>"
    )


@app.command("add")
def resource_add(
    type_name: Annotated[
        str,
        typer.Argument(help="Resource type name (e.g., git-checkout, fs-directory)"),
    ],
    name: Annotated[
        str,
        typer.Argument(help="Resource name (e.g., local/my-repo)"),
    ],
    path: Annotated[
        str | None,
        typer.Option("--path", "-p", help="Path for physical resources"),
    ] = None,
    branch: Annotated[
        str | None,
        typer.Option("--branch", "-b", help="Branch for git-checkout resources"),
    ] = None,
    description: Annotated[
        str | None,
        typer.Option("--description", "-d", help="Resource description"),
    ] = None,
    image: Annotated[
        str | None,
        typer.Option(
            "--image",
            help=(
                "Container image for container-instance resources. "
                "Required when --container-id is not provided."
            ),
        ),
    ] = None,
    container_id: Annotated[
        str | None,
        typer.Option(
            "--container-id",
            help=(
                "ID of an existing container to attach "
                "(alternative to --image for container-instance resources)."
            ),
        ),
    ] = None,
    mount: Annotated[
        list[str] | None,
        typer.Option(
            "--mount",
            help=(
                "Mount specification for container types. "
                "Formats: <resource>:<path> (resource-ref, rw) or "
                "<host-path>:<container-path>:<mode> (host-path, mode=rw|ro). "
                "Can be specified multiple times."
            ),
        ),
    ] = None,
    clone_into: Annotated[
        str | None,
        typer.Option(
            "--clone-into",
            help=(
                "Clone a repository into the container at a specified path. "
                "Format: <REPO_URL>:<CONTAINER_PATH>. "
                "Only valid for container types."
            ),
        ),
    ] = None,
    update: Annotated[
        bool,
        typer.Option(
            "--update",
            help="Re-register an existing resource (remove and re-add).",
        ),
    ] = False,
    read_only: Annotated[
        bool,
        typer.Option("--read-only", help="Mark resource as read-only"),
    ] = False,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Add a new resource instance of the specified type.

    Use ``--update`` to re-register an existing resource by name.

    Examples:
        agents resource add git-checkout local/my-repo --path /home/user/repo
        agents resource add fs-directory local/data --path /data --read-only
        agents resource add devcontainer-instance local/dc --path /project
        agents resource add container-instance local/ctr --image ubuntu:latest
        agents resource add container-instance local/ctr --container-id abc123def456
        agents resource add container-instance local/ctr --image node:18 \\
            --mount local/api-repo:/workspace \\
            --mount /var/shared/config:/config:ro
        agents resource add container-instance local/ctr --image node:18 \\
            --clone-into https://github.com/org/repo.git:/workspace/repo
        agents resource add git-checkout local/my-repo --path /tmp --update
    """
    try:
        # Validate --clone-into is only used with container resource types.
        _CONTAINER_TYPES = {"container-instance", "devcontainer-instance"}
        if clone_into is not None and type_name not in _CONTAINER_TYPES:
            allowed = ", ".join(sorted(_CONTAINER_TYPES))
            console.print(
                f"[red]Validation error:[/red] --clone-into is only "
                f"valid for container resource types ({allowed}), "
                f"not '{type_name}'"
            )
            raise typer.Abort()

        # Validate --clone-into format: REPO_URL:CONTAINER_PATH
        if clone_into is not None:
            parts = clone_into.rsplit(":", 1)
            if len(parts) != 2 or not parts[0].strip() or not parts[1].strip():
                console.print(
                    "[red]Validation error:[/red] --clone-into must be in "
                    "REPO_URL:CONTAINER_PATH format (e.g., "
                    "https://github.com/acme/api.git:/workspace)"
                )
                raise typer.Abort()

        # Validate --container-id usage rules.
        if container_id is not None and image is not None:
            console.print(
                "[red]Validation error:[/red] --container-id and --image are "
                "mutually exclusive; provide one or the other, not both."
            )
            raise typer.Abort()
        if container_id is not None and type_name != "container-instance":
            console.print(
                f"[red]Validation error:[/red] --container-id is only valid "
                f"for container-instance resources, not '{type_name}'."
            )
            raise typer.Abort()
        if type_name == "container-instance" and container_id is None and image is None:
            console.print(
                "[red]Validation error:[/red] container-instance resources "
                "require either --image or --container-id."
            )
            raise typer.Abort()

        service = _get_registry_service()

        # --update: remove existing resource before re-registering
        if update:
            try:
                existing = service.show_resource(name)
                # Remove the existing resource so we can re-register
                session = service._session()
                try:
                    from cleveragents.infrastructure.database.models import (
                        ResourceEdgeModel,
                        ResourceModel,
                    )

                    # Remove edges first
                    session.query(ResourceEdgeModel).filter(
                        (ResourceEdgeModel.parent_id == existing.resource_id)
                        | (ResourceEdgeModel.child_id == existing.resource_id)
                    ).delete(synchronize_session="fetch")
                    session.query(ResourceModel).filter_by(
                        resource_id=existing.resource_id
                    ).delete(synchronize_session="fetch")
                    session.commit()
                except Exception:
                    session.rollback()
                    raise
                finally:
                    session.close()
            except NotFoundError:
                # Resource doesn't exist yet — proceed with normal add
                pass

        # Build properties from type-specific flags
        properties: dict[str, Any] = {}
        if path is not None:
            properties["path"] = path
        if branch is not None:
            properties["branch"] = branch
        if image is not None:
            properties["image"] = image
        if container_id is not None:
            properties["container_id"] = container_id
        if mount:
            if type_name not in _CONTAINER_TYPES:
                console.print(
                    "[red]Error:[/red] --mount is only valid for "
                    "container types "
                    f"({', '.join(sorted(_CONTAINER_TYPES))})."
                )
                raise typer.Abort()
            properties["mounts"] = json.dumps([_parse_mount_spec(m) for m in mount])
        if clone_into is not None:
            if type_name not in _CONTAINER_TYPES:
                console.print(
                    "[red]Error:[/red] --clone-into is only valid for "
                    "container types "
                    f"({', '.join(sorted(_CONTAINER_TYPES))})."
                )
                raise typer.Abort()
            parts = clone_into.rsplit(":", 1)
            if len(parts) != 2 or not parts[0] or not parts[1]:
                console.print(
                    "[red]Error:[/red] --clone-into must be in "
                    "<REPO_URL>:<CONTAINER_PATH> format."
                )
                raise typer.Abort()
            properties["clone_into"] = json.dumps(
                {"repo_url": parts[0], "container_path": parts[1]}
            )

        resource = service.register_resource(
            type_name=type_name,
            name=name,
            location=path,
            description=description,
            read_only=read_only,
            properties=properties if properties else None,
        )

        if fmt != OutputFormat.RICH.value:
            data = _resource_dict(resource)
            console.print(format_output(data, fmt))
            return

        console.print(
            f"[green]Added resource:[/green] {resource.name} "
            f"(id: {resource.resource_id})"
        )

    except NotFoundError as exc:
        console.print(f"[red]Resource type not found:[/red] {type_name}")
        raise typer.Abort() from exc
    except ValidationError as exc:
        console.print(f"[red]Validation error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except Exception as exc:
        if isinstance(exc, (typer.Abort, typer.Exit)):
            raise
        console.print(f"[red]Unexpected error:[/red] {exc}")
        raise typer.Abort() from exc


@app.command("list")
def resource_list(
    type_filter: Annotated[
        str | None,
        typer.Option("--type", "-t", help="Filter by resource type"),
    ] = None,
    show_all: Annotated[
        bool,
        typer.Option(
            "--all",
            help="Include auto-discovered child resources in the listing.",
        ),
    ] = False,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """List registered resources.

    By default only user-added (named) resources are shown. Use
    ``--all`` to include auto-discovered children.

    Examples:
        agents resource list
        agents resource list --all
        agents resource list --type git-checkout
        agents resource list --format json
    """
    try:
        service = _get_registry_service()
        resources = service.list_resources(
            type_name=type_filter,
            include_auto_discovered=show_all,
        )

        if not resources:
            console.print("[yellow]No resources found.[/yellow]")
            return

        if fmt != OutputFormat.RICH.value:
            data = [_resource_dict(r) for r in resources]
            console.print(format_output(data, fmt))
            return

        table = Table(title=f"Resources ({len(resources)} total)")
        table.add_column("ID", style="dim")
        table.add_column("Name", style="cyan")
        table.add_column("Type", style="blue")
        table.add_column("Status", style="yellow")
        table.add_column("Kind", style="magenta")
        table.add_column("Location", style="dim")
        table.add_column("Description", style="dim")

        detected_devcontainers: list[Any] = []
        for res in resources:
            desc = res.description or ""
            if len(desc) > 40:
                desc = desc[:37] + "..."
            res_id = res.resource_id
            if len(res_id) > 12:
                res_id = res_id[:12] + "..."
            lifecycle_state = _get_lifecycle_state_str(res)
            if lifecycle_state == "detected (not built)":
                detected_devcontainers.append(res)
            table.add_row(
                res_id,
                res.name or "(unnamed)",
                res.resource_type_name,
                lifecycle_state or "",
                str(res.classification),
                res.location or "",
                desc,
            )

        console.print(table)

        for res in detected_devcontainers:
            location = res.location or "(unknown path)"
            devcontainer_json = f"{location}/.devcontainer/devcontainer.json"
            console.print(
                f"[yellow]⚠ Devcontainer detected at {devcontainer_json}[/yellow]"
            )
            console.print("  Container will be built lazily on first access.")
            console.print(
                f"  Use [cyan]agents resource show[/cyan] {res.resource_id} to inspect."
            )

    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except Exception as exc:
        if isinstance(exc, (typer.Abort, typer.Exit)):
            raise
        console.print(f"[red]Unexpected error:[/red] {exc}")
        raise typer.Abort() from exc


@app.command("show")
def resource_show(
    resource: Annotated[
        str,
        typer.Argument(
            help="Resource name or ULID to show",
        ),
    ],
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Show details of a specific resource.

    Examples:
        agents resource show local/my-repo
        agents resource show 01HXYZ1234567890ABCDEFGHIJ
    """
    try:
        service = _get_registry_service()
        res = service.show_resource(resource)

        if fmt != OutputFormat.RICH.value:
            data = _resource_dict(res)
            console.print(format_output(data, fmt))
            return

        props_display = _format_properties(res.properties)

        details = (
            f"[bold]Resource ID:[/bold] {res.resource_id}\n"
            f"[bold]Name:[/bold] {res.name or '(unnamed)'}\n"
            f"[bold]Type:[/bold] {res.resource_type_name}\n"
            f"[bold]Classification:[/bold] {res.classification}\n"
            f"[bold]Description:[/bold] {res.description or '(none)'}\n"
            f"[bold]Location:[/bold] {res.location or '(none)'}\n"
            f"[bold]Properties:[/bold]\n{props_display}\n"
            f"[bold]Created:[/bold] {res.created_at}\n"
            f"[bold]Updated:[/bold] {res.updated_at}"
        )

        console.print(Panel(details, title="Resource Details", expand=False))

    except NotFoundError as exc:
        console.print(f"[red]Resource not found:[/red] {resource}")
        raise typer.Abort() from exc
    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except Exception as exc:
        if isinstance(exc, (typer.Abort, typer.Exit)):
            raise
        console.print(f"[red]Unexpected error:[/red] {exc}")
        raise typer.Abort() from exc


# ---------------------------------------------------------------------------
# Resource tree command
# ---------------------------------------------------------------------------


@app.command("tree")
def resource_tree(
    resource: Annotated[
        str,
        typer.Argument(
            help="Resource name or ULID to display tree for",
        ),
    ],
    depth: Annotated[
        int,
        typer.Option("--depth", "-d", help="Maximum tree depth (-1 for unlimited)"),
    ] = 3,
    type_filter: Annotated[
        str | None,
        typer.Option("--type", "-t", help="Filter children by resource type"),
    ] = None,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Display a resource and its children as a tree (DAG).

    Shows the resource hierarchy with optional depth limit and type filter.
    Output is deterministically ordered by resource name.

    Examples:
        agents resource tree local/my-repo
        agents resource tree local/my-repo --depth 2
        agents resource tree local/my-repo --type fs-directory
        agents resource tree local/my-repo --format json
    """
    try:
        service = _get_registry_service()
        tree = service.get_resource_tree(resource, depth=depth, type_filter=type_filter)

        if fmt != OutputFormat.RICH.value:
            data = _tree_to_dict(tree)
            console.print(format_output(data, fmt))
            return

        _print_tree_rich(tree, indent=0)

    except NotFoundError as exc:
        console.print(f"[red]Resource not found:[/red] {resource}")
        raise typer.Abort() from exc
    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except Exception as exc:
        if isinstance(exc, (typer.Abort, typer.Exit)):
            raise
        console.print(f"[red]Unexpected error:[/red] {exc}")
        raise typer.Abort() from exc


def _tree_to_dict(
    nodes: list[dict[str, object]],
) -> list[dict[str, object]]:
    """Convert tree nodes to serialisable dicts."""
    result: list[dict[str, object]] = []
    for node in nodes:
        res = node["resource"]
        children_raw = node.get("children", [])
        children_list: list[dict[str, object]] = []
        if isinstance(children_raw, list):
            children_list = children_raw
        child_dicts = _tree_to_dict(children_list)
        entry: dict[str, object] = _resource_dict(res)
        entry["children"] = child_dicts
        result.append(entry)
    return result


def _print_tree_rich(
    nodes: list[dict[str, object]],
    indent: int,
) -> None:
    """Render tree nodes with rich formatting."""
    for node in nodes:
        res = node["resource"]
        prefix = "  " * indent
        name_display = getattr(res, "name", None) or getattr(res, "resource_id", "?")
        type_name = getattr(res, "resource_type_name", "")
        if indent == 0:
            console.print(
                f"{prefix}[bold cyan]{name_display}[/bold cyan] "
                f"[dim]({type_name})[/dim]"
            )
        else:
            console.print(
                f"{prefix}[cyan]{name_display}[/cyan] [dim]({type_name})[/dim]"
            )
        children_raw = node.get("children", [])
        children_list: list[dict[str, object]] = []
        if isinstance(children_raw, list):
            children_list = children_raw
        _print_tree_rich(children_list, indent + 1)


# ---------------------------------------------------------------------------
# Resource inspect command
# ---------------------------------------------------------------------------


@app.command("inspect")
def resource_inspect(
    resource: Annotated[
        str,
        typer.Argument(
            help="Resource name or ULID to inspect",
        ),
    ],
    tree: Annotated[
        bool,
        typer.Option("--tree", help="Also show child tree"),
    ] = False,
    file: Annotated[
        str | None,
        typer.Option(
            "--file",
            help="Show file content (for git-checkout/fs-mount resources)",
        ),
    ] = None,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Show detailed information about a resource.

    Optionally display the child tree with ``--tree`` or show file
    content for git-checkout/fs-mount resources with ``--file PATH``.

    Examples:
        agents resource inspect local/my-repo
        agents resource inspect local/my-repo --tree
        agents resource inspect local/my-repo --file README.md
        agents resource inspect local/my-repo --format json
    """
    try:
        service = _get_registry_service()
        res = service.show_resource(resource)

        data: dict[str, object] = _resource_dict(res)

        # Optionally include child tree
        if tree:
            tree_data = service.get_resource_tree(resource, depth=-1)
            data["children"] = _tree_to_dict(
                tree_data[0].get("children", [])  # type: ignore[union-attr]
                if tree_data
                else []
            )

        # Optionally show file content
        if file is not None:
            file_content = _read_resource_file(res, file)
            data["file_path"] = file
            data["file_content"] = file_content

        if fmt != OutputFormat.RICH.value:
            console.print(format_output(data, fmt))
            return

        # Rich display
        props_display = _format_properties(res.properties)

        details = (
            f"[bold]Resource ID:[/bold] {res.resource_id}\n"
            f"[bold]Name:[/bold] {res.name or '(unnamed)'}\n"
            f"[bold]Type:[/bold] {res.resource_type_name}\n"
            f"[bold]Classification:[/bold] {res.classification}\n"
            f"[bold]Description:[/bold] {res.description or '(none)'}\n"
            f"[bold]Location:[/bold] {res.location or '(none)'}\n"
            f"[bold]Properties:[/bold]\n{props_display}\n"
            f"[bold]Created:[/bold] {res.created_at}\n"
            f"[bold]Updated:[/bold] {res.updated_at}"
        )

        console.print(Panel(details, title="Resource Inspect", expand=False))

        if tree:
            console.print("\n[bold]Children:[/bold]")
            tree_nodes = service.get_resource_tree(resource, depth=-1)
            if tree_nodes:
                children_raw = tree_nodes[0].get("children", [])
                children_list: list[dict[str, object]] = []
                if isinstance(children_raw, list):
                    children_list = children_raw
                if children_list:
                    _print_tree_rich(children_list, indent=1)
                else:
                    console.print("  (no children)")

        if file is not None:
            file_content = _read_resource_file(res, file)
            console.print(f"\n[bold]File:[/bold] {file}")
            console.print(file_content)

    except NotFoundError as exc:
        console.print(f"[red]Resource not found:[/red] {resource}")
        raise typer.Abort() from exc
    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except Exception as exc:
        if isinstance(exc, (typer.Abort, typer.Exit)):
            raise
        console.print(f"[red]Unexpected error:[/red] {exc}")
        raise typer.Abort() from exc


def _read_resource_file(resource: Any, file_path: str) -> str:
    """Read a file relative to a resource's location.

    For git-checkout and fs-mount resources, the file is resolved
    relative to the resource's location (path property or location field).
    """
    location = resource.location
    if not location and resource.properties:
        location = resource.properties.get("path")

    if not location:
        return "(no location set for this resource)"

    # S3 fix: guard against path traversal (e.g. "../../etc/passwd")
    base = Path(location).resolve()
    full_path = (base / file_path).resolve()
    if not full_path.is_relative_to(base):
        return f"(path traversal rejected: {file_path})"

    if not full_path.exists():
        return f"(file not found: {full_path})"

    try:
        return full_path.read_text(encoding="utf-8")
    except (OSError, UnicodeDecodeError) as exc:
        return f"(error reading file: {exc})"


# ---------------------------------------------------------------------------
# Resource link-child command
# ---------------------------------------------------------------------------


@app.command("link-child")
def resource_link_child(
    parent: Annotated[
        str,
        typer.Argument(help="Parent resource name or ULID"),
    ],
    child: Annotated[
        str,
        typer.Argument(help="Child resource name or ULID"),
    ],
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Create a DAG edge between two resources (parent -> child).

    The child's resource type must be allowed by the parent's type
    constraints. Cycles are rejected.

    Examples:
        agents resource link-child local/my-repo local/my-dir
    """
    try:
        service = _get_registry_service()
        service.link_child(parent, child)

        if fmt != OutputFormat.RICH.value:
            data: dict[str, object] = {
                "status": "linked",
                "parent": parent,
                "child": child,
            }
            console.print(format_output(data, fmt))
            return

        console.print(f"[green]Linked:[/green] {parent} -> {child}")

    except NotFoundError as exc:
        console.print(f"[red]Resource not found:[/red] {exc.resource_id}")
        raise typer.Abort() from exc
    except ValidationError as exc:
        console.print(f"[red]Validation error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except Exception as exc:
        if isinstance(exc, (typer.Abort, typer.Exit)):
            raise
        console.print(f"[red]Unexpected error:[/red] {exc}")
        raise typer.Abort() from exc


# ---------------------------------------------------------------------------
# Resource unlink-child command
# ---------------------------------------------------------------------------


@app.command("unlink-child")
def resource_unlink_child(
    parent: Annotated[
        str,
        typer.Argument(help="Parent resource name or ULID"),
    ],
    child: Annotated[
        str,
        typer.Argument(help="Child resource name or ULID"),
    ],
    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 DAG edge between two resources.

    Requires confirmation unless ``--yes`` is passed.

    Examples:
        agents resource unlink-child local/my-repo local/my-dir
        agents resource unlink-child --yes local/my-repo local/my-dir
    """
    try:
        if not yes:
            confirm = typer.confirm(
                f"Unlink '{child}' from '{parent}'?",
            )
            if not confirm:
                console.print("[yellow]Aborted.[/yellow]")
                raise typer.Abort()

        service = _get_registry_service()
        service.unlink_child(parent, child)

        if fmt != OutputFormat.RICH.value:
            data: dict[str, object] = {
                "status": "unlinked",
                "parent": parent,
                "child": child,
            }
            console.print(format_output(data, fmt))
            return

        console.print(f"[green]Unlinked:[/green] {parent} -> {child}")

    except NotFoundError as exc:
        console.print(f"[red]Not found:[/red] {exc.resource_id}")
        raise typer.Abort() from exc
    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except Exception as exc:
        if isinstance(exc, (typer.Abort, typer.Exit)):
            raise
        console.print(f"[red]Unexpected error:[/red] {exc}")
        raise typer.Abort() from exc


@app.command("remove")
def resource_remove(
    resource: Annotated[
        str,
        typer.Argument(help="Resource name or ULID to remove"),
    ],
    yes: Annotated[
        bool,
        typer.Option("--yes", "-y", help="Skip confirmation prompt"),
    ] = False,
) -> None:
    """Remove a resource from the registry.

    Examples:
        agents resource remove local/my-repo
        agents resource remove --yes local/my-repo
    """
    try:
        service = _get_registry_service()

        # Verify the resource exists
        res = service.show_resource(resource)

        if not yes:
            confirm = typer.confirm(
                f"Remove resource '{res.name or res.resource_id}'?",
            )
            if not confirm:
                console.print("[yellow]Aborted.[/yellow]")
                raise typer.Abort()

        # Delete via session
        session = service._session()
        try:
            from cleveragents.infrastructure.database.models import (
                ResourceEdgeModel,
                ResourceModel,
            )

            # Check for edges
            edge_count: int = (
                session.query(ResourceEdgeModel)
                .filter(
                    (ResourceEdgeModel.parent_id == res.resource_id)
                    | (ResourceEdgeModel.child_id == res.resource_id)
                )
                .count()
            )
            if edge_count > 0:
                console.print(
                    f"[red]Cannot remove resource '{res.name or res.resource_id}': "
                    f"{edge_count} edge(s) still reference it.[/red]"
                )
                raise typer.Abort()

            row = (
                session.query(ResourceModel)
                .filter_by(resource_id=res.resource_id)
                .first()
            )
            if row is not None:
                session.delete(row)
                session.commit()
        except typer.Abort:
            raise
        except Exception:
            session.rollback()
            raise
        finally:
            session.close()

        console.print(f"[green]Removed resource:[/green] {res.name or res.resource_id}")

    except NotFoundError as exc:
        console.print(f"[red]Resource not found:[/red] {resource}")
        raise typer.Abort() from exc
    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except Exception as exc:
        if isinstance(exc, (typer.Abort, typer.Exit)):
            raise
        console.print(f"[red]Unexpected error:[/red] {exc}")
        raise typer.Abort() from exc


# ---------------------------------------------------------------------------
# Devcontainer lifecycle commands (issue #514)
# ---------------------------------------------------------------------------

# B2 fix: separate type sets — stop works on any container, but rebuild
# requires devcontainer-instance since it invokes ``devcontainer up``.
# Issue #2588 fix: container-instance is now stoppable — stop_container uses
# docker stop with the container_id from the lifecycle tracker, which works
# for both container-instance and devcontainer-instance resources.
_STOPPABLE_TYPES = frozenset({"devcontainer-instance", "container-instance"})
_REBUILDABLE_TYPES = frozenset({"devcontainer-instance"})
# F5-r6 fix: moved from inside resource_rebuild() to module level to avoid
# per-call allocation (consistent with _STOPPABLE_TYPES / _REBUILDABLE_TYPES).
_REBUILD_ALLOWED = frozenset(
    {ContainerLifecycleState.STOPPED, ContainerLifecycleState.FAILED}
)


@app.command("stop")
def resource_stop(
    name: Annotated[
        str,
        typer.Argument(help="Devcontainer resource name or ULID to stop"),
    ],
    # A2 fix: add --yes/-y confirmation flag to skip interactive prompt
    yes: Annotated[
        bool,
        typer.Option("--yes", "-y", help="Skip confirmation prompt"),
    ] = False,
) -> None:
    """Stop a running container-instance or devcontainer-instance resource.

    Transitions the container from ``running`` -> ``stopping`` -> ``stopped``.
    Both container-instance and devcontainer-instance resources
    may be stopped; other resource types produce an error.

    Use ``--yes`` / ``-y`` to skip the confirmation prompt in scripts.

    Examples:
        agents resource stop local/my-devcontainer
        agents resource stop local/my-devcontainer --yes
    """
    try:
        service = _get_registry_service()
        res = service.show_resource(name)

        if res.resource_type_name not in _STOPPABLE_TYPES:
            console.print(
                f"[red]Resource '{name}' is type '{res.resource_type_name}', "
                f"not a stoppable container type.[/red]"
            )
            raise typer.Abort()

        # R12 fix: validate lifecycle state before delegating
        tracker = get_lifecycle_tracker(res.resource_id)
        if tracker.current_state != ContainerLifecycleState.RUNNING:
            console.print(
                f"[red]Cannot stop:[/red] container is in "
                f"'{tracker.current_state.value}' state (must be 'running')."
            )
            raise typer.Abort()

        if not yes:
            typer.confirm(
                f"Stop container '{res.name or res.resource_id}'?",
                abort=True,
            )

        console.print(
            f"Stopping container {res.name or res.resource_id} "
            f"(state: {tracker.current_state.value})..."
        )

        stop_container(res.resource_id)
        console.print(f"[green]Stopped:[/green] {res.name or res.resource_id}")

    except NotFoundError as exc:
        console.print(f"[red]Resource not found:[/red] {name}")
        raise typer.Abort() from exc
    except (ValueError, RuntimeError) as exc:
        console.print(f"[red]Stop failed:[/red] {exc}")
        raise typer.Abort() from exc
    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except Exception as exc:
        if isinstance(exc, (typer.Abort, typer.Exit)):
            raise
        console.print(f"[red]Unexpected error:[/red] {exc}")
        raise typer.Abort() from exc


@app.command("rebuild")
def resource_rebuild(
    name: Annotated[
        str,
        typer.Argument(help="Devcontainer resource name or ULID to rebuild"),
    ],
    # A2 fix: add --yes/-y confirmation flag to skip interactive prompt
    yes: Annotated[
        bool,
        typer.Option("--yes", "-y", help="Skip confirmation prompt"),
    ] = False,
) -> None:
    """Rebuild a stopped or failed devcontainer-instance resource.

    Transitions: ``stopped``/``failed`` -> ``building`` -> ``running``.
    Only devcontainer-instance resources may be rebuilt.

    Use ``--yes`` / ``-y`` to skip the confirmation prompt in scripts.

    Examples:
        agents resource rebuild local/my-devcontainer
        agents resource rebuild local/my-devcontainer --yes
    """
    try:
        service = _get_registry_service()
        res = service.show_resource(name)

        if res.resource_type_name not in _REBUILDABLE_TYPES:
            console.print(
                f"[red]Resource '{name}' is type '{res.resource_type_name}', "
                f"not a devcontainer-instance (rebuild requires devcontainer).[/red]"
            )
            raise typer.Abort()

        # R12 fix: validate lifecycle state before delegating
        tracker = get_lifecycle_tracker(res.resource_id)
        if tracker.current_state not in _REBUILD_ALLOWED:
            console.print(
                f"[red]Cannot rebuild:[/red] container is in "
                f"'{tracker.current_state.value}' state "
                f"(must be 'stopped' or 'failed')."
            )
            raise typer.Abort()

        location = res.location
        if not location and res.properties:
            path_val = res.properties.get("path")
            location = str(path_val) if path_val is not None else None

        if not location:
            console.print(f"[red]Resource '{name}' has no location for rebuild.[/red]")
            raise typer.Abort()

        if not yes:
            typer.confirm(
                f"Rebuild container '{res.name or res.resource_id}'?",
                abort=True,
            )

        console.print(f"Rebuilding {res.name or res.resource_id}...")
        rebuild_container(res.resource_id, location)
        console.print(f"[green]Rebuilt:[/green] {res.name or res.resource_id}")

    except NotFoundError as exc:
        console.print(f"[red]Resource not found:[/red] {name}")
        raise typer.Abort() from exc
    except (ValueError, RuntimeError) as exc:
        console.print(f"[red]Rebuild failed:[/red] {exc}")
        raise typer.Abort() from exc
    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except Exception as exc:
        if isinstance(exc, (typer.Abort, typer.Exit)):
            raise
        console.print(f"[red]Unexpected error:[/red] {exc}")
        raise typer.Abort() from exc
