from __future__ import annotations

import asyncio
import os
import queue
import signal
import subprocess
import threading
from collections import deque
from collections.abc import Callable
from contextlib import suppress
from dataclasses import dataclass, field
from pathlib import Path
from typing import IO, Any, Final, Literal

from fastapi import APIRouter, WebSocket, WebSocketDisconnect

from k1link.device_plugins.xgrids_k1.mqtt import validate_private_ipv4
from k1link.web.camera_archive import (
    CameraArchiveError,
    CameraArchiveKind,
    CameraArchiveStatus,
    CameraArchiveWriter,
)

CameraSourceId = Literal["sensor.camera.left", "sensor.camera.right"]

CAMERA_SOURCE_PATHS: Final[dict[CameraSourceId, str]] = {
    "sensor.camera.left": "/live/chn_left_main",
    "sensor.camera.right": "/live/chn_right_main",
}
CAMERA_SOURCE_LABELS: Final[dict[CameraSourceId, str]] = {
    "sensor.camera.left": "K1 · камера слева",
    "sensor.camera.right": "K1 · камера справа",
}
CAMERA_MEDIA_TYPE: Final = 'video/mp4; codecs="avc1.641028"'
CAMERA_EXCLUSIVE_GROUP: Final = "camera.preview.decoder"
MAX_FMP4_BOX_BYTES: Final = 8 * 1024 * 1024
MAX_FMP4_SEGMENT_BYTES: Final = 1024 * 1024
MAX_QUEUED_SEGMENTS: Final = 4
CAMERA_DRAIN_TIMEOUT_SECONDS: Final = 5.0


@dataclass
class CameraProcessLease:
    generation: int
    source_id: CameraSourceId
    process: subprocess.Popen[bytes]
    stderr_tail: deque[str]
    segments: queue.Queue[tuple[str, bytes] | None] = field(
        default_factory=lambda: queue.Queue(maxsize=MAX_QUEUED_SEGMENTS)
    )
    failure_code: str | None = None


@dataclass(frozen=True, slots=True)
class CommittedCameraSegment:
    """A camera fragment observed only after its raw archive commit."""

    source_id: CameraSourceId
    generation: int
    kind: CameraArchiveKind
    sequence: int
    host_epoch_ns: int
    host_monotonic_ns: int
    payload: bytes


CommittedCameraSegmentObserver = Callable[[CommittedCameraSegment], None]


@dataclass
class _CameraProducer:
    generation: int
    source_id: CameraSourceId
    process: subprocess.Popen[bytes]
    archive: CameraArchiveWriter | None
    stderr_tail: deque[str] = field(default_factory=lambda: deque(maxlen=12))
    delivery: CameraProcessLease | None = None
    init_segment: bytes | None = None
    failure_code: str | None = None
    stop_requested: bool = False
    drain_requested: bool = False
    reader_started: bool = False
    reader_done: threading.Event = field(default_factory=threading.Event)


class XgridsK1CameraGateway:
    """One fail-closed K1 producer with independent archive and preview planes.

    During an acquisition the gateway, rather than a browser WebSocket, owns
    FFmpeg. Every complete fMP4 segment is durably appended before it can enter
    the bounded preview queue. Attaching, dropping, or disconnecting a browser
    therefore cannot stop or back-pressure the source-of-record camera stream.
    Outside an acquisition the legacy lazy-preview lifecycle remains available.
    """

    def __init__(
        self,
        repository_root: Path,
        plugin_id: str,
        *,
        committed_segment_observer: CommittedCameraSegmentObserver | None = None,
    ) -> None:
        self._repository_root = repository_root.resolve()
        self._plugin_id = plugin_id
        self._lock = threading.RLock()
        self._lifecycle_lock = threading.Lock()
        self._revision = 0
        self._generation = 0
        self._phase = "idle"
        self._source_id: CameraSourceId | None = None
        self._target_host: str | None = None
        self._producer: _CameraProducer | None = None
        self._recording_root: Path | None = None
        self._expected_source_end_generation: int | None = None
        self._archive_summaries: list[dict[str, Any]] = []
        self._error: dict[str, str] | None = None
        self._closed = False
        self._committed_segment_observer = committed_segment_observer
        self._committed_segment_observer_errors = 0
        self._ffmpeg_path, self._ffmpeg_source = _resolve_ffmpeg(self._repository_root)

    def snapshot(self) -> dict[str, Any]:
        with self._lock:
            delivery = None
            if (
                self._source_id is not None
                and self._ffmpeg_path is not None
                and self._phase != "error"
            ):
                delivery = {
                    "id": f"camera-preview-{self._generation}",
                    "kind": "mse-fmp4-websocket",
                    "url": (
                        f"/api/v1/device-plugins/{self._plugin_id}"
                        f"/camera-preview/{self._generation}"
                    ),
                    "media_type": CAMERA_MEDIA_TYPE,
                }
            return {
                "schema_version": "missioncore.camera-preview/v1alpha1",
                "phase": self._phase,
                "revision": self._revision,
                "generation": self._generation if self._source_id is not None else None,
                "active_source_id": self._source_id,
                "activation": {
                    "exclusive_group": CAMERA_EXCLUSIVE_GROUP,
                    "max_active": 1,
                },
                "recording": {
                    "active": self._recording_root is not None,
                    "source_end_expected": (
                        self._expected_source_end_generation is not None
                    ),
                    "session": (
                        self._recording_root.name if self._recording_root is not None else None
                    ),
                    "active_epoch": (
                        self._producer.generation
                        if self._producer is not None and self._producer.archive is not None
                        else None
                    ),
                    "completed_epochs": len(self._archive_summaries),
                    "last_summary": (
                        dict(self._archive_summaries[-1]) if self._archive_summaries else None
                    ),
                },
                "delivery": delivery,
                "runtime_dependency": {
                    "kind": "ffmpeg",
                    "status": "available" if self._ffmpeg_path is not None else "missing",
                    "source": self._ffmpeg_source,
                },
                "error": dict(self._error) if self._error is not None else None,
                "derived_observer_errors": self._committed_segment_observer_errors,
            }

    def select(self, source_id: CameraSourceId, target_host: str) -> dict[str, Any]:
        if source_id not in CAMERA_SOURCE_PATHS:
            raise ValueError("неизвестный camera source")
        target = validate_private_ipv4(target_host)
        with self._lifecycle_lock:
            with self._lock:
                self._require_open_locked()
                if self._ffmpeg_path is None:
                    self._generation += 1
                    self._revision += 1
                    self._source_id = source_id
                    self._target_host = target
                    self._set_error_locked(
                        "ffmpeg-unavailable",
                        "Локальный camera adapter FFmpeg не найден.",
                    )
                    raise RuntimeError("локальный camera adapter FFmpeg не найден")
                if (
                    self._source_id == source_id
                    and self._target_host == target
                    and self._phase in {"selected", "connecting", "streaming"}
                ):
                    return self.snapshot()

                old_producer, old_delivery = self._detach_producer_locked()
                self._generation += 1
                self._revision += 1
                self._source_id = source_id
                self._target_host = target
                self._expected_source_end_generation = None
                self._phase = "selected"
                self._error = None
                recording_active = self._recording_root is not None

            self._shutdown_producer(
                old_producer,
                old_delivery,
                status="complete",
                failure_code="source-switch",
            )
            if recording_active:
                self._spawn_selected_producer()
            return self.snapshot()

    def stop(self, generation: int) -> dict[str, Any]:
        with self._lifecycle_lock:
            with self._lock:
                if self._source_id is None:
                    return self.snapshot()
                if generation != self._generation:
                    raise ValueError("camera preview generation устарело")
                producer, delivery = self._detach_producer_locked()
                self._revision += 1
                self._phase = "idle"
                self._source_id = None
                self._target_host = None
                self._error = None
            self._shutdown_producer(
                producer,
                delivery,
                status="complete",
                failure_code="source-stopped",
            )
            return self.snapshot()

    def stop_current(self) -> dict[str, Any]:
        with self._lifecycle_lock:
            with self._lock:
                producer, delivery = self._detach_producer_locked()
                changed = self._source_id is not None or self._phase != "idle"
                if changed:
                    self._revision += 1
                self._phase = "idle"
                self._source_id = None
                self._target_host = None
                self._recording_root = None
                self._expected_source_end_generation = None
                self._error = None
            self._shutdown_producer(
                producer,
                delivery,
                status="interrupted",
                failure_code="gateway-stop",
            )
            return self.snapshot()

    def start_recording(self, session_dir: Path) -> dict[str, Any]:
        """Make an existing observation session the camera recording root."""

        root = session_dir.expanduser().resolve()
        if not root.is_dir():
            raise ValueError("observation session directory does not exist")
        if not root.is_relative_to(self._repository_root):
            raise ValueError("camera recording root must stay inside the repository")

        with self._lifecycle_lock:
            with self._lock:
                self._require_open_locked()
                if self._recording_root is not None:
                    if self._recording_root == root:
                        return self.snapshot()
                    raise RuntimeError("для camera gateway уже активна другая acquisition-сессия")
                producer, delivery = self._detach_producer_locked()
                self._recording_root = root
                self._expected_source_end_generation = None
                self._archive_summaries = []
                selected = self._source_id is not None
                if selected:
                    self._phase = "selected"
                self._revision += 1

            # A pre-acquisition browser-owned process cannot become evidence
            # retrospectively. Restart it at a clean codec epoch instead.
            self._shutdown_producer(
                producer,
                delivery,
                status="interrupted",
                failure_code="recording-start-restart",
            )
            if selected:
                self._spawn_selected_producer()
            return self.snapshot()

    def expect_source_end_for_device_stop(self) -> dict[str, Any]:
        """Classify one clean FFmpeg EOF as the camera tail of canonical STOP."""

        with self._lock:
            producer = self._producer
            if (
                self._recording_root is not None
                and producer is not None
                and producer.archive is not None
            ):
                self._expected_source_end_generation = producer.generation
                self._revision += 1
            return self.snapshot()

    def cancel_expected_source_end(self) -> dict[str, Any]:
        """Revoke a STOP expectation when the control checkpoint was not released."""

        with self._lock:
            if self._expected_source_end_generation is not None:
                self._expected_source_end_generation = None
                self._revision += 1
            return self.snapshot()

    def stop_recording(
        self,
        *,
        status: CameraArchiveStatus = "complete",
        failure_code: str | None = None,
    ) -> dict[str, Any]:
        """Stop the acquisition-owned producer and seal its active epoch."""

        with self._lifecycle_lock:
            with self._lock:
                producer, delivery = self._detach_producer_locked()
                recording_was_active = self._recording_root is not None
                self._recording_root = None
                self._expected_source_end_generation = None
                if self._source_id is not None and self._phase != "error":
                    self._phase = "selected"
                if recording_was_active or producer is not None:
                    self._revision += 1
            self._shutdown_producer(
                producer,
                delivery,
                status=status,
                failure_code=failure_code,
            )
            return self.snapshot()

    def open_delivery(self, generation: int) -> CameraProcessLease:
        with self._lifecycle_lock:
            with self._lock:
                self._require_open_locked()
                if generation != self._generation or self._source_id is None:
                    raise ValueError("camera preview generation не активно")
                producer = self._producer
            if producer is None:
                producer = self._spawn_selected_producer()
            with self._lock:
                if self._producer is not producer or producer.generation != generation:
                    raise ValueError("camera preview generation не активно")
                if producer.delivery is not None:
                    raise RuntimeError("для camera preview уже открыт browser consumer")
                lease = CameraProcessLease(
                    generation=producer.generation,
                    source_id=producer.source_id,
                    process=producer.process,
                    stderr_tail=producer.stderr_tail,
                )
                producer.delivery = lease
                if producer.init_segment is not None:
                    lease.segments.put_nowait(("init", producer.init_segment))
                self._revision += 1
                return lease

    def mark_streaming(self, lease: CameraProcessLease) -> None:
        with self._lock:
            producer = self._producer
            if producer is None or producer.delivery is not lease:
                return
            self._mark_streaming_locked(producer)

    def release_delivery(self, lease: CameraProcessLease, *, client_closed: bool) -> None:
        producer_to_stop: _CameraProducer | None = None
        with self._lifecycle_lock:
            with self._lock:
                producer = self._producer
                if producer is None or producer.delivery is not lease:
                    return
                producer.delivery = None
                self._revision += 1
                # During acquisition the browser is a disposable observer. In
                # legacy preview-only mode retain the old lazy-owner behavior.
                if self._recording_root is None:
                    producer.stop_requested = True
                    self._producer = None
                    producer_to_stop = producer
                    self._phase = "selected"
                    self._error = None
            if producer_to_stop is not None:
                self._shutdown_producer(
                    producer_to_stop,
                    None,
                    status="interrupted",
                    failure_code=(lease.failure_code or "browser-disconnected"),
                )

    def close(self) -> None:
        with self._lifecycle_lock:
            with self._lock:
                if self._closed:
                    return
                self._closed = True
                producer, delivery = self._detach_producer_locked()
                self._revision += 1
                self._phase = "idle"
                self._source_id = None
                self._target_host = None
                self._recording_root = None
                self._expected_source_end_generation = None
                self._error = None
            self._shutdown_producer(
                producer,
                delivery,
                status="interrupted",
                failure_code="gateway-closed",
            )

    def _spawn_selected_producer(self) -> _CameraProducer:
        with self._lock:
            self._require_open_locked()
            source_id = self._source_id
            target_host = self._target_host
            ffmpeg_path = self._ffmpeg_path
            generation = self._generation
            recording_root = self._recording_root
            if source_id is None or target_host is None or ffmpeg_path is None:
                raise RuntimeError("camera preview runtime не готов")
            if self._producer is not None:
                return self._producer

        archive: CameraArchiveWriter | None = None
        if recording_root is not None:
            try:
                archive = CameraArchiveWriter(recording_root, source_id, generation)
            except (OSError, ValueError, CameraArchiveError) as exc:
                with self._lock:
                    self._set_error_locked(
                        "camera-storage-failed",
                        "Не удалось открыть долговременное хранилище camera stream.",
                    )
                raise RuntimeError(
                    "Не удалось открыть долговременное хранилище camera stream."
                ) from exc
        try:
            process = subprocess.Popen(
                _build_ffmpeg_argv(ffmpeg_path, target_host, source_id),
                stdin=subprocess.DEVNULL,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                shell=False,
                start_new_session=(os.name == "posix"),
            )
        except (OSError, ValueError) as exc:
            if archive is not None:
                with suppress(CameraArchiveError):
                    self._record_archive_summary(
                        archive.close(status="failed", failure_code="ffmpeg-start-failed")
                    )
            with self._lock:
                self._set_error_locked(
                    "ffmpeg-start-failed",
                    "Не удалось запустить локальный camera adapter.",
                )
            raise RuntimeError("Не удалось запустить локальный camera adapter.") from exc

        if process.stdout is None or process.stderr is None:
            _terminate_process(process)
            if archive is not None:
                self._record_archive_summary(
                    archive.close(status="failed", failure_code="ffmpeg-pipes-unavailable")
                )
            with self._lock:
                self._set_error_locked(
                    "ffmpeg-pipes-unavailable",
                    "Camera adapter не открыл media pipes.",
                )
            raise RuntimeError("camera adapter не открыл media pipes")

        producer = _CameraProducer(
            generation=generation,
            source_id=source_id,
            process=process,
            archive=archive,
        )
        with self._lock:
            if (
                self._closed
                or generation != self._generation
                or source_id != self._source_id
                or target_host != self._target_host
            ):
                producer.stop_requested = True
                stale = True
            else:
                self._producer = producer
                self._revision += 1
                self._phase = "connecting"
                self._error = None
                stale = False
        if stale:
            self._shutdown_producer(
                producer,
                None,
                status="interrupted",
                failure_code="stale-generation",
            )
            raise RuntimeError("camera generation изменилась во время запуска adapter")

        threading.Thread(
            target=_drain_stderr,
            args=(producer,),
            name=f"k1-camera-stderr-{generation}",
            daemon=True,
        ).start()
        producer.reader_started = True
        threading.Thread(
            target=_read_fmp4_stdout,
            args=(self, producer),
            name=f"k1-camera-fmp4-{generation}",
            daemon=True,
        ).start()
        return producer

    def _publish_segment(
        self,
        producer: _CameraProducer,
        kind: CameraArchiveKind,
        payload: bytes,
    ) -> bool:
        if len(payload) > MAX_FMP4_SEGMENT_BYTES:
            self._mark_producer_failure(
                producer,
                "segment-too-large",
                "Camera adapter отклонил слишком большой video segment.",
            )
            return False

        with self._lock:
            producer_owned = self._producer is producer or (
                producer.drain_requested and producer.archive is not None
            )
            if not producer_owned or producer.stop_requested:
                return False
            archive = producer.archive
        if archive is not None:
            try:
                # Source of record first; preview is always expendable.
                committed = archive.append(kind, payload)
            except (CameraArchiveError, OSError, ValueError):
                self._mark_producer_failure(
                    producer,
                    "camera-storage-failed",
                    "Долговременная запись camera stream завершилась ошибкой.",
                )
                return False
            observer = self._committed_segment_observer
            if observer is not None:
                try:
                    observer(
                        CommittedCameraSegment(
                            source_id=producer.source_id,
                            generation=producer.generation,
                            kind=kind,
                            sequence=int(committed["sequence"]),
                            host_epoch_ns=int(committed["host_epoch_ns"]),
                            host_monotonic_ns=int(committed["host_monotonic_ns"]),
                            payload=payload,
                        )
                    )
                except Exception:
                    # Derived diagnostics can be dropped, but can never fail or
                    # back-pressure the authoritative camera recording.
                    with self._lock:
                        self._committed_segment_observer_errors += 1
                        self._revision += 1

        with self._lock:
            producer_owned = self._producer is producer or (
                producer.drain_requested and producer.archive is not None
            )
            if not producer_owned or producer.stop_requested:
                return False
            if kind == "init":
                producer.init_segment = payload
            else:
                self._mark_streaming_locked(producer)
            delivery = producer.delivery
        if delivery is None:
            return True
        try:
            delivery.segments.put_nowait((kind, payload))
        except queue.Full:
            self._drop_slow_delivery(producer, delivery)
        return True

    def _drop_slow_delivery(
        self,
        producer: _CameraProducer,
        delivery: CameraProcessLease,
    ) -> None:
        with self._lock:
            if self._producer is producer and producer.delivery is delivery:
                producer.delivery = None
                delivery.failure_code = "consumer-too-slow"
                self._revision += 1
        _close_segment_queue(delivery)

    def _mark_producer_failure(
        self,
        producer: _CameraProducer,
        code: str,
        message: str,
    ) -> None:
        with self._lock:
            producer_owned = self._producer is producer or producer.drain_requested
            if not producer_owned or producer.stop_requested:
                return
            producer.failure_code = code
            self._set_error_locked(code, message)
        with suppress(OSError):
            producer.process.terminate()

    def _producer_ended(self, producer: _CameraProducer) -> None:
        with self._lock:
            delivery = producer.delivery
            producer.delivery = None
            owns_shutdown = self._producer is producer and not producer.stop_requested
            expected_clean_end = (
                owns_shutdown
                and producer.failure_code is None
                and self._expected_source_end_generation == producer.generation
            )
            if owns_shutdown:
                self._producer = None
                self._expected_source_end_generation = None
                self._revision += 1
                if expected_clean_end:
                    self._phase = "idle"
                    self._source_id = None
                    self._target_host = None
                    self._error = None
                elif producer.failure_code is None:
                    producer.failure_code = "camera-source-ended"
                    self._set_error_locked(
                        "camera-source-ended",
                        _safe_ffmpeg_message(producer.stderr_tail),
                    )
        if delivery is not None:
            delivery.failure_code = (
                None
                if expected_clean_end
                else producer.failure_code or "camera-source-ended"
            )
            _close_segment_queue(delivery)
        if not owns_shutdown:
            return
        _terminate_process(producer.process)
        status: CameraArchiveStatus = (
            "complete"
            if expected_clean_end
            else "interrupted"
            if producer.failure_code in {"camera-source-ended", "incomplete-fmp4-fragment"}
            else "failed"
        )
        try:
            self._finalize_archive(
                producer,
                status,
                None if expected_clean_end else producer.failure_code,
            )
        except CameraArchiveError:
            with self._lock:
                self._set_error_locked(
                    "camera-storage-finalize-failed",
                    "Не удалось завершить долговременную запись camera stream.",
                )

    def _shutdown_producer(
        self,
        producer: _CameraProducer | None,
        delivery: CameraProcessLease | None,
        *,
        status: CameraArchiveStatus,
        failure_code: str | None,
    ) -> None:
        if delivery is not None:
            delivery.failure_code = producer.failure_code if producer is not None else failure_code
            _close_segment_queue(delivery)
        if producer is None:
            return
        _terminate_process(producer.process, close_streams=False)
        drained = (
            not producer.reader_started
            or producer.reader_done.wait(timeout=CAMERA_DRAIN_TIMEOUT_SECONDS)
        )
        _close_process_streams(producer.process)
        if not drained:
            producer.failure_code = "camera-drain-timeout"
            with self._lock:
                self._set_error_locked(
                    "camera-drain-timeout",
                    "Camera adapter не завершил долговременную запись вовремя.",
                )
        effective_failure_code = producer.failure_code or failure_code
        effective_status: CameraArchiveStatus = (
            "interrupted"
            if producer.failure_code == "incomplete-fmp4-fragment"
            else "failed"
            if producer.failure_code is not None
            else status
        )
        if delivery is not None:
            delivery.failure_code = effective_failure_code
        try:
            self._finalize_archive(
                producer,
                effective_status,
                effective_failure_code,
            )
        except CameraArchiveError:
            with self._lock:
                self._set_error_locked(
                    "camera-storage-finalize-failed",
                    "Не удалось завершить долговременную запись camera stream.",
                )
            raise

    def _finalize_archive(
        self,
        producer: _CameraProducer,
        status: CameraArchiveStatus,
        failure_code: str | None,
    ) -> None:
        archive = producer.archive
        producer.archive = None
        if archive is None:
            return
        self._record_archive_summary(archive.close(status=status, failure_code=failure_code))

    def _record_archive_summary(self, summary: dict[str, Any]) -> None:
        with self._lock:
            self._archive_summaries.append(dict(summary))

    def _detach_producer_locked(
        self,
    ) -> tuple[_CameraProducer | None, CameraProcessLease | None]:
        producer = self._producer
        self._producer = None
        if producer is None:
            return None, None
        producer.drain_requested = producer.archive is not None
        producer.stop_requested = not producer.drain_requested
        delivery = producer.delivery
        producer.delivery = None
        return producer, delivery

    def _mark_streaming_locked(self, producer: _CameraProducer) -> None:
        if self._producer is producer and self._phase != "streaming":
            self._revision += 1
            self._phase = "streaming"

    def _set_error_locked(self, code: str, message: str) -> None:
        self._revision += 1
        self._phase = "error"
        self._error = {"code": code, "message": message}

    def _require_open_locked(self) -> None:
        if self._closed:
            raise RuntimeError("camera gateway уже закрыт")


def _resolve_ffmpeg(repository_root: Path) -> tuple[Path | None, str]:
    configured = os.environ.get("MISSIONCORE_FFMPEG_BINARY")
    candidates: list[tuple[Path, str]] = []
    if configured:
        candidates.append((Path(configured).expanduser(), "configured"))
    candidates.append((repository_root / ".runtime" / "ffmpeg" / "ffmpeg", "bundled-local"))
    # Explicit development-only fallbacks. Product packaging must provide the
    # repository-local binary or MISSIONCORE_FFMPEG_BINARY instead.
    candidates.extend(
        (
            (Path("/opt/homebrew/bin/ffmpeg"), "development-system"),
            (Path("/usr/local/bin/ffmpeg"), "development-system"),
        )
    )
    for candidate, source in candidates:
        try:
            resolved = candidate.resolve(strict=True)
        except OSError:
            continue
        if resolved.is_file() and os.access(resolved, os.X_OK):
            return resolved, source
    return None, "missing"


def _build_ffmpeg_argv(
    ffmpeg_path: Path,
    target_host: str,
    source_id: CameraSourceId,
) -> list[str]:
    target = validate_private_ipv4(target_host)
    path = CAMERA_SOURCE_PATHS.get(source_id)
    if path is None:
        raise ValueError("неизвестный camera source")
    upstream = f"rtsp://{target}:8554{path}"
    return [
        str(ffmpeg_path),
        "-hide_banner",
        "-loglevel",
        "warning",
        "-nostdin",
        "-rtsp_transport",
        "tcp",
        "-allowed_media_types",
        "video",
        "-timeout",
        "5000000",
        "-probesize",
        "4000000",
        "-analyzeduration",
        "2000000",
        "-fflags",
        "nobuffer",
        "-i",
        upstream,
        "-map",
        "0:v:0",
        "-an",
        "-sn",
        "-dn",
        "-c:v",
        "copy",
        "-f",
        "mp4",
        "-movflags",
        "+empty_moov+default_base_moof+omit_tfhd_offset+frag_every_frame+skip_trailer",
        "-flush_packets",
        "1",
        "pipe:1",
    ]


def _read_exact(stream: IO[bytes], size: int) -> bytes:
    chunks: list[bytes] = []
    remaining = size
    while remaining:
        chunk = stream.read(remaining)
        if not chunk:
            raise EOFError
        chunks.append(chunk)
        remaining -= len(chunk)
    return b"".join(chunks)


def _read_mp4_box(stream: IO[bytes]) -> tuple[bytes, bytes]:
    header = _read_exact(stream, 8)
    size = int.from_bytes(header[:4], "big")
    box_type = header[4:8]
    if size == 1:
        extended = _read_exact(stream, 8)
        size = int.from_bytes(extended, "big")
        header += extended
    if size == 0 or size < len(header) or size > MAX_FMP4_BOX_BYTES:
        raise ValueError("invalid or unbounded ISO-BMFF box")
    return box_type, header + _read_exact(stream, size - len(header))


def _close_segment_queue(lease: CameraProcessLease) -> None:
    try:
        lease.segments.put_nowait(None)
    except queue.Full:
        while True:
            try:
                lease.segments.get_nowait()
            except queue.Empty:
                break
        lease.segments.put_nowait(None)


def _read_fmp4_stdout(
    gateway: XgridsK1CameraGateway,
    producer: _CameraProducer,
) -> None:
    stream = producer.process.stdout
    if stream is None:
        try:
            gateway._mark_producer_failure(
                producer,
                "invalid-fmp4",
                "Camera adapter вернул некорректный fMP4 stream.",
            )
            gateway._producer_ended(producer)
        finally:
            producer.reader_done.set()
        return

    init_parts: list[bytes] = []
    fragment_parts: list[bytes] = []
    init_sent = False
    try:
        while True:
            box_type, box = _read_mp4_box(stream)
            if not init_sent:
                init_parts.append(box)
                if box_type == b"moov":
                    if not gateway._publish_segment(
                        producer,
                        "init",
                        b"".join(init_parts),
                    ):
                        return
                    init_sent = True
                continue

            if box_type == b"moof":
                fragment_parts = [box]
                continue
            if fragment_parts:
                fragment_parts.append(box)
                if box_type == b"mdat":
                    if not gateway._publish_segment(
                        producer,
                        "media",
                        b"".join(fragment_parts),
                    ):
                        return
                    fragment_parts = []
                continue
            # `styp`/`sidx` are valid media-segment prefixes. Preserve them and
            # wait for the following moof+mdat instead of forwarding raw boxes.
            if box_type in {b"styp", b"sidx"}:
                fragment_parts.append(box)
    except EOFError:
        if not init_sent:
            gateway._mark_producer_failure(
                producer,
                "invalid-fmp4",
                "Camera adapter вернул некорректный fMP4 stream.",
            )
        elif fragment_parts:
            gateway._mark_producer_failure(
                producer,
                "incomplete-fmp4-fragment",
                "Camera stream завершился на неполном video fragment.",
            )
    except (OSError, ValueError):
        gateway._mark_producer_failure(
            producer,
            "invalid-fmp4",
            "Camera adapter вернул некорректный fMP4 stream.",
        )
    finally:
        try:
            gateway._producer_ended(producer)
        finally:
            producer.reader_done.set()


def _drain_stderr(producer: _CameraProducer) -> None:
    stream = producer.process.stderr
    if stream is None:
        return
    try:
        while True:
            line = stream.readline()
            if not line:
                return
            text = line.decode("utf-8", errors="replace").strip()
            if text:
                producer.stderr_tail.append(text[-240:])
    except (OSError, ValueError):
        return


def _safe_ffmpeg_message(stderr_tail: deque[str]) -> str:
    # Never reflect the RTSP URL or device address into browser state.
    joined = " ".join(stderr_tail).lower()
    if "connection refused" in joined:
        return "Camera endpoint отклонил локальное соединение."
    if "timed out" in joined or "timeout" in joined:
        return "Camera endpoint не ответил за отведённое время."
    if "invalid data" in joined or "could not find codec" in joined:
        return "Camera endpoint вернул неподдерживаемый media stream."
    return "Camera stream завершился до первого пригодного видеофрагмента."


def _terminate_process(
    process: subprocess.Popen[bytes] | None,
    *,
    close_streams: bool = True,
) -> None:
    if process is None:
        return
    try:
        if process.poll() is None:
            if os.name == "posix":
                # The process can have exited or lost its dedicated process
                # group between poll() and killpg().  Either outcome means
                # there is no group left for us to signal; still reap the
                # child and finish the archive instead of aborting teardown.
                with suppress(OSError):
                    os.killpg(process.pid, signal.SIGINT)
            else:
                process.terminate()
            try:
                process.wait(timeout=2.0)
            except subprocess.TimeoutExpired:
                process.terminate()
                try:
                    process.wait(timeout=1.0)
                except subprocess.TimeoutExpired:
                    process.kill()
                    process.wait(timeout=1.0)
    finally:
        if close_streams:
            _close_process_streams(process)


def _close_process_streams(process: subprocess.Popen[bytes]) -> None:
    for stream in (process.stdout, process.stderr):
        if stream is not None:
            with suppress(OSError):
                stream.close()


def build_xgrids_k1_camera_router(
    gateway: XgridsK1CameraGateway,
    plugin_id: str,
) -> APIRouter:
    router = APIRouter(include_in_schema=False)

    @router.websocket(f"/api/v1/device-plugins/{plugin_id}/camera-preview/{{generation}}")
    async def camera_preview(websocket: WebSocket, generation: int) -> None:
        await websocket.accept()
        try:
            lease = gateway.open_delivery(generation)
        except (ValueError, RuntimeError):
            await websocket.close(code=1008, reason="Camera preview lease is not active")
            return

        client_closed = False
        try:
            while True:
                segment = await asyncio.to_thread(lease.segments.get)
                if segment is None:
                    break
                kind, payload = segment
                if kind == "media":
                    gateway.mark_streaming(lease)
                await websocket.send_bytes(payload)
        except WebSocketDisconnect:
            client_closed = True
        except RuntimeError:
            client_closed = True
        finally:
            gateway.release_delivery(lease, client_closed=client_closed)
            with suppress(RuntimeError):
                await websocket.close()

    return router
