from __future__ import annotations

import hashlib
import json
import math
import os
import shutil
import stat
import subprocess
import threading
import time
from contextlib import suppress
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from uuid import uuid4

import numpy as np
import numpy.typing as npt
import rerun as rr
from PIL import Image, ImageDraw

from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import (
    Kb4ProjectionProfile,
    ProjectedPointCloud,
    depth_colors,
    project_map_points_kb4,
)
from k1link.device_plugins.xgrids_k1.calibration_schema import (
    K1FactoryCalibration,
    parse_k1_factory_calibration,
)
from k1link.device_plugins.xgrids_k1.calibration_snapshot import (
    CALIBRATION_SNAPSHOT_MANIFEST_VERSION,
)
from k1link.device_plugins.xgrids_k1.mqtt.capture import (
    read_capture_clock_envelope,
    read_capture_clock_origin,
)
from k1link.device_plugins.xgrids_k1.protocol.streams import (
    LioPointCloudFrame,
    LioPoseFrame,
    decode_lio_pcl,
    decode_lio_pose,
)
from k1link.device_plugins.xgrids_k1.viewer.replay import iter_replay_messages
from k1link.sessions import inspect_recorded_media_epoch

CALIBRATED_OVERLAY_SCHEMA = "missioncore.k1-calibrated-overlay-experiment/v1"
CALIBRATED_OVERLAY_SUFFIX = "k1_calibrated_overlay"
MAX_JSON_BYTES = 2 * 1024 * 1024
MAX_INDEX_LINE_BYTES = 64 * 1024
MAX_OPERATOR_NOTES_BYTES = 64 * 1024
FFMPEG_TIMEOUT_SECONDS = 180.0
MAX_SYNC_DELTA_SECONDS = 0.25
_READ_CHUNK_BYTES = 1024 * 1024

RgbImage = npt.NDArray[np.uint8]


class CalibratedOverlayExperimentError(RuntimeError):
    """Raised when a recorded calibrated-overlay experiment cannot be sealed."""


@dataclass(frozen=True, slots=True)
class CalibratedOverlayExperiment:
    experiment_id: str
    experiment_root: Path
    manifest_path: Path
    rerun_path: Path
    mosaic_path: Path
    frame_count: int
    source_id: str
    calibration_content_identity: str


@dataclass(frozen=True, slots=True)
class _CameraFrameAnchor:
    requested_video_offset_seconds: float
    sequence: int
    session_time_seconds: float
    host_epoch_ns: int
    host_monotonic_ns: int


@dataclass(frozen=True, slots=True)
class _LidarSample:
    camera: _CameraFrameAnchor
    point_session_time_seconds: float
    pose_session_time_seconds: float
    point_frame: LioPointCloudFrame
    pose_frame: LioPoseFrame


@dataclass(frozen=True, slots=True)
class _RenderedFrame:
    camera: _CameraFrameAnchor
    lidar: _LidarSample
    image_rgb: RgbImage
    overlay_rgb: RgbImage
    projection: ProjectedPointCloud
    colors_rgb: RgbImage
    camera_artifact_name: str
    artifact_name: str


def run_calibrated_overlay_experiment(
    *,
    session_root: Path,
    calibration_snapshot_root: Path,
    source_id: str,
    video_offsets_seconds: tuple[float, ...],
    output_root: Path,
    ffmpeg_path: Path,
    temporal_offset_seconds: float = 0.0,
) -> CalibratedOverlayExperiment:
    """Seal a derived LiDAR→KB4→camera diagnostic without changing native evidence."""

    started_monotonic_ns = time.monotonic_ns()
    session = session_root.expanduser().resolve(strict=True)
    if not session.is_dir() or session.name in {"", ".", ".."}:
        raise CalibratedOverlayExperimentError("session root is invalid")
    if not video_offsets_seconds:
        raise CalibratedOverlayExperimentError("at least one video offset is required")
    if len(video_offsets_seconds) > 16:
        raise CalibratedOverlayExperimentError("at most sixteen diagnostic frames are allowed")
    offsets = tuple(sorted(video_offsets_seconds))
    if len(set(offsets)) != len(offsets) or any(
        not math.isfinite(value) or value < 0.0 for value in offsets
    ):
        raise CalibratedOverlayExperimentError(
            "video offsets must be unique finite non-negative seconds"
        )
    if not math.isfinite(temporal_offset_seconds) or abs(temporal_offset_seconds) > 5.0:
        raise CalibratedOverlayExperimentError(
            "temporal offset must be finite and inside the reviewed ±5 second window"
        )

    calibration, calibration_identity = _load_calibration_snapshot(
        calibration_snapshot_root
    )
    profile = Kb4ProjectionProfile.from_factory_calibration(calibration, source_id)
    capture_root = session / "captures" / "mqtt_live"
    raw_path = capture_root / "mqtt.raw.k1mqtt"
    metadata_path = capture_root / "mqtt.metadata.jsonl"
    summary_path = capture_root / "mqtt.summary.json"
    clock_origin_path = capture_root / "mqtt.timeline.origin.json"
    clock_summary = _read_json(summary_path, MAX_JSON_BYTES)
    clock_name = _required_text(
        _required_mapping(clock_summary.get("artifacts"), "artifacts").get(
            "capture_clock"
        ),
        "capture clock filename",
    )
    clock_path = capture_root / clock_name
    capture_clock = read_capture_clock_envelope(clock_path)
    capture_origin = read_capture_clock_origin(clock_origin_path)
    if (
        capture_clock.started_at_epoch_ns != capture_origin.started_at_epoch_ns
        or capture_clock.started_monotonic_ns != capture_origin.started_monotonic_ns
    ):
        raise CalibratedOverlayExperimentError("MQTT capture clock does not match its origin")
    if not raw_path.is_file() or not metadata_path.is_file():
        raise CalibratedOverlayExperimentError("sealed MQTT evidence is incomplete")

    camera_epoch_root = session / "media" / source_id / "epoch-1"
    inspected_epoch = inspect_recorded_media_epoch(
        camera_epoch_root,
        expected_source_name=source_id,
        origin_epoch_ns=capture_origin.started_at_epoch_ns,
        origin_monotonic_ns=capture_origin.started_monotonic_ns,
    )
    camera_duration_seconds = (
        inspected_epoch.timeline_end_seconds - inspected_epoch.timeline_start_seconds
    )
    if offsets[-1] >= camera_duration_seconds:
        raise CalibratedOverlayExperimentError("a video offset is outside the camera epoch")
    camera_anchors = _select_camera_anchors(
        camera_epoch_root / "index.jsonl",
        expected_count=len(inspected_epoch.segments),
        offsets_seconds=offsets,
        timeline_start_seconds=inspected_epoch.timeline_start_seconds,
        origin_monotonic_ns=capture_origin.started_monotonic_ns,
    )
    camera_images = _extract_camera_frames(
        ffmpeg_path.expanduser().resolve(strict=True),
        init_path=inspected_epoch.init_path,
        segment_paths=tuple(segment.path for segment in inspected_epoch.segments),
        frame_sequences=tuple(anchor.sequence for anchor in camera_anchors),
        width=profile.width,
        height=profile.height,
    )
    lidar_samples = _select_lidar_samples(
        raw_path,
        camera_anchors,
        origin_monotonic_ns=capture_origin.started_monotonic_ns,
        temporal_offset_seconds=temporal_offset_seconds,
    )

    created_at = datetime.now(UTC)
    experiment_uuid = uuid4().hex
    experiment_id = (
        f"{created_at.strftime('%Y%m%dT%H%M%SZ')}_{CALIBRATED_OVERLAY_SUFFIX}_"
        f"{experiment_uuid[:12]}"
    )
    private_root = output_root.expanduser().resolve() / "private" / "perception-experiments"
    private_root.mkdir(mode=0o700, parents=True, exist_ok=True)
    _chmod_private(private_root)
    final_root = private_root / experiment_id
    staging_root = private_root / f".{experiment_id}.incomplete"
    staging_root.mkdir(mode=0o700, exist_ok=False)
    published = False
    try:
        rendered: list[_RenderedFrame] = []
        for image_rgb, camera_anchor, lidar_sample in zip(
            camera_images,
            camera_anchors,
            lidar_samples,
            strict=True,
        ):
            positions = np.asarray(
                [
                    point.scaled_xyz(lidar_sample.point_frame.header.scaler)
                    for point in lidar_sample.point_frame.points
                ],
                dtype=np.float64,
            )
            projection = project_map_points_kb4(
                positions,
                position_map_xyz=lidar_sample.pose_frame.position_xyz,
                orientation_map_from_lidar_xyzw=(
                    lidar_sample.pose_frame.orientation_xyzw
                ),
                profile=profile,
            )
            if projection.projected_point_count == 0:
                raise CalibratedOverlayExperimentError(
                    f"camera frame {camera_anchor.sequence} has no projected LiDAR points"
                )
            colors = depth_colors(projection.depths_m)
            overlay = _render_overlay(
                image_rgb,
                projection,
                colors,
                camera_anchor=camera_anchor,
                lidar_sample=lidar_sample,
            )
            artifact_name = (
                f"frame-{camera_anchor.sequence:06d}-"
                f"session-{round(camera_anchor.session_time_seconds * 1000):09d}ms.png"
            )
            camera_artifact_name = (
                f"camera-{camera_anchor.sequence:06d}-"
                f"session-{round(camera_anchor.session_time_seconds * 1000):09d}ms.png"
            )
            _write_image_exclusive(staging_root / camera_artifact_name, image_rgb)
            _write_image_exclusive(staging_root / artifact_name, overlay)
            rendered.append(
                _RenderedFrame(
                    camera=camera_anchor,
                    lidar=lidar_sample,
                    image_rgb=image_rgb,
                    overlay_rgb=overlay,
                    projection=projection,
                    colors_rgb=colors,
                    camera_artifact_name=camera_artifact_name,
                    artifact_name=artifact_name,
                )
            )

        mosaic_path = staging_root / "overlay-mosaic.png"
        _write_image_exclusive(mosaic_path, _mosaic(tuple(rendered)))
        rerun_path = staging_root / "diagnostic.rrd"
        _write_rerun_diagnostic(
            rerun_path,
            experiment_id=experiment_id,
            rendered=tuple(rendered),
            calibration_identity=calibration_identity,
            profile=profile,
        )
        operator_notes = _operator_notes(
            experiment_id=experiment_id,
            session_id=session.name,
            source_id=source_id,
            calibration_identity=calibration_identity,
            temporal_offset_seconds=temporal_offset_seconds,
        )
        if len(operator_notes) > MAX_OPERATOR_NOTES_BYTES:
            raise CalibratedOverlayExperimentError("operator notes exceed their byte bound")
        _write_exclusive(staging_root / "operator-notes.md", operator_notes)

        input_identity = {
            "session_id": session.name,
            "source_id": source_id,
            "raw_mqtt_sha256": _verified_raw_sha256(raw_path, clock_summary),
            "camera_stream_sha256": _required_text(
                _read_json(camera_epoch_root / "summary.json", MAX_JSON_BYTES).get(
                    "stream_sha256"
                ),
                "camera stream sha256",
            ),
            "calibration_content_identity_sha256": calibration_identity,
            "video_offsets_seconds": list(offsets),
            "temporal_offset_seconds": temporal_offset_seconds,
            "projection_contract": "map→inverse(T_map_from_lidar)→T_camera_1_from_lidar→KB4",
        }
        generation_sha256 = hashlib.sha256(_canonical_json(input_identity)).hexdigest()
        frame_documents = [_frame_document(item) for item in rendered]
        output_names = [
            *(item.camera_artifact_name for item in rendered),
            *(item.artifact_name for item in rendered),
            mosaic_path.name,
            rerun_path.name,
            "operator-notes.md",
        ]
        manifest = {
            "schema_version": CALIBRATED_OVERLAY_SCHEMA,
            "experiment_id": experiment_id,
            "created_at_utc": created_at.isoformat(timespec="milliseconds").replace(
                "+00:00", "Z"
            ),
            "completed_monotonic_ns": time.monotonic_ns(),
            "elapsed_seconds": round(
                (time.monotonic_ns() - started_monotonic_ns) / 1_000_000_000,
                6,
            ),
            "classification": "private-derived-calibration-diagnostic",
            "generation_sha256": generation_sha256,
            "input": input_identity,
            "camera_epoch": {
                "codec_epoch": 1,
                "resolution": [profile.width, profile.height],
                "timeline_start_seconds": inspected_epoch.timeline_start_seconds,
                "timeline_end_seconds": inspected_epoch.timeline_end_seconds,
                "synchronization": "host-arrival-best-effort",
            },
            "geometry": {
                "calibration_slot": profile.calibration_slot,
                "camera_model": "kb4",
                "transform_notation": "T_destination_from_source",
                "point_frame": "map",
                "pose_interpretation": "T_map_from_lidar",
                "behind_camera_policy": "reject-z-less-than-or-equal-to-zero",
                "occlusion_policy": "diagnostic-draw-far-to-near",
            },
            "frames": frame_documents,
            "outputs": [_artifact_document(staging_root / name) for name in output_names],
            "acceptance": {
                "geometry_input_subgate": "accepted",
                "diagnostic_layer": "generated",
                "measured_reprojection": "pending-static-landmark-correspondences",
                "temporal_calibration": "unverified-host-arrival-only",
                "p0": "not-yet-accepted",
            },
        }
        _write_exclusive(
            staging_root / "manifest.redacted.json",
            json.dumps(
                manifest,
                ensure_ascii=False,
                indent=2,
                sort_keys=True,
                allow_nan=False,
            ).encode("utf-8")
            + b"\n",
        )
        _fsync_directory(staging_root)
        os.rename(staging_root, final_root)
        _fsync_directory(private_root)
        published = True
    finally:
        if not published and staging_root.exists():
            shutil.rmtree(staging_root)

    return CalibratedOverlayExperiment(
        experiment_id=experiment_id,
        experiment_root=final_root,
        manifest_path=final_root / "manifest.redacted.json",
        rerun_path=final_root / "diagnostic.rrd",
        mosaic_path=final_root / "overlay-mosaic.png",
        frame_count=len(camera_anchors),
        source_id=source_id,
        calibration_content_identity=calibration_identity,
    )


def _load_calibration_snapshot(
    snapshot_root: Path,
) -> tuple[K1FactoryCalibration, str]:
    root = snapshot_root.expanduser().resolve(strict=True)
    if not root.is_dir():
        raise CalibratedOverlayExperimentError("calibration snapshot is not a directory")
    manifest = _read_json(root / "manifest.json", MAX_JSON_BYTES)
    if manifest.get("schema_version") != CALIBRATION_SNAPSHOT_MANIFEST_VERSION:
        raise CalibratedOverlayExperimentError("calibration snapshot schema is incompatible")
    camera_payload = _read_regular(root / "camera.yaml", 64 * 1024)
    extrinsic_payload = _read_regular(root / "extrinsic_camera_lidar.yaml", 64 * 1024)
    artifact_digests = {
        item.get("artifact_name"): item.get("sha256")
        for item in _required_list(manifest.get("artifacts"), "calibration artifacts")
        if isinstance(item, dict)
    }
    expected = {
        "camera.yaml": hashlib.sha256(camera_payload).hexdigest(),
        "extrinsic_camera_lidar.yaml": hashlib.sha256(extrinsic_payload).hexdigest(),
    }
    if artifact_digests != expected:
        raise CalibratedOverlayExperimentError("calibration artifact identity changed")
    calibration = parse_k1_factory_calibration(camera_payload, extrinsic_payload)
    if manifest.get("normalized_calibration") != calibration.normalized_profile():
        raise CalibratedOverlayExperimentError("normalized calibration changed")
    identity = _required_sha256(
        manifest.get("content_identity_sha256"),
        "calibration content identity",
    )
    return calibration, identity


def _select_camera_anchors(
    index_path: Path,
    *,
    expected_count: int,
    offsets_seconds: tuple[float, ...],
    timeline_start_seconds: float,
    origin_monotonic_ns: int,
) -> tuple[_CameraFrameAnchor, ...]:
    targets = tuple(timeline_start_seconds + offset for offset in offsets_seconds)
    best: list[tuple[float, _CameraFrameAnchor] | None] = [None] * len(targets)
    with index_path.open("rb") as stream:
        for expected_sequence in range(1, expected_count + 1):
            line = stream.readline(MAX_INDEX_LINE_BYTES + 1)
            if not line or len(line) > MAX_INDEX_LINE_BYTES or not line.endswith(b"\n"):
                raise CalibratedOverlayExperimentError("camera index is incomplete or unbounded")
            try:
                entry = json.loads(line)
            except json.JSONDecodeError as exc:
                raise CalibratedOverlayExperimentError(
                    "camera index contains invalid JSON"
                ) from exc
            if (
                not isinstance(entry, dict)
                or entry.get("sequence") != expected_sequence
                or entry.get("kind") != "media"
                or entry.get("path") != f"segments/{expected_sequence}.m4s"
            ):
                raise CalibratedOverlayExperimentError("camera index sequence is inconsistent")
            host_monotonic_ns = entry.get("host_monotonic_ns")
            host_epoch_ns = entry.get("host_epoch_ns")
            if (
                not isinstance(host_monotonic_ns, int)
                or host_monotonic_ns < origin_monotonic_ns
                or not isinstance(host_epoch_ns, int)
                or host_epoch_ns < 0
            ):
                raise CalibratedOverlayExperimentError("camera index time is invalid")
            session_time = (host_monotonic_ns - origin_monotonic_ns) / 1_000_000_000
            for index, (offset, target) in enumerate(zip(offsets_seconds, targets, strict=True)):
                delta = abs(session_time - target)
                current = best[index]
                if current is None or delta < current[0]:
                    best[index] = (
                        delta,
                        _CameraFrameAnchor(
                            requested_video_offset_seconds=offset,
                            sequence=expected_sequence,
                            session_time_seconds=session_time,
                            host_epoch_ns=host_epoch_ns,
                            host_monotonic_ns=host_monotonic_ns,
                        ),
                    )
        if stream.read(1):
            raise CalibratedOverlayExperimentError("camera index has undeclared rows")
    anchors_list: list[_CameraFrameAnchor] = []
    for item in best:
        if item is None:
            raise CalibratedOverlayExperimentError("camera offset has no frame")
        anchors_list.append(item[1])
    anchors = tuple(anchors_list)
    if len({item.sequence for item in anchors}) != len(anchors):
        raise CalibratedOverlayExperimentError("camera offsets do not select unique frames")
    return anchors


def _extract_camera_frames(
    ffmpeg_path: Path,
    *,
    init_path: Path,
    segment_paths: tuple[Path, ...],
    frame_sequences: tuple[int, ...],
    width: int,
    height: int,
) -> tuple[RgbImage, ...]:
    if not ffmpeg_path.is_file():
        raise CalibratedOverlayExperimentError("ffmpeg is unavailable")
    frame_indices = tuple(sequence - 1 for sequence in frame_sequences)
    select = "+".join(f"eq(n\\,{index})" for index in frame_indices)
    read_fd, write_fd = os.pipe()
    process = subprocess.Popen(
        [
            str(ffmpeg_path),
            "-hide_banner",
            "-loglevel",
            "error",
            "-i",
            "pipe:0",
            "-vf",
            f"select={select}",
            "-fps_mode",
            "passthrough",
            "-frames:v",
            str(len(frame_indices)),
            "-f",
            "rawvideo",
            "-pix_fmt",
            "rgb24",
            "pipe:1",
        ],
        stdin=read_fd,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        shell=False,
    )
    os.close(read_fd)
    feeder_errors: list[BaseException] = []

    def feed() -> None:
        try:
            with os.fdopen(write_fd, "wb", buffering=0) as sink:
                for path in (init_path, *segment_paths):
                    with path.open("rb") as source:
                        shutil.copyfileobj(source, sink, length=_READ_CHUNK_BYTES)
        except BrokenPipeError:
            return
        except BaseException as exc:  # pragma: no cover - defensive child-pipe boundary
            feeder_errors.append(exc)

    feeder = threading.Thread(target=feed, name="k1-calibrated-overlay-ffmpeg", daemon=True)
    feeder.start()
    try:
        stdout, stderr = process.communicate(timeout=FFMPEG_TIMEOUT_SECONDS)
    except subprocess.TimeoutExpired as exc:
        process.kill()
        process.communicate()
        raise CalibratedOverlayExperimentError("ffmpeg frame extraction timed out") from exc
    feeder.join(timeout=5.0)
    if feeder.is_alive():
        raise CalibratedOverlayExperimentError("camera archive feeder did not stop")
    if feeder_errors:
        raise CalibratedOverlayExperimentError(
            "camera archive streaming failed"
        ) from feeder_errors[0]
    if process.returncode != 0:
        message = stderr.decode("utf-8", errors="replace").strip()[-1000:]
        raise CalibratedOverlayExperimentError(
            f"ffmpeg rejected the sealed camera epoch: {message}"
        )
    frame_bytes = width * height * 3
    if len(stdout) != frame_bytes * len(frame_indices):
        raise CalibratedOverlayExperimentError("ffmpeg returned an unexpected frame count")
    return tuple(
        np.frombuffer(stdout, dtype=np.uint8, count=frame_bytes, offset=index * frame_bytes)
        .reshape((height, width, 3))
        .copy()
        for index in range(len(frame_indices))
    )


def _select_lidar_samples(
    raw_path: Path,
    camera_anchors: tuple[_CameraFrameAnchor, ...],
    *,
    origin_monotonic_ns: int,
    temporal_offset_seconds: float,
) -> tuple[_LidarSample, ...]:
    targets = tuple(
        anchor.session_time_seconds + temporal_offset_seconds for anchor in camera_anchors
    )
    best_points: list[tuple[float, float, LioPointCloudFrame] | None] = [None] * len(targets)
    nearby_poses: list[list[tuple[float, LioPoseFrame]]] = [[] for _ in targets]
    last_target = max(targets)
    first_target = min(targets)
    for message in iter_replay_messages(raw_path):
        monotonic_ns = message.received_monotonic_ns
        if monotonic_ns is None:
            raise CalibratedOverlayExperimentError("MQTT metadata has no monotonic time")
        session_time = (monotonic_ns - origin_monotonic_ns) / 1_000_000_000
        if session_time < first_target - 1.0:
            continue
        if session_time > last_target + 1.0:
            break
        if message.topic.endswith("/lio_pcl"):
            for index, target in enumerate(targets):
                delta = abs(session_time - target)
                current = best_points[index]
                if delta <= MAX_SYNC_DELTA_SECONDS and (current is None or delta < current[0]):
                    best_points[index] = (delta, session_time, decode_lio_pcl(message.payload))
        elif message.topic.endswith("/lio_pose"):
            for index, target in enumerate(targets):
                if abs(session_time - target) <= 0.5:
                    nearby_poses[index].append((session_time, decode_lio_pose(message.payload)))

    samples: list[_LidarSample] = []
    for index, anchor in enumerate(camera_anchors):
        point = best_points[index]
        if point is None or not nearby_poses[index]:
            raise CalibratedOverlayExperimentError(
                f"camera frame {anchor.sequence} has no temporally compatible LiDAR sample"
            )
        _delta, point_time, point_frame = point
        pose_time, pose_frame = min(
            nearby_poses[index],
            key=lambda item: abs(item[0] - point_time),
        )
        if abs(pose_time - point_time) > MAX_SYNC_DELTA_SECONDS:
            raise CalibratedOverlayExperimentError(
                f"camera frame {anchor.sequence} has no compatible pose"
            )
        samples.append(
            _LidarSample(
                camera=anchor,
                point_session_time_seconds=point_time,
                pose_session_time_seconds=pose_time,
                point_frame=point_frame,
                pose_frame=pose_frame,
            )
        )
    return tuple(samples)


def _render_overlay(
    image_rgb: RgbImage,
    projection: ProjectedPointCloud,
    colors_rgb: RgbImage,
    *,
    camera_anchor: _CameraFrameAnchor,
    lidar_sample: _LidarSample,
) -> RgbImage:
    image = Image.fromarray(image_rgb, mode="RGB").convert("RGBA")
    draw = ImageDraw.Draw(image, "RGBA")
    order = np.argsort(projection.depths_m)[::-1]
    for index in order:
        u, v = projection.pixels_xy[index]
        red, green, blue = (int(value) for value in colors_rgb[index])
        draw.ellipse(
            (u - 2.0, v - 2.0, u + 2.0, v + 2.0),
            fill=(red, green, blue, 205),
        )
    point_delta_ms = (
        lidar_sample.point_session_time_seconds - camera_anchor.session_time_seconds
    ) * 1000.0
    draw.rectangle((0, 0, image.width, 34), fill=(0, 0, 0, 190))
    draw.text(
        (8, 9),
        (
            f"session={camera_anchor.session_time_seconds:.3f}s  "
            f"LiDAR-camera={point_delta_ms:+.1f}ms  "
            f"projected={projection.projected_point_count}/"
            f"{projection.source_point_count}"
        ),
        fill=(255, 255, 255, 255),
    )
    return np.asarray(image.convert("RGB"), dtype=np.uint8)


def _mosaic(rendered: tuple[_RenderedFrame, ...]) -> RgbImage:
    columns = 2 if len(rendered) > 1 else 1
    rows = math.ceil(len(rendered) / columns)
    width = rendered[0].overlay_rgb.shape[1]
    height = rendered[0].overlay_rgb.shape[0]
    canvas = Image.new("RGB", (columns * width, rows * height), "black")
    for index, item in enumerate(rendered):
        canvas.paste(
            Image.fromarray(item.overlay_rgb, mode="RGB"),
            ((index % columns) * width, (index // columns) * height),
        )
    return np.asarray(canvas, dtype=np.uint8)


def _write_rerun_diagnostic(
    path: Path,
    *,
    experiment_id: str,
    rendered: tuple[_RenderedFrame, ...],
    calibration_identity: str,
    profile: Kb4ProjectionProfile,
) -> None:
    recording = rr.RecordingStream(
        "nodedc_mission_core_k1_calibration",
        recording_id=experiment_id,
    )
    recording.set_sinks(rr.FileSink(path, write_footer=True))
    try:
        recording.log(
            "k1/calibrated/contract",
            rr.TextDocument(
                "\n".join(
                    (
                        "K1 factory-calibrated diagnostic",
                        f"source={profile.source_id}",
                        f"slot={profile.calibration_slot}",
                        f"calibration={calibration_identity}",
                        "projection=map→inverse(T_map_from_lidar)→T_camera_from_lidar→KB4",
                        "timing=host-arrival-best-effort; P0 temporal acceptance pending",
                    )
                )
            ),
            static=True,
        )
        for item in rendered:
            session_time_ns = round(item.camera.session_time_seconds * 1_000_000_000)
            recording.set_time(
                "session_time",
                duration=np.timedelta64(session_time_ns, "ns"),
            )
            recording.log("k1/calibrated/camera", rr.Image(item.image_rgb))
            recording.log(
                "k1/calibrated/camera/projected_lidar",
                rr.Points2D(
                    item.projection.pixels_xy.astype(np.float32),
                    colors=item.colors_rgb,
                    radii=rr.Radius.ui_points(2.0),
                ),
            )
        recording.flush(timeout_sec=30.0)
        recording.disconnect()
    finally:
        path.chmod(0o600)
    _fsync_file(path)


def _frame_document(item: _RenderedFrame) -> dict[str, object]:
    depth = item.projection.depths_m
    return {
        "camera_sequence": item.camera.sequence,
        "requested_video_offset_seconds": item.camera.requested_video_offset_seconds,
        "camera_session_time_seconds": item.camera.session_time_seconds,
        "point_session_time_seconds": item.lidar.point_session_time_seconds,
        "pose_session_time_seconds": item.lidar.pose_session_time_seconds,
        "point_minus_camera_ms": round(
            (
                item.lidar.point_session_time_seconds
                - item.camera.session_time_seconds
            )
            * 1000.0,
            6,
        ),
        "pose_minus_point_ms": round(
            (
                item.lidar.pose_session_time_seconds
                - item.lidar.point_session_time_seconds
            )
            * 1000.0,
            6,
        ),
        "source_points": item.projection.source_point_count,
        "camera_front_points": item.projection.camera_front_point_count,
        "projected_points": item.projection.projected_point_count,
        "projected_fraction": round(
            item.projection.projected_point_count / item.projection.source_point_count,
            9,
        ),
        "depth_m": {
            "p05": float(np.percentile(depth, 5.0)),
            "median": float(np.median(depth)),
            "p95": float(np.percentile(depth, 95.0)),
        },
        "artifact": item.artifact_name,
        "camera_artifact": item.camera_artifact_name,
    }


def _operator_notes(
    *,
    experiment_id: str,
    session_id: str,
    source_id: str,
    calibration_identity: str,
    temporal_offset_seconds: float,
) -> bytes:
    return (
        f"# {experiment_id}\n\n"
        "Offline read-only diagnostic. Native MQTT/camera evidence was not modified.\n\n"
        f"- session: {session_id}\n"
        f"- source: {source_id}\n"
        f"- calibration content identity: {calibration_identity}\n"
        f"- requested host-arrival temporal offset: {temporal_offset_seconds:+.6f} s\n"
        "- point frame: K1 map\n"
        "- pose interpretation: T_map_from_lidar; inverted before factory extrinsic\n"
        "- camera model: KB4 at admitted 800x600\n\n"
        "This run proves a reproducible diagnostic layer only. Static landmark pixel "
        "correspondences and a temporal-offset error budget are still required before "
        "P0 acceptance.\n"
    ).encode()


def _verified_raw_sha256(raw_path: Path, summary: dict[str, object]) -> str:
    expected = _required_sha256(
        _required_mapping(summary.get("artifact_hashes"), "artifact hashes").get(
            "raw_sha256"
        ),
        "raw MQTT sha256",
    )
    actual = _sha256_file(raw_path)
    if actual != expected:
        raise CalibratedOverlayExperimentError("raw MQTT identity changed")
    return actual


def _artifact_document(path: Path) -> dict[str, object]:
    metadata = path.stat()
    if not stat.S_ISREG(metadata.st_mode) or metadata.st_size <= 0:
        raise CalibratedOverlayExperimentError("derived artifact is unavailable")
    return {
        "name": path.name,
        "bytes": metadata.st_size,
        "sha256": _sha256_file(path),
    }


def _read_json(path: Path, max_bytes: int) -> dict[str, object]:
    payload = _read_regular(path, max_bytes)
    try:
        value = json.loads(payload)
    except json.JSONDecodeError as exc:
        raise CalibratedOverlayExperimentError(f"{path.name} is not valid JSON") from exc
    if not isinstance(value, dict):
        raise CalibratedOverlayExperimentError(f"{path.name} is not a JSON object")
    return value


def _read_regular(path: Path, max_bytes: int) -> bytes:
    flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
    descriptor = os.open(path, flags)
    try:
        before = os.fstat(descriptor)
        if not stat.S_ISREG(before.st_mode) or not 1 <= before.st_size <= max_bytes:
            raise CalibratedOverlayExperimentError(f"{path.name} is outside bounds")
        chunks: list[bytes] = []
        remaining = before.st_size
        while remaining:
            chunk = os.read(descriptor, min(_READ_CHUNK_BYTES, remaining))
            if not chunk:
                break
            chunks.append(chunk)
            remaining -= len(chunk)
        after = os.fstat(descriptor)
        if (
            remaining
            or (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns)
            != (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns)
        ):
            raise CalibratedOverlayExperimentError(f"{path.name} changed during read")
        return b"".join(chunks)
    finally:
        os.close(descriptor)


def _write_image_exclusive(path: Path, image_rgb: RgbImage) -> None:
    descriptor = os.open(
        path,
        os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0),
        0o600,
    )
    try:
        with os.fdopen(descriptor, "wb", closefd=False) as stream:
            Image.fromarray(image_rgb, mode="RGB").save(stream, format="PNG")
            stream.flush()
            os.fsync(stream.fileno())
    finally:
        os.close(descriptor)


def _write_exclusive(path: Path, payload: bytes) -> None:
    descriptor = os.open(
        path,
        os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0),
        0o600,
    )
    try:
        view = memoryview(payload)
        while view:
            written = os.write(descriptor, view)
            view = view[written:]
        os.fsync(descriptor)
    finally:
        os.close(descriptor)


def _sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        for chunk in iter(lambda: stream.read(_READ_CHUNK_BYTES), b""):
            digest.update(chunk)
    return digest.hexdigest()


def _canonical_json(value: object) -> bytes:
    return json.dumps(
        value,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
        allow_nan=False,
    ).encode("utf-8")


def _required_mapping(value: object, label: str) -> dict[str, object]:
    if not isinstance(value, dict):
        raise CalibratedOverlayExperimentError(f"{label} is missing")
    return value


def _required_list(value: object, label: str) -> list[object]:
    if not isinstance(value, list):
        raise CalibratedOverlayExperimentError(f"{label} is missing")
    return value


def _required_text(value: object, label: str) -> str:
    if not isinstance(value, str) or not value:
        raise CalibratedOverlayExperimentError(f"{label} is missing")
    return value


def _required_sha256(value: object, label: str) -> str:
    text = _required_text(value, label)
    if len(text) != 64 or any(character not in "0123456789abcdef" for character in text):
        raise CalibratedOverlayExperimentError(f"{label} is invalid")
    return text


def _chmod_private(path: Path) -> None:
    current = path
    while current.name in {"private", "perception-experiments"}:
        with suppress(OSError):
            current.chmod(0o700)
        current = current.parent


def _fsync_file(path: Path) -> None:
    with path.open("rb") as stream:
        os.fsync(stream.fileno())


def _fsync_directory(path: Path) -> None:
    descriptor = os.open(path, os.O_RDONLY)
    try:
        os.fsync(descriptor)
    finally:
        os.close(descriptor)
