"""Capability ports. The only executable adapter is deliberately synthetic."""
from dataclasses import dataclass, field
from datetime import datetime, timezone, timedelta
import hashlib
import json
from typing import Protocol
from .geometry import FeaturePort
from .state import Observation


@dataclass(frozen=True)
class ActionRequest:
    action: str
    operation_id: str
    parameters: dict
    checkpoint: str


@dataclass(frozen=True)
class Outcome:
    status: str
    observations: tuple[Observation, ...] = ()
    reason: str = ""
    details: dict = field(default_factory=dict)


class ActionPort(Protocol):
    mode: str
    def perform(self, request: ActionRequest) -> Outcome: ...
    def observe(self) -> Outcome: ...


class SyntheticAdapter:
    mode = "synthetic"

    def __init__(self, catalog, fixture, session, *, cursor=0, epoch=None):
        if fixture.get("mode") != "synthetic" or fixture.get("schema") != 1:
            raise ValueError("Only explicitly synthetic fixtures may execute")
        self.fixture, self.session, self.cursor = fixture, session, cursor
        self.epoch = epoch or datetime.now(timezone.utc).isoformat()
        self.fingerprint = hashlib.sha256(json.dumps(fixture, sort_keys=True).encode()).hexdigest()
        self.catalog, self.port = catalog, FeaturePort(catalog)
        self.calls = []

    def observations(self, facts, event_id):
        at = (datetime.fromisoformat(self.epoch) + timedelta(microseconds=event_id + 1)).isoformat()
        return tuple(Observation(f"{self.session}:{event_id}:{key}", key, value,
            "synthetic:" + self.fixture["id"], self.fixture["version"], at, "synthetic", self.session)
            for key, value in facts.items())

    def seed(self):
        return self.observations(self.fixture["seed"], -1)

    def _take(self, action):
        if self.cursor >= len(self.fixture["events"]):
            return None
        event = self.fixture["events"][self.cursor]
        if event["action"] != action:
            return None
        self.cursor += 1
        return event

    def perform(self, request):
        event = self._take(request.action)
        if event is None:
            return Outcome("waiting", reason="fixture_has_no_matching_event")
        self.calls.append(request.action)
        contract = self.catalog.contracts["actions"][request.action]
        details = {"adapter": "synthetic", "game_input_count": 0, "request": request.action,
                   "semantic_effects": "authored fixture evidence; not inferred from sending a command"}
        if contract["tape"]:
            details["recorded_tape"] = self.port.run_tape(contract["tape"])
            details["tape_limit"] = "Selection/confirmation tape only; travel and activity outcomes are synthetic. No delay was slept or game input sent."
        return Outcome(event["status"], self.observations(event["facts"], self.cursor), event.get("reason", ""), details)

    def observe(self):
        event = self._take("$observe")
        if event is None:
            return Outcome("waiting", reason="no_additional_observation")
        return Outcome("observed", self.observations(event["facts"], self.cursor))
