"""Phase-wise context analysis summaries for ACMS project context views.

Builds per-phase summaries (strategize/execute/apply) from tiered fragments
using the project's resolved ``ProjectContextPolicy``. The summaries are
designed to be human-readable and machine-assertable for CLI output and tests.
"""

from __future__ import annotations

import fnmatch
from pathlib import PurePosixPath
from typing import Any

from cleveragents.domain.models.acms.tiers import TieredFragment
from cleveragents.domain.models.core.context_policy import ProjectContextPolicy


def _resource_matches(resource_id: str, include_patterns: list[str]) -> bool:
    """Return ``True`` when the resource is included by patterns."""
    if not include_patterns:
        return True
    return any(fnmatch.fnmatch(resource_id, pattern) for pattern in include_patterns)


def _path_matches(path: str, include_patterns: list[str]) -> bool:
    """Return ``True`` when the path is included by patterns."""
    if not include_patterns:
        return True
    path_obj = PurePosixPath(path)
    return any(_matches_pattern(path_obj, pattern) for pattern in include_patterns)


def _matches_pattern(path_obj: PurePosixPath, pattern: str) -> bool:
    """Match with a small compatibility shim for ``**/`` zero-depth cases."""
    return path_obj.match(pattern) or (
        "**/" in pattern and path_obj.match(pattern.replace("**/", ""))
    )


def _extract_path(fragment: TieredFragment) -> str:
    """Extract a best-effort source path for include/exclude path filtering."""
    raw_path = fragment.metadata.get("path")
    if isinstance(raw_path, str) and raw_path:
        return raw_path
    return fragment.resource_id or ""


def _fragment_size_bytes(fragment: TieredFragment) -> int:
    """Resolve fragment size in bytes with metadata override when present."""
    raw_size = fragment.metadata.get("byte_size")
    if isinstance(raw_size, int) and raw_size >= 0:
        return raw_size
    return len(fragment.content.encode("utf-8"))


def analyze_phase_summaries(
    policy: ProjectContextPolicy,
    fragments: list[TieredFragment],
    budget_tokens: int,
) -> dict[str, Any]:
    """Build per-phase summaries and narrowing diagnostics.

    Returns a dictionary with:
    - ``phases``: per-phase summary rows
    - ``narrowing``: monotonic checks from strategize->execute->apply
    """

    phase_names = ["strategize", "execute", "apply"]
    rows: dict[str, dict[str, Any]] = {}

    for phase in phase_names:
        view = policy.resolve_view(phase)
        selected: list[TieredFragment] = []

        excluded_by_resource = 0
        excluded_by_path = 0
        excluded_by_file_size = 0
        excluded_by_total_size = 0

        total_bytes = 0
        total_tokens = 0

        for fragment in fragments:
            resource_id = fragment.resource_id or ""
            path = _extract_path(fragment)

            if not _resource_matches(resource_id, view.include_resources):
                excluded_by_resource += 1
                continue
            if any(fnmatch.fnmatch(resource_id, p) for p in view.exclude_resources):
                excluded_by_resource += 1
                continue

            if not _path_matches(path, view.include_paths):
                excluded_by_path += 1
                continue
            path_obj = PurePosixPath(path)
            if any(_matches_pattern(path_obj, p) for p in view.exclude_paths):
                excluded_by_path += 1
                continue

            file_size = _fragment_size_bytes(fragment)
            if view.max_file_size is not None and file_size > view.max_file_size:
                excluded_by_file_size += 1
                continue

            if (
                view.max_total_size is not None
                and total_bytes + file_size > view.max_total_size
            ):
                excluded_by_total_size += 1
                continue

            selected.append(fragment)
            total_bytes += file_size
            total_tokens += fragment.token_count

        distinct_resources = len({f.resource_id for f in selected if f.resource_id})
        budget_used = (total_tokens / budget_tokens) if budget_tokens > 0 else 0.0

        rows[phase] = {
            "phase": phase,
            "fragment_count": len(selected),
            "resource_count": distinct_resources,
            "total_bytes": total_bytes,
            "total_tokens": total_tokens,
            "budget_tokens": budget_tokens,
            "budget_used_ratio": min(max(budget_used, 0.0), 1.0),
            "excluded": {
                "resource_or_scope": excluded_by_resource,
                "path_rules": excluded_by_path,
                "max_file_size": excluded_by_file_size,
                "max_total_size": excluded_by_total_size,
            },
            "summary": (
                f"{phase}: {len(selected)} fragments from {distinct_resources} "
                f"resources, {total_bytes} bytes, {total_tokens} tokens "
                f"({min(max(budget_used, 0.0), 1.0):.1%} budget)"
            ),
        }

    strat = rows["strategize"]
    exe = rows["execute"]
    app = rows["apply"]

    tokens_non_increasing = (
        strat["total_tokens"] >= exe["total_tokens"] >= app["total_tokens"]
    )
    bytes_non_increasing = (
        strat["total_bytes"] >= exe["total_bytes"] >= app["total_bytes"]
    )
    fragments_non_increasing = (
        strat["fragment_count"] >= exe["fragment_count"] >= app["fragment_count"]
    )

    narrowing = {
        "tokens_non_increasing": tokens_non_increasing,
        "bytes_non_increasing": bytes_non_increasing,
        "fragments_non_increasing": fragments_non_increasing,
        "overall": (
            tokens_non_increasing and bytes_non_increasing and fragments_non_increasing
        ),
    }

    return {"phases": rows, "narrowing": narrowing}
