from __future__ import annotations

import hashlib
import math
import threading
import time
from collections.abc import Callable, Collection, Sequence
from dataclasses import dataclass, field
from typing import Literal, Protocol

from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
    MODELING_STATUS_RESPONSE_TOPIC,
    ApplicationBootstrapError,
    ApplicationCompatibilityProfileError,
    ApplicationControlAuthority,
    CanonicalPostStartObservation,
    LiveDeviceControlBinding,
    ShadowApplicationBootstrapOrchestrator,
    correlate_application_response,
    decode_and_bind_device_info_response,
)
from k1link.device_plugins.xgrids_k1.protocol.application_mqtt import (
    MODELING_RESPONSE_TOPIC,
)
from k1link.device_plugins.xgrids_k1.protocol.application_publish import (
    OneShotPublishEnvelope,
)
from k1link.device_plugins.xgrids_k1.protocol.modeling_control import (
    ModelingAction,
    ModelingProtocolError,
    ModelingResponse,
    correlate_modeling_response,
)
from k1link.device_plugins.xgrids_k1.protocol.modeling_safety import (
    ShadowModelingCommand,
)

MIN_ACCEPTANCE_PERMIT_SECONDS = 15.0
MAX_ACCEPTANCE_PERMIT_SECONDS = 120.0

# Socket-pump quantum only. It is never used to advance a K1 dialogue stage.
CONTROL_NETWORK_PUMP_QUANTUM_SECONDS = 1.0
SCAN_INITIALIZATION_TIMEOUT_SECONDS = 120.0


class ApplicationAcceptanceError(RuntimeError):
    """The operator-present physical acceptance contract failed closed."""


class ScanInitializationTimeout(ApplicationAcceptanceError):
    """START was acknowledged but the reviewed SCANNING gate never completed."""

    reason_code = "scan_initialization_timeout"


class ApplicationProfileMismatchError(ApplicationAcceptanceError):
    """A correlated live DeviceInfo response rejected the selected profile."""

    reason_code = "compatibility_profile_mismatch"


class ApplicationBatchExchange(Protocol):
    def exchange_batch_once(
        self,
        envelopes: Sequence[OneShotPublishEnvelope],
        *,
        required_response_operation_keys: Collection[str],
    ) -> dict[str, bytes]: ...

    def maintain_open_for(
        self,
        duration_seconds: float,
        *,
        allowed_response_topics: Collection[str] = (),
    ) -> None: ...

    def scan_initialization_complete(self, binding: LiveDeviceControlBinding) -> bool: ...

    def pre_start_ready(self, binding: LiveDeviceControlBinding) -> bool: ...

    def standby_complete(self, binding: LiveDeviceControlBinding) -> bool: ...

    def validate_bound_status(self, binding: LiveDeviceControlBinding) -> None: ...


@dataclass(frozen=True, slots=True)
class PhysicalAcceptanceChecklist:
    action: ModelingAction
    operator_present: Literal[True]
    owner_controlled_device: Literal[True]
    lixelgo_closed: Literal[True]
    battery_storage_confirmed: Literal[True]
    expected_physical_state_confirmed: Literal[True]

    def __post_init__(self) -> None:
        confirmations = (
            self.operator_present,
            self.owner_controlled_device,
            self.lixelgo_closed,
            self.battery_storage_confirmed,
            self.expected_physical_state_confirmed,
        )
        if any(value is not True for value in confirmations):
            raise ApplicationAcceptanceError(
                "every physical acceptance confirmation must be explicitly true"
            )
        if type(self.action) is not ModelingAction:
            raise ApplicationAcceptanceError("acceptance action must be an explicit ModelingAction")


@dataclass(frozen=True, slots=True)
class OperatorDialogueCheckpoint:
    """One explicit LixelGO-equivalent UI transition; never a wall-clock gate."""

    event: Literal["workspace-entered", "project-prompt-opened", "start-confirmed"]
    operator_initiated: Literal[True]
    owner_token: object = field(repr=False, compare=False)

    def __post_init__(self) -> None:
        if self.event not in {
            "workspace-entered",
            "project-prompt-opened",
            "start-confirmed",
        }:
            raise ApplicationAcceptanceError("operator dialogue checkpoint is unknown")
        if self.operator_initiated is not True:
            raise ApplicationAcceptanceError(
                "operator dialogue checkpoint must be explicitly initiated"
            )


class PhysicalAcceptancePermit:
    """Short, single-action capability that is consumed before MQTT publish."""

    def __init__(
        self,
        checklist: PhysicalAcceptanceChecklist,
        *,
        ttl_seconds: float = 60.0,
        monotonic: Callable[[], float] = time.monotonic,
    ) -> None:
        if not isinstance(ttl_seconds, (int, float)) or isinstance(ttl_seconds, bool):
            raise ApplicationAcceptanceError("acceptance permit TTL must be numeric")
        if not math.isfinite(ttl_seconds) or not (
            MIN_ACCEPTANCE_PERMIT_SECONDS <= ttl_seconds <= MAX_ACCEPTANCE_PERMIT_SECONDS
        ):
            raise ApplicationAcceptanceError(
                "acceptance permit TTL is outside the reviewed 15-120 second range"
            )
        self._lock = threading.Lock()
        self._checklist = checklist
        self._monotonic = monotonic
        self._expires_at = monotonic() + float(ttl_seconds)
        self._consumed = False

    @property
    def action(self) -> ModelingAction:
        return self._checklist.action

    def consume(self, action: ModelingAction) -> None:
        with self._lock:
            if self._consumed:
                raise ApplicationAcceptanceError("physical acceptance permit was already consumed")
            if self._monotonic() >= self._expires_at:
                raise ApplicationAcceptanceError("physical acceptance permit expired")
            if action is not self._checklist.action:
                raise ApplicationAcceptanceError("physical acceptance permit action mismatch")
            self._consumed = True

    def snapshot(self) -> dict[str, object]:
        with self._lock:
            remaining = max(0.0, self._expires_at - self._monotonic())
            return {
                "mode": "operator-present-physical-acceptance",
                "action": self._checklist.action.name.casefold(),
                "consumed": self._consumed,
                "remaining_seconds": round(remaining, 3),
                "automatic_retry": False,
            }


class PhysicalAcceptanceDialogueExecutor:
    """Drive only explicitly staged portions of the recovered K1 dialogue.

    The legacy implementation ran all ten pre-START reads/mutations in one
    tight loop.  That preserved topic order but did not preserve the observed
    LixelGO lifecycle.  The staged methods below intentionally expose the
    connection, scan-workspace and project-prompt UI boundaries.  They do not
    replay wall-clock gaps from the operator capture.  The canonical START/STOP
    methods keep the same transport owner from operation 1 through the
    operator-confirmed post-STOP standby boundary.  The generic one-command
    entry point remains disabled so START and STOP cannot be emitted by two
    unrelated one-shot processes.
    """

    def __init__(
        self,
        transport: ApplicationBatchExchange,
        *,
        monotonic: Callable[[], float] = time.monotonic,
        scan_initialization_timeout_seconds: float = SCAN_INITIALIZATION_TIMEOUT_SECONDS,
    ) -> None:
        if (
            not isinstance(scan_initialization_timeout_seconds, (int, float))
            or isinstance(scan_initialization_timeout_seconds, bool)
            or not math.isfinite(scan_initialization_timeout_seconds)
            or scan_initialization_timeout_seconds <= 0
        ):
            raise ApplicationAcceptanceError(
                "scan initialization timeout must be a positive finite number"
            )
        self._transport = transport
        self._monotonic = monotonic
        self._scan_initialization_timeout_seconds = float(
            scan_initialization_timeout_seconds
        )
        self._bootstrap_complete = False
        self._command_complete = False
        self._dialogue_stage = "new"
        self._start_complete = False
        self._stop_attempted = False
        self._stop_complete = False
        self._active_authority: ApplicationControlAuthority | None = None
        self._active_binding: LiveDeviceControlBinding | None = None
        self._prepared_binding: LiveDeviceControlBinding | None = None
        self._checkpoint_owner = object()
        self._issued_checkpoint: str | None = None
        self._start_permit_snapshot: dict[str, object] | None = None
        self._stop_permit_snapshot: dict[str, object] | None = None
        self._response_evidence: list[dict[str, object]] = []
        self._correlation_failure: dict[str, object] | None = None
        self._compatibility_failure: dict[str, object] | None = None

    def run_bootstrap(
        self,
        orchestrator: ShadowApplicationBootstrapOrchestrator,
    ) -> LiveDeviceControlBinding:
        del orchestrator
        raise ApplicationAcceptanceError(
            "collapsed bootstrap is disabled; use the canonical staged dialogue"
        )

    def run_connection_stage(
        self,
        orchestrator: ShadowApplicationBootstrapOrchestrator,
    ) -> LiveDeviceControlBinding:
        """Emit retained ordinals 1-6 at control-session establishment."""

        if self._dialogue_stage != "new" or self._bootstrap_complete or self._command_complete:
            raise ApplicationAcceptanceError("connection stage is not admissible now")
        for expected_batch in (1, 2):
            self._exchange_bootstrap_batch(orchestrator, expected_batch=expected_batch)
        binding = orchestrator.binding
        if binding is None:
            raise ApplicationAcceptanceError("connection stage produced no live device binding")
        self._prepared_binding = binding
        self._dialogue_stage = "connection-ready"
        return binding

    def wait_for_operator_checkpoint(
        self,
        event: Literal["workspace-entered", "project-prompt-opened", "start-confirmed"],
        event_observed: Callable[[], bool],
    ) -> OperatorDialogueCheckpoint:
        """Service the original socket until one exact operator UI event occurs."""

        expected = {
            "connection-ready": "workspace-entered",
            "workspace-ready": "project-prompt-opened",
            "project-ready": "start-confirmed",
        }.get(self._dialogue_stage)
        if event != expected or self._issued_checkpoint is not None:
            raise ApplicationAcceptanceError(
                "operator checkpoint does not match the canonical dialogue stage"
            )
        binding = self._prepared_binding
        if binding is None:
            raise ApplicationAcceptanceError("canonical preparation binding is unavailable")
        while not (
            self._transport.pre_start_ready(binding) and event_observed()
        ):
            self._transport.maintain_open_for(
                CONTROL_NETWORK_PUMP_QUANTUM_SECONDS,
                allowed_response_topics={MODELING_STATUS_RESPONSE_TOPIC},
            )
            self._transport.validate_bound_status(binding)
        self._issued_checkpoint = event
        return OperatorDialogueCheckpoint(
            event=event,
            operator_initiated=True,
            owner_token=self._checkpoint_owner,
        )

    def run_workspace_entry_stage(
        self,
        orchestrator: ShadowApplicationBootstrapOrchestrator,
        checkpoint: OperatorDialogueCheckpoint,
    ) -> LiveDeviceControlBinding:
        """Emit ordinal 7 only for the observed scan-workspace entry action."""

        if self._dialogue_stage != "connection-ready":
            raise ApplicationAcceptanceError("workspace entry requires the connection stage")
        self._consume_checkpoint(checkpoint, expected="workspace-entered")
        self._exchange_bootstrap_batch(orchestrator, expected_batch=3)
        binding = orchestrator.binding
        if binding is None:
            raise ApplicationAcceptanceError("workspace entry lost the live device binding")
        self._dialogue_stage = "workspace-ready"
        return binding

    def run_project_prompt_stage(
        self,
        orchestrator: ShadowApplicationBootstrapOrchestrator,
        checkpoint: OperatorDialogueCheckpoint,
    ) -> LiveDeviceControlBinding:
        """Emit ordinals 8-10 when the operator opens the project-name prompt."""

        if self._dialogue_stage != "workspace-ready":
            raise ApplicationAcceptanceError("project prompt requires workspace entry")
        self._consume_checkpoint(checkpoint, expected="project-prompt-opened")
        self._exchange_bootstrap_batch(orchestrator, expected_batch=4)
        if not orchestrator.snapshot().bootstrap_complete:
            raise ApplicationAcceptanceError("project prompt did not complete the transcript")
        binding = orchestrator.binding
        if binding is None:
            raise ApplicationAcceptanceError("project prompt lost the live device binding")
        self._bootstrap_complete = True
        self._dialogue_stage = "project-ready"
        return binding

    def execute_modeling(self, command: ShadowModelingCommand) -> ModelingResponse:
        del command
        raise ApplicationAcceptanceError(
            "standalone modeling commands are disabled; use one canonical START-to-STOP session"
        )

    def execute_canonical_start(
        self,
        command: ShadowModelingCommand,
        post_start: CanonicalPostStartObservation,
        *,
        authority: ApplicationControlAuthority,
        binding: LiveDeviceControlBinding,
        permit: PhysicalAcceptancePermit,
        checkpoint: OperatorDialogueCheckpoint,
    ) -> ModelingResponse:
        """Execute retained operations 11-14 on one continuously serviced socket."""

        if command.action is not ModelingAction.START:
            raise ApplicationAcceptanceError("canonical START executor requires START")
        if self._command_complete:
            raise ApplicationAcceptanceError("physical acceptance command was already attempted")
        if not self._bootstrap_complete or self._dialogue_stage != "project-ready":
            raise ApplicationAcceptanceError("START requires the complete staged preparation")
        if [request.ordinal for request in post_start.requests] != [12, 13, 14]:
            raise ApplicationAcceptanceError("post-START transcript ordinals are invalid")
        self._consume_checkpoint(checkpoint, expected="start-confirmed")
        self._require_command_identity(command, authority=authority, binding=binding)
        if not self._transport.pre_start_ready(binding):
            raise ApplicationAcceptanceError(
                "canonical START requires live READY with no bound project"
            )
        if permit.action is not ModelingAction.START:
            raise ApplicationAcceptanceError("canonical START requires a fresh START permit")

        permit.consume(ModelingAction.START)
        self._start_permit_snapshot = permit.snapshot()
        self._command_complete = True
        self._dialogue_stage = "start-attempted"
        responses = self._transport.exchange_batch_once(
            [OneShotPublishEnvelope.from_modeling_command(command)],
            required_response_operation_keys={"modeling:start"},
        )
        payload = responses["modeling:start"]
        self._record_response_evidence(
            phase="modeling",
            operation_key="modeling:start",
            response_topic=MODELING_RESPONSE_TOPIC,
            payload=payload,
        )
        try:
            response = correlate_modeling_response(payload, command.command)
        except ModelingProtocolError as exc:
            self._record_correlation_failure(
                phase="modeling",
                operation_key="modeling:start",
                response_topic=MODELING_RESPONSE_TOPIC,
                reason=str(exc),
            )
            raise ApplicationAcceptanceError(
                f"modeling response correlation failed for START: {exc}"
            ) from exc

        immediate = post_start.immediate_modeling_status
        self._transport.exchange_batch_once(
            [OneShotPublishEnvelope.from_dialogue_request(immediate)],
            required_response_operation_keys=(),
        )
        self._dialogue_stage = "initializing"
        initialization_deadline = (
            self._monotonic() + self._scan_initialization_timeout_seconds
        )
        while not self._transport.scan_initialization_complete(binding):
            if self._monotonic() >= initialization_deadline:
                raise ScanInitializationTimeout(
                    "K1 did not confirm bound SCANNING initialization before the safety deadline"
                )
            self._transport.maintain_open_for(
                CONTROL_NETWORK_PUMP_QUANTUM_SECONDS,
                allowed_response_topics={MODELING_STATUS_RESPONSE_TOPIC},
            )

        refresh = post_start.post_initialization_refresh
        required_operations = {
            f"dialogue:{request.ordinal}:{request.message_type}" for request in refresh
        }
        refresh_responses = self._transport.exchange_batch_once(
            [OneShotPublishEnvelope.from_dialogue_request(request) for request in refresh],
            required_response_operation_keys=required_operations,
        )
        for request in refresh:
            operation_key = f"dialogue:{request.ordinal}:{request.message_type}"
            refresh_payload = refresh_responses[operation_key]
            self._record_response_evidence(
                phase="post-start",
                operation_key=operation_key,
                response_topic=request.response_topic,
                payload=refresh_payload,
            )
            try:
                if request.message_type == "DeviceInfoRequest":
                    observed = decode_and_bind_device_info_response(
                        refresh_payload,
                        authority,
                        expected_session_id=request.session_id,
                        expected_vendor_device_id=binding.vendor_device_id,
                    )
                    if observed.binding != binding:
                        raise ApplicationBootstrapError(
                            "live DeviceInfo facts changed after START"
                        )
                else:
                    correlate_application_response(
                        refresh_payload,
                        request,
                        authority,
                        live_binding=binding,
                    )
            except ApplicationBootstrapError as exc:
                self._record_correlation_failure(
                    phase="post-start",
                    operation_key=operation_key,
                    response_topic=request.response_topic,
                    reason=str(exc),
                )
                raise ApplicationAcceptanceError(
                    f"post-START response correlation failed for {operation_key}: {exc}"
                ) from exc
        self._dialogue_stage = "post-initialization-observed"
        self._start_complete = True
        self._active_authority = authority
        self._active_binding = binding
        self._prepared_binding = None
        return response

    def maintain_active_until_stop_requested(
        self,
        stop_requested: Callable[[], bool],
    ) -> None:
        """Continuously service the original socket until the operator requests STOP."""

        if self._dialogue_stage != "post-initialization-observed" or not self._start_complete:
            raise ApplicationAcceptanceError(
                "active control ownership requires the complete post-START dialogue"
            )
        binding = self._active_binding
        if binding is None:
            raise ApplicationAcceptanceError("canonical START binding is no longer available")
        while not stop_requested():
            self._transport.maintain_open_for(
                CONTROL_NETWORK_PUMP_QUANTUM_SECONDS,
                allowed_response_topics={MODELING_STATUS_RESPONSE_TOPIC},
            )
            if not self._transport.scan_initialization_complete(binding):
                raise ApplicationAcceptanceError(
                    "K1 left the bound SCANNING state before canonical STOP"
                )
        self._dialogue_stage = "stop-requested"

    def execute_canonical_stop(
        self,
        command: ShadowModelingCommand,
        permit: PhysicalAcceptancePermit,
    ) -> ModelingResponse:
        """Emit retained STOP on the same socket and with a separate permit."""

        if command.action is not ModelingAction.STOP:
            raise ApplicationAcceptanceError("canonical STOP executor requires STOP")
        if self._dialogue_stage != "stop-requested" or not self._start_complete:
            raise ApplicationAcceptanceError(
                "STOP requires continuous ownership from the canonical START session"
            )
        if self._stop_attempted:
            raise ApplicationAcceptanceError("canonical STOP was already attempted")
        authority = self._active_authority
        binding = self._active_binding
        if authority is None or binding is None:
            raise ApplicationAcceptanceError("canonical START binding is no longer available")
        self._require_command_identity(command, authority=authority, binding=binding)
        if not self._transport.scan_initialization_complete(binding):
            raise ApplicationAcceptanceError(
                "canonical STOP requires the bound K1 to still report SCANNING"
            )
        if permit.action is not ModelingAction.STOP:
            raise ApplicationAcceptanceError("canonical STOP requires a separate STOP permit")

        permit.consume(ModelingAction.STOP)
        self._stop_permit_snapshot = permit.snapshot()
        self._stop_attempted = True
        self._dialogue_stage = "stop-attempted"
        responses = self._transport.exchange_batch_once(
            [OneShotPublishEnvelope.from_modeling_command(command)],
            required_response_operation_keys={"modeling:stop"},
        )
        payload = responses["modeling:stop"]
        self._record_response_evidence(
            phase="modeling",
            operation_key="modeling:stop",
            response_topic=MODELING_RESPONSE_TOPIC,
            payload=payload,
        )
        try:
            response = correlate_modeling_response(payload, command.command)
        except ModelingProtocolError as exc:
            self._record_correlation_failure(
                phase="modeling",
                operation_key="modeling:stop",
                response_topic=MODELING_RESPONSE_TOPIC,
                reason=str(exc),
            )
            raise ApplicationAcceptanceError(
                f"modeling response correlation failed for STOP: {exc}"
            ) from exc
        self._stop_complete = True
        self._dialogue_stage = "stop-acknowledged"
        return response

    def maintain_post_stop_until_standby(self) -> None:
        """Keep servicing control reports through save and protocol standby.

        There is no wall-clock shortcut after STOP. The dialogue remains owned
        until the bound K1 itself reports READY with no project attached. That
        live protocol state is the canonical completion evidence; a second
        operator acknowledgement would add no independent information.
        """

        if self._dialogue_stage != "stop-acknowledged" or not self._stop_complete:
            raise ApplicationAcceptanceError(
                "post-STOP ownership requires a correlated canonical STOP"
            )
        binding = self._active_binding
        if binding is None:
            raise ApplicationAcceptanceError("canonical START binding is no longer available")
        while not self._transport.standby_complete(binding):
            self._transport.maintain_open_for(
                CONTROL_NETWORK_PUMP_QUANTUM_SECONDS,
                allowed_response_topics={MODELING_STATUS_RESPONSE_TOPIC},
            )
        self._dialogue_stage = "standby-confirmed"
        self._active_authority = None
        self._active_binding = None

    def snapshot(self) -> dict[str, object]:
        return {
            "mode": "physical-acceptance-only",
            "bootstrap_complete": self._bootstrap_complete,
            "command_complete": self._command_complete,
            "start_attempted": self._command_complete,
            "start_complete": self._start_complete,
            "stop_attempted": self._stop_attempted,
            "stop_complete": self._stop_complete,
            "dialogue_stage": self._dialogue_stage,
            "start_permit": (
                dict(self._start_permit_snapshot)
                if self._start_permit_snapshot is not None
                else None
            ),
            "stop_permit": (
                dict(self._stop_permit_snapshot)
                if self._stop_permit_snapshot is not None
                else None
            ),
            # This snapshot crosses the plugin SDK boundary, whose JsonValue
            # contract intentionally rejects Python-only container types.
            "response_evidence": [dict(item) for item in self._response_evidence],
            "correlation_failure": (
                dict(self._correlation_failure)
                if self._correlation_failure is not None
                else None
            ),
            "compatibility_failure": (
                dict(self._compatibility_failure)
                if self._compatibility_failure is not None
                else None
            ),
            "automatic_retry": False,
        }

    @staticmethod
    def _require_command_identity(
        command: ShadowModelingCommand,
        *,
        authority: ApplicationControlAuthority,
        binding: LiveDeviceControlBinding,
    ) -> None:
        header = command.command.header
        if (
            header.device_id != binding.vendor_device_id
            or header.openapi_key != authority.openapi_key
        ):
            raise ApplicationAcceptanceError(
                "modeling command identity does not match the live canonical session"
            )

    def _consume_checkpoint(
        self,
        checkpoint: OperatorDialogueCheckpoint,
        *,
        expected: str,
    ) -> None:
        if (
            checkpoint.owner_token is not self._checkpoint_owner
            or checkpoint.event != expected
            or self._issued_checkpoint != expected
        ):
            raise ApplicationAcceptanceError(
                "operator checkpoint was not issued by this canonical session"
            )
        self._issued_checkpoint = None

    def _exchange_bootstrap_batch(
        self,
        orchestrator: ShadowApplicationBootstrapOrchestrator,
        *,
        expected_batch: int,
    ) -> None:
        snapshot = orchestrator.snapshot()
        if snapshot.current_batch != expected_batch or snapshot.batch_issued:
            raise ApplicationAcceptanceError(
                f"canonical bootstrap expected batch {expected_batch}"
            )
        batch = orchestrator.next_batch()
        required_operations = {
            f"bootstrap:{request.ordinal}:{request.message_type}"
            for request in batch
            if request.response_required
        }
        responses = self._transport.exchange_batch_once(
            [OneShotPublishEnvelope.from_bootstrap_request(request) for request in batch],
            required_response_operation_keys=required_operations,
        )
        for request in batch:
            if not request.response_required:
                continue
            operation_key = f"bootstrap:{request.ordinal}:{request.message_type}"
            payload = responses[operation_key]
            self._record_response_evidence(
                phase="bootstrap",
                operation_key=operation_key,
                response_topic=request.response_topic,
                payload=payload,
            )
            try:
                orchestrator.accept_response(operation_key, payload)
            except ApplicationCompatibilityProfileError as exc:
                self._compatibility_failure = {
                    "phase": "bootstrap",
                    "operation_key": operation_key,
                    "response_topic": request.response_topic,
                    "reason_code": exc.reason_code,
                    "expected": {
                        "device_model": "LixelKity K1",
                        "platform_type": "A4",
                        "firmware": "3.0.2",
                        "is_activated": True,
                    },
                    "observed": dict(exc.observed_facts),
                }
                raise ApplicationProfileMismatchError(
                    "live DeviceInfo is incompatible with the selected "
                    "LixelKity K1 / A4 / FW 3.0.2 profile"
                ) from exc
            except ApplicationBootstrapError as exc:
                self._record_correlation_failure(
                    phase="bootstrap",
                    operation_key=operation_key,
                    response_topic=request.response_topic,
                    reason=str(exc),
                )
                raise ApplicationAcceptanceError(
                    "bootstrap response correlation failed for "
                    f"ordinal {request.ordinal} ({request.message_type}): {exc}"
                ) from exc

    def _record_response_evidence(
        self,
        *,
        phase: Literal["bootstrap", "modeling", "post-start"],
        operation_key: str,
        response_topic: str,
        payload: bytes,
    ) -> None:
        self._response_evidence.append(
            {
                "phase": phase,
                "operation_key": operation_key,
                "response_topic": response_topic,
                "payload_bytes": len(payload),
                "payload_sha256": hashlib.sha256(payload).hexdigest(),
            }
        )

    def _record_correlation_failure(
        self,
        *,
        phase: Literal["bootstrap", "modeling", "post-start"],
        operation_key: str,
        response_topic: str,
        reason: str,
    ) -> None:
        self._correlation_failure = {
            "phase": phase,
            "operation_key": operation_key,
            "response_topic": response_topic,
            "reason_code": self._correlation_reason_code(reason),
            "reason": reason,
        }

    @staticmethod
    def _correlation_reason_code(reason: str) -> str:
        reason_fragments = (
            ("session correlation failed", "response_session_mismatch"),
            ("vendor identity mismatch", "response_device_identity_mismatch"),
            ("authority mismatch", "response_authority_mismatch"),
            ("result code", "response_rejected"),
            ("facts changed", "response_device_facts_changed"),
            ("invalid", "response_decode_failed"),
            ("missing", "response_decode_failed"),
            ("wrong wire type", "response_decode_failed"),
        )
        return next(
            (code for fragment, code in reason_fragments if fragment in reason),
            "response_correlation_failed",
        )
