from __future__ import annotations

import hashlib
import threading
from dataclasses import dataclass, field
from typing import Literal

from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
    REVIEWED_PROFILE_ATTESTATION_FAILURE,
    ApplicationControlAuthority,
    LiveDeviceControlBinding,
)
from k1link.device_plugins.xgrids_k1.protocol.modeling_control import (
    CommandHeaderIdentity,
    DeviceStatusReport,
    EncodedModelingCommand,
    ModelingAction,
    MountType,
    RecordMode,
    ScanMode,
    SessionState,
    decode_device_status_report,
    encode_modeling_start,
    encode_modeling_stop,
)
from k1link.device_plugins.xgrids_k1.viewer.messages import StreamMessage
from k1link.viewer.metrics import BridgeMetrics

DEVICE_STATUS_TOPIC = "lixel/application/report/device_status"
MODELING_REQUEST_TOPIC = "lixel/application/request/modeling"
SHADOW_BLOCKERS = ("vendor-writes-disabled", "publisher-not-installed")


class ModelingControlSafetyError(RuntimeError):
    """A live K1 could not be bound to reviewed device-specific control input."""


@dataclass(frozen=True, slots=True)
class ModelingControlSafetySnapshot:
    status_reports_observed: int
    decode_errors: int
    vendor_identity_observed: bool
    device_serial_observed: bool
    identity_conflict: bool
    session_state: str | None
    project_bound: bool
    ready_for_start_shadow: bool
    ready_for_stop_shadow: bool

    def as_dict(self) -> dict[str, object]:
        return {
            "mode": "shadow-only",
            "status_reports_observed": self.status_reports_observed,
            "decode_errors": self.decode_errors,
            "vendor_identity_observed": self.vendor_identity_observed,
            "device_serial_observed": self.device_serial_observed,
            "identity_conflict": self.identity_conflict,
            "session_state": self.session_state,
            "project_bound": self.project_bound,
            "ready_for_start_shadow": self.ready_for_start_shadow,
            "ready_for_stop_shadow": self.ready_for_stop_shadow,
            "vendor_writes_enabled": False,
            "publisher_installed": False,
        }


@dataclass(frozen=True, slots=True)
class ShadowModelingCommand:
    """A reviewed command plan with no transport or execution authority."""

    action: ModelingAction
    command: EncodedModelingCommand = field(repr=False)
    payload_sha256: str
    payload_bytes: int
    topic: str = MODELING_REQUEST_TOPIC
    qos: int = 2
    retain: bool = False
    executable: bool = False
    blockers: tuple[str, ...] = SHADOW_BLOCKERS
    retry_policy: Literal["never-automatic"] = "never-automatic"

    @classmethod
    def from_command(cls, command: EncodedModelingCommand) -> ShadowModelingCommand:
        return cls(
            action=command.action,
            command=command,
            payload_sha256=hashlib.sha256(command.payload).hexdigest(),
            payload_bytes=len(command.payload),
        )

    def as_dict(self) -> dict[str, object]:
        return {
            "mode": "shadow-only",
            "action": self.action.name.casefold(),
            "topic": self.topic,
            "qos": self.qos,
            "retain": self.retain,
            "payload_sha256": self.payload_sha256,
            "payload_bytes": self.payload_bytes,
            "executable": self.executable,
            "blockers": list(self.blockers),
            "retry_policy": self.retry_policy,
        }


class LiveModelingControlSafety:
    """Track only live status evidence and fail closed on identity drift."""

    def __init__(self) -> None:
        self._lock = threading.Lock()
        self._status_reports_observed = 0
        self._decode_errors = 0
        self._vendor_device_id: str | None = None
        self._device_serial: str | None = None
        self._identity_conflict = False
        self._latest_report: DeviceStatusReport | None = None

    def reset(self) -> None:
        with self._lock:
            self._status_reports_observed = 0
            self._decode_errors = 0
            self._vendor_device_id = None
            self._device_serial = None
            self._identity_conflict = False
            self._latest_report = None

    def observe(self, message: StreamMessage, _metrics: BridgeMetrics) -> bool:
        if message.source != "live_mqtt" or message.topic != DEVICE_STATUS_TOPIC:
            return False
        try:
            report = decode_device_status_report(message.payload)
        except ValueError:
            with self._lock:
                self._decode_errors += 1
            return True

        header_device_id = report.header.device_id if report.header is not None else None
        with self._lock:
            self._status_reports_observed += 1
            if header_device_id is None or report.device_sn is None:
                self._identity_conflict = True
            else:
                if self._vendor_device_id is None:
                    self._vendor_device_id = header_device_id
                elif self._vendor_device_id != header_device_id:
                    self._identity_conflict = True
                if self._device_serial is None:
                    self._device_serial = report.device_sn
                elif self._device_serial != report.device_sn:
                    self._identity_conflict = True
            self._latest_report = report
        return True

    def snapshot(self) -> ModelingControlSafetySnapshot:
        with self._lock:
            report = self._latest_report
            state = report.session_state if report is not None else None
            project_bound = bool(report and report.project_id)
            observed = self._vendor_device_id is not None and self._device_serial is not None
            safe = observed and not self._identity_conflict and self._decode_errors == 0
            return ModelingControlSafetySnapshot(
                status_reports_observed=self._status_reports_observed,
                decode_errors=self._decode_errors,
                vendor_identity_observed=self._vendor_device_id is not None,
                device_serial_observed=self._device_serial is not None,
                identity_conflict=self._identity_conflict,
                session_state=state.name.casefold() if state is not None else None,
                project_bound=project_bound,
                ready_for_start_shadow=safe and state is SessionState.READY and not project_bound,
                ready_for_stop_shadow=safe and state is SessionState.SCANNING and project_bound,
            )

    def plan_start(
        self,
        authority: ApplicationControlAuthority,
        binding: LiveDeviceControlBinding,
        *,
        project_name: str,
    ) -> ShadowModelingCommand:
        self._require_bound(binding, required_state=SessionState.READY, project_bound=False)
        command = encode_modeling_start(
            _command_header(authority, binding),
            project_name=project_name,
            record_mode=RecordMode.RECORD_AND_CALCULATE,
            scan_mode=ScanMode.LCC,
            mount_type=MountType.HANDHELD,
        )
        return ShadowModelingCommand.from_command(command)

    def plan_stop(
        self,
        authority: ApplicationControlAuthority,
        binding: LiveDeviceControlBinding,
    ) -> ShadowModelingCommand:
        self._require_bound(binding, required_state=SessionState.SCANNING, project_bound=True)
        return ShadowModelingCommand.from_command(
            encode_modeling_stop(_command_header(authority, binding))
        )

    def _require_bound(
        self,
        binding: LiveDeviceControlBinding,
        *,
        required_state: SessionState,
        project_bound: bool,
    ) -> None:
        with self._lock:
            report = self._latest_report
            if self._identity_conflict or self._decode_errors:
                raise ModelingControlSafetyError("live K1 identity/status evidence is conflicted")
            if report is None or self._vendor_device_id is None or self._device_serial is None:
                raise ModelingControlSafetyError("live K1 identity/status evidence is missing")
            if (
                binding.vendor_device_id != self._vendor_device_id
                or binding.device_serial != self._device_serial
            ):
                raise ModelingControlSafetyError(
                    "DeviceInfo binding does not match the live K1 status stream"
                )
            if not binding.ready_for_reviewed_profile:
                raise ModelingControlSafetyError(REVIEWED_PROFILE_ATTESTATION_FAILURE)
            if report.session_state is not required_state:
                raise ModelingControlSafetyError(
                    f"live K1 must be {required_state.name.casefold()} for this shadow plan"
                )
            if bool(report.project_id) is not project_bound:
                raise ModelingControlSafetyError("live K1 project binding is not in the safe state")


def _command_header(
    authority: ApplicationControlAuthority,
    binding: LiveDeviceControlBinding,
) -> CommandHeaderIdentity:
    return CommandHeaderIdentity(
        device_id=binding.vendor_device_id,
        openapi_key=authority.openapi_key,
    )
