"""Built-in context skill tools for CleverAgents (CRP).

Provides the ``builtin/context`` skill tools that actors use to issue
context requests during reasoning via the Context Request Protocol:

- **request_context**: Request specific context via the ACMS pipeline.
  Accepts query, focus, breadth, depth, purpose, and an optional
  plan_id.  Delegates to ``ACMSPipeline.assemble()`` with fragments
  sourced from the ``ContextTierService``.
- **query_history**: Query historical context fragments stored across
  hot/warm/cold tiers.  Filters by keyword match on fragment content.
- **get_context_budget**: Return the current context token budget
  including max, reserved, available, and used token counts.

## Registration

```python
from cleveragents.skills.builtins.context_ops import (
    register_skill_context_tools,
)
from cleveragents.tool.registry import ToolRegistry

registry = ToolRegistry()
register_skill_context_tools(registry)
```

## SkillDefinition

```python
from cleveragents.skills.builtins.context_ops import (
    build_context_skill_definition,
)
skill_def = build_context_skill_definition()
```

Based on ``docs/specification.md`` ACMS > CRP > ``builtin/context`` Skill.
"""

from __future__ import annotations

from typing import Any

import structlog
from ulid import ULID

from cleveragents.domain.models.core.context_fragment import (
    ContextBudget,
    ContextFragment,
    FragmentProvenance,
)
from cleveragents.domain.models.core.skill import ResolvedToolEntry, Skill
from cleveragents.domain.models.core.tool import ToolCapability
from cleveragents.skills.protocol import SkillDefinition, SkillMetadata
from cleveragents.tool.registry import ToolRegistry
from cleveragents.tool.runtime import ToolSpec

logger = structlog.get_logger(__name__)

# ---------------------------------------------------------------------------
# Default budget constants
# ---------------------------------------------------------------------------

_DEFAULT_MAX_TOKENS: int = 4096
_DEFAULT_RESERVED_TOKENS: int = 512
_MAX_QUERY_HISTORY_RESULTS: int = 50


# ---------------------------------------------------------------------------
# Lazy service resolution
# ---------------------------------------------------------------------------


def _get_pipeline() -> Any:
    """Lazily resolve the ``ACMSPipeline`` from the DI container."""
    from cleveragents.application.container import get_container

    return get_container().acms_pipeline()


def _get_tier_service() -> Any:
    """Lazily resolve the ``ContextTierService`` from the DI container."""
    from cleveragents.application.container import get_container

    return get_container().context_tier_service()


# ---------------------------------------------------------------------------
# Fragment conversion helpers
# ---------------------------------------------------------------------------


def _tiered_to_pipeline_fragment(
    tiered: Any,
) -> ContextFragment:
    """Convert a ``TieredFragment`` to a pipeline ``ContextFragment``.

    The ACMS pipeline operates on ``ContextFragment`` (from
    ``domain.models.core.context_fragment``), while the tier service
    stores ``TieredFragment`` objects.  This helper bridges the two.
    """
    return ContextFragment(
        fragment_id=tiered.fragment_id,
        uko_node=tiered.resource_id or f"tier:{tiered.fragment_id}",
        content=tiered.content,
        detail_depth=0,
        token_count=tiered.token_count,
        relevance_score=0.5,
        provenance=FragmentProvenance(
            resource_uri=tiered.resource_id or "unknown",
        ),
        tier=tiered.tier.value if hasattr(tiered.tier, "value") else tiered.tier,
    )


def _fragment_to_dict(fragment: ContextFragment) -> dict[str, Any]:
    """Serialise a ``ContextFragment`` to a plain dict for tool output."""
    return {
        "fragment_id": fragment.fragment_id,
        "uko_node": fragment.uko_node,
        "content": fragment.content,
        "detail_depth": fragment.detail_depth,
        "token_count": fragment.token_count,
        "relevance_score": fragment.relevance_score,
        "tier": fragment.tier,
    }


def _tiered_to_dict(tiered: Any) -> dict[str, Any]:
    """Serialise a ``TieredFragment`` to a plain dict for tool output."""
    return {
        "fragment_id": tiered.fragment_id,
        "content": tiered.content,
        "tier": tiered.tier.value if hasattr(tiered.tier, "value") else tiered.tier,
        "token_count": tiered.token_count,
        "resource_id": tiered.resource_id,
        "project_name": tiered.project_name,
    }


# ---------------------------------------------------------------------------
# Handlers (wired to ACMS pipeline and tier services)
# ---------------------------------------------------------------------------


def _handle_request_context(inputs: dict[str, Any]) -> dict[str, Any]:
    """Request specific context via the ACMS pipeline.

    Builds a ``ContextBudget``, sources fragments from the
    ``ContextTierService``, optionally filters by query/focus keywords,
    and delegates to ``ACMSPipeline.assemble()`` for budget-constrained
    context assembly.

    Args:
        inputs: Tool input dict.  Required: ``purpose``.  Optional:
            ``query``, ``focus``, ``breadth``, ``depth``, ``plan_id``.

    Returns:
        Dict with ``fragments`` (list of serialised fragments) and
        ``total_tokens`` (int).
    """
    purpose: str = inputs["purpose"]
    query: str | None = inputs.get("query")
    focus: list[str] = inputs.get("focus", [])
    plan_id: str = inputs.get("plan_id", str(ULID()))

    pipeline = _get_pipeline()
    tier_service = _get_tier_service()

    # Source fragments from all tiers via the tier service
    all_tiered: list[Any] = tier_service.get_all_fragments()

    # Convert TieredFragments to pipeline ContextFragments
    pipeline_fragments: list[ContextFragment] = [
        _tiered_to_pipeline_fragment(tf) for tf in all_tiered
    ]

    # Filter by query keywords if provided
    if query:
        query_lower = query.lower()
        pipeline_fragments = [
            f
            for f in pipeline_fragments
            if query_lower in f.content.lower() or query_lower in f.uko_node.lower()
        ]

    # Filter by focus targets if provided
    if focus:
        focus_lower = [t.lower() for t in focus]
        pipeline_fragments = [
            f
            for f in pipeline_fragments
            if any(
                t in f.content.lower() or t in f.uko_node.lower() for t in focus_lower
            )
        ]

    budget = ContextBudget(
        max_tokens=_DEFAULT_MAX_TOKENS,
        reserved_tokens=_DEFAULT_RESERVED_TOKENS,
    )

    # If no fragments available, return empty result
    if not pipeline_fragments:
        logger.info(
            "context_ops.request_context.empty",
            purpose=purpose,
            query=query,
        )
        return {"fragments": [], "total_tokens": 0}

    payload = pipeline.assemble(
        plan_id=plan_id,
        fragments=pipeline_fragments,
        budget=budget,
    )

    logger.info(
        "context_ops.request_context.assembled",
        purpose=purpose,
        plan_id=plan_id,
        fragment_count=len(payload.fragments),
        total_tokens=payload.total_tokens,
    )

    return {
        "fragments": [_fragment_to_dict(f) for f in payload.fragments],
        "total_tokens": payload.total_tokens,
    }


def _handle_query_history(inputs: dict[str, Any]) -> dict[str, Any]:
    """Query historical context fragments from the tier service.

    Searches across all tiers for fragments whose content or resource ID
    matches the query string via case-insensitive substring matching.

    Args:
        inputs: Tool input dict. Required: ``query``.

    Returns:
        Dict with ``entries`` (list of serialised fragment dicts).
    """
    query: str = inputs["query"]
    tier_service = _get_tier_service()

    # Gather fragments from all tiers
    all_tiered: list[Any] = tier_service.get_all_fragments()

    # Filter by query keywords (case-insensitive substring)
    query_lower = query.lower()
    matched = [
        tf
        for tf in all_tiered
        if query_lower in tf.content.lower()
        or query_lower in tf.fragment_id.lower()
        or query_lower in tf.resource_id.lower()
    ]

    # Sort by last_accessed descending, cap results
    matched.sort(key=lambda f: f.last_accessed, reverse=True)
    matched = matched[:_MAX_QUERY_HISTORY_RESULTS]

    logger.info(
        "context_ops.query_history",
        query=query,
        result_count=len(matched),
    )

    return {"entries": [_tiered_to_dict(tf) for tf in matched]}


def _handle_get_context_budget(inputs: dict[str, Any]) -> dict[str, Any]:
    """Return the current context token budget state.

    Computes used tokens by summing token counts across all fragments
    in the hot tier of the ``ContextTierService``.

    Args:
        inputs: Tool input dict (no required fields).

    Returns:
        Dict with ``max_tokens``, ``reserved_tokens``,
        ``available_tokens``, and ``used_tokens``.
    """
    tier_service = _get_tier_service()

    budget = ContextBudget(
        max_tokens=_DEFAULT_MAX_TOKENS,
        reserved_tokens=_DEFAULT_RESERVED_TOKENS,
    )

    # Calculate used tokens from hot tier
    used_tokens = sum(f.token_count for f in tier_service.get_hot_fragments())

    logger.info(
        "context_ops.get_context_budget",
        max_tokens=budget.max_tokens,
        reserved_tokens=budget.reserved_tokens,
        available_tokens=budget.available_tokens,
        used_tokens=used_tokens,
    )

    return {
        "max_tokens": budget.max_tokens,
        "reserved_tokens": budget.reserved_tokens,
        "available_tokens": budget.available_tokens,
        "used_tokens": used_tokens,
    }


# ---------------------------------------------------------------------------
# Tool specs
# ---------------------------------------------------------------------------

SKILL_CONTEXT_REQUEST_SPEC = ToolSpec(
    name="builtin/request-context",
    description="Request specific context to be added to the conversation",
    input_schema={
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "What information do you need?",
            },
            "focus": {
                "type": "array",
                "items": {"type": "string"},
                "description": ("Specific files, classes, or functions to focus on"),
            },
            "breadth": {
                "type": "integer",
                "default": 2,
                "description": "How many dependency hops outward (0-5)",
            },
            "depth": {
                "oneOf": [
                    {"type": "integer", "minimum": 0},
                    {"type": "string"},
                ],
                "default": 3,
                "description": (
                    "Detail depth -- integer (0-N) or named level from "
                    "the active DetailLevelMap (e.g., 'SIGNATURES', "
                    "'FULL_SOURCE')"
                ),
            },
            "purpose": {
                "type": "string",
                "description": "Why do you need this context?",
            },
            "plan_id": {
                "type": "string",
                "description": (
                    "Plan identifier (ULID).  If omitted a fresh ULID is "
                    "generated for standalone invocation."
                ),
            },
        },
        "required": ["purpose"],
    },
    output_schema={
        "type": "object",
        "properties": {
            "fragments": {
                "type": "array",
                "items": {"type": "object"},
                "description": "Assembled context fragments",
            },
            "total_tokens": {
                "type": "integer",
                "description": "Total tokens consumed",
            },
        },
    },
    capabilities=ToolCapability(read_only=True),
    handler=_handle_request_context,
)

SKILL_CONTEXT_QUERY_HISTORY_SPEC = ToolSpec(
    name="builtin/query-history",
    description="Query historical context about past decisions and changes",
    input_schema={
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "What historical information do you need?",
            },
        },
        "required": ["query"],
    },
    output_schema={
        "type": "object",
        "properties": {
            "entries": {
                "type": "array",
                "items": {"type": "object"},
                "description": "Historical context entries",
            },
        },
    },
    capabilities=ToolCapability(read_only=True),
    handler=_handle_query_history,
)

SKILL_CONTEXT_BUDGET_SPEC = ToolSpec(
    name="builtin/get-context-budget",
    description="Check remaining context token budget",
    input_schema={
        "type": "object",
        "properties": {},
    },
    output_schema={
        "type": "object",
        "properties": {
            "max_tokens": {
                "type": "integer",
                "description": "Maximum token budget",
            },
            "reserved_tokens": {
                "type": "integer",
                "description": "Tokens reserved for skeleton context",
            },
            "available_tokens": {
                "type": "integer",
                "description": "Tokens available for use",
            },
            "used_tokens": {
                "type": "integer",
                "description": "Tokens already consumed",
            },
        },
    },
    capabilities=ToolCapability(read_only=True),
    handler=_handle_get_context_budget,
)

ALL_SKILL_CONTEXT_TOOLS: list[ToolSpec] = [
    SKILL_CONTEXT_REQUEST_SPEC,
    SKILL_CONTEXT_QUERY_HISTORY_SPEC,
    SKILL_CONTEXT_BUDGET_SPEC,
]


# ---------------------------------------------------------------------------
# Tool registration
# ---------------------------------------------------------------------------


def register_skill_context_tools(registry: ToolRegistry) -> None:
    """Register all skill-level context tools into *registry*."""
    for spec in ALL_SKILL_CONTEXT_TOOLS:
        registry.register(spec)


# ---------------------------------------------------------------------------
# SkillDefinition factory
# ---------------------------------------------------------------------------

#: Canonical skill name for the builtin context skill.
BUILTIN_CONTEXT_SKILL_NAME: str = "builtin/context"


def build_context_skill_definition() -> SkillDefinition:
    """Build a ``SkillDefinition`` for the ``builtin/context`` skill.

    The returned definition wraps the three CRP tools
    (``request_context``, ``query_history``, ``get_context_budget``)
    and can be registered in the ``SkillRegistry`` at startup.

    Returns:
        A frozen ``SkillDefinition`` ready for registry insertion.
    """
    skill = Skill(
        name=BUILTIN_CONTEXT_SKILL_NAME,
        description=(
            "Context Request Protocol (CRP) skill providing tools for "
            "requesting context, querying history, and checking budget "
            "during plan execution."
        ),
        tool_refs=[
            "builtin/request-context",
            "builtin/query-history",
            "builtin/get-context-budget",
        ],
    )

    resolved_tools = [
        ResolvedToolEntry(
            name="builtin/request-context",
            source_skill=BUILTIN_CONTEXT_SKILL_NAME,
            is_inline=False,
        ),
        ResolvedToolEntry(
            name="builtin/query-history",
            source_skill=BUILTIN_CONTEXT_SKILL_NAME,
            is_inline=False,
        ),
        ResolvedToolEntry(
            name="builtin/get-context-budget",
            source_skill=BUILTIN_CONTEXT_SKILL_NAME,
            is_inline=False,
        ),
    ]

    metadata = SkillMetadata(
        name=BUILTIN_CONTEXT_SKILL_NAME,
        description=skill.description,
        version="1.0.0",
        tool_count=len(resolved_tools),
        source_types=["tool_ref"],
        writes=False,
        read_only=True,
    )

    return SkillDefinition(
        skill=skill,
        resolved_tools=resolved_tools,
        metadata=metadata,
    )
