"""Project context policy CLI commands.

Implements ``agents project context set/show/inspect/simulate`` to
manage per-project context policies with view inheritance and to
query the ACMS pipeline for context state and dry-run assembly.

Commands:
- ``agents project context set``  — persist a context policy view
  and ACMS pipeline configuration overrides
- ``agents project context show`` — display the active context policy
  including effective ACMS pipeline config
- ``agents project context inspect`` — inspect effective context state
  from the ACMS tier service for a project
- ``agents project context simulate`` — dry-run context assembly
  showing fragments, budget usage, and strategy selection

Based on ADR-014 (Context Management / ACMS) and the
``ProjectContextPolicy`` domain model.
"""

from __future__ import annotations

import contextlib
import hashlib
import json as _json
from typing import Annotated, Any

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

from cleveragents.application.services.context_phase_analysis import (
    analyze_phase_summaries,
)
from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.domain.models.acms.crp import (
    AssembledContext,
    ContextFragment,
    ContextRequest,
    FragmentProvenance,
)
from cleveragents.domain.models.acms.tiers import (
    ActorRole,
    ContextTier,
    TieredFragment,
    TierMetrics,
)
from cleveragents.domain.models.core.context_policy import (
    VALID_PHASES,
    ContextView,
    ProjectContextPolicy,
)
from cleveragents.domain.models.core.project import TemporalScope

app = typer.Typer(help="Manage project context policies")
console = Console()
err_console = Console(stderr=True)

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

# Default ACMS pipeline configuration values
_DEFAULT_HOT_MAX_TOKENS = 8000
_DEFAULT_WARM_MAX_DECISIONS = 500
_DEFAULT_COLD_MAX_DECISIONS = 5000
_DEFAULT_BREADTH = 2
_DEFAULT_DEPTH = 3
_DEFAULT_SKELETON_RATIO = 0.15
_DEFAULT_BUDGET_TOKENS = 8000


# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------


def _get_namespaced_project_repo() -> Any:
    """Return a NamespacedProjectRepository from the DI container."""
    from cleveragents.application.container import get_container

    container = get_container()
    return container.namespaced_project_repo()


def _get_context_tier_service() -> Any:
    """Return the ContextTierService from the DI container."""
    from cleveragents.application.container import get_container

    container = get_container()
    return container.context_tier_service()


def _load_policy_json(
    session_factory: Any,
    namespaced_name: str,
) -> dict[str, Any] | None:
    """Read raw ``context_policy_json`` from ``ns_projects``."""
    from sqlalchemy import text

    session = session_factory()
    try:
        row = session.execute(
            text(
                "SELECT context_policy_json FROM ns_projects "
                "WHERE namespaced_name = :ns"
            ),
            {"ns": namespaced_name},
        ).fetchone()
        if row is None or row[0] is None:
            return None
        return _json.loads(row[0])  # type: ignore[no-any-return]
    finally:
        session.close()


def _save_policy_json(
    session_factory: Any,
    namespaced_name: str,
    policy_dict: dict[str, Any],
) -> None:
    """Write ``context_policy_json`` on ``ns_projects``."""
    from sqlalchemy import text

    session = session_factory()
    try:
        session.execute(
            text(
                "UPDATE ns_projects "
                "SET context_policy_json = :blob "
                "WHERE namespaced_name = :ns"
            ),
            {
                "blob": _json.dumps(policy_dict),
                "ns": namespaced_name,
            },
        )
        session.commit()
    except Exception:
        # Explicit rollback on commit failure to release DB locks.
        # Wrapped in contextlib.suppress so a rollback failure does not
        # mask the original commit exception.
        with contextlib.suppress(Exception):
            session.rollback()
        raise
    finally:
        session.close()


def _read_policy(
    session_factory: Any,
    namespaced_name: str,
) -> ProjectContextPolicy:
    """Deserialise the stored policy (or return a fresh default)."""
    raw = _load_policy_json(session_factory, namespaced_name)
    if raw is None:
        return ProjectContextPolicy()
    # Only parse as ProjectContextPolicy if it looks like one
    if "default_view" in raw:
        return ProjectContextPolicy.model_validate(raw)
    return ProjectContextPolicy()


def _read_acms_config(
    session_factory: Any,
    namespaced_name: str,
) -> dict[str, Any]:
    """Read the ACMS pipeline config from the stored policy JSON.

    The ACMS config is stored as an ``acms_config`` key in the
    same JSON blob as the context policy.  Returns defaults when
    not present.
    """
    raw = _load_policy_json(session_factory, namespaced_name)
    if raw is None:
        return _default_acms_config()
    return raw.get("acms_config", _default_acms_config())


def _default_acms_config() -> dict[str, Any]:
    """Return the default ACMS pipeline configuration."""
    return {
        "hot_max_tokens": _DEFAULT_HOT_MAX_TOKENS,
        "warm_max_decisions": _DEFAULT_WARM_MAX_DECISIONS,
        "cold_max_decisions": _DEFAULT_COLD_MAX_DECISIONS,
        "query_limit": None,
        "summarize": True,
        "summary_max_tokens": None,
        "strategies": [],
        "default_breadth": _DEFAULT_BREADTH,
        "default_depth": _DEFAULT_DEPTH,
        "depth_gradient": {},
        "skeleton_ratio": _DEFAULT_SKELETON_RATIO,
        "temporal_scope": "current",
        "auto_refresh": True,
    }


def _write_policy(
    session_factory: Any,
    namespaced_name: str,
    policy: ProjectContextPolicy,
    acms_config: dict[str, Any] | None = None,
) -> None:
    """Serialise and persist the policy with optional ACMS config."""
    data = policy.model_dump(mode="json")
    if acms_config is not None:
        data["acms_config"] = acms_config
    else:
        # Preserve existing ACMS config
        existing = _load_policy_json(session_factory, namespaced_name)
        if existing is not None and "acms_config" in existing:
            data["acms_config"] = existing["acms_config"]
    _save_policy_json(session_factory, namespaced_name, data)


def _policy_to_dict(policy: ProjectContextPolicy) -> dict[str, Any]:
    """Convert a policy to a display-friendly dict."""
    result: dict[str, Any] = {}
    result["default_view"] = policy.default_view.model_dump(mode="json")
    result["strategize_view"] = (
        policy.strategize_view.model_dump(mode="json")
        if policy.strategize_view
        else None
    )
    result["execute_view"] = (
        policy.execute_view.model_dump(mode="json") if policy.execute_view else None
    )
    result["apply_view"] = (
        policy.apply_view.model_dump(mode="json") if policy.apply_view else None
    )
    return result


def _tier_metrics_to_dict(metrics: TierMetrics) -> dict[str, Any]:
    """Convert tier metrics to a display-friendly dict."""
    return {
        "hot_count": metrics.hot_count,
        "warm_count": metrics.warm_count,
        "cold_count": metrics.cold_count,
        "hot_hit_count": metrics.hot_hit_count,
        "hot_miss_count": metrics.hot_miss_count,
        "warm_hit_count": metrics.warm_hit_count,
        "warm_miss_count": metrics.warm_miss_count,
    }


def _fragment_to_dict(fragment: TieredFragment) -> dict[str, Any]:
    """Convert a tiered fragment to a display-friendly dict."""
    return {
        "fragment_id": fragment.fragment_id,
        "tier": fragment.tier.value,
        "resource_id": fragment.resource_id,
        "project_name": fragment.project_name,
        "token_count": fragment.token_count,
        "access_count": fragment.access_count,
        "last_accessed": fragment.last_accessed.isoformat(),
    }


def _assembled_to_dict(assembled: AssembledContext) -> dict[str, Any]:
    """Convert an AssembledContext to a display-friendly dict."""
    return {
        "total_tokens": assembled.total_tokens,
        "budget_used": assembled.budget_used,
        "strategies_used": assembled.strategies_used,
        "context_hash": assembled.context_hash,
        "preamble": assembled.preamble,
        "fragment_count": len(assembled.fragments),
        "fragments": [
            {
                "uko_node": f.uko_node,
                "detail_depth": f.detail_depth,
                "token_count": f.token_count,
                "relevance_score": f.relevance_score,
                "provenance": {
                    "resource_uri": f.provenance.resource_uri,
                    "location": f.provenance.location,
                    "strategy": f.provenance.strategy,
                },
            }
            for f in assembled.fragments
        ],
    }


def _simulate_context_assembly(
    project_name: str,
    acms_config: dict[str, Any],
    budget_tokens: int | None = None,
    focus_uris: list[str] | None = None,
    strategy_hints: list[str] | None = None,
    view: str = "default",
) -> AssembledContext:
    """Run a dry-run context assembly using the CRP models.

    This creates a simulated AssembledContext based on available
    backend data and the project's ACMS configuration.  When the
    full ACMS pipeline is wired, this will delegate to the live
    pipeline instead.
    """
    effective_budget = budget_tokens or acms_config.get(
        "hot_max_tokens", _DEFAULT_BUDGET_TOKENS
    )
    effective_strategies = strategy_hints or acms_config.get("strategies", [])
    default_breadth = acms_config.get("default_breadth", _DEFAULT_BREADTH)
    default_depth = acms_config.get("default_depth", _DEFAULT_DEPTH)
    temporal_str = acms_config.get("temporal_scope", "current")

    # Resolve temporal scope
    temporal = TemporalScope.CURRENT
    if temporal_str == "recent":
        temporal = TemporalScope.RECENT
    elif temporal_str == "all":
        temporal = TemporalScope.ALL

    # Build the context request
    _request = ContextRequest(
        query=f"Context assembly for project {project_name} (view: {view})",
        focus=focus_uris or [],
        breadth=default_breadth,
        depth=default_depth,
        temporal=temporal,
        max_tokens=effective_budget,
        preferred_strategies=effective_strategies,
        purpose=f"dry-run simulate for {project_name}",
    )

    # Query the tier service for existing fragments
    tier_service = _get_context_tier_service()
    project_fragments = tier_service.get_scoped_view([project_name])

    # Apply focus URI filtering if specified
    if focus_uris:
        project_fragments = [
            f
            for f in project_fragments
            if f.resource_id in focus_uris
            or any(u in (f.resource_id or "") for u in focus_uris)
        ]

    # Build simulated context fragments from tier data
    fragments: list[ContextFragment] = []
    total_tokens = 0
    for tf in project_fragments:
        if total_tokens + tf.token_count > effective_budget:
            break
        fragments.append(
            ContextFragment(
                uko_node=tf.resource_id or f"uko:{tf.fragment_id}",
                content=tf.content[:100] + "..."
                if len(tf.content) > 100
                else tf.content,
                detail_depth=default_depth if isinstance(default_depth, int) else 3,
                token_count=tf.token_count,
                relevance_score=min(1.0, tf.access_count / 10.0)
                if tf.access_count > 0
                else 0.5,
                provenance=FragmentProvenance(
                    resource_uri=tf.resource_id or "unknown",
                    location="",
                    strategy="tier_retrieval",
                ),
            )
        )
        total_tokens += tf.token_count

    budget_used = total_tokens / effective_budget if effective_budget > 0 else 0.0
    content_for_hash = f"{project_name}:{view}:{total_tokens}"
    context_hash = hashlib.sha256(content_for_hash.encode()).hexdigest()[:16]

    strategies_used = (
        effective_strategies if effective_strategies else ["tier_retrieval"]
    )

    return AssembledContext(
        fragments=tuple(fragments),
        total_tokens=total_tokens,
        budget_used=min(budget_used, 1.0),
        strategies_used=tuple(strategies_used),
        context_hash=context_hash,
        preamble=(
            f"Simulated context for {project_name}"
            f" (view: {view}, budget: {effective_budget} tokens)"
        ),
    )


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

PHASE_NAMES = sorted(VALID_PHASES)


@app.command(name="set")
def context_set(
    project: Annotated[
        str,
        typer.Argument(help="Project namespaced name"),
    ],
    view: Annotated[
        str,
        typer.Option(
            "--view",
            "-v",
            help=("Target phase view: default, strategize, execute, or apply"),
        ),
    ] = "default",
    include_resource: Annotated[
        list[str] | None,
        typer.Option(
            "--include-resource",
            help="Resource pattern to include (repeatable)",
        ),
    ] = None,
    exclude_resource: Annotated[
        list[str] | None,
        typer.Option(
            "--exclude-resource",
            help="Resource pattern to exclude (repeatable)",
        ),
    ] = None,
    include_path: Annotated[
        list[str] | None,
        typer.Option(
            "--include-path",
            help="File path glob to include (repeatable)",
        ),
    ] = None,
    exclude_path: Annotated[
        list[str] | None,
        typer.Option(
            "--exclude-path",
            help="File path glob to exclude (repeatable)",
        ),
    ] = None,
    max_file_size: Annotated[
        int | None,
        typer.Option(
            "--max-file-size",
            help="Max file size in bytes (None = no limit)",
        ),
    ] = None,
    max_total_size: Annotated[
        int | None,
        typer.Option(
            "--max-total-size",
            help="Max total context size in bytes",
        ),
    ] = None,
    execution_environment: Annotated[
        str | None,
        typer.Option(
            "--execution-environment",
            help="Default execution environment for this project (host or container)",
        ),
    ] = None,
    execution_env_priority: Annotated[
        str | None,
        typer.Option(
            "--execution-env-priority",
            help=(
                "Priority semantics: fallback (default) defers to "
                "auto-detected devcontainers; override always uses "
                "the specified environment"
            ),
        ),
    ] = None,
    hot_max_tokens: Annotated[
        int | None,
        typer.Option(
            "--hot-max-tokens",
            help="Max tokens for the hot tier context window",
        ),
    ] = None,
    warm_max_decisions: Annotated[
        int | None,
        typer.Option(
            "--warm-max-decisions",
            help="Max decisions for the warm tier",
        ),
    ] = None,
    cold_max_decisions: Annotated[
        int | None,
        typer.Option(
            "--cold-max-decisions",
            help="Max decisions for the cold tier",
        ),
    ] = None,
    query_limit: Annotated[
        int | None,
        typer.Option(
            "--query-limit",
            help="Max number of fragments per query",
        ),
    ] = None,
    summarize: Annotated[
        bool | None,
        typer.Option(
            "--summarize/--no-summarize",
            help="Enable or disable context summarization",
        ),
    ] = None,
    summary_max_tokens: Annotated[
        int | None,
        typer.Option(
            "--summary-max-tokens",
            help="Max tokens for context summaries",
        ),
    ] = None,
    strategy: Annotated[
        list[str] | None,
        typer.Option(
            "--strategy",
            help="Preferred context strategy (repeatable)",
        ),
    ] = None,
    default_breadth: Annotated[
        int | None,
        typer.Option(
            "--default-breadth",
            help="Default breadth for context retrieval",
        ),
    ] = None,
    default_depth: Annotated[
        str | None,
        typer.Option(
            "--default-depth",
            help="Default depth (integer or named level)",
        ),
    ] = None,
    skeleton_ratio: Annotated[
        float | None,
        typer.Option(
            "--skeleton-ratio",
            help="Skeleton compression ratio (0.0-1.0)",
        ),
    ] = None,
    temporal_scope: Annotated[
        str | None,
        typer.Option(
            "--temporal-scope",
            help="Temporal scope: current, recent, or all",
        ),
    ] = None,
    depth_gradient: Annotated[
        list[str] | None,
        typer.Option(
            "--depth-gradient",
            help=(
                "Per-hop detail depth override (repeatable). "
                "Format: HOP:INT_OR_NAME, e.g. 0:9, 1:SIGNATURES"
            ),
        ),
    ] = None,
    auto_refresh: Annotated[
        bool | None,
        typer.Option(
            "--auto-refresh/--no-auto-refresh",
            help="Auto-refresh context on resource changes",
        ),
    ] = None,
    clear: Annotated[
        bool,
        typer.Option(
            "--clear",
            help="Clear the view for the given phase",
        ),
    ] = False,
    output_format: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Set context policy for a project phase view.

    Configures both the context view filtering (resource/path
    include/exclude) and the ACMS pipeline parameters (tier budgets,
    strategies, temporal scope, etc.) for a project.
    """
    if view not in VALID_PHASES:
        err_console.print(
            f"[red]Invalid view '{view}': must be one of {PHASE_NAMES}[/red]"
        )
        raise typer.Exit(1)

    from cleveragents.application.container import get_container

    container = get_container()
    session_factory = container.session_factory()

    # Validate project exists
    repo = _get_namespaced_project_repo()
    try:
        repo.get(project)
    except Exception as exc:
        err_console.print(f"[red]Project not found:[/red] {project}")
        raise typer.Exit(1) from exc

    policy = _read_policy(session_factory, project)
    acms = _read_acms_config(session_factory, project)

    if clear:
        # Reset the view to None (inherit from parent)
        if view == "default":
            policy = policy.model_copy(update={"default_view": ContextView()})
        else:
            policy = policy.model_copy(update={f"{view}_view": None})
    else:
        new_view = ContextView(
            include_resources=include_resource or [],
            exclude_resources=exclude_resource or [],
            include_paths=include_path or [],
            exclude_paths=exclude_path or [],
            max_file_size=max_file_size,
            max_total_size=max_total_size,
        )
        policy = policy.model_copy(update={f"{view}_view": new_view})

    # Update ACMS config with any provided overrides
    if hot_max_tokens is not None:
        acms["hot_max_tokens"] = hot_max_tokens
    if warm_max_decisions is not None:
        acms["warm_max_decisions"] = warm_max_decisions
    if cold_max_decisions is not None:
        acms["cold_max_decisions"] = cold_max_decisions
    if query_limit is not None:
        acms["query_limit"] = query_limit
    if summarize is not None:
        acms["summarize"] = summarize
    if summary_max_tokens is not None:
        acms["summary_max_tokens"] = summary_max_tokens
    if strategy is not None:
        acms["strategies"] = strategy
    if default_breadth is not None:
        acms["default_breadth"] = default_breadth
    if default_depth is not None:
        # Parse as int if possible, otherwise keep as string
        try:
            acms["default_depth"] = int(default_depth)
        except ValueError:
            acms["default_depth"] = default_depth
    if skeleton_ratio is not None:
        acms["skeleton_ratio"] = skeleton_ratio
    if temporal_scope is not None:
        if temporal_scope not in ("current", "recent", "all"):
            err_console.print(
                f"[red]Invalid temporal scope '{temporal_scope}': "
                f"must be current, recent, or all[/red]"
            )
            raise typer.Exit(1)
        acms["temporal_scope"] = temporal_scope
    if auto_refresh is not None:
        acms["auto_refresh"] = auto_refresh
    if depth_gradient is not None:
        parsed_gradient: dict[str, int | str] = {}
        for entry in depth_gradient:
            if ":" not in entry:
                err_console.print(
                    f"[red]Invalid depth-gradient '{entry}': "
                    f"expected HOP:INT_OR_NAME format[/red]"
                )
                raise typer.Exit(1)
            hop_str, _, value_str = entry.partition(":")
            try:
                hop = int(hop_str)
            except ValueError:
                err_console.print(
                    f"[red]Invalid depth-gradient hop '{hop_str}': "
                    f"hop must be a non-negative integer[/red]"
                )
                raise typer.Exit(1) from None
            if hop < 0:
                err_console.print(
                    f"[red]Invalid depth-gradient hop '{hop}': "
                    f"hop must be a non-negative integer[/red]"
                )
                raise typer.Exit(1)
            # Value: try int first, else keep as named level string
            try:
                parsed_value: int | str = int(value_str)
            except ValueError:
                parsed_value = value_str
            parsed_gradient[str(hop)] = parsed_value
        acms["depth_gradient"] = parsed_gradient

    _write_policy(session_factory, project, policy, acms)

    # Handle execution_environment at the project level
    extra_fields: dict[str, str] = {}
    if execution_environment is not None:
        from cleveragents.domain.models.core.plan import ExecutionEnvironment

        try:
            ExecutionEnvironment(execution_environment.lower())
        except ValueError as exc:
            valid = ", ".join(e.value for e in ExecutionEnvironment)
            err_console.print(
                f"[red]Invalid execution environment:[/red] "
                f"{execution_environment}. Valid values: {valid}"
            )
            raise typer.Exit(1) from exc
        extra_fields["execution_environment"] = execution_environment.lower()

    # Handle execution_env_priority at the project level
    if execution_env_priority is not None:
        # --execution-env-priority requires --execution-environment
        if execution_environment is None:
            err_console.print(
                "[red]--execution-env-priority requires --execution-environment[/red]"
            )
            raise typer.Exit(1)

        from cleveragents.domain.models.core.plan import ExecutionEnvPriority

        try:
            ExecutionEnvPriority(execution_env_priority.lower())
        except ValueError as exc:
            valid = ", ".join(e.value for e in ExecutionEnvPriority)
            err_console.print(
                f"[red]Invalid execution env priority:[/red] "
                f"{execution_env_priority}. Valid values: {valid}"
            )
            raise typer.Exit(1) from exc
        extra_fields["execution_env_priority"] = execution_env_priority.lower()
    elif execution_environment is not None:
        # Default to "fallback" when --execution-environment is set
        # without explicit --execution-env-priority
        extra_fields["execution_env_priority"] = "fallback"

    if extra_fields:
        # Merge with existing persisted blob to preserve previously set fields
        existing = _load_policy_json(session_factory, project) or {}
        _save_policy_json(
            session_factory,
            project,
            {
                **existing,
                **policy.model_dump(mode="json"),
                "acms_config": acms,
                **extra_fields,
            },
        )

    data = _policy_to_dict(policy)
    data["acms_config"] = acms
    if output_format.lower() == OutputFormat.RICH:
        console.print(
            Panel(
                f"[green]✓[/green] Context policy "
                f"'{view}' view updated for "
                f"project '{project}'.",
                title="Context Policy Updated",
                expand=False,
            )
        )
    else:
        console.print(format_output(data, output_format))


@app.command(name="show")
def context_show(
    project: Annotated[
        str,
        typer.Argument(help="Project namespaced name"),
    ],
    view: Annotated[
        str | None,
        typer.Option(
            "--view",
            "-v",
            help="Show resolved view for a specific phase",
        ),
    ] = None,
    output_format: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Show the context policy and ACMS pipeline config for a project."""
    from cleveragents.application.container import get_container

    container = get_container()
    session_factory = container.session_factory()

    # Validate project exists
    repo = _get_namespaced_project_repo()
    try:
        repo.get(project)
    except Exception as exc:
        err_console.print(f"[red]Project not found:[/red] {project}")
        raise typer.Exit(1) from exc

    policy = _read_policy(session_factory, project)
    acms = _read_acms_config(session_factory, project)

    if view is not None:
        if view not in VALID_PHASES:
            err_console.print(
                f"[red]Invalid view '{view}': must be one of {PHASE_NAMES}[/red]"
            )
            raise typer.Exit(1)
        resolved = policy.resolve_view(view)
        data: dict[str, Any] = {
            "phase": view,
            "resolved_view": resolved.model_dump(mode="json"),
            "acms_config": acms,
        }
    else:
        data = _policy_to_dict(policy)
        data["acms_config"] = acms
        # Include execution environment fields from raw persisted blob
        try:
            raw_blob = _load_policy_json(session_factory, project)
        except Exception:
            raw_blob = None
        if raw_blob:
            for key in ("execution_environment", "execution_env_priority"):
                if key in raw_blob:
                    data[key] = raw_blob[key]

    if output_format.lower() == OutputFormat.RICH:
        title = f"Context Policy: {project}"
        if view:
            title += f" (phase: {view})"
        lines: list[str] = []
        if view:
            rv = policy.resolve_view(view)
            lines.append(f"[bold]Phase:[/bold] {view}")
            lines.append(
                f"[bold]Include resources:[/bold] {rv.include_resources or '(all)'}"
            )
            lines.append(
                f"[bold]Exclude resources:[/bold] {rv.exclude_resources or '(none)'}"
            )
            lines.append(f"[bold]Include paths:[/bold] {rv.include_paths or '(all)'}")
            lines.append(f"[bold]Exclude paths:[/bold] {rv.exclude_paths or '(none)'}")
            lines.append(
                f"[bold]Max file size:[/bold] {rv.max_file_size or '(no limit)'}"
            )
            lines.append(
                f"[bold]Max total size:[/bold] {rv.max_total_size or '(no limit)'}"
            )
        else:
            for phase in [
                "default",
                "strategize",
                "execute",
                "apply",
            ]:
                view_obj = getattr(policy, f"{phase}_view")
                if view_obj is not None:
                    lines.append(f"[bold]{phase}:[/bold] configured")
                else:
                    lines.append(f"[bold]{phase}:[/bold] (inherits)")
        # Execution environment section
        try:
            raw_blob = _load_policy_json(session_factory, project)
        except Exception:
            raw_blob = None
        ee = (raw_blob or {}).get("execution_environment")
        eep = (raw_blob or {}).get("execution_env_priority")
        if ee or eep:
            lines.append("")
            lines.append("[bold]Execution Environment:[/bold]")
            if ee:
                lines.append(f"  Environment: {ee}")
            if eep:
                lines.append(f"  Priority: {eep}")

        # ACMS pipeline config section
        lines.append("")
        lines.append("[bold]ACMS Pipeline Config:[/bold]")
        lines.append(
            f"  Hot max tokens: {acms.get('hot_max_tokens', _DEFAULT_HOT_MAX_TOKENS)}"
        )
        lines.append(
            f"  Warm max decisions: "
            f"{acms.get('warm_max_decisions', _DEFAULT_WARM_MAX_DECISIONS)}"
        )
        lines.append(
            f"  Cold max decisions: "
            f"{acms.get('cold_max_decisions', _DEFAULT_COLD_MAX_DECISIONS)}"
        )
        lines.append(f"  Summarize: {acms.get('summarize', True)}")
        lines.append(f"  Temporal scope: {acms.get('temporal_scope', 'current')}")
        lines.append(f"  Auto-refresh: {acms.get('auto_refresh', True)}")
        strategies = acms.get("strategies", [])
        lines.append(
            f"  Strategies: {', '.join(strategies) if strategies else '(default)'}"
        )
        lines.append(
            f"  Default breadth: {acms.get('default_breadth', _DEFAULT_BREADTH)}"
        )
        lines.append(f"  Default depth: {acms.get('default_depth', _DEFAULT_DEPTH)}")
        dg = acms.get("depth_gradient", {})
        if dg:
            gradient_parts = [
                f"{h}:{v}" for h, v in sorted(dg.items(), key=lambda x: int(x[0]))
            ]
            lines.append(f"  Depth gradient: {', '.join(gradient_parts)}")
        else:
            lines.append("  Depth gradient: (none)")
        lines.append(
            f"  Skeleton ratio: {acms.get('skeleton_ratio', _DEFAULT_SKELETON_RATIO)}"
        )
        console.print(
            Panel(
                "\n".join(lines),
                title=title,
                expand=False,
            )
        )
    else:
        console.print(format_output(data, output_format))


@app.command(name="inspect")
def context_inspect(
    project: Annotated[
        str,
        typer.Argument(help="Project namespaced name"),
    ],
    view: Annotated[
        str | None,
        typer.Option(
            "--view",
            "-v",
            help="Phase view to inspect: default, strategize, execute, or apply",
        ),
    ] = None,
    strategy_filter: Annotated[
        str | None,
        typer.Option(
            "--strategy",
            help="Filter fragments by strategy name",
        ),
    ] = None,
    focus: Annotated[
        list[str] | None,
        typer.Option(
            "--focus",
            help="UKO URIs to focus on (repeatable)",
        ),
    ] = None,
    breadth: Annotated[
        int | None,
        typer.Option(
            "--breadth",
            help="Override breadth for context retrieval",
        ),
    ] = None,
    depth: Annotated[
        str | None,
        typer.Option(
            "--depth",
            help="Override depth (integer or named level)",
        ),
    ] = None,
    output_format: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Inspect effective context state for a project.

    Queries the ACMS tier service to display the current context
    fragments across hot/warm/cold tiers, tier metrics, budget
    utilisation, and the effective ACMS pipeline configuration.
    """
    if view is not None and view not in VALID_PHASES:
        err_console.print(
            f"[red]Invalid view '{view}': must be one of {PHASE_NAMES}[/red]"
        )
        raise typer.Exit(1)

    from cleveragents.application.container import get_container

    container = get_container()
    session_factory = container.session_factory()

    # Validate project exists
    repo = _get_namespaced_project_repo()
    try:
        repo.get(project)
    except Exception as exc:
        err_console.print(f"[red]Project not found:[/red] {project}")
        raise typer.Exit(1) from exc

    # Query the tier service (project-scoped fragment counts)
    tier_service = _get_context_tier_service()
    metrics: TierMetrics = tier_service.get_scoped_metrics([project])
    budget = tier_service.budget

    # Get project-scoped fragments
    project_fragments: list[TieredFragment] = tier_service.get_scoped_view([project])

    # Apply optional filters
    if strategy_filter is not None:
        # Filter fragments whose metadata or resource_id relates to the strategy
        project_fragments = [
            f
            for f in project_fragments
            if f.metadata.get("strategy") == strategy_filter
            or strategy_filter in (f.resource_id or "")
        ]
    if focus is not None:
        # Filter fragments whose resource_id matches any focus URI
        project_fragments = [
            f
            for f in project_fragments
            if f.resource_id in focus or any(u in (f.resource_id or "") for u in focus)
        ]

    # Get actor views for each role
    actor_views: dict[str, int] = {}
    for role in ActorRole:
        role_frags = tier_service.get_for_actor(role, [project])
        actor_views[role.value] = len(role_frags)

    # Read ACMS config and apply overrides
    acms = _read_acms_config(session_factory, project)
    policy = _read_policy(session_factory, project)
    if breadth is not None:
        acms["default_breadth"] = breadth
    if depth is not None:
        try:
            acms["default_depth"] = int(depth)
        except ValueError:
            acms["default_depth"] = depth

    budget_tokens = int(acms.get("hot_max_tokens", _DEFAULT_HOT_MAX_TOKENS))
    phase_analysis = analyze_phase_summaries(policy, project_fragments, budget_tokens)

    # Build result
    data: dict[str, Any] = {
        "project": project,
        "view": view or "all",
        "tier_metrics": _tier_metrics_to_dict(metrics),
        "tier_budget": {
            "max_tokens_hot": budget.max_tokens_hot,
            "max_decisions_warm": budget.max_decisions_warm,
            "max_decisions_cold": budget.max_decisions_cold,
        },
        "project_fragments": {
            "total": len(project_fragments),
            "by_tier": {
                "hot": len([f for f in project_fragments if f.tier == ContextTier.HOT]),
                "warm": len(
                    [f for f in project_fragments if f.tier == ContextTier.WARM]
                ),
                "cold": len(
                    [f for f in project_fragments if f.tier == ContextTier.COLD]
                ),
            },
        },
        "actor_visibility": actor_views,
        "acms_config": acms,
        "phase_analysis": phase_analysis,
    }

    # Add fragment details (limited to first 20)
    fragment_details = [_fragment_to_dict(f) for f in project_fragments[:20]]
    data["fragments"] = fragment_details

    if output_format.lower() == OutputFormat.RICH:
        title = f"Context State: {project}"
        if view:
            title += f" (phase: {view})"

        # Metrics table
        metrics_table = Table(title="Tier Metrics", expand=False)
        metrics_table.add_column("Metric", style="bold")
        metrics_table.add_column("Value", justify="right")
        metrics_table.add_row("Hot fragments", str(metrics.hot_count))
        metrics_table.add_row("Warm fragments", str(metrics.warm_count))
        metrics_table.add_row("Cold fragments", str(metrics.cold_count))
        metrics_table.add_row("Hot hits", str(metrics.hot_hit_count))
        metrics_table.add_row("Hot misses", str(metrics.hot_miss_count))
        metrics_table.add_row("Warm hits", str(metrics.warm_hit_count))
        metrics_table.add_row("Warm misses", str(metrics.warm_miss_count))
        console.print(Panel(metrics_table, title=title, expand=False))

        # Budget info
        budget_lines: list[str] = []
        budget_lines.append(f"[bold]Hot max tokens:[/bold] {budget.max_tokens_hot}")
        budget_lines.append(
            f"[bold]Warm max decisions:[/bold] {budget.max_decisions_warm}"
        )
        budget_lines.append(
            f"[bold]Cold max decisions:[/bold] {budget.max_decisions_cold}"
        )
        console.print(Panel("\n".join(budget_lines), title="Tier Budget", expand=False))

        # Project fragments summary
        by_tier = data["project_fragments"]["by_tier"]
        console.print(
            Panel(
                f"Total: {data['project_fragments']['total']}  |  "
                f"Hot: {by_tier['hot']}  |  "
                f"Warm: {by_tier['warm']}  |  "
                f"Cold: {by_tier['cold']}",
                title=f"Project Fragments: {project}",
                expand=False,
            )
        )

        # Actor visibility
        av_lines = [f"[bold]{r}:[/bold] {c} fragments" for r, c in actor_views.items()]
        console.print(
            Panel("\n".join(av_lines), title="Actor Visibility", expand=False)
        )

        phase_table = Table(title="Per-Phase Context Analysis", expand=False)
        phase_table.add_column("Phase", style="bold")
        phase_table.add_column("Fragments", justify="right")
        phase_table.add_column("Resources", justify="right")
        phase_table.add_column("Bytes", justify="right")
        phase_table.add_column("Tokens", justify="right")
        phase_table.add_column("Budget", justify="right")
        for phase in ["strategize", "execute", "apply"]:
            row = phase_analysis["phases"][phase]
            phase_table.add_row(
                phase,
                str(row["fragment_count"]),
                str(row["resource_count"]),
                str(row["total_bytes"]),
                str(row["total_tokens"]),
                f"{row['budget_used_ratio']:.1%}",
            )
        console.print(phase_table)

        narrowing = phase_analysis["narrowing"]
        console.print(
            Panel(
                "\n".join(
                    [
                        "[bold]Narrowing checks:[/bold]",
                        (
                            "tokens strategize >= execute >= apply: "
                            f"{narrowing['tokens_non_increasing']}"
                        ),
                        (
                            "bytes strategize >= execute >= apply: "
                            f"{narrowing['bytes_non_increasing']}"
                        ),
                        (
                            "fragments strategize >= execute >= apply: "
                            f"{narrowing['fragments_non_increasing']}"
                        ),
                    ]
                ),
                title=(
                    "Phase Narrowing "
                    f"({'OK' if narrowing['overall'] else 'Needs Review'})"
                ),
                expand=False,
            )
        )
    else:
        console.print(format_output(data, output_format))


@app.command(name="simulate")
def context_simulate(
    project: Annotated[
        str,
        typer.Argument(help="Project namespaced name"),
    ],
    view: Annotated[
        str | None,
        typer.Option(
            "--view",
            "-v",
            help="Phase view for simulation: default, strategize, execute, or apply",
        ),
    ] = None,
    budget: Annotated[
        int | None,
        typer.Option(
            "--budget",
            help="Token budget for simulation (overrides project config)",
        ),
    ] = None,
    focus: Annotated[
        list[str] | None,
        typer.Option(
            "--focus",
            help="UKO URIs to focus on (repeatable)",
        ),
    ] = None,
    strategy_hint: Annotated[
        list[str] | None,
        typer.Option(
            "--strategy",
            help="Preferred strategies for simulation (repeatable)",
        ),
    ] = None,
    output_format: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Simulate context window assembly for a project.

    Runs a dry-run context assembly using the project's ACMS
    configuration and displays the assembled fragments, budget
    usage, and strategy selection.
    """
    effective_view = view or "default"
    if effective_view not in VALID_PHASES:
        err_console.print(
            f"[red]Invalid view '{effective_view}': must be one of {PHASE_NAMES}[/red]"
        )
        raise typer.Exit(1)

    from cleveragents.application.container import get_container

    container = get_container()
    session_factory = container.session_factory()

    # Validate project exists
    repo = _get_namespaced_project_repo()
    try:
        repo.get(project)
    except Exception as exc:
        err_console.print(f"[red]Project not found:[/red] {project}")
        raise typer.Exit(1) from exc

    acms = _read_acms_config(session_factory, project)
    policy = _read_policy(session_factory, project)
    tier_service = _get_context_tier_service()
    project_fragments = tier_service.get_scoped_view([project])
    budget_tokens = int(acms.get("hot_max_tokens", _DEFAULT_HOT_MAX_TOKENS))
    phase_analysis = analyze_phase_summaries(policy, project_fragments, budget_tokens)
    assembled = _simulate_context_assembly(
        project_name=project,
        acms_config=acms,
        budget_tokens=budget,
        focus_uris=focus,
        strategy_hints=strategy_hint,
        view=effective_view,
    )

    data = _assembled_to_dict(assembled)
    data["project"] = project
    data["view"] = effective_view
    data["acms_config"] = acms
    data["phase_analysis"] = phase_analysis

    if output_format.lower() == OutputFormat.RICH:
        title = f"Context Simulation: {project} (view: {effective_view})"

        # Summary panel
        summary_lines: list[str] = []
        summary_lines.append(f"[bold]Total tokens:[/bold] {assembled.total_tokens}")
        summary_lines.append(f"[bold]Budget used:[/bold] {assembled.budget_used:.1%}")
        summary_lines.append(
            f"[bold]Strategies:[/bold] "
            f"{', '.join(assembled.strategies_used) or '(none)'}"
        )
        summary_lines.append(f"[bold]Fragment count:[/bold] {len(assembled.fragments)}")
        summary_lines.append(f"[bold]Context hash:[/bold] {assembled.context_hash}")
        if assembled.preamble:
            summary_lines.append(f"[bold]Preamble:[/bold] {assembled.preamble}")
        console.print(Panel("\n".join(summary_lines), title=title, expand=False))

        # Fragment table
        if assembled.fragments:
            frag_table = Table(title="Assembled Fragments", expand=False)
            frag_table.add_column("#", style="dim", justify="right")
            frag_table.add_column("UKO Node")
            frag_table.add_column("Tokens", justify="right")
            frag_table.add_column("Relevance", justify="right")
            frag_table.add_column("Depth", justify="right")
            frag_table.add_column("Strategy")
            for idx, frag in enumerate(assembled.fragments, 1):
                frag_table.add_row(
                    str(idx),
                    frag.uko_node,
                    str(frag.token_count),
                    f"{frag.relevance_score:.2f}",
                    str(frag.detail_depth),
                    frag.provenance.strategy,
                )
            console.print(frag_table)

        phase_table = Table(title="Per-Phase Context Analysis", expand=False)
        phase_table.add_column("Phase", style="bold")
        phase_table.add_column("Fragments", justify="right")
        phase_table.add_column("Resources", justify="right")
        phase_table.add_column("Bytes", justify="right")
        phase_table.add_column("Tokens", justify="right")
        phase_table.add_column("Budget", justify="right")
        for phase in ["strategize", "execute", "apply"]:
            row = phase_analysis["phases"][phase]
            phase_table.add_row(
                phase,
                str(row["fragment_count"]),
                str(row["resource_count"]),
                str(row["total_bytes"]),
                str(row["total_tokens"]),
                f"{row['budget_used_ratio']:.1%}",
            )
        console.print(phase_table)
    else:
        console.print(format_output(data, output_format))
