"""Execute-phase ACMS context assembly for LLM execution prompts."""

from __future__ import annotations

import fnmatch
from pathlib import PurePath
from typing import Any, Protocol

import structlog

from cleveragents.application.services.acms_service import ACMSPipeline
from cleveragents.domain.models.acms.crp import AssembledContext, ContextRequest
from cleveragents.domain.models.acms.tiers import TieredFragment
from cleveragents.domain.models.core.context_fragment import (
    ContextBudget as CoreContextBudget,
)
from cleveragents.domain.models.core.context_fragment import (
    ContextFragment as CoreContextFragment,
)
from cleveragents.domain.models.core.context_fragment import (
    FragmentProvenance as CoreFragmentProvenance,
)
from cleveragents.domain.models.core.context_policy import ProjectContextPolicy
from cleveragents.infrastructure.database.repositories import (
    NamespacedProjectRepository,
)

logger = structlog.get_logger(__name__)


class ExecutePhaseContextAssembler(Protocol):
    """Protocol for assembling execute-phase ACMS context."""

    def assemble(self, plan: Any) -> AssembledContext | None:
        """Return assembled context for *plan* or ``None`` when unavailable."""
        ...


class ACMSExecutePhaseContextAssembler(ExecutePhaseContextAssembler):
    """Assemble execute-phase ACMS context from tier fragments and project policy."""

    def __init__(
        self,
        context_tier_service: Any,
        project_repository: NamespacedProjectRepository,
        acms_pipeline: ACMSPipeline | None = None,
        hot_max_tokens: int = 4096,
    ) -> None:
        self._tier = context_tier_service
        self._project_repository = project_repository
        self._pipeline = acms_pipeline or ACMSPipeline()
        self._hot_max_tokens = hot_max_tokens
        self._logger = logger.bind(component="execute_phase_context_assembler")

    @property
    def tier_service(self) -> Any:
        """Public accessor for the underlying ContextTierService."""
        return self._tier

    def _resolve_execute_view(self, project_name: str) -> Any:
        """Resolve the effective execute-phase view for *project_name*."""
        try:
            policy = self._project_repository.get_context_policy(project_name)
        except Exception:
            self._logger.warning(
                "context_policy_lookup_failed",
                project_name=project_name,
                exc_info=True,
            )
            return ProjectContextPolicy().resolve_view("execute")
        return policy.resolve_view("execute")

    @staticmethod
    def _path_matches(path: str, include: list[str], exclude: list[str]) -> bool:
        """Return whether *path* passes include/exclude path globs."""
        pure_path = PurePath(path)
        if include and not any(pure_path.full_match(pattern) for pattern in include):
            return False
        return not (
            exclude and any(pure_path.full_match(pattern) for pattern in exclude)
        )

    @staticmethod
    def _resource_matches(
        resource_id: str, include: list[str], exclude: list[str]
    ) -> bool:
        """Return whether *resource_id* passes include/exclude resource globs."""
        if include and not any(
            fnmatch.fnmatch(resource_id, pattern) for pattern in include
        ):
            return False
        return not (
            exclude
            and any(fnmatch.fnmatch(resource_id, pattern) for pattern in exclude)
        )

    def _to_context_fragment(
        self,
        fragment: TieredFragment,
        *,
        strategy: str,
    ) -> CoreContextFragment:
        """Convert a ``TieredFragment`` into a CRP ``ContextFragment``."""
        metadata = dict(fragment.metadata)
        detail_depth_raw = metadata.get("detail_depth", 1)
        detail_depth = detail_depth_raw if isinstance(detail_depth_raw, int) else 1
        score = metadata.get("relevance_score")
        relevance = float(score) if isinstance(score, (int, float)) else 0.5
        bounded_relevance = min(1.0, max(0.0, relevance))
        location = metadata.get("path", "")
        return CoreContextFragment(
            uko_node=fragment.resource_id or f"uko:{fragment.fragment_id}",
            content=fragment.content,
            detail_depth=detail_depth,
            token_count=fragment.token_count,
            relevance_score=bounded_relevance,
            provenance=CoreFragmentProvenance(
                resource_uri=fragment.resource_id or "unknown",
                location=location if isinstance(location, str) else "",
                strategy=strategy,
            ),
            metadata=metadata,
        )

    def assemble(self, plan: Any) -> AssembledContext | None:
        """Assemble execute-phase context for *plan* from scoped tier fragments."""
        project_names = [
            project.project_name for project in getattr(plan, "project_links", [])
        ]
        if not project_names:
            return None

        views = {name: self._resolve_execute_view(name) for name in project_names}
        scoped = self._tier.get_scoped_view(project_names)
        if not scoped:
            return None

        filtered: list[CoreContextFragment] = []
        excluded_resource_or_scope = 0
        excluded_path_rules = 0
        excluded_max_file = 0
        excluded_max_total = 0
        per_project_bytes: dict[str, int] = {name: 0 for name in project_names}

        for fragment in scoped:
            view = views.get(fragment.project_name)
            if view is None:
                excluded_resource_or_scope += 1
                continue

            resource_id = fragment.resource_id or ""
            if not self._resource_matches(
                resource_id,
                view.include_resources,
                view.exclude_resources,
            ):
                excluded_resource_or_scope += 1
                continue

            path_value = (
                fragment.metadata.get("path")
                if isinstance(fragment.metadata, dict)
                else None
            )
            path_text = path_value if isinstance(path_value, str) else ""
            if path_text and not self._path_matches(
                path_text,
                view.include_paths,
                view.exclude_paths,
            ):
                excluded_path_rules += 1
                continue
            if view.include_paths and not path_text:
                excluded_path_rules += 1
                continue

            byte_size = len(fragment.content.encode("utf-8"))
            if view.max_file_size is not None and byte_size > view.max_file_size:
                excluded_max_file += 1
                continue

            current_bytes = per_project_bytes.get(fragment.project_name, 0)
            if (
                view.max_total_size is not None
                and current_bytes + byte_size > view.max_total_size
            ):
                excluded_max_total += 1
                continue
            per_project_bytes[fragment.project_name] = current_bytes + byte_size

            filtered.append(
                self._to_context_fragment(
                    fragment,
                    strategy="execute_phase_context",
                )
            )

        if not filtered:
            self._logger.info(
                "execute_context_empty_after_filtering",
                plan_id=plan.identity.plan_id,
                project_names=project_names,
                excluded_resource_or_scope=excluded_resource_or_scope,
                excluded_path_rules=excluded_path_rules,
                excluded_max_file_size=excluded_max_file,
                excluded_max_total_size=excluded_max_total,
            )
            return None

        budget = CoreContextBudget(max_tokens=self._hot_max_tokens, reserved_tokens=0)
        request = ContextRequest(
            query=(
                f"Execute-phase context for plan {plan.identity.plan_id} "
                f"({', '.join(project_names)})"
            ),
            purpose="llm_execute_phase_prompt",
            max_tokens=self._hot_max_tokens,
        )
        payload = self._pipeline.assemble(
            plan_id=plan.identity.plan_id,
            fragments=filtered,
            budget=budget,
            strategy="relevance",
            request=request,
        )
        self._logger.info(
            "execute_context_assembled",
            plan_id=plan.identity.plan_id,
            project_names=project_names,
            included_fragments=len(payload.fragments),
            total_tokens=payload.total_tokens,
            budget_used=payload.budget_used,
            excluded_resource_or_scope=excluded_resource_or_scope,
            excluded_path_rules=excluded_path_rules,
            excluded_max_file_size=excluded_max_file,
            excluded_max_total_size=excluded_max_total,
        )
        return AssembledContext(
            fragments=payload.fragments,
            total_tokens=payload.total_tokens,
            budget_used=payload.budget_used,
            strategies_used=payload.strategies_used,
            context_hash=payload.context_hash,
            preamble=payload.preamble,
            provenance_map=payload.provenance_map,
        )
