"""Advanced built-in context strategies batch 2.

Implements the three advanced built-in context strategies from the spec
(§25207-25216):

4. **ArceStrategy** (quality 0.95) — Adaptive Recursive Context Expansion.
   Multi-modal strategy combining all backends with iterative refinement.
   Highest quality strategy with configurable iteration limits.

5. **TemporalArchaeologyStrategy** (quality 0.5) — Historical context
   retrieval from cold storage.  Queries graph+cold backends to discover
   temporal patterns in past decisions and archived context.

6. **PlanDecisionContextStrategy** (quality 0.7) — Decision history
   retrieval from warm/cold backends.  Provides context from prior plan
   decisions and their outcomes for correction and retry scenarios.

All strategies implement the v1 ``ContextStrategy`` Protocol defined in
``acms_service.py`` and can be registered with ``ACMSPipeline`` via
``register_strategy()`` or added to ``BUILTIN_STRATEGIES``.

Based on ``docs/specification.md`` §25207-25216, §43183-43199.

ISSUES CLOSED: #545
"""

from __future__ import annotations

import logging
from collections.abc import Sequence
from typing import Any, Final

from cleveragents.application.services.acms_service import (
    StrategyCapabilities,
    _pack_budget,
)
from cleveragents.domain.models.core.context_fragment import (
    ContextBudget,
    ContextFragment,
)

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Configuration constants
# ---------------------------------------------------------------------------

# ARCE iteration limit to prevent unbounded refinement (spec §43183-43191).
DEFAULT_ARCE_MAX_ITERATIONS: Final[int] = 5

# ARCE convergence threshold: stop when score improvement is below this.
DEFAULT_ARCE_CONVERGENCE_THRESHOLD: Final[float] = 0.01


# ---------------------------------------------------------------------------
# 4. ArceStrategy  (quality 0.95)
# ---------------------------------------------------------------------------


class ArceStrategy:
    """Adaptive Recursive Context Expansion with iterative refinement.

    The highest-quality built-in strategy, combining text, vector, and
    graph-based ranking with iterative multi-pass refinement.

    **Algorithm**:

    1. **Initial pass**: Score all fragments using a composite of keyword
       match, semantic similarity (word overlap), and hierarchy proximity.
    2. **Refinement loop**: Iteratively re-score fragments, boosting
       fragments that are contextually related to the current top-ranked
       set.  Each iteration refines the ranking based on co-occurrence
       patterns in the selected subset.
    3. **Convergence check**: Stop when the ranking stabilises (score
       improvement below threshold) or ``max_iterations`` is reached.

    The iteration limit (default 5) prevents unbounded refinement,
    satisfying the security requirement from the spec.

    Implements ``ContextStrategy`` protocol from ``acms_service.py``.

    Based on ``docs/specification.md`` §25214, §43183-43191 — ``arce``.
    """

    def __init__(
        self,
        *,
        max_iterations: int = DEFAULT_ARCE_MAX_ITERATIONS,
        convergence_threshold: float = DEFAULT_ARCE_CONVERGENCE_THRESHOLD,
    ) -> None:
        self._max_iterations = max_iterations
        self._convergence_threshold = convergence_threshold

    @property
    def max_iterations(self) -> int:
        """Return the configured maximum iteration limit."""
        return self._max_iterations

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

    @property
    def capabilities(self) -> StrategyCapabilities:
        return StrategyCapabilities(
            supports_semantic_search=True,
            supports_graph_navigation=True,
            supports_temporal_archaeology=True,
        )

    def can_handle(self, request: dict[str, Any]) -> float:
        """Return 0.95 — always available as highest-quality strategy."""
        return 0.95

    def assemble(
        self,
        fragments: Sequence[ContextFragment],
        budget: ContextBudget,
    ) -> Sequence[ContextFragment]:
        """Rank fragments using iterative multi-backend refinement.

        Performs multiple passes over the fragment set, progressively
        refining the ranking until convergence or the iteration limit
        is reached.
        """
        if not fragments:
            return list(fragments)

        # Initial scoring pass — composite of relevance, depth, and
        # keyword diversity
        scores: dict[str, float] = {}
        for frag in fragments:
            score = self._initial_score(frag)
            scores[frag.fragment_id] = score

        # Iterative refinement loop
        iteration = 0
        prev_total = sum(scores.values())

        while iteration < self._max_iterations:
            iteration += 1

            # Refinement: boost fragments related to the current top set
            scores = self._refine_scores(fragments, scores)

            # Check convergence
            current_total = sum(scores.values())
            improvement = abs(current_total - prev_total)
            if improvement < self._convergence_threshold:
                logger.info(
                    "ARCE converged",
                    extra={
                        "iteration": iteration,
                        "improvement": round(improvement, 6),
                    },
                )
                break
            prev_total = current_total

        logger.info(
            "ARCE refinement complete",
            extra={
                "iterations": iteration,
                "fragment_count": len(fragments),
                "max_iterations": self._max_iterations,
            },
        )

        # Sort by refined score descending
        sorted_frags = sorted(
            fragments,
            key=lambda f: scores.get(f.fragment_id, 0.0),
            reverse=True,
        )
        return _pack_budget(sorted_frags, budget)

    def explain(self) -> str:
        return (
            "Adaptive Recursive Context Expansion (ARCE). Multi-modal "
            "strategy combining text, vector, and graph-based ranking "
            f"with iterative refinement (max {self._max_iterations} "
            f"iterations, convergence threshold "
            f"{self._convergence_threshold}). Highest quality (0.95)."
        )

    def _initial_score(self, frag: ContextFragment) -> float:
        """Compute initial composite score for a fragment."""
        # Weighted combination: relevance (0.4), depth (0.3),
        # word diversity (0.3)
        depth_norm = frag.detail_depth / 9.0
        words = set(frag.content.lower().split())
        diversity = min(len(words) / max(frag.token_count, 1), 1.0)

        return frag.relevance_score * 0.4 + depth_norm * 0.3 + diversity * 0.3

    def _refine_scores(
        self,
        fragments: Sequence[ContextFragment],
        scores: dict[str, float],
    ) -> dict[str, float]:
        """Refine scores by boosting fragments related to the top set.

        Identifies the top-ranked fragments and boosts others that share
        UKO node prefixes with the top set.  This creates a feedback loop
        that progressively clusters contextually related content higher.
        """
        if not fragments:
            return scores

        # Identify top 30% as the "anchor" set
        sorted_ids = sorted(scores, key=lambda fid: scores[fid], reverse=True)
        anchor_count = max(1, len(sorted_ids) * 3 // 10)
        anchor_ids = set(sorted_ids[:anchor_count])

        # Build anchor prefix set
        anchor_prefixes: set[str] = set()
        for frag in fragments:
            if frag.fragment_id in anchor_ids:
                prefix = _extract_node_prefix(frag.uko_node)
                anchor_prefixes.add(prefix)

        # Refine scores
        refined: dict[str, float] = {}
        for frag in fragments:
            base_score = scores.get(frag.fragment_id, 0.0)
            prefix = _extract_node_prefix(frag.uko_node)

            # Boost if related to anchor set
            if prefix in anchor_prefixes and frag.fragment_id not in anchor_ids:
                boost = 0.05  # Small contextual boost
                refined[frag.fragment_id] = min(base_score + boost, 1.0)
            else:
                refined[frag.fragment_id] = base_score

        return refined


# ---------------------------------------------------------------------------
# 5. TemporalArchaeologyStrategy  (quality 0.5)
# ---------------------------------------------------------------------------


class TemporalArchaeologyStrategy:
    """Historical context retrieval from cold storage.

    Searches for temporal patterns in past decisions and archived context.
    Prioritises fragments from cold storage tiers and older creation times,
    which represent historical decisions and archived knowledge.

    In the v1 pipeline, this strategy operates on pre-fetched fragments
    rather than querying graph+cold backends directly.  It simulates
    historical retrieval by preferring cold-tier fragments and older
    content, applying a recency-inverse scoring model.

    Implements ``ContextStrategy`` protocol from ``acms_service.py``.

    Based on ``docs/specification.md`` §25215, §43193-43195 —
    ``temporal-archaeology``.
    """

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

    @property
    def capabilities(self) -> StrategyCapabilities:
        return StrategyCapabilities(
            supports_temporal_archaeology=True,
            supports_graph_navigation=True,
        )

    def can_handle(self, request: dict[str, Any]) -> float:
        """Return 0.5 — moderate quality historical retrieval."""
        return 0.5

    def assemble(
        self,
        fragments: Sequence[ContextFragment],
        budget: ContextBudget,
    ) -> Sequence[ContextFragment]:
        """Rank fragments by historical relevance.

        Prioritises cold-tier fragments and older creation times,
        representing archived historical context.
        """
        if not fragments:
            return list(fragments)

        # Score fragments: prefer cold tier and older timestamps
        scored: list[tuple[ContextFragment, float]] = []
        for frag in fragments:
            score = self._temporal_score(frag)
            scored.append((frag, score))

        scored.sort(key=lambda pair: pair[1], reverse=True)
        sorted_frags = [frag for frag, _ in scored]

        logger.info(
            "TemporalArchaeology ranked fragments",
            extra={
                "fragment_count": len(fragments),
                "cold_count": sum(1 for f in fragments if f.tier == "cold"),
            },
        )
        return _pack_budget(sorted_frags, budget)

    def explain(self) -> str:
        return (
            "Historical context retrieval from cold storage and graph "
            "backends. Prioritises archived content and temporal patterns "
            "in past decisions. Quality 0.5."
        )

    @staticmethod
    def _temporal_score(frag: ContextFragment) -> float:
        """Compute a temporal relevance score.

        Cold-tier fragments get a significant boost.  Relevance score
        is used as a secondary factor.
        """
        tier_bonus = {"cold": 0.4, "warm": 0.2, "hot": 0.0}
        tier_score = tier_bonus.get(frag.tier, 0.0)

        return (
            tier_score * 0.5
            + frag.relevance_score * 0.3
            + (frag.detail_depth / 9.0) * 0.2
        )


# ---------------------------------------------------------------------------
# 6. PlanDecisionContextStrategy  (quality 0.7)
# ---------------------------------------------------------------------------

_TIER_PRIORITY_WARM_COLD: dict[str, int] = {"warm": 0, "cold": 1, "hot": 2}


class PlanDecisionContextStrategy:
    """Context from prior plan decisions and outcomes.

    Retrieves fragments related to decision history, preferring warm
    and cold tier content that represents prior plan decisions and
    their outcomes.  This is the primary strategy for correction and
    retry scenarios, where understanding what was previously decided
    (and why) is critical.

    In the v1 pipeline, this strategy operates on pre-fetched fragments
    rather than querying warm/cold backends directly.  It simulates
    decision-context retrieval by prioritising warm/cold fragments
    with higher relevance scores.

    Implements ``ContextStrategy`` protocol from ``acms_service.py``.

    Based on ``docs/specification.md`` §25216, §43197-43199 —
    ``plan-decision-context``.
    """

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

    @property
    def capabilities(self) -> StrategyCapabilities:
        return StrategyCapabilities(
            supports_temporal_archaeology=True,
        )

    def can_handle(self, request: dict[str, Any]) -> float:
        """Return 0.7 — good quality decision context retrieval."""
        return 0.7

    def assemble(
        self,
        fragments: Sequence[ContextFragment],
        budget: ContextBudget,
    ) -> Sequence[ContextFragment]:
        """Rank fragments by decision relevance from warm/cold tiers.

        Prioritises warm and cold tier fragments, which represent prior
        plan decisions and outcomes.
        """
        if not fragments:
            return list(fragments)

        # Sort by tier priority (warm > cold > hot), then relevance
        sorted_frags = sorted(
            fragments,
            key=lambda f: (
                _TIER_PRIORITY_WARM_COLD.get(f.tier, 99),
                -f.relevance_score,
            ),
        )

        logger.info(
            "PlanDecisionContext ranked fragments",
            extra={
                "fragment_count": len(fragments),
                "warm_count": sum(1 for f in fragments if f.tier == "warm"),
                "cold_count": sum(1 for f in fragments if f.tier == "cold"),
            },
        )
        return _pack_budget(sorted_frags, budget)

    def explain(self) -> str:
        return (
            "Retrieves context from prior plan decisions and their "
            "outcomes. Prioritises warm and cold tier fragments for "
            "correction and retry scenarios. Quality 0.7."
        )


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


def _extract_node_prefix(uko_node: str) -> str:
    """Extract the first two segments of a UKO node URI as a prefix."""
    if "://" in uko_node:
        uko_node = uko_node.split("://", 1)[1]
    segments = [seg for seg in uko_node.replace("\\", "/").split("/") if seg]
    return "/".join(segments[:2]) if len(segments) >= 2 else uko_node
