"""Context Analysis Engine for ACMS index metrics.

Provides actionable insight into the current ACMS state:

| Metric              | Description                                                  |
|---------------------|--------------------------------------------------------------|
| ``entry_count``     | Total entries across all tiers                               |
| ``tier_distribution``| Count and size per tier (hot/warm/cold)                     |
| ``budget_utilization``| Current total size vs. configured max, as %               |
| ``top_files``       | Top-N entries by access frequency (configurable N)           |

The engine is wired to the ``ContextTierService`` and exposes both
human-readable (text) and machine-readable (JSON) output via the
``format_text`` and ``format_json`` helpers.

Based on issue #9984 -- feat(acms): implement context analysis engine.
"""

from __future__ import annotations

import json
from typing import Any

from cleveragents.application.services.context_tiers import ContextTierService
from cleveragents.domain.models.acms.tiers import ContextTier, TieredFragment

# ---------------------------------------------------------------------------
# Result models (plain dataclasses -- no Pydantic to keep this lightweight)
# ---------------------------------------------------------------------------


class TierStats:
    """Count and total size for a single tier."""

    def __init__(self, count: int, size_bytes: int) -> None:
        self.count = count
        self.size_bytes = size_bytes

    def to_dict(self) -> dict[str, int]:
        return {"count": self.count, "size_bytes": self.size_bytes}


class TierDistribution:
    """Distribution of entries across hot/warm/cold tiers."""

    def __init__(
        self,
        hot: TierStats,
        warm: TierStats,
        cold: TierStats,
    ) -> None:
        self.hot = hot
        self.warm = warm
        self.cold = cold

    def to_dict(self) -> dict[str, dict[str, int]]:
        return {
            "hot": self.hot.to_dict(),
            "warm": self.warm.to_dict(),
            "cold": self.cold.to_dict(),
        }


class BudgetUtilization:
    """Budget utilization metrics."""

    def __init__(
        self,
        current_bytes: int,
        max_bytes: int,
        utilization_pct: float,
    ) -> None:
        self.current_bytes = current_bytes
        self.max_bytes = max_bytes
        self.utilization_pct = utilization_pct

    def to_dict(self) -> dict[str, Any]:
        return {
            "current_bytes": self.current_bytes,
            "max_bytes": self.max_bytes,
            "utilization_pct": round(self.utilization_pct, 2),
        }


class TopFileEntry:
    """A single entry in the top-files list."""

    def __init__(
        self,
        fragment_id: str,
        resource_id: str,
        access_count: int,
        tier: str,
    ) -> None:
        self.fragment_id = fragment_id
        self.resource_id = resource_id
        self.access_count = access_count
        self.tier = tier

    def to_dict(self) -> dict[str, Any]:
        return {
            "fragment_id": self.fragment_id,
            "resource_id": self.resource_id,
            "access_count": self.access_count,
            "tier": self.tier,
        }


class AnalysisResult:
    """Full analysis result from the ContextAnalysisEngine."""

    def __init__(
        self,
        entry_count: int,
        tier_distribution: TierDistribution,
        budget_utilization: BudgetUtilization,
        top_files: list[TopFileEntry],
    ) -> None:
        self.entry_count = entry_count
        self.tier_distribution = tier_distribution
        self.budget_utilization = budget_utilization
        self.top_files = top_files

    def to_dict(self) -> dict[str, Any]:
        return {
            "entry_count": self.entry_count,
            "tier_distribution": self.tier_distribution.to_dict(),
            "budget_utilization": self.budget_utilization.to_dict(),
            "top_files": [f.to_dict() for f in self.top_files],
        }


# ---------------------------------------------------------------------------
# Engine
# ---------------------------------------------------------------------------


class ContextAnalysisEngine:
    """Query the ACMS index and produce analysis metrics.

    Args:
        tier_service: The ``ContextTierService`` to query.  When ``None``
            a fresh in-memory service is created (useful for testing).
        max_total_size: The configured maximum total size in bytes used
            for budget utilisation calculation.  Defaults to the hot-tier
            token budget from the service's ``TierBudget`` (treated as
            bytes for simplicity when no explicit override is given).
    """

    def __init__(
        self,
        tier_service: ContextTierService | None = None,
        max_total_size: int | None = None,
    ) -> None:
        self._tier_service: ContextTierService = (
            tier_service if tier_service is not None else ContextTierService()
        )
        # Use explicit override or fall back to hot-tier token budget as proxy.
        if max_total_size is not None:
            self._max_total_size = max_total_size
        else:
            self._max_total_size = self._tier_service.budget.max_tokens_hot

    # ------------------------------------------------------------------
    # Individual metrics
    # ------------------------------------------------------------------

    def entry_count(self) -> int:
        """Return the total number of entries across all tiers."""
        metrics = self._tier_service.get_metrics()
        return metrics.total_fragments

    def tier_distribution(self) -> TierDistribution:
        """Return count and total content size per tier.

        Size is measured in bytes (``len(fragment.content.encode())``).
        """
        all_frags = self._tier_service.get_all_fragments()

        hot_count = 0
        hot_size = 0
        warm_count = 0
        warm_size = 0
        cold_count = 0
        cold_size = 0

        for frag in all_frags:
            size = len(frag.content.encode())
            if frag.tier == ContextTier.HOT:
                hot_count += 1
                hot_size += size
            elif frag.tier == ContextTier.WARM:
                warm_count += 1
                warm_size += size
            else:
                cold_count += 1
                cold_size += size

        return TierDistribution(
            hot=TierStats(count=hot_count, size_bytes=hot_size),
            warm=TierStats(count=warm_count, size_bytes=warm_size),
            cold=TierStats(count=cold_count, size_bytes=cold_size),
        )

    def budget_utilization(self) -> BudgetUtilization:
        """Return budget utilisation metrics.

        ``current_bytes`` is the sum of encoded content sizes across all
        tiers.  ``max_bytes`` is the configured ``max_total_size``.
        ``utilization_pct`` is ``current_bytes / max_bytes * 100``,
        capped at 100.0 when over budget.
        """
        all_frags = self._tier_service.get_all_fragments()
        current_bytes = sum(len(f.content.encode()) for f in all_frags)
        max_bytes = self._max_total_size

        pct = min(current_bytes / max_bytes * 100.0, 100.0) if max_bytes > 0 else 0.0

        return BudgetUtilization(
            current_bytes=current_bytes,
            max_bytes=max_bytes,
            utilization_pct=pct,
        )

    def top_files(self, n: int = 10) -> list[TopFileEntry]:
        """Return the top-N entries by ``access_count`` descending.

        Args:
            n: Number of entries to return (default 10).

        Raises:
            ValueError: If *n* is not positive.
        """
        if n < 1:
            raise ValueError(f"n must be positive, got {n}")

        all_frags: list[TieredFragment] = self._tier_service.get_all_fragments()
        sorted_frags = sorted(
            all_frags,
            key=lambda f: f.access_count,
            reverse=True,
        )
        return [
            TopFileEntry(
                fragment_id=frag.fragment_id,
                resource_id=frag.resource_id,
                access_count=frag.access_count,
                tier=frag.tier.value,
            )
            for frag in sorted_frags[:n]
        ]

    # ------------------------------------------------------------------
    # Full analysis
    # ------------------------------------------------------------------

    def analyze(self, top_n: int = 10) -> AnalysisResult:
        """Run all metrics and return a combined ``AnalysisResult``.

        Args:
            top_n: Number of top files to include (default 10).
        """
        return AnalysisResult(
            entry_count=self.entry_count(),
            tier_distribution=self.tier_distribution(),
            budget_utilization=self.budget_utilization(),
            top_files=self.top_files(n=top_n),
        )

    # ------------------------------------------------------------------
    # Formatters
    # ------------------------------------------------------------------

    @staticmethod
    def format_json(result: AnalysisResult) -> str:
        """Return a JSON string representation of *result*."""
        return json.dumps(result.to_dict(), indent=2)

    @staticmethod
    def format_text(result: AnalysisResult) -> str:
        """Return a human-readable text representation of *result*."""
        lines: list[str] = []
        lines.append("=== ACMS Context Analysis ===")
        lines.append(f"Total entries: {result.entry_count}")
        lines.append("")

        dist = result.tier_distribution
        lines.append("Tier Distribution:")
        lines.append(
            f"  hot:  {dist.hot.count:>6} entries, {dist.hot.size_bytes:>10} bytes"
        )
        lines.append(
            f"  warm: {dist.warm.count:>6} entries, {dist.warm.size_bytes:>10} bytes"
        )
        lines.append(
            f"  cold: {dist.cold.count:>6} entries, {dist.cold.size_bytes:>10} bytes"
        )
        lines.append("")

        util = result.budget_utilization
        lines.append("Budget Utilization:")
        lines.append(f"  current: {util.current_bytes:>10} bytes")
        lines.append(f"  max:     {util.max_bytes:>10} bytes")
        lines.append(f"  used:    {util.utilization_pct:>9.2f}%")
        lines.append("")

        lines.append(f"Top {len(result.top_files)} Files by Access Frequency:")
        if result.top_files:
            for i, entry in enumerate(result.top_files, start=1):
                resource = entry.resource_id or entry.fragment_id
                lines.append(
                    f"  {i:>3}. [{entry.tier:>4}] {resource}"
                    f" (access_count={entry.access_count})"
                )
        else:
            lines.append("  (no entries)")

        return "\n".join(lines)


__all__ = [
    "AnalysisResult",
    "BudgetUtilization",
    "ContextAnalysisEngine",
    "TierDistribution",
    "TierStats",
    "TopFileEntry",
]
