"""ACMS pipeline Phase 3 — Context Finalization component implementations.

Provides the two production-grade Phase 3 components for the ACMS context
assembly pipeline, replacing the no-op defaults from ``acms_service.py``:

8. **RelevanceCoherenceOrderer** — Orders budget-selected fragments for
   coherent presentation, balancing relevance ranking with narrative
   coherence.  Groups related fragments (same UKO node prefix) together
   and maintains narrative flow within groups.

9. **ProvenancePreambleGenerator** — Generates a provenance preamble
   summarizing context assembly: which strategies contributed, confidence
   scores, coverage gaps, and fragment statistics.

All components satisfy the v1 Protocol signatures defined in
``acms_service.py`` and can be injected into ``ACMSPipeline`` via
constructor dependency injection.

Based on ``docs/specification.md`` §42648-42653.

ISSUES CLOSED: #545
"""

from __future__ import annotations

import logging
from collections import defaultdict
from collections.abc import Sequence
from typing import Final

from cleveragents.domain.models.core.context_fragment import ContextFragment

logger = logging.getLogger(__name__)

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

# Weight for relevance vs. coherence in the ordering algorithm.
# Higher values favour relevance ranking; lower values favour grouping.
DEFAULT_RELEVANCE_WEIGHT: Final[float] = 0.6
DEFAULT_COHERENCE_WEIGHT: Final[float] = 0.4

# Minimum number of common URI path segments to consider two fragments
# "related" for grouping purposes.
DEFAULT_MIN_SHARED_SEGMENTS: Final[int] = 2


# ---------------------------------------------------------------------------
# 8. RelevanceCoherenceOrderer  (FragmentOrderer protocol)
# ---------------------------------------------------------------------------


class RelevanceCoherenceOrderer:
    """Order fragments by relevance while maintaining narrative coherence.

    The ordering algorithm operates in two phases:

    1. **Grouping phase**: Fragments are grouped by UKO node prefix
       (shared URI path segments).  Related fragments that share a common
       ancestor in the knowledge hierarchy are placed together.

    2. **Ranking phase**: Groups are sorted by the maximum relevance score
       of their members (descending).  Within each group, fragments are
       sorted by relevance score descending then by detail depth ascending,
       so that higher-level overviews precede detailed content.

    This produces a narrative-coherent ordering where related content
    is clustered together, but high-relevance content still appears first.

    Implements ``FragmentOrderer`` protocol from ``acms_service.py``.

    Based on ``docs/specification.md`` §42648 — ``FragmentOrdererProtocol``
    and ``RelevanceCoherenceOrderer``.
    """

    def __init__(
        self,
        *,
        relevance_weight: float = DEFAULT_RELEVANCE_WEIGHT,
        coherence_weight: float = DEFAULT_COHERENCE_WEIGHT,
        min_shared_segments: int = DEFAULT_MIN_SHARED_SEGMENTS,
    ) -> None:
        self._relevance_weight = relevance_weight
        self._coherence_weight = coherence_weight
        self._min_shared_segments = min_shared_segments

    @property
    def relevance_weight(self) -> float:
        """Return the configured relevance weight."""
        return self._relevance_weight

    @property
    def coherence_weight(self) -> float:
        """Return the configured coherence weight."""
        return self._coherence_weight

    def order(
        self,
        fragments: Sequence[ContextFragment],
    ) -> Sequence[ContextFragment]:
        """Order fragments by relevance while maintaining narrative coherence.

        Returns fragments grouped by related UKO node prefix, with groups
        ordered by maximum relevance and fragments within groups ordered
        by relevance descending then detail depth ascending.
        """
        if not fragments or len(fragments) <= 1:
            return list(fragments)

        # Phase 1: Group fragments by UKO node prefix
        groups = self._group_by_prefix(fragments)

        # Phase 2: Sort groups by max relevance (descending)
        sorted_groups = sorted(
            groups.values(),
            key=lambda g: max(f.relevance_score for f in g),
            reverse=True,
        )

        # Phase 3: Sort within each group by relevance desc, depth asc
        ordered: list[ContextFragment] = []
        for group in sorted_groups:
            sorted_group = sorted(
                group,
                key=lambda f: (-f.relevance_score, f.detail_depth),
            )
            ordered.extend(sorted_group)

        logger.info(
            "Ordered fragments for coherent presentation",
            extra={
                "input_count": len(fragments),
                "group_count": len(sorted_groups),
                "output_count": len(ordered),
            },
        )
        return ordered

    def _group_by_prefix(
        self,
        fragments: Sequence[ContextFragment],
    ) -> dict[str, list[ContextFragment]]:
        """Group fragments by shared UKO node prefix.

        Fragments with at least ``min_shared_segments`` common URI path
        segments are placed in the same group.  Fragments that don't
        match any group are placed in a singleton group.
        """
        groups: dict[str, list[ContextFragment]] = defaultdict(list)

        for frag in fragments:
            prefix = self._extract_prefix(frag.uko_node)
            groups[prefix].append(frag)

        return dict(groups)

    def _extract_prefix(self, uko_node: str) -> str:
        """Extract the grouping prefix from a UKO node URI.

        Takes the first ``min_shared_segments`` path segments as the
        prefix.  If the URI has fewer segments, the full URI is used.
        """
        segments = _uri_segments(uko_node)
        prefix_segments = segments[: self._min_shared_segments]
        return "/".join(prefix_segments) if prefix_segments else uko_node


# ---------------------------------------------------------------------------
# 9. ProvenancePreambleGenerator  (PreambleGenerator protocol)
# ---------------------------------------------------------------------------


class ProvenancePreambleGenerator:
    """Generate a provenance preamble summarizing context assembly.

    Produces a structured text preamble that includes:

    - **Strategy contributions**: Which strategies produced fragments and
      what percentage of the total context each contributed.
    - **Confidence indicators**: Average and range of relevance scores
      across all fragments.
    - **Coverage summary**: UKO node coverage, tier distribution, and
      depth distribution.
    - **Coverage gaps**: Identification of missing tiers or low-coverage
      areas.

    The preamble is formatted as a concise, machine-readable text block
    suitable for prepending to the assembled context.

    Implements ``PreambleGenerator`` protocol from ``acms_service.py``.

    Based on ``docs/specification.md`` §42653 — ``PreambleGeneratorProtocol``
    and ``ProvenancePreambleGenerator``.
    """

    def generate(
        self,
        fragments: Sequence[ContextFragment],
    ) -> str | None:
        """Generate a structured provenance preamble.

        Returns ``None`` if no fragments are provided.
        """
        if not fragments:
            return None

        total_tokens = sum(f.token_count for f in fragments)
        total_fragments = len(fragments)

        # Strategy contributions
        strategy_counts: dict[str, int] = defaultdict(int)
        strategy_tokens: dict[str, int] = defaultdict(int)
        for frag in fragments:
            source = frag.strategy_source or frag.provenance.resource_type or "unknown"
            strategy_counts[source] += 1
            strategy_tokens[source] += frag.token_count

        strategy_lines: list[str] = []
        for source in sorted(strategy_counts):
            count = strategy_counts[source]
            tokens = strategy_tokens[source]
            pct = (tokens / total_tokens * 100) if total_tokens > 0 else 0.0
            strategy_lines.append(
                f"  - {source}: {count} fragments, {tokens} tokens ({pct:.1f}%)"
            )

        # Confidence indicators
        scores = [f.relevance_score for f in fragments]
        avg_score = sum(scores) / len(scores)
        min_score = min(scores)
        max_score = max(scores)

        # Tier distribution
        tier_counts: dict[str, int] = defaultdict(int)
        for frag in fragments:
            tier_counts[frag.tier] += 1

        tier_lines: list[str] = []
        for tier in sorted(tier_counts):
            count = tier_counts[tier]
            tier_lines.append(f"  - {tier}: {count} fragments")

        # Depth distribution
        depth_counts: dict[int, int] = defaultdict(int)
        for frag in fragments:
            depth_counts[frag.detail_depth] += 1

        depth_lines: list[str] = []
        for depth in sorted(depth_counts):
            count = depth_counts[depth]
            depth_lines.append(f"  - depth {depth}: {count} fragments")

        # UKO node coverage
        unique_nodes = len({f.uko_node for f in fragments})

        # Coverage gaps
        gaps: list[str] = []
        all_tiers = {"hot", "warm", "cold"}
        missing_tiers = all_tiers - set(tier_counts)
        if missing_tiers:
            gaps.append(f"  - Missing tiers: {', '.join(sorted(missing_tiers))}")
        if avg_score < 0.3:
            gaps.append(f"  - Low average confidence: {avg_score:.3f}")
        if total_fragments < 3:
            gaps.append(f"  - Low fragment count: {total_fragments}")

        # Build preamble
        lines = [
            "--- Context Assembly Provenance ---",
            f"Fragments: {total_fragments} | Tokens: {total_tokens} "
            f"| UKO Nodes: {unique_nodes}",
            "",
            "Strategy Contributions:",
            *strategy_lines,
            "",
            f"Confidence: avg={avg_score:.3f} min={min_score:.3f} max={max_score:.3f}",
            "",
            "Tier Distribution:",
            *tier_lines,
            "",
            "Depth Distribution:",
            *depth_lines,
        ]

        if gaps:
            lines.extend(["", "Coverage Gaps:", *gaps])

        lines.append("--- End Provenance ---")

        preamble = "\n".join(lines)

        logger.info(
            "Generated provenance preamble",
            extra={
                "fragment_count": total_fragments,
                "total_tokens": total_tokens,
                "strategy_count": len(strategy_counts),
                "unique_nodes": unique_nodes,
                "gap_count": len(gaps),
            },
        )
        return preamble


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


def _uri_segments(uri: str) -> list[str]:
    """Split a UKO URI into path segments for hierarchy comparison."""
    if "://" in uri:
        uri = uri.split("://", 1)[1]
    return [seg for seg in uri.replace("\\", "/").split("/") if seg]
