"""Deterministic read-only profile evaluation."""

from __future__ import annotations

from collections import Counter
from collections.abc import Iterable

from ..domain.enums import ProfileEligibilityState
from ..domain.models import ProfileVersion, RegionScopedIdentity
from ..errors import ContractViolation
from ..pbf.models import ParsedOsmObject
from ..pbf.tags import canonicalize_osm_tags
from .models import (
    ProfileDecision,
    ProfileDefinition,
    ProfileImpactReport,
    ProfileRegistry,
    RuleEffect,
)

_MAX_IMPACT_OBJECTS = 1_000_000


class ProfileEvaluator:
    """Evaluate exact profile versions without side effects or implicit activation."""

    def __init__(self, registry: ProfileRegistry) -> None:
        if not isinstance(registry, ProfileRegistry):
            raise ContractViolation("registry must be ProfileRegistry")
        self._registry = registry

    def evaluate(
        self,
        *,
        region_id: str,
        parsed_object: ParsedOsmObject,
        profile: ProfileVersion,
    ) -> ProfileDecision:
        if not isinstance(parsed_object, ParsedOsmObject):
            raise ContractViolation("parsed_object must be ParsedOsmObject")
        definition = self._registry.get(profile)
        identity = RegionScopedIdentity(
            region_id=region_id,
            osm_type=parsed_object.identity.osm_type,
            osm_id=parsed_object.identity.osm_id,
        )
        tags = canonicalize_osm_tags(parsed_object.tags)
        matching = tuple(
            rule for rule in definition.effective_rules(identity.region_id) if rule.matches(tags)
        )
        exclusions = tuple(
            sorted(rule.rule_id for rule in matching if rule.effect is RuleEffect.EXCLUDE)
        )
        includes = tuple(rule for rule in matching if rule.effect is RuleEffect.INCLUDE)
        include_ids = tuple(sorted(rule.rule_id for rule in includes))

        if exclusions:
            return ProfileDecision(
                identity=identity,
                profile=definition.profile,
                state=ProfileEligibilityState.EXCLUDED,
                definition_digest=definition.definition_digest,
                matched_rule_ids=include_ids,
                excluded_rule_ids=exclusions,
                evidence=tuple(f"excluded_by:{rule_id}" for rule_id in exclusions),
            )

        if not includes:
            return ProfileDecision(
                identity=identity,
                profile=definition.profile,
                state=ProfileEligibilityState.OUT_OF_SCOPE,
                definition_digest=definition.definition_digest,
                evidence=("no_include_rule_matched",),
            )

        highest_priority = max(rule.priority for rule in includes)
        winning_includes = tuple(rule for rule in includes if rule.priority == highest_priority)
        suppressed_includes = tuple(rule for rule in includes if rule.priority != highest_priority)
        mappings = tuple(
            sorted({(rule.category or "", rule.subcategory or "") for rule in winning_includes})
        )
        evidence_items = {f"matched_rule:{rule.rule_id}" for rule in includes} | {
            item for rule in includes for item in rule.evidence
        }
        if suppressed_includes:
            evidence_items.add(f"selected_mapping_priority:{highest_priority}")
            evidence_items.update(
                f"suppressed_by_priority:{rule.rule_id}" for rule in suppressed_includes
            )
        evidence = tuple(sorted(evidence_items))
        if len(mappings) != 1:
            mapping_evidence = tuple(
                f"mapping:{category}:{subcategory}" for category, subcategory in mappings
            )
            return ProfileDecision(
                identity=identity,
                profile=definition.profile,
                state=ProfileEligibilityState.PENDING_REVIEW,
                definition_digest=definition.definition_digest,
                matched_rule_ids=include_ids,
                evidence=evidence + mapping_evidence + ("conflicting_category_mappings",),
            )

        category, subcategory = mappings[0]
        return ProfileDecision(
            identity=identity,
            profile=definition.profile,
            state=ProfileEligibilityState.ACTIVE,
            definition_digest=definition.definition_digest,
            matched_rule_ids=include_ids,
            category=category,
            subcategory=subcategory or None,
            evidence=evidence,
        )

    def impact_report(
        self,
        *,
        region_id: str,
        objects: Iterable[ParsedOsmObject],
        profile: ProfileVersion,
    ) -> ProfileImpactReport:
        definition: ProfileDefinition = self._registry.get(profile)
        state_counts: Counter[str] = Counter()
        category_counts: Counter[str] = Counter()
        seen_identities: set[tuple[str, int]] = set()
        for parsed_object in objects:
            if not isinstance(parsed_object, ParsedOsmObject):
                raise ContractViolation("objects must contain ParsedOsmObject values")
            identity_key = (
                parsed_object.identity.osm_type.value,
                parsed_object.identity.osm_id,
            )
            if identity_key in seen_identities:
                raise ContractViolation("impact report contains a repeated source identity")
            if len(seen_identities) >= _MAX_IMPACT_OBJECTS:
                raise ContractViolation("impact report exceeds the bounded object count")
            seen_identities.add(identity_key)
            decision = self.evaluate(
                region_id=region_id,
                parsed_object=parsed_object,
                profile=profile,
            )
            state_counts[decision.state.value] += 1
            if decision.category is not None:
                category_key = decision.category
                if decision.subcategory is not None:
                    category_key = f"{category_key}/{decision.subcategory}"
                category_counts[category_key] += 1

        return ProfileImpactReport(
            profile=definition.profile,
            definition_digest=definition.definition_digest,
            object_count=len(seen_identities),
            state_counts=tuple(state_counts.items()),
            category_counts=tuple(category_counts.items()),
        )
