"""Einzige Routinglogik des Aria-V3-Kerns.

`route()` ist eine reine Funktion: Sie entscheidet, führt nichts aus und
vergibt keine Freigabe. Ausführen kann nur ein Adapter mit gültiger
HMAC-Clearance (siehe authorization.py, adapter_contract.py).

Eingangsgrößen: Aufgabenklasse, Fähigkeiten, Datenschutzklasse (über die
PrivacyMatrix), Qualität (inkl. Untergrenze), Verfügbarkeit, Budget,
Kontextgröße und manuelle Modellwahl. Bei gleicher Eignung gewinnt Codex.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import FrozenSet, Iterable, Mapping, Optional


class PrivacyTier(str, Enum):
    PUBLIC = "public"
    INTERNAL = "internal"
    CONFIDENTIAL = "confidential"
    UNKNOWN = "unknown"


# Aggregation "am empfindlichsten gewinnt". UNKNOWN liegt über INTERNAL, weil
# ein unbekannter Inhalt auch vertraulich sein kann; CONFIDENTIAL ist maximal.
PRIVACY_SEVERITY = {
    PrivacyTier.PUBLIC: 0,
    PrivacyTier.INTERNAL: 1,
    PrivacyTier.UNKNOWN: 2,
    PrivacyTier.CONFIDENTIAL: 3,
}


def most_sensitive(tiers: Iterable[PrivacyTier]) -> PrivacyTier:
    result = PrivacyTier.PUBLIC
    seen = False
    for tier in tiers:
        seen = True
        if PRIVACY_SEVERITY[tier] > PRIVACY_SEVERITY[result]:
            result = tier
    return result if seen else PrivacyTier.UNKNOWN


class Mode(str, Enum):
    AUTO = "auto"
    CODEX = "codex"
    CLAUDE = "claude"


KNOWN_BACKENDS = frozenset({"codex", "claude"})


@dataclass(frozen=True)
class JobRequest:
    job_id: str
    task_class: str
    # Fail-closed-Default: ohne Klassifikation gilt UNKNOWN.
    privacy: PrivacyTier = PrivacyTier.UNKNOWN
    required_capabilities: FrozenSet[str] = field(default_factory=frozenset)
    mode: Mode = Mode.AUTO
    context_bytes: int = 0


@dataclass(frozen=True)
class Backend:
    name: str
    capabilities: FrozenSet[str]
    supported_tasks: FrozenSet[str]
    available: bool = True
    budget_allowed: bool = True
    quality_score: int = 50
    max_context_bytes: Optional[int] = None
    # Qualität je Aufgabenklasse als Paare (hashbar); fehlt eine Klasse,
    # gilt quality_score.
    task_quality: tuple[tuple[str, int], ...] = ()

    def quality_for(self, task_class: str) -> int:
        for task, score in self.task_quality:
            if task == task_class:
                return score
        return self.quality_score


@dataclass(frozen=True)
class PrivacyMatrix:
    """Welche Datenschutzklasse an welches Backend darf (Konfiguration)."""

    allowed: tuple[tuple[str, FrozenSet[PrivacyTier]], ...]

    def permits(self, backend: str, tier: PrivacyTier) -> bool:
        if not isinstance(tier, PrivacyTier):
            return False
        for name, tiers in self.allowed:
            if name == backend:
                return tier in tiers
        return False

    def tiers_for(self, backend: str) -> FrozenSet[PrivacyTier]:
        for name, tiers in self.allowed:
            if name == backend:
                return tiers
        return frozenset()

    @classmethod
    def from_mapping(cls, mapping: Mapping[str, Iterable[PrivacyTier]]) -> "PrivacyMatrix":
        return cls(tuple(sorted(
            (name, frozenset(tiers)) for name, tiers in mapping.items()
        )))

    @classmethod
    def strict(cls) -> "PrivacyMatrix":
        """Altvertrag von route(): nur PUBLIC darf an irgendein Backend."""
        return cls.from_mapping({
            "codex": {PrivacyTier.PUBLIC},
            "claude": {PrivacyTier.PUBLIC},
        })


@dataclass(frozen=True)
class CandidateEvaluation:
    backend: str
    eligible: bool
    reasons: tuple[str, ...]
    quality: int


@dataclass(frozen=True)
class RoutingDecision:
    job_id: str
    backend: Optional[str]
    allowed: bool
    reason: str
    code: str = ""
    candidates: tuple[CandidateEvaluation, ...] = ()
    tie_break: bool = False
    manual: bool = False
    fallback_from: Optional[str] = None


# Entscheidungscodes
SELECTED = "SELECTED"
MANUAL_SELECTED = "MANUAL_SELECTED"
CHECKED_FALLBACK = "CHECKED_FALLBACK"
PRIVACY_DENIED = "PRIVACY_DENIED"
NO_ELIGIBLE_BACKEND = "NO_ELIGIBLE_BACKEND"
MANUAL_CHOICE_NOT_PERMITTED = "MANUAL_CHOICE_NOT_PERMITTED"


def _evaluate(job: JobRequest, backend: Backend, matrix: PrivacyMatrix,
              min_quality: int) -> CandidateEvaluation:
    reasons: list[str] = []
    if backend.name not in KNOWN_BACKENDS:
        reasons.append("UNKNOWN_BACKEND")
    if not matrix.permits(backend.name, job.privacy):
        reasons.append("PRIVACY_NOT_PERMITTED")
    if not backend.available:
        reasons.append("UNAVAILABLE")
    if not backend.budget_allowed:
        reasons.append("BUDGET_EXHAUSTED")
    if not job.required_capabilities.issubset(backend.capabilities):
        reasons.append("CAPABILITY_MISSING")
    if job.task_class not in backend.supported_tasks:
        reasons.append("TASK_UNSUPPORTED")
    if backend.max_context_bytes is not None and job.context_bytes > backend.max_context_bytes:
        reasons.append("CONTEXT_TOO_LARGE")
    quality = backend.quality_for(job.task_class)
    if quality < min_quality:
        reasons.append("QUALITY_BELOW_MINIMUM")
    return CandidateEvaluation(backend.name, not reasons, tuple(reasons), quality)


def _rank(evaluations: list[CandidateEvaluation]) -> list[CandidateEvaluation]:
    # Höhere Qualität zuerst; bei Gleichstand Codex (Tie-Breaker), dann Name.
    return sorted(evaluations, key=lambda e: (-e.quality, e.backend != "codex", e.backend))


def route(job: JobRequest, backends: list[Backend], matrix: Optional[PrivacyMatrix] = None,
          *, min_quality: int = 0, manual_fallback: bool = False) -> RoutingDecision:
    """Wählt ein Backend oder lehnt strukturiert ab.

    matrix=None bedeutet die strikte Altmatrix (nur PUBLIC). Das Gateway
    übergibt immer die konfigurierte Matrix.

    Manuelle Wahl überstimmt nie eine Prüfung. Ist das gewünschte Backend
    nicht zulässig, wird blockiert. Nur mit manual_fallback=True darf ein
    anderes Backend gewählt werden, und auch dann nur eines, das jede
    einzelne Prüfung (Datenschutz, Verfügbarkeit, Budget, Fähigkeiten,
    Kontextgröße, Qualität) selbst bestanden hat.
    """
    if matrix is None:
        matrix = PrivacyMatrix.strict()
    evaluations = [_evaluate(job, b, matrix, min_quality) for b in backends]
    candidates = tuple(evaluations)
    eligible = [e for e in evaluations if e.eligible]

    if job.mode is not Mode.AUTO:
        wanted = job.mode.value
        chosen = [e for e in eligible if e.backend == wanted]
        if chosen:
            return RoutingDecision(
                job.job_id, wanted, True,
                f"Selected {wanted}: manual choice passed all checks",
                MANUAL_SELECTED, candidates, manual=True,
            )
        wanted_reasons = [r for e in evaluations if e.backend == wanted for r in e.reasons]
        if manual_fallback:
            alternatives = _rank([e for e in eligible if e.backend != wanted])
            if alternatives:
                selected = alternatives[0]
                return RoutingDecision(
                    job.job_id, selected.backend, True,
                    f"Selected {selected.backend}: checked fallback, {wanted} not permitted "
                    f"({', '.join(wanted_reasons) or 'not configured'})",
                    CHECKED_FALLBACK, candidates, manual=True, fallback_from=wanted,
                )
        return RoutingDecision(
            job.job_id, None, False,
            f"Manual choice {wanted} not permitted: {', '.join(wanted_reasons) or 'not configured'}",
            MANUAL_CHOICE_NOT_PERMITTED, candidates, manual=True,
        )

    if not eligible:
        privacy_only = bool(evaluations) and all(
            "PRIVACY_NOT_PERMITTED" in e.reasons for e in evaluations
        )
        if privacy_only:
            return RoutingDecision(
                job.job_id, None, False,
                "Privacy policy: execution denied for every backend",
                PRIVACY_DENIED, candidates,
            )
        return RoutingDecision(job.job_id, None, False, "No eligible backend",
                               NO_ELIGIBLE_BACKEND, candidates)

    ranked = _rank(eligible)
    selected = ranked[0]
    tie_break = len(ranked) > 1 and ranked[1].quality == selected.quality
    return RoutingDecision(
        job.job_id, selected.backend, True,
        f"Selected {selected.backend}: eligible backend"
        + (" (tie-break codex)" if tie_break and selected.backend == "codex" else ""),
        SELECTED, candidates, tie_break=tie_break,
    )
