"""Built-in context strategies for the ACMS.

Each strategy implements the ``ContextStrategy`` protocol with real
retrieval logic that queries the appropriate backends and returns
``ContextFragment`` objects.

Based on ``docs/specification.md`` §25207-25216 (Built-in Strategies
table) and §43167-43199 (Built-in Strategy Catalogue).

Six built-in strategies per spec:

| Name                       | Quality | Backends Required         |
|----------------------------|---------|---------------------------|
| ``simple-keyword``         | 0.3     | Text only                 |
| ``semantic-embedding``     | 0.6     | Vector                    |
| ``breadth-depth-navigator``| 0.85    | Graph                     |
| ``arce``                   | 0.95    | All                       |
| ``temporal-archaeology``   | 0.5     | Graph + temporal/cold     |
| ``plan-decision-context``  | 0.7     | Warm/cold tiers           |
"""

from __future__ import annotations

import functools

from cleveragents.domain.models.acms.crp import (
    ContextFragment,
    ContextRequest,
    FragmentProvenance,
)
from cleveragents.domain.models.acms.strategy import (
    BackendSet,
    PlanContext,
    StrategyCapabilities,
)
from cleveragents.domain.models.acms.temporal import TierRetentionConfig
from cleveragents.domain.models.acms.tiers import ContextTier
from cleveragents.domain.models.core.project import TemporalScope

__all__ = [
    "BUILTIN_STRATEGY_CLASSES",
    "DEFAULT_ENABLED_STRATEGIES",
    "ARCEStrategy",
    "BreadthDepthNavigatorStrategy",
    "PlanDecisionContextStrategy",
    "SemanticEmbeddingStrategy",
    "SimpleKeywordStrategy",
    "TemporalArchaeologyStrategy",
]

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

# Approximate tokens per character for budget estimation.
# 1 token ≈ 4 characters is a common approximation.
_CHARS_PER_TOKEN: int = 4


def _estimate_tokens(text: str) -> int:
    """Estimate token count from character length."""
    return max(1, len(text) // _CHARS_PER_TOKEN)


def _make_fragment(
    uko_node: str,
    content: str,
    relevance_score: float,
    strategy_name: str,
    resource_uri: str = "",
    location: str = "",
    detail_depth: int = 1,
    metadata: dict | None = None,
) -> ContextFragment:
    """Build a ``ContextFragment`` from backend result data."""
    return ContextFragment(
        uko_node=uko_node,
        content=content,
        detail_depth=detail_depth,
        token_count=_estimate_tokens(content),
        relevance_score=min(1.0, max(0.0, relevance_score)),
        provenance=FragmentProvenance(
            resource_uri=resource_uri or uko_node,
            location=location,
            strategy=strategy_name,
        ),
        metadata=metadata or {},
    )


def _scope_from_request(request: ContextRequest) -> frozenset[str]:
    """Extract a resource scope frozenset from the request's focus list."""
    return frozenset(request.focus) if request.focus else frozenset()


def _query_text(request: ContextRequest) -> str:
    """Return the best query string from the request."""
    if request.query:
        return request.query
    if request.entities:
        return " ".join(request.entities)
    return ""


def _budget_fragments(
    fragments: list[ContextFragment],
    budget: int,
) -> list[ContextFragment]:
    """Return fragments that fit within the token budget (greedy packing)."""
    if budget <= 0:
        return fragments  # No budget constraint — return all
    result: list[ContextFragment] = []
    used = 0
    for frag in fragments:
        if used + frag.token_count <= budget:
            result.append(frag)
            used += frag.token_count
    return result


def _sparql_str_literal(value: str) -> str:
    """Escape a string for use in a SPARQL string literal."""
    escaped = value.replace("\\", "\\\\").replace('"', '\\"')
    return f'"{escaped}"'


def _triples_to_fragments(
    triples: list[tuple[str, str, str]],
    strategy_name: str,
    base_score: float,
    budget: int,
) -> list[ContextFragment]:
    """Convert a list of RDF triples to ContextFragment objects."""
    fragments = [
        _make_fragment(
            uko_node=triple[0],
            content=f"{triple[0]} {triple[1]} {triple[2]}",
            relevance_score=base_score,
            strategy_name=strategy_name,
            resource_uri=triple[0],
            metadata={"predicate": triple[1], "object": triple[2]},
        )
        for triple in triples
    ]
    fragments.sort(key=lambda f: f.relevance_score, reverse=True)
    return _budget_fragments(fragments, budget)


# ---------------------------------------------------------------------------
# simple-keyword  (spec §25211, §43171-43173)
# ---------------------------------------------------------------------------


class SimpleKeywordStrategy:
    """Basic keyword/regex text search.  Universal fallback.

    Queries the ``TextBackend`` with keywords extracted from the
    ``ContextRequest`` query or entity list.  Returns fragments sorted
    by backend relevance score, packed within the token budget.

    Works with any backend, any resource type.  No graph or vector
    required.  Quality score: 0.3.

    Spec: ``specification.md:25211``, ``specification.md:43171-43173``.
    """

    @property
    def name(self) -> str:
        return "simple-keyword"

    @functools.cached_property
    def capabilities(self) -> StrategyCapabilities:
        return StrategyCapabilities(
            uses_text=True,
            uses_vector=False,
            uses_graph=False,
            uses_temporal=False,
            resource_types=("*",),
            quality_score=0.3,
        )

    def can_handle(
        self,
        request: ContextRequest,
        backends: BackendSet,
    ) -> float:
        """Returns quality score if text backend is available."""
        if backends.text is not None:
            return self.capabilities.quality_score
        return 0.0

    def assemble(
        self,
        request: ContextRequest,
        backends: BackendSet,
        budget: int,
        plan_context: PlanContext,
    ) -> list[ContextFragment]:
        """Query TextBackend with keywords from the ContextRequest.

        Returns fragments sorted by relevance score (descending),
        packed within the token budget.
        """
        if backends.text is None:
            return []

        query = _query_text(request)
        if not query:
            return []

        scope = _scope_from_request(request)
        max_results = max(1, budget // max(1, _CHARS_PER_TOKEN * 10))

        results = backends.text.search(
            query,
            scope=scope,
            max_results=max_results,
        )

        fragments = [
            _make_fragment(
                uko_node=r.uko_uri,
                content=r.content,
                relevance_score=r.score,
                strategy_name=self.name,
                resource_uri=r.uko_uri,
                metadata=dict(r.metadata),
            )
            for r in results
        ]

        # Sort by relevance descending
        fragments.sort(key=lambda f: f.relevance_score, reverse=True)
        return _budget_fragments(fragments, budget)

    def explain(self) -> str:
        return (
            "Basic keyword/regex text search. Universal fallback. "
            "Works with any text backend."
        )


# ---------------------------------------------------------------------------
# semantic-embedding  (spec §25212, §43175-43177)
# ---------------------------------------------------------------------------


class SemanticEmbeddingStrategy:
    """Vector similarity search for semantically related content.

    Queries the ``VectorBackend`` with a semantic embedding derived from
    the ``ContextRequest`` query.  In v1, the query string is converted
    to a simple character-frequency embedding vector.  Returns fragments
    sorted by cosine similarity score, packed within the token budget.

    Requires a vector backend.  Quality score: 0.6.

    Spec: ``specification.md:25212``, ``specification.md:43175-43177``.
    """

    @property
    def name(self) -> str:
        return "semantic-embedding"

    @functools.cached_property
    def capabilities(self) -> StrategyCapabilities:
        return StrategyCapabilities(
            uses_text=False,
            uses_vector=True,
            uses_graph=False,
            uses_temporal=False,
            resource_types=("*",),
            quality_score=0.6,
        )

    def can_handle(
        self,
        request: ContextRequest,
        backends: BackendSet,
    ) -> float:
        if backends.vector is not None:
            return self.capabilities.quality_score
        return 0.0

    @staticmethod
    def _query_to_embedding(query: str) -> list[float]:
        """Convert a query string to a simple character-frequency embedding.

        In v1, this is a bag-of-characters vector over the 64 most common
        ASCII characters (ordinals 32-95), normalised to unit length.
        Production implementations will use a real embedding model.
        """
        if not query:
            return [0.0] * 64
        # Character frequency vector (printable ASCII range 32-95)
        vec = [0.0] * 64
        for ch in query.lower():
            idx = ord(ch) - 32
            if 0 <= idx < 64:
                vec[idx] += 1.0
        # L2 normalise
        magnitude = sum(v * v for v in vec) ** 0.5
        if magnitude > 0:
            vec = [v / magnitude for v in vec]
        return vec

    def assemble(
        self,
        request: ContextRequest,
        backends: BackendSet,
        budget: int,
        plan_context: PlanContext,
    ) -> list[ContextFragment]:
        """Query VectorBackend with semantic embedding of request query.

        Returns fragments sorted by similarity score (descending),
        packed within the token budget.
        """
        if backends.vector is None:
            return []

        query = _query_text(request)
        if not query:
            return []

        embedding = self._query_to_embedding(query)
        scope = _scope_from_request(request)
        top_k = max(1, budget // max(1, _CHARS_PER_TOKEN * 10))

        results = backends.vector.similarity_search(
            embedding,
            scope=scope,
            top_k=top_k,
        )

        fragments = [
            _make_fragment(
                uko_node=r.uko_uri,
                content=r.content,
                relevance_score=r.score,
                strategy_name=self.name,
                resource_uri=r.uko_uri,
                metadata=dict(r.metadata),
            )
            for r in results
        ]

        fragments.sort(key=lambda f: f.relevance_score, reverse=True)
        return _budget_fragments(fragments, budget)

    def explain(self) -> str:
        return (
            "Vector similarity search. Finds semantically related "
            "content even without exact keyword matches. "
            "Requires a vector backend."
        )


# ---------------------------------------------------------------------------
# breadth-depth-navigator  (spec §25213, §43179-43181)
# ---------------------------------------------------------------------------


class BreadthDepthNavigatorStrategy:
    """Graph-aware UKO traversal with depth/breadth projection.

    Traverses the ``GraphBackend`` from focus nodes specified in the
    ``ContextRequest``, collecting triples up to the requested breadth
    (hop count).  Each triple is converted to a ``ContextFragment``
    with a relevance score based on the quality score.

    Primary strategy for code-aware context.  Requires a graph backend.
    Quality score: 0.85.

    Spec: ``specification.md:25213``, ``specification.md:43179-43181``.
    """

    @property
    def name(self) -> str:
        return "breadth-depth-navigator"

    @functools.cached_property
    def capabilities(self) -> StrategyCapabilities:
        return StrategyCapabilities(
            uses_text=False,
            uses_vector=False,
            uses_graph=True,
            uses_temporal=False,
            resource_types=("*",),
            supports_depth_breadth=True,
            quality_score=0.85,
        )

    def can_handle(
        self,
        request: ContextRequest,
        backends: BackendSet,
    ) -> float:
        if backends.graph is not None:
            return self.capabilities.quality_score
        return 0.0

    def assemble(
        self,
        request: ContextRequest,
        backends: BackendSet,
        budget: int,
        plan_context: PlanContext,
    ) -> list[ContextFragment]:
        """Traverse GraphBackend from focus nodes with specified breadth/depth.

        For each focus node, traverses up to ``request.breadth`` hops
        and collects triples.  Falls back to SPARQL query when no focus
        nodes are specified.  Returns fragments packed within the token
        budget.
        """
        if backends.graph is None:
            return []

        focus_nodes = list(request.focus)
        if not focus_nodes:
            # No focus nodes — try SPARQL query if query text is available
            query_text = _query_text(request)
            if not query_text:
                return []
            scope = _scope_from_request(request)
            sparql = (
                f"SELECT ?s ?p ?o WHERE {{ "
                f"?s ?p ?o . "
                f"FILTER(CONTAINS(STR(?s), {_sparql_str_literal(query_text)}) || "
                f"CONTAINS(STR(?o), {_sparql_str_literal(query_text)})) "
                f"}} LIMIT 50"
            )
            result = backends.graph.sparql_query(sparql, scope=scope)
            return _triples_to_fragments(
                result.triples,
                strategy_name=self.name,
                base_score=self.capabilities.quality_score,
                budget=budget,
            )

        # Traverse from each focus node
        all_fragments: list[ContextFragment] = []
        seen_triples: set[tuple[str, str, str]] = set()
        max_depth = max(1, int(request.breadth))

        for focus_uri in focus_nodes:
            result = backends.graph.traverse(focus_uri, depth=max_depth)
            for triple in result.triples:
                if triple not in seen_triples:
                    seen_triples.add(triple)
                    content = f"{triple[0]} {triple[1]} {triple[2]}"
                    all_fragments.append(
                        _make_fragment(
                            uko_node=triple[0],
                            content=content,
                            relevance_score=self.capabilities.quality_score,
                            strategy_name=self.name,
                            resource_uri=triple[0],
                            metadata={"predicate": triple[1], "object": triple[2]},
                        )
                    )

        all_fragments.sort(key=lambda f: f.relevance_score, reverse=True)
        return _budget_fragments(all_fragments, budget)

    def explain(self) -> str:
        return (
            "Graph-aware UKO traversal with depth/breadth projection. "
            "Primary strategy for code-aware context. "
            "Supports focus items, hop traversal, and detail depth gradients."
        )


# ---------------------------------------------------------------------------
# arce  (spec §25214, §43183-43191)
# ---------------------------------------------------------------------------


class ARCEStrategy:
    """Multi-modal pipeline combining text, vector, and graph.

    Autonomous Reasoning Context Extraction (ARCE).  Queries all three
    backends in sequence and merges results, deduplicating by UKO URI.
    Text results anchor the search; vector results expand semantically;
    graph results provide structural context.

    Requires all backends.  Highest quality.  Quality score: 0.95.

    Spec: ``specification.md:25214``, ``specification.md:43183-43191``.
    """

    @property
    def name(self) -> str:
        return "arce"

    @functools.cached_property
    def capabilities(self) -> StrategyCapabilities:
        return StrategyCapabilities(
            uses_text=True,
            uses_vector=True,
            uses_graph=True,
            uses_temporal=False,
            resource_types=("*",),
            supports_depth_breadth=True,
            quality_score=0.95,
        )

    def can_handle(
        self,
        request: ContextRequest,
        backends: BackendSet,
    ) -> float:
        if (
            backends.text is not None
            and backends.vector is not None
            and backends.graph is not None
        ):
            return self.capabilities.quality_score
        return 0.0

    def assemble(
        self,
        request: ContextRequest,
        backends: BackendSet,
        budget: int,
        plan_context: PlanContext,
    ) -> list[ContextFragment]:
        """Multi-modal pipeline: text + vector + graph results merged.

        Allocates budget proportionally across backends (40% text,
        40% vector, 20% graph).  Deduplicates by UKO URI, preferring
        higher-scored fragments.  Returns packed within total budget.
        """
        if backends.text is None or backends.vector is None or backends.graph is None:
            return []

        query = _query_text(request)
        if not query:
            return []

        scope = _scope_from_request(request)
        # Allocate budget across backends
        text_budget = max(1, int(budget * 0.4))
        vector_budget = max(1, int(budget * 0.4))

        all_fragments: list[ContextFragment] = []
        seen_uris: dict[str, float] = {}  # uri -> best score

        # --- Text phase ---
        text_max = max(1, text_budget // max(1, _CHARS_PER_TOKEN * 10))
        text_results = backends.text.search(query, scope=scope, max_results=text_max)
        for r in text_results:
            frag = _make_fragment(
                uko_node=r.uko_uri,
                content=r.content,
                relevance_score=r.score,
                strategy_name=self.name,
                resource_uri=r.uko_uri,
                metadata={**dict(r.metadata), "source": "text"},
            )
            if r.uko_uri not in seen_uris or r.score > seen_uris[r.uko_uri]:
                seen_uris[r.uko_uri] = r.score
                all_fragments.append(frag)

        # --- Vector phase ---
        embedding = SemanticEmbeddingStrategy._query_to_embedding(query)
        vector_top_k = max(1, vector_budget // max(1, _CHARS_PER_TOKEN * 10))
        vector_results = backends.vector.similarity_search(
            embedding, scope=scope, top_k=vector_top_k
        )
        for r in vector_results:
            if r.uko_uri not in seen_uris or r.score > seen_uris[r.uko_uri]:
                seen_uris[r.uko_uri] = r.score
                frag = _make_fragment(
                    uko_node=r.uko_uri,
                    content=r.content,
                    relevance_score=r.score,
                    strategy_name=self.name,
                    resource_uri=r.uko_uri,
                    metadata={**dict(r.metadata), "source": "vector"},
                )
                all_fragments.append(frag)

        # --- Graph phase (focus traversal) ---
        focus_nodes = list(request.focus)
        if focus_nodes:
            for focus_uri in focus_nodes[:3]:  # Limit to 3 focus nodes
                result = backends.graph.traverse(focus_uri, depth=1)
                for triple in result.triples[:10]:  # Limit triples per node
                    uri = triple[0]
                    content = f"{triple[0]} {triple[1]} {triple[2]}"
                    score = 0.7  # Graph results get moderate score
                    if uri not in seen_uris or score > seen_uris[uri]:
                        seen_uris[uri] = score
                        all_fragments.append(
                            _make_fragment(
                                uko_node=uri,
                                content=content,
                                relevance_score=score,
                                strategy_name=self.name,
                                resource_uri=uri,
                                metadata={
                                    "predicate": triple[1],
                                    "object": triple[2],
                                    "source": "graph",
                                },
                            )
                        )

        all_fragments.sort(key=lambda f: f.relevance_score, reverse=True)
        return _budget_fragments(all_fragments, budget)

    def explain(self) -> str:
        return (
            "Multi-modal pipeline combining text, vector, and graph "
            "with intent analysis. Autonomous Reasoning Context "
            "Extraction (ARCE). Highest quality."
        )


# ---------------------------------------------------------------------------
# temporal-archaeology  (spec §25215, §43193-43195)
# ---------------------------------------------------------------------------


class TemporalArchaeologyStrategy:
    """Historical pattern discovery from past decisions and archived context.

    Queries the ``GraphBackend`` for current nodes and the
    ``TemporalBackend`` for historical versions, surfacing patterns
    from past decisions and archived context.  Useful for understanding
    how the codebase evolved and what decisions were made.

    Requires graph backend and cold-tier access.  Quality score: 0.5.

    Spec: ``specification.md:25215``, ``specification.md:43193-43195``.
    """

    @property
    def name(self) -> str:
        return "temporal-archaeology"

    @functools.cached_property
    def capabilities(self) -> StrategyCapabilities:
        return StrategyCapabilities(
            uses_text=False,
            uses_vector=False,
            uses_graph=True,
            uses_temporal=True,
            resource_types=("*",),
            supports_temporal=True,
            quality_score=0.5,
        )

    def can_handle(
        self,
        request: ContextRequest,
        backends: BackendSet,
    ) -> float:
        # Requires both graph AND temporal/cold-tier (spec §43193-43195).
        if backends.graph is not None and backends.temporal is not None:
            return self.capabilities.quality_score
        return 0.0

    def assemble(
        self,
        request: ContextRequest,
        backends: BackendSet,
        budget: int,
        plan_context: PlanContext,
    ) -> list[ContextFragment]:
        """Query GraphBackend and TemporalBackend for historical versions.

        Retrieves current graph triples for focus nodes, then queries
        the temporal backend for historical versions of those nodes.
        Returns fragments representing the temporal evolution of the
        requested context.
        """
        if backends.graph is None or backends.temporal is None:
            return []

        all_fragments: list[ContextFragment] = []

        # Determine temporal scope from request
        temporal_scope = request.temporal

        # Query temporal backend for historical nodes
        retention = TierRetentionConfig()
        tier_result = backends.temporal.query_by_tier(
            tier=ContextTier.COLD,
            temporal_scope=temporal_scope,
            retention=retention,
        )

        for node in tier_result.nodes:
            # Score historical nodes lower than current ones
            is_current = node.temporal.is_current
            score = (
                self.capabilities.quality_score
                if is_current
                else (self.capabilities.quality_score * 0.7)
            )
            content = (
                f"Node: {node.node_uri} "
                f"(source: {node.source_path}, "
                f"valid_from: {node.temporal.valid_from.isoformat()}"
                + (
                    f", valid_until: {node.temporal.valid_until.isoformat()}"
                    if node.temporal.valid_until
                    else ""
                )
                + ")"
            )
            all_fragments.append(
                _make_fragment(
                    uko_node=node.node_uri,
                    content=content,
                    relevance_score=score,
                    strategy_name=self.name,
                    resource_uri=node.source_resource,
                    location=node.source_path,
                    metadata={
                        "is_current": is_current,
                        "valid_from": node.temporal.valid_from.isoformat(),
                        "is_revision_of": node.temporal.is_revision_of or "",
                    },
                )
            )

        # Also query graph for focus nodes if available
        focus_nodes = list(request.focus)
        if focus_nodes:
            for focus_uri in focus_nodes[:5]:
                result = backends.graph.traverse(focus_uri, depth=1)
                for triple in result.triples[:5]:
                    content = f"{triple[0]} {triple[1]} {triple[2]}"
                    all_fragments.append(
                        _make_fragment(
                            uko_node=triple[0],
                            content=content,
                            relevance_score=self.capabilities.quality_score * 0.8,
                            strategy_name=self.name,
                            resource_uri=triple[0],
                            metadata={
                                "predicate": triple[1],
                                "object": triple[2],
                                "source": "graph",
                            },
                        )
                    )

        all_fragments.sort(key=lambda f: f.relevance_score, reverse=True)
        return _budget_fragments(all_fragments, budget)

    def explain(self) -> str:
        return (
            "Historical pattern discovery from past decisions and "
            "archived context. Searches the cold tier for temporal "
            "patterns."
        )


# ---------------------------------------------------------------------------
# plan-decision-context  (spec §25216, §43197-43199)
# ---------------------------------------------------------------------------


class PlanDecisionContextStrategy:
    """Retrieves context from parent/ancestor plan decisions.

    How child plans 'remember' what their parent decided and why.
    Queries the ``TemporalBackend`` for decisions associated with
    parent and ancestor plan IDs from the ``PlanContext``.

    Operates on warm/cold tiers.  Quality score: 0.7.

    Spec: ``specification.md:25216``, ``specification.md:43197-43199``.
    """

    @property
    def name(self) -> str:
        return "plan-decision-context"

    @functools.cached_property
    def capabilities(self) -> StrategyCapabilities:
        return StrategyCapabilities(
            uses_text=False,
            uses_vector=False,
            uses_graph=False,
            uses_temporal=True,
            resource_types=("*",),
            supports_plan_hierarchy=True,
            quality_score=0.7,
        )

    def can_handle(
        self,
        request: ContextRequest,
        backends: BackendSet,
    ) -> float:
        # Requires temporal/cold-tier backend (spec §43197-43199).
        if backends.temporal is not None:
            return self.capabilities.quality_score
        return 0.0

    def assemble(
        self,
        request: ContextRequest,
        backends: BackendSet,
        budget: int,
        plan_context: PlanContext,
    ) -> list[ContextFragment]:
        """Retrieve decisions from parent/ancestor plans via TemporalBackend.

        Queries the temporal backend for nodes associated with the
        parent plan and ancestor plan IDs.  Returns fragments
        representing decisions made in ancestor plans, ordered by
        recency (most recent first).
        """
        if backends.temporal is None:
            return []

        all_fragments: list[ContextFragment] = []

        # Collect plan IDs to query: parent + ancestors
        plan_ids_to_query: list[str] = []
        if plan_context.parent_plan_id:
            plan_ids_to_query.append(plan_context.parent_plan_id)
        plan_ids_to_query.extend(plan_context.ancestor_plan_ids)

        if not plan_ids_to_query:
            # No parent/ancestor plans — query warm tier for recent decisions
            retention = TierRetentionConfig()
            tier_result = backends.temporal.query_by_tier(
                tier=ContextTier.WARM,
                temporal_scope=TemporalScope.RECENT,
                retention=retention,
            )
            for node in tier_result.nodes:
                content = (
                    f"Decision node: {node.node_uri} "
                    f"(source: {node.source_path}, "
                    f"valid_from: {node.temporal.valid_from.isoformat()})"
                )
                all_fragments.append(
                    _make_fragment(
                        uko_node=node.node_uri,
                        content=content,
                        relevance_score=self.capabilities.quality_score * 0.8,
                        strategy_name=self.name,
                        resource_uri=node.source_resource,
                        location=node.source_path,
                        metadata={
                            "plan_id": "",
                            "is_current": node.temporal.is_current,
                        },
                    )
                )
        else:
            # Query for nodes associated with each ancestor plan
            for plan_id in plan_ids_to_query:
                # Use plan_id as a URI base to find associated nodes
                current_node = backends.temporal.get_current(plan_id)
                if current_node is not None:
                    content = (
                        f"Plan decision: {current_node.node_uri} "
                        f"(plan: {plan_id}, "
                        f"source: {current_node.source_path}, "
                        f"valid_from: {current_node.temporal.valid_from.isoformat()})"
                    )
                    # Parent plan decisions get higher score than ancestors
                    is_parent = plan_id == plan_context.parent_plan_id
                    score = (
                        self.capabilities.quality_score
                        if is_parent
                        else self.capabilities.quality_score * 0.8
                    )
                    all_fragments.append(
                        _make_fragment(
                            uko_node=current_node.node_uri,
                            content=content,
                            relevance_score=score,
                            strategy_name=self.name,
                            resource_uri=current_node.source_resource,
                            location=current_node.source_path,
                            metadata={
                                "plan_id": plan_id,
                                "is_parent": is_parent,
                                "is_current": True,
                            },
                        )
                    )

                # Also get history for this plan
                history = backends.temporal.get_history(
                    plan_id,
                    temporal_scope=TemporalScope.RECENT,
                )
                for node in history:
                    if not node.temporal.is_current:
                        valid_until_str = (
                            node.temporal.valid_until.isoformat()
                            if node.temporal.valid_until
                            else "N/A"
                        )
                        content = (
                            f"Historical decision: {node.node_uri} "
                            f"(plan: {plan_id}, "
                            f"valid_from: {node.temporal.valid_from.isoformat()}, "
                            f"valid_until: {valid_until_str})"
                        )
                        all_fragments.append(
                            _make_fragment(
                                uko_node=node.node_uri,
                                content=content,
                                relevance_score=self.capabilities.quality_score * 0.6,
                                strategy_name=self.name,
                                resource_uri=node.source_resource,
                                location=node.source_path,
                                metadata={
                                    "plan_id": plan_id,
                                    "is_current": False,
                                    "is_revision_of": node.temporal.is_revision_of
                                    or "",
                                },
                            )
                        )

        all_fragments.sort(key=lambda f: f.relevance_score, reverse=True)
        return _budget_fragments(all_fragments, budget)

    def explain(self) -> str:
        return (
            "Retrieves context from parent and ancestor plan "
            "decisions. This is how child plans 'remember' what "
            "their parent decided and why."
        )


# ---------------------------------------------------------------------------
# Registry helper — register all built-in strategies
# ---------------------------------------------------------------------------

# Canonical ordered list per spec §28682 default:
# ["simple-keyword", "semantic-embedding", "breadth-depth-navigator"]
# Plus the remaining 3 registered but not enabled by default.
#
# NOTE: Spec §25223 lists 4 strategies (including "arce") in the default
# enabled config, but §28682 lists only 3 (without "arce").  This is a
# spec internal contradiction.  We follow §28682 (the config key table)
# because it is the more specific/operational reference.  This decision
# should be confirmed with the spec author.

BUILTIN_STRATEGY_CLASSES: tuple[type, ...] = (
    SimpleKeywordStrategy,
    SemanticEmbeddingStrategy,
    BreadthDepthNavigatorStrategy,
    ARCEStrategy,
    TemporalArchaeologyStrategy,
    PlanDecisionContextStrategy,
)

# Spec §28682 default enabled list
DEFAULT_ENABLED_STRATEGIES: tuple[str, ...] = (
    "simple-keyword",
    "semantic-embedding",
    "breadth-depth-navigator",
)
