"""Einheitliche Adapter-Schnittstelle.

Jeder Adapter erbt von BackendAdapter. BackendAdapter.execute() ist final:
1. _prepare(job) erzeugt den Outbound-Schnappschuss, also genau das, was
   gesendet wird (für Claude: Kanal-Parameter; für Codex: stdin + Dateien).
2. Der Digest wird über diesen Schnappschuss NEU berechnet, die
   Datenschutzklasse über den Kontext NEU klassifiziert.
3. ClearanceVerifier.verify() prüft HMAC, Backend, Job, Absender, Chat,
   Ablauf, Nonce, Digest, Datenschutzklasse gegen die Matrix.
4. Erst dann _transmit(outbound). Ohne gültige Clearance wird nichts gesendet.

Unterklassen dürfen execute()/outbound_digest() nicht definieren
(__init_subclass__ verweigert das).
"""
from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from typing import Any, ClassVar, Mapping, Optional, final

from .authorization import AuthorizationError, ClearanceVerifier
from .context import ContextEnvelope, canonical_json, sha256_hex

OUTBOUND_DOMAIN = b"aria-v3/outbound/v1\n"


class AdapterContractError(ValueError):
    pass


@dataclass(frozen=True)
class ExecutionPolicy:
    timeout_seconds: int = 120
    max_attempts: int = 1
    allow_side_effects: bool = False

    def __post_init__(self):
        if type(self.timeout_seconds) is not int:
            raise AdapterContractError("Invalid timeout")

        if not 1 <= self.timeout_seconds <= 3600:
            raise AdapterContractError("Timeout out of range")

        if type(self.max_attempts) is not int:
            raise AdapterContractError("Invalid attempts")

        if self.max_attempts != 1:
            raise AdapterContractError(
                "Retries disabled until idempotency is implemented"
            )

        if type(self.allow_side_effects) is not bool:
            raise AdapterContractError(
                "Invalid side-effect policy"
            )

        if self.allow_side_effects:
            raise AdapterContractError(
                "Side effects disabled in Phase 3.1"
            )


class ResultStatus(str, Enum):
    SUCCESS = "success"      # Backend hat geantwortet (Codex)
    FORWARD = "forward"      # Weiterleitungsanweisung erzeugt (Claude-Kanal)
    FAILED = "failed"        # Backend-/Prozessfehler
    BLOCKED = "blocked"      # vor dem Senden abgelehnt (Clearance, Eingabe)


@dataclass(frozen=True)
class AdapterResult:
    job_id: str
    backend: str
    status: ResultStatus
    output: str = ""
    error_code: Optional[str] = None
    forward: Optional[Mapping[str, Any]] = None
    timed_out: bool = False

    @property
    def success(self) -> bool:
        return self.status in (ResultStatus.SUCCESS, ResultStatus.FORWARD)


@dataclass(frozen=True)
class AdapterJob:
    job_id: str
    envelope: ContextEnvelope
    sender_id: str
    chat_id: str
    message_id: int
    sender_username: Optional[str] = None
    ts: Optional[str] = None
    policy: ExecutionPolicy = ExecutionPolicy()


class AdapterInputError(ValueError):
    def __init__(self, code: str):
        super().__init__(code)
        self.code = code


@dataclass(frozen=True)
class Outbound:
    """Schnappschuss genau dessen, was der Adapter sendet."""

    backend: str
    transmitted: Mapping[str, Any]
    payload: Any = None

    def digest(self) -> str:
        return sha256_hex(OUTBOUND_DOMAIN + canonical_json({
            "backend": self.backend, "transmitted": self.transmitted,
        }))


class BackendAdapter(ABC):
    name: ClassVar[str] = ""
    accepts_memory: ClassVar[bool] = False

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        for protected in ("execute", "outbound_digest", "__init_subclass__"):
            if protected in cls.__dict__:
                raise TypeError(f"{cls.__name__} must not override {protected}()")

    def __init__(self, verifier: ClearanceVerifier):
        if not isinstance(verifier, ClearanceVerifier):
            raise TypeError("ClearanceVerifier required")
        if not self.name:
            raise TypeError("adapter name required")
        self._verifier = verifier

    @property
    def verifier(self) -> ClearanceVerifier:
        return self._verifier

    @abstractmethod
    def _prepare(self, job: AdapterJob) -> Outbound:
        """Baut den Outbound-Schnappschuss. Sendet nichts."""

    @abstractmethod
    def _transmit(self, outbound: Outbound, job: AdapterJob, clearance) -> AdapterResult:
        """Sendet den Schnappschuss. Wird nur nach erfolgreicher Prüfung gerufen."""

    def _blocked(self, job_id: str, code: str) -> AdapterResult:
        return AdapterResult(job_id, self.name, ResultStatus.BLOCKED, error_code=code)

    @final
    def outbound_digest(self, job: AdapterJob) -> str:
        return self._prepare(job).digest()

    @final
    def execute(self, clearance, job: AdapterJob) -> AdapterResult:
        if not isinstance(job, AdapterJob):
            return AdapterResult("", self.name, ResultStatus.BLOCKED, error_code="INVALID_JOB")
        verifier = getattr(self, "_verifier", None)
        if not isinstance(verifier, ClearanceVerifier):
            return self._blocked(job.job_id, "NO_VERIFIER")
        try:
            outbound = self._prepare(job)
        except AdapterInputError as exc:
            return self._blocked(job.job_id, exc.code)
        except Exception:
            return self._blocked(job.job_id, "PREPARE_FAILED")
        if not isinstance(outbound, Outbound) or outbound.backend != self.name:
            return self._blocked(job.job_id, "INVALID_OUTBOUND")
        try:
            privacy = verifier.classifier.classify(job.envelope).tier
        except Exception:
            return self._blocked(job.job_id, "CLASSIFICATION_FAILED")
        try:
            verified = verifier.verify(
                clearance, backend=self.name, job_id=job.job_id,
                context_digest=outbound.digest(), privacy=privacy,
                sender_id=job.sender_id, chat_id=job.chat_id,
            )
        except AuthorizationError as exc:
            return self._blocked(job.job_id, exc.code)
        except Exception:
            # z. B. Nonce-Speicher defekt: ohne vollständige Prüfung nichts senden.
            return self._blocked(job.job_id, "VERIFICATION_FAILED")
        try:
            result = self._transmit(outbound, job, verified)
        except Exception:
            return AdapterResult(job.job_id, self.name, ResultStatus.FAILED, error_code="ADAPTER_EXCEPTION")
        if (not isinstance(result, AdapterResult) or result.job_id != job.job_id
                or result.backend != self.name or not isinstance(result.output, str)):
            return AdapterResult(job.job_id, self.name, ResultStatus.FAILED, error_code="INVALID_ADAPTER_RESULT")
        if result.status is ResultStatus.SUCCESS and not result.output.strip():
            return AdapterResult(job.job_id, self.name, ResultStatus.FAILED, error_code="EMPTY_RESPONSE")
        return result


def transmitted_context(envelope: ContextEnvelope) -> dict:
    """Kanonische Kontextdarstellung für Outbound-Schnappschüsse."""
    return envelope.canonical_dict()


__all__ = [
    "AdapterContractError", "ExecutionPolicy", "ResultStatus", "AdapterResult", "AdapterJob",
    "AdapterInputError", "Outbound", "BackendAdapter", "transmitted_context", "canonical_json",
]
