//! Node runner: orchestrates ingest, chunking, and MoQ publication.

mod blockchain;
mod nbc;
mod source;

use anyhow::{anyhow, Context, Result};
use blake3;
use blockchain::{ManifestObservationLocator, ObservationSink, ObservationSinkOptions};
use bytes::BytesMut;
use clap::ValueEnum;
use clap::{Parser, Subcommand};
use ec_chopper::{build_manifest_body_for_chunks, TsChunk};
use ec_core::{
    merkle_proof_for_index, signed_record_index, verify_merkle_proof, Manifest, ManifestBody,
    ManifestSummary, ManifestVariant, MoqStreamDescriptor, SignedRecord, StreamCatalogEntry,
    StreamControlAnnouncement, StreamDescriptor, StreamEncryptionInfo, StreamId, StreamKey,
    StreamMetadata, StreamTransportDescriptor, MERKLE_PROOF_ALG_BLAKE3,
};
use ec_crypto::{
    decrypt_stream_data, encrypt_stream_data, load_ethereum_manifest_keypair_from_env,
    load_manifest_keypair_from_env, sign_manifest_body_eip712, sign_manifest_id,
    verify_manifest_signature_with_body, ENCRYPTION_ALG,
};
use ec_direct::{decode_direct_link, encode_direct_link, DirectCodeV1};
use ec_eth::{
    control_announcement_commitments, manifest_commitments, manifest_commitments_match,
    stream_descriptor_commitments,
};
use ec_iroh::DiscoveryConfig;
use ec_moq::{
    chunk_duration_secs, decode_object_frame, encode_object_frame, FileRelay, GroupId, HlsWriter,
    MoqNode, MoqPublishSet, ObjectId, ObjectMeta, ObjectPayload, TimingMeta, TrackName,
    DEFAULT_MANIFEST_TRACK_NAME, DEFAULT_TRACK_NAME,
};
use futures_util::{future::join_all, SinkExt, StreamExt};
use iroh::Watcher;
use just_webrtc::types::{DataChannelOptions, ICEServer, PeerConfiguration, PeerConnectionState};
use just_webrtc::{DataChannelExt, PeerConnectionBuilder, PeerConnectionExt};
use nbc::{
    bootstrap_nbc_auth, nbc_capture_fps, resolve_nbc_chrome_path, resolve_nbc_profile_dir,
    spawn_nbc_frame_reader,
};
use opentelemetry::trace::TracerProvider as _;
use opentelemetry::KeyValue;
use opentelemetry_otlp::{Protocol, SpanExporter, WithExportConfig};
use opentelemetry_sdk::{trace::SdkTracerProvider, Resource};
use source::{HdhrSource, HlsMode, HlsSource, LinuxDvbSource, StreamSource, TsSource};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
use std::ffi::OsString;
use std::fs;
use std::fs::File;
use std::future::Future;
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
use std::net::TcpStream as StdTcpStream;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::str::FromStr;
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::process::Command as TokioCommand;
use tokio_tungstenite::tungstenite::Message as WsMessage;
use tokio_util::io::SyncIoBridge;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use url::Url;

const DIRECT_WIRE_TAG_FRAME: u8 = 0x00;
const DIRECT_WIRE_TAG_STREAM: u8 = 0x01;
const DIRECT_WIRE_TAG_PING: u8 = 0x02;
const DIRECT_HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(8);
// Conservatively under typical SCTP data channel max message sizes.
const DIRECT_WIRE_CHUNK_BYTES: usize = 16 * 1024;
const WT_PUBLISH_PRIMARY_VIDEO_TRACK: &str = "0.m4s";
const WT_PUBLISH_PRIMARY_AUDIO_TRACK: &str = "1.m4s";
const WT_PUBLISH_INIT_TRACK: &str = "init.mp4";
const WT_PUBLISH_INIT_CAPTURE_MAX_BYTES: usize = 4 * 1024 * 1024;
const WT_PUBLISHER_ORIGIN_TRACK: &str = "publisher.m4s";
const WT_ARCHIVE_HLS_DEFAULT_LIMIT: usize = 900;
const WT_ARCHIVE_TIMELINE_FULL_SCAN_MAX_BYTES: u64 = 128 * 1024 * 1024;
const WT_ARCHIVE_DEFAULT_RETENTION_SECONDS: u64 = 30 * 24 * 60 * 60;
const WT_LADDER_PRIMARY_RENDITION: &str = "720p";
const WT_LADDER_RENDITIONS: &[&str] = &["480p", "720p", "1080p"];
const ARCHIVE_CONVERGENCE_PROMETHEUS_METRICS: &[&str] = &[
    "every_channel_archive_convergence_ok",
    "every_channel_archive_duplicate_complete",
    "every_channel_archive_duplicate_hash_source_records",
    "every_channel_archive_duplicate_hash_sequences",
    "every_channel_archive_hash_divergent_sequences",
    "every_channel_archive_source_local_hash_divergent_sequences",
    "every_channel_archive_missing_hash_records",
    "every_channel_archive_missing_source_identity_records",
    "every_channel_archive_media_timing_conflict_sequences",
];
const WT_ARCHIVE_DEFAULT_TRACKS: &[&str] = &[
    "catalog.json",
    "catalog",
    WT_PUBLISH_INIT_TRACK,
    WT_PUBLISH_PRIMARY_VIDEO_TRACK,
    WT_PUBLISH_PRIMARY_AUDIO_TRACK,
];
const WT_PUBLISH_GOP_FRAMES: u32 = 30;
const WT_PUBLISH_VIDEO_PRESET: &str = "medium";
const WT_PUBLISH_VIDEO_CRF: u8 = 23;
const WT_PUBLISH_VIDEO_FILTER: &str =
    "yadif=mode=send_frame:parity=auto:deint=interlaced,fps=30000/1001";
const WT_PUBLISH_ARCHIVE_SEGMENT_DURATION_MS: u64 = 1001;
const WT_PUBLISH_PROOF_PREROLL_PACKETS: usize = 2048;
const WT_PUBLISH_MOVFLAGS: &str = "empty_moov+frag_keyframe+separate_moof+omit_tfhd_offset";
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio::sync::RwLock;

#[derive(Parser, Debug)]
#[command(name = "ec-node")]
#[command(about = "every.channel node runner", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand, Debug)]
enum Commands {
    /// Ingest a source and publish MoQ objects into a local relay directory.
    Ingest(IngestArgs),
    /// Ingest a source and publish via MoQ over iroh.
    MoqPublish(MoqPublishArgs),
    /// Subscribe to a MoQ stream and write HLS segments locally.
    MoqSubscribe(MoqSubscribeArgs),
    /// Publish and subscribe to a MoQ stream locally, verifying chunk hashes.
    MoqSelftest(MoqSelftestArgs),
    /// Publish a stream over a direct WebRTC data channel (manual copy/paste connect code).
    DirectPublish(DirectPublishArgs),
    /// Subscribe to a direct WebRTC stream (directory or offer link) and optionally capture an .mp4 proof.
    DirectSubscribe(DirectSubscribeArgs),
    /// Publish a stream to the global one-to-many relay (`/api/stream/ws`) and keep the directory entry live.
    WsPublish(WsPublishArgs),
    /// Subscribe to the global one-to-many relay (`/api/stream/ws`) and capture CMAF fragments + an mp4 proof.
    WsSubscribe(WsSubscribeArgs),
    /// Publish a CMAF (fMP4) stream to a MoQ relay over WebTransport (Cloudflare preview by default).
    WtPublish(WtPublishArgs),
    /// Warm an NBC / Adobe auth session in Chrome, only requiring operator interaction when the session is cold.
    NbcBootstrap(NbcBootstrapArgs),
    /// Publish a live NBC browser-backed stream to a MoQ relay over WebTransport.
    NbcWtPublish(NbcWtPublishArgs),
    /// Subscribe to a relay broadcast over WebTransport/MoQ and archive groups into CAS.
    WtArchive(WtArchiveArgs),
    /// Serve archived relay groups as DVR-style HLS playlists + object endpoints.
    #[command(alias = "archive-serve")]
    WtArchiveServe(WtArchiveServeArgs),
    /// Announce stream transport availability over iroh gossip control topic.
    ControlAnnounce(ControlAnnounceArgs),
    /// Listen for stream transport announcements from iroh gossip control topic.
    ControlListen(ControlListenArgs),
    /// Resolve a stream id to the best currently-announced transport.
    ControlResolve(ControlResolveArgs),
    /// Bridge iroh control announcements into the website public stream directory.
    ControlBridgeWeb(ControlBridgeWebArgs),
    /// Encode one bounded MPEG-TS window with fresh deterministic publisher proof state.
    PublisherProofSegment(PublisherProofSegmentArgs),
    /// Split MPEG-TS by source clock and encode each window with fresh proof state.
    PublisherProofWindows(PublisherProofWindowsArgs),
    /// Prove duplicate publisher identities produce identical stateless proof windows.
    PublisherProofDuplicates(PublisherProofDuplicatesArgs),
    /// Compare stateless proof-window JSON reports produced on separate publishers.
    PublisherProofCompare(PublisherProofCompareArgs),
    /// Run stateless proof windows on remote publishers and compare the JSON reports.
    PublisherProofRemoteCompare(PublisherProofRemoteCompareArgs),
    /// Archive live source-clock proof windows into publisher-origin CAS/index records.
    PublisherProofArchiveSource(PublisherProofArchiveSourceArgs),
    /// Compare archive manifests from duplicate publishers by media BLAKE3 identity.
    ArchiveConvergence(ArchiveConvergenceArgs),
    /// Fetch remote archive manifests repeatedly and measure duplicate convergence.
    ArchiveConvergenceMeasure(ArchiveConvergenceMeasureArgs),
    /// Serve remote duplicate archive convergence measurement metrics over HTTP.
    ArchiveConvergenceMeasureServe(ArchiveConvergenceMeasureServeArgs),
    /// Serve duplicate archive convergence proof metrics over HTTP.
    ArchiveConvergenceServe(ArchiveConvergenceServeArgs),
    /// Run deterministic control-plane propagation simulations without booting nodes.
    SimControlPlane(SimControlPlaneArgs),
    /// Run deterministic distributed-system simulations without booting nodes.
    SimSystem(SimSystemArgs),
    /// Run deterministic duplicate-publisher simulations without booting nodes.
    SimDuplicatePublishers(SimDuplicatePublishersArgs),
    /// Build a deterministic off-chain signed-record index JSON document.
    SignedRecordIndex(SignedRecordIndexArgs),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum EncodeMode {
    /// Publish/subscribe CMAF-style fragmented MP4 segments (HLS fMP4) encoded with x264/AAC.
    Cmaf,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum CmafLadderPreset {
    /// 3-rung ladder: 1080p@6000k, 720p@3000k, 480p@1200k (CBR-ish).
    Hd3,
}

#[derive(Parser, Debug)]
struct IngestArgs {
    /// Output directory for temporary chunks.
    #[arg(long, default_value = "./tmp/chunks")]
    chunk_dir: PathBuf,
    /// Relay directory to write MoQ objects.
    #[arg(long, default_value = "./tmp/relay")]
    relay_dir: PathBuf,
    /// Chunk duration in ms.
    #[arg(long, default_value_t = 2000)]
    chunk_ms: u64,
    /// Maximum number of chunks to write.
    #[arg(long)]
    max_chunks: Option<usize>,
    /// Optional stream id override.
    #[arg(long)]
    stream_id: Option<String>,
    /// Optional network secret (hex) for stream encryption.
    #[arg(long)]
    network_secret: Option<String>,
    /// Enable deterministic transcode before chunking.
    #[arg(long)]
    deterministic: bool,
    #[command(subcommand)]
    source: IngestSource,
}

#[derive(Parser, Debug)]
struct MoqPublishArgs {
    /// Output directory for temporary chunks.
    #[arg(long, default_value = "./tmp/chunks")]
    chunk_dir: PathBuf,
    /// Chunk duration in ms.
    #[arg(long, default_value_t = 2000)]
    chunk_ms: u64,
    /// Maximum number of chunks to write.
    #[arg(long)]
    max_chunks: Option<usize>,
    /// Optional stream id override.
    #[arg(long)]
    stream_id: Option<String>,
    /// Optional network secret (hex) for stream encryption.
    #[arg(long)]
    network_secret: Option<String>,
    /// Enable deterministic transcode before chunking.
    #[arg(long)]
    deterministic: bool,
    /// Broadcast name override (defaults to stream id).
    #[arg(long)]
    broadcast_name: Option<String>,
    /// Track name override.
    #[arg(long, default_value = DEFAULT_TRACK_NAME)]
    track_name: String,
    /// Publish chunk objects on the main track.
    #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
    publish_chunks: bool,
    /// Publish manifests alongside chunks.
    #[arg(long)]
    publish_manifests: bool,
    /// Track name for manifest frames.
    #[arg(long, default_value = DEFAULT_MANIFEST_TRACK_NAME)]
    manifest_track: String,
    /// Number of chunks per manifest epoch.
    #[arg(long, default_value_t = 1)]
    epoch_chunks: usize,
    /// Optional iroh secret key (hex).
    #[arg(long)]
    iroh_secret: Option<String>,
    /// Discovery modes to enable (comma-separated: dht, mdns, dns).
    #[arg(long)]
    discovery: Option<String>,
    /// Announce catalog entries over iroh-gossip (requires peers).
    #[arg(long)]
    announce: bool,
    /// Gossip peers to connect to (repeatable).
    #[arg(long)]
    gossip_peer: Vec<String>,
    /// Optional startup delay (ms) after binding/publishing tracks, before ingest begins.
    /// Useful for E2E tests that need time to connect subscribers.
    #[arg(long)]
    startup_delay_ms: Option<u64>,
    /// Encoding/container mode.
    #[arg(long, value_enum, default_value_t = EncodeMode::Cmaf)]
    encode: EncodeMode,
    /// Track name for CMAF init segment objects.
    #[arg(long, default_value = "init")]
    init_track: String,
    /// Publish a CMAF ladder (multiple quality variants) using x264/AAC.
    #[arg(long, value_enum)]
    cmaf_ladder: Option<CmafLadderPreset>,
    /// RPC URL for submitting manifest-derived observation headers.
    #[arg(long)]
    observation_rpc_url: Option<String>,
    /// Deployed EveryChannelObservationLedger address.
    #[arg(long)]
    observation_ledger: Option<String>,
    /// Witness private key for observation transactions (hex). Prefer --observation-private-key-file for services.
    #[arg(long)]
    observation_private_key: Option<String>,
    /// File containing the witness private key for observation transactions.
    #[arg(long)]
    observation_private_key_file: Option<PathBuf>,
    /// Parent observation hash to put into new headers.
    #[arg(long)]
    observation_parent_hash: Option<String>,
    /// Maximum time to wait for each observation transaction.
    #[arg(long, default_value_t = 30000)]
    observation_timeout_ms: u64,
    #[command(subcommand)]
    source: IngestSource,
}

#[derive(Parser, Debug)]
struct MoqSubscribeArgs {
    /// Output directory for HLS segments.
    #[arg(long, default_value = "./tmp/moq-hls")]
    output_dir: PathBuf,
    /// Fallback chunk duration in ms.
    #[arg(long, default_value_t = 2000)]
    chunk_ms: u64,
    /// HLS window size.
    #[arg(long, default_value_t = 6)]
    window: usize,
    /// Optional stream id override (for decryption).
    #[arg(long)]
    stream_id: Option<String>,
    /// Optional network secret (hex) for stream decryption.
    #[arg(long)]
    network_secret: Option<String>,
    /// Remote endpoint address (iroh EndpointAddr).
    #[arg(long)]
    remote: String,
    /// Optional remote endpoint address to fetch manifests from.
    /// Defaults to `--remote` when not provided.
    #[arg(long)]
    remote_manifests: Option<String>,
    /// Broadcast name to subscribe to.
    #[arg(long)]
    broadcast_name: String,
    /// Track name to subscribe to.
    #[arg(long, default_value = DEFAULT_TRACK_NAME)]
    track_name: String,
    /// Subscribe to manifest frames.
    #[arg(long)]
    subscribe_manifests: bool,
    /// Require a manifest to accept chunk data.
    #[arg(long)]
    require_manifest: bool,
    /// Track name for manifest frames.
    #[arg(long, default_value = DEFAULT_MANIFEST_TRACK_NAME)]
    manifest_track: String,
    /// Allowed manifest signer ids (comma-separated).
    #[arg(long)]
    manifest_signers: Option<String>,
    /// Maximum bytes per second to accept (anti-junk).
    #[arg(long)]
    max_bytes_per_sec: Option<u64>,
    /// Maximum burst bytes before throttling.
    #[arg(long)]
    max_bytes_burst: Option<u64>,
    /// Maximum invalid chunks before disconnect.
    #[arg(long, default_value_t = 1)]
    max_invalid_chunks: u32,
    /// Stop after writing this many segments (useful for E2E tests).
    #[arg(long)]
    stop_after: Option<u64>,
    /// Optional iroh secret key (hex).
    #[arg(long)]
    iroh_secret: Option<String>,
    /// Discovery modes to enable (comma-separated: dht, mdns, dns).
    #[arg(long)]
    discovery: Option<String>,
    /// Container mode for local HLS output.
    #[arg(long, value_enum, default_value_t = EncodeMode::Cmaf)]
    container: EncodeMode,
    /// Track name to subscribe to for CMAF init segment objects.
    #[arg(long, default_value = "init")]
    init_track: String,
    /// Subscribe to the init segment track (CMAF).
    #[arg(long)]
    subscribe_init: bool,
    /// Write raw CMAF init+segments (no HLS playlist) to `--output-dir`.
    #[arg(long)]
    raw_cmaf: bool,
}

#[derive(Parser, Debug)]
struct MoqSelftestArgs {
    /// Input TS file or URL (e.g. http://HDHR_HOST/auto/v8.1).
    input: String,
    /// Output directory for temporary chunks.
    #[arg(long, default_value = "./tmp/moq-selftest")]
    chunk_dir: PathBuf,
    /// Chunk duration in ms.
    #[arg(long, default_value_t = 2000)]
    chunk_ms: u64,
    /// Maximum number of chunks to publish/verify.
    #[arg(long, default_value_t = 8)]
    max_chunks: usize,
    /// Optional stream id override.
    #[arg(long)]
    stream_id: Option<String>,
    /// Track name override.
    #[arg(long, default_value = DEFAULT_TRACK_NAME)]
    track_name: String,
    /// Discovery modes to enable (comma-separated: dht, mdns, dns).
    #[arg(long)]
    discovery: Option<String>,
}

#[derive(Parser, Debug)]
struct DirectPublishArgs {
    /// Global stream id (used for directory/signaling). If omitted, a fresh id is generated.
    #[arg(long)]
    stream_id: Option<String>,
    /// Human-friendly title (used for directory listing).
    #[arg(long, default_value = "Live channel")]
    title: String,
    /// Optional directory base URL (e.g. https://every.channel). When set, the publisher
    /// announces the offer to `/api/announce` and polls `/api/answer`.
    #[arg(long)]
    directory_url: Option<String>,
    /// Offer TTL (ms) when announcing to the directory.
    #[arg(long, default_value_t = 20000)]
    announce_ttl_ms: u64,
    /// Output directory for temporary chunks.
    #[arg(long, default_value = "./tmp/direct-chunks")]
    chunk_dir: PathBuf,
    /// Chunk duration in ms.
    #[arg(long, default_value_t = 2000)]
    chunk_ms: u64,
    /// Maximum number of media segments to send.
    #[arg(long, default_value_t = 30)]
    max_segments: usize,
    /// Optional answer code/link to avoid interactive stdin.
    #[arg(long)]
    answer: Option<String>,
    /// How long to wait for a browser answer when using `--directory-url` or stdin (seconds).
    /// Set to 0 to wait indefinitely.
    #[arg(long, default_value_t = 900)]
    answer_timeout_secs: u64,
    /// Ingest source.
    #[command(subcommand)]
    source: IngestSource,
}

#[derive(Parser, Debug)]
struct DirectSubscribeArgs {
    /// Directory base URL (e.g. https://every.channel). Used to locate offers by stream_id
    /// and to POST answers back to the publisher.
    #[arg(long, default_value = "https://every.channel")]
    directory_url: String,
    /// Stream id to locate in the directory.
    #[arg(long)]
    stream_id: Option<String>,
    /// Direct offer link/code to use instead of looking up a stream in the directory.
    #[arg(long)]
    offer: Option<String>,
    /// Output directory for captured CMAF fragments (init+segments) and index.m3u8.
    #[arg(long, default_value = "./tmp/direct-subscribe")]
    out_dir: PathBuf,
    /// Maximum number of media segments to capture (init not counted).
    #[arg(long, default_value_t = 12)]
    max_segments: usize,
    /// Optional time limit (seconds). When set, capture stops after this duration even if `--max-segments` is not reached.
    #[arg(long)]
    duration_secs: Option<u64>,
    /// If set, remux the captured playlist to this mp4 path (best-effort, `ffmpeg -c copy`).
    #[arg(long)]
    mp4: Option<PathBuf>,
}

#[derive(Parser, Debug)]
struct WsPublishArgs {
    /// Global stream id (used for directory listing + relay instance key). If omitted, a fresh id is generated.
    #[arg(long)]
    stream_id: Option<String>,
    /// Human-friendly title (used for directory listing).
    #[arg(long, default_value = "Live channel")]
    title: String,
    /// Directory base URL (e.g. https://every.channel). Used for listing at `/api/announce`,
    /// and as the base for relay websocket URL (`/api/stream/ws`).
    #[arg(long, default_value = "https://every.channel")]
    directory_url: String,
    /// Offer TTL (ms) when announcing to the directory.
    #[arg(long, default_value_t = 20000)]
    announce_ttl_ms: u64,
    /// Output directory for temporary chunks.
    #[arg(long, default_value = "./tmp/ws-chunks")]
    chunk_dir: PathBuf,
    /// Chunk duration in ms.
    #[arg(long, default_value_t = 2000)]
    chunk_ms: u64,
    /// Maximum number of media segments to send (init not counted). Set high for "run forever" behavior.
    #[arg(long, default_value_t = 1_000_000)]
    max_segments: usize,
    /// Ingest source.
    #[command(subcommand)]
    source: IngestSource,
}

#[derive(Parser, Debug)]
struct WsSubscribeArgs {
    /// Directory base URL (e.g. https://every.channel). Used for relay websocket URL (`/api/stream/ws`).
    #[arg(long, default_value = "https://every.channel")]
    directory_url: String,
    /// Stream id to subscribe to.
    #[arg(long)]
    stream_id: String,
    /// Output directory for captured CMAF fragments (init+segments) and index.m3u8.
    #[arg(long, default_value = "./tmp/ws-subscribe")]
    out_dir: PathBuf,
    /// Maximum number of media segments to capture (init not counted).
    #[arg(long, default_value_t = 12)]
    max_segments: usize,
    /// Optional time limit (seconds). When set, capture stops after this duration even if `--max-segments` is not reached.
    #[arg(long)]
    duration_secs: Option<u64>,
    /// If set, remux the captured playlist to this mp4 path (best-effort, `ffmpeg -c copy`).
    #[arg(long)]
    mp4: Option<PathBuf>,
}

#[derive(Parser, Debug)]
struct WtPublishArgs {
    /// Relay URL (WebTransport) to connect to.
    /// Default points at every.channel's public relay.
    #[arg(long, default_value = "https://relay.every.channel/anon")]
    url: String,
    /// Broadcast name to publish.
    ///
    /// This should be stable so you can share:
    /// `https://every.channel/watch?url=...&name=<broadcast>`.
    #[arg(long)]
    name: String,
    /// Input URL or file for ffmpeg (e.g. HDHomeRun `http://hdhomerun.local/auto/v4.1`).
    #[arg(long)]
    input: String,
    /// Optional ffmpeg input format passed before `--input` (for example `lavfi` for test sources).
    #[arg(long)]
    input_format: Option<String>,
    /// Read the input at native rate. Useful for files and lavfi sources used as live publishers.
    #[arg(long, default_value_t = false)]
    realtime_input: bool,
    /// If set, transcode to H.264/AAC before fragmenting to fMP4.
    #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
    transcode: bool,
    /// ffmpeg video filter used by the transcode path.
    #[arg(long, default_value = WT_PUBLISH_VIDEO_FILTER)]
    video_filter: String,
    /// ntsc-rs preset JSON to render before ffmpeg publishing.
    ///
    /// This is an explicit file preprocessor: pair it with `--ntsc-rs-output`.
    #[arg(long)]
    ntsc_rs_preset: Option<PathBuf>,
    /// ntsc-rs CLI binary path. Only used when `--ntsc-rs-preset` is set.
    #[arg(long, default_value = "ntsc-rs-cli")]
    ntsc_rs_cli: PathBuf,
    /// Output file written by ntsc-rs and then read by ffmpeg.
    #[arg(long)]
    ntsc_rs_output: Option<PathBuf>,
    /// H.264 GOP/keyframe interval in frames for the transcode path.
    #[arg(long, default_value_t = WT_PUBLISH_GOP_FRAMES)]
    gop_frames: u32,
    /// x264 preset for the transcode path. Slower presets usually keep quality at lower bitrate.
    #[arg(long, default_value = WT_PUBLISH_VIDEO_PRESET)]
    video_preset: String,
    /// x264 CRF for the transcode path.
    #[arg(long, default_value_t = WT_PUBLISH_VIDEO_CRF)]
    video_crf: u8,
    /// fMP4 movflags used for WebTransport publishing.
    #[arg(long, default_value = WT_PUBLISH_MOVFLAGS)]
    movflags: String,
    /// Transmit fMP4 fragments directly (passthrough mode).
    /// When false, the importer may reframe into CMAF fragments.
    #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
    passthrough: bool,
    /// Danger: disable TLS verification for the relay.
    #[arg(long, default_value_t = false)]
    tls_disable_verify: bool,
    /// Announce this relay stream over iroh gossip control topic.
    #[arg(long, default_value_t = false)]
    control_announce: bool,
    /// Control gossip TTL (ms) for control announcements.
    #[arg(long, default_value_t = 15000)]
    control_ttl_ms: u64,
    /// Control gossip announce interval (ms).
    #[arg(long, default_value_t = 5000)]
    control_interval_ms: u64,
    /// Exit if the relay `/announced` endpoint stops listing this broadcast for this long.
    ///
    /// This turns wedged relay sessions into supervised restarts instead of leaving a running
    /// process that relays and the public directory consider offline. Set 0 to disable.
    #[arg(long, default_value_t = 90000)]
    relay_announced_watchdog_ms: u64,
    /// Interval for the relay announcement watchdog.
    #[arg(long, default_value_t = 10000)]
    relay_announced_watchdog_interval_ms: u64,
    /// Optional local archive-buffer output dir for publisher-origin fMP4 fragment proof.
    #[arg(long)]
    publisher_archive_output_dir: Option<PathBuf>,
    /// Optional manifest dir for publisher-origin proof records.
    ///
    /// Defaults to `<publisher-archive-output-dir>/manifests`.
    #[arg(long)]
    publisher_archive_manifest_dir: Option<PathBuf>,
    /// Optional source node stamped into publisher-origin proof records.
    ///
    /// Defaults to EVERY_CHANNEL_ARCHIVE_SOURCE_NODE, EVERY_CHANNEL_NODE_NAME, HOSTNAME, then
    /// `unknown`.
    #[arg(long)]
    publisher_archive_source_node: Option<String>,
    /// Track name for publisher-origin fMP4 fragment proof records.
    #[arg(long, default_value = WT_PUBLISHER_ORIGIN_TRACK)]
    publisher_archive_track: String,
    /// Segment duration used to map fMP4 decode timestamps onto archive group_sequence.
    ///
    /// When fragments carry `tfdt` timestamps, publisher-origin proof uses this media-time cadence
    /// instead of a local process counter. Missing timestamps fall back to the local counter.
    #[arg(long, default_value_t = WT_PUBLISH_ARCHIVE_SEGMENT_DURATION_MS)]
    publisher_archive_segment_duration_ms: u64,
    /// Delay ffmpeg startup until the next Unix-epoch media boundary.
    ///
    /// This keeps restarted duplicate publishers on the same fragment/keyframe phase. Set 0 to
    /// start immediately.
    #[arg(long, default_value_t = WT_PUBLISH_ARCHIVE_SEGMENT_DURATION_MS)]
    publisher_start_boundary_ms: u64,
    /// Use wall-clock input timestamps for live publisher output.
    ///
    /// Duplicate publishers need fMP4 `tfdt` values to share a global media-time origin. Set false
    /// only for file/replay inputs that must keep source timestamps.
    #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
    publisher_wallclock_timestamps: bool,
    /// Optional iroh secret key (hex) for control gossip endpoint identity.
    #[arg(long)]
    iroh_secret: Option<String>,
    /// Discovery modes to enable for control gossip endpoint (comma-separated: dht, mdns, dns).
    #[arg(long)]
    discovery: Option<String>,
    /// Gossip peers to connect to for control announcements (repeatable).
    #[arg(long)]
    gossip_peer: Vec<String>,
    /// Optional path to write this publisher's control endpoint address JSON.
    /// Useful for bootstrapping local control bridges without manual peer copy/paste.
    #[arg(long)]
    control_endpoint_addr_out: Option<PathBuf>,
}

#[derive(Parser, Debug)]
struct NbcBootstrapArgs {
    /// NBC watch or live URL to warm.
    #[arg(long)]
    source_url: String,
    /// Optional Chrome binary override.
    #[arg(long)]
    chrome_path: Option<PathBuf>,
    /// Optional persistent profile directory override.
    #[arg(long)]
    profile_dir: Option<PathBuf>,
    /// Optional path for a screenshot captured when interactive auth is required.
    #[arg(long)]
    screenshot_out: Option<PathBuf>,
}

#[derive(Parser, Debug)]
struct NbcWtPublishArgs {
    /// Relay URL (WebTransport) to connect to.
    #[arg(long, default_value = "https://relay.every.channel/anon")]
    url: String,
    /// Broadcast name to publish.
    #[arg(long)]
    name: String,
    /// NBC watch or live URL to open in Chrome.
    #[arg(long)]
    source_url: String,
    /// Transmit fMP4 fragments directly (passthrough mode).
    #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
    passthrough: bool,
    /// H.264 GOP/keyframe interval in frames.
    #[arg(long, default_value_t = WT_PUBLISH_GOP_FRAMES)]
    gop_frames: u32,
    /// fMP4 movflags used for WebTransport publishing.
    #[arg(long, default_value = WT_PUBLISH_MOVFLAGS)]
    movflags: String,
    /// Danger: disable TLS verification for the relay.
    #[arg(long, default_value_t = false)]
    tls_disable_verify: bool,
    /// Announce this relay stream over iroh gossip control topic.
    #[arg(long, default_value_t = false)]
    control_announce: bool,
    /// Control gossip TTL (ms) for control announcements.
    #[arg(long, default_value_t = 15000)]
    control_ttl_ms: u64,
    /// Control gossip announce interval (ms).
    #[arg(long, default_value_t = 5000)]
    control_interval_ms: u64,
    /// Optional iroh secret key (hex) for control gossip endpoint identity.
    #[arg(long)]
    iroh_secret: Option<String>,
    /// Discovery modes to enable for control gossip endpoint (comma-separated: dht, mdns, dns).
    #[arg(long)]
    discovery: Option<String>,
    /// Gossip peers to connect to for control announcements (repeatable).
    #[arg(long)]
    gossip_peer: Vec<String>,
    /// Optional path to write this publisher's control endpoint address JSON.
    #[arg(long)]
    control_endpoint_addr_out: Option<PathBuf>,
    /// Optional Chrome binary override.
    #[arg(long)]
    chrome_path: Option<PathBuf>,
    /// Optional persistent profile directory override.
    #[arg(long)]
    profile_dir: Option<PathBuf>,
}

#[derive(Parser, Debug)]
struct WtArchiveArgs {
    /// Relay URL (WebTransport) to connect to.
    #[arg(long, default_value = "https://relay.every.channel/anon")]
    url: String,
    /// Broadcast name to subscribe to.
    #[arg(long)]
    name: String,
    /// Output directory for CAS objects.
    #[arg(long, default_value = "./tmp/wt-archive")]
    output_dir: PathBuf,
    /// Optional manifest/index output directory.
    /// Defaults to `<output-dir>/manifests` when omitted.
    #[arg(long)]
    manifest_dir: Option<PathBuf>,
    /// Track names to archive (repeatable).
    /// When omitted, defaults to catalog+init+primary audio/video tracks.
    #[arg(long)]
    track: Vec<String>,
    /// Source identity stamped into archive records.
    ///
    /// Defaults to EVERY_CHANNEL_ARCHIVE_SOURCE_NODE, EVERY_CHANNEL_NODE_NAME, HOSTNAME, then
    /// `unknown`. Production duplicate proof requires at least two distinct source identities.
    #[arg(long)]
    source_node: Option<String>,
    /// Danger: disable TLS verification for the relay.
    #[arg(long, default_value_t = false)]
    tls_disable_verify: bool,
}

#[derive(Parser, Debug)]
struct WtArchiveServeArgs {
    /// Output directory used by `wt-archive`.
    #[arg(long, default_value = "./tmp/wt-archive")]
    output_dir: PathBuf,
    /// Optional manifest/index directory.
    /// Defaults to `<output-dir>/manifests` when omitted.
    #[arg(long)]
    manifest_dir: Option<PathBuf>,
    /// TCP listen address for HTTP replay endpoints.
    #[arg(long, default_value = "0.0.0.0:7788")]
    listen: String,
    /// Optional archive root URL to fetch missing CAS objects from.
    /// The root is expected to contain `objects/blake3/<shard>/<hash>.bin`.
    #[arg(long)]
    archive_origin_url: Option<String>,
    /// Optional CAS root URL to fetch missing objects from.
    /// The root is expected to contain `<shard>/<hash>.bin`.
    #[arg(long)]
    archive_cas_origin_url: Option<String>,
    /// Maximum object size fetched from an archive origin.
    #[arg(long, default_value_t = 64 * 1024 * 1024)]
    archive_origin_max_bytes: usize,
    /// Access marker root used by hot-cache pruning to retain demand-fetched objects.
    /// Defaults to `<output-dir>/cache-access/blake3`.
    #[arg(long)]
    cache_access_dir: Option<PathBuf>,
    /// Retention target reported by archive status endpoints.
    #[arg(long, default_value_t = WT_ARCHIVE_DEFAULT_RETENTION_SECONDS)]
    archive_retention_seconds: u64,
}

#[derive(Parser, Debug)]
struct ControlAnnounceArgs {
    /// Stable stream id to announce.
    #[arg(long)]
    stream_id: String,
    /// Optional human title. Defaults to stream id.
    #[arg(long)]
    title: Option<String>,
    /// Announcement TTL in milliseconds.
    #[arg(long, default_value_t = 15000)]
    ttl_ms: u64,
    /// Announcement interval in milliseconds.
    #[arg(long, default_value_t = 5000)]
    interval_ms: u64,
    /// Relay URL for relay transport advertisement.
    #[arg(long)]
    relay_url: Option<String>,
    /// Relay broadcast name for relay transport advertisement.
    #[arg(long)]
    relay_broadcast: Option<String>,
    /// Relay track name for relay transport advertisement.
    #[arg(long, default_value = "0.m4s")]
    relay_track: String,
    /// Direct iroh endpoint address/id for direct transport advertisement.
    /// Defaults to this process endpoint id when `--direct-broadcast` is set.
    #[arg(long)]
    direct_endpoint: Option<String>,
    /// Direct iroh broadcast name for direct transport advertisement.
    #[arg(long)]
    direct_broadcast: Option<String>,
    /// Direct iroh track name for direct transport advertisement.
    #[arg(long, default_value = DEFAULT_TRACK_NAME)]
    direct_track: String,
    /// Optional iroh secret key (hex) for control gossip endpoint identity.
    #[arg(long)]
    iroh_secret: Option<String>,
    /// Discovery modes to enable (comma-separated: dht, mdns, dns).
    #[arg(long)]
    discovery: Option<String>,
    /// Gossip peers to connect to (repeatable).
    #[arg(long)]
    gossip_peer: Vec<String>,
}

#[derive(Parser, Debug)]
struct ControlListenArgs {
    /// Optional iroh secret key (hex) for control gossip endpoint identity.
    #[arg(long)]
    iroh_secret: Option<String>,
    /// Discovery modes to enable (comma-separated: dht, mdns, dns).
    #[arg(long)]
    discovery: Option<String>,
    /// Gossip peers to connect to (repeatable).
    #[arg(long)]
    gossip_peer: Vec<String>,
    /// Exit after the first announcement.
    #[arg(long, default_value_t = false)]
    once: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum ControlTransportPreference {
    /// Prefer iroh direct transport when both are available.
    DirectFirst,
    /// Prefer relay transport when both are available.
    RelayFirst,
    /// Accept only iroh direct transport.
    DirectOnly,
    /// Accept only relay transport.
    RelayOnly,
}

#[derive(Parser, Debug)]
struct ControlResolveArgs {
    /// Stable stream id to resolve from control announcements.
    #[arg(long)]
    stream_id: String,
    /// Transport selection preference.
    #[arg(long, value_enum, default_value_t = ControlTransportPreference::DirectFirst)]
    prefer: ControlTransportPreference,
    /// Maximum wall-clock wait for a matching announcement (ms).
    #[arg(long, default_value_t = 30000)]
    timeout_ms: u64,
    /// Maximum accepted staleness from updated_unix_ms (ms).
    #[arg(long, default_value_t = 30000)]
    max_age_ms: u64,
    /// Include the full raw announcement in output JSON.
    #[arg(long, default_value_t = false)]
    include_announcement: bool,
    /// Optional iroh secret key (hex) for control gossip endpoint identity.
    #[arg(long)]
    iroh_secret: Option<String>,
    /// Discovery modes to enable (comma-separated: dht, mdns, dns).
    #[arg(long)]
    discovery: Option<String>,
    /// Gossip peers to connect to (repeatable).
    #[arg(long)]
    gossip_peer: Vec<String>,
}

#[derive(Parser, Debug)]
struct ControlBridgeWebArgs {
    /// Directory base URL (e.g. https://every.channel).
    #[arg(long, default_value = "https://every.channel")]
    directory_url: String,
    /// Optional bearer token for `/api/stream-upsert`.
    /// Falls back to `EVERY_CHANNEL_WEB_UPSERT_TOKEN` when omitted.
    #[arg(long)]
    auth_token: Option<String>,
    /// Maximum wall-clock wait for control announcements before exit (ms).
    /// Set to 0 to run forever.
    #[arg(long, default_value_t = 0)]
    timeout_ms: u64,
    /// Maximum accepted staleness from updated_unix_ms (ms).
    #[arg(long, default_value_t = 30000)]
    max_age_ms: u64,
    /// Optional stream id prefix filter.
    #[arg(long)]
    stream_prefix: Option<String>,
    /// Exit after the first successful upsert.
    #[arg(long, default_value_t = false)]
    once: bool,
    /// Optional iroh secret key (hex) for control gossip endpoint identity.
    #[arg(long)]
    iroh_secret: Option<String>,
    /// Discovery modes to enable (comma-separated: dht, mdns, dns).
    #[arg(long)]
    discovery: Option<String>,
    /// Gossip peers to connect to (repeatable).
    #[arg(long)]
    gossip_peer: Vec<String>,
}

#[derive(Parser, Debug)]
struct PublisherProofSegmentArgs {
    /// Bounded MPEG-TS source-clock window to encode.
    #[arg(long)]
    input_ts: PathBuf,
    /// Output fMP4 proof file.
    #[arg(long)]
    output_mp4: PathBuf,
    /// If set, transcode to H.264/AAC before fragmenting to fMP4.
    #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
    transcode: bool,
    /// ffmpeg video filter used by the transcode path.
    #[arg(long, default_value = WT_PUBLISH_VIDEO_FILTER)]
    video_filter: String,
    /// H.264 GOP/keyframe interval in frames for the transcode path.
    #[arg(long, default_value_t = WT_PUBLISH_GOP_FRAMES)]
    gop_frames: u32,
    /// x264 preset for the transcode path.
    #[arg(long, default_value = WT_PUBLISH_VIDEO_PRESET)]
    video_preset: String,
    /// x264 CRF for the transcode path.
    #[arg(long, default_value_t = WT_PUBLISH_VIDEO_CRF)]
    video_crf: u8,
    /// fMP4 movflags used for proof encoding.
    #[arg(long, default_value = WT_PUBLISH_MOVFLAGS)]
    movflags: String,
    /// Pretty-print output JSON.
    #[arg(long, default_value_t = false)]
    pretty: bool,
}

#[derive(Parser, Debug)]
struct PublisherProofWindowsArgs {
    /// MPEG-TS input to split by source clock.
    #[arg(long)]
    input_ts: PathBuf,
    /// Output directory for source windows and fMP4 proof files.
    #[arg(long)]
    output_dir: PathBuf,
    /// Source-clock chunk duration.
    #[arg(long, default_value_t = WT_PUBLISH_ARCHIVE_SEGMENT_DURATION_MS)]
    chunk_ms: u64,
    /// Maximum source windows to encode.
    #[arg(long)]
    max_chunks: Option<usize>,
    /// MPEG-TS packets prepended from before each source-clock window for decoder context.
    #[arg(long, default_value_t = WT_PUBLISH_PROOF_PREROLL_PACKETS)]
    preroll_packets: usize,
    /// If set, transcode to H.264/AAC before fragmenting to fMP4.
    #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
    transcode: bool,
    /// ffmpeg video filter used by the transcode path.
    #[arg(long, default_value = WT_PUBLISH_VIDEO_FILTER)]
    video_filter: String,
    /// H.264 GOP/keyframe interval in frames for the transcode path.
    #[arg(long, default_value_t = WT_PUBLISH_GOP_FRAMES)]
    gop_frames: u32,
    /// x264 preset for the transcode path.
    #[arg(long, default_value = WT_PUBLISH_VIDEO_PRESET)]
    video_preset: String,
    /// x264 CRF for the transcode path.
    #[arg(long, default_value_t = WT_PUBLISH_VIDEO_CRF)]
    video_crf: u8,
    /// fMP4 movflags used for proof encoding.
    #[arg(long, default_value = WT_PUBLISH_MOVFLAGS)]
    movflags: String,
    /// Pretty-print output JSON.
    #[arg(long, default_value_t = false)]
    pretty: bool,
}

#[derive(Parser, Debug)]
struct PublisherProofDuplicatesArgs {
    /// MPEG-TS input to split and encode independently for each publisher identity.
    #[arg(long)]
    input_ts: PathBuf,
    /// Output directory containing per-publisher source windows and fMP4 proof files.
    #[arg(long)]
    output_dir: PathBuf,
    /// Publisher identity labels to compare. Defaults to publisher-a and publisher-b.
    #[arg(long, action = clap::ArgAction::Append)]
    publisher: Vec<String>,
    /// Source-clock chunk duration.
    #[arg(long, default_value_t = WT_PUBLISH_ARCHIVE_SEGMENT_DURATION_MS)]
    chunk_ms: u64,
    /// Maximum source windows to encode per publisher.
    #[arg(long)]
    max_chunks: Option<usize>,
    /// MPEG-TS packets prepended from before each source-clock window for decoder context.
    #[arg(long, default_value_t = WT_PUBLISH_PROOF_PREROLL_PACKETS)]
    preroll_packets: usize,
    /// If set, transcode to H.264/AAC before fragmenting to fMP4.
    #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
    transcode: bool,
    /// ffmpeg video filter used by the transcode path.
    #[arg(long, default_value = WT_PUBLISH_VIDEO_FILTER)]
    video_filter: String,
    /// H.264 GOP/keyframe interval in frames for the transcode path.
    #[arg(long, default_value_t = WT_PUBLISH_GOP_FRAMES)]
    gop_frames: u32,
    /// x264 preset for the transcode path.
    #[arg(long, default_value = WT_PUBLISH_VIDEO_PRESET)]
    video_preset: String,
    /// x264 CRF for the transcode path.
    #[arg(long, default_value_t = WT_PUBLISH_VIDEO_CRF)]
    video_crf: u8,
    /// fMP4 movflags used for proof encoding.
    #[arg(long, default_value = WT_PUBLISH_MOVFLAGS)]
    movflags: String,
    /// Pretty-print output JSON.
    #[arg(long, default_value_t = false)]
    pretty: bool,
    /// Exit non-zero unless every compared source/init/media hash matches.
    #[arg(long, default_value_t = false)]
    require_ok: bool,
}

#[derive(Parser, Debug)]
struct PublisherProofCompareArgs {
    /// Named proof-window JSON report, e.g. `nuc-a=/tmp/nuc-a-proof.json`. Repeatable.
    #[arg(long = "report", action = clap::ArgAction::Append)]
    report: Vec<String>,
    /// Pretty-print output JSON.
    #[arg(long, default_value_t = false)]
    pretty: bool,
    /// Exit non-zero unless every compared source/init/media hash matches.
    #[arg(long, default_value_t = false)]
    require_ok: bool,
}

#[derive(Parser, Debug)]
struct PublisherProofRemoteCompareArgs {
    /// Bounded MPEG-TS proof input copied to every remote publisher.
    #[arg(long)]
    input_ts: PathBuf,
    /// Local output directory for per-publisher reports and final comparison JSON.
    #[arg(long)]
    output_dir: PathBuf,
    /// Named remote publisher target, e.g. `nuc-a=100.64.0.44`. Repeatable.
    #[arg(long = "remote", action = clap::ArgAction::Append)]
    remote: Vec<String>,
    /// Remote ec-node executable path.
    #[arg(long, default_value = "ec-node")]
    remote_ec_node: String,
    /// SSH option passed as `-o KEY=VALUE`. Repeatable.
    #[arg(long = "ssh-option", action = clap::ArgAction::Append)]
    ssh_option: Vec<String>,
    /// Remote root directory. Defaults to a generated `/tmp/every-channel-publisher-proof-*` path.
    #[arg(long)]
    remote_root: Option<String>,
    /// Source-clock chunk duration.
    #[arg(long, default_value_t = WT_PUBLISH_ARCHIVE_SEGMENT_DURATION_MS)]
    chunk_ms: u64,
    /// Maximum source windows to encode per publisher.
    #[arg(long)]
    max_chunks: Option<usize>,
    /// MPEG-TS packets prepended from before each source-clock window for decoder context.
    #[arg(long, default_value_t = WT_PUBLISH_PROOF_PREROLL_PACKETS)]
    preroll_packets: usize,
    /// If set, transcode to H.264/AAC before fragmenting to fMP4.
    #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
    transcode: bool,
    /// ffmpeg video filter used by the transcode path.
    #[arg(long, default_value = WT_PUBLISH_VIDEO_FILTER)]
    video_filter: String,
    /// H.264 GOP/keyframe interval in frames for the transcode path.
    #[arg(long, default_value_t = WT_PUBLISH_GOP_FRAMES)]
    gop_frames: u32,
    /// x264 preset for the transcode path.
    #[arg(long, default_value = WT_PUBLISH_VIDEO_PRESET)]
    video_preset: String,
    /// x264 CRF for the transcode path.
    #[arg(long, default_value_t = WT_PUBLISH_VIDEO_CRF)]
    video_crf: u8,
    /// fMP4 movflags used for proof encoding.
    #[arg(long, default_value = WT_PUBLISH_MOVFLAGS)]
    movflags: String,
    /// Remove generated remote proof directories after reports are collected.
    #[arg(long, default_value_t = false)]
    cleanup_remote: bool,
    /// Pretty-print output JSON.
    #[arg(long, default_value_t = false)]
    pretty: bool,
    /// Exit non-zero unless every compared source/init/media hash matches.
    #[arg(long, default_value_t = false)]
    require_ok: bool,
}

#[derive(Parser, Debug, Clone)]
struct PublisherProofArchiveSourceArgs {
    /// MPEG-TS source input path or URL to split by source clock.
    #[arg(long)]
    input: String,
    /// Optional input format passed through for non-standard source inputs.
    #[arg(long)]
    input_format: Option<String>,
    /// Local archive-buffer output dir for publisher-origin proof fragments.
    #[arg(long)]
    output_dir: PathBuf,
    /// Optional manifest/index output dir. Defaults to `<output-dir>/manifests`.
    #[arg(long)]
    manifest_dir: Option<PathBuf>,
    /// Relay URL stamped into publisher-origin proof records.
    #[arg(long)]
    relay_url: String,
    /// Broadcast name stamped into publisher-origin proof records.
    #[arg(long)]
    name: String,
    /// Track name for publisher-origin proof records.
    #[arg(long, default_value = WT_PUBLISHER_ORIGIN_TRACK)]
    track: String,
    /// Optional source node stamped into publisher-origin proof records.
    #[arg(long)]
    source_node: Option<String>,
    /// Source-clock chunk duration.
    #[arg(long, default_value_t = WT_PUBLISH_ARCHIVE_SEGMENT_DURATION_MS)]
    chunk_ms: u64,
    /// Maximum source windows to encode. Omit for live continuous operation.
    #[arg(long)]
    max_chunks: Option<usize>,
    /// MPEG-TS packets prepended from before each source-clock window for decoder context.
    #[arg(long, default_value_t = WT_PUBLISH_PROOF_PREROLL_PACKETS)]
    preroll_packets: usize,
    /// If set, transcode to H.264/AAC before fragmenting to fMP4.
    #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
    transcode: bool,
    /// ffmpeg video filter used by the transcode path.
    #[arg(long, default_value = WT_PUBLISH_VIDEO_FILTER)]
    video_filter: String,
    /// H.264 GOP/keyframe interval in frames for the transcode path.
    #[arg(long, default_value_t = WT_PUBLISH_GOP_FRAMES)]
    gop_frames: u32,
    /// x264 preset for the transcode path.
    #[arg(long, default_value = WT_PUBLISH_VIDEO_PRESET)]
    video_preset: String,
    /// x264 CRF for the transcode path.
    #[arg(long, default_value_t = WT_PUBLISH_VIDEO_CRF)]
    video_crf: u8,
    /// fMP4 movflags used for proof encoding.
    #[arg(long, default_value = WT_PUBLISH_MOVFLAGS)]
    movflags: String,
    /// Remove temporary source-window and fMP4 proof files after CAS/index records are written.
    #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
    cleanup_temp: bool,
    /// Pretty-print a final JSON summary on normal EOF.
    #[arg(long, default_value_t = false)]
    pretty: bool,
}

#[derive(Parser, Debug)]
struct ArchiveConvergenceArgs {
    /// Named archive manifest root, e.g. `nuc-a=/srv/every-channel/archive-buffer/manifests`.
    #[arg(long, action = clap::ArgAction::Append)]
    source: Vec<String>,
    /// Logical broadcast name under each manifest root.
    #[arg(long)]
    broadcast: String,
    /// Track to compare.
    #[arg(long, default_value = WT_PUBLISHER_ORIGIN_TRACK)]
    track: String,
    /// Logical stream id for the comparison. Defaults to `--broadcast` without ladder suffix.
    #[arg(long)]
    stream_id: Option<String>,
    /// Logical rendition id. Defaults to track or broadcast ladder suffix, then `primary`.
    #[arg(long)]
    rendition: Option<String>,
    /// First sequence to include. Defaults to the earliest observed sequence.
    #[arg(long)]
    start_sequence: Option<u64>,
    /// Exclusive end sequence. Defaults to latest observed sequence + 1.
    #[arg(long)]
    end_sequence: Option<u64>,
    /// Pretty-print output JSON.
    #[arg(long, default_value_t = false)]
    pretty: bool,
    /// Render Prometheus text metrics instead of JSON.
    #[arg(long, default_value_t = false)]
    prometheus: bool,
    /// Node label used by `--prometheus` output.
    #[arg(long)]
    metrics_node: Option<String>,
    /// Role label used by `--prometheus` output.
    #[arg(long, default_value = "duplicate-proof")]
    metrics_role: String,
    /// Exit non-zero unless duplicate convergence is proven.
    #[arg(long, default_value_t = false)]
    require_ok: bool,
}

#[derive(Parser, Debug, Clone)]
struct ArchiveConvergenceMeasureArgs {
    /// Named node-agent base URL, e.g. `nuc-a=http://100.x.y.z:7799`.
    #[arg(long = "agent-manifest", action = clap::ArgAction::Append)]
    agent_manifest: Vec<String>,
    /// Prometheus file-SD JSON containing node-agent scrape targets.
    #[arg(long = "agent-prometheus-sd", action = clap::ArgAction::Append)]
    agent_prometheus_sd: Vec<PathBuf>,
    /// Label filter for Prometheus file-SD groups, e.g. `headscale_user=node`.
    #[arg(long = "agent-prometheus-sd-label", action = clap::ArgAction::Append)]
    agent_prometheus_sd_label: Vec<String>,
    /// Role query parameter for node-agent `/v1/archive-manifest`.
    #[arg(long, default_value = "publisher-buffer")]
    agent_manifest_role: String,
    /// Named manifest JSONL URL, e.g. `nuc-a=https://host/manifests/la-kcop/publisher.m4s.jsonl`.
    #[arg(long, action = clap::ArgAction::Append)]
    manifest: Vec<String>,
    /// Logical broadcast name under each manifest root.
    #[arg(long)]
    broadcast: String,
    /// Track to compare.
    #[arg(long, default_value = WT_PUBLISHER_ORIGIN_TRACK)]
    track: String,
    /// Logical stream id for the comparison. Defaults to `--broadcast` without ladder suffix.
    #[arg(long)]
    stream_id: Option<String>,
    /// Logical rendition id. Defaults to track or broadcast ladder suffix, then `primary`.
    #[arg(long)]
    rendition: Option<String>,
    /// First sequence to include. Defaults to the earliest observed sequence.
    #[arg(long)]
    start_sequence: Option<u64>,
    /// Exclusive end sequence. Defaults to latest observed sequence + 1.
    #[arg(long)]
    end_sequence: Option<u64>,
    /// Prometheus base URL used to check Grafana-facing duplicate proof series.
    #[arg(long)]
    prometheus_url: Option<String>,
    /// Total wall-clock measurement duration. Set 0 for one sample.
    #[arg(long, default_value_t = 0.0)]
    duration_seconds: f64,
    /// Delay between samples when duration is non-zero.
    #[arg(long, default_value_t = 30.0)]
    poll_interval_seconds: f64,
    /// HTTP request timeout in milliseconds.
    #[arg(long, default_value_t = 10000)]
    timeout_ms: u64,
    /// Maximum manifest bytes fetched per source.
    #[arg(long, default_value_t = 4 * 1024 * 1024)]
    max_manifest_bytes: usize,
    /// Pretty-print output JSON.
    #[arg(long, default_value_t = false)]
    pretty: bool,
    /// Exit non-zero unless elapsed production duplicate convergence is proven.
    #[arg(long, default_value_t = false)]
    require_ok: bool,
}

#[derive(Parser, Debug, Clone)]
struct ArchiveConvergenceMeasureServeArgs {
    /// Named node-agent base URL, e.g. `nuc-a=http://100.x.y.z:7799`.
    #[arg(long = "agent-manifest", action = clap::ArgAction::Append)]
    agent_manifest: Vec<String>,
    /// Prometheus file-SD JSON containing node-agent scrape targets.
    #[arg(long = "agent-prometheus-sd", action = clap::ArgAction::Append)]
    agent_prometheus_sd: Vec<PathBuf>,
    /// Label filter for Prometheus file-SD groups, e.g. `headscale_user=node`.
    #[arg(long = "agent-prometheus-sd-label", action = clap::ArgAction::Append)]
    agent_prometheus_sd_label: Vec<String>,
    /// Role query parameter for node-agent `/v1/archive-manifest`.
    #[arg(long, default_value = "publisher-buffer")]
    agent_manifest_role: String,
    /// Named manifest JSONL URL, e.g. `nuc-a=https://host/manifests/la-kcop/publisher.m4s.jsonl`.
    #[arg(long, action = clap::ArgAction::Append)]
    manifest: Vec<String>,
    /// Logical broadcast name under each manifest root.
    #[arg(long)]
    broadcast: String,
    /// Track to compare.
    #[arg(long, default_value = WT_PUBLISHER_ORIGIN_TRACK)]
    track: String,
    /// Logical stream id for the comparison. Defaults to `--broadcast` without ladder suffix.
    #[arg(long)]
    stream_id: Option<String>,
    /// Logical rendition id. Defaults to track or broadcast ladder suffix, then `primary`.
    #[arg(long)]
    rendition: Option<String>,
    /// First sequence to include. Defaults to the earliest observed sequence.
    #[arg(long)]
    start_sequence: Option<u64>,
    /// Exclusive end sequence. Defaults to latest observed sequence + 1.
    #[arg(long)]
    end_sequence: Option<u64>,
    /// Prometheus base URL used to check Grafana-facing duplicate proof series.
    #[arg(long)]
    prometheus_url: Option<String>,
    /// HTTP request timeout in milliseconds.
    #[arg(long, default_value_t = 10000)]
    timeout_ms: u64,
    /// Maximum manifest bytes fetched per source.
    #[arg(long, default_value_t = 4 * 1024 * 1024)]
    max_manifest_bytes: usize,
    /// Listen address for this proof service's `/health` and `/metrics` endpoints.
    #[arg(long, default_value = "127.0.0.1:7813")]
    listen: String,
    /// Maximum recent scrape samples retained for elapsed convergence proof.
    #[arg(long, default_value_t = 16)]
    max_samples: usize,
    /// Minimum elapsed sample window required before this service reports measurement ok.
    #[arg(long, default_value_t = 30.0)]
    min_elapsed_seconds: f64,
    /// Node label used by emitted Prometheus metrics.
    #[arg(long)]
    metrics_node: Option<String>,
    /// Role label used by emitted Prometheus metrics.
    #[arg(long, default_value = "duplicate-proof")]
    metrics_role: String,
}

#[derive(Parser, Debug, Clone)]
struct ArchiveConvergenceServeArgs {
    /// Named archive manifest root, e.g. `nuc-a=/srv/every-channel/archive-buffer/manifests`.
    #[arg(long, action = clap::ArgAction::Append)]
    source: Vec<String>,
    /// Logical broadcast name under each manifest root.
    #[arg(long)]
    broadcast: String,
    /// Track to compare.
    #[arg(long, default_value = WT_PUBLISHER_ORIGIN_TRACK)]
    track: String,
    /// Logical stream id for the comparison. Defaults to `--broadcast` without ladder suffix.
    #[arg(long)]
    stream_id: Option<String>,
    /// Logical rendition id. Defaults to track or broadcast ladder suffix, then `primary`.
    #[arg(long)]
    rendition: Option<String>,
    /// First sequence to include. Defaults to the earliest observed sequence.
    #[arg(long)]
    start_sequence: Option<u64>,
    /// Exclusive end sequence. Defaults to latest observed sequence + 1.
    #[arg(long)]
    end_sequence: Option<u64>,
    /// TCP listen address for Prometheus `/metrics`.
    #[arg(long, default_value = "127.0.0.1:7812")]
    listen: String,
    /// Node label used by Prometheus output.
    #[arg(long)]
    metrics_node: Option<String>,
    /// Role label used by Prometheus output.
    #[arg(long, default_value = "duplicate-proof")]
    metrics_role: String,
}

#[derive(Parser, Debug)]
struct SimControlPlaneArgs {
    /// Replay an exact ControlPlanePropagationScenario JSON. Use `-` to read stdin.
    #[arg(long)]
    scenario_json: Option<PathBuf>,
    /// First deterministic seed for the campaign.
    #[arg(long, default_value_t = 1)]
    seed: u64,
    /// Number of consecutive seed schedules to run.
    #[arg(long, default_value_t = 1024)]
    iterations: u64,
    /// Logical node name. Repeat to override the default topology.
    #[arg(long = "node", action = clap::ArgAction::Append)]
    nodes: Vec<String>,
    /// Node where the control announcement originates.
    #[arg(long, default_value = "nuc-a")]
    origin_node: String,
    /// Logical gossip topic under test.
    #[arg(long, default_value = "ec.control.broadcast.la-kcop")]
    topic: String,
    /// Announcement id under test.
    #[arg(long, default_value = "la-kcop@42")]
    announcement_id: String,
    /// Number of peers each known node gossips to per round.
    #[arg(long, default_value_t = 3)]
    fanout: u64,
    /// Logical milliseconds between gossip rounds for a node that knows the message.
    #[arg(long, default_value_t = 35)]
    gossip_interval_ms: u64,
    /// Maximum gossip rounds per node after it learns the announcement.
    #[arg(long, default_value_t = 12)]
    max_gossip_rounds: u64,
    /// Base network delivery delay per control message.
    #[arg(long, default_value_t = 6)]
    base_network_delay_ms: u64,
    /// Maximum deterministic jitter added to each control message.
    #[arg(long, default_value_t = 45)]
    max_jitter_ms: u64,
    /// Probability of transient delivery drop, expressed per million messages.
    #[arg(long, default_value_t = 120_000)]
    transient_drop_per_million: u32,
    /// Required propagation deadline. Set 0 to disable the deadline gate.
    #[arg(long, default_value_t = 900)]
    max_propagation_complete_ms: u64,
    /// Partition fault as NODE:START_MS:END_MS:RELEASE_DELAY_MS. Repeatable.
    #[arg(long = "partition", action = clap::ArgAction::Append)]
    partitions: Vec<String>,
    /// Node outage as NODE:START_MS:END_MS:RECOVERY_DELAY_MS. Repeatable.
    #[arg(long = "node-outage", action = clap::ArgAction::Append)]
    node_outages: Vec<String>,
    /// Pretty-print output JSON.
    #[arg(long, default_value_t = false)]
    pretty: bool,
    /// Emit failure JSON without returning a non-zero exit status.
    #[arg(long, default_value_t = false)]
    allow_failure: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum SimPublisherSequenceClock {
    Global,
    LocalActivation,
}

impl From<SimPublisherSequenceClock> for ec_core::sim::PublisherSequenceClock {
    fn from(value: SimPublisherSequenceClock) -> Self {
        match value {
            SimPublisherSequenceClock::Global => ec_core::sim::PublisherSequenceClock::Global,
            SimPublisherSequenceClock::LocalActivation => {
                ec_core::sim::PublisherSequenceClock::LocalActivation
            }
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum SimSystemFaultProfile {
    Fixed,
    #[value(name = "foundationdb")]
    FoundationDb,
}

#[derive(Parser, Debug)]
struct SimSystemArgs {
    /// Replay an exact SystemDuplicatePublisherScenario JSON. Use `-` to read stdin.
    #[arg(long)]
    scenario_json: Option<PathBuf>,
    /// First deterministic seed for the campaign.
    #[arg(long, default_value_t = 1)]
    seed: u64,
    /// Number of consecutive seed schedules to run.
    #[arg(long, default_value_t = 1024)]
    iterations: u64,
    /// Logical node name. Repeat to override the default topology.
    #[arg(long = "node", action = clap::ArgAction::Append)]
    nodes: Vec<String>,
    /// Publisher node name. Repeat for more than two publishers.
    #[arg(long = "publisher", action = clap::ArgAction::Append)]
    publishers: Vec<String>,
    /// Control-plane origin node.
    #[arg(long, default_value = "forge")]
    origin_node: String,
    /// Control topic under test.
    #[arg(long, default_value = "ec.control.broadcast.la-kcop")]
    topic: String,
    /// Control announcement id under test.
    #[arg(long, default_value = "la-kcop@42")]
    announcement_id: String,
    /// Media sequence clock model.
    #[arg(long, value_enum, default_value_t = SimPublisherSequenceClock::Global)]
    sequence_clock: SimPublisherSequenceClock,
    /// Fault scenario profile to generate for each deterministic seed.
    #[arg(long, value_enum, default_value_t = SimSystemFaultProfile::Fixed)]
    fault_profile: SimSystemFaultProfile,
    /// Require the campaign to exercise every built-in distributed fault class.
    #[arg(long, default_value_t = false)]
    require_fault_coverage: bool,
    /// Minimum seeds that must exercise each required fault class. Set 0 for profile default.
    #[arg(long, default_value_t = 0)]
    min_fault_seed_coverage: u64,
    /// Logical stream id under test.
    #[arg(long, default_value = "la-kcop")]
    stream_id: String,
    /// Logical rendition id under test.
    #[arg(long, default_value = WT_LADDER_PRIMARY_RENDITION)]
    rendition: String,
    /// Track name under test.
    #[arg(long, default_value = WT_PUBLISH_PRIMARY_VIDEO_TRACK)]
    track: String,
    /// Encoder/profile identity expected to produce byte-identical chunks.
    #[arg(long, default_value = "x264-hd3-v1")]
    profile: String,
    /// Number of media sequences to model.
    #[arg(long, default_value_t = 48)]
    sequence_count: u64,
    /// Logical milliseconds between produced media sequences.
    #[arg(long, default_value_t = 40)]
    segment_step_ms: u64,
    /// Additional delay after a publisher learns the control message before it can publish.
    #[arg(long, default_value_t = 25)]
    publisher_activation_delay_ms: u64,
    /// Delay before pre-activation media is backfilled.
    #[arg(long, default_value_t = 180)]
    publisher_backfill_delay_ms: u64,
    /// Source material identity as NODE:MATERIAL_ID. Different ids model independent captures that cannot be byte-identical.
    #[arg(long = "publisher-source-material", action = clap::ArgAction::Append)]
    publisher_source_material: Vec<String>,
    /// Required system convergence deadline. Set 0 to disable the deadline gate.
    #[arg(long, default_value_t = 3_500)]
    max_system_complete_ms: u64,
    /// Write the first failing system simulation artifact to this JSON path.
    #[arg(long)]
    failure_artifact: Option<PathBuf>,
    /// Pretty-print output JSON.
    #[arg(long, default_value_t = false)]
    pretty: bool,
    /// Emit failure JSON without returning a non-zero exit status.
    #[arg(long, default_value_t = false)]
    allow_failure: bool,
}

#[derive(Parser, Debug)]
struct SimDuplicatePublishersArgs {
    /// Replay an exact DuplicatePublisherScenario JSON. Use `-` to read stdin.
    #[arg(long)]
    scenario_json: Option<PathBuf>,
    /// First deterministic seed for the campaign.
    #[arg(long, default_value_t = 1)]
    seed: u64,
    /// Number of consecutive seed schedules to run.
    #[arg(long, default_value_t = 1024)]
    iterations: u64,
    /// Publisher node name. Repeat for more than two publishers.
    #[arg(long = "publisher", action = clap::ArgAction::Append)]
    publishers: Vec<String>,
    /// Logical stream id under test.
    #[arg(long, default_value = "la-kcop")]
    stream_id: String,
    /// Logical rendition id under test.
    #[arg(long, default_value = WT_LADDER_PRIMARY_RENDITION)]
    rendition: String,
    /// Track name under test.
    #[arg(long, default_value = WT_PUBLISH_PRIMARY_VIDEO_TRACK)]
    track: String,
    /// Encoder/profile identity expected to produce byte-identical chunks.
    #[arg(long, default_value = "x264-hd3-v1")]
    profile: String,
    /// First media sequence.
    #[arg(long, default_value_t = 0)]
    start_sequence: u64,
    /// Number of media sequences to model.
    #[arg(long, default_value_t = 48)]
    sequence_count: u64,
    /// Logical milliseconds between produced media sequences.
    #[arg(long, default_value_t = 40)]
    segment_step_ms: u64,
    /// Base network delivery delay per observation.
    #[arg(long, default_value_t = 5)]
    base_network_delay_ms: u64,
    /// Maximum deterministic jitter added to each delivery.
    #[arg(long, default_value_t = 75)]
    max_jitter_ms: u64,
    /// Probability of transient delivery drop, expressed per million observations.
    #[arg(long, default_value_t = 275_000)]
    transient_drop_per_million: u32,
    /// Delay before a dropped observation is backfilled.
    #[arg(long, default_value_t = 600)]
    backfill_after_ms: u64,
    /// Required duplicate convergence deadline. Set 0 to disable the deadline gate.
    #[arg(long, default_value_t = 3_000)]
    max_duplicate_complete_ms: u64,
    /// Partition fault as NODE:START_MS:END_MS:RELEASE_DELAY_MS. Repeatable.
    #[arg(long = "partition", action = clap::ArgAction::Append)]
    partitions: Vec<String>,
    /// Publisher outage as NODE:START_MS:END_MS:BACKFILL_DELAY_MS. Repeatable.
    #[arg(long = "publisher-outage", action = clap::ArgAction::Append)]
    publisher_outages: Vec<String>,
    /// Publisher content phase offset as NODE:SEQUENCE_OFFSET. Repeatable.
    #[arg(long = "publisher-sequence-offset", action = clap::ArgAction::Append)]
    publisher_sequence_offsets: Vec<String>,
    /// Publisher media clock offset as NODE:OFFSET_MS. Repeatable.
    #[arg(long = "publisher-media-time-offset", action = clap::ArgAction::Append)]
    publisher_media_time_offsets_ms: Vec<String>,
    /// Publisher that emits archive records without media timing. Repeatable.
    #[arg(long = "missing-media-timing-publisher", action = clap::ArgAction::Append)]
    missing_media_timing_publishers: Vec<String>,
    /// Encoder drift fault as NODE:SEQUENCE:PROFILE_ID. Repeatable.
    #[arg(long = "encoder-drift", action = clap::ArgAction::Append)]
    encoder_drifts: Vec<String>,
    /// Source material identity as NODE:MATERIAL_ID. Different ids model independent captures that cannot be byte-identical.
    #[arg(long = "publisher-source-material", action = clap::ArgAction::Append)]
    publisher_source_material: Vec<String>,
    /// Write the first failing simulation artifact to this JSON path.
    #[arg(long)]
    failure_artifact: Option<PathBuf>,
    /// Pretty-print output JSON.
    #[arg(long, default_value_t = false)]
    pretty: bool,
    /// Emit failure JSON without returning a non-zero exit status.
    #[arg(long, default_value_t = false)]
    allow_failure: bool,
}

#[derive(Parser, Debug)]
struct SignedRecordIndexArgs {
    /// JSON input path. Use `-` to read from stdin.
    ///
    /// Accepted shapes are `[SignedRecord, ...]`, `{ "records": [...] }`, or a single SignedRecord.
    #[arg(long, default_value = "-")]
    input: PathBuf,
    /// Override the generated_unix_ms timestamp in the output index.
    #[arg(long)]
    generated_unix_ms: Option<u64>,
    /// Pretty-print output JSON.
    #[arg(long, default_value_t = false)]
    pretty: bool,
}

#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
enum SignedRecordIndexInput {
    Records(Vec<SignedRecord>),
    Wrapped { records: Vec<SignedRecord> },
    Single(SignedRecord),
}

#[derive(Subcommand, Debug)]
enum IngestSource {
    /// Ingest from an HDHomeRun device.
    Hdhr {
        /// Hostname or IP (e.g. 192.168.1.10). If omitted, auto-discover.
        #[arg(long)]
        host: Option<String>,
        /// Device ID (uses <deviceid>.local when host is omitted).
        #[arg(long)]
        device_id: Option<String>,
        /// Channel number (e.g. 8.1).
        #[arg(long)]
        channel: Option<String>,
        /// Channel name (e.g. KQED).
        #[arg(long)]
        name: Option<String>,
        /// Prefer mDNS (hdhomerun.local) before UDP discovery.
        #[arg(long)]
        prefer_mdns: bool,
    },
    /// Ingest from an HLS playlist URL.
    Hls {
        /// HLS playlist URL.
        url: String,
        /// Ingest mode (passthrough, remux, transcode).
        #[arg(long, value_enum, default_value_t = HlsMode::Passthrough)]
        mode: HlsMode,
    },
    /// Ingest from a Linux DVB device.
    LinuxDvb {
        /// DVB adapter index.
        #[arg(long, default_value_t = 0)]
        adapter: u32,
        /// DVR device index.
        #[arg(long, default_value_t = 0)]
        dvr: u32,
        /// Optional tune command (repeat for each arg).
        #[arg(long)]
        tune_cmd: Vec<String>,
        /// Optional tune wait (ms).
        #[arg(long)]
        tune_wait_ms: Option<u64>,
    },
    /// Ingest from a raw TS file or URL.
    Ts {
        /// Input TS file or URL.
        input: String,
    },
}

fn main() -> Result<()> {
    // rustls 0.23 requires an explicit process-level CryptoProvider when multiple providers
    // are enabled. We install a default provider once at startup.
    if rustls::crypto::CryptoProvider::get_default().is_none() {
        let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
        let _ = rustls::crypto::ring::default_provider().install_default();
    }

    let _telemetry = init_tracing()?;
    let cli = Cli::parse();

    match cli.command {
        Commands::Ingest(args) => ingest(args)?,
        Commands::MoqPublish(args) => run_async(moq_publish(args))?,
        Commands::MoqSubscribe(args) => run_async(moq_subscribe(args))?,
        Commands::MoqSelftest(args) => run_async(moq_selftest(args))?,
        Commands::DirectPublish(args) => run_async(direct_publish(args))?,
        Commands::DirectSubscribe(args) => run_async(direct_subscribe(args))?,
        Commands::WsPublish(args) => run_async(ws_publish(args))?,
        Commands::WsSubscribe(args) => run_async(ws_subscribe(args))?,
        Commands::WtPublish(args) => run_async(wt_publish(args))?,
        Commands::NbcBootstrap(args) => nbc_bootstrap(args)?,
        Commands::NbcWtPublish(args) => run_async(nbc_wt_publish(args))?,
        Commands::WtArchive(args) => run_async(wt_archive(args))?,
        Commands::WtArchiveServe(args) => run_async(wt_archive_serve(args))?,
        Commands::ControlAnnounce(args) => run_async(control_announce(args))?,
        Commands::ControlListen(args) => run_async(control_listen(args))?,
        Commands::ControlResolve(args) => run_async(control_resolve(args))?,
        Commands::ControlBridgeWeb(args) => run_async(control_bridge_web(args))?,
        Commands::PublisherProofSegment(args) => publisher_proof_segment_command(args)?,
        Commands::PublisherProofWindows(args) => publisher_proof_windows_command(args)?,
        Commands::PublisherProofDuplicates(args) => publisher_proof_duplicates_command(args)?,
        Commands::PublisherProofCompare(args) => publisher_proof_compare_command(args)?,
        Commands::PublisherProofRemoteCompare(args) => {
            publisher_proof_remote_compare_command(args)?
        }
        Commands::PublisherProofArchiveSource(args) => {
            publisher_proof_archive_source_command(args)?
        }
        Commands::ArchiveConvergence(args) => archive_convergence_command(args)?,
        Commands::ArchiveConvergenceMeasure(args) => {
            run_async(archive_convergence_measure_command(args))?
        }
        Commands::ArchiveConvergenceMeasureServe(args) => {
            run_async(archive_convergence_measure_serve(args))?
        }
        Commands::ArchiveConvergenceServe(args) => run_async(archive_convergence_serve(args))?,
        Commands::SimControlPlane(args) => sim_control_plane_command(args)?,
        Commands::SimSystem(args) => sim_system_command(args)?,
        Commands::SimDuplicatePublishers(args) => sim_duplicate_publishers_command(args)?,
        Commands::SignedRecordIndex(args) => signed_record_index_command(args)?,
    }

    Ok(())
}

fn signed_record_index_command(args: SignedRecordIndexArgs) -> Result<()> {
    let input = read_signed_record_index_input(&args.input)?;
    let records = parse_signed_record_index_input(&input)?;
    let generated_unix_ms = args.generated_unix_ms.unwrap_or_else(now_unix_ms);
    let index = signed_record_index(&records, generated_unix_ms);
    let stdout = std::io::stdout();
    let mut out = stdout.lock();
    if args.pretty {
        serde_json::to_writer_pretty(&mut out, &index)
            .context("failed to write signed-record index")?;
    } else {
        serde_json::to_writer(&mut out, &index).context("failed to write signed-record index")?;
    }
    out.write_all(b"\n")
        .context("failed to newline-terminate signed-record index")?;
    Ok(())
}

fn read_signed_record_index_input(path: &Path) -> Result<Vec<u8>> {
    if path == Path::new("-") {
        let mut input = Vec::new();
        std::io::stdin()
            .read_to_end(&mut input)
            .context("failed to read signed-record input from stdin")?;
        return Ok(input);
    }
    fs::read(path).with_context(|| format!("failed to read signed-record input {}", path.display()))
}

fn parse_signed_record_index_input(input: &[u8]) -> Result<Vec<SignedRecord>> {
    match serde_json::from_slice::<SignedRecordIndexInput>(input)
        .context("failed to parse signed-record input JSON")?
    {
        SignedRecordIndexInput::Records(records) => Ok(records),
        SignedRecordIndexInput::Wrapped { records } => Ok(records),
        SignedRecordIndexInput::Single(record) => Ok(vec![record]),
    }
}

struct TelemetryGuard {
    tracer_provider: Option<SdkTracerProvider>,
}

impl Drop for TelemetryGuard {
    fn drop(&mut self) {
        if let Some(provider) = self.tracer_provider.take() {
            let _ = provider.shutdown();
        }
    }
}

fn init_tracing() -> Result<TelemetryGuard> {
    // Keep stdout reserved for machine-readable output (endpoint addr, etc).
    let filter = tracing_subscriber::EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
    let fmt_layer = tracing_subscriber::fmt::layer().with_writer(std::io::stderr);

    if let Some(endpoint) = otel_traces_endpoint() {
        let service_name =
            std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| "ec-node".to_string());
        let service_instance = std::env::var("EVERY_CHANNEL_NODE_NAME")
            .or_else(|_| std::env::var("HOSTNAME"))
            .unwrap_or_else(|_| "unknown".to_string());
        let exporter = SpanExporter::builder()
            .with_http()
            .with_protocol(Protocol::HttpBinary)
            .with_endpoint(endpoint)
            .build()
            .context("failed to build OTLP span exporter")?;
        let tracer_provider = SdkTracerProvider::builder()
            .with_batch_exporter(exporter)
            .with_resource(
                Resource::builder()
                    .with_service_name(service_name.clone())
                    .with_attribute(KeyValue::new("service.namespace", "every.channel"))
                    .with_attribute(KeyValue::new("service.instance.id", service_instance))
                    .build(),
            )
            .build();
        let tracer = tracer_provider.tracer("ec-node");
        let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
        tracing_subscriber::registry()
            .with(filter)
            .with(fmt_layer)
            .with(otel_layer)
            .init();
        Ok(TelemetryGuard {
            tracer_provider: Some(tracer_provider),
        })
    } else {
        tracing_subscriber::registry()
            .with(filter)
            .with(fmt_layer)
            .init();
        Ok(TelemetryGuard {
            tracer_provider: None,
        })
    }
}

fn otel_traces_endpoint() -> Option<String> {
    for name in [
        "EVERY_CHANNEL_OTEL_TRACES_ENDPOINT",
        "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
        "OTEL_EXPORTER_OTLP_ENDPOINT",
    ] {
        let Ok(raw) = std::env::var(name) else {
            continue;
        };
        let trimmed = raw.trim().trim_end_matches('/');
        if trimmed.is_empty() {
            continue;
        }
        if trimmed.ends_with("/v1/traces") {
            return Some(trimmed.to_string());
        }
        return Some(format!("{trimmed}/v1/traces"));
    }
    None
}

fn run_async<F>(future: F) -> Result<()>
where
    F: Future<Output = Result<()>>,
{
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()?;
    runtime.block_on(future)
}

fn ingest(args: IngestArgs) -> Result<()> {
    fs::create_dir_all(&args.chunk_dir)
        .with_context(|| format!("failed to create {}", args.chunk_dir.display()))?;

    let deterministic = deterministic_enabled(args.deterministic);
    let (source, _needs_transcode): (Box<dyn StreamSource>, bool) = match args.source {
        IngestSource::Hls { url, mut mode } => {
            if deterministic {
                mode = HlsMode::Transcode;
            }
            (Box::new(HlsSource { url, mode }), false)
        }
        IngestSource::Hdhr {
            host,
            device_id,
            channel,
            name,
            prefer_mdns,
        } => (
            Box::new(HdhrSource {
                host,
                device_id,
                channel,
                name,
                prefer_mdns,
            }),
            deterministic,
        ),
        IngestSource::LinuxDvb {
            adapter,
            dvr,
            tune_cmd,
            tune_wait_ms,
        } => (
            Box::new(LinuxDvbSource {
                adapter,
                dvr,
                tune_cmd,
                tune_wait_ms,
            }),
            deterministic,
        ),
        IngestSource::Ts { input } => (Box::new(TsSource { input }), deterministic),
    };

    let source_id = source.source_id();
    let reader = source.open_stream()?;
    let encoder_profile_id = if deterministic {
        "deterministic-h264-aac".to_string()
    } else {
        // NOTE: We still normalize into CMAF for interoperability, even when determinism is off.
        "h264-aac".to_string()
    };

    // CMAF-only: segment into init.mp4 + segment_*.m4s via ffmpeg.
    let out_dir = args.chunk_dir.join("cmaf");
    let (init_path, segments) = chunk_stream_cmaf_ffmpeg(
        reader,
        &out_dir,
        args.chunk_ms,
        args.max_chunks.unwrap_or(usize::MAX),
        deterministic,
    )?;
    let mut chunk_hashes = Vec::with_capacity(1 + segments.len());
    // Chunk index 0 is always init.mp4; segments are 1..N.
    let (init_bytes, init_hash) = read_chunk_bytes_and_hash(&init_path)?;
    chunk_hashes.push(init_hash);
    let mut segment_meta = Vec::with_capacity(segments.len());
    for seg_path in &segments {
        let (bytes, hash) = read_chunk_bytes_and_hash(seg_path)?;
        chunk_hashes.push(hash.clone());
        segment_meta.push((seg_path.clone(), bytes, hash));
    }
    let chunk_start_index = 0u64;
    let created_unix_ms = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as u64;
    let relay = FileRelay::new(args.relay_dir);
    let track = TrackName {
        namespace: "every.channel".to_string(),
        name: match args.stream_id {
            Some(stream_id) => stream_id,
            None => {
                default_stream_id_for_source(source.as_ref(), format!("chunk-{}ms", args.chunk_ms))?
            }
        },
    };
    let stream_id = StreamId(track.name.clone());
    let manifest_payload = build_manifest(
        stream_id,
        format!("epoch-{created_unix_ms}"),
        args.chunk_ms,
        chunk_start_index,
        encoder_profile_id,
        created_unix_ms,
        vec![StreamMetadata {
            key: "source_kind".to_string(),
            value: source_id.kind.clone(),
        }],
        chunk_hashes,
    )?;
    let manifest_id = manifest_payload.manifest_id.clone();
    let manifest_path = args.chunk_dir.join("manifest.json");
    fs::write(
        &manifest_path,
        serde_json::to_vec_pretty(&manifest_payload)?,
    )?;

    let network_secret = parse_network_secret(args.network_secret)?;

    // Publish init at chunk_index 0 to avoid colliding with segment_000000.
    publish_chunk_file(
        &relay,
        &track,
        TsChunk {
            index: 0,
            path: init_path,
            timing: ec_chopper::ChunkTiming {
                chunk_index: 0,
                chunk_start_27mhz: None,
                chunk_duration_27mhz: 0,
                utc_start_unix: None,
                sync_status: "init".to_string(),
            },
        },
        "video/mp4",
        Some(init_bytes),
        network_secret.as_deref(),
        Some(&manifest_id),
    )?;
    for (i, (seg_path, bytes, _hash)) in segment_meta.into_iter().enumerate() {
        let chunk_index = (i as u64) + 1;
        publish_chunk_file(
            &relay,
            &track,
            TsChunk {
                index: chunk_index,
                path: seg_path,
                timing: ec_chopper::ChunkTiming {
                    chunk_index,
                    chunk_start_27mhz: None,
                    chunk_duration_27mhz: args.chunk_ms * 27_000,
                    utc_start_unix: None,
                    sync_status: "cmaf".to_string(),
                },
            },
            "video/iso.segment",
            Some(bytes),
            network_secret.as_deref(),
            Some(&manifest_id),
        )?;
    }

    Ok(())
}

fn publish_chunk_file(
    relay: &FileRelay,
    track: &TrackName,
    chunk: TsChunk,
    content_type: &str,
    data_override: Option<Vec<u8>>,
    network_secret: Option<&[u8]>,
    manifest_id: Option<&str>,
) -> Result<()> {
    let (data, chunk_hash) = match data_override {
        Some(bytes) => {
            let hash = blake3::hash(&bytes).to_hex().to_string();
            (bytes, hash)
        }
        None => read_chunk_bytes_and_hash(&chunk.path)?,
    };
    let object = build_object(
        chunk,
        data,
        chunk_hash,
        None,
        network_secret,
        manifest_id,
        content_type,
        &track.name,
    )?;
    relay.write_object(
        track,
        GroupId(
            object
                .meta
                .timing
                .as_ref()
                .map(|t| t.chunk_index)
                .unwrap_or(0),
        ),
        ObjectId(
            object
                .meta
                .timing
                .as_ref()
                .map(|t| t.chunk_index)
                .unwrap_or(0),
        ),
        &object,
    )
}

fn chunk_stream_cmaf_ffmpeg(
    mut reader: Box<dyn Read + Send>,
    out_dir: &std::path::Path,
    chunk_ms: u64,
    max_segments: usize,
    deterministic: bool,
) -> Result<(std::path::PathBuf, Vec<std::path::PathBuf>)> {
    let _ = fs::remove_dir_all(out_dir);
    fs::create_dir_all(out_dir)
        .with_context(|| format!("failed to create {}", out_dir.display()))?;

    let profile = if deterministic {
        Some(ec_chopper::deterministic_h264_profile())
    } else {
        // For now, keep encoder args stable even when determinism is off.
        Some(ec_chopper::deterministic_h264_profile())
    };

    let mut cmd = Command::new("ffmpeg");
    cmd.current_dir(out_dir);
    cmd.arg("-hide_banner")
        .arg("-loglevel")
        .arg("error")
        .arg("-nostdin")
        .arg("-y")
        .arg("-i")
        .arg("pipe:0")
        // Keep stream mapping predictable.
        .arg("-map")
        .arg("0:v:0")
        .arg("-map")
        .arg("0:a:0?")
        .arg("-sn")
        .arg("-dn")
        .arg("-map_metadata")
        .arg("-1")
        // Reduce opportunities for non-deterministic scheduling in filters/decoders.
        .arg("-filter_threads")
        .arg("1")
        .arg("-filter_complex_threads")
        .arg("1")
        .arg("-threads")
        .arg("1");

    if let Some(profile) = profile {
        for arg in ec_chopper::ffmpeg_profile_args(&profile) {
            cmd.arg(arg);
        }
    }

    let seg_time = format!("{:.3}", chunk_ms as f64 / 1000.0);
    cmd.arg("-f")
        .arg("hls")
        .arg("-hls_time")
        .arg(seg_time)
        .arg("-hls_list_size")
        .arg("0")
        .arg("-hls_segment_type")
        .arg("fmp4")
        .arg("-hls_flags")
        .arg("independent_segments")
        .arg("-hls_fmp4_init_filename")
        .arg("init.mp4")
        .arg("-hls_segment_filename")
        .arg("segment_%06d.m4s")
        .arg("index.m3u8")
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .stderr(Stdio::inherit());

    let mut child = cmd.spawn().with_context(|| "failed to spawn ffmpeg")?;
    let mut stdin = child
        .stdin
        .take()
        .ok_or_else(|| anyhow!("ffmpeg stdin unavailable"))?;
    let writer = std::thread::spawn(move || -> Result<()> {
        std::io::copy(&mut reader, &mut stdin)?;
        Ok(())
    });

    let init_path = out_dir.join("init.mp4");
    wait_for_stable_file(&init_path, Duration::from_secs(20))?;

    let mut segments = Vec::new();
    for i in 0..max_segments {
        let seg_path = out_dir.join(format!("segment_{i:06}.m4s"));
        match wait_for_stable_file(&seg_path, Duration::from_secs(30)) {
            Ok(()) => segments.push(seg_path),
            Err(err) => {
                // If ffmpeg ended cleanly, stop; otherwise bubble the error.
                if let Ok(Some(status)) = child.try_wait() {
                    if status.success() {
                        break;
                    }
                    return Err(anyhow!("ffmpeg exited with {status} ({err:#})"));
                }
                return Err(err);
            }
        }
    }

    let _ = child.kill();
    let _ = child.wait();
    let _ = writer.join();

    Ok((init_path, segments))
}

fn parse_network_secret(value: Option<String>) -> Result<Option<Vec<u8>>> {
    let value = value.or_else(|| std::env::var("EVERY_CHANNEL_NETWORK_SECRET").ok());
    let Some(value) = value else { return Ok(None) };
    let bytes = hex::decode(value).context("network secret must be hex")?;
    Ok(Some(bytes))
}

fn deterministic_enabled(flag: bool) -> bool {
    if flag {
        return true;
    }
    std::env::var("EVERY_CHANNEL_DETERMINISTIC")
        .ok()
        .map(|value| {
            let value = value.trim().to_ascii_lowercase();
            value == "1" || value == "true" || value == "yes" || value == "on"
        })
        .unwrap_or(false)
}

fn default_stream_id_for_source(
    source: &dyn StreamSource,
    profile: impl Into<String>,
) -> Result<String> {
    let source_id = source.source_id();
    let profile = profile.into();
    let stream_key = match source.broadcast_id()? {
        Some(broadcast) => StreamKey {
            version: 1,
            broadcast: Some(broadcast),
            source: None,
            profile: Some(profile.clone()),
            variant: None,
        },
        None => StreamKey {
            version: 1,
            broadcast: None,
            source: Some(source_id),
            profile: Some(profile),
            variant: None,
        },
    };

    Ok(stream_key.to_stream_id().0)
}

fn read_chunk_bytes_and_hash(path: &std::path::Path) -> Result<(Vec<u8>, String)> {
    let data = fs::read(path).with_context(|| format!("failed to read {}", path.display()))?;
    let hash = blake3::hash(&data).to_hex().to_string();
    Ok((data, hash))
}

fn collect_manifest_signatures(
    manifest_id: &str,
    body: &ManifestBody,
) -> Result<Vec<ec_core::ManifestSignature>> {
    let mut signatures = Vec::new();
    if let Some(keypair) = load_manifest_keypair_from_env().map_err(|err| anyhow!(err))? {
        signatures.push(sign_manifest_id(manifest_id, &keypair));
    }
    if let Some(keypair) = load_ethereum_manifest_keypair_from_env().map_err(|err| anyhow!(err))? {
        signatures.push(sign_manifest_body_eip712(body, &keypair).map_err(|err| anyhow!(err))?);
    }
    Ok(signatures)
}

fn build_manifest(
    stream_id: StreamId,
    epoch_id: impl Into<String>,
    chunk_duration_ms: u64,
    chunk_start_index: u64,
    encoder_profile_id: impl Into<String>,
    created_unix_ms: u64,
    metadata: Vec<StreamMetadata>,
    chunk_hashes: Vec<String>,
) -> Result<Manifest> {
    let body = build_manifest_body_for_chunks(
        stream_id,
        epoch_id,
        chunk_duration_ms,
        chunk_start_index,
        encoder_profile_id,
        created_unix_ms,
        metadata,
        &chunk_hashes,
    )?;
    let manifest_id = body.manifest_id()?;
    let signatures = collect_manifest_signatures(&manifest_id, &body)?;
    let mut manifest = Manifest {
        body,
        manifest_id,
        signatures,
        commitments: Vec::new(),
    };
    manifest.commitments = manifest_commitments(&manifest).map_err(|err| anyhow!("{err}"))?;
    Ok(manifest)
}

async fn submit_manifest_observation(
    sink: Option<&ObservationSink>,
    manifest: &Manifest,
    broadcast_name: &str,
    track_name: &str,
    manifest_track: &str,
) -> Result<()> {
    let Some(sink) = sink else {
        return Ok(());
    };

    let locator = ManifestObservationLocator {
        transport: "moq-publish".to_string(),
        broadcast_name: broadcast_name.to_string(),
        track_name: track_name.to_string(),
        manifest_track: manifest_track.to_string(),
        stream_id: manifest.body.stream_id.0.clone(),
        epoch_id: manifest.body.epoch_id.clone(),
        manifest_id: manifest.manifest_id.clone(),
    };
    let submitted = sink.submit_manifest(manifest, locator).await?;
    eprintln!(
        "observation submitted: observation_hash={} slot_hash={} stream_hash={} epoch_hash={} data_root={} locator_hash={} sequence={}",
        submitted.observation_hash,
        submitted.slot_hash,
        submitted.stream_hash,
        submitted.epoch_hash,
        submitted.data_root,
        submitted.locator_hash,
        submitted.sequence
    );
    Ok(())
}

#[derive(Debug, Clone)]
struct CmafVariantSpec {
    id: String,
    width: u32,
    height: u32,
    video_bitrate_kbps: u32,
}

fn cmaf_ladder_variants(preset: CmafLadderPreset) -> Vec<CmafVariantSpec> {
    match preset {
        CmafLadderPreset::Hd3 => vec![
            CmafVariantSpec {
                id: "1080p".to_string(),
                width: 1920,
                height: 1080,
                video_bitrate_kbps: 6000,
            },
            CmafVariantSpec {
                id: "720p".to_string(),
                width: 1280,
                height: 720,
                video_bitrate_kbps: 3000,
            },
            CmafVariantSpec {
                id: "480p".to_string(),
                width: 854,
                height: 480,
                video_bitrate_kbps: 1200,
            },
        ],
    }
}

fn sanitize_component(value: &str) -> String {
    value
        .chars()
        .map(|c| match c {
            'a'..='z' | '0'..='9' | '-' | '_' | '/' => c,
            'A'..='Z' => c.to_ascii_lowercase(),
            _ => '_',
        })
        .collect()
}

fn derive_variant_stream_id(base_stream_id: &str, variant_id: &str) -> String {
    // Match StreamKey encoding style (`variant-...`) without requiring we reconstruct StreamKey.
    let v = sanitize_component(variant_id);
    format!("{}/variant-{}", base_stream_id.trim_end_matches('/'), v)
}

fn build_multi_variant_manifest(
    base_stream_id: StreamId,
    epoch_id: String,
    chunk_duration_ms: u64,
    chunk_start_index: u64,
    encoder_profile_id: String,
    created_unix_ms: u64,
    metadata: Vec<StreamMetadata>,
    variants: &[CmafVariantSpec],
    variant_chunk_start_index: u64,
    per_variant_hash: Vec<(String, String)>,
) -> Result<Manifest> {
    let mut entries = Vec::new();
    for variant in variants {
        let Some((_, hash)) = per_variant_hash.iter().find(|(id, _)| id == &variant.id) else {
            return Err(anyhow!("missing hash for variant {}", variant.id));
        };
        let chunk_hashes = vec![hash.clone()];
        let merkle_root =
            ec_core::merkle_root_from_hashes(&chunk_hashes).map_err(|err| anyhow!("{err}"))?;
        let stream_id = StreamId(derive_variant_stream_id(&base_stream_id.0, &variant.id));
        entries.push(ManifestVariant {
            variant_id: variant.id.clone(),
            stream_id,
            chunk_start_index: variant_chunk_start_index,
            total_chunks: 1,
            merkle_root,
            chunk_hashes,
            metadata: vec![
                StreamMetadata {
                    key: "width".to_string(),
                    value: variant.width.to_string(),
                },
                StreamMetadata {
                    key: "height".to_string(),
                    value: variant.height.to_string(),
                },
                StreamMetadata {
                    key: "video_bitrate_kbps".to_string(),
                    value: variant.video_bitrate_kbps.to_string(),
                },
            ],
        });
    }
    entries.sort_by(|a, b| a.variant_id.cmp(&b.variant_id));
    let roots = entries
        .iter()
        .map(|v| v.merkle_root.clone())
        .collect::<Vec<_>>();
    let body_root = ec_core::merkle_root_from_hashes(&roots).map_err(|err| anyhow!("{err}"))?;
    let body = ec_core::ManifestBody {
        stream_id: base_stream_id,
        epoch_id,
        chunk_duration_ms,
        total_chunks: 1,
        chunk_start_index,
        encoder_profile_id,
        merkle_root: body_root,
        created_unix_ms,
        metadata,
        chunk_hashes: Vec::new(),
        variants: Some(entries),
    };
    let manifest_id = body.manifest_id()?;
    let signatures = collect_manifest_signatures(&manifest_id, &body)?;
    let mut manifest = Manifest {
        body,
        manifest_id,
        signatures,
        commitments: Vec::new(),
    };
    manifest.commitments = manifest_commitments(&manifest).map_err(|err| anyhow!("{err}"))?;
    Ok(manifest)
}

struct EpochBuffer {
    capacity: usize,
    chunks: Vec<TsChunk>,
    data: Vec<Option<Vec<u8>>>,
    hashes: Vec<String>,
    start_index: Option<u64>,
}

impl EpochBuffer {
    fn new(capacity: usize) -> Self {
        Self {
            capacity: capacity.max(1),
            chunks: Vec::new(),
            data: Vec::new(),
            hashes: Vec::new(),
            start_index: None,
        }
    }

    fn push(&mut self, chunk: TsChunk, data: Option<Vec<u8>>, hash: String) {
        if self.start_index.is_none() {
            self.start_index = Some(chunk.timing.chunk_index);
        }
        self.chunks.push(chunk);
        self.data.push(data);
        self.hashes.push(hash);
    }

    fn is_full(&self) -> bool {
        self.chunks.len() >= self.capacity
    }

    fn is_empty(&self) -> bool {
        self.chunks.is_empty()
    }

    fn start_index(&self) -> u64 {
        self.start_index.unwrap_or(0)
    }

    fn take(&mut self) -> (Vec<TsChunk>, Vec<Option<Vec<u8>>>, Vec<String>) {
        self.start_index = None;
        let chunks = std::mem::take(&mut self.chunks);
        let data = std::mem::take(&mut self.data);
        let hashes = std::mem::take(&mut self.hashes);
        (chunks, data, hashes)
    }
}

fn parse_manifest_allowlist(value: Option<&str>) -> Option<HashSet<String>> {
    let value = value?.trim();
    if value.is_empty() {
        return None;
    }
    let set = value
        .split(|c: char| c == ',' || c == ';' || c.is_whitespace())
        .filter_map(|token| {
            let trimmed = token.trim();
            if trimmed.is_empty() {
                None
            } else {
                Some(trimmed.to_string())
            }
        })
        .collect::<HashSet<_>>();
    if set.is_empty() {
        None
    } else {
        Some(set)
    }
}

fn validate_manifest(manifest: &Manifest, allowlist: Option<&HashSet<String>>) -> bool {
    let body_id = match manifest.body.manifest_id() {
        Ok(id) => id,
        Err(_) => return false,
    };
    if body_id != manifest.manifest_id {
        return false;
    }
    if let Some(variants) = manifest.body.variants.as_ref().filter(|v| !v.is_empty()) {
        // Multi-variant: validate each variant and ensure body merkle_root commits to per-variant roots.
        let mut roots = Vec::with_capacity(variants.len());
        for variant in variants {
            if variant.chunk_hashes.len() != variant.total_chunks as usize {
                return false;
            }
            match ec_core::merkle_root_from_hashes(&variant.chunk_hashes) {
                Ok(root) if root == variant.merkle_root => {}
                _ => return false,
            }
            roots.push((variant.variant_id.clone(), variant.merkle_root.clone()));
        }
        roots.sort_by(|a, b| a.0.cmp(&b.0));
        let ordered = roots.into_iter().map(|(_, root)| root).collect::<Vec<_>>();
        match ec_core::merkle_root_from_hashes(&ordered) {
            Ok(root) if root == manifest.body.merkle_root => {}
            _ => return false,
        }
    } else if !manifest.body.chunk_hashes.is_empty() {
        if manifest.body.chunk_hashes.len() != manifest.body.total_chunks as usize {
            return false;
        }
        match ec_core::merkle_root_from_hashes(&manifest.body.chunk_hashes) {
            Ok(root) if root == manifest.body.merkle_root => {}
            _ => return false,
        }
    }
    if !matches!(manifest_commitments_match(manifest), Ok(true)) {
        return false;
    }

    if let Some(allowlist) = allowlist {
        return manifest.signatures.iter().any(|sig| {
            verify_manifest_signature_with_body(&manifest.manifest_id, &manifest.body, sig)
                && allowlist.contains(&sig.signer_id)
        });
    }

    if manifest.signatures.is_empty() {
        // Unsigned manifests are only acceptable when they include full hashes for verification.
        return !manifest.body.chunk_hashes.is_empty()
            || manifest
                .body
                .variants
                .as_ref()
                .is_some_and(|v| !v.is_empty());
    }
    manifest
        .signatures
        .iter()
        .any(|sig| verify_manifest_signature_with_body(&manifest.manifest_id, &manifest.body, sig))
}

fn strip_init_suffix(key_id: &str) -> &str {
    key_id.strip_suffix("/init").unwrap_or(key_id)
}

fn manifest_hash_for_chunk(
    manifest: &Manifest,
    stream_id: &str,
    chunk_index: u64,
) -> Option<String> {
    if let Some(variants) = manifest.body.variants.as_ref() {
        let variant = variants.iter().find(|v| v.stream_id.0 == stream_id)?;
        if chunk_index < variant.chunk_start_index {
            return None;
        }
        let offset = (chunk_index - variant.chunk_start_index) as usize;
        return variant.chunk_hashes.get(offset).cloned();
    }

    // Legacy single-variant manifests.
    if manifest.body.stream_id.0 != stream_id {
        return None;
    }
    if chunk_index < manifest.body.chunk_start_index {
        return None;
    }
    let offset = (chunk_index - manifest.body.chunk_start_index) as usize;
    manifest.body.chunk_hashes.get(offset).cloned()
}

fn manifest_covers_stream_index(manifest: &Manifest, stream_id: &str, chunk_index: u64) -> bool {
    if let Some(variants) = manifest.body.variants.as_ref() {
        let Some(variant) = variants.iter().find(|v| v.stream_id.0 == stream_id) else {
            return false;
        };
        if chunk_index < variant.chunk_start_index {
            return false;
        }
        let end = variant
            .chunk_start_index
            .saturating_add(variant.total_chunks as u64);
        return chunk_index < end;
    }

    if manifest.body.stream_id.0 != stream_id {
        return false;
    }
    if chunk_index < manifest.body.chunk_start_index {
        return false;
    }
    let end = manifest
        .body
        .chunk_start_index
        .saturating_add(manifest.body.total_chunks as u64);
    chunk_index < end
}

fn find_manifest_for_stream_index(
    store: &HashMap<String, Manifest>,
    stream_id: &str,
    chunk_index: u64,
) -> Option<Manifest> {
    // Prefer the latest manifest whose range covers the index for this stream.
    let mut best: Option<&Manifest> = None;
    for manifest in store.values() {
        if !manifest_covers_stream_index(manifest, stream_id, chunk_index) {
            continue;
        }
        match best {
            None => best = Some(manifest),
            Some(current) => {
                if manifest.body.chunk_start_index >= current.body.chunk_start_index {
                    best = Some(manifest);
                }
            }
        }
    }
    best.cloned()
}

fn build_object(
    chunk: TsChunk,
    data: Vec<u8>,
    chunk_hash: String,
    chunk_proof: Option<Vec<String>>,
    network_secret: Option<&[u8]>,
    manifest_id: Option<&str>,
    content_type: &str,
    key_id: &str,
) -> Result<ObjectPayload> {
    let created_unix_ms = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as u64;

    let timing = TimingMeta {
        chunk_index: chunk.timing.chunk_index,
        chunk_start_27mhz: chunk.timing.chunk_start_27mhz.unwrap_or(0),
        chunk_duration_27mhz: chunk.timing.chunk_duration_27mhz,
        utc_start_unix: chunk.timing.utc_start_unix,
        sync_status: chunk.timing.sync_status.clone(),
    };

    let encrypted = encrypt_stream_data(key_id, chunk.timing.chunk_index, &data, network_secret);
    let meta = ObjectMeta {
        created_unix_ms,
        content_type: content_type.to_string(),
        size_bytes: encrypted.ciphertext.len() as u64,
        timing: Some(timing),
        encryption: Some(ec_moq::EncryptionMeta {
            alg: encrypted.alg.to_string(),
            key_id: key_id.to_string(),
            nonce_hex: hex::encode(encrypted.nonce),
        }),
        chunk_hash: Some(chunk_hash),
        chunk_hash_alg: Some("blake3".to_string()),
        chunk_proof,
        chunk_proof_alg: Some(MERKLE_PROOF_ALG_BLAKE3.to_string()),
        manifest_id: manifest_id.map(|value| value.to_string()),
    };

    Ok(ObjectPayload {
        meta,
        data: encrypted.ciphertext,
    })
}

fn flush_epoch_publish(
    publish_set: &mut MoqPublishSet,
    object_track_name: &str,
    manifest_track_name: &str,
    publish_chunks: bool,
    publish_manifests: bool,
    epoch_buffer: &mut EpochBuffer,
    stream_id_value: &StreamId,
    chunk_ms: u64,
    encoder_profile_id: &str,
    source_kind: &str,
    network_secret: Option<&[u8]>,
    segment_content_type: &str,
    key_id: &str,
    object_sequence: &mut u64,
    manifest_sequence: &mut u64,
    announce_tx: Option<&tokio::sync::mpsc::UnboundedSender<ManifestSummary>>,
) -> Result<Option<Manifest>> {
    if epoch_buffer.is_empty() {
        return Ok(None);
    }

    let (chunks, datas, hashes) = epoch_buffer.take();
    let start_index = chunks
        .first()
        .map(|chunk| chunk.timing.chunk_index)
        .unwrap_or_else(|| epoch_buffer.start_index());
    let created_unix_ms = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as u64;

    let mut manifest_id = None;
    let mut published_manifest = None;
    if publish_manifests {
        let manifest = build_manifest(
            stream_id_value.clone(),
            format!("epoch-{start_index}"),
            chunk_ms,
            start_index,
            encoder_profile_id.to_string(),
            created_unix_ms,
            vec![StreamMetadata {
                key: "source_kind".to_string(),
                value: source_kind.to_string(),
            }],
            hashes.clone(),
        )?;
        manifest_id = Some(manifest.manifest_id.clone());
        publish_set.publish_manifest(manifest_track_name, *manifest_sequence, &manifest)?;
        *manifest_sequence += 1;
        if let Some(tx) = announce_tx {
            let _ = tx.send(manifest.summary());
        }
        published_manifest = Some(manifest);
    }

    // Compute per-chunk Merkle proofs so subscribers can validate membership
    // even if future manifests omit the full chunk hash list.
    let mut proofs = Vec::with_capacity(hashes.len());
    for (offset, _) in hashes.iter().enumerate() {
        proofs.push(merkle_proof_for_index(&hashes, offset)?);
    }

    if publish_chunks {
        for (((chunk, data), hash), proof) in chunks.into_iter().zip(datas).zip(hashes).zip(proofs)
        {
            let Some(data) = data else {
                return Err(anyhow!("missing chunk data for publish"));
            };
            let object = build_object(
                chunk,
                data,
                hash,
                Some(proof),
                network_secret,
                manifest_id.as_deref(),
                segment_content_type,
                key_id,
            )?;
            tracing::info!(
                "publish segment chunk_index={} bytes={} content_type={}",
                object
                    .meta
                    .timing
                    .as_ref()
                    .map(|t| t.chunk_index)
                    .unwrap_or(0),
                object.data.len(),
                object.meta.content_type
            );
            publish_set.publish_object(object_track_name, GroupId(*object_sequence), object)?;
            *object_sequence += 1;
        }
    }

    Ok(published_manifest)
}

#[cfg(test)]
mod tests {
    use super::*;
    use ec_core::BroadcastId;
    use std::io::Cursor;

    #[test]
    fn wt_publish_defaults_are_smooth_live_defaults() {
        assert_eq!(
            WT_PUBLISH_VIDEO_FILTER,
            "yadif=mode=send_frame:parity=auto:deint=interlaced,fps=30000/1001"
        );
        assert_eq!(WT_PUBLISH_GOP_FRAMES, 30);
        assert_eq!(WT_PUBLISH_VIDEO_PRESET, "medium");
        assert_eq!(WT_PUBLISH_VIDEO_CRF, 23);
        assert_eq!(
            WT_PUBLISH_MOVFLAGS,
            "empty_moov+frag_keyframe+separate_moof+omit_tfhd_offset"
        );
    }

    #[test]
    fn wt_publish_defaults_align_start_to_publisher_proof_cadence() {
        let args = WtPublishArgs::parse_from([
            "wt-publish",
            "--url",
            "https://lax.relay.every.channel/anon",
            "--name",
            "la-test",
            "--input",
            "/tmp/source.mp4",
        ]);

        assert_eq!(
            args.publisher_start_boundary_ms,
            WT_PUBLISH_ARCHIVE_SEGMENT_DURATION_MS
        );
        assert!(args.publisher_wallclock_timestamps);
        assert_eq!(0, publisher_start_boundary_delay_ms(10_010, 1001));
        assert_eq!(1, publisher_start_boundary_delay_ms(10_009, 1001));
        assert_eq!(0, publisher_start_boundary_delay_ms(10_009, 0));
    }

    #[test]
    fn wt_publish_ffmpeg_plan_is_deterministic_live_profile() {
        let args = WtPublishArgs::parse_from([
            "wt-publish",
            "--url",
            "https://lax.relay.every.channel/anon",
            "--name",
            "la-test",
            "--input",
            "/tmp/source.ts",
        ]);

        let ffmpeg_args = wt_publish_ffmpeg_args(&args, "/tmp/source.ts")
            .into_iter()
            .map(|arg| arg.to_string_lossy().into_owned())
            .collect::<Vec<_>>();

        assert_eq!(
            ffmpeg_args,
            vec![
                "-hide_banner",
                "-loglevel",
                "error",
                "-nostats",
                "-fflags",
                "+nobuffer+bitexact",
                "-flags",
                "low_delay",
                "-copyts",
                "-use_wallclock_as_timestamps",
                "1",
                "-i",
                "/tmp/source.ts",
                "-map",
                "0:v:0",
                "-map",
                "0:a:0?",
                "-c:v",
                "libx264",
                "-vf",
                WT_PUBLISH_VIDEO_FILTER,
                "-preset",
                WT_PUBLISH_VIDEO_PRESET,
                "-tune",
                "zerolatency",
                "-crf",
                "23",
                "-pix_fmt",
                "yuv420p",
                "-profile:v",
                "main",
                "-g",
                "30",
                "-keyint_min",
                "30",
                "-force_key_frames",
                "expr:if(isnan(prev_forced_t),gte(t,0),gte(t,prev_forced_t+1.001))",
                "-sc_threshold",
                "0",
                "-bf",
                "0",
                "-x264-params",
                "open-gop=0:scenecut=0:rc-lookahead=0:sync-lookahead=0:stitchable=1",
                "-threads",
                "1",
                "-c:a",
                "aac",
                "-profile:a",
                "aac_low",
                "-b:a",
                "160k",
                "-ac",
                "2",
                "-ar",
                "48000",
                "-af",
                ec_chopper::LIVE_AUDIO_RESAMPLE_FILTER,
                "-max_muxing_queue_size",
                "2048",
                "-avoid_negative_ts",
                "disabled",
                "-f",
                "mp4",
                "-movflags",
                WT_PUBLISH_MOVFLAGS,
                "pipe:1",
            ]
        );
    }

    #[test]
    fn wt_publish_stateless_proof_plan_uses_fresh_bitexact_encoder() {
        let args = WtPublishArgs::parse_from([
            "wt-publish",
            "--url",
            "https://lax.relay.every.channel/anon",
            "--name",
            "la-test",
            "--input",
            "/tmp/source.ts",
        ]);

        let ffmpeg_args = wt_publish_stateless_proof_ffmpeg_args(
            &args,
            Path::new("/tmp/source-window.ts"),
            Path::new("/tmp/proof.mp4"),
        )
        .into_iter()
        .map(|arg| arg.to_string_lossy().into_owned())
        .collect::<Vec<_>>();

        assert!(ffmpeg_args
            .windows(2)
            .any(|window| window == ["-map_metadata", "-1"]));
        assert!(ffmpeg_args
            .windows(2)
            .any(|window| window == ["-filter_threads", "1"]));
        assert!(ffmpeg_args
            .windows(2)
            .any(|window| window == ["-filter_complex_threads", "1"]));
        assert!(ffmpeg_args
            .windows(2)
            .any(|window| window == ["-force_key_frames", "expr:gte(t,0)"]));
        assert!(ffmpeg_args
            .windows(2)
            .any(|window| window == ["-avoid_negative_ts", "make_zero"]));
        assert!(ffmpeg_args
            .windows(2)
            .any(|window| window == ["-flags:v", "+bitexact"]));
        assert!(ffmpeg_args
            .windows(2)
            .any(|window| window == ["-flags:a", "+bitexact"]));
        assert_eq!(
            ffmpeg_args.last().map(String::as_str),
            Some("/tmp/proof.mp4")
        );
        assert!(!ffmpeg_args.iter().any(|arg| arg == "pipe:1"));
    }

    fn test_command_available(name: &str) -> bool {
        Command::new(name)
            .arg("-version")
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status()
            .map(|status| status.success())
            .unwrap_or(false)
    }

    fn write_short_deterministic_ts(out_path: &Path) -> Result<()> {
        let status = Command::new("ffmpeg")
            .arg("-hide_banner")
            .arg("-loglevel")
            .arg("error")
            .arg("-nostdin")
            .arg("-y")
            .arg("-f")
            .arg("lavfi")
            .arg("-i")
            .arg("testsrc2=size=320x180:rate=30000/1001")
            .arg("-f")
            .arg("lavfi")
            .arg("-i")
            .arg("sine=frequency=1000:sample_rate=48000")
            .arg("-t")
            .arg("1.25")
            .arg("-map")
            .arg("0:v:0")
            .arg("-map")
            .arg("1:a:0")
            .arg("-c:v")
            .arg("libx264")
            .arg("-pix_fmt")
            .arg("yuv420p")
            .arg("-g")
            .arg("30")
            .arg("-keyint_min")
            .arg("30")
            .arg("-sc_threshold")
            .arg("0")
            .arg("-bf")
            .arg("0")
            .arg("-x264-params")
            .arg("repeat-headers=1")
            .arg("-threads")
            .arg("1")
            .arg("-fflags")
            .arg("+bitexact")
            .arg("-flags:v")
            .arg("+bitexact")
            .arg("-c:a")
            .arg("aac")
            .arg("-b:a")
            .arg("128k")
            .arg("-ac")
            .arg("2")
            .arg("-ar")
            .arg("48000")
            .arg("-flags:a")
            .arg("+bitexact")
            .arg("-f")
            .arg("mpegts")
            .arg(out_path)
            .status()
            .context("failed to run ffmpeg synthetic TS writer")?;
        if !status.success() {
            return Err(anyhow!(
                "ffmpeg synthetic TS generation failed with {status}"
            ));
        }
        Ok(())
    }

    fn encode_stateless_proof_segment(
        args: &WtPublishArgs,
        input_ts: &Path,
        output_mp4: &Path,
    ) -> Result<Fmp4InitAndMedia> {
        let status = Command::new("ffmpeg")
            .args(wt_publish_stateless_proof_ffmpeg_args(
                args, input_ts, output_mp4,
            ))
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::inherit())
            .status()
            .context("failed to run stateless proof ffmpeg")?;
        if !status.success() {
            return Err(anyhow!("stateless proof ffmpeg exited with {status}"));
        }
        split_fmp4_init_and_media(&fs::read(output_mp4)?)
    }

    #[test]
    fn stateless_publisher_proof_segment_encodes_same_window_byte_identically() -> Result<()> {
        if !test_command_available("ffmpeg") {
            eprintln!("skipping stateless publisher proof determinism test: ffmpeg unavailable");
            return Ok(());
        }

        let root = std::env::temp_dir().join(format!(
            "ec-stateless-publisher-proof-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        fs::create_dir_all(&root)?;
        let input_ts = root.join("window.ts");
        write_short_deterministic_ts(&input_ts)?;

        let args = WtPublishArgs::parse_from([
            "wt-publish",
            "--url",
            "https://lax.relay.every.channel/anon",
            "--name",
            "la-test",
            "--input",
            input_ts.to_str().unwrap_or("/tmp/window.ts"),
            "--publisher-archive-segment-duration-ms",
            "1001",
        ]);

        let first = encode_stateless_proof_segment(&args, &input_ts, &root.join("first.mp4"))?;
        let second = encode_stateless_proof_segment(&args, &input_ts, &root.join("second.mp4"))?;

        assert_eq!(first.init, second.init);
        assert_eq!(first.media.len(), second.media.len());
        assert!(!first.media.is_empty());
        for (left, right) in first.media.iter().zip(second.media.iter()) {
            assert_eq!(blake3::hash(left), blake3::hash(right));
            assert_eq!(left, right);
        }

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    fn comparable_proof_windows(
        report: &PublisherProofWindowsReport,
    ) -> Vec<(u64, String, String, Vec<String>)> {
        report
            .windows
            .iter()
            .map(|window| {
                (
                    window.chunk_index,
                    window.source_ts_blake3.clone(),
                    window.init_blake3.clone(),
                    window.media_fragment_blake3.clone(),
                )
            })
            .collect()
    }

    #[test]
    fn publisher_proof_windows_same_input_produces_identical_hashes() -> Result<()> {
        if !test_command_available("ffmpeg") {
            eprintln!("skipping publisher proof windows determinism test: ffmpeg unavailable");
            return Ok(());
        }

        let root = std::env::temp_dir().join(format!(
            "ec-publisher-proof-windows-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        fs::create_dir_all(&root)?;
        let input_ts = root.join("input.ts");
        write_short_deterministic_ts(&input_ts)?;

        let args_a = PublisherProofWindowsArgs::parse_from([
            "publisher-proof-windows",
            "--input-ts",
            input_ts.to_str().unwrap_or("/tmp/input.ts"),
            "--output-dir",
            root.join("publisher-a")
                .to_str()
                .unwrap_or("/tmp/publisher-a"),
            "--chunk-ms",
            "1001",
            "--max-chunks",
            "2",
        ]);
        let args_b = PublisherProofWindowsArgs::parse_from([
            "publisher-proof-windows",
            "--input-ts",
            input_ts.to_str().unwrap_or("/tmp/input.ts"),
            "--output-dir",
            root.join("publisher-b")
                .to_str()
                .unwrap_or("/tmp/publisher-b"),
            "--chunk-ms",
            "1001",
            "--max-chunks",
            "2",
        ]);

        let report_a = publisher_proof_windows_report(&args_a)?;
        let report_b = publisher_proof_windows_report(&args_b)?;

        assert_eq!(report_a.preroll_packets, WT_PUBLISH_PROOF_PREROLL_PACKETS);
        assert_eq!(report_b.preroll_packets, WT_PUBLISH_PROOF_PREROLL_PACKETS);
        assert_eq!(report_a.window_count, 2);
        assert_eq!(report_b.window_count, 2);
        assert_eq!(
            comparable_proof_windows(&report_a),
            comparable_proof_windows(&report_b)
        );
        assert!(report_a
            .windows
            .iter()
            .all(|window| window.media_fragment_count > 0));

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn publisher_proof_duplicates_same_input_proves_identical_windows() -> Result<()> {
        if !test_command_available("ffmpeg") {
            eprintln!("skipping publisher proof duplicate comparison test: ffmpeg unavailable");
            return Ok(());
        }

        let root = std::env::temp_dir().join(format!(
            "ec-publisher-proof-duplicates-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        fs::create_dir_all(&root)?;
        let input_ts = root.join("input.ts");
        write_short_deterministic_ts(&input_ts)?;

        let args = PublisherProofDuplicatesArgs::parse_from([
            "publisher-proof-duplicates",
            "--input-ts",
            input_ts.to_str().unwrap_or("/tmp/input.ts"),
            "--output-dir",
            root.join("duplicate-proof")
                .to_str()
                .unwrap_or("/tmp/duplicate-proof"),
            "--chunk-ms",
            "1001",
            "--max-chunks",
            "2",
        ]);

        let report = publisher_proof_duplicates_report(&args)?;

        assert!(report.ok, "{:?}", report.reasons);
        assert_eq!(report.publisher_count, 2);
        assert_eq!(report.compared_window_count, 2);
        assert_eq!(report.matching_window_count, 2);
        assert_eq!(report.divergent_window_count, 0);
        assert!(report.divergences.is_empty());
        assert_eq!(
            report
                .publishers
                .iter()
                .map(|publisher| publisher.publisher.as_str())
                .collect::<Vec<_>>(),
            vec!["publisher-a", "publisher-b"]
        );

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn publisher_proof_compare_reports_from_same_input_passes() -> Result<()> {
        if !test_command_available("ffmpeg") {
            eprintln!("skipping publisher proof report comparison test: ffmpeg unavailable");
            return Ok(());
        }

        let root = std::env::temp_dir().join(format!(
            "ec-publisher-proof-report-compare-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        fs::create_dir_all(&root)?;
        let input_ts = root.join("input.ts");
        write_short_deterministic_ts(&input_ts)?;

        let report_a = publisher_proof_windows_report(&PublisherProofWindowsArgs::parse_from([
            "publisher-proof-windows",
            "--input-ts",
            input_ts.to_str().unwrap_or("/tmp/input.ts"),
            "--output-dir",
            root.join("nuc-a").to_str().unwrap_or("/tmp/nuc-a"),
            "--chunk-ms",
            "1001",
            "--max-chunks",
            "2",
        ]))?;
        let report_b = publisher_proof_windows_report(&PublisherProofWindowsArgs::parse_from([
            "publisher-proof-windows",
            "--input-ts",
            input_ts.to_str().unwrap_or("/tmp/input.ts"),
            "--output-dir",
            root.join("nuc-b").to_str().unwrap_or("/tmp/nuc-b"),
            "--chunk-ms",
            "1001",
            "--max-chunks",
            "2",
        ]))?;
        let report_a_path = root.join("nuc-a.json");
        let report_b_path = root.join("nuc-b.json");
        fs::write(&report_a_path, serde_json::to_vec_pretty(&report_a)?)?;
        fs::write(&report_b_path, serde_json::to_vec_pretty(&report_b)?)?;

        let args = PublisherProofCompareArgs::parse_from(vec![
            "publisher-proof-compare".to_string(),
            "--report".to_string(),
            format!("nuc-a={}", report_a_path.display()),
            "--report".to_string(),
            format!("nuc-b={}", report_b_path.display()),
            "--require-ok".to_string(),
        ]);
        let compare = publisher_proof_compare_report(&args)?;

        assert!(compare.ok, "{:?}", compare.reasons);
        assert_eq!(compare.publisher_count, 2);
        assert_eq!(compare.compared_window_count, 2);
        assert_eq!(compare.matching_window_count, 2);
        assert_eq!(compare.divergent_window_count, 0);
        assert!(compare.divergences.is_empty());

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn publisher_proof_compare_reports_detects_media_hash_mismatch() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-publisher-proof-report-mismatch-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        fs::create_dir_all(&root)?;
        let base_report = PublisherProofWindowsReport {
            input_ts: root.join("input.ts"),
            output_dir: root.join("proof-a"),
            chunk_ms: 1001,
            preroll_packets: WT_PUBLISH_PROOF_PREROLL_PACKETS,
            window_count: 1,
            windows: vec![PublisherProofWindowReport {
                chunk_index: 0,
                chunk_start_27mhz: Some(0),
                chunk_duration_27mhz: 27_027_000,
                sync_status: "aligned".to_string(),
                source_ts: root.join("source.ts"),
                source_ts_blake3: "source".to_string(),
                output_mp4: root.join("proof.mp4"),
                size_bytes: 10,
                init_size_bytes: 4,
                init_blake3: "init".to_string(),
                media_fragment_count: 1,
                media_fragment_blake3: vec!["media-a".to_string()],
            }],
        };
        let mut divergent_report = PublisherProofWindowsReport {
            output_dir: root.join("proof-b"),
            ..base_report.clone()
        };
        divergent_report.windows[0].media_fragment_blake3 = vec!["media-b".to_string()];

        let report_a_path = root.join("nuc-a.json");
        let report_b_path = root.join("nuc-b.json");
        fs::write(&report_a_path, serde_json::to_vec_pretty(&base_report)?)?;
        fs::write(
            &report_b_path,
            serde_json::to_vec_pretty(&divergent_report)?,
        )?;

        let args = PublisherProofCompareArgs::parse_from(vec![
            "publisher-proof-compare".to_string(),
            "--report".to_string(),
            format!("nuc-a={}", report_a_path.display()),
            "--report".to_string(),
            format!("nuc-b={}", report_b_path.display()),
        ]);
        let compare = publisher_proof_compare_report(&args)?;

        assert!(!compare.ok);
        assert_eq!(compare.compared_window_count, 1);
        assert_eq!(compare.matching_window_count, 0);
        assert_eq!(compare.divergent_window_count, 1);
        assert!(compare.reasons.contains(&"divergent_windows".to_string()));
        assert!(compare.divergences[0]
            .reasons
            .contains(&"media_fragment_hash_mismatch".to_string()));

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn publisher_proof_duplicates_rejects_duplicate_publisher_identity() {
        let args = PublisherProofDuplicatesArgs::parse_from([
            "publisher-proof-duplicates",
            "--input-ts",
            "/tmp/input.ts",
            "--output-dir",
            "/tmp/proof",
            "--publisher",
            "nuc-a",
            "--publisher",
            "nuc-a",
        ]);

        let err = publisher_proof_duplicates_report(&args).expect_err("duplicate should fail");

        assert!(
            err.to_string().contains("duplicated"),
            "unexpected error: {err:#}"
        );
    }

    #[test]
    fn publisher_proof_remote_compare_parses_distinct_safe_targets() -> Result<()> {
        let targets = parse_publisher_proof_remote_targets(&[
            "nuc-a=100.64.0.44".to_string(),
            "nuc-b=node@example.internal".to_string(),
        ])?;

        assert_eq!(
            targets,
            vec![
                PublisherProofRemoteTarget {
                    publisher: "nuc-a".to_string(),
                    host: "100.64.0.44".to_string(),
                },
                PublisherProofRemoteTarget {
                    publisher: "nuc-b".to_string(),
                    host: "node@example.internal".to_string(),
                },
            ]
        );
        Ok(())
    }

    #[test]
    fn publisher_proof_remote_compare_rejects_duplicate_target_labels() {
        let err = parse_publisher_proof_remote_targets(&[
            "nuc-a=100.64.0.44".to_string(),
            "nuc-a=100.64.0.45".to_string(),
        ])
        .expect_err("duplicate remote label should fail");

        assert!(
            err.to_string().contains("duplicated"),
            "unexpected error: {err:#}"
        );
    }

    #[test]
    fn publisher_proof_remote_compare_rejects_unsafe_remote_shell_args() {
        let args = PublisherProofRemoteCompareArgs::parse_from([
            "publisher-proof-remote-compare",
            "--input-ts",
            "/tmp/input.ts",
            "--output-dir",
            "/tmp/proof",
            "--remote",
            "nuc-a=100.64.0.44",
            "--remote",
            "nuc-b=100.64.0.45",
            "--remote-root",
            "/tmp/every-channel-proof",
            "--video-filter",
            "fps=30;touch/tmp/bad",
        ]);

        let err = publisher_proof_remote_compare_report(&args)
            .expect_err("unsafe video filter should fail before ssh");

        assert!(
            err.to_string().contains("unsupported shell characters"),
            "unexpected error: {err:#}"
        );
    }

    #[test]
    fn publisher_proof_remote_compare_command_uses_shared_input_window() {
        let args = PublisherProofRemoteCompareArgs::parse_from([
            "publisher-proof-remote-compare",
            "--input-ts",
            "/tmp/input.ts",
            "--output-dir",
            "/tmp/proof",
            "--remote",
            "nuc-a=100.64.0.44",
            "--remote",
            "nuc-b=100.64.0.45",
            "--max-chunks",
            "3",
        ]);

        let command = publisher_proof_remote_command_args(
            &args,
            "ec-node",
            "/tmp/every-channel-proof/nuc-a/source.ts",
            "/tmp/every-channel-proof/nuc-a/proof",
        );

        assert_eq!(command[0], "ec-node");
        assert!(command
            .windows(2)
            .any(|pair| pair[0] == "--input-ts"
                && pair[1] == "/tmp/every-channel-proof/nuc-a/source.ts"));
        assert!(command
            .windows(2)
            .any(|pair| pair[0] == "--output-dir"
                && pair[1] == "/tmp/every-channel-proof/nuc-a/proof"));
        assert!(command
            .windows(2)
            .any(|pair| pair[0] == "--max-chunks" && pair[1] == "3"));
        assert!(command.windows(2).any(|pair| pair[0] == "--preroll-packets"
            && pair[1] == WT_PUBLISH_PROOF_PREROLL_PACKETS.to_string()));
    }

    #[test]
    fn wt_publish_defaults_restart_when_relay_stops_announcing() {
        let args = WtPublishArgs::parse_from([
            "wt-publish",
            "--url",
            "https://lax.relay.every.channel/anon",
            "--name",
            "la-test",
            "--input",
            "/tmp/source.mp4",
        ]);

        assert_eq!(90_000, args.relay_announced_watchdog_ms);
        assert_eq!(10_000, args.relay_announced_watchdog_interval_ms);
    }

    fn signed_record_fixture(record_type: &str, created_unix_ms: u64) -> SignedRecord {
        let body = ec_core::SignedRecordBody {
            record_type: record_type.to_string(),
            subject: "channel:la-kcop".to_string(),
            time_ranges: vec![ec_core::RecordTimeRange {
                stream_id: Some(StreamId("la-kcop".to_string())),
                rendition_id: Some("720p".to_string()),
                start_unix_ms: created_unix_ms,
                end_unix_ms: created_unix_ms + 10_000,
            }],
            content_hashes: vec![ec_core::RecordContentHash {
                alg: ec_core::RECORD_CONTENT_HASH_BLAKE3.to_string(),
                digest: blake3::hash(b"record-payload").to_hex().to_string(),
                uri: Some("garage://every-channel-records/la-kcop/record.json".to_string()),
            }],
            source: "did:key:z6MkOperator".to_string(),
            visibility: ec_core::RECORD_VISIBILITY_PUBLIC.to_string(),
            created_unix_ms,
            valid_from_unix_ms: None,
            valid_until_unix_ms: None,
            supersedes: Vec::new(),
            revokes: Vec::new(),
            metadata: vec![StreamMetadata {
                key: "text".to_string(),
                value: "operator note".to_string(),
            }],
        };
        let record_id = body.record_id().unwrap();
        SignedRecord {
            body,
            record_id,
            signatures: vec![ec_core::SignedRecordSignature {
                signer_id: "did:key:z6MkOperator".to_string(),
                alg: ec_core::RECORD_SIG_ALG_SECP256K1_EIP712_BODY_V1.to_string(),
                signature: format!("0x{}", "11".repeat(65)),
            }],
            commitments: Vec::new(),
        }
    }

    #[test]
    fn signed_record_index_args_default_to_stdin_and_compact_json() {
        let args = SignedRecordIndexArgs::parse_from(["signed-record-index"]);

        assert_eq!(args.input, PathBuf::from("-"));
        assert_eq!(args.generated_unix_ms, None);
        assert!(!args.pretty);

        let explicit = SignedRecordIndexArgs::parse_from([
            "signed-record-index",
            "--input",
            "/tmp/records.json",
            "--generated-unix-ms",
            "42",
            "--pretty",
        ]);
        assert_eq!(explicit.input, PathBuf::from("/tmp/records.json"));
        assert_eq!(explicit.generated_unix_ms, Some(42));
        assert!(explicit.pretty);
    }

    #[test]
    fn signed_record_index_input_accepts_array_wrapped_and_single_record() {
        let record = signed_record_fixture(ec_core::RECORD_TYPE_FRIEND_COMMENT, 1_771_000_000_000);

        let array = serde_json::to_vec(&vec![record.clone()]).unwrap();
        let wrapped =
            serde_json::to_vec(&serde_json::json!({ "records": [record.clone()] })).unwrap();
        let single = serde_json::to_vec(&record).unwrap();

        assert_eq!(
            parse_signed_record_index_input(&array).unwrap()[0].record_id,
            record.record_id
        );
        assert_eq!(
            parse_signed_record_index_input(&wrapped).unwrap()[0].record_id,
            record.record_id
        );
        assert_eq!(
            parse_signed_record_index_input(&single).unwrap()[0].record_id,
            record.record_id
        );
    }

    #[test]
    fn relay_announced_watchdog_uses_relay_announced_endpoint() {
        let url = relay_announced_url("https://lax.relay.every.channel/anon?x=1").unwrap();

        assert_eq!("https://lax.relay.every.channel/announced", url.as_str());
    }

    #[test]
    fn relay_announced_watchdog_matches_prefixed_and_plain_names() {
        assert!(announced_body_contains_broadcast(
            "anon/la-kcbs\nanon/la-nbc4\n",
            "la-nbc4"
        ));
        assert!(announced_body_contains_broadcast("la-kcet", "la-kcet"));
        assert!(!announced_body_contains_broadcast(
            "anon/la-kcet",
            "la-kcbs"
        ));
    }

    #[test]
    fn wt_publish_ntsc_rs_plan_builds_cli_invocation() {
        let args = WtPublishArgs::parse_from([
            "wt-publish",
            "--url",
            "https://relay.every.channel/anon",
            "--name",
            "la-test",
            "--input",
            "/tmp/source.mp4",
            "--ntsc-rs-preset",
            "/etc/every-channel/ntsc/vhs.json",
            "--ntsc-rs-output",
            "/tmp/every-channel-ntsc-rs/la-test.mp4",
            "--ntsc-rs-cli",
            "/opt/ntsc-rs-cli",
        ]);

        let plan = ntsc_rs_preprocess_plan(&args).unwrap().unwrap();

        assert_eq!(PathBuf::from("/opt/ntsc-rs-cli"), plan.cli);
        assert_eq!(
            PathBuf::from("/tmp/every-channel-ntsc-rs/la-test.mp4"),
            plan.output
        );
        assert_eq!(
            vec![
                OsString::from("-i"),
                OsString::from("/tmp/source.mp4"),
                OsString::from("-o"),
                OsString::from("/tmp/every-channel-ntsc-rs/la-test.mp4"),
                OsString::from("-p"),
                OsString::from("/etc/every-channel/ntsc/vhs.json"),
                OsString::from("--overwrite"),
            ],
            plan.args
        );
    }

    #[test]
    fn wt_publish_ntsc_rs_requires_explicit_output() {
        let args = WtPublishArgs::parse_from([
            "wt-publish",
            "--url",
            "https://relay.every.channel/anon",
            "--name",
            "la-test",
            "--input",
            "/tmp/source.mp4",
            "--ntsc-rs-preset",
            "/etc/every-channel/ntsc/vhs.json",
        ]);

        let err = ntsc_rs_preprocess_plan(&args).unwrap_err();

        assert!(err.to_string().contains("--ntsc-rs-output"));
    }

    #[test]
    fn parse_manifest_allowlist_splits_and_trims() {
        let set = parse_manifest_allowlist(Some(" a,b ; c\t d  ")).unwrap();
        assert!(set.contains("a"));
        assert!(set.contains("b"));
        assert!(set.contains("c"));
        assert!(set.contains("d"));
    }

    #[test]
    fn publish_commands_default_to_current_relay_passthrough() {
        let cli = Cli::parse_from([
            "ec-node",
            "wt-publish",
            "--name",
            "la-test",
            "--input",
            "http://example.invalid/auto/v2.1",
        ]);
        match cli.command {
            Commands::WtPublish(args) => {
                assert_eq!(args.url, "https://relay.every.channel/anon");
                assert!(args.passthrough);
            }
            _ => panic!("expected wt-publish command"),
        }

        let cli = Cli::parse_from([
            "ec-node",
            "nbc-wt-publish",
            "--name",
            "nbc-test",
            "--source-url",
            "https://example.invalid/live",
        ]);
        match cli.command {
            Commands::NbcWtPublish(args) => {
                assert_eq!(args.url, "https://relay.every.channel/anon");
                assert!(args.passthrough);
            }
            _ => panic!("expected nbc-wt-publish command"),
        }
    }

    #[test]
    fn deterministic_enabled_reads_env() {
        let prev = std::env::var("EVERY_CHANNEL_DETERMINISTIC").ok();
        std::env::set_var("EVERY_CHANNEL_DETERMINISTIC", "true");
        assert!(deterministic_enabled(false));
        std::env::set_var("EVERY_CHANNEL_DETERMINISTIC", "0");
        assert!(!deterministic_enabled(false));
        match prev {
            Some(value) => std::env::set_var("EVERY_CHANNEL_DETERMINISTIC", value),
            None => std::env::remove_var("EVERY_CHANNEL_DETERMINISTIC"),
        }
    }

    #[test]
    fn parse_network_secret_accepts_hex_and_rejects_invalid() {
        let out = parse_network_secret(Some("00".repeat(8))).unwrap().unwrap();
        assert_eq!(out.len(), 8);
        assert!(parse_network_secret(Some("not-hex".to_string())).is_err());
    }

    fn build_valid_manifest(unsigned: bool) -> Manifest {
        let chunk_hashes = vec![
            blake3::hash(b"c0").to_hex().to_string(),
            blake3::hash(b"c1").to_hex().to_string(),
        ];
        let body = build_manifest_body_for_chunks(
            StreamId("s".to_string()),
            "epoch-1",
            2000,
            10,
            "p",
            1,
            Vec::new(),
            &chunk_hashes,
        )
        .unwrap();
        let manifest_id = body.manifest_id().unwrap();
        let signatures = if unsigned {
            Vec::new()
        } else {
            let prev = std::env::var("EVERY_CHANNEL_MANIFEST_SIGNING_KEY").ok();
            std::env::set_var("EVERY_CHANNEL_MANIFEST_SIGNING_KEY", "11".repeat(32));
            let keypair = load_manifest_keypair_from_env().unwrap().unwrap();
            let sig = sign_manifest_id(&manifest_id, &keypair);
            match prev {
                Some(value) => std::env::set_var("EVERY_CHANNEL_MANIFEST_SIGNING_KEY", value),
                None => std::env::remove_var("EVERY_CHANNEL_MANIFEST_SIGNING_KEY"),
            }
            vec![sig]
        };

        let mut manifest = Manifest {
            body,
            manifest_id,
            signatures,
            commitments: Vec::new(),
        };
        manifest.commitments = manifest_commitments(&manifest).unwrap();
        manifest
    }

    #[test]
    fn validate_manifest_accepts_unsigned_only_with_hashes() {
        let mut manifest = build_valid_manifest(true);
        assert!(validate_manifest(&manifest, None));

        // Remove hashes: unsigned should be rejected.
        manifest.body.chunk_hashes.clear();
        manifest.body.total_chunks = 0;
        manifest.body.merkle_root = "00".repeat(32);
        manifest.manifest_id = manifest.body.manifest_id().unwrap();
        assert!(!validate_manifest(&manifest, None));
    }

    #[test]
    fn validate_manifest_accepts_signed_and_obeys_allowlist() {
        let manifest = build_valid_manifest(false);
        assert!(validate_manifest(&manifest, None));
        let signer = manifest.signatures[0].signer_id.clone();
        let allow = HashSet::from([signer.clone()]);
        assert!(validate_manifest(&manifest, Some(&allow)));
        let deny = HashSet::from(["other".to_string()]);
        assert!(!validate_manifest(&manifest, Some(&deny)));
    }

    #[test]
    fn validate_manifest_accepts_ethereum_signature() {
        let chunk_hashes = vec![blake3::hash(b"c0").to_hex().to_string()];
        let body = build_manifest_body_for_chunks(
            StreamId("s".to_string()),
            "epoch-eth",
            2000,
            0,
            "p",
            1,
            Vec::new(),
            &chunk_hashes,
        )
        .unwrap();
        let manifest_id = body.manifest_id().unwrap();
        let prev = std::env::var(ec_crypto::ETH_MANIFEST_SIGNING_KEY_ENV).ok();
        std::env::set_var(ec_crypto::ETH_MANIFEST_SIGNING_KEY_ENV, "22".repeat(32));
        let keypair = load_ethereum_manifest_keypair_from_env().unwrap().unwrap();
        let sig = sign_manifest_body_eip712(&body, &keypair).unwrap();
        match prev {
            Some(value) => std::env::set_var(ec_crypto::ETH_MANIFEST_SIGNING_KEY_ENV, value),
            None => std::env::remove_var(ec_crypto::ETH_MANIFEST_SIGNING_KEY_ENV),
        }

        let mut manifest = Manifest {
            body,
            manifest_id,
            signatures: vec![sig],
            commitments: Vec::new(),
        };
        manifest.commitments = manifest_commitments(&manifest).unwrap();
        assert!(validate_manifest(&manifest, None));
        let allow = HashSet::from([manifest.signatures[0].signer_id.clone()]);
        assert!(validate_manifest(&manifest, Some(&allow)));
    }

    #[test]
    fn validate_manifest_rejects_bad_ethereum_commitment() {
        let mut manifest = build_valid_manifest(true);
        manifest.commitments[0].digest = "0xdeadbeef".to_string();
        assert!(!validate_manifest(&manifest, None));
    }

    #[test]
    fn control_announcement_carries_stream_and_announcement_commitments() {
        let announcement = build_control_announcement(
            "ec/stream/v1/broadcast/atsc/tsid-42/program-3".to_string(),
            "KCBS".to_string(),
            vec![StreamTransportDescriptor::IrohDirect {
                endpoint: "ed25519:node".to_string(),
                broadcast_name: "kcbs".to_string(),
                track_name: WT_PUBLISH_PRIMARY_VIDEO_TRACK.to_string(),
            }],
            5_000,
        );
        assert!(!announcement.stream.commitments.is_empty());
        assert!(!announcement.commitments.is_empty());
    }

    #[test]
    fn manifest_hash_for_chunk_indexes_into_hash_list() {
        let manifest = build_valid_manifest(true);
        let sid = manifest.body.stream_id.0.as_str();
        assert!(manifest_hash_for_chunk(&manifest, sid, 9).is_none());
        assert_eq!(
            manifest_hash_for_chunk(&manifest, sid, 10).as_deref(),
            Some(manifest.body.chunk_hashes[0].as_str())
        );
        assert_eq!(
            manifest_hash_for_chunk(&manifest, sid, 11).as_deref(),
            Some(manifest.body.chunk_hashes[1].as_str())
        );
        assert!(manifest_hash_for_chunk(&manifest, sid, 12).is_none());
    }

    #[test]
    fn duplicate_publisher_manifests_share_media_roots_not_envelopes() {
        let variants = cmaf_ladder_variants(CmafLadderPreset::Hd3);
        let per_variant_hash = vec![
            (
                "1080p".to_string(),
                blake3::hash(b"same-1080p-segment").to_hex().to_string(),
            ),
            (
                "720p".to_string(),
                blake3::hash(b"same-720p-segment").to_hex().to_string(),
            ),
            (
                "480p".to_string(),
                blake3::hash(b"same-480p-segment").to_hex().to_string(),
            ),
        ];
        let metadata = vec![StreamMetadata {
            key: "source_kind".to_string(),
            value: "hdhomerun".to_string(),
        }];

        let publisher_a = build_multi_variant_manifest(
            StreamId("publisher-a/la-kcop".to_string()),
            "epoch-a".to_string(),
            2_000,
            42,
            "x264-hd3".to_string(),
            1_000,
            metadata.clone(),
            &variants,
            42,
            per_variant_hash.clone(),
        )
        .unwrap();
        let publisher_b = build_multi_variant_manifest(
            StreamId("publisher-b/la-kcop".to_string()),
            "epoch-b".to_string(),
            2_000,
            42,
            "x264-hd3".to_string(),
            2_000,
            metadata,
            &variants,
            42,
            per_variant_hash,
        )
        .unwrap();

        assert!(validate_manifest(&publisher_a, None));
        assert!(validate_manifest(&publisher_b, None));
        assert_ne!(publisher_a.manifest_id, publisher_b.manifest_id);
        assert_ne!(publisher_a.body.stream_id.0, publisher_b.body.stream_id.0);
        assert_ne!(publisher_a.body.epoch_id, publisher_b.body.epoch_id);
        assert_ne!(
            publisher_a.body.created_unix_ms,
            publisher_b.body.created_unix_ms
        );

        assert_eq!(publisher_a.body.chunk_hashes.len(), 0);
        assert_eq!(publisher_b.body.chunk_hashes.len(), 0);
        assert_eq!(publisher_a.body.merkle_root, publisher_b.body.merkle_root);

        let variants_a = publisher_a.body.variants.as_ref().unwrap();
        let variants_b = publisher_b.body.variants.as_ref().unwrap();
        assert_eq!(variants_a.len(), variants_b.len());
        for (a, b) in variants_a.iter().zip(variants_b.iter()) {
            assert_eq!(a.variant_id, b.variant_id);
            assert_ne!(a.stream_id.0, b.stream_id.0);
            assert_eq!(a.chunk_start_index, b.chunk_start_index);
            assert_eq!(a.total_chunks, b.total_chunks);
            assert_eq!(a.chunk_hashes, b.chunk_hashes);
            assert_eq!(a.merkle_root, b.merkle_root);
            assert_eq!(a.metadata.len(), b.metadata.len());
            for (left, right) in a.metadata.iter().zip(b.metadata.iter()) {
                assert_eq!(left.key, right.key);
                assert_eq!(left.value, right.value);
            }
            assert_eq!(
                manifest_hash_for_chunk(&publisher_a, &a.stream_id.0, a.chunk_start_index)
                    .as_deref(),
                Some(a.chunk_hashes[0].as_str())
            );
            assert_eq!(
                manifest_hash_for_chunk(&publisher_b, &b.stream_id.0, b.chunk_start_index)
                    .as_deref(),
                Some(b.chunk_hashes[0].as_str())
            );
        }
    }

    #[test]
    fn decode_archive_group_bytes_unwraps_concatenated_object_frames() {
        let meta_a = ObjectMeta {
            created_unix_ms: 1,
            content_type: "video/mp4".to_string(),
            size_bytes: 4,
            timing: None,
            encryption: None,
            chunk_hash: None,
            chunk_hash_alg: None,
            chunk_proof: None,
            chunk_proof_alg: None,
            manifest_id: None,
        };
        let meta_b = ObjectMeta {
            created_unix_ms: 2,
            content_type: "video/mp4".to_string(),
            size_bytes: 3,
            timing: None,
            encryption: None,
            chunk_hash: None,
            chunk_hash_alg: None,
            chunk_proof: None,
            chunk_proof_alg: None,
            manifest_id: None,
        };

        let mut group = encode_object_frame(&meta_a, b"init").unwrap();
        group.extend_from_slice(&encode_object_frame(&meta_b, b"seg").unwrap());

        let decoded = decode_archive_group_bytes(&group).unwrap();
        assert_eq!(decoded, b"initseg");
    }

    #[test]
    fn decode_archive_group_bytes_passes_through_raw_payloads() {
        let raw = b"\x00\x00\x00\x18ftypisom\x00\x00\x02\x00isomiso2";
        let decoded = decode_archive_group_bytes(raw).unwrap();
        assert_eq!(decoded, raw);
    }

    #[test]
    fn archive_origin_object_urls_support_archive_and_cas_roots() {
        let digest = "a".repeat(64);
        let state = ArchiveReplayState {
            cas_root: PathBuf::from("/cache/objects/blake3"),
            manifest_root: PathBuf::from("/cache/manifests"),
            archive_origin_url: Some(Url::parse("https://archive.example/root").unwrap()),
            archive_cas_origin_url: Some(
                Url::parse("https://garage.example/bucket/objects/blake3").unwrap(),
            ),
            archive_origin_max_bytes: 1024,
            archive_retention_seconds: WT_ARCHIVE_DEFAULT_RETENTION_SECONDS,
            cache_access_root: PathBuf::from("/cache/cache-access/blake3"),
            http_client: reqwest::Client::new(),
        };

        let urls = archive_origin_object_urls(&state, &digest)
            .unwrap()
            .into_iter()
            .map(|url| url.to_string())
            .collect::<Vec<_>>();

        assert_eq!(
            urls,
            vec![
                format!("https://garage.example/bucket/objects/blake3/aa/{digest}.bin"),
                format!("https://archive.example/root/objects/blake3/aa/{digest}.bin"),
            ]
        );
    }

    #[test]
    fn archive_replay_reads_legacy_video_audio_track_names() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-legacy-track-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let manifest_root = root.join("manifests");
        let broadcast_dir = manifest_root.join("legacy");
        fs::create_dir_all(&broadcast_dir)?;

        let video_hash = "b".repeat(64);
        let audio_hash = "c".repeat(64);
        let video_record = ArchiveIndexRecord {
            received_unix_ms: 1_000,
            relay_url: "https://relay.every.channel/anon".to_string(),
            source_node: Some("nuc-a".to_string()),
            source_session: Some("nuc-a-test".to_string()),
            media_timing: None,
            broadcast_name: "legacy".to_string(),
            track_name: "video0.m4s".to_string(),
            group_sequence: 7,
            frame_count: 1,
            size_bytes: 4,
            blake3: video_hash.clone(),
            cas_path: format!("objects/blake3/bb/{video_hash}.bin"),
        };
        let audio_record = ArchiveIndexRecord {
            received_unix_ms: 1_010,
            relay_url: "https://relay.every.channel/anon".to_string(),
            source_node: Some("nuc-a".to_string()),
            source_session: Some("nuc-a-test".to_string()),
            media_timing: None,
            broadcast_name: "legacy".to_string(),
            track_name: "audio0.m4s".to_string(),
            group_sequence: 8,
            frame_count: 1,
            size_bytes: 4,
            blake3: audio_hash.clone(),
            cas_path: format!("objects/blake3/cc/{audio_hash}.bin"),
        };
        fs::write(
            broadcast_dir.join("video0.m4s.jsonl"),
            format!("{}\n", serde_json::to_string(&video_record)?),
        )?;
        fs::write(
            broadcast_dir.join("audio0.m4s.jsonl"),
            format!("{}\n", serde_json::to_string(&audio_record)?),
        )?;

        let video = parse_archive_track(
            &manifest_root,
            "legacy",
            WT_PUBLISH_PRIMARY_VIDEO_TRACK,
            None,
            None,
        )?;
        let audio = parse_archive_track(
            &manifest_root,
            "legacy",
            WT_PUBLISH_PRIMARY_AUDIO_TRACK,
            None,
            None,
        )?;

        assert_eq!(video.len(), 1);
        assert_eq!(video[0].track_name, "video0.m4s");
        assert_eq!(audio.len(), 1);
        assert_eq!(audio[0].track_name, "audio0.m4s");
        assert_eq!(
            latest_init_hash(&manifest_root, "legacy", WT_PUBLISH_PRIMARY_VIDEO_TRACK)?,
            None
        );
        assert_eq!(
            latest_init_hash(&manifest_root, "legacy", WT_PUBLISH_PRIMARY_AUDIO_TRACK)?,
            None
        );

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_convergence_compares_duplicate_publisher_manifest_roots() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-convergence-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let publisher_a = root.join("publisher-a").join("manifests");
        let publisher_b = root.join("publisher-b").join("manifests");
        for manifest_root in [&publisher_a, &publisher_b] {
            fs::create_dir_all(manifest_root.join("la-kcop"))?;
        }

        for manifest_root in [&publisher_a, &publisher_b] {
            let mut lines = String::new();
            for sequence in 0..4u64 {
                let hash = blake3::hash(format!("la-kcop-720p-{sequence}").as_bytes())
                    .to_hex()
                    .to_string();
                let record = ArchiveIndexRecord {
                    received_unix_ms: 1_000 + sequence * 1_000,
                    relay_url: "https://lax.relay.every.channel/anon".to_string(),
                    source_node: Some(
                        manifest_root
                            .parent()
                            .and_then(|path| path.file_name())
                            .and_then(|name| name.to_str())
                            .unwrap_or("unknown")
                            .to_string(),
                    ),
                    source_session: Some(format!("test-session-{sequence}")),
                    media_timing: Some(ArchiveRecordMediaTiming {
                        track_id: 7,
                        timescale: 90_000,
                        decode_time: sequence * 90_090,
                        sequence,
                    }),
                    broadcast_name: "la-kcop".to_string(),
                    track_name: WT_PUBLISH_PRIMARY_VIDEO_TRACK.to_string(),
                    group_sequence: sequence,
                    frame_count: 30,
                    size_bytes: 1024,
                    blake3: hash.clone(),
                    cas_path: format!("objects/blake3/{}/{hash}.bin", &hash[0..2]),
                };
                lines.push_str(&serde_json::to_string(&record)?);
                lines.push('\n');
            }
            fs::write(manifest_root.join("la-kcop").join("0.m4s.jsonl"), lines)?;
        }

        let report = archive_convergence_report(&ArchiveConvergenceArgs {
            source: vec![
                format!("nuc-a={}", publisher_a.display()),
                format!("nuc-b={}", publisher_b.display()),
            ],
            broadcast: "la-kcop".to_string(),
            track: WT_PUBLISH_PRIMARY_VIDEO_TRACK.to_string(),
            stream_id: None,
            rendition: Some("720p".to_string()),
            start_sequence: None,
            end_sequence: None,
            pretty: false,
            prometheus: false,
            metrics_node: None,
            metrics_role: "duplicate-proof".to_string(),
            require_ok: false,
        })?;

        assert!(report.ok);
        assert!(report.duplicate_complete);
        assert!(report.reasons.is_empty());
        assert_eq!(report.source_count, 2);
        assert_eq!(report.record_source_count, 2);
        assert!(report.record_source_identity_ok);
        assert_eq!(
            report.record_source_nodes,
            vec!["publisher-a".to_string(), "publisher-b".to_string()]
        );
        assert_eq!(report.summary.expected_sequences, 4);
        assert_eq!(report.summary.missing_sequences, Vec::<u64>::new());
        assert_eq!(report.summary.divergent_sequences, Vec::<u64>::new());
        assert_eq!(
            report.summary.matching_duplicate_sequences,
            vec![0, 1, 2, 3]
        );
        assert_eq!(report.summary.duplicate_source_records, 8);

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_convergence_rejects_matching_hashes_with_conflicting_media_timing() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-convergence-media-timing-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let publisher_a = root.join("publisher-a").join("manifests");
        let publisher_b = root.join("publisher-b").join("manifests");
        fs::create_dir_all(publisher_a.join("la-kcop"))?;
        fs::create_dir_all(publisher_b.join("la-kcop"))?;

        let hash = blake3::hash(b"same-bytes").to_hex().to_string();
        for (manifest_root, source_node, media_sequence, decode_time) in [
            (&publisher_a, "publisher-a", 200_u64, 180_000_u64),
            (&publisher_b, "publisher-b", 201_u64, 270_000_u64),
        ] {
            let record = ArchiveIndexRecord {
                received_unix_ms: 1_000,
                relay_url: "https://lax.relay.every.channel/anon".to_string(),
                source_node: Some(source_node.to_string()),
                source_session: Some(format!("{source_node}-test")),
                media_timing: Some(ArchiveRecordMediaTiming {
                    track_id: 7,
                    timescale: 90_000,
                    decode_time,
                    sequence: media_sequence,
                }),
                broadcast_name: "la-kcop".to_string(),
                track_name: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
                group_sequence: 100,
                frame_count: 1,
                size_bytes: 1024,
                blake3: hash.clone(),
                cas_path: format!("objects/blake3/{}/{hash}.bin", &hash[0..2]),
            };
            append_archive_index_record(
                &manifest_root
                    .join("la-kcop")
                    .join(format!("{WT_PUBLISHER_ORIGIN_TRACK}.jsonl")),
                &record,
            )?;
        }

        let report = archive_convergence_report(&ArchiveConvergenceArgs {
            source: vec![
                format!("nuc-a={}", publisher_a.display()),
                format!("nuc-b={}", publisher_b.display()),
            ],
            broadcast: "la-kcop".to_string(),
            track: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
            stream_id: None,
            rendition: Some("primary".to_string()),
            start_sequence: None,
            end_sequence: None,
            pretty: false,
            prometheus: false,
            metrics_node: None,
            metrics_role: "duplicate-proof".to_string(),
            require_ok: false,
        })?;

        assert!(!report.ok);
        assert!(report.duplicate_complete);
        assert_eq!(report.summary.divergent_sequences, Vec::<u64>::new());
        assert_eq!(report.summary.matching_duplicate_sequences, vec![100]);
        assert_eq!(report.media_timing_conflict_count, 1);
        assert_eq!(report.media_timing_conflicts.len(), 1);
        assert_eq!(report.media_timing_conflicts[0].group_sequence, 100);
        assert_eq!(
            report.media_timing_conflicts[0].media_sequences,
            vec![200, 201]
        );
        assert_eq!(
            report.media_timing_conflicts[0].media_decode_times,
            vec![180_000, 270_000]
        );
        assert!(report
            .reasons
            .contains(&"media_sequence_conflict".to_string()));

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_convergence_reports_divergent_sequence_samples() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-convergence-divergence-sample-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let publisher_a = root.join("publisher-a").join("manifests");
        let publisher_b = root.join("publisher-b").join("manifests");
        fs::create_dir_all(publisher_a.join("la-kcop"))?;
        fs::create_dir_all(publisher_b.join("la-kcop"))?;

        let hash_a = blake3::hash(b"publisher-a-bytes").to_hex().to_string();
        let hash_b = blake3::hash(b"publisher-b-bytes").to_hex().to_string();
        for (manifest_root, source_node, source_session, received_unix_ms, size_bytes, hash) in [
            (
                &publisher_a,
                "publisher-a",
                "publisher-a-session",
                1_000_u64,
                111_usize,
                hash_a.as_str(),
            ),
            (
                &publisher_b,
                "publisher-b",
                "publisher-b-session",
                1_030_u64,
                222_usize,
                hash_b.as_str(),
            ),
        ] {
            let record = ArchiveIndexRecord {
                received_unix_ms,
                relay_url: "https://lax.relay.every.channel/anon".to_string(),
                source_node: Some(source_node.to_string()),
                source_session: Some(source_session.to_string()),
                media_timing: None,
                broadcast_name: "la-kcop".to_string(),
                track_name: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
                group_sequence: 42,
                frame_count: 1,
                size_bytes,
                blake3: hash.to_string(),
                cas_path: format!("objects/blake3/{}/{hash}.bin", &hash[0..2]),
            };
            append_archive_index_record(
                &manifest_root
                    .join("la-kcop")
                    .join(format!("{WT_PUBLISHER_ORIGIN_TRACK}.jsonl")),
                &record,
            )?;
        }

        let report = archive_convergence_report(&ArchiveConvergenceArgs {
            source: vec![
                format!("nuc-a={}", publisher_a.display()),
                format!("nuc-b={}", publisher_b.display()),
            ],
            broadcast: "la-kcop".to_string(),
            track: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
            stream_id: None,
            rendition: Some("primary".to_string()),
            start_sequence: None,
            end_sequence: None,
            pretty: false,
            prometheus: false,
            metrics_node: None,
            metrics_role: "duplicate-proof".to_string(),
            require_ok: false,
        })?;

        assert!(!report.ok);
        assert_eq!(report.media_timing_missing_records, 2);
        assert_eq!(report.summary.divergent_sequences, vec![42]);
        assert_eq!(report.divergent_sequence_samples.len(), 1);
        let sample = &report.divergent_sequence_samples[0];
        assert_eq!(sample.group_sequence, 42);
        assert_eq!(sample.record_count, 2);
        assert_eq!(sample.hash_count, 2);
        assert_eq!(
            sample.sources,
            vec!["publisher-a".to_string(), "publisher-b".to_string()]
        );
        assert_eq!(sample.records.len(), 2);
        assert_eq!(sample.records[0].manifest_source, "nuc-a");
        assert_eq!(
            sample.records[0].source_node.as_deref(),
            Some("publisher-a")
        );
        assert_eq!(
            sample.records[0].source_session.as_deref(),
            Some("publisher-a-session")
        );
        assert_eq!(sample.records[0].size_bytes, 111);
        assert_eq!(sample.records[0].received_unix_ms, 1_000);
        assert_eq!(sample.records[0].blake3, hash_a);
        assert_eq!(sample.records[1].manifest_source, "nuc-b");
        assert_eq!(
            sample.records[1].source_node.as_deref(),
            Some("publisher-b")
        );
        assert_eq!(
            sample.records[1].source_session.as_deref(),
            Some("publisher-b-session")
        );
        assert_eq!(sample.records[1].size_bytes, 222);
        assert_eq!(sample.records[1].received_unix_ms, 1_030);
        assert_eq!(sample.records[1].blake3, hash_b);

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_convergence_prometheus_metrics_include_duplicate_and_miss_gauges() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-convergence-prometheus-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let publisher_a = root.join("publisher-a").join("manifests");
        let publisher_b = root.join("publisher-b").join("manifests");
        fs::create_dir_all(publisher_a.join("la-kcop"))?;
        fs::create_dir_all(publisher_b.join("la-kcop"))?;

        let hash = blake3::hash(b"same-window").to_hex().to_string();
        for (manifest_root, source_node, sequence, digest) in [
            (&publisher_a, "publisher-a", 0_u64, hash.as_str()),
            (&publisher_b, "publisher-b", 0_u64, hash.as_str()),
            (&publisher_a, "publisher-a", 1_u64, ""),
        ] {
            let record = ArchiveIndexRecord {
                received_unix_ms: 1_000 + sequence * 1_000,
                relay_url: "https://lax.relay.every.channel/anon".to_string(),
                source_node: Some(source_node.to_string()),
                source_session: Some(format!("{source_node}-test")),
                media_timing: Some(ArchiveRecordMediaTiming {
                    track_id: 7,
                    timescale: 90_000,
                    decode_time: 0,
                    sequence: 0,
                }),
                broadcast_name: "la-kcop".to_string(),
                track_name: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
                group_sequence: sequence,
                frame_count: 1,
                size_bytes: 1024,
                blake3: digest.to_string(),
                cas_path: if digest.is_empty() {
                    String::new()
                } else {
                    format!("objects/blake3/{}/{digest}.bin", &digest[0..2])
                },
            };
            append_archive_index_record(
                &manifest_root
                    .join("la-kcop")
                    .join(format!("{WT_PUBLISHER_ORIGIN_TRACK}.jsonl")),
                &record,
            )?;
        }

        let args = ArchiveConvergenceArgs {
            source: vec![
                format!("nuc-a={}", publisher_a.display()),
                format!("nuc-b={}", publisher_b.display()),
            ],
            broadcast: "la-kcop".to_string(),
            track: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
            stream_id: None,
            rendition: Some("primary".to_string()),
            start_sequence: None,
            end_sequence: None,
            pretty: false,
            prometheus: true,
            metrics_node: Some("nuc\"a\nb".to_string()),
            metrics_role: "publisher-buffer".to_string(),
            require_ok: false,
        };
        let report = archive_convergence_report(&args)?;
        let text = archive_convergence_prometheus_metrics(&report, &args);

        assert!(!report.ok);
        assert_eq!(report.missing_hash_records, 1);
        assert!(report.reasons.contains(&"hash_missing".to_string()));
        assert!(text.contains("node=\"nuc\\\"a\\nb\""));
        assert!(text.contains("role=\"publisher-buffer\""));
        assert!(text.contains("every_channel_archive_convergence_scrape_ok"));
        assert!(text.contains("every_channel_archive_convergence_ok"));
        assert!(text.contains("every_channel_archive_duplicate_hash_source_records"));
        assert!(text.contains("every_channel_archive_duplicate_hash_sequences"));
        assert!(text.contains("every_channel_archive_missing_hash_records"));
        assert!(text.contains("every_channel_archive_missing_source_identity_records"));
        assert!(text.contains("every_channel_archive_media_timing_missing_records"));
        assert!(text.contains("every_channel_archive_media_timing_conflict_sequences"));
        assert!(text.contains("every_channel_archive_duplicate_hash_sequences{"));
        assert!(text.contains(" 1.000000\n"));

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_convergence_serve_metrics_response_uses_rust_report() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-convergence-serve-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let publisher_a = root.join("publisher-a").join("manifests");
        let publisher_b = root.join("publisher-b").join("manifests");
        fs::create_dir_all(publisher_a.join("la-kcop"))?;
        fs::create_dir_all(publisher_b.join("la-kcop"))?;

        let hash = blake3::hash(b"same-window").to_hex().to_string();
        for (manifest_root, source_node) in
            [(&publisher_a, "publisher-a"), (&publisher_b, "publisher-b")]
        {
            let record = ArchiveIndexRecord {
                received_unix_ms: 1_000,
                relay_url: "https://lax.relay.every.channel/anon".to_string(),
                source_node: Some(source_node.to_string()),
                source_session: Some(format!("{source_node}-test")),
                media_timing: None,
                broadcast_name: "la-kcop".to_string(),
                track_name: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
                group_sequence: 0,
                frame_count: 1,
                size_bytes: 1024,
                blake3: hash.clone(),
                cas_path: format!("objects/blake3/{}/{hash}.bin", &hash[0..2]),
            };
            append_archive_index_record(
                &manifest_root
                    .join("la-kcop")
                    .join(format!("{WT_PUBLISHER_ORIGIN_TRACK}.jsonl")),
                &record,
            )?;
        }

        let args = ArchiveConvergenceServeArgs {
            source: vec![
                format!("nuc-a={}", publisher_a.display()),
                format!("nuc-b={}", publisher_b.display()),
            ],
            broadcast: "la-kcop".to_string(),
            track: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
            stream_id: None,
            rendition: Some("primary".to_string()),
            start_sequence: None,
            end_sequence: None,
            listen: "127.0.0.1:0".to_string(),
            metrics_node: Some("forge".to_string()),
            metrics_role: "duplicate-proof".to_string(),
        };

        let response = archive_convergence_serve_response(&args, "GET", "/metrics");
        let text = String::from_utf8(response.body)?;

        assert_eq!(response.status, 200);
        assert!(text.contains("every_channel_archive_convergence_scrape_ok"));
        assert!(text.contains("every_channel_archive_convergence_ok"));
        assert!(text.contains("every_channel_archive_duplicate_hash_sequences"));
        assert!(text.contains("node=\"forge\""));
        assert!(text.contains(" 1.000000\n"));

        let health = archive_convergence_serve_response(&args, "GET", "/health");
        assert_eq!(health.status, 200);

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_convergence_serve_metrics_response_reports_scrape_failure() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-convergence-serve-empty-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let publisher_a = root.join("publisher-a").join("manifests");
        let publisher_b = root.join("publisher-b").join("manifests");
        fs::create_dir_all(&publisher_a)?;
        fs::create_dir_all(&publisher_b)?;

        let args = ArchiveConvergenceServeArgs {
            source: vec![
                format!("nuc-a={}", publisher_a.display()),
                format!("nuc-b={}", publisher_b.display()),
            ],
            broadcast: "la-kcop".to_string(),
            track: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
            stream_id: None,
            rendition: Some("primary".to_string()),
            start_sequence: None,
            end_sequence: None,
            listen: "127.0.0.1:0".to_string(),
            metrics_node: None,
            metrics_role: "duplicate-proof".to_string(),
        };

        let response = archive_convergence_serve_response(&args, "GET", "/metrics");
        let text = String::from_utf8(response.body)?;

        assert_eq!(response.status, 200);
        assert!(text.contains("# archive_convergence_error"));
        assert!(text.contains("every_channel_archive_convergence_scrape_ok"));
        assert!(text.contains(" 0.000000\n"));

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_convergence_measure_parses_agent_manifest_records() -> Result<()> {
        let hash = blake3::hash(b"agent-record").to_hex().to_string();
        let record = ArchiveIndexRecord {
            received_unix_ms: 1_000,
            relay_url: "https://lax.relay.every.channel/anon".to_string(),
            source_node: Some("publisher-a".to_string()),
            source_session: Some("publisher-a-session".to_string()),
            media_timing: None,
            broadcast_name: "la-kcop".to_string(),
            track_name: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
            group_sequence: 7,
            frame_count: 1,
            size_bytes: 512,
            blake3: hash.clone(),
            cas_path: format!("objects/blake3/{}/{hash}.bin", &hash[0..2]),
        };
        let body = serde_json::json!({
            "ok": true,
            "invalid_lines": 1,
            "records": [
                record,
                {"not": "an archive index record"}
            ],
        })
        .to_string();

        let (records, invalid) = parse_agent_archive_records(&body)?;

        assert_eq!(records.len(), 1);
        assert_eq!(invalid, 2);
        assert_eq!(records[0].source_node.as_deref(), Some("publisher-a"));
        assert_eq!(records[0].group_sequence, 7);
        Ok(())
    }

    #[test]
    fn archive_convergence_measure_ignores_partial_tail_line() -> Result<()> {
        let hash = blake3::hash(b"jsonl-record").to_hex().to_string();
        let record = ArchiveIndexRecord {
            received_unix_ms: 2_000,
            relay_url: "https://lax.relay.every.channel/anon".to_string(),
            source_node: Some("publisher-b".to_string()),
            source_session: Some("publisher-b-session".to_string()),
            media_timing: None,
            broadcast_name: "la-kcop".to_string(),
            track_name: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
            group_sequence: 11,
            frame_count: 1,
            size_bytes: 1024,
            blake3: hash.clone(),
            cas_path: format!("objects/blake3/{}/{hash}.bin", &hash[0..2]),
        };
        let body = format!(
            "partial-json-tail\n{}\n{{\"bad\": true}}\n",
            serde_json::to_string(&record)?
        );

        let (records, invalid) = parse_archive_records_jsonl(&body);

        assert_eq!(records.len(), 1);
        assert_eq!(invalid, 1);
        assert_eq!(records[0].source_node.as_deref(), Some("publisher-b"));
        assert_eq!(records[0].group_sequence, 11);
        Ok(())
    }

    #[test]
    fn archive_convergence_measure_requires_elapsed_samples() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-convergence-measure-summary-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let publisher_a = root.join("publisher-a").join("manifests");
        let publisher_b = root.join("publisher-b").join("manifests");
        fs::create_dir_all(publisher_a.join("la-kcop"))?;
        fs::create_dir_all(publisher_b.join("la-kcop"))?;

        let hash = blake3::hash(b"elapsed-perfect-match").to_hex().to_string();
        for (manifest_root, source_node) in
            [(&publisher_a, "publisher-a"), (&publisher_b, "publisher-b")]
        {
            let record = ArchiveIndexRecord {
                received_unix_ms: 1_000,
                relay_url: "https://lax.relay.every.channel/anon".to_string(),
                source_node: Some(source_node.to_string()),
                source_session: Some(format!("{source_node}-session")),
                media_timing: Some(ArchiveRecordMediaTiming {
                    track_id: 7,
                    timescale: 90_000,
                    decode_time: 0,
                    sequence: 0,
                }),
                broadcast_name: "la-kcop".to_string(),
                track_name: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
                group_sequence: 0,
                frame_count: 1,
                size_bytes: 1024,
                blake3: hash.clone(),
                cas_path: format!("objects/blake3/{}/{hash}.bin", &hash[0..2]),
            };
            append_archive_index_record(
                &manifest_root
                    .join("la-kcop")
                    .join(format!("{WT_PUBLISHER_ORIGIN_TRACK}.jsonl")),
                &record,
            )?;
        }

        let convergence = archive_convergence_report(&ArchiveConvergenceArgs {
            source: vec![
                format!("nuc-a={}", publisher_a.display()),
                format!("nuc-b={}", publisher_b.display()),
            ],
            broadcast: "la-kcop".to_string(),
            track: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
            stream_id: None,
            rendition: Some("primary".to_string()),
            start_sequence: None,
            end_sequence: None,
            pretty: false,
            prometheus: false,
            metrics_node: None,
            metrics_role: "duplicate-proof".to_string(),
            require_ok: false,
        })?;
        assert!(convergence.ok);

        let summary = archive_convergence_measure_summary(&[ArchiveConvergenceMeasureSample {
            sample_unix_ms: 1_000,
            sources: vec![
                ArchiveConvergenceMeasureFetch {
                    name: "nuc-a".to_string(),
                    kind: "manifest".to_string(),
                    url: "https://example.invalid/a.jsonl".to_string(),
                    ok: true,
                    status: Some(200),
                    elapsed_ms: 1,
                    record_count: 1,
                    invalid_record_count: 0,
                    error: None,
                },
                ArchiveConvergenceMeasureFetch {
                    name: "nuc-b".to_string(),
                    kind: "manifest".to_string(),
                    url: "https://example.invalid/b.jsonl".to_string(),
                    ok: true,
                    status: Some(200),
                    elapsed_ms: 1,
                    record_count: 1,
                    invalid_record_count: 0,
                    error: None,
                },
            ],
            convergence: Some(convergence),
            convergence_error: None,
            prometheus: Vec::new(),
        }]);

        assert!(!summary.ok);
        assert_eq!(summary.sample_count, 1);
        assert_eq!(summary.elapsed_ms, 0);
        assert_eq!(summary.latest_matching_duplicate_sequences, Some(1));
        assert_eq!(summary.latest_divergent_sequences, Some(0));
        assert_eq!(summary.reasons, vec!["insufficient_elapsed_samples"]);

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_convergence_measure_reads_prometheus_file_sd_agents() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-convergence-file-sd-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        fs::create_dir_all(&root)?;
        let sd_path = root.join("node-agents.json");
        fs::write(
            &sd_path,
            serde_json::json!([
                {
                    "targets": ["100.64.0.5:7799"],
                    "labels": {
                        "headscale_name": "nuc-a",
                        "headscale_user": "node"
                    }
                },
                {
                    "targets": ["127.0.0.1:7799"],
                    "labels": {
                        "headscale_name": "ecp-forge",
                        "headscale_user": "forge"
                    }
                },
                {
                    "targets": ["http://100.64.0.6:7799/"],
                    "labels": {
                        "headscale_name": "nuc-b",
                        "headscale_user": "node"
                    }
                }
            ])
            .to_string(),
        )?;
        let args = ArchiveConvergenceMeasureArgs::parse_from([
            "archive-convergence-measure",
            "--agent-prometheus-sd",
            sd_path.to_str().expect("utf-8 temp path"),
            "--agent-prometheus-sd-label",
            "headscale_user=node",
            "--broadcast",
            "la-kcop",
        ]);

        let (entries, errors) = archive_measure_prometheus_sd_agent_manifests(&args);

        assert!(errors.is_empty(), "{errors:?}");
        assert_eq!(
            entries,
            vec![
                "nuc-a=http://100.64.0.5:7799".to_string(),
                "nuc-b=http://100.64.0.6:7799".to_string()
            ]
        );

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_convergence_measure_serve_metrics_report_elapsed_gate() {
        let args = ArchiveConvergenceMeasureServeArgs {
            agent_manifest: vec![
                "nuc-a=http://nuc-a.example.invalid:7799".to_string(),
                "nuc-b=http://nuc-b.example.invalid:7799".to_string(),
            ],
            agent_prometheus_sd: Vec::new(),
            agent_prometheus_sd_label: Vec::new(),
            agent_manifest_role: "publisher-buffer".to_string(),
            manifest: Vec::new(),
            broadcast: "la-kcop".to_string(),
            track: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
            stream_id: None,
            rendition: Some("primary".to_string()),
            start_sequence: None,
            end_sequence: None,
            prometheus_url: None,
            timeout_ms: 1000,
            max_manifest_bytes: 1024 * 1024,
            listen: "127.0.0.1:7813".to_string(),
            max_samples: 16,
            min_elapsed_seconds: 30.0,
            metrics_node: Some("forge".to_string()),
            metrics_role: "duplicate-proof".to_string(),
        };
        let sample = ArchiveConvergenceMeasureSample {
            sample_unix_ms: 1_000,
            sources: vec![
                ArchiveConvergenceMeasureFetch {
                    name: "nuc-a".to_string(),
                    kind: "agent-manifest".to_string(),
                    url: "http://nuc-a.example.invalid:7799/v1/archive-manifest".to_string(),
                    ok: true,
                    status: Some(200),
                    elapsed_ms: 10,
                    record_count: 3,
                    invalid_record_count: 0,
                    error: None,
                },
                ArchiveConvergenceMeasureFetch {
                    name: "nuc-b".to_string(),
                    kind: "agent-manifest".to_string(),
                    url: "http://nuc-b.example.invalid:7799/v1/archive-manifest".to_string(),
                    ok: true,
                    status: Some(200),
                    elapsed_ms: 11,
                    record_count: 3,
                    invalid_record_count: 0,
                    error: None,
                },
            ],
            convergence: None,
            convergence_error: Some("synthetic convergence missing".to_string()),
            prometheus: Vec::new(),
        };
        let mut next_sample = sample.clone();
        next_sample.sample_unix_ms = 1_100;
        let report =
            archive_convergence_measure_report_from_samples(&args, vec![sample, next_sample]);

        assert!(!report.ok);
        assert_eq!(report.summary.elapsed_ms, 100);
        assert!(report
            .summary
            .reasons
            .iter()
            .any(|reason| reason == "elapsed_window_too_short"));

        let metrics = archive_convergence_measure_prometheus_metrics(&report, &args);
        assert!(metrics.contains("every_channel_archive_convergence_measure_ok"));
        assert!(metrics.contains("reason=\"elapsed_window_too_short\""));
        assert!(metrics.contains("source=\"nuc-a\""));
        assert!(metrics.contains("every_channel_archive_convergence_scrape_ok"));
    }

    #[test]
    fn archive_convergence_defaults_to_publisher_origin_track() {
        let args = ArchiveConvergenceArgs::parse_from([
            "archive-convergence",
            "--source",
            "nuc-a=/tmp/a",
            "--source",
            "nuc-b=/tmp/b",
            "--broadcast",
            "la-kcop",
        ]);

        assert_eq!(args.track, WT_PUBLISHER_ORIGIN_TRACK);
    }

    #[test]
    fn archive_convergence_require_ok_fails_on_incomplete_proof() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-convergence-require-ok-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let publisher_a = root.join("publisher-a").join("manifests");
        let publisher_b = root.join("publisher-b").join("manifests");
        fs::create_dir_all(publisher_a.join("la-kcop"))?;
        fs::create_dir_all(publisher_b.join("la-kcop"))?;

        let hash = blake3::hash(b"single-source").to_hex().to_string();
        let record = ArchiveIndexRecord {
            received_unix_ms: 1_000,
            relay_url: "https://lax.relay.every.channel/anon".to_string(),
            source_node: Some("publisher-a".to_string()),
            source_session: Some("publisher-a-test".to_string()),
            media_timing: None,
            broadcast_name: "la-kcop".to_string(),
            track_name: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
            group_sequence: 0,
            frame_count: 1,
            size_bytes: 1024,
            blake3: hash.clone(),
            cas_path: format!("objects/blake3/{}/{hash}.bin", &hash[0..2]),
        };
        append_archive_index_record(
            &publisher_a
                .join("la-kcop")
                .join(format!("{WT_PUBLISHER_ORIGIN_TRACK}.jsonl")),
            &record,
        )?;

        let err = archive_convergence_command(ArchiveConvergenceArgs {
            source: vec![
                format!("nuc-a={}", publisher_a.display()),
                format!("nuc-b={}", publisher_b.display()),
            ],
            broadcast: "la-kcop".to_string(),
            track: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
            stream_id: None,
            rendition: Some("primary".to_string()),
            start_sequence: None,
            end_sequence: None,
            pretty: false,
            prometheus: false,
            metrics_node: None,
            metrics_role: "duplicate-proof".to_string(),
            require_ok: true,
        })
        .expect_err("require-ok should fail incomplete duplicate proof");

        assert!(err
            .to_string()
            .contains("archive convergence failed for la-kcop/publisher.m4s"));

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_convergence_rejects_single_source_coverage_as_duplicate_proof() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-convergence-incomplete-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let publisher_a = root.join("publisher-a").join("manifests");
        let publisher_b = root.join("publisher-b").join("manifests");
        fs::create_dir_all(publisher_a.join("la-kcop"))?;
        fs::create_dir_all(publisher_b.join("la-kcop"))?;

        let mut lines = String::new();
        for sequence in 0..2u64 {
            let hash = blake3::hash(format!("la-kcop-only-a-{sequence}").as_bytes())
                .to_hex()
                .to_string();
            let record = ArchiveIndexRecord {
                received_unix_ms: 1_000 + sequence * 1_000,
                relay_url: "https://lax.relay.every.channel/anon".to_string(),
                source_node: Some("publisher-a".to_string()),
                source_session: Some("publisher-a-test".to_string()),
                media_timing: None,
                broadcast_name: "la-kcop".to_string(),
                track_name: WT_PUBLISH_PRIMARY_VIDEO_TRACK.to_string(),
                group_sequence: sequence,
                frame_count: 30,
                size_bytes: 1024,
                blake3: hash.clone(),
                cas_path: format!("objects/blake3/{}/{hash}.bin", &hash[0..2]),
            };
            lines.push_str(&serde_json::to_string(&record)?);
            lines.push('\n');
        }
        fs::write(publisher_a.join("la-kcop").join("0.m4s.jsonl"), lines)?;

        let report = archive_convergence_report(&ArchiveConvergenceArgs {
            source: vec![
                format!("nuc-a={}", publisher_a.display()),
                format!("nuc-b={}", publisher_b.display()),
            ],
            broadcast: "la-kcop".to_string(),
            track: WT_PUBLISH_PRIMARY_VIDEO_TRACK.to_string(),
            stream_id: None,
            rendition: Some("720p".to_string()),
            start_sequence: None,
            end_sequence: None,
            pretty: false,
            prometheus: false,
            metrics_node: None,
            metrics_role: "duplicate-proof".to_string(),
            require_ok: false,
        })?;

        assert!(!report.ok);
        assert!(!report.duplicate_complete);
        assert_eq!(report.summary.expected_sequences, 2);
        assert_eq!(report.summary.missing_sequences, Vec::<u64>::new());
        assert_eq!(
            report.summary.matching_duplicate_sequences,
            Vec::<u64>::new()
        );
        assert_eq!(
            report.reasons,
            vec![
                "duplicate_sequences_incomplete".to_string(),
                "media_timing_missing".to_string(),
                "source_identity_not_diverse".to_string(),
            ]
        );

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_convergence_rejects_legacy_records_without_source_identity() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-convergence-no-source-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let publisher_a = root.join("publisher-a").join("manifests");
        let publisher_b = root.join("publisher-b").join("manifests");
        for manifest_root in [&publisher_a, &publisher_b] {
            fs::create_dir_all(manifest_root.join("la-kcop"))?;
            let mut lines = String::new();
            for sequence in 0..2u64 {
                let hash = blake3::hash(format!("legacy-source-{sequence}").as_bytes())
                    .to_hex()
                    .to_string();
                lines.push_str(
                    &serde_json::json!({
                        "received_unix_ms": 1_000 + sequence * 1_000,
                        "relay_url": "https://lax.relay.every.channel/anon",
                        "broadcast_name": "la-kcop",
                        "track_name": WT_PUBLISH_PRIMARY_VIDEO_TRACK,
                        "group_sequence": sequence,
                        "frame_count": 30,
                        "size_bytes": 1024,
                        "blake3": hash,
                        "cas_path": format!("objects/blake3/{}/{hash}.bin", &hash[0..2]),
                    })
                    .to_string(),
                );
                lines.push('\n');
            }
            fs::write(manifest_root.join("la-kcop").join("0.m4s.jsonl"), lines)?;
        }

        let report = archive_convergence_report(&ArchiveConvergenceArgs {
            source: vec![
                format!("nuc-a={}", publisher_a.display()),
                format!("nuc-b={}", publisher_b.display()),
            ],
            broadcast: "la-kcop".to_string(),
            track: WT_PUBLISH_PRIMARY_VIDEO_TRACK.to_string(),
            stream_id: None,
            rendition: Some("720p".to_string()),
            start_sequence: None,
            end_sequence: None,
            pretty: false,
            prometheus: false,
            metrics_node: None,
            metrics_role: "duplicate-proof".to_string(),
            require_ok: false,
        })?;

        assert!(!report.ok);
        assert_eq!(report.missing_source_identity_records, 4);
        assert!(!report.record_source_identity_ok);
        assert!(report
            .reasons
            .contains(&"source_identity_missing".to_string()));

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_convergence_rejects_mirrored_same_source_records() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-convergence-mirrored-source-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let publisher_a = root.join("publisher-a").join("manifests");
        let publisher_b = root.join("publisher-b").join("manifests");
        for manifest_root in [&publisher_a, &publisher_b] {
            fs::create_dir_all(manifest_root.join("la-kcop"))?;
            let mut lines = String::new();
            for sequence in 0..2u64 {
                let hash = blake3::hash(format!("mirrored-source-{sequence}").as_bytes())
                    .to_hex()
                    .to_string();
                let record = ArchiveIndexRecord {
                    received_unix_ms: 1_000 + sequence * 1_000,
                    relay_url: "https://lax.relay.every.channel/anon".to_string(),
                    source_node: Some("archive-origin".to_string()),
                    source_session: Some("archive-origin-session".to_string()),
                    media_timing: None,
                    broadcast_name: "la-kcop".to_string(),
                    track_name: WT_PUBLISH_PRIMARY_VIDEO_TRACK.to_string(),
                    group_sequence: sequence,
                    frame_count: 30,
                    size_bytes: 1024,
                    blake3: hash.clone(),
                    cas_path: format!("objects/blake3/{}/{hash}.bin", &hash[0..2]),
                };
                lines.push_str(&serde_json::to_string(&record)?);
                lines.push('\n');
            }
            fs::write(manifest_root.join("la-kcop").join("0.m4s.jsonl"), lines)?;
        }

        let report = archive_convergence_report(&ArchiveConvergenceArgs {
            source: vec![
                format!("nuc-a={}", publisher_a.display()),
                format!("nuc-b={}", publisher_b.display()),
            ],
            broadcast: "la-kcop".to_string(),
            track: WT_PUBLISH_PRIMARY_VIDEO_TRACK.to_string(),
            stream_id: None,
            rendition: Some("720p".to_string()),
            start_sequence: None,
            end_sequence: None,
            pretty: false,
            prometheus: false,
            metrics_node: None,
            metrics_role: "duplicate-proof".to_string(),
            require_ok: false,
        })?;

        assert!(!report.ok);
        assert_eq!(
            report.record_source_nodes,
            vec!["archive-origin".to_string()]
        );
        assert_eq!(report.record_source_count, 1);
        assert_eq!(
            report.reasons,
            vec![
                "duplicate_sequences_incomplete".to_string(),
                "media_timing_missing".to_string(),
                "source_identity_not_diverse".to_string(),
            ]
        );

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_convergence_reports_source_local_hash_divergence() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-convergence-local-divergence-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let publisher_a = root.join("publisher-a").join("manifests");
        let publisher_b = root.join("publisher-b").join("manifests");
        for manifest_root in [&publisher_a, &publisher_b] {
            fs::create_dir_all(manifest_root.join("la-kcop"))?;
        }

        let write_record = |manifest_root: &Path,
                            source_node: &str,
                            sequence: u64,
                            hash_material: &str| {
            let hash = blake3::hash(hash_material.as_bytes()).to_hex().to_string();
            let record = ArchiveIndexRecord {
                received_unix_ms: 1_000 + sequence * 1_000,
                relay_url: "https://lax.relay.every.channel/anon".to_string(),
                source_node: Some(source_node.to_string()),
                source_session: Some(format!("{source_node}-test")),
                media_timing: None,
                broadcast_name: "la-kcop".to_string(),
                track_name: WT_PUBLISH_PRIMARY_VIDEO_TRACK.to_string(),
                group_sequence: sequence,
                frame_count: 30,
                size_bytes: 1024,
                blake3: hash.clone(),
                cas_path: format!("objects/blake3/{}/{hash}.bin", &hash[0..2]),
            };
            append_archive_index_record(&manifest_root.join("la-kcop").join("0.m4s.jsonl"), &record)
        };

        write_record(&publisher_a, "publisher-a", 0, "same")?;
        write_record(&publisher_a, "publisher-a", 0, "different")?;
        write_record(&publisher_b, "publisher-b", 0, "same")?;

        let report = archive_convergence_report(&ArchiveConvergenceArgs {
            source: vec![
                format!("nuc-a={}", publisher_a.display()),
                format!("nuc-b={}", publisher_b.display()),
            ],
            broadcast: "la-kcop".to_string(),
            track: WT_PUBLISH_PRIMARY_VIDEO_TRACK.to_string(),
            stream_id: None,
            rendition: Some("720p".to_string()),
            start_sequence: None,
            end_sequence: None,
            pretty: false,
            prometheus: false,
            metrics_node: None,
            metrics_role: "duplicate-proof".to_string(),
            require_ok: false,
        })?;

        assert!(!report.ok);
        assert_eq!(report.summary.source_local_divergent_sequences, vec![0]);
        assert!(report
            .reasons
            .contains(&"source_local_divergent_sequences".to_string()));

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_serve_alias_parses_for_node_agent_supervision() {
        let cli = Cli::parse_from([
            "ec-node",
            "archive-serve",
            "--output-dir",
            "/tmp/ec-archive",
            "--manifest-dir",
            "/tmp/ec-archive/manifests",
            "--listen",
            "127.0.0.1:7789",
        ]);

        assert!(matches!(cli.command, Commands::WtArchiveServe(_)));
    }

    #[test]
    fn sim_duplicate_publishers_default_campaign_passes() -> Result<()> {
        let args = SimDuplicatePublishersArgs::parse_from([
            "sim-duplicate-publishers",
            "--seed",
            "1",
            "--iterations",
            "128",
        ]);

        let output = sim_duplicate_publishers_output(&args)?;

        assert!(output.ok, "{:?}", output.campaign.first_failure);
        assert_eq!(output.campaign.passed, 128);
        assert_eq!(output.campaign.failed, 0);
        assert!(output.campaign.max_duplicate_complete_ms_observed <= 3_000);
        assert!(output.campaign.total_transient_dropped_observations > 0);
        assert!(output.campaign.total_partition_delayed_observations > 0);
        assert!(output.campaign.total_publisher_outage_observations > 0);
        assert!(output.campaign.total_backfill_observations > 0);
        Ok(())
    }

    #[test]
    fn sim_control_plane_default_campaign_passes() -> Result<()> {
        let args = SimControlPlaneArgs::parse_from([
            "sim-control-plane",
            "--seed",
            "1",
            "--iterations",
            "128",
        ]);

        let output = sim_control_plane_output(&args)?;

        assert!(output.ok, "{:?}", output.campaign.first_failure);
        assert_eq!(output.campaign.passed, 128);
        assert_eq!(output.campaign.failed, 0);
        assert!(output.campaign.max_propagation_complete_ms_observed <= 900);
        assert!(output.campaign.total_transient_dropped_messages > 0);
        assert!(output.campaign.total_partition_delayed_messages > 0);
        assert!(output.campaign.total_node_outage_delayed_messages > 0);
        assert!(output.campaign.total_duplicate_messages > 0);
        Ok(())
    }

    #[test]
    fn sim_control_plane_preserves_replayable_failure() -> Result<()> {
        let args = SimControlPlaneArgs::parse_from([
            "sim-control-plane",
            "--seed",
            "1",
            "--iterations",
            "1",
            "--fanout",
            "0",
            "--allow-failure",
        ]);

        let output = sim_control_plane_output(&args)?;
        let failure = output
            .campaign
            .first_failure
            .as_ref()
            .expect("zero fanout should produce a replayable failure");

        assert!(!output.ok);
        assert_eq!(failure.replay_hint, "EC_SIM_SEED=0000000000000001");
        assert_eq!(failure.report.known_nodes, vec!["nuc-a".to_string()]);
        assert_eq!(failure.report.missing_nodes.len(), 6);
        assert_eq!(
            failure.invariant.failures,
            vec![
                "propagation_incomplete".to_string(),
                "propagation_deadline_unreached".to_string(),
            ]
        );
        assert!(!failure.report.trace.is_empty());
        Ok(())
    }

    #[test]
    fn sim_control_plane_replays_exact_scenario_json() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-sim-control-plane-replay-{}",
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        fs::create_dir_all(&root)?;
        let scenario_path = root.join("scenario.json");
        let mut scenario = ec_core::sim::ControlPlanePropagationScenario::new(
            ec_core::sim::SimulationSeed::new(44),
            vec![
                "nuc-a".to_string(),
                "nuc-b".to_string(),
                "tower".to_string(),
                "forge".to_string(),
            ],
            "nuc-a",
            "ec.control.broadcast.la-kcop",
            "la-kcop@44",
        );
        scenario.fanout = 0;
        scenario.transient_drop_per_million = 0;
        fs::write(&scenario_path, serde_json::to_vec_pretty(&scenario)?)?;

        let args = SimControlPlaneArgs::parse_from(vec![
            "sim-control-plane".to_string(),
            "--scenario-json".to_string(),
            scenario_path.display().to_string(),
            "--allow-failure".to_string(),
        ]);
        let output = sim_control_plane_output(&args)?;
        let direct_report = ec_core::sim::run_control_plane_propagation_simulation(&scenario);
        let failure = output
            .campaign
            .first_failure
            .as_ref()
            .expect("zero-fanout scenario should fail");

        assert!(!output.ok);
        assert_eq!(output.scenario_template, scenario);
        assert_eq!(
            output.campaign.seed_start,
            ec_core::sim::SimulationSeed::new(44)
        );
        assert_eq!(output.campaign.iterations, 1);
        assert_eq!(failure.report, direct_report);
        assert_eq!(failure.report.known_nodes, vec!["nuc-a".to_string()]);

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn sim_control_plane_rejects_unknown_origin() {
        let args = SimControlPlaneArgs::parse_from([
            "sim-control-plane",
            "--node",
            "nuc-a",
            "--node",
            "nuc-b",
            "--origin-node",
            "forge",
        ]);

        let err = sim_control_plane_output(&args).expect_err("unknown origin should fail");

        assert!(err.to_string().contains("origin node"), "{err:#}");
    }

    #[test]
    fn sim_system_default_campaign_passes_with_global_sequence_clock() -> Result<()> {
        let args = SimSystemArgs::parse_from(["sim-system", "--seed", "1", "--iterations", "128"]);

        let output = sim_system_output(&args)?;

        assert!(output.ok, "{:?}", output.campaign.first_failure);
        assert_eq!(output.campaign.passed, 128);
        assert_eq!(output.campaign.failed, 0);
        assert!(output.campaign.max_system_complete_ms_observed <= 3_500);
        assert!(output.campaign.max_control_propagation_ms_observed > 0);
        assert!(output.campaign.max_media_duplicate_complete_ms_observed > 0);
        assert!(output.campaign.total_control_transient_drops > 0);
        assert!(output.campaign.total_media_transient_drops > 0);
        assert!(output.campaign.total_media_backfill_observations > 0);
        assert!(output.campaign.seeds_with_control_transient_drops > 0);
        assert!(output.campaign.seeds_with_media_transient_drops > 0);
        assert!(output.campaign.seeds_with_media_backfill_observations > 0);
        assert!(!output.campaign.slowest_system_runs.is_empty());
        Ok(())
    }

    #[test]
    fn sim_system_foundationdb_profile_exercises_fault_coverage() -> Result<()> {
        let args = SimSystemArgs::parse_from([
            "sim-system",
            "--fault-profile",
            "foundationdb",
            "--seed",
            "1",
            "--iterations",
            "128",
            "--max-system-complete-ms",
            "6000",
        ]);

        let output = sim_system_output(&args)?;

        assert!(output.ok, "{:?}", output.campaign.first_failure);
        assert!(output.fault_coverage.required);
        assert!(output.fault_coverage.ok, "{:?}", output.fault_coverage);
        assert_eq!(output.fault_coverage.min_seed_coverage, 4);
        assert!(output.runtime.wall_elapsed_ms >= 0.0);
        assert!(output.runtime.iterations_per_second > 0.0);
        assert!(output.runtime.simulated_system_seconds_per_wall_second > 0.0);
        assert!(output.runtime.trace_events_per_second > 0.0);
        assert_eq!(output.campaign.passed, 128);
        assert_eq!(output.campaign.failed, 0);
        assert!(output.campaign.fault_coverage_ok());
        assert!(output.campaign.total_system_complete_ms_observed > 0);
        assert!(output.campaign.total_trace_events > 0);
        assert!(output.campaign.total_control_partition_delays > 0);
        assert!(output.campaign.total_control_node_outage_delays > 0);
        assert!(output.campaign.total_control_duplicate_messages > 0);
        assert!(output.campaign.total_media_partition_delays > 0);
        assert!(output.campaign.total_media_publisher_outages > 0);
        assert!(output.campaign.total_media_backfill_observations > 0);
        assert!(output.campaign.seeds_with_control_partition_delays > 0);
        assert!(output.campaign.seeds_with_control_node_outage_delays > 0);
        assert!(output.campaign.seeds_with_control_duplicate_messages > 0);
        assert!(output.campaign.seeds_with_media_partition_delays > 0);
        assert!(output.campaign.seeds_with_media_publisher_outages > 0);
        assert!(output.campaign.seeds_with_media_backfill_observations > 0);
        assert!(!output.campaign.slowest_system_runs.is_empty());
        Ok(())
    }

    #[test]
    fn sim_system_foundationdb_profile_rejects_weak_seed_coverage() -> Result<()> {
        let args = SimSystemArgs::parse_from([
            "sim-system",
            "--fault-profile",
            "foundationdb",
            "--seed",
            "1",
            "--iterations",
            "32",
            "--max-system-complete-ms",
            "6000",
            "--min-fault-seed-coverage",
            "33",
            "--allow-failure",
        ]);

        let output = sim_system_output(&args)?;

        assert!(!output.ok);
        assert!(output.campaign.all_passed());
        assert!(output.fault_coverage.required);
        assert_eq!(output.fault_coverage.min_seed_coverage, 33);
        assert!(output
            .fault_coverage
            .failures
            .contains(&"control_transient_drops_seed_coverage_below_min".to_string()));
        assert!(output
            .fault_coverage
            .failures
            .contains(&"media_backfill_seed_coverage_below_min".to_string()));
        Ok(())
    }

    #[test]
    fn sim_system_fault_coverage_gate_rejects_weak_campaign() -> Result<()> {
        let args = SimSystemArgs::parse_from([
            "sim-system",
            "--seed",
            "1",
            "--iterations",
            "1",
            "--node",
            "forge",
            "--node",
            "nuc-a",
            "--node",
            "nuc-b",
            "--require-fault-coverage",
            "--allow-failure",
        ]);

        let output = sim_system_output(&args)?;

        assert!(!output.ok);
        assert!(output.campaign.all_passed());
        assert!(output.campaign.first_failure.is_none());
        assert!(output.fault_coverage.required);
        assert!(!output.fault_coverage.ok);
        assert!(output
            .fault_coverage
            .failures
            .contains(&"control_node_outage_delays_uncovered".to_string()));
        Ok(())
    }

    #[test]
    fn sim_system_writes_weak_coverage_artifact() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-sim-system-weak-coverage-{}",
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        fs::create_dir_all(&root)?;
        let artifact_path = root.join("weak-coverage.json");
        let args = SimSystemArgs::parse_from(vec![
            "sim-system".to_string(),
            "--seed".to_string(),
            "1".to_string(),
            "--iterations".to_string(),
            "1".to_string(),
            "--node".to_string(),
            "forge".to_string(),
            "--node".to_string(),
            "nuc-a".to_string(),
            "--node".to_string(),
            "nuc-b".to_string(),
            "--require-fault-coverage".to_string(),
            "--failure-artifact".to_string(),
            artifact_path.display().to_string(),
            "--allow-failure".to_string(),
        ]);

        sim_system_command(args)?;
        let artifact: serde_json::Value = serde_json::from_slice(&fs::read(&artifact_path)?)?;
        assert_eq!(
            artifact["artifact_type"],
            "every.channel.sim.system_duplicate_publishers.weak_coverage.v1"
        );
        assert!(artifact["rerun_command"]
            .as_str()
            .unwrap_or_default()
            .contains("--require-fault-coverage"));
        assert_eq!(artifact["campaign"]["iterations"], 1);
        assert!(artifact["fault_coverage"]["required"]
            .as_bool()
            .unwrap_or(false));
        assert!(!artifact["fault_coverage"]["ok"].as_bool().unwrap_or(true));
        let failures = artifact["fault_coverage"]["failures"]
            .as_array()
            .expect("weak coverage artifact has failure array");
        assert!(failures
            .iter()
            .any(|failure| failure == "control_node_outage_delays_uncovered"));
        assert!(artifact.get("scenario_template").is_some());

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn sim_system_rejects_local_activation_sequence_clock() -> Result<()> {
        let args = SimSystemArgs::parse_from([
            "sim-system",
            "--seed",
            "1",
            "--iterations",
            "1",
            "--sequence-clock",
            "local-activation",
            "--allow-failure",
        ]);

        let output = sim_system_output(&args)?;
        let failure = output
            .campaign
            .first_failure
            .as_ref()
            .expect("local activation clock should produce a replayable failure");

        assert!(!output.ok);
        assert_eq!(failure.replay_hint, "EC_SIM_SEED=0000000000000001");
        assert!(failure
            .invariant
            .failures
            .contains(&"media_divergent_sequences".to_string()));
        assert!(!failure.report.media.summary.divergent_sequences.is_empty());
        Ok(())
    }

    #[test]
    fn sim_system_writes_replayable_foundationdb_failure_artifact() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-sim-system-artifact-{}",
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        fs::create_dir_all(&root)?;
        let artifact_path = root.join("failure.json");
        let args = SimSystemArgs::parse_from(vec![
            "sim-system".to_string(),
            "--fault-profile".to_string(),
            "foundationdb".to_string(),
            "--seed".to_string(),
            "1".to_string(),
            "--iterations".to_string(),
            "256".to_string(),
            "--sequence-clock".to_string(),
            "local-activation".to_string(),
            "--max-system-complete-ms".to_string(),
            "6000".to_string(),
            "--failure-artifact".to_string(),
            artifact_path.display().to_string(),
            "--allow-failure".to_string(),
        ]);

        sim_system_command(args)?;
        let artifact: serde_json::Value = serde_json::from_slice(&fs::read(&artifact_path)?)?;
        assert_eq!(
            artifact["artifact_type"],
            "every.channel.sim.system_duplicate_publishers.failure.v1"
        );
        assert_eq!(artifact["replay_scenario_pointer"], "/replay_scenario");
        assert!(artifact["replay_command"]
            .as_str()
            .unwrap_or_default()
            .contains("sim-system --scenario-json -"));
        assert!(artifact["replay_command"]
            .as_str()
            .unwrap_or_default()
            .contains("--max-system-complete-ms 6000"));

        let replay_scenario: ec_core::sim::SystemDuplicatePublisherScenario =
            serde_json::from_value(artifact["replay_scenario"].clone())?;
        let report = ec_core::sim::run_system_duplicate_publisher_simulation(&replay_scenario);
        let invariant = ec_core::sim::check_system_duplicate_publisher_invariants(
            &report,
            &ec_core::sim::SystemDuplicatePublisherInvariantConfig::complete_with_deadline(6_000),
        );
        assert!(!invariant.ok);
        assert!(invariant
            .failures
            .contains(&"media_divergent_sequences".to_string()));
        assert!(!report.media.summary.divergent_sequences.is_empty());

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn sim_duplicate_publishers_preserves_replayable_failure() -> Result<()> {
        let args = SimDuplicatePublishersArgs::parse_from([
            "sim-duplicate-publishers",
            "--seed",
            "19",
            "--iterations",
            "1",
            "--encoder-drift",
            "nuc-b:17:x264-hd3-drift",
            "--allow-failure",
        ]);

        let output = sim_duplicate_publishers_output(&args)?;
        let failure = output
            .campaign
            .first_failure
            .as_ref()
            .expect("drift should produce a replayable failure");
        let shrunk = failure
            .shrunk_failure
            .as_ref()
            .expect("drift should produce a shrunk replay");

        assert!(!output.ok);
        assert_eq!(failure.replay_hint, "EC_SIM_SEED=0000000000000013");
        assert_eq!(failure.report.summary.divergent_sequences, vec![17]);
        assert_eq!(failure.report.duplicate_complete_at_ms, None);
        assert!(!failure.report.trace.is_empty());
        assert_eq!(
            failure.invariant.failures,
            vec![
                "divergent_sequences".to_string(),
                "duplicate_incomplete".to_string(),
                "duplicate_complete_deadline_unreached".to_string(),
            ]
        );
        assert_eq!(shrunk.scenario.expected_sequences(), 18);
        assert_eq!(shrunk.report.summary.divergent_sequences, vec![17]);
        Ok(())
    }

    #[test]
    fn sim_duplicate_publishers_writes_replayable_failure_artifact() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-sim-duplicate-publisher-artifact-{}",
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        fs::create_dir_all(&root)?;
        let artifact_path = root.join("failure.json");
        let args = SimDuplicatePublishersArgs::parse_from(vec![
            "sim-duplicate-publishers".to_string(),
            "--seed".to_string(),
            "19".to_string(),
            "--iterations".to_string(),
            "1".to_string(),
            "--encoder-drift".to_string(),
            "nuc-b:17:x264-hd3-drift".to_string(),
            "--failure-artifact".to_string(),
            artifact_path.display().to_string(),
            "--allow-failure".to_string(),
        ]);

        sim_duplicate_publishers_command(args)?;
        let artifact: serde_json::Value = serde_json::from_slice(&fs::read(&artifact_path)?)?;
        assert_eq!(
            artifact["artifact_type"],
            "every.channel.sim.duplicate_publishers.failure.v1"
        );
        assert_eq!(artifact["shrunk"], true);
        assert_eq!(artifact["replay_scenario_pointer"], "/replay_scenario");
        assert!(artifact["replay_command"]
            .as_str()
            .unwrap_or_default()
            .contains("--scenario-json -"));

        let replay_scenario: ec_core::sim::DuplicatePublisherScenario =
            serde_json::from_value(artifact["replay_scenario"].clone())?;
        assert_eq!(replay_scenario.expected_sequences(), 18);
        let report = ec_core::sim::run_duplicate_publisher_simulation(&replay_scenario);
        let invariant = ec_core::sim::check_duplicate_publisher_invariants(
            &report,
            &ec_core::sim::DuplicatePublisherInvariantConfig::duplicate_complete_with_deadline(
                3_000,
            ),
        );
        assert!(!invariant.ok);
        assert_eq!(report.summary.divergent_sequences, vec![17]);

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn sim_duplicate_publishers_detects_unaligned_publisher_sequence_phase() -> Result<()> {
        let args = SimDuplicatePublishersArgs::parse_from([
            "sim-duplicate-publishers",
            "--seed",
            "1",
            "--iterations",
            "1",
            "--partition",
            "nuc-b:1000:1100:0",
            "--publisher-outage",
            "nuc-b:2000:2100:0",
            "--transient-drop-per-million",
            "0",
            "--max-jitter-ms",
            "0",
            "--publisher-sequence-offset",
            "nuc-b:3",
            "--allow-failure",
        ]);

        let output = sim_duplicate_publishers_output(&args)?;
        let failure = output
            .campaign
            .first_failure
            .as_ref()
            .expect("phase offset should produce a replayable failure");

        assert!(!output.ok);
        assert_eq!(
            output
                .scenario_template
                .publisher_sequence_offsets
                .get("nuc-b"),
            Some(&3)
        );
        assert_eq!(failure.report.summary.matching_duplicate_sequences.len(), 0);
        assert_eq!(
            failure
                .report
                .fault_stats
                .publisher_phase_offset_observations,
            48
        );
        assert!(failure
            .invariant
            .failures
            .contains(&"divergent_sequences".to_string()));
        Ok(())
    }

    #[test]
    fn sim_duplicate_publishers_detects_missing_media_timing() -> Result<()> {
        let args = SimDuplicatePublishersArgs::parse_from([
            "sim-duplicate-publishers",
            "--seed",
            "1",
            "--iterations",
            "1",
            "--transient-drop-per-million",
            "0",
            "--max-jitter-ms",
            "0",
            "--missing-media-timing-publisher",
            "nuc-b",
            "--allow-failure",
        ]);

        let output = sim_duplicate_publishers_output(&args)?;
        let failure = output
            .campaign
            .first_failure
            .as_ref()
            .expect("missing media timing should produce a replayable failure");

        assert!(!output.ok);
        assert!(output
            .scenario_template
            .missing_media_timing_publishers
            .contains("nuc-b"));
        assert!(failure
            .invariant
            .failures
            .contains(&"media_timing_missing_records".to_string()));
        assert_eq!(
            failure.report.summary.media_timing_missing_records,
            output.scenario_template.expected_sequences()
        );
        Ok(())
    }

    #[test]
    fn sim_duplicate_publishers_detects_conflicting_media_timing() -> Result<()> {
        let args = SimDuplicatePublishersArgs::parse_from([
            "sim-duplicate-publishers",
            "--seed",
            "1",
            "--iterations",
            "1",
            "--transient-drop-per-million",
            "0",
            "--max-jitter-ms",
            "0",
            "--publisher-media-time-offset",
            "nuc-b:17",
            "--allow-failure",
        ]);

        let output = sim_duplicate_publishers_output(&args)?;
        let failure = output
            .campaign
            .first_failure
            .as_ref()
            .expect("conflicting media timing should produce a replayable failure");

        assert!(!output.ok);
        assert_eq!(
            output
                .scenario_template
                .publisher_media_time_offsets_ms
                .get("nuc-b"),
            Some(&17)
        );
        assert!(failure
            .invariant
            .failures
            .contains(&"media_timing_conflict_sequences".to_string()));
        assert_eq!(
            failure.report.summary.media_timing_conflict_sequences.len() as u64,
            output.scenario_template.expected_sequences()
        );
        Ok(())
    }

    #[test]
    fn sim_duplicate_publishers_detects_independent_source_material() -> Result<()> {
        let args = SimDuplicatePublishersArgs::parse_from([
            "sim-duplicate-publishers",
            "--seed",
            "1",
            "--iterations",
            "1",
            "--transient-drop-per-million",
            "0",
            "--max-jitter-ms",
            "0",
            "--publisher-source-material",
            "nuc-b:independent-rf-window",
            "--allow-failure",
        ]);

        let output = sim_duplicate_publishers_output(&args)?;
        let failure = output
            .campaign
            .first_failure
            .as_ref()
            .expect("source material mismatch should produce a replayable failure");

        assert!(!output.ok);
        assert_eq!(
            output
                .scenario_template
                .publisher_source_material
                .get("nuc-b")
                .map(String::as_str),
            Some("independent-rf-window")
        );
        assert!(failure
            .invariant
            .failures
            .contains(&"source_material_mismatch_observations".to_string()));
        assert_eq!(
            failure
                .report
                .fault_stats
                .source_material_mismatch_observations,
            output.scenario_template.expected_sequences() * 2
        );
        assert!(!failure.report.summary.divergent_sequences.is_empty());
        Ok(())
    }

    #[test]
    fn sim_system_detects_independent_source_material() -> Result<()> {
        let args = SimSystemArgs::parse_from([
            "sim-system",
            "--seed",
            "1",
            "--iterations",
            "1",
            "--publisher-source-material",
            "nuc-b:independent-rf-window",
            "--allow-failure",
        ]);

        let output = sim_system_output(&args)?;
        let failure = output
            .campaign
            .first_failure
            .as_ref()
            .expect("source material mismatch should fail the composed system model");

        assert!(!output.ok);
        assert_eq!(
            output
                .scenario_template
                .media
                .publisher_source_material
                .get("nuc-b")
                .map(String::as_str),
            Some("independent-rf-window")
        );
        assert!(failure
            .invariant
            .failures
            .contains(&"media_source_material_mismatch_observations".to_string()));
        assert!(output.campaign.total_media_source_material_mismatches > 0);
        assert_eq!(
            output.campaign.seeds_with_media_source_material_mismatches,
            1
        );
        Ok(())
    }

    #[test]
    fn sim_duplicate_publishers_replays_exact_scenario_json() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-sim-duplicate-publisher-replay-{}",
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        fs::create_dir_all(&root)?;
        let scenario_path = root.join("scenario.json");
        let mut scenario = ec_core::sim::DuplicatePublisherScenario::new(
            ec_core::sim::SimulationSeed::new(19),
            vec!["nuc-a".to_string(), "nuc-b".to_string()],
            "la-kcop",
            WT_LADDER_PRIMARY_RENDITION,
            WT_PUBLISH_PRIMARY_VIDEO_TRACK,
            "x264-hd3-v1",
            0,
            48,
        );
        scenario.segment_step_ms = 40;
        scenario.base_network_delay_ms = 5;
        scenario.max_jitter_ms = 75;
        scenario.transient_drop_per_million = 275_000;
        scenario.backfill_after_ms = 600;
        scenario.partitions = vec![
            ec_core::sim::SimulationPartition::new("nuc-b", 120, 520, 140),
            ec_core::sim::SimulationPartition::new("nuc-a", 940, 1_260, 90),
        ];
        scenario.publisher_outages = vec![ec_core::sim::SimulationOutage::new(
            "nuc-b", 1_360, 1_520, 220,
        )];
        scenario.encoder_drifts = vec![ec_core::sim::EncoderDriftFault::new(
            "nuc-b",
            17,
            "x264-hd3-drift",
        )];
        fs::write(&scenario_path, serde_json::to_vec_pretty(&scenario)?)?;

        let args = SimDuplicatePublishersArgs::parse_from(vec![
            "sim-duplicate-publishers".to_string(),
            "--scenario-json".to_string(),
            scenario_path.display().to_string(),
            "--allow-failure".to_string(),
        ]);
        let output = sim_duplicate_publishers_output(&args)?;
        let direct_report = ec_core::sim::run_duplicate_publisher_simulation(&scenario);
        let failure = output
            .campaign
            .first_failure
            .as_ref()
            .expect("drift scenario should fail");

        assert!(!output.ok);
        assert_eq!(output.scenario_template, scenario);
        assert_eq!(
            output.campaign.seed_start,
            ec_core::sim::SimulationSeed::new(19)
        );
        assert_eq!(output.campaign.iterations, 1);
        assert_eq!(failure.report, direct_report);
        assert_eq!(failure.report.summary.divergent_sequences, vec![17]);
        assert!(failure.shrunk_failure.is_some());

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn sim_duplicate_publishers_rejects_invalid_partition() {
        let args = SimDuplicatePublishersArgs::parse_from([
            "sim-duplicate-publishers",
            "--partition",
            "nuc-a:100:50:10",
        ]);

        let err = sim_duplicate_publishers_output(&args).expect_err("partition should fail");

        assert!(
            err.to_string().contains("end_ms must be greater"),
            "{err:#}"
        );
    }

    #[test]
    fn sim_duplicate_publishers_rejects_invalid_publisher_outage() {
        let args = SimDuplicatePublishersArgs::parse_from([
            "sim-duplicate-publishers",
            "--publisher-outage",
            "nuc-a:100:50:10",
        ]);

        let err = sim_duplicate_publishers_output(&args).expect_err("outage should fail");

        assert!(
            err.to_string().contains("end_ms must be greater"),
            "{err:#}"
        );
    }

    #[test]
    fn sim_duplicate_publishers_rejects_unknown_sequence_offset_publisher() {
        let args = SimDuplicatePublishersArgs::parse_from([
            "sim-duplicate-publishers",
            "--publisher-sequence-offset",
            "nuc-z:3",
        ]);

        let err = sim_duplicate_publishers_output(&args).expect_err("offset should fail");

        assert!(err.to_string().contains("unknown publisher"), "{err:#}");
    }

    #[test]
    fn wt_archive_defaults_include_real_init_track() {
        assert!(WT_ARCHIVE_DEFAULT_TRACKS.contains(&WT_PUBLISH_INIT_TRACK));
    }

    #[test]
    fn fmp4_init_prefix_scanner_finds_first_moof() -> Result<()> {
        let mut data = Vec::new();
        data.extend_from_slice(&16u32.to_be_bytes());
        data.extend_from_slice(b"ftyp");
        data.extend_from_slice(&[0u8; 8]);
        data.extend_from_slice(&24u32.to_be_bytes());
        data.extend_from_slice(b"moov");
        data.extend_from_slice(&[1u8; 16]);
        let expected = data.len();
        data.extend_from_slice(&8u32.to_be_bytes());
        data.extend_from_slice(b"moof");
        assert_eq!(top_level_moof_offset(&data)?, Some(expected));
        assert_eq!(top_level_moof_offset(&data[..expected])?, None);
        Ok(())
    }

    fn test_mp4_box(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
        let mut data = Vec::with_capacity(8 + payload.len());
        data.extend_from_slice(&(8u32 + payload.len() as u32).to_be_bytes());
        data.extend_from_slice(kind);
        data.extend_from_slice(payload);
        data
    }

    fn test_mp4_full_box(kind: &[u8; 4], version: u8, payload: &[u8]) -> Vec<u8> {
        let mut full_payload = Vec::with_capacity(4 + payload.len());
        full_payload.push(version);
        full_payload.extend_from_slice(&[0, 0, 0]);
        full_payload.extend_from_slice(payload);
        test_mp4_box(kind, &full_payload)
    }

    fn test_mp4_moov_with_track(track_id: u32, timescale: u32) -> Vec<u8> {
        let mut tkhd_payload = Vec::new();
        tkhd_payload.extend_from_slice(&0u32.to_be_bytes());
        tkhd_payload.extend_from_slice(&0u32.to_be_bytes());
        tkhd_payload.extend_from_slice(&track_id.to_be_bytes());
        let tkhd = test_mp4_full_box(b"tkhd", 0, &tkhd_payload);

        let mut mdhd_payload = Vec::new();
        mdhd_payload.extend_from_slice(&0u32.to_be_bytes());
        mdhd_payload.extend_from_slice(&0u32.to_be_bytes());
        mdhd_payload.extend_from_slice(&timescale.to_be_bytes());
        let mdhd = test_mp4_full_box(b"mdhd", 0, &mdhd_payload);
        let mdia = test_mp4_box(b"mdia", &mdhd);

        let mut trak_payload = Vec::new();
        trak_payload.extend_from_slice(&tkhd);
        trak_payload.extend_from_slice(&mdia);
        let trak = test_mp4_box(b"trak", &trak_payload);

        test_mp4_box(b"moov", &trak)
    }

    fn test_mp4_moof_with_tfdt(track_id: u32, base_media_decode_time: u64) -> Vec<u8> {
        let tfhd = test_mp4_full_box(b"tfhd", 0, &track_id.to_be_bytes());
        let tfdt = test_mp4_full_box(b"tfdt", 1, &base_media_decode_time.to_be_bytes());
        let mut traf_payload = Vec::new();
        traf_payload.extend_from_slice(&tfhd);
        traf_payload.extend_from_slice(&tfdt);
        let traf = test_mp4_box(b"traf", &traf_payload);
        test_mp4_box(b"moof", &traf)
    }

    #[test]
    fn split_fmp4_init_and_media_groups_top_level_fragments() -> Result<()> {
        let mut bytes = Vec::new();
        let ftyp = test_mp4_box(b"ftyp", b"brand");
        let moov = test_mp4_box(b"moov", b"init");
        let moof_a = test_mp4_box(b"moof", b"frag-a-header");
        let mdat_a = test_mp4_box(b"mdat", b"frag-a-data");
        let moof_b = test_mp4_box(b"moof", b"frag-b-header");
        let mdat_b = test_mp4_box(b"mdat", b"frag-b-data");
        bytes.extend_from_slice(&ftyp);
        bytes.extend_from_slice(&moov);
        bytes.extend_from_slice(&moof_a);
        bytes.extend_from_slice(&mdat_a);
        bytes.extend_from_slice(&moof_b);
        bytes.extend_from_slice(&mdat_b);

        let split = split_fmp4_init_and_media(&bytes)?;

        assert_eq!(split.init, [ftyp, moov].concat());
        assert_eq!(
            split.media,
            vec![[moof_a, mdat_a].concat(), [moof_b, mdat_b].concat()]
        );
        Ok(())
    }

    #[test]
    fn publisher_archive_writer_records_source_stamped_fmp4_fragments() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-publisher-archive-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let output_dir = root.join("archive-buffer");
        let manifest_dir = output_dir.join("manifests");
        let mut writer = PublisherArchiveWriter::new(
            &output_dir,
            Some(&manifest_dir),
            "https://relay.every.channel/anon",
            "la-kcop",
            WT_PUBLISHER_ORIGIN_TRACK,
            1000,
            "nuc-a".to_string(),
        );

        let mut stream = Vec::new();
        stream.extend(test_mp4_box(b"ftyp", b"init-a"));
        stream.extend(test_mp4_box(b"moov", b"init-b"));
        stream.extend(test_mp4_box(b"moof", b"fragment-a-header"));
        stream.extend(test_mp4_box(b"mdat", b"fragment-a-data"));
        stream.extend(test_mp4_box(b"moof", b"fragment-b-header"));
        stream.extend(test_mp4_box(b"mdat", b"fragment-b-data"));

        writer.observe_bytes(&stream[..17])?;
        writer.observe_bytes(&stream[17..])?;
        writer.finish()?;

        let records = read_archive_records_from_path(&archive_index_path(
            &manifest_dir,
            "la-kcop",
            WT_PUBLISHER_ORIGIN_TRACK,
        ))?;
        assert_eq!(records.len(), 2);
        assert_eq!(records[0].source_node.as_deref(), Some("nuc-a"));
        assert_eq!(
            records[0].source_session.as_deref(),
            Some(writer.source_session.as_str())
        );
        assert_eq!(records[0].broadcast_name, "la-kcop");
        assert_eq!(records[0].track_name, WT_PUBLISHER_ORIGIN_TRACK);
        assert_eq!(records[0].group_sequence, 0);
        assert_eq!(records[1].group_sequence, 1);
        for record in &records {
            assert!(output_dir.join(&record.cas_path).exists());
        }

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn publisher_archive_writer_maps_fmp4_fragments_to_media_time_slots() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-publisher-archive-tfdt-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let output_dir = root.join("archive-buffer");
        let manifest_dir = output_dir.join("manifests");
        let mut writer = PublisherArchiveWriter::new(
            &output_dir,
            Some(&manifest_dir),
            "https://relay.every.channel/anon",
            "la-kcop",
            WT_PUBLISHER_ORIGIN_TRACK,
            1000,
            "nuc-a".to_string(),
        );

        let mut stream = Vec::new();
        stream.extend(test_mp4_box(b"ftyp", b"init-a"));
        stream.extend(test_mp4_moov_with_track(7, 90_000));
        stream.extend(test_mp4_moof_with_tfdt(7, 180_000));
        stream.extend(test_mp4_box(b"mdat", b"fragment-a-data"));
        stream.extend(test_mp4_moof_with_tfdt(7, 270_000));
        stream.extend(test_mp4_box(b"mdat", b"fragment-b-data"));

        writer.observe_bytes(&stream)?;
        writer.finish()?;

        let records = read_archive_records_from_path(&archive_index_path(
            &manifest_dir,
            "la-kcop",
            WT_PUBLISHER_ORIGIN_TRACK,
        ))?;
        assert_eq!(records.len(), 2);
        let first_origin = (records[0].received_unix_ms / 1000).saturating_sub(2);
        for (record, expected_sequence) in records.iter().zip([first_origin + 2, first_origin + 3])
        {
            let timing = record.media_timing.as_ref().expect("media timing");
            assert_eq!(timing.track_id, 7);
            assert_eq!(timing.sequence, expected_sequence);
            assert_eq!(
                timing.decode_time,
                fmp4_decode_time_for_media_sequence(expected_sequence, timing.timescale, 1000)
                    .expect("decode time")
            );
            assert_eq!(
                record.group_sequence,
                PublisherArchiveWriter::group_sequence_for_media_timing_subfragment(timing, 0)
            );
        }

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn publisher_archive_writer_coalesces_subsegment_timed_fragments() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-publisher-archive-fast-tfdt-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let output_dir = root.join("archive-buffer");
        let manifest_dir = output_dir.join("manifests");
        let mut writer = PublisherArchiveWriter::new(
            &output_dir,
            Some(&manifest_dir),
            "https://relay.every.channel/anon",
            "la-kcop",
            WT_PUBLISHER_ORIGIN_TRACK,
            1000,
            "nuc-a".to_string(),
        );

        let mut stream = Vec::new();
        stream.extend(test_mp4_box(b"ftyp", b"init-a"));
        stream.extend(test_mp4_moov_with_track(7, 90_000));
        for decode_time in [0, 30_000, 60_000, 90_000] {
            stream.extend(test_mp4_moof_with_tfdt(7, decode_time));
            stream.extend(test_mp4_box(b"mdat", &decode_time.to_be_bytes()));
        }

        writer.observe_bytes(&stream)?;
        writer.finish()?;

        let records = read_archive_records_from_path(&archive_index_path(
            &manifest_dir,
            "la-kcop",
            WT_PUBLISHER_ORIGIN_TRACK,
        ))?;
        assert_eq!(records.len(), 4);
        let first_origin = records[0].received_unix_ms / 1000;
        let mut subfragment_counts = BTreeMap::<(u32, u64), u64>::new();
        for (record, expected_sequence) in records.iter().zip([
            first_origin,
            first_origin,
            first_origin + 1,
            first_origin + 1,
        ]) {
            let timing = record.media_timing.as_ref().expect("media timing");
            assert_eq!(timing.track_id, 7);
            assert_eq!(timing.sequence, expected_sequence);
            let subfragment_index = subfragment_counts
                .entry((timing.track_id, timing.sequence))
                .or_default();
            let expected_group_sequence =
                PublisherArchiveWriter::group_sequence_for_media_timing_subfragment(
                    timing,
                    *subfragment_index,
                );
            *subfragment_index = subfragment_index.saturating_add(1);
            assert_eq!(record.group_sequence, expected_group_sequence);
        }

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn rewrite_fmp4_fragment_decode_time_replaces_tfdt() -> Result<()> {
        let mut fragment = Vec::new();
        fragment.extend(test_mp4_moof_with_tfdt(7, 180_000));
        fragment.extend(test_mp4_box(b"mdat", b"fragment-a-data"));

        assert!(rewrite_fmp4_fragment_decode_time(
            &mut fragment,
            53_407_213_650_000
        )?);
        let (_, decode_time) = fmp4_fragment_decode_time(&fragment)?.expect("decode time");

        assert_eq!(decode_time, 53_407_213_650_000);
        Ok(())
    }

    #[test]
    fn publisher_archive_writer_aligns_timed_fragments_by_media_identity() {
        let publisher_a_timing = ArchiveRecordMediaTiming {
            track_id: 1,
            timescale: 30_000,
            decode_time: 53_407_213_650_000,
            sequence: 1_780_240_988,
        };
        let publisher_b_same_timing = ArchiveRecordMediaTiming {
            track_id: 1,
            timescale: 30_000,
            decode_time: 53_407_213_650_000,
            sequence: 1_780_240_988,
        };
        let publisher_b_late_timing = ArchiveRecordMediaTiming {
            track_id: 1,
            timescale: 30_000,
            decode_time: 53_407_216_653_000,
            sequence: 1_780_240_989,
        };
        let publisher_b_audio_timing = ArchiveRecordMediaTiming {
            track_id: 2,
            timescale: 48_000,
            decode_time: 85_451_541_840_000,
            sequence: 1_780_240_988,
        };
        assert_eq!(
            PublisherArchiveWriter::group_sequence_for_media_timing_subfragment(
                &publisher_a_timing,
                0
            ),
            PublisherArchiveWriter::group_sequence_for_media_timing_subfragment(
                &publisher_b_same_timing,
                0
            )
        );
        assert_ne!(
            PublisherArchiveWriter::group_sequence_for_media_timing_subfragment(
                &publisher_a_timing,
                0
            ),
            PublisherArchiveWriter::group_sequence_for_media_timing_subfragment(
                &publisher_b_late_timing,
                0
            )
        );
        assert_ne!(
            PublisherArchiveWriter::group_sequence_for_media_timing_subfragment(
                &publisher_a_timing,
                0
            ),
            PublisherArchiveWriter::group_sequence_for_media_timing_subfragment(
                &publisher_b_audio_timing,
                0
            )
        );
    }

    #[test]
    fn wt_publish_publisher_archive_spawns_source_proof_worker() -> Result<()> {
        let args = WtPublishArgs::parse_from([
            "wt-publish",
            "--url",
            "https://lax.relay.every.channel/anon",
            "--name",
            "la-kcop",
            "--input",
            "http://hdhr/auto/v13.1",
            "--publisher-archive-output-dir",
            "/tmp/ec-publisher-archive",
            "--publisher-archive-manifest-dir",
            "/tmp/ec-publisher-archive/manifests",
            "--publisher-archive-source-node",
            "nuc-a",
            "--publisher-archive-segment-duration-ms",
            "1001",
        ]);

        let child_args = publisher_proof_archive_source_child_args(&args, &args.input)?
            .expect("publisher archive child args");
        let child_args = child_args
            .into_iter()
            .map(|arg| arg.to_string_lossy().into_owned())
            .collect::<Vec<_>>();

        assert_eq!(
            child_args.first().map(String::as_str),
            Some("publisher-proof-archive-source")
        );
        assert!(child_args
            .windows(2)
            .any(|window| window == ["--input", "http://hdhr/auto/v13.1"]));
        assert!(child_args
            .windows(2)
            .any(|window| window == ["--track", WT_PUBLISHER_ORIGIN_TRACK]));
        assert!(child_args
            .windows(2)
            .any(|window| window == ["--source-node", "nuc-a"]));
        assert!(child_args.iter().any(|arg| arg == "--transcode=true"));
        Ok(())
    }

    #[test]
    fn publisher_proof_archive_source_converges_two_publishers() -> Result<()> {
        if !test_command_available("ffmpeg") {
            eprintln!(
                "skipping publisher source proof archive convergence test: ffmpeg unavailable"
            );
            return Ok(());
        }

        let root = std::env::temp_dir().join(format!(
            "ec-source-proof-archive-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        fs::create_dir_all(&root)?;
        let input_ts = root.join("source.ts");
        write_short_deterministic_ts(&input_ts)?;

        let archive_a = root.join("nuc-a");
        let archive_b = root.join("nuc-b");
        let report_a =
            publisher_proof_archive_source_report(&PublisherProofArchiveSourceArgs::parse_from([
                "publisher-proof-archive-source",
                "--input",
                input_ts.to_str().unwrap_or("/tmp/source.ts"),
                "--output-dir",
                archive_a.to_str().unwrap_or("/tmp/nuc-a"),
                "--relay-url",
                "https://lax.relay.every.channel/anon",
                "--name",
                "la-kcop",
                "--source-node",
                "nuc-a",
                "--chunk-ms",
                "1001",
                "--max-chunks",
                "1",
            ]))?;
        let report_b =
            publisher_proof_archive_source_report(&PublisherProofArchiveSourceArgs::parse_from([
                "publisher-proof-archive-source",
                "--input",
                input_ts.to_str().unwrap_or("/tmp/source.ts"),
                "--output-dir",
                archive_b.to_str().unwrap_or("/tmp/nuc-b"),
                "--relay-url",
                "https://lax.relay.every.channel/anon",
                "--name",
                "la-kcop",
                "--source-node",
                "nuc-b",
                "--chunk-ms",
                "1001",
                "--max-chunks",
                "1",
            ]))?;

        assert_eq!(report_a.archived_windows, 1);
        assert_eq!(report_b.archived_windows, 1);
        assert!(report_a.archived_fragments > 0);
        assert_eq!(report_a.archived_fragments, report_b.archived_fragments);

        let convergence = archive_convergence_report(&ArchiveConvergenceArgs {
            source: vec![
                format!("nuc-a={}", archive_a.join("manifests").display()),
                format!("nuc-b={}", archive_b.join("manifests").display()),
            ],
            broadcast: "la-kcop".to_string(),
            track: WT_PUBLISHER_ORIGIN_TRACK.to_string(),
            stream_id: None,
            rendition: None,
            start_sequence: None,
            end_sequence: None,
            pretty: false,
            prometheus: false,
            metrics_node: None,
            metrics_role: "duplicate-proof".to_string(),
            require_ok: false,
        })?;

        assert!(convergence.ok, "{:#?}", convergence.reasons);
        assert_eq!(convergence.record_source_count, 2);
        assert_eq!(convergence.summary.divergent_sequences.len(), 0);
        assert_eq!(
            convergence.summary.source_local_divergent_sequences.len(),
            0
        );
        assert_eq!(
            convergence.summary.matching_duplicate_sequences.len() as u64,
            convergence.summary.expected_sequences
        );

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn publisher_proof_archive_source_skips_bad_ts_window() -> Result<()> {
        if !test_command_available("ffmpeg") {
            eprintln!("skipping bad source proof window test: ffmpeg unavailable");
            return Ok(());
        }

        let root = std::env::temp_dir().join(format!(
            "ec-source-proof-bad-window-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        fs::create_dir_all(&root)?;
        let input_ts = root.join("bad-window.ts");
        fs::write(&input_ts, b"not a valid mpeg transport stream")?;

        let args = PublisherProofArchiveSourceArgs::parse_from([
            "publisher-proof-archive-source",
            "--input",
            input_ts.to_str().unwrap_or("/tmp/bad-window.ts"),
            "--output-dir",
            root.join("archive").to_str().unwrap_or("/tmp/archive"),
            "--relay-url",
            "https://lax.relay.every.channel/anon",
            "--name",
            "la-kcop",
            "--source-node",
            "nuc-a",
            "--chunk-ms",
            "1001",
        ]);
        let config = PublisherProofArchiveSourceConfig::from_args(&args)?;
        fs::create_dir_all(&config.source_window_dir)?;
        fs::create_dir_all(&config.proof_dir)?;
        fs::create_dir_all(&config.cas_root)?;
        let chunk_path = config.source_window_dir.join("bad-window.ts");
        fs::copy(&input_ts, &chunk_path)?;

        let mut stats = PublisherProofArchiveSourceStats::default();
        archive_publisher_source_proof_chunk(
            &config,
            &mut stats,
            TsChunk {
                index: 1,
                path: chunk_path,
                timing: ec_chopper::ChunkTiming {
                    chunk_index: 1,
                    chunk_start_27mhz: None,
                    chunk_duration_27mhz: 27_000_000,
                    utc_start_unix: None,
                    sync_status: "test-invalid".to_string(),
                },
            },
        )?;

        assert_eq!(stats.archived_windows, 0);
        assert_eq!(stats.archived_fragments, 0);
        assert!(!config.manifest_path.exists());

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn publisher_source_proof_window_sequence_uses_synced_utc_or_wallclock_start() {
        let synced = TsChunk {
            index: 390_545_408,
            path: PathBuf::from("unused.ts"),
            timing: ec_chopper::ChunkTiming {
                chunk_index: 390_545_408,
                chunk_start_27mhz: Some(123),
                chunk_duration_27mhz: 54_000_000,
                utc_start_unix: Some(10),
                sync_status: "synced".to_string(),
            },
        };
        assert_eq!(
            publisher_source_proof_window_sequence(&synced, 12_345, 2_000),
            390_545_408
        );

        let unsynced = TsChunk {
            index: 390_545_408,
            path: PathBuf::from("unused.ts"),
            timing: ec_chopper::ChunkTiming {
                chunk_index: 390_545_408,
                chunk_start_27mhz: Some(123),
                chunk_duration_27mhz: 54_000_000,
                utc_start_unix: Some(10),
                sync_status: "unsynced".to_string(),
            },
        };
        assert_eq!(
            publisher_source_proof_window_sequence(&unsynced, 12_345, 2_000),
            5
        );
    }

    #[test]
    fn publisher_source_proof_stats_keeps_subfragment_slots_across_chunks() {
        let mut stats = PublisherProofArchiveSourceStats::default();
        let timing = ArchiveRecordMediaTiming {
            track_id: 1,
            timescale: 90_000,
            decode_time: 178_095_128_8116,
            sequence: 1_779_171_116,
        };

        let first = stats.next_source_proof_group_sequence(&timing);
        let second = stats.next_source_proof_group_sequence(&timing);

        assert_ne!(first, second);
        assert_eq!(second, first + 1);
        assert_eq!(stats.archived_fragments, 2);
        assert_eq!(stats.first_group_sequence, Some(first));
        assert_eq!(stats.latest_group_sequence, Some(second));
    }

    #[test]
    fn publisher_source_proof_fragment_timing_rewrites_to_window_clock() -> Result<()> {
        let mut fragment = Vec::new();
        fragment.extend(test_mp4_moof_with_tfdt(7, 90_000));
        fragment.extend(test_mp4_box(b"mdat", b"fragment"));
        let mut track_timescales = BTreeMap::new();
        track_timescales.insert(7, 90_000);

        let timing =
            publisher_source_proof_fragment_timing(&mut fragment, &track_timescales, 1_000, 1_000)?
                .expect("timed fragment");

        assert_eq!(timing.track_id, 7);
        assert_eq!(timing.timescale, 90_000);
        assert_eq!(timing.sequence, 1_001);
        assert_eq!(
            timing.decode_time,
            fmp4_decode_time_for_media_sequence(1_001, 90_000, 1_000).expect("decode time")
        );
        let (_, rewritten_decode_time) =
            fmp4_fragment_decode_time(&fragment)?.expect("rewritten tfdt");
        assert_eq!(rewritten_decode_time, timing.decode_time);
        Ok(())
    }

    #[test]
    fn archive_track_limits_tail_or_seek_window() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-limit-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let manifest_root = root.join("manifests");
        let broadcast_dir = manifest_root.join("limited");
        fs::create_dir_all(&broadcast_dir)?;

        let mut lines = String::new();
        for sequence in 0..10u64 {
            let hash = format!("{sequence:064x}");
            let record = ArchiveIndexRecord {
                received_unix_ms: 1_000 + sequence * 1_000,
                relay_url: "https://relay.every.channel/anon".to_string(),
                source_node: Some("nuc-a".to_string()),
                source_session: Some("nuc-a-test".to_string()),
                media_timing: None,
                broadcast_name: "limited".to_string(),
                track_name: WT_PUBLISH_PRIMARY_VIDEO_TRACK.to_string(),
                group_sequence: sequence,
                frame_count: 1,
                size_bytes: 4,
                blake3: hash.clone(),
                cas_path: format!("objects/blake3/{}/{hash}.bin", &hash[0..2]),
            };
            lines.push_str(&serde_json::to_string(&record)?);
            lines.push('\n');
        }
        fs::write(broadcast_dir.join("0.m4s.jsonl"), lines)?;

        let tail = parse_archive_track(
            &manifest_root,
            "limited",
            WT_PUBLISH_PRIMARY_VIDEO_TRACK,
            None,
            Some(3),
        )?;
        assert_eq!(
            tail.iter()
                .map(|record| record.group_sequence)
                .collect::<Vec<_>>(),
            vec![7, 8, 9]
        );

        let seek_window = parse_archive_track(
            &manifest_root,
            "limited",
            WT_PUBLISH_PRIMARY_VIDEO_TRACK,
            Some(4_000),
            Some(2),
        )?;
        assert_eq!(
            seek_window
                .iter()
                .map(|record| record.group_sequence)
                .collect::<Vec<_>>(),
            vec![3, 4]
        );

        let timeline = summarize_archive_track(
            &manifest_root,
            "limited",
            WT_PUBLISH_PRIMARY_VIDEO_TRACK,
            None,
            None,
        )?;
        assert_eq!(timeline.start_unix_ms, Some(1_000));
        assert_eq!(timeline.end_unix_ms, Some(10_000));
        assert_eq!(timeline.segments, 10);

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_ladder_status_groups_directory_and_track_renditions() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-ladder-status-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let manifest_root = root.join("manifests");

        let write_record = |broadcast_name: &str,
                            track_name: &str,
                            sequence: u64,
                            received_unix_ms: u64,
                            frame_count: u64,
                            size_bytes: usize|
         -> Result<()> {
            let hash = format!("{sequence:064x}");
            let record = ArchiveIndexRecord {
                received_unix_ms,
                relay_url: "https://relay.every.channel/anon".to_string(),
                source_node: Some("nuc-a".to_string()),
                source_session: Some("nuc-a-test".to_string()),
                media_timing: None,
                broadcast_name: broadcast_name.to_string(),
                track_name: track_name.to_string(),
                group_sequence: sequence,
                frame_count,
                size_bytes,
                blake3: hash.clone(),
                cas_path: format!("objects/blake3/{}/{hash}.bin", &hash[0..2]),
            };
            append_archive_index_record(
                &archive_index_path(&manifest_root, broadcast_name, track_name),
                &record,
            )
        };

        write_record(
            "west-cbs",
            WT_PUBLISH_PRIMARY_VIDEO_TRACK,
            10,
            1_000,
            30,
            100,
        )?;
        write_record(
            "west-cbs",
            WT_PUBLISH_PRIMARY_VIDEO_TRACK,
            11,
            2_000,
            30,
            200,
        )?;
        write_record(
            "west-cbs-480p",
            WT_PUBLISH_PRIMARY_VIDEO_TRACK,
            12,
            1_500,
            30,
            120,
        )?;
        write_record("west-cbs", "0.m4s/1080p", 13, 2_500, 30, 800)?;
        write_record("west-cbs", "init/1080p", 14, 900, 1, 64)?;

        let status = archive_ladder_status(&manifest_root, "west-cbs", 3_601_000, 3_600)?;
        assert_eq!(status.base_broadcast_name, "west-cbs");
        assert_eq!(status.retention_seconds, 3_600);
        assert_eq!(status.retention_window_start_unix_ms, Some(1_000));
        assert_eq!(
            status
                .renditions
                .iter()
                .map(|item| item.rendition_id.as_str())
                .collect::<Vec<_>>(),
            vec!["1080p", "480p", "720p"]
        );

        let track_1080 = status
            .tracks
            .iter()
            .find(|track| track.track_name == "0.m4s/1080p")
            .expect("1080p video track");
        assert_eq!(track_1080.media_kind, "video");
        assert_eq!(track_1080.rendition_id, "1080p");
        assert_eq!(track_1080.total_bytes, Some(800));
        assert_eq!(track_1080.total_frames, Some(30));

        let summary_720 = status
            .renditions
            .iter()
            .find(|item| item.rendition_id == "720p")
            .expect("720p rendition summary");
        assert_eq!(summary_720.segments, 2);
        assert_eq!(summary_720.total_bytes, Some(300));
        assert_eq!(summary_720.total_frames, Some(60));

        let track_480 = status
            .tracks
            .iter()
            .find(|track| track.rendition_id == "480p")
            .expect("480p track");
        assert_eq!(track_480.broadcast_name, "west-cbs-480p");

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_hls_master_playlist_lists_ladder_variant_tracks() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-ladder-master-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let manifest_root = root.join("manifests");

        let write_record = |track_name: &str,
                            sequence: u64,
                            received_unix_ms: u64,
                            frame_count: u64,
                            size_bytes: usize|
         -> Result<String> {
            let hash = format!("{sequence:064x}");
            let record = ArchiveIndexRecord {
                received_unix_ms,
                relay_url: "https://relay.every.channel/anon".to_string(),
                source_node: Some("nuc-a".to_string()),
                source_session: Some("nuc-a-test".to_string()),
                media_timing: None,
                broadcast_name: "west-cbs".to_string(),
                track_name: track_name.to_string(),
                group_sequence: sequence,
                frame_count,
                size_bytes,
                blake3: hash.clone(),
                cas_path: format!("objects/blake3/{}/{hash}.bin", &hash[0..2]),
            };
            append_archive_index_record(
                &archive_index_path(&manifest_root, "west-cbs", track_name),
                &record,
            )?;
            Ok(hash)
        };

        let init_480 = write_record("init.mp4/480p", 1, 900, 1, 64)?;
        write_record("0.m4s/480p", 10, 1_000, 30, 100)?;
        write_record("0.m4s/480p", 11, 2_000, 30, 100)?;
        write_record("init.mp4/720p", 2, 900, 1, 64)?;
        write_record("0.m4s/720p", 12, 1_000, 30, 200)?;
        write_record("init.mp4/1080p", 3, 900, 1, 64)?;
        write_record("0.m4s/1080p", 13, 1_000, 30, 400)?;

        let req_url =
            Url::parse("http://localhost/archive/west-cbs/master.m3u8?from_ms=1000&limit=12")?;
        let playlist = archive_hls_master_playlist(
            &manifest_root,
            "west-cbs",
            &req_url,
            3_000,
            WT_ARCHIVE_DEFAULT_RETENTION_SECONDS,
        )?
        .expect("ladder playlist");

        assert!(playlist.contains("#EXT-X-INDEPENDENT-SEGMENTS"));
        assert!(playlist.contains("BANDWIDTH=1400000"));
        assert!(playlist.contains("RESOLUTION=854x480"));
        assert!(playlist.contains("BANDWIDTH=3200000"));
        assert!(playlist.contains("RESOLUTION=1280x720"));
        assert!(playlist.contains("BANDWIDTH=6300000"));
        assert!(playlist.contains("RESOLUTION=1920x1080"));
        assert!(playlist.contains(
            "/archive/west-cbs/track.m3u8?broadcast=west-cbs&track=0.m4s%2F480p&from_ms=1000&limit=12"
        ));
        assert!(playlist.contains(
            "/archive/west-cbs/track.m3u8?broadcast=west-cbs&track=0.m4s%2F720p&from_ms=1000&limit=12"
        ));
        assert!(playlist.contains(
            "/archive/west-cbs/track.m3u8?broadcast=west-cbs&track=0.m4s%2F1080p&from_ms=1000&limit=12"
        ));
        assert_eq!(
            latest_init_hash(&manifest_root, "west-cbs", "0.m4s/480p")?,
            Some(init_480)
        );

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[test]
    fn archive_hls_master_playlist_falls_back_for_legacy_single_video() -> Result<()> {
        let root = std::env::temp_dir().join(format!(
            "ec-archive-legacy-master-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let manifest_root = root.join("manifests");

        for (track_name, sequence) in [
            (WT_PUBLISH_INIT_TRACK, 1),
            (WT_PUBLISH_PRIMARY_VIDEO_TRACK, 2),
        ] {
            let hash = format!("{sequence:064x}");
            let record = ArchiveIndexRecord {
                received_unix_ms: sequence * 1000,
                relay_url: "https://relay.every.channel/anon".to_string(),
                source_node: Some("nuc-a".to_string()),
                source_session: Some("nuc-a-test".to_string()),
                media_timing: None,
                broadcast_name: "legacy".to_string(),
                track_name: track_name.to_string(),
                group_sequence: sequence,
                frame_count: 1,
                size_bytes: 64,
                blake3: hash.clone(),
                cas_path: format!("objects/blake3/{}/{hash}.bin", &hash[0..2]),
            };
            append_archive_index_record(
                &archive_index_path(&manifest_root, "legacy", track_name),
                &record,
            )?;
        }

        let req_url = Url::parse("http://localhost/archive/legacy/master.m3u8")?;
        assert!(archive_hls_master_playlist(
            &manifest_root,
            "legacy",
            &req_url,
            3_000,
            WT_ARCHIVE_DEFAULT_RETENTION_SECONDS,
        )?
        .is_none());

        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[tokio::test]
    async fn read_archive_object_fetches_validates_and_caches_origin_object() -> Result<()> {
        let payload = b"origin-cas-object".to_vec();
        let digest = blake3::hash(&payload).to_hex().to_string();
        let root = std::env::temp_dir().join(format!(
            "ec-archive-origin-test-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        ));
        let cas_root = root.join("objects").join("blake3");
        let cache_access_root = root.join("cache-access").join("blake3");

        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
        let origin_addr = listener.local_addr()?;
        let expected_path = format!(
            "/archive-root/objects/blake3/{}/{}.bin",
            &digest[0..2],
            digest
        );
        let response_payload = payload.clone();
        let server = tokio::spawn(async move {
            let (mut socket, _) = listener.accept().await.unwrap();
            let mut req = vec![0u8; 1024];
            let read = socket.read(&mut req).await.unwrap();
            let request = String::from_utf8_lossy(&req[..read]);
            assert!(request.starts_with(&format!("GET {expected_path} ")));
            let head = format!(
                "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
                response_payload.len()
            );
            socket.write_all(head.as_bytes()).await.unwrap();
            socket.write_all(&response_payload).await.unwrap();
        });

        let state = ArchiveReplayState {
            cas_root: cas_root.clone(),
            manifest_root: root.join("manifests"),
            archive_origin_url: Some(Url::parse(&format!("http://{origin_addr}/archive-root"))?),
            archive_cas_origin_url: None,
            archive_origin_max_bytes: 1024,
            archive_retention_seconds: WT_ARCHIVE_DEFAULT_RETENTION_SECONDS,
            cache_access_root: cache_access_root.clone(),
            http_client: reqwest::Client::new(),
        };

        let fetched = read_archive_object(&state, &digest).await?.unwrap();
        assert_eq!(fetched, payload);
        assert_eq!(fs::read(cas_path_for_hash(&cas_root, &digest)?)?, payload);
        assert!(cache_access_root
            .join(&digest[0..2])
            .join(format!("{digest}.access"))
            .exists());

        server.await?;
        let _ = fs::remove_dir_all(root);
        Ok(())
    }

    #[derive(Clone)]
    struct DummySource {
        source_id: ec_core::SourceId,
        broadcast_id: Option<BroadcastId>,
    }

    impl StreamSource for DummySource {
        fn open_stream(&self) -> Result<Box<dyn Read + Send>> {
            Ok(Box::new(Cursor::new(Vec::<u8>::new())))
        }

        fn source_id(&self) -> ec_core::SourceId {
            self.source_id.clone()
        }

        fn broadcast_id(&self) -> Result<Option<BroadcastId>> {
            Ok(self.broadcast_id.clone())
        }
    }

    #[test]
    fn default_stream_id_for_source_uses_broadcast_identity_when_present() {
        let source = DummySource {
            source_id: ec_core::SourceId {
                kind: "hdhr".to_string(),
                device_id: Some("ABCDEF01".to_string()),
                channel: Some("2.1".to_string()),
            },
            broadcast_id: Some(BroadcastId {
                standard: "atsc".to_string(),
                transport_stream_id: None,
                program_number: Some(3),
                virtual_channel: Some("2.1".to_string()),
                callsign: Some("KCBS".to_string()),
                region: None,
                frequency: None,
            }),
        };

        let stream_id = default_stream_id_for_source(&source, "chunk-2000ms").unwrap();
        assert_eq!(
            stream_id,
            "ec/stream/v1/broadcast/atsc/program-3/channel-2_1/callsign-kcbs/profile-chunk-2000ms"
        );
    }

    #[test]
    fn default_stream_id_for_source_falls_back_to_source_identity() {
        let source = DummySource {
            source_id: ec_core::SourceId {
                kind: "ts".to_string(),
                device_id: None,
                channel: Some("capture.ts".to_string()),
            },
            broadcast_id: None,
        };

        let stream_id = default_stream_id_for_source(&source, "chunk-2000ms").unwrap();
        assert_eq!(
            stream_id,
            "ec/stream/v1/source/ts/channel-capture_ts/profile-chunk-2000ms"
        );
    }
}

async fn moq_publish(args: MoqPublishArgs) -> Result<()> {
    fs::create_dir_all(&args.chunk_dir)
        .with_context(|| format!("failed to create {}", args.chunk_dir.display()))?;

    let observation_sink = ObservationSink::from_options(ObservationSinkOptions {
        rpc_url: args.observation_rpc_url.clone(),
        ledger: args.observation_ledger.clone(),
        private_key: args.observation_private_key.clone(),
        private_key_file: args.observation_private_key_file.clone(),
        parent_hash: args.observation_parent_hash.clone(),
        timeout_ms: args.observation_timeout_ms,
    })?;

    let deterministic = deterministic_enabled(args.deterministic);
    let (source, _needs_transcode): (Box<dyn StreamSource + Send>, bool) = match args.source {
        IngestSource::Hls { url, mut mode } => {
            if deterministic {
                mode = HlsMode::Transcode;
            }
            (Box::new(HlsSource { url, mode }), false)
        }
        IngestSource::Hdhr {
            host,
            device_id,
            channel,
            name,
            prefer_mdns,
        } => (
            Box::new(HdhrSource {
                host,
                device_id,
                channel,
                name,
                prefer_mdns,
            }),
            deterministic,
        ),
        IngestSource::LinuxDvb {
            adapter,
            dvr,
            tune_cmd,
            tune_wait_ms,
        } => (
            Box::new(LinuxDvbSource {
                adapter,
                dvr,
                tune_cmd,
                tune_wait_ms,
            }),
            deterministic,
        ),
        IngestSource::Ts { input } => (Box::new(TsSource { input }), deterministic),
    };

    let source_id = source.source_id();

    let stream_id = match args.stream_id {
        Some(stream_id) => stream_id,
        None => {
            default_stream_id_for_source(source.as_ref(), format!("chunk-{}ms", args.chunk_ms))?
        }
    };

    let broadcast_name = args.broadcast_name.unwrap_or_else(|| stream_id.clone());
    let track_name = args.track_name.clone();

    let secret = parse_iroh_secret(args.iroh_secret)?;
    let discovery = parse_discovery(args.discovery.as_deref())?;
    let node = MoqNode::bind_with_discovery(secret, discovery).await?;

    // Wait briefly for direct addresses so "remote" can connect without needing discovery.
    let mut endpoint_addr = node.endpoint_addr();
    if endpoint_addr.addrs.is_empty() {
        let mut watcher = node.endpoint().watch_addr();
        let start = Instant::now();
        while endpoint_addr.addrs.is_empty() && start.elapsed() < Duration::from_secs(3) {
            tokio::time::sleep(Duration::from_millis(200)).await;
            endpoint_addr = watcher.get();
        }
    }

    println!("moq endpoint id: {}", node.endpoint().id());
    if let Ok(addr_json) = serde_json::to_string(&endpoint_addr) {
        println!("moq endpoint addr: {}", addr_json);
    }
    println!("moq broadcast: {}", broadcast_name);
    println!("moq track: {}", track_name);

    let publish_chunks = args.publish_chunks;
    let ladder = args.cmaf_ladder;
    let ladder_variants = ladder.map(cmaf_ladder_variants);

    let mut object_tracks = Vec::new();
    if let Some(variants) = ladder_variants.as_ref() {
        if !publish_chunks {
            return Err(anyhow!("--cmaf-ladder requires --publish-chunks true"));
        }
        for variant in variants {
            object_tracks.push(format!("{}/{}", track_name, variant.id));
            object_tracks.push(format!("{}/{}", args.init_track, variant.id));
        }
    } else {
        object_tracks.push(track_name.clone());
        if publish_chunks {
            object_tracks.push(args.init_track.clone());
        }
    }
    let mut manifest_tracks = Vec::new();
    if args.publish_manifests {
        manifest_tracks.push(args.manifest_track.clone());
    }
    let mut publish_set = node
        .publish_track_set(&broadcast_name, object_tracks, manifest_tracks)
        .await?;

    if let Some(ms) = args.startup_delay_ms {
        tokio::time::sleep(Duration::from_millis(ms)).await;
    }

    let network_secret = parse_network_secret(args.network_secret)?;

    let track = TrackName {
        namespace: "every.channel".to_string(),
        name: stream_id.clone(),
    };
    let stream_id_value = StreamId(track.name.clone());
    let source_kind = source_id.kind.clone();
    let encoder_profile_id = "deterministic-h264-aac".to_string();

    if let Some(variants) = ladder_variants {
        if args.epoch_chunks != 1 {
            return Err(anyhow!("--cmaf-ladder currently requires --epoch-chunks 1"));
        }
        if !args.publish_manifests {
            return Err(anyhow!(
                "--cmaf-ladder currently requires --publish-manifests"
            ));
        }

        #[derive(Debug)]
        enum PendingPublish {
            Object {
                track: String,
                group: u64,
                object: ObjectPayload,
            },
            Manifest {
                track: String,
                sequence: u64,
                manifest: Manifest,
            },
        }

        let (tx, mut rx) = mpsc::channel::<PendingPublish>(16);
        let chunk_ms = args.chunk_ms;
        let max_chunks = args.max_chunks.unwrap_or(usize::MAX);
        let out_dir = args.chunk_dir.join("cmaf-ladder");
        let init_track_prefix = args.init_track.clone();
        let chunk_track_prefix = track_name.clone();
        let manifest_track = args.manifest_track.clone();
        let publish_manifests = args.publish_manifests;
        let source_kind = source_id.kind.clone();
        let base_stream_id = stream_id.clone();
        let network_secret_bytes = network_secret.clone();
        let startup_delay_ms = args.startup_delay_ms;

        let chunk_task = tokio::task::spawn_blocking(move || -> Result<()> {
            let _ = fs::remove_dir_all(&out_dir);
            fs::create_dir_all(&out_dir)
                .with_context(|| format!("failed to create {}", out_dir.display()))?;
            for variant in &variants {
                fs::create_dir_all(out_dir.join(&variant.id))?;
            }

            let mut cmd = Command::new("ffmpeg");
            cmd.current_dir(&out_dir);
            cmd.arg("-hide_banner")
                .arg("-loglevel")
                .arg("error")
                .arg("-nostdin")
                .arg("-y")
                .arg("-i")
                .arg("pipe:0")
                // Reduce opportunities for non-deterministic scheduling in filters/decoders.
                .arg("-filter_threads")
                .arg("1")
                .arg("-filter_complex_threads")
                .arg("1")
                .arg("-threads")
                .arg("1")
                .arg("-map")
                .arg("0:v:0")
                .arg("-map")
                .arg("0:a:0?")
                .arg("-sn")
                .arg("-dn")
                .arg("-map_metadata")
                .arg("-1");

            // Build a split+scale filter graph for all variants.
            // NOTE: We keep it simple: split video N ways, then scale each output.
            let mut filter = String::new();
            filter.push_str(&format!("[0:v]split={}", variants.len()));
            for (i, _) in variants.iter().enumerate() {
                filter.push_str(&format!("[v{i}]"));
            }
            filter.push(';');
            for (i, variant) in variants.iter().enumerate() {
                // Scale flags influence quality but should be deterministic.
                filter.push_str(&format!(
                    "[v{i}]scale=w={}:h={}:flags=bicubic[v{i}o];",
                    variant.width, variant.height
                ));
            }
            cmd.arg("-filter_complex").arg(filter);

            let seg_time = format!("{:.3}", chunk_ms as f64 / 1000.0);
            for (i, variant) in variants.iter().enumerate() {
                let v_bitrate = format!("{}k", variant.video_bitrate_kbps);
                let bufsize = format!("{}k", variant.video_bitrate_kbps.saturating_mul(2));
                let out_variant_dir = out_dir.join(&variant.id);
                let seg_template = out_variant_dir.join("segment_%06d.m4s");
                let seg_template = seg_template
                    .to_str()
                    .ok_or_else(|| anyhow!("invalid segment template path"))?
                    .to_string();

                cmd.arg("-map")
                    .arg(format!("[v{i}o]"))
                    .arg("-map")
                    .arg("0:a:0?")
                    .arg("-c:v")
                    .arg("libx264")
                    // Force keyframes aligned to segment boundaries (chunk_ms).
                    .arg("-force_key_frames")
                    .arg(format!(
                        "expr:gte(t,n_forced*{:.3})",
                        chunk_ms as f64 / 1000.0
                    ))
                    .arg("-b:v")
                    .arg(&v_bitrate)
                    .arg("-minrate")
                    .arg(&v_bitrate)
                    .arg("-maxrate")
                    .arg(&v_bitrate)
                    .arg("-bufsize")
                    .arg(&bufsize)
                    .arg("-c:a")
                    .arg("aac")
                    .arg("-profile:a")
                    .arg("aac_low")
                    .arg("-b:a")
                    .arg("128k")
                    .arg("-ac")
                    .arg("2")
                    .arg("-ar")
                    .arg("48000")
                    .arg("-af")
                    .arg(ec_chopper::LIVE_AUDIO_RESAMPLE_FILTER)
                    .arg("-max_muxing_queue_size")
                    .arg("2048")
                    .arg("-pix_fmt")
                    .arg("yuv420p")
                    .arg("-g")
                    .arg("60")
                    .arg("-keyint_min")
                    .arg("60")
                    .arg("-sc_threshold")
                    .arg("0")
                    .arg("-bf")
                    .arg("0")
                    .arg("-threads")
                    .arg("1")
                    .arg("-fflags")
                    .arg("+bitexact")
                    .arg("-flags:v")
                    .arg("+bitexact")
                    .arg("-flags:a")
                    .arg("+bitexact")
                    .arg("-f")
                    .arg("hls")
                    .arg("-hls_time")
                    .arg(&seg_time)
                    .arg("-hls_list_size")
                    .arg("0")
                    .arg("-hls_segment_type")
                    .arg("fmp4")
                    .arg("-hls_flags")
                    .arg("independent_segments")
                    .arg("-hls_fmp4_init_filename")
                    .arg("init.mp4")
                    .arg("-hls_segment_filename")
                    .arg(seg_template)
                    .arg(out_variant_dir.join("index.m3u8"));
            }

            cmd.stdin(Stdio::piped())
                .stdout(Stdio::null())
                .stderr(Stdio::inherit());

            let mut child = cmd.spawn().with_context(|| "failed to spawn ffmpeg")?;
            let mut stdin = child
                .stdin
                .take()
                .ok_or_else(|| anyhow!("ffmpeg stdin unavailable"))?;
            let mut reader = source.open_stream()?;
            let writer = std::thread::spawn(move || -> Result<()> {
                std::io::copy(&mut reader, &mut stdin)?;
                Ok(())
            });

            // Optional startup delay to allow subscribers to connect.
            if let Some(ms) = startup_delay_ms {
                std::thread::sleep(Duration::from_millis(ms));
            }

            // Publish init per variant as soon as they exist.
            for variant in &variants {
                let init_path = out_dir.join(&variant.id).join("init.mp4");
                wait_for_stable_file(&init_path, Duration::from_secs(20))?;
                let data = fs::read(&init_path)?;
                let hash = blake3::hash(&data).to_hex().to_string();
                let key_id = format!(
                    "{}/init",
                    derive_variant_stream_id(&base_stream_id, &variant.id)
                );
                let object = build_object(
                    TsChunk {
                        index: 0,
                        path: init_path,
                        timing: ec_chopper::ChunkTiming {
                            chunk_index: 0,
                            chunk_start_27mhz: None,
                            chunk_duration_27mhz: 0,
                            utc_start_unix: None,
                            sync_status: "init".to_string(),
                        },
                    },
                    data,
                    hash,
                    None,
                    network_secret_bytes.as_deref(),
                    None,
                    "video/mp4",
                    &key_id,
                )?;
                tx.blocking_send(PendingPublish::Object {
                    track: format!("{}/{}", init_track_prefix, variant.id),
                    group: 0,
                    object,
                })
                .map_err(|_| anyhow!("publish channel closed"))?;
            }

            let mut manifest_seq: u64 = 0;
            for index in 0..max_chunks {
                // Wait for each variant's segment for this index.
                let mut per_variant_segments = Vec::with_capacity(variants.len());
                let mut per_variant_hashes = Vec::with_capacity(variants.len());
                for variant in &variants {
                    let seg_path = out_dir
                        .join(&variant.id)
                        .join(format!("segment_{index:06}.m4s"));
                    wait_for_stable_file(&seg_path, Duration::from_secs(30))?;
                    let data = fs::read(&seg_path)?;
                    let hash = blake3::hash(&data).to_hex().to_string();
                    per_variant_segments.push((variant, seg_path, data, hash.clone()));
                    per_variant_hashes.push((variant, hash));
                }

                let created_unix_ms = SystemTime::now()
                    .duration_since(UNIX_EPOCH)
                    .unwrap_or_default()
                    .as_millis() as u64;
                let epoch_id = format!("epoch-{created_unix_ms}");

                let manifest = if publish_manifests {
                    build_multi_variant_manifest(
                        StreamId(base_stream_id.clone()),
                        epoch_id,
                        chunk_ms,
                        index as u64,
                        encoder_profile_id.clone(),
                        created_unix_ms,
                        vec![StreamMetadata {
                            key: "source_kind".to_string(),
                            value: source_kind.clone(),
                        }],
                        &variants,
                        index as u64,
                        per_variant_hashes
                            .iter()
                            .map(|(v, h)| (v.id.clone(), h.clone()))
                            .collect(),
                    )?
                } else {
                    // Still build an unsigned manifest for internal linkage if desired.
                    build_multi_variant_manifest(
                        StreamId(base_stream_id.clone()),
                        epoch_id,
                        chunk_ms,
                        index as u64,
                        encoder_profile_id.clone(),
                        created_unix_ms,
                        vec![StreamMetadata {
                            key: "source_kind".to_string(),
                            value: source_kind.clone(),
                        }],
                        &variants,
                        index as u64,
                        per_variant_hashes
                            .iter()
                            .map(|(v, h)| (v.id.clone(), h.clone()))
                            .collect(),
                    )?
                };

                tx.blocking_send(PendingPublish::Manifest {
                    track: manifest_track.clone(),
                    sequence: manifest_seq,
                    manifest: manifest.clone(),
                })
                .map_err(|_| anyhow!("publish channel closed"))?;
                manifest_seq += 1;

                // Publish segment objects for each variant, linked to the manifest.
                for (variant, seg_path, data, hash) in per_variant_segments {
                    let key_id = derive_variant_stream_id(&base_stream_id, &variant.id);
                    let chunk = TsChunk {
                        index: index as u64,
                        path: seg_path,
                        timing: ec_chopper::ChunkTiming {
                            chunk_index: index as u64,
                            chunk_start_27mhz: None,
                            chunk_duration_27mhz: chunk_ms * 27_000,
                            utc_start_unix: None,
                            sync_status: "cmaf".to_string(),
                        },
                    };
                    let object = build_object(
                        chunk,
                        data,
                        hash,
                        None,
                        network_secret_bytes.as_deref(),
                        Some(&manifest.manifest_id),
                        "video/iso.segment",
                        &key_id,
                    )?;
                    tx.blocking_send(PendingPublish::Object {
                        track: format!("{}/{}", chunk_track_prefix, variant.id),
                        group: (index as u64) + 1,
                        object,
                    })
                    .map_err(|_| anyhow!("publish channel closed"))?;
                }
            }

            let _ = child.kill();
            let _ = child.wait();
            let _ = writer.join();
            Ok(())
        });

        while let Some(item) = rx.recv().await {
            match item {
                PendingPublish::Object {
                    track,
                    group,
                    object,
                } => {
                    publish_set.publish_object(&track, GroupId(group), object)?;
                }
                PendingPublish::Manifest {
                    track,
                    sequence,
                    manifest,
                } => {
                    publish_set.publish_manifest(&track, sequence, &manifest)?;
                    submit_manifest_observation(
                        observation_sink.as_ref(),
                        &manifest,
                        &broadcast_name,
                        &track_name,
                        &track,
                    )
                    .await?;
                }
            }
        }

        chunk_task
            .await
            .map_err(|err| anyhow!("chunk task join error: {err}"))??;

        return Ok(());
    }
    let needs_init_track = publish_chunks;
    let mut epoch_buffer = EpochBuffer::new(args.epoch_chunks);
    // Some early MoQ implementations have surprising assumptions around group sequence ids.
    // In CMAF mode we reserve group 0 for the init segment on the init track and start
    // segment groups at 1 to avoid any potential cross-track collisions.
    let mut object_sequence: u64 = if needs_init_track { 1 } else { 0 };
    let mut manifest_sequence: u64 = 0;

    let announce_tx = if args.announce {
        let gossip_peers = parse_gossip_peers(args.gossip_peer.clone());
        Some(
            spawn_catalog_announcer(&node, &track, &broadcast_name, &track_name, gossip_peers)
                .await?,
        )
    } else {
        None
    };

    #[derive(Debug)]
    enum PendingKind {
        Init,
        Segment,
    }

    #[derive(Debug)]
    struct PendingChunk {
        kind: PendingKind,
        chunk: TsChunk,
        data: Option<Vec<u8>>,
        hash: String,
    }

    let segment_content_type = "video/iso.segment";

    // Chunking is CPU and IO heavy and must not block the async runtime.
    // We do ingest + chunking on a blocking thread and feed finalized chunks
    // to the async publisher over a channel.
    let (tx, mut rx) = mpsc::channel::<PendingChunk>(8);
    let chunk_dir = args.chunk_dir.clone();
    let chunk_ms = args.chunk_ms;
    let max_chunks = args.max_chunks;
    let chunk_task = tokio::task::spawn_blocking(move || -> Result<()> {
        let out_dir = chunk_dir.join("cmaf");
        let _ = fs::remove_dir_all(&out_dir);
        fs::create_dir_all(&out_dir)
            .with_context(|| format!("failed to create {}", out_dir.display()))?;

        let profile = ec_chopper::deterministic_h264_profile();
        let mut cmd = std::process::Command::new("ffmpeg");
        cmd.current_dir(&out_dir);
        cmd.arg("-hide_banner")
            .arg("-loglevel")
            .arg("error")
            .arg("-nostdin")
            .arg("-y")
            .arg("-i")
            .arg("pipe:0");

        // Reduce non-determinism and "surprise streams" (data/subtitles, extra audio).
        // We intentionally pick the first video stream and (optionally) the first audio stream.
        cmd.arg("-map").arg("0:v:0");
        cmd.arg("-map").arg("0:a:0?");
        cmd.arg("-sn").arg("-dn");
        cmd.arg("-map_metadata").arg("-1");

        for arg in ec_chopper::ffmpeg_profile_args(&profile) {
            cmd.arg(arg);
        }
        let seg_time = format!("{:.3}", chunk_ms as f64 / 1000.0);
        let seg_template = "segment_%06d.m4s".to_string();
        let init_filename = "init.mp4".to_string();
        let playlist = "index.m3u8".to_string();

        cmd.arg("-f")
            .arg("hls")
            .arg("-hls_time")
            .arg(seg_time)
            .arg("-hls_list_size")
            .arg("0")
            .arg("-hls_segment_type")
            .arg("fmp4")
            .arg("-hls_flags")
            .arg("independent_segments")
            .arg("-hls_fmp4_init_filename")
            .arg(init_filename)
            .arg("-hls_segment_filename")
            .arg(seg_template)
            .arg(playlist)
            .stdin(std::process::Stdio::piped())
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::inherit());

        let mut child = cmd
            .spawn()
            .with_context(|| "failed to spawn ffmpeg".to_string())?;
        let mut stdin = child
            .stdin
            .take()
            .ok_or_else(|| anyhow!("ffmpeg stdin unavailable"))?;
        let mut reader = source.open_stream()?;
        let writer = std::thread::spawn(move || -> Result<()> {
            std::io::copy(&mut reader, &mut stdin)?;
            Ok(())
        });

        let init_path = out_dir.join("init.mp4");
        if publish_chunks {
            wait_for_stable_file(&init_path, Duration::from_secs(10))?;
            let data = fs::read(&init_path)?;
            let hash = blake3::hash(&data).to_hex().to_string();
            let chunk = TsChunk {
                index: 0,
                path: init_path.clone(),
                timing: ec_chopper::ChunkTiming {
                    chunk_index: 0,
                    chunk_start_27mhz: None,
                    chunk_duration_27mhz: 0,
                    utc_start_unix: None,
                    sync_status: "init".to_string(),
                },
            };
            tx.blocking_send(PendingChunk {
                kind: PendingKind::Init,
                chunk,
                data: Some(data),
                hash,
            })
            .map_err(|_| anyhow!("publish channel closed"))?;
        }

        let limit = max_chunks.unwrap_or(usize::MAX);
        for index in 0..limit {
            let seg_path = out_dir.join(format!("segment_{index:06}.m4s"));
            // If the segment never appears and ffmpeg exited, we are done (short input).
            // If the segment never appears and ffmpeg is still running, treat as error.
            match wait_for_stable_file(&seg_path, Duration::from_secs(20)) {
                Ok(()) => {}
                Err(err) => {
                    if let Ok(Some(status)) = child.try_wait() {
                        if status.success() {
                            break;
                        }
                        return Err(anyhow!("ffmpeg exited with {status} ({err:#})"));
                    }
                    return Err(err);
                }
            }
            let data = if publish_chunks {
                Some(fs::read(&seg_path)?)
            } else {
                None
            };
            let hash = if let Some(ref bytes) = data {
                blake3::hash(bytes).to_hex().to_string()
            } else {
                ec_chopper::hash_file_blake3(&seg_path)?
            };

            let chunk = TsChunk {
                index: index as u64,
                path: seg_path.clone(),
                timing: ec_chopper::ChunkTiming {
                    chunk_index: index as u64,
                    chunk_start_27mhz: None,
                    chunk_duration_27mhz: chunk_ms * 27_000,
                    utc_start_unix: None,
                    sync_status: "cmaf".to_string(),
                },
            };

            tx.blocking_send(PendingChunk {
                kind: PendingKind::Segment,
                chunk,
                data,
                hash,
            })
            .map_err(|_| anyhow!("publish channel closed"))?;
        }

        let _ = child.kill();
        let _ = child.wait();
        let _ = writer.join();
        Ok(())
    });

    while let Some(pending) = rx.recv().await {
        match pending.kind {
            PendingKind::Init => {
                let Some(data) = pending.data else {
                    continue;
                };
                let key_id = format!("{}/init", track.name);
                let object = build_object(
                    pending.chunk,
                    data,
                    pending.hash,
                    None,
                    network_secret.as_deref(),
                    None,
                    "video/mp4",
                    &key_id,
                )?;
                tracing::info!(
                    "publish init bytes={} track={}",
                    object.data.len(),
                    args.init_track
                );
                publish_set.publish_object(&args.init_track, GroupId(0), object)?;
            }
            PendingKind::Segment => {
                epoch_buffer.push(pending.chunk, pending.data, pending.hash);
                if epoch_buffer.is_full() {
                    if let Some(manifest) = flush_epoch_publish(
                        &mut publish_set,
                        &track_name,
                        &args.manifest_track,
                        publish_chunks,
                        args.publish_manifests,
                        &mut epoch_buffer,
                        &stream_id_value,
                        args.chunk_ms,
                        &encoder_profile_id,
                        &source_kind,
                        network_secret.as_deref(),
                        segment_content_type,
                        &track.name,
                        &mut object_sequence,
                        &mut manifest_sequence,
                        announce_tx.as_ref(),
                    )? {
                        submit_manifest_observation(
                            observation_sink.as_ref(),
                            &manifest,
                            &broadcast_name,
                            &track_name,
                            &args.manifest_track,
                        )
                        .await?;
                    }
                }
            }
        }
    }

    // Ensure chunking completed successfully.
    chunk_task
        .await
        .map_err(|err| anyhow!("chunk task join error: {err}"))??;

    if let Some(manifest) = flush_epoch_publish(
        &mut publish_set,
        &track_name,
        &args.manifest_track,
        publish_chunks,
        args.publish_manifests,
        &mut epoch_buffer,
        &stream_id_value,
        args.chunk_ms,
        &encoder_profile_id,
        &source_kind,
        network_secret.as_deref(),
        segment_content_type,
        &track.name,
        &mut object_sequence,
        &mut manifest_sequence,
        announce_tx.as_ref(),
    )? {
        submit_manifest_observation(
            observation_sink.as_ref(),
            &manifest,
            &broadcast_name,
            &track_name,
            &args.manifest_track,
        )
        .await?;
    }

    Ok(())
}

async fn moq_subscribe(args: MoqSubscribeArgs) -> Result<()> {
    if args.require_manifest && !args.subscribe_manifests {
        return Err(anyhow!("--require-manifest requires --subscribe-manifests"));
    }
    let secret = parse_iroh_secret(args.iroh_secret)?;
    let discovery = parse_discovery(args.discovery.as_deref())?;
    let node = MoqNode::bind_with_discovery(secret, discovery).await?;
    let remote = ec_iroh::parse_endpoint_addr(&args.remote)?;
    let remote_manifests = if let Some(value) = args.remote_manifests.as_deref() {
        ec_iroh::parse_endpoint_addr(value)?
    } else {
        remote.clone()
    };
    let mut stream = node
        .subscribe_objects(remote.clone(), &args.broadcast_name, &args.track_name)
        .await?;

    let manifest_allowlist = parse_manifest_allowlist(args.manifest_signers.as_deref());
    let manifest_store = if args.subscribe_manifests || args.require_manifest {
        let mut manifest_stream = node
            .subscribe_manifests(remote_manifests, &args.broadcast_name, &args.manifest_track)
            .await?;
        let store = Arc::new(RwLock::new(HashMap::new()));
        let store_clone = Arc::clone(&store);
        let allowlist = manifest_allowlist.clone();
        tokio::spawn(async move {
            while let Some(manifest) = manifest_stream.recv().await {
                if !validate_manifest(&manifest, allowlist.as_ref()) {
                    tracing::warn!("rejected manifest {}", manifest.manifest_id);
                    continue;
                }
                let manifest_id = manifest.manifest_id.clone();
                store_clone.write().await.insert(manifest_id, manifest);
            }
        });
        Some(store)
    } else {
        None
    };

    let network_secret = parse_network_secret(args.network_secret)?;

    let mut hls =
        HlsWriter::new_cmaf(&args.output_dir, args.chunk_ms as f64 / 1000.0, args.window)?;

    let needs_init = args.subscribe_init;
    let mut init_ready = !needs_init;
    let mut buffered_segments: Vec<(u64, f64, Vec<u8>)> = Vec::new();

    let mut init_rx = if needs_init {
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<Vec<u8>>>();
        let mut init_stream = node
            .subscribe_objects(remote.clone(), &args.broadcast_name, &args.init_track)
            .await?;
        let remote_str = args.remote.clone();
        let init_track = args.init_track.clone();
        let secret = network_secret.clone();
        tokio::spawn(async move {
            let deadline = Instant::now() + Duration::from_secs(60);
            while Instant::now() < deadline {
                let remaining = deadline.saturating_duration_since(Instant::now());
                let recv =
                    tokio::time::timeout(remaining.min(Duration::from_secs(2)), init_stream.recv())
                        .await;
                let Ok(Some(object)) = recv else { continue };

                let init_index = object
                    .meta
                    .timing
                    .as_ref()
                    .map(|t| t.chunk_index)
                    .unwrap_or(0);

                let data = if let Some(enc) = &object.meta.encryption {
                    if enc.alg != ENCRYPTION_ALG {
                        tracing::warn!("init: unsupported encryption {}", enc.alg);
                        continue;
                    }
                    tracing::info!(
                        "init: received encrypted object bytes={} key_id={} chunk_index={}",
                        object.data.len(),
                        enc.key_id,
                        init_index
                    );
                    match decrypt_stream_data(
                        &enc.key_id,
                        init_index,
                        &object.data,
                        secret.as_deref(),
                    ) {
                        Some(plaintext) => plaintext,
                        None => {
                            tracing::warn!(
                                "init: decryption failed key_id={} chunk_index={}",
                                enc.key_id,
                                init_index
                            );
                            continue;
                        }
                    }
                } else {
                    tracing::info!(
                        "init: received plaintext object bytes={} chunk_index={}",
                        object.data.len(),
                        init_index
                    );
                    object.data
                };
                let _ = tx.send(Ok(data));
                return;
            }
            let _ = tx.send(Err(anyhow!(
                "timed out waiting for CMAF init segment on track '{}' from {}",
                init_track,
                remote_str
            )));
        });
        Some(rx)
    } else {
        None
    };

    let fallback = Duration::from_millis(args.chunk_ms);
    let mut fallback_index = 0u64;
    let mut invalid_chunks = 0u32;
    let mut written_chunks = 0u64;
    let mut quota = args.max_bytes_per_sec.map(|rate| {
        let burst = args.max_bytes_burst.unwrap_or(rate.saturating_mul(2));
        ec_iroh::TokenBucket::new(burst, rate)
    });

    loop {
        tokio::select! {
            biased;
            init_res = async { if let Some(rx) = init_rx.as_mut() { Some(rx.await) } else { None } }, if init_rx.is_some() => {
                let Some(init_res) = init_res else { continue };
                let init = match init_res {
                    Ok(inner) => inner?,
                    Err(_) => return Err(anyhow!("init receiver task cancelled")),
                };
                if args.raw_cmaf {
                    fs::create_dir_all(&args.output_dir)?;
                    fs::write(args.output_dir.join("init.mp4"), &init)?;
                } else {
                    let _ = hls.write_init_segment(&init)?;
                }
                init_ready = true;
                init_rx = None;

                // Flush any segments we buffered while waiting for init.
                buffered_segments.sort_by_key(|(idx, _, _)| *idx);
                for (idx, dur, bytes) in buffered_segments.drain(..) {
                    if args.raw_cmaf {
                        fs::create_dir_all(&args.output_dir)?;
                        fs::write(args.output_dir.join(format!("segment_{idx:06}.m4s")), &bytes)?;
                    } else {
                        let _ = hls.write_segment(idx, dur, &bytes)?;
                    }
                    written_chunks += 1;
                    if let Some(limit) = args.stop_after {
                        if written_chunks >= limit {
                            return Ok(());
                        }
                    }
                }
                continue;
            }
            object = stream.recv() => {
                let Some(object) = object else { break };
        if let Some(bucket) = quota.as_mut() {
            if !bucket.allow(object.data.len() as u64) {
                tracing::warn!("quota exceeded; dropping chunk");
                continue;
            }
        }
        let stream_id_for_manifest = object
            .meta
            .encryption
            .as_ref()
            .map(|enc| strip_init_suffix(enc.key_id.as_str()).to_string());
        let index = object
            .meta
            .timing
            .as_ref()
            .map(|t| t.chunk_index)
            .unwrap_or_else(|| {
                let current = fallback_index;
                fallback_index += 1;
                current
            });
        let stream_id = args
            .stream_id
            .as_deref()
            .or_else(|| object.meta.encryption.as_ref().map(|enc| enc.key_id.as_str()));

        let data = if let Some(enc) = &object.meta.encryption {
            if enc.alg != ENCRYPTION_ALG {
                tracing::warn!("unsupported encryption {}", enc.alg);
                continue;
            }
            let Some(stream_id) = stream_id else {
                tracing::warn!("missing stream id for decryption");
                continue;
            };
            match decrypt_stream_data(stream_id, index, &object.data, network_secret.as_deref()) {
                Some(plaintext) => plaintext,
                None => {
                    tracing::warn!("decryption failed for chunk {}", index);
                    continue;
                }
            }
        } else {
            object.data
        };

        if let Some(store) = manifest_store.as_ref() {
            let manifest = {
                let store = store.read().await;
                if let Some(manifest_id) = object.meta.manifest_id.as_ref() {
                    store.get(manifest_id).cloned()
                } else {
                    if let Some(stream_id) = stream_id_for_manifest.as_deref() {
                        find_manifest_for_stream_index(&store, stream_id, index)
                    } else {
                        None
                    }
                }
            };

            if let Some(manifest) = manifest {
                    let expected = stream_id_for_manifest
                        .as_deref()
                        .and_then(|sid| manifest_hash_for_chunk(&manifest, sid, index));
                    if let Some(expected) = expected {
                        if let Some(meta_hash) = object.meta.chunk_hash.as_ref() {
                            if expected != *meta_hash {
                                tracing::warn!(
                                    "manifest mismatch for chunk {} (expected {}, got {})",
                                    index,
                                    expected,
                                    meta_hash
                                );
                                invalid_chunks += 1;
                                if invalid_chunks > args.max_invalid_chunks {
                                    tracing::warn!("too many invalid chunks; closing");
                                    break;
                                }
                                continue;
                            }
                        } else if args.require_manifest {
                            tracing::warn!("missing chunk hash for manifest");
                            invalid_chunks += 1;
                            if invalid_chunks > args.max_invalid_chunks {
                                tracing::warn!("too many invalid chunks; closing");
                                break;
                            }
                            continue;
                        }
                    } else {
                        // If the manifest doesn't include hash lists, fall back to Merkle proof.
                        if let (Some(meta_hash), Some(proof)) =
                            (object.meta.chunk_hash.as_ref(), object.meta.chunk_proof.as_ref())
                        {
                            let offset = (index - manifest.body.chunk_start_index) as usize;
                            if !verify_merkle_proof(
                                meta_hash,
                                offset,
                                proof,
                                &manifest.body.merkle_root,
                            ) {
                                tracing::warn!("chunk {} proof invalid for manifest", index);
                                invalid_chunks += 1;
                                if invalid_chunks > args.max_invalid_chunks {
                                    tracing::warn!("too many invalid chunks; closing");
                                    break;
                                }
                                continue;
                            }
                        } else if args.require_manifest {
                            tracing::warn!("chunk {} outside manifest", index);
                            invalid_chunks += 1;
                            if invalid_chunks > args.max_invalid_chunks {
                                tracing::warn!("too many invalid chunks; closing");
                                break;
                            }
                            continue;
                        }
                    }
            } else if args.require_manifest {
                tracing::warn!("missing manifest covering chunk {}", index);
                continue;
            }
        }

        if let Some(expected) = object.meta.chunk_hash.as_ref() {
            let actual = blake3::hash(&data).to_hex().to_string();
            if &actual != expected {
                tracing::warn!(
                    "chunk {} hash mismatch (expected {}, got {})",
                    index,
                    expected,
                    actual
                );
                invalid_chunks += 1;
                if invalid_chunks > args.max_invalid_chunks {
                    tracing::warn!("too many invalid chunks; closing");
                    break;
                }
                continue;
            }
        }

        let duration = chunk_duration_secs(&object.meta, fallback);
        if !init_ready {
            // Keep draining the MoQ track to avoid flow-control stalls while init is pending.
            buffered_segments.push((index, duration, data));
        } else {
            if args.raw_cmaf {
                fs::create_dir_all(&args.output_dir)?;
                fs::write(args.output_dir.join(format!("segment_{index:06}.m4s")), &data)?;
            } else {
                let _ = hls.write_segment(index, duration, &data)?;
            }
            written_chunks += 1;
        }
        if let Some(limit) = args.stop_after {
            if written_chunks >= limit {
                break;
            }
        }
            }
        }
    }

    if needs_init && !init_ready {
        return Err(anyhow!(
            "stream ended before receiving CMAF init segment on track '{}' from {}",
            args.init_track,
            args.remote
        ));
    }

    Ok(())
}

async fn moq_selftest(args: MoqSelftestArgs) -> Result<()> {
    let discovery = parse_discovery(args.discovery.as_deref())?;
    let publisher_node = MoqNode::bind_with_discovery(None, discovery).await?;
    let subscriber_node = MoqNode::bind_with_discovery(None, discovery).await?;

    publisher_node.endpoint().online().await;
    subscriber_node.endpoint().online().await;

    let stream_id = args.stream_id.unwrap_or_else(|| {
        let ts = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_millis();
        format!("selftest-{ts}")
    });
    let broadcast_name = stream_id.clone();
    let track_name = args.track_name.clone();

    let mut publisher = publisher_node
        .publish_objects(&broadcast_name, &track_name)
        .await?;
    let mut endpoint_addr = publisher_node.endpoint().addr();
    if endpoint_addr.addrs.is_empty() {
        let mut watcher = publisher_node.endpoint().watch_addr();
        let start = Instant::now();
        while endpoint_addr.addrs.is_empty() && start.elapsed() < Duration::from_secs(3) {
            tokio::time::sleep(Duration::from_millis(200)).await;
            endpoint_addr = watcher.get();
        }
    }
    if endpoint_addr.addrs.is_empty() {
        tracing::warn!(
            "publisher endpoint has no direct addrs; selftest may rely on discovery/relays"
        );
    }

    let mut stream = subscriber_node
        .subscribe_objects(endpoint_addr, &broadcast_name, &track_name)
        .await?;

    let expected: Arc<Mutex<BTreeMap<u64, blake3::Hash>>> = Arc::new(Mutex::new(BTreeMap::new()));
    let (done_tx, mut done_rx) = tokio::sync::watch::channel(0usize);
    let (progress_tx, mut progress_rx) = tokio::sync::mpsc::channel::<u64>(8);

    let subscriber_task = tokio::spawn(async move {
        let mut received: BTreeMap<u64, blake3::Hash> = BTreeMap::new();
        loop {
            tokio::select! {
                item = stream.recv() => {
                    match item {
                        Some(object) => {
                            let index = object
                                .meta
                                .timing
                                .as_ref()
                                .map(|timing| timing.chunk_index)
                                .unwrap_or(0);
                            let hash = blake3::hash(&object.data);
                            received.insert(index, hash);
                            let _ = progress_tx.send(index).await;
                            let expected_total = *done_rx.borrow();
                            if expected_total > 0 && received.len() >= expected_total {
                                break;
                            }
                        }
                        None => break,
                    }
                }
                _ = done_rx.changed() => {
                    let expected_total = *done_rx.borrow();
                    if expected_total > 0 && received.len() >= expected_total {
                        break;
                    }
                }
            }
        }
        Ok::<_, anyhow::Error>(received)
    });

    let chunk_dir = args.chunk_dir.clone();
    let input = args.input.clone();
    let max_chunks = args.max_chunks;
    let track = TrackName {
        namespace: "every.channel".to_string(),
        name: stream_id.clone(),
    };

    let objects =
        tokio::task::spawn_blocking(move || -> Result<Vec<(u64, ObjectPayload, blake3::Hash)>> {
            let _ = fs::remove_dir_all(&chunk_dir);
            fs::create_dir_all(&chunk_dir)
                .with_context(|| format!("failed to create {}", chunk_dir.display()))?;

            let reader: Box<dyn Read + Send> = if input.starts_with("http://")
                || input.starts_with("https://")
            {
                Box::new(ec_hdhomerun::open_stream_url(&input, None)?)
            } else {
                Box::new(File::open(&input).with_context(|| format!("failed to open {}", input))?)
            };

            let mut objects = Vec::new();
            let out_dir = chunk_dir.join("cmaf");
            let (init_path, segments) =
                chunk_stream_cmaf_ffmpeg(reader, &out_dir, args.chunk_ms, max_chunks, true)?;

            // Index 0: init.mp4
            let (init_bytes, init_hash) = read_chunk_bytes_and_hash(&init_path)?;
            let init_chunk = TsChunk {
                index: 0,
                path: init_path,
                timing: ec_chopper::ChunkTiming {
                    chunk_index: 0,
                    chunk_start_27mhz: None,
                    chunk_duration_27mhz: 0,
                    utc_start_unix: None,
                    sync_status: "init".to_string(),
                },
            };
            let object = build_object(
                init_chunk,
                init_bytes,
                init_hash,
                None,
                None,
                None,
                "video/mp4",
                &track.name,
            )?;
            let hash = blake3::hash(&object.data);
            objects.push((0, object, hash));

            // Index 1..N: segments
            for (i, seg_path) in segments.into_iter().enumerate() {
                let index = (i as u64) + 1;
                let (bytes, hash_hex) = read_chunk_bytes_and_hash(&seg_path)?;
                let chunk = TsChunk {
                    index,
                    path: seg_path,
                    timing: ec_chopper::ChunkTiming {
                        chunk_index: index,
                        chunk_start_27mhz: None,
                        chunk_duration_27mhz: args.chunk_ms * 27_000,
                        utc_start_unix: None,
                        sync_status: "cmaf".to_string(),
                    },
                };
                let object = build_object(
                    chunk,
                    bytes,
                    hash_hex,
                    None,
                    None,
                    None,
                    "video/iso.segment",
                    &track.name,
                )?;
                let hash = blake3::hash(&object.data);
                objects.push((index, object, hash));
            }

            Ok(objects)
        })
        .await
        .map_err(|err| anyhow!("chunking task failed: {err}"))??;

    {
        let mut map = expected.lock().expect("mutex poisoned");
        for (index, _, hash) in &objects {
            map.insert(*index, *hash);
        }
        let _ = done_tx.send(map.len());
    }

    tokio::time::sleep(Duration::from_millis(250)).await;

    for (index, object, _) in objects {
        publisher.publish_object(GroupId(index), object)?;
        match tokio::time::timeout(Duration::from_secs(2), progress_rx.recv()).await {
            Ok(Some(received)) if received == index => {}
            Ok(Some(received)) => {
                tracing::warn!("selftest received chunk {received} while waiting for {index}");
            }
            Ok(None) => {
                return Err(anyhow!("selftest subscriber closed before chunk {index}"));
            }
            Err(_) => {
                return Err(anyhow!("selftest timed out waiting for chunk {index}"));
            }
        }
    }

    let received = tokio::time::timeout(Duration::from_secs(20), subscriber_task)
        .await
        .map_err(|_| anyhow!("selftest timed out waiting for subscriber"))?
        .map_err(|err| anyhow!("subscriber task failed: {err}"))??;

    let expected_map = expected.lock().expect("mutex poisoned");
    let mut mismatches = 0usize;

    for (index, expected_hash) in expected_map.iter() {
        match received.get(index) {
            Some(actual) if actual == expected_hash => {}
            Some(_) => {
                mismatches += 1;
                tracing::warn!("hash mismatch at chunk {index}");
            }
            None => {
                mismatches += 1;
                tracing::warn!("missing chunk {index}");
            }
        }
    }

    if received.len() > expected_map.len() {
        mismatches += received.len() - expected_map.len();
        tracing::warn!(
            "received extra chunks ({})",
            received.len() - expected_map.len()
        );
    }

    if mismatches > 0 {
        return Err(anyhow!("moq selftest failed with {mismatches} mismatches"));
    }

    println!(
        "moq selftest ok: {} chunks verified (broadcast {}, track {})",
        expected_map.len(),
        broadcast_name,
        track_name
    );

    Ok(())
}

#[derive(serde::Deserialize)]
struct TurnResp {
    ice_servers: Vec<ICEServer>,
}

async fn fetch_turn_ice_servers(client: &reqwest::Client, dir: &str) -> Option<Vec<ICEServer>> {
    let base = dir.trim_end_matches('/');
    let url = format!("{base}/api/turn");
    let res = client.get(url).send().await.ok()?;
    if !res.status().is_success() {
        return None;
    }
    let body: TurnResp = res.json().await.ok()?;
    if body.ice_servers.is_empty() {
        return None;
    }
    Some(body.ice_servers)
}

async fn direct_publish(args: DirectPublishArgs) -> Result<()> {
    fs::create_dir_all(&args.chunk_dir)
        .with_context(|| format!("failed to create {}", args.chunk_dir.display()))?;

    // For browser interop, we currently always normalize into deterministic H.264/AAC CMAF.
    // This also keeps the codec stable for MSE playback.
    let deterministic = true;

    let source: Box<dyn StreamSource + Send> = match args.source {
        IngestSource::Hls { url, .. } => Box::new(HlsSource {
            url,
            mode: HlsMode::Transcode,
        }),
        IngestSource::Hdhr {
            host,
            device_id,
            channel,
            name,
            prefer_mdns,
        } => Box::new(HdhrSource {
            host,
            device_id,
            channel,
            name,
            prefer_mdns,
        }),
        IngestSource::LinuxDvb {
            adapter,
            dvr,
            tune_cmd,
            tune_wait_ms,
        } => Box::new(LinuxDvbSource {
            adapter,
            dvr,
            tune_cmd,
            tune_wait_ms,
        }),
        IngestSource::Ts { input } => Box::new(TsSource { input }),
    };

    fn normalize_base(url: &str) -> String {
        url.trim_end_matches('/').to_string()
    }

    let directory_url = args.directory_url.as_deref().map(normalize_base);
    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(8))
        .build()
        .with_context(|| "failed to build http client")?;

    let mut cfg = PeerConfiguration::default();
    if let Some(dir) = directory_url.as_deref() {
        if let Some(ice) = fetch_turn_ice_servers(&client, dir).await {
            cfg.ice_servers = ice;
        }
    }

    let stream_id = args.stream_id.clone().unwrap_or_else(|| {
        let ts = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_millis();
        format!("every.channel/direct/{ts}")
    });

    #[derive(serde::Serialize)]
    struct AnnounceReq<'a> {
        stream_id: &'a str,
        title: &'a str,
        offer: &'a str,
        expires_ms: u64,
    }

    #[derive(serde::Deserialize)]
    struct AnswerResp {
        answer: String,
    }
    let title = args.title.clone();
    let out_dir = args.chunk_dir.join("cmaf");
    let chunk_ms = args.chunk_ms;
    let max_segments = args.max_segments;
    let answer_override = args.answer.clone();

    loop {
        // New offerer per session so viewers can reconnect (or a new viewer can take over).
        let offerer = PeerConnectionBuilder::new()
            .set_config(cfg.clone())
            .with_channel_options(vec![(
                "simple_channel_".to_string(),
                DataChannelOptions {
                    ordered: Some(true),
                    ..Default::default()
                },
            )])
            .map_err(|e| anyhow!("{e:#}"))?
            .build()
            .await
            .map_err(|e| anyhow!("{e:#}"))?;

        let offer_desc = offerer
            .get_local_description()
            .await
            .ok_or_else(|| anyhow!("missing local offer description"))?;
        let offer_candidates = offerer
            .collect_ice_candidates()
            .await
            .map_err(|e| anyhow!("{e:#}"))?;
        let offer_link = encode_direct_link(&DirectCodeV1 {
            v: 1,
            desc: offer_desc,
            candidates: offer_candidates,
            label: Some("every.channel0".to_string()),
        })?;

        println!("{offer_link}");

        let stop_refresh = tokio::sync::watch::channel(false);
        if let Some(dir) = directory_url.as_deref() {
            let now = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_millis() as u64;
            let req = AnnounceReq {
                stream_id: &stream_id,
                title: &title,
                offer: &offer_link,
                expires_ms: now.saturating_add(args.announce_ttl_ms),
            };
            let url = format!("{dir}/api/announce");
            let res = client.post(url).json(&req).send().await?;
            if !res.status().is_success() {
                tracing::warn!("directory announce failed: {}", res.status());
            } else {
                tracing::info!("announced offer for {}", stream_id);
            }

            // Best-effort refresh while waiting for an answer (stops when the session starts).
            let mut stop_rx = stop_refresh.1.clone();
            let client2 = client.clone();
            let dir2 = dir.to_string();
            let stream_id2 = stream_id.clone();
            let title2 = title.clone();
            let offer2 = offer_link.clone();
            let ttl = args.announce_ttl_ms;
            tokio::spawn(async move {
                loop {
                    if *stop_rx.borrow() {
                        break;
                    }
                    tokio::time::sleep(Duration::from_millis(ttl.saturating_mul(3) / 4)).await;
                    if *stop_rx.borrow() {
                        break;
                    }
                    let now = SystemTime::now()
                        .duration_since(UNIX_EPOCH)
                        .unwrap_or_default()
                        .as_millis() as u64;
                    let req = AnnounceReq {
                        stream_id: &stream_id2,
                        title: &title2,
                        offer: &offer2,
                        expires_ms: now.saturating_add(ttl),
                    };
                    let url = format!("{dir2}/api/announce");
                    let fut = client2.post(url).json(&req).send();
                    let _ = tokio::time::timeout(Duration::from_secs(5), fut).await;
                }
            });
        }

        let answer = if let Some(answer) = answer_override.clone() {
            answer
        } else if let Some(dir) = directory_url.as_deref() {
            eprintln!("waiting for browser answer via {dir}/api/answer?stream_id=...");
            let url = format!(
                "{dir}/api/answer?stream_id={}",
                urlencoding::encode(&stream_id)
            );
            let deadline = if args.answer_timeout_secs == 0 {
                None
            } else {
                Some(Instant::now() + Duration::from_secs(args.answer_timeout_secs))
            };
            loop {
                if deadline.is_some_and(|d| Instant::now() > d) {
                    return Err(anyhow!(
                        "timed out waiting for answer for stream_id {stream_id}"
                    ));
                }
                match client.get(&url).send().await {
                    Ok(res) if res.status().is_success() => {
                        let body: AnswerResp = res.json().await?;
                        break body.answer;
                    }
                    _ => {
                        tokio::time::sleep(Duration::from_millis(300)).await;
                    }
                }
            }
        } else {
            eprintln!("paste direct answer link/code, then press enter:");
            let mut line = String::new();
            std::io::stdin().read_line(&mut line)?;
            line
        };

        // Stop refreshing announcements. If we keep the listing live while connected, people
        // will try to join and silently fail (this direct path is 1:1 today).
        let _ = stop_refresh.0.send(true);

        let answer = decode_direct_link(&answer)?;
        offerer
            .set_remote_description(answer.desc)
            .await
            .map_err(|e| anyhow!("{e:#}"))?;
        offerer
            .add_ice_candidates(answer.candidates)
            .await
            .map_err(|e| anyhow!("{e:#}"))?;

        let channel = offerer
            .receive_channel()
            .await
            .map_err(|e| anyhow!("{e:#}"))?;
        channel.wait_ready().await;
        eprintln!("direct channel open: {}", channel.label());

        // Detect viewer disconnect promptly. Relying only on `channel.send` errors can lag
        // (depending on buffering), leaving the directory entry effectively "stuck".
        let (pc_dead_tx, mut pc_dead_rx) = tokio::sync::oneshot::channel::<PeerConnectionState>();
        let (pc_stop_tx, mut pc_stop_rx) = tokio::sync::watch::channel(false);
        tokio::spawn(async move {
            loop {
                tokio::select! {
                    _ = pc_stop_rx.changed() => {
                        if *pc_stop_rx.borrow() {
                            break;
                        }
                    }
                    st = offerer.state_change() => {
                        if matches!(
                            st,
                            PeerConnectionState::Disconnected | PeerConnectionState::Failed | PeerConnectionState::Closed
                        ) {
                            let _ = pc_dead_tx.send(st);
                            break;
                        }
                    }
                }
            }
        });

        // Expire the listing quickly now that we have a live session.
        if let Some(dir) = directory_url.as_deref() {
            let now = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_millis() as u64;
            let req = AnnounceReq {
                stream_id: &stream_id,
                title: &title,
                offer: &offer_link,
                expires_ms: now.saturating_add(1500),
            };
            let url = format!("{dir}/api/announce");
            let _ = client.post(url).json(&req).send().await;
        }

        enum ChunkItem {
            Init(Vec<u8>),
            Segment { index: u64, bytes: Vec<u8> },
        }

        let (tx, mut rx) = mpsc::channel::<ChunkItem>(8);
        let reader = source.open_stream()?;
        let out_dir2 = out_dir.clone();
        let chunk_task = tokio::task::spawn_blocking(move || -> Result<()> {
            let _ = fs::remove_dir_all(&out_dir2);
            fs::create_dir_all(&out_dir2)
                .with_context(|| format!("failed to create {}", out_dir2.display()))?;

            let profile = if deterministic {
                ec_chopper::deterministic_h264_profile()
            } else {
                ec_chopper::deterministic_h264_profile()
            };

            let mut cmd = Command::new("ffmpeg");
            cmd.current_dir(&out_dir2);
            cmd.arg("-hide_banner")
                .arg("-loglevel")
                .arg("error")
                .arg("-nostdin")
                .arg("-y")
                .arg("-i")
                .arg("pipe:0")
                // predictable mapping
                .arg("-map")
                .arg("0:v:0")
                .arg("-map")
                .arg("0:a:0?")
                .arg("-sn")
                .arg("-dn")
                .arg("-map_metadata")
                .arg("-1")
                // reduce scheduling nondeterminism
                .arg("-filter_threads")
                .arg("1")
                .arg("-filter_complex_threads")
                .arg("1")
                .arg("-threads")
                .arg("1");

            for arg in ec_chopper::ffmpeg_profile_args(&profile) {
                cmd.arg(arg);
            }

            let seg_time = format!("{:.3}", chunk_ms as f64 / 1000.0);
            cmd.arg("-f")
                .arg("hls")
                .arg("-hls_time")
                .arg(seg_time)
                .arg("-hls_list_size")
                .arg("0")
                .arg("-hls_segment_type")
                .arg("fmp4")
                .arg("-hls_flags")
                .arg("independent_segments")
                .arg("-hls_fmp4_init_filename")
                .arg("init.mp4")
                .arg("-hls_segment_filename")
                .arg("segment_%06d.m4s")
                .arg("index.m3u8")
                .stdin(Stdio::piped())
                .stdout(Stdio::null())
                .stderr(Stdio::inherit());

            let mut child = cmd.spawn().with_context(|| "failed to spawn ffmpeg")?;
            let mut stdin = child
                .stdin
                .take()
                .ok_or_else(|| anyhow!("ffmpeg stdin unavailable"))?;
            let mut reader = reader;
            let writer = std::thread::spawn(move || -> Result<()> {
                std::io::copy(&mut reader, &mut stdin)?;
                Ok(())
            });

            let init_path = out_dir2.join("init.mp4");
            wait_for_stable_file(&init_path, Duration::from_secs(20))?;
            let init = fs::read(&init_path)?;
            tx.blocking_send(ChunkItem::Init(init))
                .map_err(|_| anyhow!("receiver closed"))?;

            for i in 0..max_segments {
                let seg_path = out_dir2.join(format!("segment_{i:06}.m4s"));
                match wait_for_stable_file(&seg_path, Duration::from_secs(30)) {
                    Ok(()) => {}
                    Err(err) => {
                        if let Ok(Some(status)) = child.try_wait() {
                            if status.success() {
                                break;
                            }
                            return Err(anyhow!("ffmpeg exited with {status} ({err:#})"));
                        }
                        return Err(err);
                    }
                }
                let bytes = fs::read(&seg_path)?;
                tx.blocking_send(ChunkItem::Segment {
                    index: (i as u64) + 1,
                    bytes,
                })
                .map_err(|_| anyhow!("receiver closed"))?;
            }

            let _ = child.kill();
            let _ = child.wait();
            let _ = writer.join();
            Ok(())
        });

        let mut send_failed = false;
        let mut have_heartbeat = false;
        let mut last_heartbeat = Instant::now();
        let mut heartbeat_check = tokio::time::interval(Duration::from_secs(1));
        heartbeat_check.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
        loop {
            tokio::select! {
                biased;
                st = (&mut pc_dead_rx) => {
                    match st {
                        Ok(st) => tracing::info!("peer connection ended ({st:?}); restarting session"),
                        Err(_) => tracing::info!("peer connection ended; restarting session"),
                    }
                    send_failed = true;
                    break;
                }
                _ = heartbeat_check.tick() => {
                    if have_heartbeat && last_heartbeat.elapsed() > DIRECT_HEARTBEAT_TIMEOUT {
                        tracing::info!("direct session heartbeat timed out; restarting session");
                        send_failed = true;
                        break;
                    }
                }
                msg = channel.receive() => {
                    match msg {
                        Ok(b) => {
                            // Any message from the subscriber counts as a heartbeat today.
                            // (We reserve DIRECT_WIRE_TAG_PING for explicit pings, but don't require it.)
                            have_heartbeat = true;
                            last_heartbeat = Instant::now();
                            if !b.is_empty() && b[0] == DIRECT_WIRE_TAG_PING {
                                // ignore
                            }
                        }
                        Err(_) => {
                            send_failed = true;
                            break;
                        }
                    }
                }
                item = rx.recv() => {
                    let Some(item) = item else { break };
                    let (index, content_type, bytes, duration_27mhz, sync_status) = match item {
                        ChunkItem::Init(bytes) => (0u64, "video/mp4", bytes, 0u64, "init"),
                        ChunkItem::Segment { index, bytes } => {
                            (index, "video/iso.segment", bytes, chunk_ms * 27_000, "cmaf")
                        }
                    };
                    if index == 0 {
                        tracing::info!("direct send: init.mp4 ({} bytes)", bytes.len());
                    } else if index % 5 == 0 {
                        tracing::info!("direct send: segment {index} ({} bytes)", bytes.len());
                    }
                    let hash = blake3::hash(&bytes).to_hex().to_string();
                    let meta = ObjectMeta {
                        created_unix_ms: SystemTime::now()
                            .duration_since(UNIX_EPOCH)
                            .unwrap_or_default()
                            .as_millis() as u64,
                        content_type: content_type.to_string(),
                        size_bytes: bytes.len() as u64,
                        timing: Some(TimingMeta {
                            chunk_index: index,
                            chunk_start_27mhz: 0,
                            chunk_duration_27mhz: duration_27mhz,
                            utc_start_unix: None,
                            sync_status: sync_status.to_string(),
                        }),
                        encryption: None,
                        chunk_hash: Some(hash),
                        chunk_hash_alg: Some("blake3".to_string()),
                        chunk_proof: None,
                        chunk_proof_alg: None,
                        manifest_id: None,
                    };
                    let frame = encode_object_frame(&meta, &bytes)?;
                    match tokio::time::timeout(
                        Duration::from_secs(5),
                        direct_wire_send_frame(&channel, &frame),
                    )
                    .await
                    {
                        Ok(Ok(())) => {}
                        Ok(Err(err)) => {
                            tracing::warn!("direct send failed (restarting session): {err:#}");
                            send_failed = true;
                            break;
                        }
                        Err(_) => {
                            tracing::warn!("direct send timed out (restarting session)");
                            send_failed = true;
                            break;
                        }
                    }
                }
            }
        }

        // Ensure the peer connection is dropped if we restart for non-ICE reasons (e.g. ffmpeg end).
        let _ = pc_stop_tx.send(true);

        drop(rx);
        match chunk_task.await {
            Ok(Ok(())) => {}
            Ok(Err(err)) => {
                // Common when the viewer disconnects: the receiver is dropped and the
                // blocking sender errors out. Treat as a reconnect.
                tracing::debug!("chunk task ended: {err:#}");
            }
            Err(err) => tracing::debug!("chunk task join error: {err}"),
        }

        if answer_override.is_some() {
            // For manual mode, we can't reasonably loop.
            break;
        }
        if !send_failed {
            // ffmpeg ended or source ended; try again.
            tokio::time::sleep(Duration::from_millis(500)).await;
        }
    }

    Ok(())
}

async fn direct_wire_send_frame(channel: &impl DataChannelExt, frame: &[u8]) -> Result<()> {
    let len = u32::try_from(frame.len()).map_err(|_| anyhow!("frame too large"))?;
    let mut stream = Vec::with_capacity(4 + frame.len());
    stream.extend_from_slice(&len.to_be_bytes());
    stream.extend_from_slice(frame);

    for chunk in stream.chunks(DIRECT_WIRE_CHUNK_BYTES) {
        let mut msg = Vec::with_capacity(1 + chunk.len());
        msg.push(DIRECT_WIRE_TAG_STREAM);
        msg.extend_from_slice(chunk);
        channel
            .send(&bytes::Bytes::from(msg))
            .await
            .map_err(|e| anyhow!("{e:#}"))?;
    }
    Ok(())
}

#[derive(Debug, Default)]
struct DirectWireDecoder {
    buf: Vec<u8>,
    pos: usize,
    want: Option<usize>,
}

impl DirectWireDecoder {
    fn push(&mut self, msg: &[u8]) -> Result<Vec<Vec<u8>>> {
        if msg.is_empty() {
            return Ok(Vec::new());
        }

        match msg[0] {
            DIRECT_WIRE_TAG_FRAME => return Ok(vec![msg[1..].to_vec()]),
            DIRECT_WIRE_TAG_STREAM => {
                self.buf.extend_from_slice(&msg[1..]);
            }
            DIRECT_WIRE_TAG_PING => {
                // Control message; ignore.
                return Ok(Vec::new());
            }
            _ => {
                // Unknown tag: treat as legacy "whole frame per message".
                return Ok(vec![msg.to_vec()]);
            }
        }

        let mut out = Vec::new();
        loop {
            if self.want.is_none() {
                if self.buf.len().saturating_sub(self.pos) < 4 {
                    break;
                }
                let start = self.pos;
                let meta = &self.buf[start..start + 4];
                let len = u32::from_be_bytes([meta[0], meta[1], meta[2], meta[3]]) as usize;
                self.pos += 4;
                self.want = Some(len);
            }

            let Some(want) = self.want else { break };
            if self.buf.len().saturating_sub(self.pos) < want {
                break;
            }
            let start = self.pos;
            let end = start + want;
            out.push(self.buf[start..end].to_vec());
            self.pos = end;
            self.want = None;

            // Avoid unbounded growth: occasionally compact the buffer.
            if self.pos > 64 * 1024 {
                self.buf.drain(0..self.pos);
                self.pos = 0;
            }
        }
        Ok(out)
    }
}

async fn direct_subscribe(args: DirectSubscribeArgs) -> Result<()> {
    fs::create_dir_all(&args.out_dir)
        .with_context(|| format!("failed to create {}", args.out_dir.display()))?;

    #[derive(serde::Deserialize)]
    struct DirectoryResp {
        entries: Vec<DirectoryEntry>,
    }

    #[derive(serde::Deserialize)]
    struct DirectoryEntry {
        stream_id: String,
        title: String,
        offer: String,
        #[allow(dead_code)]
        expires_ms: Option<u64>,
    }

    fn normalize_base(url: &str) -> String {
        url.trim_end_matches('/').to_string()
    }

    let dir = normalize_base(&args.directory_url);
    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(8))
        .build()
        .with_context(|| "failed to build http client")?;

    let (stream_id, offer_link) = match (args.stream_id.clone(), args.offer.clone()) {
        (_, Some(offer)) => (args.stream_id.clone(), offer),
        (Some(stream_id), None) => {
            let url = format!("{dir}/api/directory");
            let res = client.get(&url).send().await?;
            if !res.status().is_success() {
                return Err(anyhow!("directory GET failed: {}", res.status()));
            }
            let body: DirectoryResp = res.json().await?;
            let entry = body
                .entries
                .into_iter()
                .find(|e| e.stream_id == stream_id)
                .ok_or_else(|| anyhow!("stream_id not found in directory: {stream_id}"))?;
            (Some(stream_id), entry.offer)
        }
        (None, None) => {
            return Err(anyhow!("either --offer or --stream-id is required"));
        }
    };

    let offer = decode_direct_link(&offer_link)?;
    let mut cfg = PeerConfiguration::default();
    if let Some(ice) = fetch_turn_ice_servers(&client, &dir).await {
        cfg.ice_servers = ice;
    }
    let pc = PeerConnectionBuilder::new()
        .set_config(cfg)
        .with_remote_offer(Some(offer.desc))
        .map_err(|e| anyhow!("{e:#}"))?
        .build()
        .await
        .map_err(|e| anyhow!("{e:#}"))?;
    pc.add_ice_candidates(offer.candidates)
        .await
        .map_err(|e| anyhow!("{e:#}"))?;

    // Build our answer link (used both for directory POST and manual copy/paste).
    let desc = pc
        .get_local_description()
        .await
        .ok_or_else(|| anyhow!("missing local answer description"))?;
    let candidates = pc
        .collect_ice_candidates()
        .await
        .map_err(|e| anyhow!("{e:#}"))?;
    let answer_link = encode_direct_link(&DirectCodeV1 {
        v: 1,
        desc,
        candidates,
        label: Some("every.channel0".to_string()),
    })?;

    if let Some(stream_id) = stream_id.as_deref() {
        #[derive(serde::Serialize)]
        struct AnswerReq<'a> {
            stream_id: &'a str,
            answer: &'a str,
        }
        let url = format!("{dir}/api/answer");
        let res = client
            .post(url)
            .json(&AnswerReq {
                stream_id,
                answer: &answer_link,
            })
            .send()
            .await?;
        if !res.status().is_success() {
            return Err(anyhow!(
                "directory POST /api/answer failed: {}",
                res.status()
            ));
        }
    } else {
        eprintln!("answer link (paste into publisher):\n{answer_link}");
    }

    let ch = pc.receive_channel().await.map_err(|e| anyhow!("{e:#}"))?;
    ch.wait_ready().await;
    eprintln!("direct channel open: {}", ch.label());

    let cmaf_dir = args.out_dir.join("cmaf");
    fs::create_dir_all(&cmaf_dir)
        .with_context(|| format!("failed to create {}", cmaf_dir.display()))?;

    let mut init_written = false;
    let mut durations = Vec::<f64>::new();
    let mut captured_segments = 0usize;
    let mut decoder = DirectWireDecoder::default();

    let mut ping = tokio::time::interval(Duration::from_secs(1));
    ping.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);

    let deadline = args
        .duration_secs
        .map(|s| Instant::now() + Duration::from_secs(s));
    while captured_segments < args.max_segments {
        if deadline.is_some_and(|d| Instant::now() > d) {
            break;
        }
        let msg = tokio::select! {
            _ = ping.tick() => {
                // Heartbeat to help the publisher detect disconnects promptly.
                let _ = ch.send(&bytes::Bytes::from(vec![DIRECT_WIRE_TAG_PING])).await;
                continue;
            }
            msg = ch.receive() => msg.map_err(|e| anyhow!("{e:#}"))?,
        };
        for frame in decoder.push(&msg)? {
            let payload = decode_object_frame(&frame)?;
            let idx = payload
                .meta
                .timing
                .as_ref()
                .map(|t| t.chunk_index)
                .unwrap_or(0);

            if idx == 0 {
                tracing::info!("direct recv: init.mp4 ({} bytes)", payload.data.len());
                let path = cmaf_dir.join("init.mp4");
                fs::write(&path, &payload.data)
                    .with_context(|| format!("failed to write {}", path.display()))?;
                init_written = true;
                continue;
            }

            if !init_written {
                // Ignore segments until init arrives.
                continue;
            }

            // Publisher indexes segments starting at 1; our filenames start at 0.
            let seg = idx.saturating_sub(1);
            let path = cmaf_dir.join(format!("segment_{seg:06}.m4s"));
            fs::write(&path, &payload.data)
                .with_context(|| format!("failed to write {}", path.display()))?;
            tracing::info!("direct recv: segment {idx} ({} bytes)", payload.data.len());

            let dur = payload
                .meta
                .timing
                .as_ref()
                .map(|t| t.chunk_duration_27mhz as f64 / 27_000_000.0)
                .unwrap_or(2.0);
            durations.push(dur);
            captured_segments += 1;
            if captured_segments >= args.max_segments {
                break;
            }
        }
    }

    if !init_written || durations.is_empty() {
        return Err(anyhow!("no media captured (missing init or segments)"));
    }

    // Write a VOD playlist so ffmpeg can remux the fragments.
    let target = durations
        .iter()
        .copied()
        .fold(0.0_f64, f64::max)
        .ceil()
        .max(1.0) as u64;
    let mut m3u8 = String::new();
    m3u8.push_str("#EXTM3U\n");
    m3u8.push_str("#EXT-X-VERSION:7\n");
    m3u8.push_str(&format!("#EXT-X-TARGETDURATION:{target}\n"));
    m3u8.push_str("#EXT-X-PLAYLIST-TYPE:VOD\n");
    m3u8.push_str("#EXT-X-INDEPENDENT-SEGMENTS\n");
    m3u8.push_str("#EXT-X-MAP:URI=\"init.mp4\"\n");
    for (i, dur) in durations.iter().enumerate() {
        m3u8.push_str(&format!("#EXTINF:{:.3},\n", dur));
        m3u8.push_str(&format!("segment_{i:06}.m4s\n"));
    }
    m3u8.push_str("#EXT-X-ENDLIST\n");
    let playlist_path = cmaf_dir.join("index.m3u8");
    fs::write(&playlist_path, m3u8)
        .with_context(|| format!("failed to write {}", playlist_path.display()))?;

    let mp4_path = args
        .mp4
        .clone()
        .unwrap_or_else(|| args.out_dir.join("capture.mp4"));
    let status = Command::new("ffmpeg")
        .arg("-hide_banner")
        .arg("-loglevel")
        .arg("error")
        .arg("-nostdin")
        .arg("-y")
        .arg("-protocol_whitelist")
        .arg("file,crypto,data")
        .arg("-i")
        .arg(&playlist_path)
        .arg("-c")
        .arg("copy")
        .arg(&mp4_path)
        .status();
    match status {
        Ok(s) if s.success() => {
            println!("{}", mp4_path.display());
        }
        Ok(s) => {
            eprintln!("ffmpeg remux failed: {s}");
            println!("{}", playlist_path.display());
        }
        Err(err) => {
            eprintln!("failed to run ffmpeg: {err}");
            println!("{}", playlist_path.display());
        }
    }

    Ok(())
}

fn ws_url_for(base: &str, stream_id: &str, role: &str) -> String {
    let base = base.trim_end_matches('/');
    let ws_base = if let Some(rest) = base.strip_prefix("https://") {
        format!("wss://{rest}")
    } else if let Some(rest) = base.strip_prefix("http://") {
        format!("ws://{rest}")
    } else if base.starts_with("ws://") || base.starts_with("wss://") {
        base.to_string()
    } else {
        format!("wss://{base}")
    };
    format!(
        "{ws_base}/api/stream/ws?stream_id={}&role={role}",
        urlencoding::encode(stream_id)
    )
}

async fn ws_send_frame(
    ws: &mut tokio_tungstenite::WebSocketStream<
        tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
    >,
    frame: &[u8],
) -> Result<()> {
    let len = u32::try_from(frame.len()).map_err(|_| anyhow!("frame too large"))?;
    let mut stream = Vec::with_capacity(4 + frame.len());
    stream.extend_from_slice(&len.to_be_bytes());
    stream.extend_from_slice(frame);
    for chunk in stream.chunks(DIRECT_WIRE_CHUNK_BYTES) {
        let mut msg = Vec::with_capacity(1 + chunk.len());
        msg.push(DIRECT_WIRE_TAG_STREAM);
        msg.extend_from_slice(chunk);
        ws.send(WsMessage::Binary(msg)).await?;
    }
    Ok(())
}

async fn ws_publish(args: WsPublishArgs) -> Result<()> {
    fs::create_dir_all(&args.chunk_dir)
        .with_context(|| format!("failed to create {}", args.chunk_dir.display()))?;

    // For browser interop, we currently always normalize into deterministic H.264/AAC CMAF.
    let deterministic = true;

    let source: Box<dyn StreamSource + Send> = match args.source {
        IngestSource::Hls { url, .. } => Box::new(HlsSource {
            url,
            mode: HlsMode::Transcode,
        }),
        IngestSource::Hdhr {
            host,
            device_id,
            channel,
            name,
            prefer_mdns,
        } => Box::new(HdhrSource {
            host,
            device_id,
            channel,
            name,
            prefer_mdns,
        }),
        IngestSource::LinuxDvb {
            adapter,
            dvr,
            tune_cmd,
            tune_wait_ms,
        } => Box::new(LinuxDvbSource {
            adapter,
            dvr,
            tune_cmd,
            tune_wait_ms,
        }),
        IngestSource::Ts { input } => Box::new(TsSource { input }),
    };

    fn normalize_base(url: &str) -> String {
        url.trim_end_matches('/').to_string()
    }

    let directory_url = normalize_base(&args.directory_url);
    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(8))
        .build()
        .with_context(|| "failed to build http client")?;

    let stream_id = args.stream_id.clone().unwrap_or_else(|| {
        let ts = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_millis();
        format!("every.channel/ws/{ts}")
    });
    let title = args.title.clone();

    #[derive(serde::Serialize)]
    struct AnnounceReq<'a> {
        stream_id: &'a str,
        title: &'a str,
        offer: &'a str,
        expires_ms: u64,
    }

    // Directory listing is still "offer"-shaped today; keep a non-empty placeholder for legacy clients.
    let offer_placeholder = format!("every.channel://watch?stream_id={}", stream_id);

    // Refresh listing forever while publishing (one-to-many relay supports multiple viewers).
    let ttl = args.announce_ttl_ms;
    let client2 = client.clone();
    let dir2 = directory_url.clone();
    let stream_id2 = stream_id.clone();
    let title2 = title.clone();
    let offer2 = offer_placeholder.clone();
    tokio::spawn(async move {
        loop {
            let now = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_millis() as u64;
            let req = AnnounceReq {
                stream_id: &stream_id2,
                title: &title2,
                offer: &offer2,
                expires_ms: now.saturating_add(ttl),
            };
            let url = format!("{dir2}/api/announce");
            let _ =
                tokio::time::timeout(Duration::from_secs(5), client2.post(url).json(&req).send())
                    .await;
            tokio::time::sleep(Duration::from_millis(ttl.saturating_mul(3) / 4)).await;
        }
    });

    // Connect to relay websocket as publisher.
    let ws_url = ws_url_for(&directory_url, &stream_id, "pub");
    eprintln!("ws publish: {stream_id}");
    eprintln!("ws url: {ws_url}");
    let (mut ws, _resp) = tokio_tungstenite::connect_async(&ws_url).await?;

    enum ChunkItem {
        Init(Vec<u8>),
        Segment { index: u64, bytes: Vec<u8> },
    }
    let out_dir = args.chunk_dir.join("cmaf");
    let chunk_ms = args.chunk_ms;
    let max_segments = args.max_segments;

    let (tx, mut rx) = mpsc::channel::<ChunkItem>(8);
    let reader = source.open_stream()?;
    let out_dir2 = out_dir.clone();
    let chunk_task = tokio::task::spawn_blocking(move || -> Result<()> {
        let _ = fs::remove_dir_all(&out_dir2);
        fs::create_dir_all(&out_dir2)
            .with_context(|| format!("failed to create {}", out_dir2.display()))?;

        let profile = if deterministic {
            ec_chopper::deterministic_h264_profile()
        } else {
            ec_chopper::deterministic_h264_profile()
        };

        let mut cmd = Command::new("ffmpeg");
        cmd.current_dir(&out_dir2);
        cmd.arg("-hide_banner")
            .arg("-loglevel")
            .arg("error")
            .arg("-nostdin")
            .arg("-y")
            .arg("-i")
            .arg("pipe:0")
            // predictable mapping
            .arg("-map")
            .arg("0:v:0")
            .arg("-map")
            .arg("0:a:0?")
            .arg("-sn")
            .arg("-dn")
            .arg("-map_metadata")
            .arg("-1")
            // reduce scheduling nondeterminism
            .arg("-filter_threads")
            .arg("1")
            .arg("-filter_complex_threads")
            .arg("1")
            .arg("-threads")
            .arg("1");

        for arg in ec_chopper::ffmpeg_profile_args(&profile) {
            cmd.arg(arg);
        }

        let seg_time = format!("{:.3}", chunk_ms as f64 / 1000.0);
        cmd.arg("-f")
            .arg("hls")
            .arg("-hls_time")
            .arg(seg_time)
            .arg("-hls_list_size")
            .arg("0")
            .arg("-hls_segment_type")
            .arg("fmp4")
            .arg("-hls_flags")
            .arg("independent_segments")
            .arg("-hls_fmp4_init_filename")
            .arg("init.mp4")
            .arg("-hls_segment_filename")
            .arg("segment_%06d.m4s")
            .arg("index.m3u8")
            .stdin(Stdio::piped())
            .stdout(Stdio::null())
            .stderr(Stdio::inherit());

        let mut child = cmd.spawn().with_context(|| "failed to spawn ffmpeg")?;
        let mut stdin = child
            .stdin
            .take()
            .ok_or_else(|| anyhow!("ffmpeg stdin unavailable"))?;
        let mut reader = reader;
        let writer = std::thread::spawn(move || -> Result<()> {
            std::io::copy(&mut reader, &mut stdin)?;
            Ok(())
        });

        let init_path = out_dir2.join("init.mp4");
        wait_for_stable_file(&init_path, Duration::from_secs(20))?;
        let init = fs::read(&init_path)?;
        tx.blocking_send(ChunkItem::Init(init))
            .map_err(|_| anyhow!("receiver closed"))?;

        for i in 0..max_segments {
            let seg_path = out_dir2.join(format!("segment_{i:06}.m4s"));
            match wait_for_stable_file(&seg_path, Duration::from_secs(30)) {
                Ok(()) => {}
                Err(err) => {
                    if let Ok(Some(status)) = child.try_wait() {
                        if status.success() {
                            break;
                        }
                        return Err(anyhow!("ffmpeg exited with {status} ({err:#})"));
                    }
                    return Err(err);
                }
            }
            let bytes = fs::read(&seg_path)?;
            tx.blocking_send(ChunkItem::Segment {
                index: (i as u64) + 1,
                bytes,
            })
            .map_err(|_| anyhow!("receiver closed"))?;
        }

        let _ = child.kill();
        let _ = child.wait();
        let _ = writer.join();
        Ok(())
    });

    while let Some(item) = rx.recv().await {
        let (index, content_type, bytes, duration_27mhz, sync_status) = match item {
            ChunkItem::Init(bytes) => (0u64, "video/mp4", bytes, 0u64, "init"),
            ChunkItem::Segment { index, bytes } => {
                (index, "video/iso.segment", bytes, chunk_ms * 27_000, "cmaf")
            }
        };
        if index == 0 {
            tracing::info!("ws send: init.mp4 ({} bytes)", bytes.len());
        } else if index % 10 == 0 {
            tracing::info!("ws send: segment {index} ({} bytes)", bytes.len());
        }

        let hash = blake3::hash(&bytes).to_hex().to_string();
        let meta = ObjectMeta {
            created_unix_ms: SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_millis() as u64,
            content_type: content_type.to_string(),
            size_bytes: bytes.len() as u64,
            timing: Some(TimingMeta {
                chunk_index: index,
                chunk_start_27mhz: 0,
                chunk_duration_27mhz: duration_27mhz,
                utc_start_unix: None,
                sync_status: sync_status.to_string(),
            }),
            encryption: None,
            chunk_hash: Some(hash),
            chunk_hash_alg: Some("blake3".to_string()),
            chunk_proof: None,
            chunk_proof_alg: None,
            manifest_id: None,
        };
        let frame = encode_object_frame(&meta, &bytes)?;
        tokio::time::timeout(Duration::from_secs(5), ws_send_frame(&mut ws, &frame)).await??;
    }

    match chunk_task.await {
        Ok(Ok(())) => Ok(()),
        Ok(Err(err)) => Err(err),
        Err(err) => Err(anyhow!("chunk task join error: {err}")),
    }
}

async fn ws_subscribe(args: WsSubscribeArgs) -> Result<()> {
    fs::create_dir_all(&args.out_dir)
        .with_context(|| format!("failed to create {}", args.out_dir.display()))?;

    let ws_url = ws_url_for(&args.directory_url, &args.stream_id, "sub");
    eprintln!("ws subscribe: {} ({ws_url})", args.stream_id);
    let (mut ws, _resp) = tokio_tungstenite::connect_async(&ws_url).await?;

    let cmaf_dir = args.out_dir.join("cmaf");
    fs::create_dir_all(&cmaf_dir)
        .with_context(|| format!("failed to create {}", cmaf_dir.display()))?;

    let mut init_written = false;
    let mut durations = Vec::<f64>::new();
    let mut captured_segments = 0usize;
    let mut decoder = DirectWireDecoder::default();

    let deadline = args
        .duration_secs
        .map(|s| Instant::now() + Duration::from_secs(s));

    while captured_segments < args.max_segments {
        if deadline.is_some_and(|d| Instant::now() > d) {
            break;
        }
        let next = ws
            .next()
            .await
            .ok_or_else(|| anyhow!("websocket closed"))??;
        let bytes = match next {
            WsMessage::Binary(b) => b,
            WsMessage::Close(_) => break,
            _ => continue,
        };
        for frame in decoder.push(&bytes)? {
            let payload = decode_object_frame(&frame)?;
            let idx = payload
                .meta
                .timing
                .as_ref()
                .map(|t| t.chunk_index)
                .unwrap_or(0);

            if idx == 0 {
                tracing::info!("ws recv: init.mp4 ({} bytes)", payload.data.len());
                let path = cmaf_dir.join("init.mp4");
                fs::write(&path, &payload.data)
                    .with_context(|| format!("failed to write {}", path.display()))?;
                init_written = true;
                continue;
            }

            if !init_written {
                continue;
            }

            let seg = idx.saturating_sub(1);
            let path = cmaf_dir.join(format!("segment_{seg:06}.m4s"));
            fs::write(&path, &payload.data)
                .with_context(|| format!("failed to write {}", path.display()))?;
            if idx % 10 == 0 {
                tracing::info!("ws recv: segment {idx} ({} bytes)", payload.data.len());
            }

            let dur = payload
                .meta
                .timing
                .as_ref()
                .map(|t| t.chunk_duration_27mhz as f64 / 27_000_000.0)
                .unwrap_or(2.0);
            durations.push(dur);
            captured_segments += 1;
            if captured_segments >= args.max_segments {
                break;
            }
        }
    }

    if !init_written || durations.is_empty() {
        return Err(anyhow!("no media captured (missing init or segments)"));
    }

    let target = durations
        .iter()
        .copied()
        .fold(0.0_f64, f64::max)
        .ceil()
        .max(1.0) as u64;
    let mut m3u8 = String::new();
    m3u8.push_str("#EXTM3U\n");
    m3u8.push_str("#EXT-X-VERSION:7\n");
    m3u8.push_str(&format!("#EXT-X-TARGETDURATION:{target}\n"));
    m3u8.push_str("#EXT-X-PLAYLIST-TYPE:VOD\n");
    m3u8.push_str("#EXT-X-INDEPENDENT-SEGMENTS\n");
    m3u8.push_str("#EXT-X-MAP:URI=\"init.mp4\"\n");
    for (i, dur) in durations.iter().enumerate() {
        m3u8.push_str(&format!("#EXTINF:{:.3},\n", dur));
        m3u8.push_str(&format!("segment_{i:06}.m4s\n"));
    }
    m3u8.push_str("#EXT-X-ENDLIST\n");
    let playlist_path = cmaf_dir.join("index.m3u8");
    fs::write(&playlist_path, m3u8)
        .with_context(|| format!("failed to write {}", playlist_path.display()))?;

    let mp4_path = args
        .mp4
        .clone()
        .unwrap_or_else(|| args.out_dir.join("capture.mp4"));
    let status = Command::new("ffmpeg")
        .arg("-hide_banner")
        .arg("-loglevel")
        .arg("error")
        .arg("-nostdin")
        .arg("-y")
        .arg("-protocol_whitelist")
        .arg("file,crypto,data")
        .arg("-i")
        .arg(&playlist_path)
        .arg("-c")
        .arg("copy")
        .arg(&mp4_path)
        .status();
    match status {
        Ok(s) if s.success() => {
            println!("{}", mp4_path.display());
        }
        Ok(s) => {
            eprintln!("ffmpeg remux failed: {s}");
            println!("{}", playlist_path.display());
        }
        Err(err) => {
            eprintln!("failed to run ffmpeg: {err}");
            println!("{}", playlist_path.display());
        }
    }

    Ok(())
}

fn parse_iroh_secret(value: Option<String>) -> Result<Option<iroh::SecretKey>> {
    let value = value.or_else(|| std::env::var("IROH_SECRET").ok());
    let Some(value) = value else { return Ok(None) };
    let secret =
        iroh::SecretKey::from_str(&value).with_context(|| "failed to parse IROH_SECRET")?;
    Ok(Some(secret))
}

fn parse_discovery(value: Option<&str>) -> Result<DiscoveryConfig> {
    match value {
        Some(value) => DiscoveryConfig::from_list(value),
        None => DiscoveryConfig::from_env(),
    }
}

fn parse_gossip_peers(mut peers: Vec<String>) -> Vec<String> {
    if let Ok(env_peers) = std::env::var("EVERY_CHANNEL_GOSSIP_PEERS") {
        for raw in env_peers.split(|c: char| c == ',' || c == ';' || c.is_whitespace()) {
            let peer = raw.trim();
            if peer.is_empty() {
                continue;
            }
            if !peers.iter().any(|existing| existing == peer) {
                peers.push(peer.to_string());
            }
        }
    }
    peers
}

async fn spawn_catalog_announcer(
    node: &MoqNode,
    track: &TrackName,
    broadcast_name: &str,
    track_name: &str,
    peers: Vec<String>,
) -> Result<tokio::sync::mpsc::UnboundedSender<ManifestSummary>> {
    let endpoint = serde_json::to_string(&node.endpoint_addr())
        .unwrap_or_else(|_| node.endpoint().id().to_string());
    let track = track.clone();
    let broadcast_name = broadcast_name.to_string();
    let track_name = track_name.to_string();
    let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<ManifestSummary>();
    let endpoint_clone = endpoint.clone();
    let node_endpoint = node.endpoint().clone();

    tokio::spawn(async move {
        let mut gossip = match ec_iroh::CatalogGossip::join(node_endpoint, &peers).await {
            Ok(gossip) => gossip,
            Err(err) => {
                tracing::warn!("catalog gossip join failed: {err:#}");
                return;
            }
        };
        let entry = build_catalog_entry(
            endpoint_clone.clone(),
            &track,
            &broadcast_name,
            &track_name,
            None,
        );
        if let Err(err) = gossip.announce(entry).await {
            tracing::warn!("catalog announce failed: {err:#}");
        }
        while let Some(summary) = rx.recv().await {
            let entry = build_catalog_entry(
                endpoint_clone.clone(),
                &track,
                &broadcast_name,
                &track_name,
                Some(summary),
            );
            if let Err(err) = gossip.announce(entry).await {
                tracing::warn!("catalog update failed: {err:#}");
            }
        }
    });

    Ok(tx)
}

fn build_catalog_entry(
    endpoint: String,
    track: &TrackName,
    broadcast_name: &str,
    track_name: &str,
    manifest: Option<ManifestSummary>,
) -> StreamCatalogEntry {
    let stream = StreamDescriptor {
        id: StreamId(track.name.clone()),
        title: track.name.clone(),
        number: None,
        source: "moq".to_string(),
        metadata: vec![StreamMetadata {
            key: "broadcast".to_string(),
            value: broadcast_name.to_string(),
        }],
        commitments: Vec::new(),
    };
    let mut stream = stream;
    stream.commitments = stream_descriptor_commitments(&stream);

    let encryption = StreamEncryptionInfo {
        alg: ENCRYPTION_ALG.to_string(),
        key_id: track.name.clone(),
        nonce_scheme: "stream-id+chunk-index".to_string(),
    };

    let moq = MoqStreamDescriptor {
        endpoint,
        broadcast_name: broadcast_name.to_string(),
        track_name: track_name.to_string(),
        encryption: Some(encryption),
    };

    StreamCatalogEntry {
        stream,
        moq: Some(moq),
        manifest,
        updated_unix_ms: SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_millis() as u64,
    }
}

fn now_unix_ms() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as u64
}

fn build_control_announcement(
    stream_id: String,
    title: String,
    transports: Vec<StreamTransportDescriptor>,
    ttl_ms: u64,
) -> StreamControlAnnouncement {
    let mut stream = StreamDescriptor {
        id: StreamId(stream_id.clone()),
        title,
        number: None,
        source: "control".to_string(),
        metadata: vec![StreamMetadata {
            key: "stream_id".to_string(),
            value: stream_id,
        }],
        commitments: Vec::new(),
    };
    stream.commitments = stream_descriptor_commitments(&stream);

    let mut announcement = StreamControlAnnouncement {
        stream,
        transports,
        updated_unix_ms: now_unix_ms(),
        ttl_ms,
        commitments: Vec::new(),
    };
    announcement.commitments = control_announcement_commitments(&announcement);
    announcement
}

async fn spawn_control_announcer_task(
    endpoint: iroh::Endpoint,
    peers: Vec<String>,
    mut announcement: StreamControlAnnouncement,
    interval: Duration,
) -> Result<oneshot::Sender<()>> {
    let mut gossip = tokio::time::timeout(
        Duration::from_secs(10),
        ec_iroh::ControlGossip::join(endpoint, &peers),
    )
    .await
    .context("timed out joining control gossip topic")??;
    let (stop_tx, mut stop_rx) = oneshot::channel::<()>();

    tokio::spawn(async move {
        let mut ticker = tokio::time::interval(interval.max(Duration::from_secs(1)));
        ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);

        loop {
            announcement.updated_unix_ms = now_unix_ms();
            if let Err(err) = gossip.announce(announcement.clone()).await {
                tracing::warn!("control announce failed: {err:#}");
            }

            tokio::select! {
                _ = ticker.tick() => {}
                _ = &mut stop_rx => break,
            }
        }
    });

    Ok(stop_tx)
}

async fn control_announce(args: ControlAnnounceArgs) -> Result<()> {
    let secret = parse_iroh_secret(args.iroh_secret)?;
    let discovery = parse_discovery(args.discovery.as_deref())?;
    let endpoint = ec_iroh::build_endpoint(secret, discovery).await?;
    let gossip_peers = parse_gossip_peers(args.gossip_peer.clone());
    let endpoint_addr =
        serde_json::to_string(&endpoint.addr()).unwrap_or_else(|_| endpoint.id().to_string());

    let mut transports = Vec::new();

    if let (Some(url), Some(broadcast_name)) =
        (args.relay_url.as_ref(), args.relay_broadcast.as_ref())
    {
        transports.push(StreamTransportDescriptor::RelayMoq {
            url: url.clone(),
            broadcast_name: broadcast_name.clone(),
            track_name: args.relay_track.clone(),
        });
    }

    if let Some(broadcast_name) = args.direct_broadcast.as_ref() {
        let endpoint_addr = args
            .direct_endpoint
            .clone()
            .unwrap_or_else(|| endpoint.id().to_string());
        transports.push(StreamTransportDescriptor::IrohDirect {
            endpoint: endpoint_addr,
            broadcast_name: broadcast_name.clone(),
            track_name: args.direct_track.clone(),
        });
    }

    if transports.is_empty() {
        return Err(anyhow!(
            "no transports configured; set relay (--relay-url + --relay-broadcast) and/or direct (--direct-broadcast)"
        ));
    }

    let title = args.title.clone().unwrap_or_else(|| args.stream_id.clone());
    let announcement =
        build_control_announcement(args.stream_id.clone(), title, transports, args.ttl_ms);

    let stop_tx = spawn_control_announcer_task(
        endpoint.clone(),
        gossip_peers,
        announcement,
        Duration::from_millis(args.interval_ms.max(1000)),
    )
    .await?;

    eprintln!("control endpoint id: {}", endpoint.id());
    eprintln!("control endpoint addr: {}", endpoint_addr);
    eprintln!("control stream_id: {}", args.stream_id);
    eprintln!(
        "control announce interval_ms: {}",
        args.interval_ms.max(1000)
    );
    eprintln!("control ttl_ms: {}", args.ttl_ms);

    tokio::signal::ctrl_c().await?;
    let _ = stop_tx.send(());
    tokio::time::sleep(Duration::from_millis(100)).await;
    Ok(())
}

async fn control_listen(args: ControlListenArgs) -> Result<()> {
    let secret = parse_iroh_secret(args.iroh_secret)?;
    let discovery = parse_discovery(args.discovery.as_deref())?;
    let endpoint = ec_iroh::build_endpoint(secret, discovery).await?;
    let gossip_peers = parse_gossip_peers(args.gossip_peer.clone());
    eprintln!("control endpoint id: {}", endpoint.id());
    eprintln!(
        "control endpoint addr: {}",
        serde_json::to_string(&endpoint.addr()).unwrap_or_else(|_| endpoint.id().to_string())
    );
    let mut gossip = tokio::time::timeout(
        Duration::from_secs(30),
        ec_iroh::ControlGossip::join(endpoint.clone(), &gossip_peers),
    )
    .await
    .context("timed out joining control gossip topic")??;

    loop {
        tokio::select! {
            maybe = gossip.next_announcement() => {
                let Some(announcement) = maybe? else {
                    continue;
                };
                println!("{}", serde_json::to_string(&announcement)?);
                if args.once {
                    break;
                }
            }
            _ = tokio::signal::ctrl_c() => {
                break;
            }
        }
    }

    Ok(())
}

fn control_announcement_is_fresh(
    announcement: &StreamControlAnnouncement,
    max_age_ms: u64,
) -> bool {
    let now = now_unix_ms();
    let freshness_ms = announcement.ttl_ms.min(max_age_ms.max(1));
    now <= announcement.updated_unix_ms.saturating_add(freshness_ms)
}

fn select_control_transport(
    transports: &[StreamTransportDescriptor],
    prefer: ControlTransportPreference,
) -> Option<StreamTransportDescriptor> {
    let is_direct =
        |t: &StreamTransportDescriptor| matches!(t, StreamTransportDescriptor::IrohDirect { .. });
    let is_relay =
        |t: &StreamTransportDescriptor| matches!(t, StreamTransportDescriptor::RelayMoq { .. });

    match prefer {
        ControlTransportPreference::DirectOnly => transports.iter().find(|t| is_direct(t)).cloned(),
        ControlTransportPreference::RelayOnly => transports.iter().find(|t| is_relay(t)).cloned(),
        ControlTransportPreference::DirectFirst => transports
            .iter()
            .find(|t| is_direct(t))
            .cloned()
            .or_else(|| transports.iter().find(|t| is_relay(t)).cloned()),
        ControlTransportPreference::RelayFirst => transports
            .iter()
            .find(|t| is_relay(t))
            .cloned()
            .or_else(|| transports.iter().find(|t| is_direct(t)).cloned()),
    }
}

async fn control_resolve(args: ControlResolveArgs) -> Result<()> {
    let secret = parse_iroh_secret(args.iroh_secret)?;
    let discovery = parse_discovery(args.discovery.as_deref())?;
    let endpoint = ec_iroh::build_endpoint(secret, discovery).await?;
    let gossip_peers = parse_gossip_peers(args.gossip_peer.clone());
    eprintln!("control endpoint id: {}", endpoint.id());
    eprintln!(
        "control endpoint addr: {}",
        serde_json::to_string(&endpoint.addr()).unwrap_or_else(|_| endpoint.id().to_string())
    );

    let mut gossip = tokio::time::timeout(
        Duration::from_secs(30),
        ec_iroh::ControlGossip::join(endpoint.clone(), &gossip_peers),
    )
    .await
    .context("timed out joining control gossip topic")??;

    let timeout = Duration::from_millis(args.timeout_ms.max(1000));
    let deadline = Instant::now() + timeout;

    loop {
        let now = Instant::now();
        if now >= deadline {
            break;
        }
        let remaining = deadline - now;

        let maybe = tokio::time::timeout(remaining, gossip.next_announcement())
            .await
            .context("timed out waiting for control announcement")??;
        let Some(announcement) = maybe else {
            continue;
        };
        if announcement.stream.id.0.as_str() != args.stream_id {
            continue;
        }
        if !control_announcement_is_fresh(&announcement, args.max_age_ms) {
            continue;
        }
        let Some(transport) = select_control_transport(&announcement.transports, args.prefer)
        else {
            continue;
        };

        let stream_id = announcement.stream.id.0.clone();
        let title = announcement.stream.title.clone();

        let output = if args.include_announcement {
            serde_json::json!({
                "stream_id": stream_id,
                "title": title,
                "transport": transport,
                "updated_unix_ms": announcement.updated_unix_ms,
                "ttl_ms": announcement.ttl_ms,
                "announcement": announcement,
            })
        } else {
            serde_json::json!({
                "stream_id": stream_id,
                "title": title,
                "transport": transport,
                "updated_unix_ms": announcement.updated_unix_ms,
                "ttl_ms": announcement.ttl_ms,
            })
        };
        println!("{}", serde_json::to_string(&output)?);
        return Ok(());
    }

    Err(anyhow!(
        "timed out resolving stream_id '{}' from control topic",
        args.stream_id
    ))
}

fn select_relay_transport_for_web(
    transports: &[StreamTransportDescriptor],
) -> Option<(String, String, String)> {
    for transport in transports {
        if let StreamTransportDescriptor::RelayMoq {
            url,
            broadcast_name,
            track_name,
        } = transport
        {
            return Some((url.clone(), broadcast_name.clone(), track_name.clone()));
        }
    }
    None
}

#[derive(Debug, serde::Serialize)]
struct WebStreamUpsertReq<'a> {
    stream_id: &'a str,
    title: &'a str,
    relay_url: &'a str,
    broadcast_name: &'a str,
    track_name: &'a str,
    expires_ms: u64,
}

async fn control_bridge_web(args: ControlBridgeWebArgs) -> Result<()> {
    let secret = parse_iroh_secret(args.iroh_secret)?;
    let discovery = parse_discovery(args.discovery.as_deref())?;
    let endpoint = ec_iroh::build_endpoint(secret, discovery).await?;
    let gossip_peers = parse_gossip_peers(args.gossip_peer.clone());
    eprintln!("control endpoint id: {}", endpoint.id());
    eprintln!(
        "control endpoint addr: {}",
        serde_json::to_string(&endpoint.addr()).unwrap_or_else(|_| endpoint.id().to_string())
    );

    let mut gossip = tokio::time::timeout(
        Duration::from_secs(30),
        ec_iroh::ControlGossip::join(endpoint.clone(), &gossip_peers),
    )
    .await
    .context("timed out joining control gossip topic")??;

    let token = args
        .auth_token
        .or_else(|| std::env::var("EVERY_CHANNEL_WEB_UPSERT_TOKEN").ok())
        .map(|v| v.trim().to_string())
        .filter(|v| !v.is_empty());
    let directory_url = args.directory_url.trim_end_matches('/').to_string();
    let upsert_url = format!("{directory_url}/api/stream-upsert");
    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(8))
        .build()
        .context("failed to build web bridge http client")?;
    let mut last_upserted_unix_ms = HashMap::<String, u64>::new();
    let timeout_deadline = if args.timeout_ms == 0 {
        None
    } else {
        Some(Instant::now() + Duration::from_millis(args.timeout_ms.max(1000)))
    };

    eprintln!("web upsert url: {}", upsert_url);

    loop {
        let maybe = if let Some(deadline) = timeout_deadline {
            let now = Instant::now();
            if now >= deadline {
                return Err(anyhow!("timed out waiting for control announcements"));
            }
            tokio::time::timeout(deadline - now, gossip.next_announcement())
                .await
                .context("timed out waiting for control announcement")??
        } else {
            gossip.next_announcement().await?
        };
        let Some(announcement) = maybe else {
            continue;
        };

        if !control_announcement_is_fresh(&announcement, args.max_age_ms) {
            continue;
        }

        let stream_id = announcement.stream.id.0.clone();
        if let Some(prefix) = args.stream_prefix.as_deref() {
            if !stream_id.starts_with(prefix) {
                continue;
            }
        }

        let Some((relay_url, broadcast_name, track_name)) =
            select_relay_transport_for_web(&announcement.transports)
        else {
            continue;
        };

        if last_upserted_unix_ms
            .get(&stream_id)
            .is_some_and(|seen| *seen >= announcement.updated_unix_ms)
        {
            continue;
        }

        let ttl_ms = announcement.ttl_ms.clamp(5_000, 60_000);
        let payload = WebStreamUpsertReq {
            stream_id: &stream_id,
            title: &announcement.stream.title,
            relay_url: &relay_url,
            broadcast_name: &broadcast_name,
            track_name: &track_name,
            expires_ms: now_unix_ms().saturating_add(ttl_ms),
        };

        let mut req = client.post(&upsert_url).json(&payload);
        if let Some(token) = token.as_ref() {
            req = req.bearer_auth(token);
        }
        let res = req
            .send()
            .await
            .with_context(|| format!("failed posting to {upsert_url}"))?;
        if !res.status().is_success() {
            let status = res.status();
            let body = res.text().await.unwrap_or_default();
            tracing::warn!(
                stream = %stream_id,
                status = %status,
                body = %body,
                "web stream upsert failed"
            );
            continue;
        }

        last_upserted_unix_ms.insert(stream_id.clone(), announcement.updated_unix_ms);
        tracing::info!(
            stream = %stream_id,
            relay = %relay_url,
            broadcast = %broadcast_name,
            "web stream upserted"
        );
        if args.once {
            break;
        }
    }

    Ok(())
}

fn wait_for_stable_file(path: &Path, timeout: Duration) -> Result<()> {
    let start = Instant::now();
    let mut last_len: Option<u64> = None;
    let mut stable_ms: u64 = 0;

    // We want "write is done" behavior. File size staying constant for a few polls
    // is a pragmatic signal that ffmpeg is done producing the segment.
    while start.elapsed() < timeout {
        if let Ok(meta) = fs::metadata(path) {
            let len = meta.len();
            if len > 0 {
                if Some(len) == last_len {
                    stable_ms += 100;
                    if stable_ms >= 300 {
                        return Ok(());
                    }
                } else {
                    last_len = Some(len);
                    stable_ms = 0;
                }
            }
        }
        std::thread::sleep(Duration::from_millis(100));
    }

    Err(anyhow!(
        "timed out waiting for stable file {} after {:?}",
        path.display(),
        timeout
    ))
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct ArchiveRecordMediaTiming {
    track_id: u32,
    timescale: u32,
    decode_time: u64,
    sequence: u64,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct ArchiveIndexRecord {
    received_unix_ms: u64,
    relay_url: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    source_node: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    source_session: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    media_timing: Option<ArchiveRecordMediaTiming>,
    broadcast_name: String,
    track_name: String,
    group_sequence: u64,
    frame_count: u64,
    size_bytes: usize,
    blake3: String,
    cas_path: String,
}

fn sanitize_path_component(value: &str) -> String {
    let mut out = String::with_capacity(value.len());
    for ch in value.chars() {
        if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' || ch == '.' {
            out.push(ch);
        } else {
            out.push('_');
        }
    }
    if out.is_empty() {
        "_".to_string()
    } else {
        out
    }
}

fn default_wt_archive_tracks() -> Vec<String> {
    WT_ARCHIVE_DEFAULT_TRACKS
        .iter()
        .map(|value| (*value).to_string())
        .collect()
}

fn non_empty_trimmed_string(value: &str) -> Option<String> {
    let trimmed = value.trim();
    (!trimmed.is_empty()).then(|| trimmed.to_string())
}

fn archive_machine_identity_suffix() -> Option<String> {
    [
        "/etc/machine-id",
        "/var/lib/dbus/machine-id",
        "/proc/sys/kernel/random/boot_id",
    ]
    .into_iter()
    .find_map(|path| {
        let value = fs::read_to_string(path).ok()?;
        let trimmed = value.trim();
        if trimmed.is_empty() {
            return None;
        }
        let hash = blake3::hash(trimmed.as_bytes()).to_hex().to_string();
        Some(hash[..12].to_string())
    })
}

fn archive_host_source_node(hostname: &str) -> Option<String> {
    let hostname = non_empty_trimmed_string(hostname)?;
    let sanitized = sanitize_path_component(&hostname);
    archive_machine_identity_suffix()
        .map(|suffix| format!("{sanitized}-{suffix}"))
        .or(Some(sanitized))
}

fn archive_source_node(explicit: Option<&str>) -> String {
    explicit
        .and_then(non_empty_trimmed_string)
        .or_else(|| std::env::var("EVERY_CHANNEL_ARCHIVE_SOURCE_NODE").ok())
        .and_then(|value| non_empty_trimmed_string(&value))
        .or_else(|| std::env::var("EVERY_CHANNEL_NODE_NAME").ok())
        .and_then(|value| archive_host_source_node(&value))
        .or_else(|| std::env::var("HOSTNAME").ok())
        .and_then(|value| archive_host_source_node(&value))
        .unwrap_or_else(|| "unknown".to_string())
}

fn archive_source_session(source_node: &str) -> String {
    format!(
        "{}-{}-{}",
        sanitize_path_component(source_node),
        std::process::id(),
        now_unix_ms()
    )
}

fn cas_store_blob(cas_root: &Path, data: &[u8]) -> Result<(String, PathBuf, bool)> {
    let hash = blake3::hash(data).to_hex().to_string();
    let shard = &hash[0..2];
    let rel_path = PathBuf::from("objects")
        .join("blake3")
        .join(shard)
        .join(format!("{hash}.bin"));
    let out_path = cas_root.join(shard).join(format!("{hash}.bin"));

    if out_path.exists() {
        return Ok((hash, rel_path, false));
    }

    let parent = out_path
        .parent()
        .ok_or_else(|| anyhow!("invalid CAS path: {}", out_path.display()))?;
    fs::create_dir_all(parent)
        .with_context(|| format!("failed to create CAS shard dir {}", parent.display()))?;

    let tmp = parent.join(format!(
        ".{}.tmp-{}-{}",
        out_path
            .file_name()
            .and_then(|f| f.to_str())
            .unwrap_or("blob"),
        std::process::id(),
        SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos()
    ));
    fs::write(&tmp, data).with_context(|| format!("failed to write {}", tmp.display()))?;
    match fs::rename(&tmp, &out_path) {
        Ok(()) => {}
        Err(err) if out_path.exists() => {
            let _ = fs::remove_file(&tmp);
            tracing::debug!(
                path = %out_path.display(),
                err = %err,
                "CAS object already exists during rename race"
            );
            return Ok((hash, rel_path, false));
        }
        Err(err) => {
            let _ = fs::remove_file(&tmp);
            return Err(err).with_context(|| {
                format!(
                    "failed to move CAS blob {} -> {}",
                    tmp.display(),
                    out_path.display()
                )
            });
        }
    }

    Ok((hash, rel_path, true))
}

fn append_archive_index_record(path: &Path, record: &ArchiveIndexRecord) -> Result<()> {
    let parent = path
        .parent()
        .ok_or_else(|| anyhow!("invalid manifest path: {}", path.display()))?;
    fs::create_dir_all(parent)
        .with_context(|| format!("failed to create manifest dir {}", parent.display()))?;

    let mut file = std::fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open(path)
        .with_context(|| format!("failed to open manifest {}", path.display()))?;
    serde_json::to_writer(&mut file, record)
        .with_context(|| format!("failed to write manifest {}", path.display()))?;
    file.write_all(b"\n")
        .with_context(|| format!("failed to newline-terminate {}", path.display()))?;
    Ok(())
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct Mp4TopLevelBox {
    kind: [u8; 4],
    size: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct Mp4BoxSpan {
    kind: [u8; 4],
    start: usize,
    size: usize,
    header_size: usize,
}

impl Mp4BoxSpan {
    fn payload_range(&self) -> std::ops::Range<usize> {
        (self.start + self.header_size)..(self.start + self.size)
    }
}

fn mp4_box_at(data: &[u8], offset: usize) -> Result<Option<Mp4BoxSpan>> {
    if data.len().saturating_sub(offset) < 8 {
        return Ok(None);
    }
    let size32 = u32::from_be_bytes([
        data[offset],
        data[offset + 1],
        data[offset + 2],
        data[offset + 3],
    ]);
    let mut header_size = 8usize;
    let box_size = if size32 == 1 {
        if data.len().saturating_sub(offset) < 16 {
            return Ok(None);
        }
        header_size = 16;
        u64::from_be_bytes([
            data[offset + 8],
            data[offset + 9],
            data[offset + 10],
            data[offset + 11],
            data[offset + 12],
            data[offset + 13],
            data[offset + 14],
            data[offset + 15],
        ])
    } else if size32 == 0 {
        return Ok(None);
    } else {
        u64::from(size32)
    };
    if box_size < header_size as u64 {
        return Err(anyhow!("invalid MP4 box size"));
    }
    let size = usize::try_from(box_size).context("MP4 box too large")?;
    let end = offset
        .checked_add(size)
        .ok_or_else(|| anyhow!("MP4 box offset overflow"))?;
    if data.len() < end {
        return Ok(None);
    }
    Ok(Some(Mp4BoxSpan {
        kind: [
            data[offset + 4],
            data[offset + 5],
            data[offset + 6],
            data[offset + 7],
        ],
        start: offset,
        size,
        header_size,
    }))
}

fn mp4_top_level_box(data: &[u8]) -> Result<Option<Mp4TopLevelBox>> {
    Ok(mp4_box_at(data, 0)?.map(|span| Mp4TopLevelBox {
        kind: span.kind,
        size: span.size,
    }))
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct Fmp4InitAndMedia {
    init: Vec<u8>,
    media: Vec<Vec<u8>>,
}

fn split_fmp4_init_and_media(data: &[u8]) -> Result<Fmp4InitAndMedia> {
    let mut offset = 0usize;
    let mut init_end = None::<usize>;
    let mut media = Vec::new();

    while offset < data.len() {
        let Some(span) = mp4_box_at(data, offset)? else {
            break;
        };
        let end = span.start + span.size;
        if span.kind == *b"moof" {
            init_end.get_or_insert(span.start);
            let mut fragment = data[span.start..end].to_vec();
            offset = end;
            while offset < data.len() {
                let Some(next) = mp4_box_at(data, offset)? else {
                    break;
                };
                let next_end = next.start + next.size;
                if next.kind == *b"moof" {
                    break;
                }
                fragment.extend_from_slice(&data[next.start..next_end]);
                offset = next_end;
                if next.kind == *b"mdat" {
                    break;
                }
            }
            media.push(fragment);
            continue;
        }
        offset = end;
    }

    let init_end = init_end.ok_or_else(|| anyhow!("fMP4 output has no media fragment"))?;
    if init_end == 0 {
        return Err(anyhow!("fMP4 output has media before init"));
    }
    if media.is_empty() {
        return Err(anyhow!("fMP4 output has no media fragments"));
    }

    Ok(Fmp4InitAndMedia {
        init: data[..init_end].to_vec(),
        media,
    })
}

fn mp4_children(data: &[u8], range: std::ops::Range<usize>) -> Result<Vec<Mp4BoxSpan>> {
    let mut children = Vec::new();
    let mut offset = range.start;
    while offset < range.end {
        let Some(child) = mp4_box_at(data, offset)? else {
            break;
        };
        let child_end = child.start + child.size;
        if child_end > range.end {
            return Err(anyhow!("nested MP4 box exceeds parent bounds"));
        }
        offset = child_end;
        children.push(child);
    }
    Ok(children)
}

fn read_be_u32(data: &[u8], offset: usize) -> Option<u32> {
    (data.len().saturating_sub(offset) >= 4).then(|| {
        u32::from_be_bytes([
            data[offset],
            data[offset + 1],
            data[offset + 2],
            data[offset + 3],
        ])
    })
}

fn read_be_u64(data: &[u8], offset: usize) -> Option<u64> {
    (data.len().saturating_sub(offset) >= 8).then(|| {
        u64::from_be_bytes([
            data[offset],
            data[offset + 1],
            data[offset + 2],
            data[offset + 3],
            data[offset + 4],
            data[offset + 5],
            data[offset + 6],
            data[offset + 7],
        ])
    })
}

fn write_be_u32(data: &mut [u8], offset: usize, value: u32) -> bool {
    if data.len().saturating_sub(offset) < 4 {
        return false;
    }
    data[offset..offset + 4].copy_from_slice(&value.to_be_bytes());
    true
}

fn write_be_u64(data: &mut [u8], offset: usize, value: u64) -> bool {
    if data.len().saturating_sub(offset) < 8 {
        return false;
    }
    data[offset..offset + 8].copy_from_slice(&value.to_be_bytes());
    true
}

fn mp4_full_box_version(data: &[u8], span: &Mp4BoxSpan) -> Option<u8> {
    data.get(span.payload_range().start).copied()
}

fn mp4_tkhd_track_id(data: &[u8], tkhd: &Mp4BoxSpan) -> Option<u32> {
    let payload_start = tkhd.payload_range().start;
    let version = mp4_full_box_version(data, tkhd)?;
    let track_id_offset = match version {
        0 => payload_start + 12,
        1 => payload_start + 20,
        _ => return None,
    };
    read_be_u32(data, track_id_offset)
}

fn mp4_mdhd_timescale(data: &[u8], mdhd: &Mp4BoxSpan) -> Option<u32> {
    let payload_start = mdhd.payload_range().start;
    let version = mp4_full_box_version(data, mdhd)?;
    let timescale_offset = match version {
        0 => payload_start + 12,
        1 => payload_start + 20,
        _ => return None,
    };
    read_be_u32(data, timescale_offset).filter(|timescale| *timescale > 0)
}

fn mp4_init_track_timescales(moov: &[u8]) -> Result<BTreeMap<u32, u32>> {
    let moov_span = mp4_box_at(moov, 0)?.ok_or_else(|| anyhow!("missing moov box"))?;
    if moov_span.kind != *b"moov" {
        return Ok(BTreeMap::new());
    }
    let mut timescales = BTreeMap::new();
    for trak in mp4_children(moov, moov_span.payload_range())? {
        if trak.kind != *b"trak" {
            continue;
        }
        let mut track_id = None;
        let mut timescale = None;
        for trak_child in mp4_children(moov, trak.payload_range())? {
            match &trak_child.kind {
                b"tkhd" => track_id = mp4_tkhd_track_id(moov, &trak_child),
                b"mdia" => {
                    for mdia_child in mp4_children(moov, trak_child.payload_range())? {
                        if mdia_child.kind == *b"mdhd" {
                            timescale = mp4_mdhd_timescale(moov, &mdia_child);
                        }
                    }
                }
                _ => {}
            }
        }
        if let (Some(track_id), Some(timescale)) = (track_id, timescale) {
            timescales.insert(track_id, timescale);
        }
    }
    Ok(timescales)
}

fn mp4_tfhd_track_id(data: &[u8], tfhd: &Mp4BoxSpan) -> Option<u32> {
    read_be_u32(data, tfhd.payload_range().start + 4)
}

fn mp4_tfdt_base_media_decode_time(data: &[u8], tfdt: &Mp4BoxSpan) -> Option<u64> {
    let payload_start = tfdt.payload_range().start;
    let version = mp4_full_box_version(data, tfdt)?;
    match version {
        0 => read_be_u32(data, payload_start + 4).map(u64::from),
        1 => read_be_u64(data, payload_start + 4),
        _ => None,
    }
}

fn fmp4_fragment_decode_time(bytes: &[u8]) -> Result<Option<(u32, u64)>> {
    let Some(moof) = mp4_box_at(bytes, 0)? else {
        return Ok(None);
    };
    if moof.kind != *b"moof" {
        return Ok(None);
    }
    for moof_child in mp4_children(bytes, moof.payload_range())? {
        if moof_child.kind != *b"traf" {
            continue;
        }
        let mut track_id = None;
        let mut base_media_decode_time = None;
        for traf_child in mp4_children(bytes, moof_child.payload_range())? {
            match &traf_child.kind {
                b"tfhd" => track_id = mp4_tfhd_track_id(bytes, &traf_child),
                b"tfdt" => {
                    base_media_decode_time = mp4_tfdt_base_media_decode_time(bytes, &traf_child)
                }
                _ => {}
            }
        }
        if let (Some(track_id), Some(base_media_decode_time)) = (track_id, base_media_decode_time) {
            return Ok(Some((track_id, base_media_decode_time)));
        }
    }
    Ok(None)
}

fn fmp4_media_time_sequence(
    base_media_decode_time: u64,
    timescale: u32,
    segment_duration_ms: u64,
) -> Option<u64> {
    if timescale == 0 || segment_duration_ms == 0 {
        return None;
    }
    let denominator = u128::from(timescale) * u128::from(segment_duration_ms);
    if denominator == 0 {
        return None;
    }
    let numerator = u128::from(base_media_decode_time) * 1000;
    let rounded = (numerator + (denominator / 2)) / denominator;
    u64::try_from(rounded).ok()
}

fn fmp4_decode_time_for_media_sequence(
    media_sequence: u64,
    timescale: u32,
    segment_duration_ms: u64,
) -> Option<u64> {
    if timescale == 0 || segment_duration_ms == 0 {
        return None;
    }
    let numerator = u128::from(media_sequence)
        .saturating_mul(u128::from(timescale))
        .saturating_mul(u128::from(segment_duration_ms));
    u64::try_from((numerator + 500) / 1000).ok()
}

fn rewrite_fmp4_fragment_decode_time(
    bytes: &mut [u8],
    replacement_decode_time: u64,
) -> Result<bool> {
    let Some(moof) = mp4_box_at(bytes, 0)? else {
        return Ok(false);
    };
    if moof.kind != *b"moof" {
        return Ok(false);
    }
    for moof_child in mp4_children(bytes, moof.payload_range())? {
        if moof_child.kind != *b"traf" {
            continue;
        }
        for traf_child in mp4_children(bytes, moof_child.payload_range())? {
            if traf_child.kind != *b"tfdt" {
                continue;
            }
            let payload_start = traf_child.payload_range().start;
            let Some(version) = mp4_full_box_version(bytes, &traf_child) else {
                return Ok(false);
            };
            return Ok(match version {
                0 => u32::try_from(replacement_decode_time)
                    .ok()
                    .is_some_and(|value| write_be_u32(bytes, payload_start + 4, value)),
                1 => write_be_u64(bytes, payload_start + 4, replacement_decode_time),
                _ => false,
            });
        }
    }
    Ok(false)
}

const PUBLISHER_ARCHIVE_EPOCH_BUCKET_STRIDE: u64 = 4096;
const PUBLISHER_PROOF_SUBFRAGMENT_SLOT_RETENTION: usize = 8192;

#[derive(Debug)]
struct PublisherArchiveWriter {
    relay_url: String,
    broadcast_name: String,
    track_name: String,
    source_node: String,
    source_session: String,
    cas_root: PathBuf,
    manifest_path: PathBuf,
    buffer: Vec<u8>,
    pending_moof: Option<Vec<u8>>,
    track_timescales: BTreeMap<u32, u32>,
    media_sequence_origins: BTreeMap<u32, u64>,
    media_sequence_fragment_counts: BTreeMap<(u32, u64), u64>,
    segment_duration_ms: u64,
    next_sequence: u64,
    archived_fragments: u64,
}

impl PublisherArchiveWriter {
    #[cfg(test)]
    fn new(
        output_dir: &Path,
        manifest_dir: Option<&Path>,
        relay_url: &str,
        broadcast_name: &str,
        track_name: &str,
        segment_duration_ms: u64,
        source_node: String,
    ) -> Self {
        let manifest_root = manifest_dir
            .map(Path::to_path_buf)
            .unwrap_or_else(|| output_dir.join("manifests"));
        let source_session = archive_source_session(&source_node);
        Self {
            relay_url: relay_url.to_string(),
            broadcast_name: broadcast_name.to_string(),
            track_name: track_name.to_string(),
            source_node,
            source_session,
            cas_root: output_dir.join("objects").join("blake3"),
            manifest_path: archive_index_path(&manifest_root, broadcast_name, track_name),
            buffer: Vec::new(),
            pending_moof: None,
            track_timescales: BTreeMap::new(),
            media_sequence_origins: BTreeMap::new(),
            media_sequence_fragment_counts: BTreeMap::new(),
            segment_duration_ms,
            next_sequence: 0,
            archived_fragments: 0,
        }
    }

    fn observe_bytes(&mut self, bytes: &[u8]) -> Result<()> {
        self.buffer.extend_from_slice(bytes);

        while let Some(top_box) = mp4_top_level_box(&self.buffer)? {
            let raw = self.buffer.drain(..top_box.size).collect::<Vec<_>>();
            match &top_box.kind {
                b"moof" => {
                    if self.pending_moof.replace(raw).is_some() {
                        return Err(anyhow!(
                            "publisher-origin fMP4 saw moof before matching mdat"
                        ));
                    }
                }
                b"mdat" => {
                    let Some(mut moof) = self.pending_moof.take() else {
                        continue;
                    };
                    moof.extend_from_slice(&raw);
                    self.archive_fragment(moof)?;
                }
                b"moov" => {
                    let timescales = mp4_init_track_timescales(&raw)?;
                    if !timescales.is_empty() {
                        self.track_timescales.extend(timescales);
                    }
                }
                _ => {}
            }
        }

        Ok(())
    }

    fn fragment_media_sequence(&self, bytes: &[u8]) -> Result<Option<ArchiveRecordMediaTiming>> {
        let Some((track_id, base_media_decode_time)) = fmp4_fragment_decode_time(bytes)? else {
            return Ok(None);
        };
        let Some(timescale) = self.track_timescales.get(&track_id).copied() else {
            return Ok(None);
        };
        Ok(
            fmp4_media_time_sequence(base_media_decode_time, timescale, self.segment_duration_ms)
                .map(|sequence| ArchiveRecordMediaTiming {
                    track_id,
                    timescale,
                    decode_time: base_media_decode_time,
                    sequence,
                }),
        )
    }

    fn group_sequence_for_media_timing_subfragment(
        timing: &ArchiveRecordMediaTiming,
        subfragment_index: u64,
    ) -> u64 {
        let track_slot_base = (u64::from(timing.track_id) % 16) * 256;
        let track_slot = (track_slot_base + subfragment_index.min(255))
            .min(PUBLISHER_ARCHIVE_EPOCH_BUCKET_STRIDE - 1);
        timing
            .sequence
            .saturating_mul(PUBLISHER_ARCHIVE_EPOCH_BUCKET_STRIDE)
            .saturating_add(track_slot)
    }

    fn wallclock_media_sequence_for_received(&self, received_unix_ms: u64) -> Option<u64> {
        (self.segment_duration_ms > 0).then(|| received_unix_ms / self.segment_duration_ms)
    }

    fn normalize_fragment_media_timing(
        &mut self,
        bytes: &mut [u8],
        media_timing: Option<ArchiveRecordMediaTiming>,
        received_unix_ms: u64,
    ) -> Result<Option<ArchiveRecordMediaTiming>> {
        let Some(mut timing) = media_timing else {
            return Ok(None);
        };
        let Some(observed_sequence) = self.wallclock_media_sequence_for_received(received_unix_ms)
        else {
            return Ok(Some(timing));
        };
        let original_sequence = timing.sequence;
        let origin_sequence = self
            .media_sequence_origins
            .entry(timing.track_id)
            .or_insert_with(|| observed_sequence.saturating_sub(original_sequence));
        let sequence = origin_sequence.saturating_add(original_sequence);
        let Some(decode_time) = fmp4_decode_time_for_media_sequence(
            sequence,
            timing.timescale,
            self.segment_duration_ms,
        ) else {
            return Ok(Some(timing));
        };
        if rewrite_fmp4_fragment_decode_time(bytes, decode_time)? {
            timing.sequence = sequence;
            timing.decode_time = decode_time;
        }
        Ok(Some(timing))
    }

    fn archive_fragment(&mut self, bytes: Vec<u8>) -> Result<()> {
        let mut bytes = bytes;
        let media_sequence = self.fragment_media_sequence(&bytes)?;
        let received_unix_ms = now_unix_ms();
        let media_sequence =
            self.normalize_fragment_media_timing(&mut bytes, media_sequence, received_unix_ms)?;
        let candidate_sequence = if let Some(timing) = media_sequence.as_ref() {
            let counter = self
                .media_sequence_fragment_counts
                .entry((timing.track_id, timing.sequence))
                .or_default();
            let group_sequence =
                Self::group_sequence_for_media_timing_subfragment(timing, *counter);
            *counter = counter.saturating_add(1);
            group_sequence
        } else {
            self.next_sequence
        };
        let group_sequence = if media_sequence.is_some() {
            candidate_sequence
        } else {
            candidate_sequence.max(self.next_sequence)
        };
        self.next_sequence = group_sequence.saturating_add(1);
        let size_bytes = bytes.len();
        let (hash, rel_path, inserted) = cas_store_blob(&self.cas_root, &bytes)?;
        let record = ArchiveIndexRecord {
            received_unix_ms,
            relay_url: self.relay_url.clone(),
            source_node: Some(self.source_node.clone()),
            source_session: Some(self.source_session.clone()),
            media_timing: media_sequence.clone(),
            broadcast_name: self.broadcast_name.clone(),
            track_name: self.track_name.clone(),
            group_sequence,
            frame_count: 1,
            size_bytes,
            blake3: hash.clone(),
            cas_path: rel_path.display().to_string(),
        };
        append_archive_index_record(&self.manifest_path, &record)?;
        self.archived_fragments += 1;
        if self.archived_fragments % 50 == 0 {
            tracing::info!(
                relay = %self.relay_url,
                source_node = %self.source_node,
                source_session = %self.source_session,
                broadcast = %self.broadcast_name,
                track = %self.track_name,
                group_sequence,
                media_timescale = ?media_sequence.as_ref().map(|timing| timing.timescale),
                media_decode_time = ?media_sequence.as_ref().map(|timing| timing.decode_time),
                media_sequence = ?media_sequence.as_ref().map(|timing| timing.sequence),
                archived_fragments = self.archived_fragments,
                inserted,
                size_bytes,
                hash = %hash,
                "archived publisher-origin fMP4 fragment"
            );
        }
        Ok(())
    }

    fn finish(&self) -> Result<()> {
        if self.pending_moof.is_some() {
            return Err(anyhow!(
                "publisher-origin fMP4 stream ended with moof but no mdat"
            ));
        }
        Ok(())
    }
}

#[derive(Debug, serde::Serialize)]
struct PublisherProofArchiveSourceReport {
    input: String,
    output_dir: PathBuf,
    manifest_path: PathBuf,
    broadcast_name: String,
    track_name: String,
    source_node: String,
    source_session: String,
    chunk_ms: u64,
    archived_windows: u64,
    archived_fragments: u64,
    first_group_sequence: Option<u64>,
    latest_group_sequence: Option<u64>,
}

#[derive(Debug, Clone)]
struct PublisherProofArchiveSourceConfig {
    input: String,
    input_format: Option<String>,
    output_dir: PathBuf,
    relay_url: String,
    broadcast_name: String,
    track_name: String,
    source_node: String,
    source_session: String,
    cas_root: PathBuf,
    manifest_path: PathBuf,
    source_window_dir: PathBuf,
    proof_dir: PathBuf,
    chunk_ms: u64,
    max_chunks: Option<usize>,
    preroll_packets: usize,
    cleanup_temp: bool,
    profile: StatelessProofEncodeProfile,
}

#[derive(Debug, Default)]
struct PublisherProofArchiveSourceStats {
    archived_windows: u64,
    archived_fragments: u64,
    first_group_sequence: Option<u64>,
    latest_group_sequence: Option<u64>,
    source_proof_subfragment_counts: BTreeMap<(u64, u32), u64>,
}

impl PublisherProofArchiveSourceStats {
    fn observe_group_sequence(&mut self, group_sequence: u64) {
        self.archived_fragments = self.archived_fragments.saturating_add(1);
        self.first_group_sequence = Some(
            self.first_group_sequence
                .map(|value| value.min(group_sequence))
                .unwrap_or(group_sequence),
        );
        self.latest_group_sequence = Some(
            self.latest_group_sequence
                .map(|value| value.max(group_sequence))
                .unwrap_or(group_sequence),
        );
    }

    fn next_source_proof_group_sequence(&mut self, timing: &ArchiveRecordMediaTiming) -> u64 {
        let subfragment_index = {
            let counter = self
                .source_proof_subfragment_counts
                .entry((timing.sequence, timing.track_id))
                .or_default();
            let subfragment_index = *counter;
            *counter = counter.saturating_add(1);
            subfragment_index
        };
        while self.source_proof_subfragment_counts.len()
            > PUBLISHER_PROOF_SUBFRAGMENT_SLOT_RETENTION
        {
            let Some(oldest_key) = self.source_proof_subfragment_counts.keys().next().copied()
            else {
                break;
            };
            self.source_proof_subfragment_counts.remove(&oldest_key);
        }
        let group_sequence = PublisherArchiveWriter::group_sequence_for_media_timing_subfragment(
            timing,
            subfragment_index,
        );
        self.observe_group_sequence(group_sequence);
        group_sequence
    }
}

impl PublisherProofArchiveSourceConfig {
    fn from_args(args: &PublisherProofArchiveSourceArgs) -> Result<Self> {
        if args.chunk_ms == 0 {
            return Err(anyhow!("--chunk-ms must be greater than 0"));
        }
        let source_node = archive_source_node(args.source_node.as_deref());
        let source_session = archive_source_session(&source_node);
        let manifest_root = args
            .manifest_dir
            .clone()
            .unwrap_or_else(|| args.output_dir.join("manifests"));
        let proof_root = args
            .output_dir
            .join("publisher-proof")
            .join(sanitize_path_component(&args.name))
            .join(sanitize_path_component(&source_session));
        Ok(Self {
            input: args.input.clone(),
            input_format: args.input_format.clone(),
            output_dir: args.output_dir.clone(),
            relay_url: args.relay_url.clone(),
            broadcast_name: args.name.clone(),
            track_name: args.track.clone(),
            source_node,
            source_session,
            cas_root: args.output_dir.join("objects").join("blake3"),
            manifest_path: archive_index_path(&manifest_root, &args.name, &args.track),
            source_window_dir: proof_root.join("source-windows"),
            proof_dir: proof_root.join("proof-mp4"),
            chunk_ms: args.chunk_ms,
            max_chunks: args.max_chunks,
            preroll_packets: args.preroll_packets,
            cleanup_temp: args.cleanup_temp,
            profile: StatelessProofEncodeProfile::from_publisher_proof_archive_source_args(args),
        })
    }

    fn report(&self, stats: PublisherProofArchiveSourceStats) -> PublisherProofArchiveSourceReport {
        PublisherProofArchiveSourceReport {
            input: self.input.clone(),
            output_dir: self.output_dir.clone(),
            manifest_path: self.manifest_path.clone(),
            broadcast_name: self.broadcast_name.clone(),
            track_name: self.track_name.clone(),
            source_node: self.source_node.clone(),
            source_session: self.source_session.clone(),
            chunk_ms: self.chunk_ms,
            archived_windows: stats.archived_windows,
            archived_fragments: stats.archived_fragments,
            first_group_sequence: stats.first_group_sequence,
            latest_group_sequence: stats.latest_group_sequence,
        }
    }
}

fn publisher_proof_archive_source_command(args: PublisherProofArchiveSourceArgs) -> Result<()> {
    let pretty = args.pretty;
    let report = publisher_proof_archive_source_report(&args)?;
    if pretty {
        println!("{}", serde_json::to_string_pretty(&report)?);
    } else {
        println!("{}", serde_json::to_string(&report)?);
    }
    Ok(())
}

fn publisher_proof_archive_source_report(
    args: &PublisherProofArchiveSourceArgs,
) -> Result<PublisherProofArchiveSourceReport> {
    let config = PublisherProofArchiveSourceConfig::from_args(args)?;
    run_publisher_proof_archive_source(config)
}

fn run_publisher_proof_archive_source(
    config: PublisherProofArchiveSourceConfig,
) -> Result<PublisherProofArchiveSourceReport> {
    fs::create_dir_all(&config.source_window_dir).with_context(|| {
        format!(
            "failed to create source proof dir {}",
            config.source_window_dir.display()
        )
    })?;
    fs::create_dir_all(&config.proof_dir)
        .with_context(|| format!("failed to create proof dir {}", config.proof_dir.display()))?;
    fs::create_dir_all(&config.cas_root)
        .with_context(|| format!("failed to create CAS dir {}", config.cas_root.display()))?;

    if let Some(path) = publisher_proof_archive_source_file_input(&config) {
        let file = File::open(&path)
            .with_context(|| format!("failed to open source {}", path.display()))?;
        return run_publisher_proof_archive_source_reader(&config, file);
    }

    if publisher_proof_archive_source_live_http_input(&config) && config.max_chunks.is_none() {
        return run_publisher_proof_archive_live_http_source(config);
    }

    if let Some(reader) = open_plain_http_body_reader(&config)? {
        return run_publisher_proof_archive_source_reader(&config, reader);
    }

    let mut child = Command::new("ffmpeg")
        .args(publisher_proof_archive_source_ffmpeg_args(&config))
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::inherit())
        .spawn()
        .context("failed to spawn ffmpeg source proof reader")?;
    let stdout = child
        .stdout
        .take()
        .ok_or_else(|| anyhow!("ffmpeg source proof stdout unavailable"))?;
    let report = run_publisher_proof_archive_source_reader(&config, stdout);
    let status = child
        .wait()
        .context("failed to wait for ffmpeg source proof reader")?;
    match report {
        Ok(report) if status.success() => Ok(report),
        Ok(_) => Err(anyhow!("ffmpeg source proof reader exited with {status}")),
        Err(err) => Err(err),
    }
}

fn publisher_proof_archive_source_live_http_input(
    config: &PublisherProofArchiveSourceConfig,
) -> bool {
    if config.input_format.is_some() {
        return false;
    }
    Url::parse(&config.input)
        .ok()
        .is_some_and(|url| url.scheme() == "http")
}

fn run_publisher_proof_archive_live_http_source(
    config: PublisherProofArchiveSourceConfig,
) -> Result<PublisherProofArchiveSourceReport> {
    let mut total_stats = PublisherProofArchiveSourceStats::default();
    loop {
        let reader = match open_plain_http_body_reader(&config) {
            Ok(Some(reader)) => reader,
            Ok(None) => return Ok(config.report(total_stats)),
            Err(err) => {
                tracing::warn!(
                    input = %config.input,
                    broadcast = %config.broadcast_name,
                    track = %config.track_name,
                    source_node = %config.source_node,
                    error = %err,
                    "publisher proof live HTTP source open failed; retrying"
                );
                std::thread::sleep(Duration::from_secs(1));
                continue;
            }
        };
        match run_publisher_proof_archive_source_reader_with_stats(
            &config,
            reader,
            &mut total_stats,
        ) {
            Ok(()) => {
                tracing::warn!(
                    input = %config.input,
                    broadcast = %config.broadcast_name,
                    track = %config.track_name,
                    source_node = %config.source_node,
                    "publisher proof live HTTP source ended; retrying"
                );
            }
            Err(err) => {
                tracing::warn!(
                    input = %config.input,
                    broadcast = %config.broadcast_name,
                    track = %config.track_name,
                    source_node = %config.source_node,
                    error = %err,
                    "publisher proof live HTTP source reader failed; retrying"
                );
            }
        }
        std::thread::sleep(Duration::from_secs(1));
    }
}

fn publisher_proof_archive_source_file_input(
    config: &PublisherProofArchiveSourceConfig,
) -> Option<PathBuf> {
    if config.input_format.is_some() || Url::parse(&config.input).is_ok() {
        return None;
    }
    let path = PathBuf::from(&config.input);
    path.exists().then_some(path)
}

fn open_plain_http_body_reader(
    config: &PublisherProofArchiveSourceConfig,
) -> Result<Option<BufReader<StdTcpStream>>> {
    if config.input_format.is_some() {
        return Ok(None);
    }
    let Ok(url) = Url::parse(&config.input) else {
        return Ok(None);
    };
    if url.scheme() != "http" {
        return Ok(None);
    }
    let host = url
        .host_str()
        .ok_or_else(|| anyhow!("HTTP source URL has no host: {}", config.input))?;
    let port = url.port_or_known_default().unwrap_or(80);
    let mut stream = StdTcpStream::connect((host, port))
        .with_context(|| format!("failed to connect to HTTP source {host}:{port}"))?;
    let mut path = if url.path().is_empty() {
        "/".to_string()
    } else {
        url.path().to_string()
    };
    if let Some(query) = url.query() {
        path.push('?');
        path.push_str(query);
    }
    write!(
        stream,
        "GET {path} HTTP/1.1\r\nHost: {host}\r\nUser-Agent: every-channel-ec-node\r\nAccept: */*\r\nConnection: close\r\n\r\n"
    )
    .context("failed to write HTTP source request")?;
    let mut reader = BufReader::new(stream);
    let mut line = String::new();
    reader
        .read_line(&mut line)
        .context("failed to read HTTP source status line")?;
    let status = line.split_whitespace().nth(1).unwrap_or("");
    if !status.starts_with('2') {
        return Err(anyhow!(
            "HTTP source {} returned non-success status line {:?}",
            config.input,
            line.trim_end()
        ));
    }
    loop {
        line.clear();
        let n = reader
            .read_line(&mut line)
            .context("failed to read HTTP source headers")?;
        if n == 0 {
            return Err(anyhow!("HTTP source ended before response body"));
        }
        let trimmed = line.trim_end_matches(['\r', '\n']);
        if trimmed.is_empty() {
            break;
        }
        let lower = trimmed.to_ascii_lowercase();
        if lower.starts_with("transfer-encoding:") && lower.contains("chunked") {
            return Err(anyhow!(
                "HTTP chunked transfer sources are not supported for byte-exact TS proof input"
            ));
        }
    }
    Ok(Some(reader))
}

fn publisher_proof_archive_source_ffmpeg_args(
    config: &PublisherProofArchiveSourceConfig,
) -> Vec<OsString> {
    let mut args = vec![
        OsString::from("-hide_banner"),
        OsString::from("-loglevel"),
        OsString::from("error"),
        OsString::from("-nostdin"),
        OsString::from("-copyts"),
    ];
    if let Some(format) = config.input_format.as_deref() {
        args.push(OsString::from("-f"));
        args.push(OsString::from(format));
    }
    args.extend([
        OsString::from("-i"),
        OsString::from(config.input.as_str()),
        OsString::from("-map"),
        OsString::from("0"),
        OsString::from("-c"),
        OsString::from("copy"),
        OsString::from("-f"),
        OsString::from("mpegts"),
        OsString::from("-mpegts_copyts"),
        OsString::from("1"),
        OsString::from("pipe:1"),
    ]);
    args
}

fn run_publisher_proof_archive_source_reader<R: Read>(
    config: &PublisherProofArchiveSourceConfig,
    reader: R,
) -> Result<PublisherProofArchiveSourceReport> {
    let mut stats = PublisherProofArchiveSourceStats::default();
    run_publisher_proof_archive_source_reader_with_stats(config, reader, &mut stats)?;
    Ok(config.report(stats))
}

fn run_publisher_proof_archive_source_reader_with_stats<R: Read>(
    config: &PublisherProofArchiveSourceConfig,
    reader: R,
    stats: &mut PublisherProofArchiveSourceStats,
) -> Result<()> {
    ec_chopper::chunk_ts_stream_live_with_preroll(
        reader,
        &config.source_window_dir,
        config.chunk_ms,
        config.max_chunks,
        config.preroll_packets,
        |chunk| archive_publisher_source_proof_chunk(config, stats, chunk),
    )?;
    Ok(())
}

fn publisher_source_proof_window_sequence(
    chunk: &TsChunk,
    received_unix_ms: u64,
    chunk_ms: u64,
) -> u64 {
    if chunk.timing.sync_status == "synced" {
        return chunk.timing.chunk_index;
    }
    received_unix_ms.saturating_sub(chunk_ms) / chunk_ms.max(1)
}

fn publisher_source_proof_fragment_timing(
    fragment: &mut [u8],
    track_timescales: &BTreeMap<u32, u32>,
    window_sequence: u64,
    chunk_ms: u64,
) -> Result<Option<ArchiveRecordMediaTiming>> {
    let Some((track_id, base_media_decode_time)) = fmp4_fragment_decode_time(fragment)? else {
        return Ok(None);
    };
    let Some(timescale) = track_timescales.get(&track_id).copied() else {
        return Ok(None);
    };
    let fragment_offset =
        fmp4_media_time_sequence(base_media_decode_time, timescale, chunk_ms).unwrap_or(0);
    let sequence = window_sequence.saturating_add(fragment_offset);
    let Some(decode_time) = fmp4_decode_time_for_media_sequence(sequence, timescale, chunk_ms)
    else {
        return Ok(Some(ArchiveRecordMediaTiming {
            track_id,
            timescale,
            decode_time: base_media_decode_time,
            sequence,
        }));
    };
    if rewrite_fmp4_fragment_decode_time(fragment, decode_time)? {
        return Ok(Some(ArchiveRecordMediaTiming {
            track_id,
            timescale,
            decode_time,
            sequence,
        }));
    }
    Ok(Some(ArchiveRecordMediaTiming {
        track_id,
        timescale,
        decode_time: base_media_decode_time,
        sequence,
    }))
}

fn archive_publisher_source_proof_chunk(
    config: &PublisherProofArchiveSourceConfig,
    stats: &mut PublisherProofArchiveSourceStats,
    chunk: TsChunk,
) -> Result<()> {
    let output_mp4 = config
        .proof_dir
        .join(format!("proof_{:010}.mp4", chunk.index));
    let status = match Command::new("ffmpeg")
        .args(stateless_proof_ffmpeg_args(
            &config.profile,
            &chunk.path,
            &output_mp4,
        ))
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::inherit())
        .status()
    {
        Ok(status) => status,
        Err(err) => {
            tracing::warn!(
                broadcast = %config.broadcast_name,
                track = %config.track_name,
                source_node = %config.source_node,
                source_session = %config.source_session,
                chunk_index = chunk.index,
                error = %err,
                "skipping source-window publisher proof chunk after ffmpeg spawn failure"
            );
            if config.cleanup_temp {
                let _ = fs::remove_file(&output_mp4);
                let _ = fs::remove_file(&chunk.path);
            }
            return Ok(());
        }
    };
    if !status.success() {
        tracing::warn!(
            broadcast = %config.broadcast_name,
            track = %config.track_name,
            source_node = %config.source_node,
            source_session = %config.source_session,
            chunk_index = chunk.index,
            %status,
            "skipping source-window publisher proof chunk after ffmpeg failure"
        );
        if config.cleanup_temp {
            let _ = fs::remove_file(&output_mp4);
            let _ = fs::remove_file(&chunk.path);
        }
        return Ok(());
    }

    let bytes = match fs::read(&output_mp4) {
        Ok(bytes) => bytes,
        Err(err) => {
            tracing::warn!(
                broadcast = %config.broadcast_name,
                track = %config.track_name,
                source_node = %config.source_node,
                source_session = %config.source_session,
                chunk_index = chunk.index,
                output = %output_mp4.display(),
                error = %err,
                "skipping source-window publisher proof chunk after output read failure"
            );
            if config.cleanup_temp {
                let _ = fs::remove_file(&output_mp4);
                let _ = fs::remove_file(&chunk.path);
            }
            return Ok(());
        }
    };
    let split = match split_fmp4_init_and_media(&bytes) {
        Ok(split) => split,
        Err(err) => {
            tracing::warn!(
                broadcast = %config.broadcast_name,
                track = %config.track_name,
                source_node = %config.source_node,
                source_session = %config.source_session,
                chunk_index = chunk.index,
                error = %err,
                "skipping source-window publisher proof chunk after fMP4 split failure"
            );
            if config.cleanup_temp {
                let _ = fs::remove_file(&output_mp4);
                let _ = fs::remove_file(&chunk.path);
            }
            return Ok(());
        }
    };
    let received_unix_ms = now_unix_ms();
    let window_sequence =
        publisher_source_proof_window_sequence(&chunk, received_unix_ms, config.chunk_ms);
    let track_timescales = mp4_init_track_timescales(&split.init).unwrap_or_default();

    for mut fragment in split.media {
        let timing = publisher_source_proof_fragment_timing(
            &mut fragment,
            &track_timescales,
            window_sequence,
            config.chunk_ms,
        )?
        .unwrap_or_else(|| ArchiveRecordMediaTiming {
            track_id: 0,
            timescale: 1000,
            decode_time: window_sequence.saturating_mul(config.chunk_ms),
            sequence: window_sequence,
        });
        let group_sequence = stats.next_source_proof_group_sequence(&timing);
        let size_bytes = fragment.len();
        let (hash, rel_path, inserted) = cas_store_blob(&config.cas_root, &fragment)?;
        let record = ArchiveIndexRecord {
            received_unix_ms,
            relay_url: config.relay_url.clone(),
            source_node: Some(config.source_node.clone()),
            source_session: Some(config.source_session.clone()),
            media_timing: Some(timing.clone()),
            broadcast_name: config.broadcast_name.clone(),
            track_name: config.track_name.clone(),
            group_sequence,
            frame_count: 1,
            size_bytes,
            blake3: hash.clone(),
            cas_path: rel_path.display().to_string(),
        };
        append_archive_index_record(&config.manifest_path, &record)?;
        tracing::debug!(
            broadcast = %config.broadcast_name,
            track = %config.track_name,
            source_node = %config.source_node,
            source_session = %config.source_session,
            chunk_index = chunk.index,
            chunk_sync_status = %chunk.timing.sync_status,
            chunk_utc_start_unix = ?chunk.timing.utc_start_unix,
            window_sequence,
            media_track_id = timing.track_id,
            media_sequence = timing.sequence,
            group_sequence,
            inserted,
            size_bytes,
            hash = %hash,
            "archived source-window publisher proof fragment"
        );
    }

    stats.archived_windows = stats.archived_windows.saturating_add(1);
    if config.cleanup_temp {
        let _ = fs::remove_file(&output_mp4);
        let _ = fs::remove_file(&chunk.path);
    }
    Ok(())
}

fn publisher_proof_archive_source_child_args(
    args: &WtPublishArgs,
    publish_input: &str,
) -> Result<Option<Vec<OsString>>> {
    let Some(output_dir) = args.publisher_archive_output_dir.as_ref() else {
        return Ok(None);
    };

    let mut out = vec![
        OsString::from("publisher-proof-archive-source"),
        OsString::from("--input"),
        OsString::from(publish_input),
        OsString::from("--output-dir"),
        output_dir.as_os_str().to_os_string(),
        OsString::from("--relay-url"),
        OsString::from(args.url.as_str()),
        OsString::from("--name"),
        OsString::from(args.name.as_str()),
        OsString::from("--track"),
        OsString::from(args.publisher_archive_track.as_str()),
        OsString::from("--chunk-ms"),
        OsString::from(args.publisher_archive_segment_duration_ms.to_string()),
        OsString::from("--preroll-packets"),
        OsString::from(WT_PUBLISH_PROOF_PREROLL_PACKETS.to_string()),
        OsString::from(format!("--transcode={}", args.transcode)),
        OsString::from("--video-filter"),
        OsString::from(args.video_filter.as_str()),
        OsString::from("--gop-frames"),
        OsString::from(args.gop_frames.to_string()),
        OsString::from("--video-preset"),
        OsString::from(args.video_preset.as_str()),
        OsString::from("--video-crf"),
        OsString::from(args.video_crf.to_string()),
        OsString::from("--movflags"),
        OsString::from(args.movflags.as_str()),
    ];

    if let Some(input_format) = args.input_format.as_deref() {
        out.push(OsString::from("--input-format"));
        out.push(OsString::from(input_format));
    }
    if let Some(manifest_dir) = args.publisher_archive_manifest_dir.as_ref() {
        out.push(OsString::from("--manifest-dir"));
        out.push(manifest_dir.as_os_str().to_os_string());
    }
    if let Some(source_node) = args.publisher_archive_source_node.as_deref() {
        out.push(OsString::from("--source-node"));
        out.push(OsString::from(source_node));
    }

    Ok(Some(out))
}

fn spawn_publisher_proof_archive_source_child(
    args: &WtPublishArgs,
    publish_input: &str,
) -> Result<Option<tokio::process::Child>> {
    let Some(child_args) = publisher_proof_archive_source_child_args(args, publish_input)? else {
        return Ok(None);
    };
    let current_exe =
        std::env::current_exe().context("failed to locate current ec-node executable")?;
    let mut cmd = TokioCommand::new(current_exe);
    cmd.args(child_args)
        .stdin(Stdio::null())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit());
    Ok(Some(cmd.spawn().context(
        "failed to spawn publisher source proof archive worker",
    )?))
}

async fn archive_track_loop(
    mut track: moq_lite::TrackConsumer,
    relay_url: String,
    source_node: String,
    source_session: String,
    broadcast_name: String,
    track_name: String,
    cas_root: PathBuf,
    manifest_path: PathBuf,
) -> Result<()> {
    let mut archived_groups: u64 = 0;
    loop {
        let next_group = track.next_group().await;
        let Some(mut group) = next_group.with_context(|| {
            format!(
                "track {} read failed for broadcast {}",
                track_name, broadcast_name
            )
        })?
        else {
            return Err(anyhow!(
                "track {} ended for broadcast {}",
                track_name,
                broadcast_name
            ));
        };

        let group_sequence = group.sequence;
        let mut frame_count: u64 = 0;
        let mut bytes = Vec::new();
        loop {
            match group.read_frame().await {
                Ok(Some(frame)) => {
                    frame_count += 1;
                    bytes.extend_from_slice(&frame);
                }
                Ok(None) => break,
                Err(err) => {
                    return Err(anyhow!(
                        "track {} group {} read frame failed: {err:#}",
                        track_name,
                        group_sequence
                    ));
                }
            }
        }

        if bytes.is_empty() {
            continue;
        }

        let size_bytes = bytes.len();
        let (hash, rel_path, inserted) = cas_store_blob(&cas_root, &bytes)?;
        let record = ArchiveIndexRecord {
            received_unix_ms: now_unix_ms(),
            relay_url: relay_url.clone(),
            source_node: Some(source_node.clone()),
            source_session: Some(source_session.clone()),
            media_timing: None,
            broadcast_name: broadcast_name.clone(),
            track_name: track_name.clone(),
            group_sequence,
            frame_count,
            size_bytes,
            blake3: hash.clone(),
            cas_path: rel_path.display().to_string(),
        };
        append_archive_index_record(&manifest_path, &record)?;
        archived_groups += 1;

        if archived_groups % 50 == 0 {
            tracing::info!(
                relay = %relay_url,
                source_node = %source_node,
                source_session = %source_session,
                broadcast = %broadcast_name,
                track = %track_name,
                archived_groups,
                inserted,
                size_bytes,
                hash = %hash,
                "archived relay group"
            );
        }
    }
}

async fn wt_archive(args: WtArchiveArgs) -> Result<()> {
    let relay_url =
        Url::parse(&args.url).with_context(|| format!("invalid relay url: {}", args.url))?;
    let relay_url_str = relay_url.to_string();
    let mut tracks = if args.track.is_empty() {
        default_wt_archive_tracks()
    } else {
        args.track.clone()
    };
    tracks.sort();
    tracks.dedup();
    if tracks.is_empty() {
        return Err(anyhow!("no tracks configured for wt-archive"));
    }

    let cas_root = args.output_dir.join("objects").join("blake3");
    let manifest_root = args
        .manifest_dir
        .unwrap_or_else(|| args.output_dir.join("manifests"));
    let source_node = archive_source_node(args.source_node.as_deref());
    let source_session = archive_source_session(&source_node);
    fs::create_dir_all(&cas_root)
        .with_context(|| format!("failed to create CAS root {}", cas_root.display()))?;
    fs::create_dir_all(&manifest_root)
        .with_context(|| format!("failed to create manifest root {}", manifest_root.display()))?;

    let consume = moq_lite::Origin::random().produce();
    let mut consume_updates = consume.consume();

    #[derive(Clone)]
    struct ProtocolOverride {
        inner: web_transport_quinn::Session,
        protocol: Option<String>,
    }

    impl web_transport_trait::Session for ProtocolOverride {
        type SendStream =
            <web_transport_quinn::Session as web_transport_trait::Session>::SendStream;
        type RecvStream =
            <web_transport_quinn::Session as web_transport_trait::Session>::RecvStream;
        type Error = <web_transport_quinn::Session as web_transport_trait::Session>::Error;

        fn accept_bi(
            &self,
        ) -> impl Future<Output = Result<(Self::SendStream, Self::RecvStream), Self::Error>>
               + web_transport_trait::MaybeSend {
            self.inner.accept_bi()
        }

        fn accept_uni(
            &self,
        ) -> impl Future<Output = Result<Self::RecvStream, Self::Error>> + web_transport_trait::MaybeSend
        {
            self.inner.accept_uni()
        }

        fn open_bi(
            &self,
        ) -> impl Future<Output = Result<(Self::SendStream, Self::RecvStream), Self::Error>>
               + web_transport_trait::MaybeSend {
            self.inner.open_bi()
        }

        fn open_uni(
            &self,
        ) -> impl Future<Output = Result<Self::SendStream, Self::Error>> + web_transport_trait::MaybeSend
        {
            self.inner.open_uni()
        }

        fn send_datagram(&self, payload: bytes::Bytes) -> Result<(), Self::Error> {
            self.inner.send_datagram(payload)
        }

        fn recv_datagram(
            &self,
        ) -> impl Future<Output = Result<bytes::Bytes, Self::Error>> + web_transport_trait::MaybeSend
        {
            self.inner.recv_datagram()
        }

        fn max_datagram_size(&self) -> usize {
            self.inner.max_datagram_size()
        }

        fn protocol(&self) -> Option<&str> {
            self.protocol.as_deref().or_else(|| self.inner.protocol())
        }

        fn close(&self, code: u32, reason: &str) {
            web_transport_trait::Session::close(&self.inner, code, reason)
        }

        fn closed(&self) -> impl Future<Output = Self::Error> + web_transport_trait::MaybeSend {
            self.inner.closed()
        }
    }

    async fn connect_moq_session(
        relay_url: &Url,
        consume: moq_lite::OriginProducer,
        tls_disable_verify: bool,
    ) -> Result<moq_lite::Session> {
        let host = relay_url
            .host_str()
            .ok_or_else(|| anyhow!("relay url missing host: {relay_url}"))?
            .to_string();
        let port = relay_url.port().unwrap_or(443);

        let mut roots = rustls::RootCertStore::empty();
        let native = rustls_native_certs::load_native_certs();
        if !native.errors.is_empty() {
            tracing::warn!(
                errors = ?native.errors,
                "some native root certs could not be loaded"
            );
        }
        for cert in native.certs {
            let _ = roots.add(cert);
        }

        let mut tls = rustls::ClientConfig::builder()
            .with_root_certificates(roots)
            .with_no_client_auth();

        if tls_disable_verify {
            #[derive(Debug)]
            struct NoCertificateVerification(Arc<rustls::crypto::CryptoProvider>);

            impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
                fn verify_server_cert(
                    &self,
                    _end_entity: &rustls::pki_types::CertificateDer<'_>,
                    _intermediates: &[rustls::pki_types::CertificateDer<'_>],
                    _server_name: &rustls::pki_types::ServerName<'_>,
                    _ocsp: &[u8],
                    _now: rustls::pki_types::UnixTime,
                ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error>
                {
                    Ok(rustls::client::danger::ServerCertVerified::assertion())
                }

                fn verify_tls12_signature(
                    &self,
                    message: &[u8],
                    cert: &rustls::pki_types::CertificateDer<'_>,
                    dss: &rustls::DigitallySignedStruct,
                ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error>
                {
                    rustls::crypto::verify_tls12_signature(
                        message,
                        cert,
                        dss,
                        &self.0.signature_verification_algorithms,
                    )
                }

                fn verify_tls13_signature(
                    &self,
                    message: &[u8],
                    cert: &rustls::pki_types::CertificateDer<'_>,
                    dss: &rustls::DigitallySignedStruct,
                ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error>
                {
                    rustls::crypto::verify_tls13_signature(
                        message,
                        cert,
                        dss,
                        &self.0.signature_verification_algorithms,
                    )
                }

                fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
                    self.0.signature_verification_algorithms.supported_schemes()
                }
            }

            let provider = rustls::crypto::CryptoProvider::get_default()
                .cloned()
                .unwrap_or_else(|| Arc::new(rustls::crypto::ring::default_provider()));
            tls.dangerous()
                .set_certificate_verifier(Arc::new(NoCertificateVerification(provider)));
        }

        tls.alpn_protocols = vec![web_transport_quinn::ALPN.as_bytes().to_vec()];

        let socket = std::net::UdpSocket::bind("[::]:0").context("failed to bind UDP socket")?;
        let mut transport = quinn::TransportConfig::default();
        transport.max_idle_timeout(Some(Duration::from_secs(10).try_into().unwrap()));
        transport.keep_alive_interval(Some(Duration::from_secs(4)));
        transport.mtu_discovery_config(None);

        let transport = Arc::new(transport);
        let runtime = quinn::default_runtime().context("no async runtime")?;
        let endpoint_config = quinn::EndpointConfig::default();
        let endpoint = quinn::Endpoint::new(endpoint_config, None, socket, runtime)
            .context("failed to create QUIC endpoint")?;

        let ip = tokio::net::lookup_host((host.clone(), port))
            .await
            .context("failed DNS lookup")?
            .next()
            .context("no DNS entries")?;

        let quic: quinn::crypto::rustls::QuicClientConfig = tls.try_into()?;
        let mut client_cfg = quinn::ClientConfig::new(Arc::new(quic));
        client_cfg.transport_config(transport);

        tracing::debug!(%ip, %host, %relay_url, "connecting QUIC");
        let connection = endpoint
            .connect_with(client_cfg, ip, &host)?
            .await
            .context("failed QUIC connect")?;

        let mut request = web_transport_quinn::proto::ConnectRequest::new(relay_url.clone());
        for alpn in moq_lite::ALPNS {
            request = request.with_protocol(alpn.to_string());
        }
        let wt = web_transport_quinn::Session::connect(connection, request)
            .await
            .context("failed WebTransport CONNECT")?;

        let client = moq_lite::Client::new().with_consume(consume);
        match client.connect(wt.clone()).await {
            Ok(session) => {
                tracing::info!("connected to relay (native protocol negotiation)");
                return Ok(session);
            }
            Err(err) => {
                tracing::debug!(err = %err, "native MoQ SETUP failed; trying protocol overrides");
            }
        }

        let attempts: [&str; 4] = ["moqt-16", "moqt-15", "moq-00", ""];
        let mut last_err: Option<anyhow::Error> = None;
        for p in attempts {
            let session = ProtocolOverride {
                inner: wt.clone(),
                protocol: (!p.is_empty()).then(|| p.to_string()),
            };
            match client.connect(session).await {
                Ok(session) => {
                    tracing::info!(protocol = %p, "connected to relay");
                    return Ok(session);
                }
                Err(err) => {
                    last_err = Some(anyhow::Error::new(err));
                    tracing::debug!(
                        protocol = %p,
                        err = %last_err.as_ref().unwrap(),
                        "MoQ SETUP failed; retrying"
                    );
                }
            }
        }

        Err(last_err.unwrap_or_else(|| anyhow!("failed to connect"))).context("failed MoQ SETUP")
    }

    tracing::info!(
        url=%relay_url,
        name=%args.name,
        source_node=%source_node,
        source_session=%source_session,
        "connecting to relay for archival"
    );
    let session = connect_moq_session(&relay_url, consume.clone(), args.tls_disable_verify).await?;

    let broadcast = if let Some(active) = consume_updates.get_broadcast(args.name.as_str()) {
        active
    } else {
        tracing::info!(name=%args.name, "waiting for relay broadcast announcement");
        loop {
            let Some((path, active)) = consume_updates.announced().await else {
                return Err(anyhow!("relay announcement stream closed"));
            };
            if path.to_string() != args.name {
                continue;
            }
            if let Some(active) = active {
                break active;
            }
        }
    };

    let broadcast_dir = sanitize_path_component(&args.name);
    let mut tasks = tokio::task::JoinSet::new();
    for track_name in tracks {
        let track = broadcast
            .subscribe_track(&moq_lite::Track::new(&track_name))
            .with_context(|| format!("failed to subscribe to relay track {track_name}"))?;
        let track_file = sanitize_path_component(&track_name);
        let manifest_path = manifest_root
            .join(&broadcast_dir)
            .join(format!("{track_file}.jsonl"));
        tasks.spawn(archive_track_loop(
            track,
            relay_url_str.clone(),
            source_node.clone(),
            source_session.clone(),
            args.name.clone(),
            track_name.clone(),
            cas_root.clone(),
            manifest_path,
        ));
        tracing::info!(
            relay = %relay_url_str,
            source_node = %source_node,
            source_session = %source_session,
            broadcast = %args.name,
            track = %track_name,
            "archival track subscribed"
        );
    }

    if tasks.is_empty() {
        return Err(anyhow!("no tracks spawned for archival"));
    }

    loop {
        tokio::select! {
            next = tasks.join_next() => {
                match next {
                    Some(Ok(Ok(()))) => {
                        return Err(anyhow!("archival track task exited unexpectedly"));
                    }
                    Some(Ok(Err(err))) => return Err(err),
                    Some(Err(err)) => return Err(anyhow!("archival track task join failure: {err}")),
                    None => return Err(anyhow!("all archival track tasks exited")),
                }
            }
            _ = session.closed() => {
                return Err(anyhow!("relay session closed"));
            }
            _ = tokio::signal::ctrl_c() => {
                tracing::info!("ctrl-c; shutting down wt-archive");
                break;
            }
        }
    }

    Ok(())
}

#[derive(Debug, Clone)]
struct ArchiveReplayState {
    cas_root: PathBuf,
    manifest_root: PathBuf,
    archive_origin_url: Option<Url>,
    archive_cas_origin_url: Option<Url>,
    archive_origin_max_bytes: usize,
    archive_retention_seconds: u64,
    cache_access_root: PathBuf,
    http_client: reqwest::Client,
}

#[derive(Debug, serde::Serialize)]
struct ArchiveTimelineResponse {
    broadcast_name: String,
    start_unix_ms: Option<u64>,
    end_unix_ms: Option<u64>,
    video_segments: usize,
    audio_segments: usize,
}

#[derive(Debug, serde::Serialize)]
struct ArchiveLadderResponse {
    broadcast_name: String,
    base_broadcast_name: String,
    generated_unix_ms: u64,
    retention_seconds: u64,
    retention_window_start_unix_ms: Option<u64>,
    renditions: Vec<ArchiveLadderRenditionSummary>,
    tracks: Vec<ArchiveLadderTrackStatus>,
}

#[derive(Debug, serde::Serialize)]
struct ArchiveLadderRenditionSummary {
    rendition_id: String,
    start_unix_ms: Option<u64>,
    end_unix_ms: Option<u64>,
    latest_age_ms: Option<u64>,
    segments: usize,
    total_bytes: Option<u64>,
    total_frames: Option<u64>,
    retention_coverage: f64,
    track_count: usize,
}

#[derive(Debug, Clone, serde::Serialize)]
struct ArchiveLadderTrackStatus {
    broadcast_name: String,
    track_name: String,
    media_kind: String,
    rendition_id: String,
    start_unix_ms: Option<u64>,
    end_unix_ms: Option<u64>,
    latest_age_ms: Option<u64>,
    segments: usize,
    total_bytes: Option<u64>,
    total_frames: Option<u64>,
    span_seconds: f64,
    retention_coverage: f64,
    partial_summary: bool,
}

#[derive(Debug, Clone)]
struct ArchiveHlsSegment {
    sequence: u64,
    duration_secs: f64,
    hash: String,
}

#[derive(Debug, Clone)]
struct ArchiveHlsVariant {
    broadcast_name: String,
    track_name: String,
    rendition_id: String,
    bandwidth: u64,
    resolution: Option<(u32, u32)>,
    segments: usize,
    latest_age_ms: Option<u64>,
}

#[derive(Debug, Clone, Default)]
struct ArchiveTrackSummary {
    start_unix_ms: Option<u64>,
    end_unix_ms: Option<u64>,
    segments: usize,
    total_bytes: Option<u64>,
    total_frames: Option<u64>,
    partial_summary: bool,
}

#[derive(Debug, Clone)]
struct ArchiveTrackSummaryCacheEntry {
    size: u64,
    modified: Option<SystemTime>,
    summary: ArchiveTrackSummary,
}

#[derive(Debug, Clone, serde::Serialize)]
struct ArchiveConvergenceSourceSummary {
    name: String,
    manifest_root: PathBuf,
    manifest_path: Option<PathBuf>,
    record_count: usize,
    source_nodes: Vec<String>,
    source_sessions: Vec<String>,
    missing_source_identity_records: usize,
    first_sequence: Option<u64>,
    latest_sequence: Option<u64>,
    first_received_unix_ms: Option<u64>,
    latest_received_unix_ms: Option<u64>,
}

#[derive(Debug, Clone, serde::Serialize)]
struct ArchiveMediaTimingConflict {
    group_sequence: u64,
    sources: Vec<String>,
    media_track_ids: Vec<u32>,
    media_timescales: Vec<u32>,
    media_sequences: Vec<u64>,
    media_decode_times: Vec<u64>,
}

#[derive(Debug, Clone)]
struct ArchiveMediaTimingObservation {
    source: String,
    timing: ArchiveRecordMediaTiming,
}

#[derive(Debug, Clone, serde::Serialize)]
struct ArchiveDivergentSequenceRecord {
    manifest_source: String,
    observation_source: String,
    source_node: Option<String>,
    source_session: Option<String>,
    received_unix_ms: u64,
    size_bytes: usize,
    blake3: String,
    cas_path: String,
    media_timing: Option<ArchiveRecordMediaTiming>,
}

#[derive(Debug, Clone, serde::Serialize)]
struct ArchiveDivergentSequenceSample {
    group_sequence: u64,
    record_count: usize,
    hash_count: usize,
    sources: Vec<String>,
    records: Vec<ArchiveDivergentSequenceRecord>,
}

#[derive(Debug, Clone, serde::Serialize)]
struct ArchiveConvergenceReport {
    ok: bool,
    reasons: Vec<String>,
    broadcast: String,
    track: String,
    stream_id: String,
    rendition_id: String,
    source_count: usize,
    record_source_count: usize,
    record_source_nodes: Vec<String>,
    missing_hash_records: usize,
    missing_source_identity_records: usize,
    media_timing_missing_records: usize,
    record_source_identity_ok: bool,
    sources: Vec<ArchiveConvergenceSourceSummary>,
    duplicate_complete: bool,
    media_timing_conflict_count: usize,
    media_timing_conflicts: Vec<ArchiveMediaTimingConflict>,
    divergent_sequence_samples: Vec<ArchiveDivergentSequenceSample>,
    summary: ec_core::sim::MediaConvergenceSummary,
}

static ARCHIVE_TRACK_SUMMARY_CACHE: OnceLock<
    Mutex<HashMap<PathBuf, ArchiveTrackSummaryCacheEntry>>,
> = OnceLock::new();

#[derive(Debug)]
struct ArchiveHttpResponse {
    status: u16,
    content_type: String,
    body: Vec<u8>,
}

fn archive_index_path(manifest_root: &Path, broadcast_name: &str, track_name: &str) -> PathBuf {
    let broadcast_dir = sanitize_path_component(broadcast_name);
    let track_file = sanitize_path_component(track_name);
    manifest_root
        .join(broadcast_dir)
        .join(format!("{track_file}.jsonl"))
}

fn parse_archive_record_line(path: &Path, line: &[u8]) -> Option<ArchiveIndexRecord> {
    let line = trim_ascii_whitespace(line);
    if line.is_empty() {
        return None;
    }
    match serde_json::from_slice::<ArchiveIndexRecord>(line) {
        Ok(record) => Some(record),
        Err(err) => {
            tracing::warn!(
                path = %path.display(),
                err = %err,
                "failed to parse archive index line"
            );
            None
        }
    }
}

fn trim_ascii_whitespace(mut bytes: &[u8]) -> &[u8] {
    while bytes.first().is_some_and(u8::is_ascii_whitespace) {
        bytes = &bytes[1..];
    }
    while bytes.last().is_some_and(u8::is_ascii_whitespace) {
        bytes = &bytes[..bytes.len() - 1];
    }
    bytes
}

fn read_archive_records(
    manifest_root: &Path,
    broadcast_name: &str,
    track_name: &str,
) -> Result<Vec<ArchiveIndexRecord>> {
    let path = archive_index_path(manifest_root, broadcast_name, track_name);
    read_archive_records_from_path(&path)
}

fn read_archive_records_from_path(path: &Path) -> Result<Vec<ArchiveIndexRecord>> {
    if !path.exists() {
        return Ok(Vec::new());
    }
    let mut out = Vec::new();
    let file = File::open(path)
        .with_context(|| format!("failed to open archive index {}", path.display()))?;
    let reader = BufReader::new(file);
    for line in reader.split(b'\n') {
        let line =
            line.with_context(|| format!("failed to read archive index {}", path.display()))?;
        if let Some(record) = parse_archive_record_line(path, &line) {
            out.push(record);
        }
    }
    Ok(out)
}

fn parse_named_path(value: &str) -> Result<(String, PathBuf)> {
    let (name, path) = value
        .split_once('=')
        .ok_or_else(|| anyhow!("expected NAME=PATH for --source, got {value:?}"))?;
    let name = name.trim();
    let path = path.trim();
    if name.is_empty() || path.is_empty() {
        return Err(anyhow!("expected NAME=PATH for --source, got {value:?}"));
    }
    Ok((name.to_string(), PathBuf::from(path)))
}

fn archive_convergence_rendition(broadcast: &str, track: &str, explicit: Option<&str>) -> String {
    if let Some(value) = explicit {
        let trimmed = value.trim();
        if !trimmed.is_empty() {
            return trimmed.to_string();
        }
    }
    if let Some(rendition) = track_rendition(track) {
        return rendition.to_string();
    }
    if let (_, Some(rendition)) = strip_ladder_rendition_suffix(broadcast) {
        return rendition.to_string();
    }
    "primary".to_string()
}

fn archive_convergence_stream_id(broadcast: &str, explicit: Option<&str>) -> String {
    if let Some(value) = explicit {
        let trimmed = value.trim();
        if !trimmed.is_empty() {
            return trimmed.to_string();
        }
    }
    strip_ladder_rendition_suffix(broadcast).0.to_string()
}

const ARCHIVE_MEDIA_TIMING_CONFLICT_REPORT_LIMIT: usize = 32;
const ARCHIVE_DIVERGENT_SEQUENCE_REPORT_LIMIT: usize = 16;

fn archive_media_timing_conflicts(
    observations_by_group: &BTreeMap<u64, Vec<ArchiveMediaTimingObservation>>,
    start_sequence: u64,
    end_sequence: u64,
) -> (usize, Vec<ArchiveMediaTimingConflict>) {
    let mut conflict_count = 0usize;
    let mut conflicts = Vec::new();

    for (group_sequence, observations) in observations_by_group {
        if *group_sequence < start_sequence || *group_sequence >= end_sequence {
            continue;
        }
        if observations.len() < 2 {
            continue;
        }

        let sources = observations
            .iter()
            .map(|observation| observation.source.clone())
            .collect::<BTreeSet<_>>()
            .into_iter()
            .collect::<Vec<_>>();
        let media_track_ids = observations
            .iter()
            .map(|observation| observation.timing.track_id)
            .collect::<BTreeSet<_>>()
            .into_iter()
            .collect::<Vec<_>>();
        let media_timescales = observations
            .iter()
            .map(|observation| observation.timing.timescale)
            .collect::<BTreeSet<_>>()
            .into_iter()
            .collect::<Vec<_>>();
        let media_sequences = observations
            .iter()
            .map(|observation| observation.timing.sequence)
            .collect::<BTreeSet<_>>()
            .into_iter()
            .collect::<Vec<_>>();
        let media_decode_times = observations
            .iter()
            .map(|observation| observation.timing.decode_time)
            .collect::<BTreeSet<_>>()
            .into_iter()
            .collect::<Vec<_>>();

        let conflicting_window = media_sequences.len() > 1 || media_decode_times.len() > 1;
        if !conflicting_window {
            continue;
        }

        conflict_count += 1;
        if conflicts.len() < ARCHIVE_MEDIA_TIMING_CONFLICT_REPORT_LIMIT {
            conflicts.push(ArchiveMediaTimingConflict {
                group_sequence: *group_sequence,
                sources,
                media_track_ids,
                media_timescales,
                media_sequences,
                media_decode_times,
            });
        }
    }

    (conflict_count, conflicts)
}

fn archive_divergent_sequence_samples(
    divergent_sequences: &[u64],
    records_by_group: &BTreeMap<u64, Vec<ArchiveDivergentSequenceRecord>>,
) -> Vec<ArchiveDivergentSequenceSample> {
    divergent_sequences
        .iter()
        .take(ARCHIVE_DIVERGENT_SEQUENCE_REPORT_LIMIT)
        .filter_map(|group_sequence| {
            let records = records_by_group.get(group_sequence)?;
            let hash_count = records
                .iter()
                .map(|record| record.blake3.as_str())
                .collect::<BTreeSet<_>>()
                .len();
            let sources = records
                .iter()
                .map(|record| record.observation_source.clone())
                .collect::<BTreeSet<_>>()
                .into_iter()
                .collect::<Vec<_>>();
            Some(ArchiveDivergentSequenceSample {
                group_sequence: *group_sequence,
                record_count: records.len(),
                hash_count,
                sources,
                records: records.clone(),
            })
        })
        .collect()
}

fn archive_convergence_report(args: &ArchiveConvergenceArgs) -> Result<ArchiveConvergenceReport> {
    if args.source.len() < 2 {
        return Err(anyhow!(
            "archive-convergence requires at least two --source NAME=PATH entries"
        ));
    }

    let sources = args
        .source
        .iter()
        .map(|item| parse_named_path(item))
        .collect::<Result<Vec<_>>>()?;
    let stream_id = archive_convergence_stream_id(&args.broadcast, args.stream_id.as_deref());
    let rendition_id =
        archive_convergence_rendition(&args.broadcast, &args.track, args.rendition.as_deref());
    let mut index = ec_core::sim::MediaConvergenceIndex::default();
    let mut source_summaries = Vec::with_capacity(sources.len());
    let mut first_observed_sequence: Option<u64> = None;
    let mut latest_observed_sequence: Option<u64> = None;
    let mut all_record_source_nodes = BTreeSet::<String>::new();
    let mut missing_hash_records = 0usize;
    let mut missing_source_identity_records = 0usize;
    let mut media_timing_missing_records = 0usize;
    let mut media_timing_observations = BTreeMap::<u64, Vec<ArchiveMediaTimingObservation>>::new();
    let mut divergent_records_by_group =
        BTreeMap::<u64, Vec<ArchiveDivergentSequenceRecord>>::new();

    for (name, manifest_root) in sources {
        let manifest_path =
            archive_index_path_with_aliases(&manifest_root, &args.broadcast, &args.track);
        let records =
            read_archive_records_with_aliases(&manifest_root, &args.broadcast, &args.track)
                .with_context(|| {
                    format!(
                        "failed to read archive records for source {name} at {}",
                        manifest_root.display()
                    )
                })?;
        let mut source_nodes = BTreeSet::<String>::new();
        let mut source_sessions = BTreeSet::<String>::new();
        let mut source_missing_records = 0usize;

        for record in &records {
            first_observed_sequence = Some(
                first_observed_sequence
                    .map(|value| value.min(record.group_sequence))
                    .unwrap_or(record.group_sequence),
            );
            latest_observed_sequence = Some(
                latest_observed_sequence
                    .map(|value| value.max(record.group_sequence))
                    .unwrap_or(record.group_sequence),
            );
            let record_source_node = record
                .source_node
                .as_deref()
                .and_then(non_empty_trimmed_string);
            if let Some(source_node) = record_source_node.as_ref() {
                source_nodes.insert(source_node.clone());
                all_record_source_nodes.insert(source_node.clone());
            } else {
                source_missing_records += 1;
                missing_source_identity_records += 1;
            }
            if let Some(source_session) = record
                .source_session
                .as_deref()
                .and_then(non_empty_trimmed_string)
            {
                source_sessions.insert(source_session);
            }
            if record.blake3.trim().is_empty() {
                missing_hash_records += 1;
            } else {
                let observation_source = record_source_node.clone().unwrap_or_else(|| name.clone());
                divergent_records_by_group
                    .entry(record.group_sequence)
                    .or_default()
                    .push(ArchiveDivergentSequenceRecord {
                        manifest_source: name.clone(),
                        observation_source: observation_source.clone(),
                        source_node: record.source_node.clone(),
                        source_session: record.source_session.clone(),
                        received_unix_ms: record.received_unix_ms,
                        size_bytes: record.size_bytes,
                        blake3: record.blake3.trim().to_string(),
                        cas_path: record.cas_path.clone(),
                        media_timing: record.media_timing.clone(),
                    });
                if let Some(timing) = record.media_timing.clone() {
                    media_timing_observations
                        .entry(record.group_sequence)
                        .or_default()
                        .push(ArchiveMediaTimingObservation {
                            source: observation_source.clone(),
                            timing,
                        });
                } else {
                    media_timing_missing_records += 1;
                }
                index.observe(ec_core::sim::MediaObservation::new_with_media_time_ms(
                    ec_core::sim::MediaKey::new(
                        &stream_id,
                        &rendition_id,
                        &args.track,
                        record.group_sequence,
                    ),
                    &observation_source,
                    record.blake3.trim(),
                    record.media_timing.as_ref().map(|timing| timing.sequence),
                    record.received_unix_ms,
                ));
            }
        }

        source_summaries.push(ArchiveConvergenceSourceSummary {
            name,
            manifest_root,
            manifest_path,
            record_count: records.len(),
            source_nodes: source_nodes.into_iter().collect(),
            source_sessions: source_sessions.into_iter().collect(),
            missing_source_identity_records: source_missing_records,
            first_sequence: records.iter().map(|record| record.group_sequence).min(),
            latest_sequence: records.iter().map(|record| record.group_sequence).max(),
            first_received_unix_ms: records.iter().map(|record| record.received_unix_ms).min(),
            latest_received_unix_ms: records.iter().map(|record| record.received_unix_ms).max(),
        });
    }

    let start_sequence = args
        .start_sequence
        .or(first_observed_sequence)
        .ok_or_else(|| anyhow!("no archive records found for {}", args.broadcast))?;
    let end_sequence = args
        .end_sequence
        .or_else(|| latest_observed_sequence.map(|value| value.saturating_add(1)))
        .ok_or_else(|| anyhow!("no archive records found for {}", args.broadcast))?;
    if end_sequence <= start_sequence {
        return Err(anyhow!(
            "end sequence {end_sequence} must be greater than start sequence {start_sequence}"
        ));
    }

    let summary = index.summarize_observed_sequences(
        &stream_id,
        &rendition_id,
        &args.track,
        start_sequence,
        end_sequence,
    );
    let duplicate_complete = summary.expected_sequences > 0
        && summary.matching_duplicate_sequences.len() as u64 == summary.expected_sequences;
    let (media_timing_conflict_count, media_timing_conflicts) =
        archive_media_timing_conflicts(&media_timing_observations, start_sequence, end_sequence);
    let divergent_sequence_samples = archive_divergent_sequence_samples(
        &summary.divergent_sequences,
        &divergent_records_by_group,
    );
    let mut reasons = Vec::new();
    if !summary.missing_sequences.is_empty() {
        reasons.push("missing_sequences".to_string());
    }
    if !summary.divergent_sequences.is_empty() {
        reasons.push("divergent_sequences".to_string());
    }
    if !summary.source_local_divergent_sequences.is_empty() {
        reasons.push("source_local_divergent_sequences".to_string());
    }
    if !duplicate_complete {
        reasons.push("duplicate_sequences_incomplete".to_string());
    }
    if media_timing_conflict_count > 0 {
        reasons.push("media_sequence_conflict".to_string());
    }
    if media_timing_missing_records > 0 {
        reasons.push("media_timing_missing".to_string());
    }
    if missing_hash_records > 0 {
        reasons.push("hash_missing".to_string());
    }
    let record_source_nodes = all_record_source_nodes.into_iter().collect::<Vec<_>>();
    let record_source_identity_ok =
        missing_source_identity_records == 0 && record_source_nodes.len() >= 2;
    if missing_source_identity_records > 0 {
        reasons.push("source_identity_missing".to_string());
    }
    if record_source_nodes.len() < 2 {
        reasons.push("source_identity_not_diverse".to_string());
    }

    Ok(ArchiveConvergenceReport {
        ok: summary.ok()
            && duplicate_complete
            && record_source_identity_ok
            && missing_hash_records == 0
            && media_timing_missing_records == 0
            && media_timing_conflict_count == 0,
        reasons,
        broadcast: args.broadcast.clone(),
        track: args.track.clone(),
        stream_id,
        rendition_id,
        source_count: source_summaries.len(),
        record_source_count: record_source_nodes.len(),
        record_source_nodes,
        missing_hash_records,
        missing_source_identity_records,
        media_timing_missing_records,
        record_source_identity_ok,
        sources: source_summaries,
        duplicate_complete,
        media_timing_conflict_count,
        media_timing_conflicts,
        divergent_sequence_samples,
        summary,
    })
}

fn prometheus_escape_label_value(value: &str) -> String {
    let mut out = String::with_capacity(value.len());
    for ch in value.chars() {
        match ch {
            '\\' => out.push_str("\\\\"),
            '"' => out.push_str("\\\""),
            '\n' => out.push_str("\\n"),
            _ => out.push(ch),
        }
    }
    out
}

fn prometheus_label_set(labels: &BTreeMap<String, String>) -> String {
    if labels.is_empty() {
        return String::new();
    }

    let pairs = labels
        .iter()
        .map(|(key, value)| format!("{key}=\"{}\"", prometheus_escape_label_value(value)))
        .collect::<Vec<_>>()
        .join(",");
    format!("{{{pairs}}}")
}

fn archive_convergence_prometheus_labels(
    broadcast: &str,
    track: &str,
    stream_id: &str,
    rendition_id: &str,
    metrics_node: Option<&str>,
    metrics_role: &str,
) -> BTreeMap<String, String> {
    let mut labels = BTreeMap::from([
        ("broadcast".to_string(), broadcast.to_string()),
        ("rendition".to_string(), rendition_id.to_string()),
        ("role".to_string(), metrics_role.to_string()),
        ("stream".to_string(), stream_id.to_string()),
        ("track".to_string(), track.to_string()),
    ]);
    if let Some(node) = metrics_node.and_then(non_empty_trimmed_string) {
        labels.insert("node".to_string(), node);
    }
    labels
}

fn push_prometheus_gauge(
    output: &mut String,
    metric: &str,
    labels: &BTreeMap<String, String>,
    value: f64,
) {
    output.push_str(metric);
    output.push_str(&prometheus_label_set(labels));
    output.push(' ');
    output.push_str(&format!("{value:.6}"));
    output.push('\n');
}

fn archive_convergence_prometheus_metrics(
    report: &ArchiveConvergenceReport,
    args: &ArchiveConvergenceArgs,
) -> String {
    let labels = archive_convergence_prometheus_labels(
        &report.broadcast,
        &report.track,
        &report.stream_id,
        &report.rendition_id,
        args.metrics_node.as_deref(),
        &args.metrics_role,
    );

    let mut output = String::new();
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_convergence_scrape_ok",
        &labels,
        1.0,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_convergence_ok",
        &labels,
        if report.ok { 1.0 } else { 0.0 },
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_duplicate_complete",
        &labels,
        if report.duplicate_complete { 1.0 } else { 0.0 },
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_record_source_identity_ok",
        &labels,
        if report.record_source_identity_ok {
            1.0
        } else {
            0.0
        },
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_duplicate_hash_source_records",
        &labels,
        report.summary.duplicate_source_records as f64,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_duplicate_hash_sequences",
        &labels,
        report.summary.matching_duplicate_sequences.len() as f64,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_hash_divergent_sequences",
        &labels,
        report.summary.divergent_sequences.len() as f64,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_source_local_hash_divergent_sequences",
        &labels,
        report.summary.source_local_divergent_sequences.len() as f64,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_missing_hash_records",
        &labels,
        report.missing_hash_records as f64,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_missing_source_identity_records",
        &labels,
        report.missing_source_identity_records as f64,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_media_timing_missing_records",
        &labels,
        report.media_timing_missing_records as f64,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_media_timing_conflict_sequences",
        &labels,
        report.media_timing_conflict_count as f64,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_expected_sequences",
        &labels,
        report.summary.expected_sequences as f64,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_observed_sequences",
        &labels,
        report.summary.observed_sequences as f64,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_record_source_count",
        &labels,
        report.record_source_count as f64,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_manifest_source_count",
        &labels,
        report.source_count as f64,
    );
    output
}

fn archive_convergence_prometheus_error_metrics(
    args: &ArchiveConvergenceArgs,
    err: &anyhow::Error,
) -> String {
    let stream_id = archive_convergence_stream_id(&args.broadcast, args.stream_id.as_deref());
    let rendition_id =
        archive_convergence_rendition(&args.broadcast, &args.track, args.rendition.as_deref());
    let labels = archive_convergence_prometheus_labels(
        &args.broadcast,
        &args.track,
        &stream_id,
        &rendition_id,
        args.metrics_node.as_deref(),
        &args.metrics_role,
    );
    let mut output = String::new();
    output.push_str("# archive_convergence_error ");
    output.push_str(&prometheus_escape_label_value(&err.to_string()));
    output.push('\n');
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_convergence_scrape_ok",
        &labels,
        0.0,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_convergence_ok",
        &labels,
        0.0,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_duplicate_complete",
        &labels,
        0.0,
    );
    output
}

fn archive_convergence_command(args: ArchiveConvergenceArgs) -> Result<()> {
    let report = archive_convergence_report(&args)?;
    if args.prometheus {
        print!("{}", archive_convergence_prometheus_metrics(&report, &args));
    } else if args.pretty {
        println!("{}", serde_json::to_string_pretty(&report)?);
    } else {
        println!("{}", serde_json::to_string(&report)?);
    }
    if args.require_ok && !report.ok {
        return Err(anyhow!(
            "archive convergence failed for {}/{}: {}",
            report.broadcast,
            report.track,
            if report.reasons.is_empty() {
                "unknown".to_string()
            } else {
                report.reasons.join(",")
            }
        ));
    }
    Ok(())
}

#[derive(Debug, Clone, serde::Serialize)]
struct ArchiveConvergenceMeasureFetch {
    name: String,
    kind: String,
    url: String,
    ok: bool,
    status: Option<u16>,
    elapsed_ms: u64,
    record_count: usize,
    invalid_record_count: usize,
    error: Option<String>,
}

#[derive(Debug, Clone, serde::Serialize)]
struct ArchiveConvergenceMeasurePrometheusSample {
    metric: String,
    ok: bool,
    series_present: bool,
    value: Option<f64>,
    error: Option<String>,
}

#[derive(Debug, Clone, serde::Serialize)]
struct ArchiveConvergenceMeasureSample {
    sample_unix_ms: u64,
    sources: Vec<ArchiveConvergenceMeasureFetch>,
    convergence: Option<ArchiveConvergenceReport>,
    convergence_error: Option<String>,
    prometheus: Vec<ArchiveConvergenceMeasurePrometheusSample>,
}

#[derive(Debug, Clone, serde::Serialize)]
struct ArchiveConvergenceMeasureSummary {
    ok: bool,
    elapsed_ms: u64,
    sample_count: usize,
    reasons: Vec<String>,
    prometheus_series_present_count: usize,
    latest_matching_duplicate_sequences: Option<usize>,
    latest_divergent_sequences: Option<usize>,
}

#[derive(Debug, Clone, serde::Serialize)]
struct ArchiveConvergenceMeasureReport {
    ok: bool,
    broadcast: String,
    track: String,
    duration_seconds: f64,
    summary: ArchiveConvergenceMeasureSummary,
    samples: Vec<ArchiveConvergenceMeasureSample>,
}

struct ArchiveConvergenceMeasureFetchedBody {
    status: Option<u16>,
    elapsed_ms: u64,
    body: Option<String>,
    error: Option<String>,
}

#[derive(Debug, Clone)]
struct ArchiveConvergenceMeasureFetchSpec {
    name: String,
    kind: String,
    url: String,
    tail_range: bool,
}

fn parse_named_endpoint(value: &str) -> Result<(String, String)> {
    let (name, endpoint) = value
        .split_once('=')
        .ok_or_else(|| anyhow!("expected NAME=URL, got {value:?}"))?;
    let name = name.trim();
    let endpoint = endpoint.trim();
    if name.is_empty() || endpoint.is_empty() {
        return Err(anyhow!("expected NAME=URL, got {value:?}"));
    }
    Ok((name.to_string(), endpoint.to_string()))
}

fn parse_label_filter(value: &str) -> Result<(String, String)> {
    let (name, expected) = value
        .split_once('=')
        .ok_or_else(|| anyhow!("expected KEY=VALUE for label filter, got {value:?}"))?;
    let name = name.trim();
    let expected = expected.trim();
    if name.is_empty() || expected.is_empty() {
        return Err(anyhow!(
            "expected KEY=VALUE for label filter, got {value:?}"
        ));
    }
    Ok((name.to_string(), expected.to_string()))
}

#[derive(Debug, serde::Deserialize)]
struct PrometheusFileSdGroup {
    targets: Vec<String>,
    #[serde(default)]
    labels: BTreeMap<String, String>,
}

fn archive_measure_prometheus_sd_agent_manifests(
    args: &ArchiveConvergenceMeasureArgs,
) -> (Vec<String>, Vec<ArchiveConvergenceMeasureFetch>) {
    let mut filters = Vec::new();
    let mut discovery_errors = Vec::new();
    for filter in &args.agent_prometheus_sd_label {
        match parse_label_filter(filter) {
            Ok(filter) => filters.push(filter),
            Err(err) => discovery_errors.push(ArchiveConvergenceMeasureFetch {
                name: filter.clone(),
                kind: "agent-prometheus-sd".to_string(),
                url: String::new(),
                ok: false,
                status: None,
                elapsed_ms: 0,
                record_count: 0,
                invalid_record_count: 0,
                error: Some(err.to_string()),
            }),
        }
    }

    let mut entries = Vec::new();
    let mut seen_targets = BTreeSet::<String>::new();
    let mut used_names = HashMap::<String, usize>::new();
    for path in &args.agent_prometheus_sd {
        let started = Instant::now();
        let body = match fs::read_to_string(path) {
            Ok(body) => body,
            Err(err) => {
                discovery_errors.push(ArchiveConvergenceMeasureFetch {
                    name: path.display().to_string(),
                    kind: "agent-prometheus-sd".to_string(),
                    url: path.display().to_string(),
                    ok: false,
                    status: None,
                    elapsed_ms: started.elapsed().as_millis() as u64,
                    record_count: 0,
                    invalid_record_count: 0,
                    error: Some(err.to_string()),
                });
                continue;
            }
        };
        let groups = match serde_json::from_str::<Vec<PrometheusFileSdGroup>>(&body) {
            Ok(groups) => groups,
            Err(err) => {
                discovery_errors.push(ArchiveConvergenceMeasureFetch {
                    name: path.display().to_string(),
                    kind: "agent-prometheus-sd".to_string(),
                    url: path.display().to_string(),
                    ok: false,
                    status: None,
                    elapsed_ms: started.elapsed().as_millis() as u64,
                    record_count: 0,
                    invalid_record_count: 0,
                    error: Some(format!("invalid prometheus file-SD JSON: {err}")),
                });
                continue;
            }
        };
        for group in groups {
            if filters
                .iter()
                .any(|(key, expected)| group.labels.get(key) != Some(expected))
            {
                continue;
            }
            let base_name = group
                .labels
                .get("headscale_name")
                .or_else(|| group.labels.get("node"))
                .or_else(|| group.labels.get("instance"))
                .map(|value| value.trim())
                .filter(|value| !value.is_empty());
            for target in group.targets {
                let target = target.trim();
                if target.is_empty() || !seen_targets.insert(target.to_string()) {
                    continue;
                }
                let url = if target.starts_with("http://") || target.starts_with("https://") {
                    target.trim_end_matches('/').to_string()
                } else {
                    format!("http://{}", target.trim_end_matches('/'))
                };
                let fallback_name = target
                    .replace("://", "-")
                    .replace(['/', ':', '.'], "-")
                    .trim_matches('-')
                    .to_string();
                let base_name = base_name.unwrap_or(&fallback_name);
                let count = used_names.entry(base_name.to_string()).or_insert(0);
                let name = if *count == 0 {
                    base_name.to_string()
                } else {
                    format!("{base_name}-{}", *count + 1)
                };
                *count += 1;
                entries.push(format!("{name}={url}"));
            }
        }
    }
    (entries, discovery_errors)
}

async fn archive_measure_fetch_body(
    client: &reqwest::Client,
    url: &str,
    timeout: Duration,
    max_bytes: usize,
    tail_range: bool,
) -> ArchiveConvergenceMeasureFetchedBody {
    let started = Instant::now();
    let mut request = client.get(url).timeout(timeout);
    if tail_range && max_bytes > 0 {
        request = request.header(reqwest::header::RANGE, format!("bytes=-{max_bytes}"));
    }
    let response = match request.send().await {
        Ok(response) => response,
        Err(err) => {
            return ArchiveConvergenceMeasureFetchedBody {
                status: None,
                elapsed_ms: started.elapsed().as_millis() as u64,
                body: None,
                error: Some(err.to_string()),
            };
        }
    };
    let status = response.status();
    let status_u16 = status.as_u16();
    let bytes = match response.bytes().await {
        Ok(bytes) => bytes,
        Err(err) => {
            return ArchiveConvergenceMeasureFetchedBody {
                status: Some(status_u16),
                elapsed_ms: started.elapsed().as_millis() as u64,
                body: None,
                error: Some(err.to_string()),
            };
        }
    };
    let body = if max_bytes > 0 && bytes.len() > max_bytes {
        String::from_utf8_lossy(&bytes[bytes.len() - max_bytes..]).into_owned()
    } else {
        String::from_utf8_lossy(&bytes).into_owned()
    };
    ArchiveConvergenceMeasureFetchedBody {
        status: Some(status_u16),
        elapsed_ms: started.elapsed().as_millis() as u64,
        body: status.is_success().then_some(body),
        error: (!status.is_success()).then(|| format!("http status {status_u16}")),
    }
}

fn parse_archive_records_jsonl(body: &str) -> (Vec<ArchiveIndexRecord>, usize) {
    let mut records = Vec::new();
    let mut invalid = 0usize;
    for (index, line) in body.lines().enumerate() {
        let raw = line.trim();
        if raw.is_empty() {
            continue;
        }
        match serde_json::from_str::<ArchiveIndexRecord>(raw) {
            Ok(record) => records.push(record),
            Err(_) if index == 0 => {}
            Err(_) => invalid += 1,
        }
    }
    (records, invalid)
}

fn parse_agent_archive_records(body: &str) -> Result<(Vec<ArchiveIndexRecord>, usize)> {
    let payload = serde_json::from_str::<serde_json::Value>(body)
        .context("failed to parse node-agent archive manifest response")?;
    if payload.get("ok").and_then(|value| value.as_bool()) == Some(false) {
        return Err(anyhow!(
            "node-agent archive manifest response was not ok: {}",
            payload
                .get("error")
                .and_then(|value| value.as_str())
                .unwrap_or("unknown")
        ));
    }
    let mut invalid = payload
        .get("invalid_lines")
        .and_then(|value| value.as_u64())
        .unwrap_or(0) as usize;
    let records = payload
        .get("records")
        .and_then(|value| value.as_array())
        .ok_or_else(|| anyhow!("node-agent archive manifest response missing records array"))?;
    let mut parsed = Vec::with_capacity(records.len());
    for record in records {
        match serde_json::from_value::<ArchiveIndexRecord>(record.clone()) {
            Ok(record) => parsed.push(record),
            Err(_) => invalid += 1,
        }
    }
    Ok((parsed, invalid))
}

fn write_measure_records(
    manifest_root: &Path,
    broadcast: &str,
    track: &str,
    records: &[ArchiveIndexRecord],
) -> Result<()> {
    let path = archive_index_path(manifest_root, broadcast, track);
    let parent = path
        .parent()
        .ok_or_else(|| anyhow!("invalid manifest path: {}", path.display()))?;
    fs::create_dir_all(parent).with_context(|| format!("failed to create {}", parent.display()))?;
    let mut file =
        File::create(&path).with_context(|| format!("failed to create {}", path.display()))?;
    for record in records {
        serde_json::to_writer(&mut file, record)
            .with_context(|| format!("failed to write {}", path.display()))?;
        file.write_all(b"\n")
            .with_context(|| format!("failed to write {}", path.display()))?;
    }
    Ok(())
}

fn archive_measure_agent_url(args: &ArchiveConvergenceMeasureArgs, base_url: &str) -> String {
    let mut query = vec![
        (
            "broadcast",
            urlencoding::encode(&args.broadcast).into_owned(),
        ),
        ("track", urlencoding::encode(&args.track).into_owned()),
        ("max_bytes", args.max_manifest_bytes.to_string()),
    ];
    if !args.agent_manifest_role.trim().is_empty() {
        query.push((
            "role",
            urlencoding::encode(args.agent_manifest_role.trim()).into_owned(),
        ));
    }
    let query = query
        .into_iter()
        .map(|(key, value)| format!("{key}={value}"))
        .collect::<Vec<_>>()
        .join("&");
    format!(
        "{}/v1/archive-manifest?{query}",
        base_url.trim_end_matches('/')
    )
}

async fn archive_convergence_measure_sample(
    args: &ArchiveConvergenceMeasureArgs,
    client: &reqwest::Client,
    sample_index: usize,
) -> ArchiveConvergenceMeasureSample {
    let sample_unix_ms = now_unix_ms();
    let timeout = Duration::from_millis(args.timeout_ms.max(1));
    let temp_root = std::env::temp_dir().join(format!(
        "ec-archive-convergence-measure-{}-{}-{}",
        std::process::id(),
        sample_unix_ms,
        sample_index
    ));
    let mut fetches = Vec::new();
    let mut source_args = Vec::new();
    let (sd_agent_manifests, mut discovery_fetches) =
        archive_measure_prometheus_sd_agent_manifests(args);
    fetches.append(&mut discovery_fetches);

    let mut specs = Vec::<ArchiveConvergenceMeasureFetchSpec>::new();
    for item in args.agent_manifest.iter().chain(sd_agent_manifests.iter()) {
        let (name, base_url) = match parse_named_endpoint(item) {
            Ok(parsed) => parsed,
            Err(err) => {
                fetches.push(ArchiveConvergenceMeasureFetch {
                    name: item.clone(),
                    kind: "agent-manifest".to_string(),
                    url: String::new(),
                    ok: false,
                    status: None,
                    elapsed_ms: 0,
                    record_count: 0,
                    invalid_record_count: 0,
                    error: Some(err.to_string()),
                });
                continue;
            }
        };
        let url = archive_measure_agent_url(args, &base_url);
        specs.push(ArchiveConvergenceMeasureFetchSpec {
            name,
            kind: "agent-manifest".to_string(),
            url,
            tail_range: false,
        });
    }

    for item in &args.manifest {
        let (name, url) = match parse_named_endpoint(item) {
            Ok(parsed) => parsed,
            Err(err) => {
                fetches.push(ArchiveConvergenceMeasureFetch {
                    name: item.clone(),
                    kind: "manifest".to_string(),
                    url: String::new(),
                    ok: false,
                    status: None,
                    elapsed_ms: 0,
                    record_count: 0,
                    invalid_record_count: 0,
                    error: Some(err.to_string()),
                });
                continue;
            }
        };
        specs.push(ArchiveConvergenceMeasureFetchSpec {
            name,
            kind: "manifest".to_string(),
            url,
            tail_range: true,
        });
    }

    let fetched_bodies = join_all(specs.iter().map(|spec| {
        let response_max_bytes = if spec.kind == "agent-manifest" {
            0
        } else {
            args.max_manifest_bytes
        };
        archive_measure_fetch_body(
            client,
            &spec.url,
            timeout,
            response_max_bytes,
            spec.tail_range,
        )
    }))
    .await;

    for (spec, fetched) in specs.into_iter().zip(fetched_bodies) {
        let mut record_count = 0usize;
        let mut invalid_record_count = 0usize;
        let mut error = fetched.error.clone();
        if let Some(body) = fetched.body.as_deref() {
            let parsed = if spec.kind == "agent-manifest" {
                parse_agent_archive_records(body)
            } else {
                let (records, invalid) = parse_archive_records_jsonl(body);
                Ok((records, invalid))
            };
            match parsed {
                Ok((records, invalid)) => {
                    record_count = records.len();
                    invalid_record_count = invalid;
                    let manifest_root = temp_root.join(sanitize_path_component(&spec.name));
                    if let Err(err) = write_measure_records(
                        &manifest_root,
                        &args.broadcast,
                        &args.track,
                        &records,
                    ) {
                        error = Some(err.to_string());
                    } else {
                        source_args.push(format!("{}={}", spec.name, manifest_root.display()));
                    }
                }
                Err(err) => error = Some(err.to_string()),
            }
        }
        fetches.push(ArchiveConvergenceMeasureFetch {
            name: spec.name,
            kind: spec.kind,
            url: spec.url,
            ok: error.is_none(),
            status: fetched.status,
            elapsed_ms: fetched.elapsed_ms,
            record_count,
            invalid_record_count,
            error,
        });
    }

    let (convergence, convergence_error) = if source_args.len() >= 2 {
        let report = archive_convergence_report(&ArchiveConvergenceArgs {
            source: source_args,
            broadcast: args.broadcast.clone(),
            track: args.track.clone(),
            stream_id: args.stream_id.clone(),
            rendition: args.rendition.clone(),
            start_sequence: args.start_sequence,
            end_sequence: args.end_sequence,
            pretty: false,
            prometheus: false,
            metrics_node: None,
            metrics_role: "duplicate-proof".to_string(),
            require_ok: false,
        });
        match report {
            Ok(report) => (Some(report), None),
            Err(err) => (None, Some(err.to_string())),
        }
    } else {
        (
            None,
            Some("fewer than two fetched manifest sources".to_string()),
        )
    };
    let prometheus = match args.prometheus_url.as_deref() {
        Some(url) if !url.trim().is_empty() => {
            archive_convergence_measure_prometheus(client, url, &args.broadcast, timeout).await
        }
        _ => Vec::new(),
    };
    let _ = fs::remove_dir_all(&temp_root);

    ArchiveConvergenceMeasureSample {
        sample_unix_ms,
        sources: fetches,
        convergence,
        convergence_error,
        prometheus,
    }
}

async fn archive_convergence_measure_prometheus(
    client: &reqwest::Client,
    prometheus_url: &str,
    broadcast: &str,
    timeout: Duration,
) -> Vec<ArchiveConvergenceMeasurePrometheusSample> {
    let base = prometheus_url.trim_end_matches('/');
    let queries = ARCHIVE_CONVERGENCE_PROMETHEUS_METRICS
        .iter()
        .map(|metric| {
            let expr = format!("sum({metric}{{broadcast=\"{broadcast}\"}})");
            let url = format!("{base}/api/v1/query?query={}", urlencoding::encode(&expr));
            ((*metric).to_string(), url)
        })
        .collect::<Vec<_>>();
    let fetched = join_all(
        queries
            .iter()
            .map(|(_, url)| archive_measure_fetch_body(client, url, timeout, 1024 * 1024, false)),
    )
    .await;
    let mut rows = Vec::with_capacity(queries.len());
    for ((metric, _), fetched) in queries.into_iter().zip(fetched) {
        let mut row = ArchiveConvergenceMeasurePrometheusSample {
            metric,
            ok: false,
            series_present: false,
            value: None,
            error: fetched.error.clone(),
        };
        if let Some(body) = fetched.body.as_deref() {
            match serde_json::from_str::<serde_json::Value>(body) {
                Ok(payload) => {
                    let result = payload
                        .get("data")
                        .and_then(|data| data.get("result"))
                        .and_then(|result| result.as_array())
                        .cloned()
                        .unwrap_or_default();
                    row.ok = true;
                    row.series_present = !result.is_empty();
                    row.value = result
                        .first()
                        .and_then(|first| first.get("value"))
                        .and_then(|value| value.as_array())
                        .and_then(|value| value.get(1))
                        .and_then(|value| value.as_str())
                        .and_then(|value| value.parse::<f64>().ok());
                }
                Err(err) => row.error = Some(format!("invalid prometheus response: {err}")),
            }
        }
        rows.push(row);
    }
    rows
}

fn archive_convergence_measure_summary(
    samples: &[ArchiveConvergenceMeasureSample],
) -> ArchiveConvergenceMeasureSummary {
    archive_convergence_measure_summary_with_min_elapsed(samples, 0)
}

fn archive_convergence_measure_summary_with_min_elapsed(
    samples: &[ArchiveConvergenceMeasureSample],
    min_elapsed_ms: u64,
) -> ArchiveConvergenceMeasureSummary {
    let mut reasons = Vec::new();
    if samples.is_empty() {
        return ArchiveConvergenceMeasureSummary {
            ok: false,
            elapsed_ms: 0,
            sample_count: 0,
            reasons: vec!["no_samples".to_string()],
            prometheus_series_present_count: 0,
            latest_matching_duplicate_sequences: None,
            latest_divergent_sequences: None,
        };
    }
    let elapsed_ms = samples
        .last()
        .and_then(|last| {
            samples
                .first()
                .map(|first| last.sample_unix_ms.saturating_sub(first.sample_unix_ms))
        })
        .unwrap_or(0);
    if samples.len() < 2 || elapsed_ms == 0 {
        reasons.push("insufficient_elapsed_samples".to_string());
    }
    if min_elapsed_ms > 0 && elapsed_ms < min_elapsed_ms {
        reasons.push("elapsed_window_too_short".to_string());
    }
    let latest = samples.last().expect("checked non-empty samples");
    if latest.sources.iter().any(|source| !source.ok) {
        reasons.push("source_fetch_failed".to_string());
    }
    let (matching, divergent) = match latest.convergence.as_ref() {
        Some(report) => {
            if !report.ok {
                reasons.push("archive_convergence_failed".to_string());
                reasons.extend(
                    report
                        .reasons
                        .iter()
                        .map(|reason| format!("archive_{reason}")),
                );
            }
            let matching = report.summary.matching_duplicate_sequences.len();
            if matching == 0 {
                reasons.push("no_matching_duplicate_sequences".to_string());
            }
            let divergent = report.summary.divergent_sequences.len();
            (Some(matching), Some(divergent))
        }
        None => {
            reasons.push("archive_convergence_missing".to_string());
            (None, None)
        }
    };
    if !latest.prometheus.is_empty() {
        let series_count = latest
            .prometheus
            .iter()
            .filter(|row| row.series_present)
            .count();
        if series_count == 0 {
            reasons.push("prometheus_duplicate_series_missing".to_string());
        }
        if latest.prometheus.iter().any(|row| !row.ok) {
            reasons.push("prometheus_query_failed".to_string());
        }
        if latest.prometheus.iter().any(|row| {
            row.metric.ends_with("hash_divergent_sequences") && row.value.unwrap_or(0.0) > 0.0
        }) {
            reasons.push("prometheus_hash_divergence_nonzero".to_string());
        }
        if latest.prometheus.iter().any(|row| {
            row.metric.ends_with("missing_source_identity_records")
                && row.value.unwrap_or(0.0) > 0.0
        }) {
            reasons.push("prometheus_source_identity_missing_nonzero".to_string());
        }
    }
    reasons.sort();
    reasons.dedup();
    ArchiveConvergenceMeasureSummary {
        ok: reasons.is_empty(),
        elapsed_ms,
        sample_count: samples.len(),
        reasons,
        prometheus_series_present_count: latest
            .prometheus
            .iter()
            .filter(|row| row.series_present)
            .count(),
        latest_matching_duplicate_sequences: matching,
        latest_divergent_sequences: divergent,
    }
}

async fn archive_convergence_measure_report(
    args: &ArchiveConvergenceMeasureArgs,
) -> Result<ArchiveConvergenceMeasureReport> {
    if args.agent_manifest.is_empty()
        && args.agent_prometheus_sd.is_empty()
        && args.manifest.is_empty()
    {
        return Err(anyhow!(
            "archive-convergence-measure requires at least two --agent-manifest or --manifest sources"
        ));
    }
    if args.agent_prometheus_sd.is_empty() && args.agent_manifest.len() + args.manifest.len() < 2 {
        return Err(anyhow!(
            "archive-convergence-measure requires at least two manifest sources"
        ));
    }
    if args.timeout_ms == 0 {
        return Err(anyhow!("--timeout-ms must be greater than 0"));
    }
    if args.max_manifest_bytes == 0 {
        return Err(anyhow!("--max-manifest-bytes must be greater than 0"));
    }
    let client = reqwest::Client::builder()
        .user_agent("every-channel-archive-convergence-measure/1")
        .build()
        .context("failed to build HTTP client")?;
    let started = Instant::now();
    let mut samples = Vec::new();
    loop {
        let sample = archive_convergence_measure_sample(args, &client, samples.len()).await;
        samples.push(sample);
        if args.duration_seconds <= 0.0 || started.elapsed().as_secs_f64() >= args.duration_seconds
        {
            break;
        }
        tokio::time::sleep(Duration::from_secs_f64(
            args.poll_interval_seconds.max(0.001),
        ))
        .await;
    }
    let summary = archive_convergence_measure_summary(&samples);
    Ok(ArchiveConvergenceMeasureReport {
        ok: summary.ok,
        broadcast: args.broadcast.clone(),
        track: args.track.clone(),
        duration_seconds: args.duration_seconds,
        summary,
        samples,
    })
}

async fn archive_convergence_measure_command(args: ArchiveConvergenceMeasureArgs) -> Result<()> {
    let report = archive_convergence_measure_report(&args).await?;
    if args.pretty {
        println!("{}", serde_json::to_string_pretty(&report)?);
    } else {
        println!("{}", serde_json::to_string(&report)?);
    }
    if args.require_ok && !report.ok {
        return Err(anyhow!(
            "archive convergence measurement failed for {}/{}: {}",
            report.broadcast,
            report.track,
            report.summary.reasons.join(",")
        ));
    }
    Ok(())
}

#[derive(Debug, Default)]
struct ArchiveConvergenceMeasureServeState {
    samples: VecDeque<ArchiveConvergenceMeasureSample>,
    next_sample_index: usize,
}

fn archive_convergence_measure_args_from_serve(
    args: &ArchiveConvergenceMeasureServeArgs,
) -> ArchiveConvergenceMeasureArgs {
    ArchiveConvergenceMeasureArgs {
        agent_manifest: args.agent_manifest.clone(),
        agent_prometheus_sd: args.agent_prometheus_sd.clone(),
        agent_prometheus_sd_label: args.agent_prometheus_sd_label.clone(),
        agent_manifest_role: args.agent_manifest_role.clone(),
        manifest: args.manifest.clone(),
        broadcast: args.broadcast.clone(),
        track: args.track.clone(),
        stream_id: args.stream_id.clone(),
        rendition: args.rendition.clone(),
        start_sequence: args.start_sequence,
        end_sequence: args.end_sequence,
        prometheus_url: args.prometheus_url.clone(),
        duration_seconds: 0.0,
        poll_interval_seconds: 30.0,
        timeout_ms: args.timeout_ms,
        max_manifest_bytes: args.max_manifest_bytes,
        pretty: false,
        require_ok: false,
    }
}

fn archive_convergence_args_from_measure_serve(
    args: &ArchiveConvergenceMeasureServeArgs,
) -> ArchiveConvergenceArgs {
    ArchiveConvergenceArgs {
        source: Vec::new(),
        broadcast: args.broadcast.clone(),
        track: args.track.clone(),
        stream_id: args.stream_id.clone(),
        rendition: args.rendition.clone(),
        start_sequence: args.start_sequence,
        end_sequence: args.end_sequence,
        pretty: false,
        prometheus: true,
        metrics_node: args.metrics_node.clone(),
        metrics_role: args.metrics_role.clone(),
        require_ok: false,
    }
}

fn archive_convergence_measure_report_from_samples(
    args: &ArchiveConvergenceMeasureServeArgs,
    samples: Vec<ArchiveConvergenceMeasureSample>,
) -> ArchiveConvergenceMeasureReport {
    let min_elapsed_ms =
        Duration::from_secs_f64(args.min_elapsed_seconds.max(0.0)).as_millis() as u64;
    let summary = archive_convergence_measure_summary_with_min_elapsed(&samples, min_elapsed_ms);
    ArchiveConvergenceMeasureReport {
        ok: summary.ok,
        broadcast: args.broadcast.clone(),
        track: args.track.clone(),
        duration_seconds: args.min_elapsed_seconds,
        summary,
        samples,
    }
}

fn archive_convergence_measure_prometheus_metrics(
    report: &ArchiveConvergenceMeasureReport,
    args: &ArchiveConvergenceMeasureServeArgs,
) -> String {
    let latest = report.samples.last();
    let latest_convergence = latest.and_then(|sample| sample.convergence.as_ref());
    let stream_id = latest_convergence
        .map(|convergence| convergence.stream_id.clone())
        .unwrap_or_else(|| {
            archive_convergence_stream_id(&args.broadcast, args.stream_id.as_deref())
        });
    let rendition_id = latest_convergence
        .map(|convergence| convergence.rendition_id.clone())
        .unwrap_or_else(|| {
            archive_convergence_rendition(&args.broadcast, &args.track, args.rendition.as_deref())
        });
    let labels = archive_convergence_prometheus_labels(
        &report.broadcast,
        &report.track,
        &stream_id,
        &rendition_id,
        args.metrics_node.as_deref(),
        &args.metrics_role,
    );

    let mut output = String::new();
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_convergence_measure_scrape_ok",
        &labels,
        1.0,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_convergence_measure_ok",
        &labels,
        if report.ok { 1.0 } else { 0.0 },
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_convergence_measure_elapsed_seconds",
        &labels,
        report.summary.elapsed_ms as f64 / 1000.0,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_convergence_measure_min_elapsed_seconds",
        &labels,
        args.min_elapsed_seconds.max(0.0),
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_convergence_measure_samples",
        &labels,
        report.summary.sample_count as f64,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_convergence_measure_prometheus_series_present",
        &labels,
        report.summary.prometheus_series_present_count as f64,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_convergence_measure_matching_duplicate_sequences",
        &labels,
        report
            .summary
            .latest_matching_duplicate_sequences
            .unwrap_or(0) as f64,
    );
    push_prometheus_gauge(
        &mut output,
        "every_channel_archive_convergence_measure_divergent_sequences",
        &labels,
        report.summary.latest_divergent_sequences.unwrap_or(0) as f64,
    );

    for reason in &report.summary.reasons {
        let mut reason_labels = labels.clone();
        reason_labels.insert("reason".to_string(), reason.clone());
        push_prometheus_gauge(
            &mut output,
            "every_channel_archive_convergence_measure_reason",
            &reason_labels,
            1.0,
        );
    }

    if let Some(sample) = latest {
        for source in &sample.sources {
            let mut source_labels = labels.clone();
            source_labels.insert("source".to_string(), source.name.clone());
            source_labels.insert("kind".to_string(), source.kind.clone());
            push_prometheus_gauge(
                &mut output,
                "every_channel_archive_convergence_measure_source_fetch_ok",
                &source_labels,
                if source.ok { 1.0 } else { 0.0 },
            );
            push_prometheus_gauge(
                &mut output,
                "every_channel_archive_convergence_measure_source_fetch_elapsed_seconds",
                &source_labels,
                source.elapsed_ms as f64 / 1000.0,
            );
            push_prometheus_gauge(
                &mut output,
                "every_channel_archive_convergence_measure_source_records",
                &source_labels,
                source.record_count as f64,
            );
            push_prometheus_gauge(
                &mut output,
                "every_channel_archive_convergence_measure_source_invalid_records",
                &source_labels,
                source.invalid_record_count as f64,
            );
            push_prometheus_gauge(
                &mut output,
                "every_channel_archive_convergence_measure_source_http_status",
                &source_labels,
                source.status.unwrap_or(0) as f64,
            );
        }

        for prometheus in &sample.prometheus {
            let mut prometheus_labels = labels.clone();
            prometheus_labels.insert("metric".to_string(), prometheus.metric.clone());
            push_prometheus_gauge(
                &mut output,
                "every_channel_archive_convergence_measure_prometheus_query_ok",
                &prometheus_labels,
                if prometheus.ok { 1.0 } else { 0.0 },
            );
            push_prometheus_gauge(
                &mut output,
                "every_channel_archive_convergence_measure_prometheus_query_series_present",
                &prometheus_labels,
                if prometheus.series_present { 1.0 } else { 0.0 },
            );
            if let Some(value) = prometheus.value {
                push_prometheus_gauge(
                    &mut output,
                    "every_channel_archive_convergence_measure_prometheus_query_value",
                    &prometheus_labels,
                    value,
                );
            }
        }

        if let Some(err) = sample.convergence_error.as_deref() {
            output.push_str("# archive_convergence_measure_error ");
            output.push_str(&prometheus_escape_label_value(err));
            output.push('\n');
        }
    }

    let convergence_args = archive_convergence_args_from_measure_serve(args);
    if let Some(convergence) = latest_convergence {
        output.push_str(&archive_convergence_prometheus_metrics(
            convergence,
            &convergence_args,
        ));
    } else {
        let err = anyhow!(
            "latest archive convergence measurement sample did not produce a convergence report"
        );
        output.push_str(&archive_convergence_prometheus_error_metrics(
            &convergence_args,
            &err,
        ));
    }

    output
}

async fn archive_convergence_measure_serve_response(
    args: &ArchiveConvergenceMeasureServeArgs,
    state: &Arc<Mutex<ArchiveConvergenceMeasureServeState>>,
    client: &reqwest::Client,
    method: &str,
    target: &str,
) -> ArchiveHttpResponse {
    let path = target.split('?').next().unwrap_or(target);
    if method != "GET" && method != "HEAD" {
        return archive_error(405, "method not allowed");
    }
    match path {
        "/health" => archive_response(
            200,
            "application/json",
            br#"{"ok":true,"service":"archive-convergence-measure-serve"}"#.to_vec(),
        ),
        "/metrics" => {
            let measure_args = archive_convergence_measure_args_from_serve(args);
            let sample_index = {
                let mut guard = state
                    .lock()
                    .expect("archive convergence measure state poisoned");
                let index = guard.next_sample_index;
                guard.next_sample_index = guard.next_sample_index.saturating_add(1);
                index
            };
            let sample =
                archive_convergence_measure_sample(&measure_args, client, sample_index).await;
            let samples = {
                let mut guard = state
                    .lock()
                    .expect("archive convergence measure state poisoned");
                guard.samples.push_back(sample);
                while guard.samples.len() > args.max_samples {
                    guard.samples.pop_front();
                }
                guard.samples.iter().cloned().collect::<Vec<_>>()
            };
            let report = archive_convergence_measure_report_from_samples(args, samples);
            let body = archive_convergence_measure_prometheus_metrics(&report, args);
            archive_response(
                200,
                "text/plain; version=0.0.4; charset=utf-8",
                body.into_bytes(),
            )
        }
        _ => archive_error(404, "not found"),
    }
}

async fn handle_archive_convergence_measure_serve_connection(
    mut stream: tokio::net::TcpStream,
    args: Arc<ArchiveConvergenceMeasureServeArgs>,
    state: Arc<Mutex<ArchiveConvergenceMeasureServeState>>,
    client: reqwest::Client,
) -> Result<()> {
    let mut buffer = vec![0_u8; 4096];
    let read = stream
        .read(&mut buffer)
        .await
        .context("failed to read archive convergence measure HTTP request")?;
    if read == 0 {
        return Ok(());
    }
    let request = String::from_utf8_lossy(&buffer[..read]);
    let first_line = request.lines().next().unwrap_or_default();
    let mut parts = first_line.split_whitespace();
    let method = parts.next().unwrap_or_default();
    let target = parts.next().unwrap_or("/");
    let response =
        archive_convergence_measure_serve_response(&args, &state, &client, method, target).await;
    let include_body = method != "HEAD";
    let body_len = if include_body { response.body.len() } else { 0 };
    let status_text = match response.status {
        200 => "OK",
        404 => "Not Found",
        405 => "Method Not Allowed",
        _ => "Error",
    };
    let header = format!(
        "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
        response.status, status_text, response.content_type, body_len
    );
    stream.write_all(header.as_bytes()).await?;
    if include_body {
        stream.write_all(&response.body).await?;
    }
    Ok(())
}

async fn archive_convergence_measure_serve(args: ArchiveConvergenceMeasureServeArgs) -> Result<()> {
    if args.agent_manifest.is_empty()
        && args.agent_prometheus_sd.is_empty()
        && args.manifest.is_empty()
    {
        return Err(anyhow!(
            "archive-convergence-measure-serve requires at least two --agent-manifest or --manifest sources"
        ));
    }
    if args.agent_prometheus_sd.is_empty() && args.agent_manifest.len() + args.manifest.len() < 2 {
        return Err(anyhow!(
            "archive-convergence-measure-serve requires at least two manifest sources"
        ));
    }
    if args.timeout_ms == 0 {
        return Err(anyhow!("--timeout-ms must be greater than 0"));
    }
    if args.max_manifest_bytes == 0 {
        return Err(anyhow!("--max-manifest-bytes must be greater than 0"));
    }
    if args.max_samples < 2 {
        return Err(anyhow!("--max-samples must be at least 2"));
    }
    if !args.min_elapsed_seconds.is_finite() || args.min_elapsed_seconds < 0.0 {
        return Err(anyhow!(
            "--min-elapsed-seconds must be a finite non-negative value"
        ));
    }
    for source in args.agent_manifest.iter().chain(args.manifest.iter()) {
        parse_named_endpoint(source)?;
    }
    for filter in &args.agent_prometheus_sd_label {
        parse_label_filter(filter)?;
    }

    let listener = TcpListener::bind(&args.listen)
        .await
        .with_context(|| format!("failed to bind {}", args.listen))?;
    let client = reqwest::Client::builder()
        .user_agent("every-channel-archive-convergence-measure-serve/1")
        .build()
        .context("failed to build HTTP client")?;
    tracing::info!(
        listen = %args.listen,
        broadcast = %args.broadcast,
        track = %args.track,
        "serving remote archive convergence measurement metrics"
    );
    let args = Arc::new(args);
    let state = Arc::new(Mutex::new(ArchiveConvergenceMeasureServeState::default()));
    loop {
        let (stream, peer) = listener
            .accept()
            .await
            .context("failed to accept archive convergence measurement metrics connection")?;
        let args_clone = args.clone();
        let state_clone = state.clone();
        let client_clone = client.clone();
        tokio::spawn(async move {
            if let Err(err) = handle_archive_convergence_measure_serve_connection(
                stream,
                args_clone,
                state_clone,
                client_clone,
            )
            .await
            {
                tracing::debug!(peer = %peer, err = %err, "archive convergence measurement metrics request failed");
            }
        });
    }
}

fn archive_convergence_args_from_serve(
    args: &ArchiveConvergenceServeArgs,
) -> ArchiveConvergenceArgs {
    ArchiveConvergenceArgs {
        source: args.source.clone(),
        broadcast: args.broadcast.clone(),
        track: args.track.clone(),
        stream_id: args.stream_id.clone(),
        rendition: args.rendition.clone(),
        start_sequence: args.start_sequence,
        end_sequence: args.end_sequence,
        pretty: false,
        prometheus: true,
        metrics_node: args.metrics_node.clone(),
        metrics_role: args.metrics_role.clone(),
        require_ok: false,
    }
}

fn archive_convergence_serve_response(
    args: &ArchiveConvergenceServeArgs,
    method: &str,
    target: &str,
) -> ArchiveHttpResponse {
    let path = target.split('?').next().unwrap_or(target);
    if method != "GET" && method != "HEAD" {
        return archive_error(405, "method not allowed");
    }
    match path {
        "/health" => archive_response(
            200,
            "application/json",
            br#"{"ok":true,"service":"archive-convergence-serve"}"#.to_vec(),
        ),
        "/metrics" => {
            let convergence_args = archive_convergence_args_from_serve(args);
            let body = match archive_convergence_report(&convergence_args) {
                Ok(report) => archive_convergence_prometheus_metrics(&report, &convergence_args),
                Err(err) => archive_convergence_prometheus_error_metrics(&convergence_args, &err),
            };
            archive_response(
                200,
                "text/plain; version=0.0.4; charset=utf-8",
                body.into_bytes(),
            )
        }
        _ => archive_error(404, "not found"),
    }
}

async fn handle_archive_convergence_serve_connection(
    mut stream: tokio::net::TcpStream,
    args: Arc<ArchiveConvergenceServeArgs>,
) -> Result<()> {
    let mut buffer = vec![0_u8; 4096];
    let read = stream
        .read(&mut buffer)
        .await
        .context("failed to read archive convergence HTTP request")?;
    if read == 0 {
        return Ok(());
    }
    let request = String::from_utf8_lossy(&buffer[..read]);
    let first_line = request.lines().next().unwrap_or_default();
    let mut parts = first_line.split_whitespace();
    let method = parts.next().unwrap_or_default();
    let target = parts.next().unwrap_or("/");
    let response = archive_convergence_serve_response(&args, method, target);
    let include_body = method != "HEAD";
    let body_len = if include_body { response.body.len() } else { 0 };
    let status_text = match response.status {
        200 => "OK",
        404 => "Not Found",
        405 => "Method Not Allowed",
        _ => "Error",
    };
    let header = format!(
        "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
        response.status, status_text, response.content_type, body_len
    );
    stream.write_all(header.as_bytes()).await?;
    if include_body {
        stream.write_all(&response.body).await?;
    }
    Ok(())
}

async fn archive_convergence_serve(args: ArchiveConvergenceServeArgs) -> Result<()> {
    if args.source.len() < 2 {
        return Err(anyhow!(
            "archive-convergence-serve requires at least two --source NAME=PATH entries"
        ));
    }
    for source in &args.source {
        parse_named_path(source)?;
    }

    let listener = TcpListener::bind(&args.listen)
        .await
        .with_context(|| format!("failed to bind {}", args.listen))?;
    tracing::info!(
        listen = %args.listen,
        broadcast = %args.broadcast,
        track = %args.track,
        "serving archive convergence metrics"
    );
    let args = Arc::new(args);
    loop {
        let (stream, peer) = listener
            .accept()
            .await
            .context("failed to accept archive convergence metrics connection")?;
        let args_clone = args.clone();
        tokio::spawn(async move {
            if let Err(err) = handle_archive_convergence_serve_connection(stream, args_clone).await
            {
                tracing::debug!(peer = %peer, err = %err, "archive convergence metrics request failed");
            }
        });
    }
}

#[derive(Debug, serde::Serialize)]
struct SimControlPlaneOutput {
    ok: bool,
    invariant: ec_core::sim::ControlPlanePropagationInvariantConfig,
    scenario_template: ec_core::sim::ControlPlanePropagationScenario,
    campaign: ec_core::sim::ControlPlanePropagationCampaignReport,
}

fn sim_control_plane_output(args: &SimControlPlaneArgs) -> Result<SimControlPlaneOutput> {
    if let Some(path) = &args.scenario_json {
        return sim_control_plane_replay_output(args, path);
    }
    if args.iterations == 0 {
        return Err(anyhow!("sim-control-plane requires --iterations > 0"));
    }
    if args.transient_drop_per_million > 1_000_000 {
        return Err(anyhow!(
            "--transient-drop-per-million must be <= 1000000, got {}",
            args.transient_drop_per_million
        ));
    }

    let nodes = sim_control_plane_nodes(args)?;
    let partitions = if args.partitions.is_empty() {
        vec![
            ec_core::sim::SimulationPartition::new("relay-hel", 70, 190, 55),
            ec_core::sim::SimulationPartition::new("tower", 220, 310, 40),
        ]
    } else {
        args.partitions
            .iter()
            .map(|value| parse_sim_partition(value))
            .collect::<Result<Vec<_>>>()?
    };
    let node_outages = if args.node_outages.is_empty() {
        vec![ec_core::sim::SimulationOutage::new(
            "relay-nyc",
            105,
            205,
            45,
        )]
    } else {
        args.node_outages
            .iter()
            .map(|value| parse_sim_node_outage(value))
            .collect::<Result<Vec<_>>>()?
    };

    let invariant = ec_core::sim::ControlPlanePropagationInvariantConfig {
        require_node_count: nodes.len() as u64,
        require_complete: true,
        max_propagation_complete_ms: if args.max_propagation_complete_ms == 0 {
            None
        } else {
            Some(args.max_propagation_complete_ms)
        },
    };

    let build_scenario = |seed| {
        let mut scenario = ec_core::sim::ControlPlanePropagationScenario::new(
            seed,
            nodes.clone(),
            &args.origin_node,
            &args.topic,
            &args.announcement_id,
        );
        scenario.fanout = args.fanout;
        scenario.gossip_interval_ms = args.gossip_interval_ms;
        scenario.max_gossip_rounds = args.max_gossip_rounds;
        scenario.base_network_delay_ms = args.base_network_delay_ms;
        scenario.max_jitter_ms = args.max_jitter_ms;
        scenario.transient_drop_per_million = args.transient_drop_per_million;
        scenario.partitions = partitions.clone();
        scenario.node_outages = node_outages.clone();
        scenario
    };

    let seed_start = ec_core::sim::SimulationSeed::new(args.seed);
    let scenario_template = build_scenario(seed_start);
    let campaign = ec_core::sim::run_control_plane_propagation_campaign(
        "control-plane-propagation-fault-campaign",
        seed_start,
        args.iterations,
        &invariant,
        build_scenario,
    );
    let ok = campaign.all_passed();

    Ok(SimControlPlaneOutput {
        ok,
        invariant,
        scenario_template,
        campaign,
    })
}

fn sim_control_plane_replay_output(
    args: &SimControlPlaneArgs,
    path: &Path,
) -> Result<SimControlPlaneOutput> {
    let scenario = read_sim_control_plane_scenario(path)?;
    validate_sim_control_plane_scenario(&scenario)?;

    let invariant = ec_core::sim::ControlPlanePropagationInvariantConfig {
        require_node_count: scenario.nodes.len() as u64,
        require_complete: true,
        max_propagation_complete_ms: if args.max_propagation_complete_ms == 0 {
            None
        } else {
            Some(args.max_propagation_complete_ms)
        },
    };
    let seed_start = scenario.seed;
    let scenario_template = scenario.clone();
    let campaign = ec_core::sim::run_control_plane_propagation_campaign(
        "control-plane-replay-scenario",
        seed_start,
        1,
        &invariant,
        |_| scenario.clone(),
    );
    let ok = campaign.all_passed();

    Ok(SimControlPlaneOutput {
        ok,
        invariant,
        scenario_template,
        campaign,
    })
}

fn read_sim_control_plane_scenario(
    path: &Path,
) -> Result<ec_core::sim::ControlPlanePropagationScenario> {
    let mut bytes = Vec::new();
    if path == Path::new("-") {
        std::io::stdin()
            .read_to_end(&mut bytes)
            .context("failed to read control-plane simulation scenario from stdin")?;
    } else {
        bytes = fs::read(path).with_context(|| {
            format!(
                "failed to read control-plane simulation scenario {}",
                path.display()
            )
        })?;
    }
    serde_json::from_slice::<ec_core::sim::ControlPlanePropagationScenario>(&bytes).with_context(
        || {
            format!(
                "failed to parse control-plane simulation scenario JSON from {}",
                path.display()
            )
        },
    )
}

fn validate_sim_control_plane_scenario(
    scenario: &ec_core::sim::ControlPlanePropagationScenario,
) -> Result<()> {
    if scenario.nodes.len() < 2 {
        return Err(anyhow!(
            "control-plane simulation scenario requires at least two nodes"
        ));
    }
    if !scenario.nodes.contains(&scenario.origin_node) {
        return Err(anyhow!(
            "control-plane simulation origin node {} is not in node list",
            scenario.origin_node
        ));
    }
    if scenario.topic.trim().is_empty() || scenario.announcement_id.trim().is_empty() {
        return Err(anyhow!(
            "control-plane simulation topic and announcement id cannot be empty"
        ));
    }
    if scenario.transient_drop_per_million > 1_000_000 {
        return Err(anyhow!(
            "control-plane simulation transient_drop_per_million must be <= 1000000"
        ));
    }
    for partition in &scenario.partitions {
        if partition.source_node.trim().is_empty() {
            return Err(anyhow!(
                "control-plane partition source node cannot be empty"
            ));
        }
        if !scenario.nodes.contains(&partition.source_node) {
            return Err(anyhow!(
                "control-plane partition references unknown node {}",
                partition.source_node
            ));
        }
        if partition.end_ms <= partition.start_ms {
            return Err(anyhow!(
                "control-plane partition end_ms must be greater than start_ms for {}",
                partition.source_node
            ));
        }
    }
    for outage in &scenario.node_outages {
        if outage.source_node.trim().is_empty() {
            return Err(anyhow!(
                "control-plane node outage source node cannot be empty"
            ));
        }
        if !scenario.nodes.contains(&outage.source_node) {
            return Err(anyhow!(
                "control-plane node outage references unknown node {}",
                outage.source_node
            ));
        }
        if outage.end_ms <= outage.start_ms {
            return Err(anyhow!(
                "control-plane node outage end_ms must be greater than start_ms for {}",
                outage.source_node
            ));
        }
    }
    Ok(())
}

fn sim_control_plane_command(args: SimControlPlaneArgs) -> Result<()> {
    let output = sim_control_plane_output(&args)?;
    if args.pretty {
        println!("{}", serde_json::to_string_pretty(&output)?);
    } else {
        println!("{}", serde_json::to_string(&output)?);
    }
    if !output.ok && !args.allow_failure {
        let replay_hint = output
            .campaign
            .first_failure
            .as_ref()
            .map(|failure| failure.replay_hint.as_str())
            .unwrap_or("no replay hint");
        return Err(anyhow!(
            "control-plane simulation invariants failed: {replay_hint}"
        ));
    }
    Ok(())
}

fn sim_control_plane_nodes(args: &SimControlPlaneArgs) -> Result<Vec<String>> {
    let nodes = if args.nodes.is_empty() {
        vec![
            "nuc-a".to_string(),
            "nuc-b".to_string(),
            "tower".to_string(),
            "forge".to_string(),
            "relay-lax".to_string(),
            "relay-nyc".to_string(),
            "relay-hel".to_string(),
        ]
    } else {
        args.nodes
            .iter()
            .map(|node| node.trim())
            .filter(|node| !node.is_empty())
            .map(ToString::to_string)
            .collect::<Vec<_>>()
    };
    if nodes.len() < 2 {
        return Err(anyhow!(
            "sim-control-plane requires at least two --node values"
        ));
    }
    let mut unique = BTreeSet::new();
    for node in &nodes {
        if !unique.insert(node.clone()) {
            return Err(anyhow!("sim-control-plane node {} is duplicated", node));
        }
    }
    if !nodes.contains(&args.origin_node) {
        return Err(anyhow!(
            "sim-control-plane origin node {} is not in node list",
            args.origin_node
        ));
    }
    Ok(nodes)
}

fn parse_sim_node_outage(value: &str) -> Result<ec_core::sim::SimulationOutage> {
    let parts = value.split(':').collect::<Vec<_>>();
    if parts.len() != 4 {
        return Err(anyhow!(
            "expected node outage NODE:START_MS:END_MS:RECOVERY_DELAY_MS, got {value:?}"
        ));
    }
    let source_node = parts[0].trim();
    if source_node.is_empty() {
        return Err(anyhow!("node outage source node cannot be empty"));
    }
    let start_ms = parse_sim_u64("node outage start_ms", parts[1])?;
    let end_ms = parse_sim_u64("node outage end_ms", parts[2])?;
    let recovery_delay_ms = parse_sim_u64("node outage recovery_delay_ms", parts[3])?;
    if end_ms <= start_ms {
        return Err(anyhow!(
            "node outage end_ms must be greater than start_ms for {source_node}"
        ));
    }
    Ok(ec_core::sim::SimulationOutage::new(
        source_node,
        start_ms,
        end_ms,
        recovery_delay_ms,
    ))
}

#[derive(Debug, serde::Serialize)]
struct SimSystemOutput {
    ok: bool,
    invariant: ec_core::sim::SystemDuplicatePublisherInvariantConfig,
    fault_coverage: SimSystemFaultCoverageReport,
    runtime: SimSystemRuntimeReport,
    scenario_template: ec_core::sim::SystemDuplicatePublisherScenario,
    campaign: ec_core::sim::SystemDuplicatePublisherCampaignReport,
}

#[derive(Debug, serde::Serialize)]
struct SimSystemFaultCoverageReport {
    required: bool,
    ok: bool,
    min_seed_coverage: u64,
    failures: Vec<String>,
}

#[derive(Debug, serde::Serialize)]
struct SimSystemRuntimeReport {
    wall_elapsed_ms: f64,
    iterations_per_second: f64,
    simulated_system_seconds_per_wall_second: f64,
    trace_events_per_second: f64,
}

fn sim_system_output(args: &SimSystemArgs) -> Result<SimSystemOutput> {
    if let Some(path) = &args.scenario_json {
        return sim_system_replay_output(args, path);
    }
    if args.iterations == 0 {
        return Err(anyhow!("sim-system requires --iterations > 0"));
    }
    if args.sequence_count == 0 {
        return Err(anyhow!("sim-system requires --sequence-count > 0"));
    }

    let nodes = sim_system_nodes(args)?;
    let publishers = sim_system_publishers(args, &nodes)?;
    let publisher_source_material =
        sim_publisher_source_material(&args.publisher_source_material, &publishers)?;
    let invariant = ec_core::sim::SystemDuplicatePublisherInvariantConfig {
        require_control_complete: true,
        require_media_duplicate_complete: true,
        require_media_timing: true,
        max_system_complete_ms: if args.max_system_complete_ms == 0 {
            None
        } else {
            Some(args.max_system_complete_ms)
        },
    };

    let build_scenario = |seed| {
        sim_system_default_scenario(args, &nodes, &publishers, &publisher_source_material, seed)
    };
    let seed_start = ec_core::sim::SimulationSeed::new(args.seed);
    let scenario_template = build_scenario(seed_start);
    let runtime_start = Instant::now();
    let campaign = ec_core::sim::run_system_duplicate_publisher_campaign(
        "system-duplicate-publisher-fault-campaign",
        seed_start,
        args.iterations,
        &invariant,
        build_scenario,
    );
    let runtime = sim_system_runtime_report(runtime_start, &campaign);
    let fault_coverage = sim_system_fault_coverage_report(args, &campaign);
    let ok = campaign.all_passed() && fault_coverage.ok;

    Ok(SimSystemOutput {
        ok,
        invariant,
        fault_coverage,
        runtime,
        scenario_template,
        campaign,
    })
}

fn sim_system_replay_output(args: &SimSystemArgs, path: &Path) -> Result<SimSystemOutput> {
    let scenario = read_sim_system_scenario(path)?;
    validate_sim_system_scenario(&scenario)?;

    let invariant = ec_core::sim::SystemDuplicatePublisherInvariantConfig {
        require_control_complete: true,
        require_media_duplicate_complete: true,
        require_media_timing: true,
        max_system_complete_ms: if args.max_system_complete_ms == 0 {
            None
        } else {
            Some(args.max_system_complete_ms)
        },
    };
    let seed_start = scenario.seed;
    let scenario_template = scenario.clone();
    let runtime_start = Instant::now();
    let campaign = ec_core::sim::run_system_duplicate_publisher_campaign(
        "system-duplicate-publisher-replay-scenario",
        seed_start,
        1,
        &invariant,
        |_| scenario.clone(),
    );
    let runtime = sim_system_runtime_report(runtime_start, &campaign);
    let fault_coverage = sim_system_fault_coverage_report(args, &campaign);
    let ok = campaign.all_passed() && fault_coverage.ok;

    Ok(SimSystemOutput {
        ok,
        invariant,
        fault_coverage,
        runtime,
        scenario_template,
        campaign,
    })
}

fn sim_system_fault_coverage_report(
    args: &SimSystemArgs,
    campaign: &ec_core::sim::SystemDuplicatePublisherCampaignReport,
) -> SimSystemFaultCoverageReport {
    let required =
        args.require_fault_coverage || args.fault_profile == SimSystemFaultProfile::FoundationDb;
    let min_seed_coverage = sim_system_min_fault_seed_coverage(args, campaign);
    let failures = if required {
        sim_system_fault_coverage_failures(campaign, min_seed_coverage)
    } else {
        Vec::new()
    };
    SimSystemFaultCoverageReport {
        required,
        ok: failures.is_empty(),
        min_seed_coverage,
        failures,
    }
}

fn sim_system_min_fault_seed_coverage(
    args: &SimSystemArgs,
    campaign: &ec_core::sim::SystemDuplicatePublisherCampaignReport,
) -> u64 {
    if args.min_fault_seed_coverage > 0 {
        return args.min_fault_seed_coverage;
    }
    if args.fault_profile == SimSystemFaultProfile::FoundationDb {
        return (campaign.iterations / 32).max(2).min(campaign.iterations);
    }
    1
}

fn sim_system_fault_coverage_failures(
    campaign: &ec_core::sim::SystemDuplicatePublisherCampaignReport,
    min_seed_coverage: u64,
) -> Vec<String> {
    let mut failures = campaign.fault_coverage_failures();
    let thresholds = [
        (
            "control_transient_drops",
            campaign.seeds_with_control_transient_drops,
        ),
        (
            "control_partition_delays",
            campaign.seeds_with_control_partition_delays,
        ),
        (
            "control_node_outage_delays",
            campaign.seeds_with_control_node_outage_delays,
        ),
        (
            "control_duplicate_messages",
            campaign.seeds_with_control_duplicate_messages,
        ),
        (
            "media_transient_drops",
            campaign.seeds_with_media_transient_drops,
        ),
        (
            "media_partition_delays",
            campaign.seeds_with_media_partition_delays,
        ),
        (
            "media_publisher_outages",
            campaign.seeds_with_media_publisher_outages,
        ),
        (
            "media_backfill",
            campaign.seeds_with_media_backfill_observations,
        ),
    ];
    for (name, observed) in thresholds {
        if observed < min_seed_coverage {
            failures.push(format!("{name}_seed_coverage_below_min"));
        }
    }
    if campaign.total_trace_events == 0 {
        failures.push("trace_events_unobserved".to_string());
    }
    if campaign.slowest_system_runs.is_empty() {
        failures.push("slowest_system_runs_unobserved".to_string());
    }
    failures
}

fn sim_system_runtime_report(
    started_at: Instant,
    campaign: &ec_core::sim::SystemDuplicatePublisherCampaignReport,
) -> SimSystemRuntimeReport {
    let elapsed = started_at.elapsed();
    let wall_elapsed_ms = elapsed.as_secs_f64() * 1000.0;
    let wall_elapsed_seconds = elapsed.as_secs_f64().max(0.000_001);
    SimSystemRuntimeReport {
        wall_elapsed_ms,
        iterations_per_second: campaign.iterations as f64 / wall_elapsed_seconds,
        simulated_system_seconds_per_wall_second: campaign.total_system_complete_ms_observed as f64
            / 1000.0
            / wall_elapsed_seconds,
        trace_events_per_second: campaign.total_trace_events as f64 / wall_elapsed_seconds,
    }
}

fn read_sim_system_scenario(path: &Path) -> Result<ec_core::sim::SystemDuplicatePublisherScenario> {
    let mut bytes = Vec::new();
    if path == Path::new("-") {
        std::io::stdin()
            .read_to_end(&mut bytes)
            .context("failed to read system simulation scenario from stdin")?;
    } else {
        bytes = fs::read(path).with_context(|| {
            format!(
                "failed to read system simulation scenario {}",
                path.display()
            )
        })?;
    }
    serde_json::from_slice::<ec_core::sim::SystemDuplicatePublisherScenario>(&bytes).with_context(
        || {
            format!(
                "failed to parse system simulation scenario JSON from {}",
                path.display()
            )
        },
    )
}

fn validate_sim_system_scenario(
    scenario: &ec_core::sim::SystemDuplicatePublisherScenario,
) -> Result<()> {
    validate_sim_control_plane_scenario(&scenario.control)?;
    validate_sim_duplicate_publishers_scenario(&scenario.media)?;
    for publisher in &scenario.media.publisher_nodes {
        if !scenario.control.nodes.contains(publisher) {
            return Err(anyhow!(
                "system simulation publisher {} is not in control node list",
                publisher
            ));
        }
    }
    Ok(())
}

fn sim_system_command(args: SimSystemArgs) -> Result<()> {
    let output = sim_system_output(&args)?;
    if !output.ok {
        if let Some(path) = &args.failure_artifact {
            write_sim_system_failure_artifact(path, &args, &output)?;
            eprintln!(
                "wrote system simulation failure artifact to {}",
                path.display()
            );
        }
    }
    if args.pretty {
        println!("{}", serde_json::to_string_pretty(&output)?);
    } else {
        println!("{}", serde_json::to_string(&output)?);
    }
    if !output.ok && !args.allow_failure {
        let replay_hint = output
            .campaign
            .first_failure
            .as_ref()
            .map(|failure| failure.replay_hint.as_str())
            .unwrap_or("no replay hint");
        if output.fault_coverage.ok {
            return Err(anyhow!(
                "system simulation invariants failed: {replay_hint}"
            ));
        }
        return Err(anyhow!(
            "system simulation fault coverage failed: {:?}",
            output.fault_coverage.failures
        ));
    }
    Ok(())
}

#[derive(Debug, serde::Serialize)]
struct SimSystemFailureCampaignSummary {
    name: String,
    seed_start: ec_core::sim::SimulationSeed,
    iterations: u64,
    passed: u64,
    failed: u64,
}

#[derive(Debug, serde::Serialize)]
struct SimSystemFailureArtifact<'a> {
    artifact_type: &'static str,
    replay_scenario_pointer: &'static str,
    replay_command: String,
    first_failure_seed: ec_core::sim::SimulationSeed,
    first_failure_replay_hint: &'a str,
    campaign: SimSystemFailureCampaignSummary,
    replay_scenario: &'a ec_core::sim::SystemDuplicatePublisherScenario,
    invariant: &'a ec_core::sim::SystemDuplicatePublisherInvariantReport,
    report: &'a ec_core::sim::SystemDuplicatePublisherSimulationReport,
}

#[derive(Debug, serde::Serialize)]
struct SimSystemWeakCoverageArtifact<'a> {
    artifact_type: &'static str,
    rerun_command: String,
    campaign: SimSystemFailureCampaignSummary,
    fault_coverage: &'a SimSystemFaultCoverageReport,
    invariant: &'a ec_core::sim::SystemDuplicatePublisherInvariantConfig,
    scenario_template: &'a ec_core::sim::SystemDuplicatePublisherScenario,
}

fn write_sim_system_failure_artifact(
    path: &Path,
    args: &SimSystemArgs,
    output: &SimSystemOutput,
) -> Result<()> {
    if let Some(parent) = path
        .parent()
        .filter(|parent| !parent.as_os_str().is_empty())
    {
        fs::create_dir_all(parent).with_context(|| {
            format!(
                "failed to create system simulation failure artifact directory {}",
                parent.display()
            )
        })?;
    }
    let campaign = SimSystemFailureCampaignSummary {
        name: output.campaign.name.clone(),
        seed_start: output.campaign.seed_start,
        iterations: output.campaign.iterations,
        passed: output.campaign.passed,
        failed: output.campaign.failed,
    };
    let bytes = if let Some(failure) = output.campaign.first_failure.as_ref() {
        let mut replay_command = format!(
            "jq '.replay_scenario' {} | ec-node sim-system --scenario-json - --allow-failure --pretty",
            path.display()
        );
        if args.max_system_complete_ms != 3_500 {
            replay_command.push_str(&format!(
                " --max-system-complete-ms {}",
                args.max_system_complete_ms
            ));
        }
        let artifact = SimSystemFailureArtifact {
            artifact_type: "every.channel.sim.system_duplicate_publishers.failure.v1",
            replay_scenario_pointer: "/replay_scenario",
            replay_command,
            first_failure_seed: failure.seed,
            first_failure_replay_hint: &failure.replay_hint,
            campaign,
            replay_scenario: &failure.scenario,
            invariant: &failure.invariant,
            report: &failure.report,
        };
        serde_json::to_vec_pretty(&artifact)
            .context("failed to serialize system simulation failure artifact")?
    } else if !output.fault_coverage.ok {
        let artifact = SimSystemWeakCoverageArtifact {
            artifact_type: "every.channel.sim.system_duplicate_publishers.weak_coverage.v1",
            rerun_command: sim_system_campaign_rerun_command(args),
            campaign,
            fault_coverage: &output.fault_coverage,
            invariant: &output.invariant,
            scenario_template: &output.scenario_template,
        };
        serde_json::to_vec_pretty(&artifact)
            .context("failed to serialize system simulation weak coverage artifact")?
    } else {
        return Err(anyhow!(
            "system simulation had no failure artifact to write"
        ));
    };
    fs::write(path, bytes).with_context(|| {
        format!(
            "failed to write system simulation failure artifact {}",
            path.display()
        )
    })?;
    Ok(())
}

fn sim_system_campaign_rerun_command(args: &SimSystemArgs) -> String {
    let mut command = format!(
        "ec-node sim-system --seed {} --iterations {} --max-system-complete-ms {} --require-fault-coverage --pretty",
        args.seed, args.iterations, args.max_system_complete_ms
    );
    if args.fault_profile == SimSystemFaultProfile::FoundationDb {
        command.push_str(" --fault-profile foundationdb");
    }
    if args.min_fault_seed_coverage != 0 {
        command.push_str(&format!(
            " --min-fault-seed-coverage {}",
            args.min_fault_seed_coverage
        ));
    }
    if args.origin_node != "forge" {
        command.push_str(&format!(" --origin-node {}", shell_arg(&args.origin_node)));
    }
    if args.topic != "ec.control.broadcast.la-kcop" {
        command.push_str(&format!(" --topic {}", shell_arg(&args.topic)));
    }
    if args.announcement_id != "la-kcop@42" {
        command.push_str(&format!(
            " --announcement-id {}",
            shell_arg(&args.announcement_id)
        ));
    }
    if args.sequence_clock == SimPublisherSequenceClock::LocalActivation {
        command.push_str(" --sequence-clock local-activation");
    }
    if args.stream_id != "la-kcop" {
        command.push_str(&format!(" --stream-id {}", shell_arg(&args.stream_id)));
    }
    if args.rendition != WT_LADDER_PRIMARY_RENDITION {
        command.push_str(&format!(" --rendition {}", shell_arg(&args.rendition)));
    }
    if args.track != WT_PUBLISH_PRIMARY_VIDEO_TRACK {
        command.push_str(&format!(" --track {}", shell_arg(&args.track)));
    }
    if args.profile != "x264-hd3-v1" {
        command.push_str(&format!(" --profile {}", shell_arg(&args.profile)));
    }
    if args.sequence_count != 48 {
        command.push_str(&format!(" --sequence-count {}", args.sequence_count));
    }
    if args.segment_step_ms != 40 {
        command.push_str(&format!(" --segment-step-ms {}", args.segment_step_ms));
    }
    if args.publisher_activation_delay_ms != 25 {
        command.push_str(&format!(
            " --publisher-activation-delay-ms {}",
            args.publisher_activation_delay_ms
        ));
    }
    if args.publisher_backfill_delay_ms != 180 {
        command.push_str(&format!(
            " --publisher-backfill-delay-ms {}",
            args.publisher_backfill_delay_ms
        ));
    }
    for node in &args.nodes {
        command.push_str(&format!(" --node {}", shell_arg(node)));
    }
    for publisher in &args.publishers {
        command.push_str(&format!(" --publisher {}", shell_arg(publisher)));
    }
    for source_material in &args.publisher_source_material {
        command.push_str(&format!(
            " --publisher-source-material {}",
            shell_arg(source_material)
        ));
    }
    command
}

fn shell_arg(value: &str) -> String {
    if value
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.' | '/' | ':' | '@'))
    {
        value.to_string()
    } else {
        format!("'{}'", value.replace('\'', "'\\''"))
    }
}

fn sim_system_nodes(args: &SimSystemArgs) -> Result<Vec<String>> {
    let nodes = if args.nodes.is_empty() {
        vec![
            "forge".to_string(),
            "nuc-a".to_string(),
            "nuc-b".to_string(),
            "tower".to_string(),
            "relay-lax".to_string(),
            "relay-nyc".to_string(),
            "relay-hel".to_string(),
        ]
    } else {
        args.nodes
            .iter()
            .map(|node| node.trim())
            .filter(|node| !node.is_empty())
            .map(ToString::to_string)
            .collect::<Vec<_>>()
    };
    if nodes.len() < 2 {
        return Err(anyhow!("sim-system requires at least two --node values"));
    }
    let mut unique = BTreeSet::new();
    for node in &nodes {
        if !unique.insert(node.clone()) {
            return Err(anyhow!("sim-system node {} is duplicated", node));
        }
    }
    if !nodes.contains(&args.origin_node) {
        return Err(anyhow!(
            "sim-system origin node {} is not in node list",
            args.origin_node
        ));
    }
    Ok(nodes)
}

fn sim_system_publishers(args: &SimSystemArgs, nodes: &[String]) -> Result<Vec<String>> {
    let publishers = if args.publishers.is_empty() {
        vec!["nuc-a".to_string(), "nuc-b".to_string()]
    } else {
        args.publishers
            .iter()
            .map(|node| node.trim())
            .filter(|node| !node.is_empty())
            .map(ToString::to_string)
            .collect::<Vec<_>>()
    };
    if publishers.len() < 2 {
        return Err(anyhow!(
            "sim-system requires at least two --publisher values"
        ));
    }
    let mut unique = BTreeSet::new();
    for publisher in &publishers {
        if !unique.insert(publisher.clone()) {
            return Err(anyhow!("sim-system publisher {} is duplicated", publisher));
        }
        if !nodes.contains(publisher) {
            return Err(anyhow!(
                "sim-system publisher {} is not in node list",
                publisher
            ));
        }
    }
    Ok(publishers)
}

fn sim_system_default_scenario(
    args: &SimSystemArgs,
    nodes: &[String],
    publishers: &[String],
    publisher_source_material: &BTreeMap<String, String>,
    seed: ec_core::sim::SimulationSeed,
) -> ec_core::sim::SystemDuplicatePublisherScenario {
    if args.fault_profile == SimSystemFaultProfile::FoundationDb {
        return sim_system_foundationdb_scenario(
            args,
            nodes,
            publishers,
            publisher_source_material,
            seed,
        );
    }

    let mut control = ec_core::sim::ControlPlanePropagationScenario::new(
        seed,
        nodes.to_vec(),
        &args.origin_node,
        &args.topic,
        &args.announcement_id,
    );
    control.fanout = 3;
    control.gossip_interval_ms = 35;
    control.max_gossip_rounds = 12;
    control.base_network_delay_ms = 6;
    control.max_jitter_ms = 45;
    control.transient_drop_per_million = 120_000;
    control.partitions = sim_system_default_control_partitions(nodes, publishers);
    control.node_outages = if nodes.iter().any(|node| node == "relay-nyc") {
        vec![ec_core::sim::SimulationOutage::new(
            "relay-nyc",
            105,
            205,
            45,
        )]
    } else {
        Vec::new()
    };

    let mut media = ec_core::sim::DuplicatePublisherScenario::new(
        ec_core::sim::SimulationSeed::new(seed.0 ^ 0x6d65_6469_6121),
        publishers.to_vec(),
        &args.stream_id,
        &args.rendition,
        &args.track,
        &args.profile,
        0,
        args.sequence_count,
    );
    media.segment_step_ms = args.segment_step_ms;
    media.base_network_delay_ms = 5;
    media.max_jitter_ms = 75;
    media.transient_drop_per_million = 275_000;
    media.backfill_after_ms = 600;
    media.partitions = publishers
        .first()
        .map(|publisher| ec_core::sim::SimulationPartition::new(publisher, 940, 1_260, 90))
        .into_iter()
        .collect();
    media.publisher_outages = publishers
        .get(1)
        .map(|publisher| ec_core::sim::SimulationOutage::new(publisher, 1_360, 1_520, 220))
        .into_iter()
        .collect();
    media.publisher_source_material = publisher_source_material.clone();

    let mut scenario = ec_core::sim::SystemDuplicatePublisherScenario::new(seed, control, media);
    scenario.publisher_activation_delay_ms = args.publisher_activation_delay_ms;
    scenario.publisher_backfill_delay_ms = args.publisher_backfill_delay_ms;
    scenario.sequence_clock = args.sequence_clock.into();
    scenario
}

fn sim_system_foundationdb_scenario(
    args: &SimSystemArgs,
    nodes: &[String],
    publishers: &[String],
    publisher_source_material: &BTreeMap<String, String>,
    seed: ec_core::sim::SimulationSeed,
) -> ec_core::sim::SystemDuplicatePublisherScenario {
    let config = ec_core::sim::FoundationStyleSystemScenarioConfig {
        nodes: nodes.to_vec(),
        publisher_nodes: publishers.to_vec(),
        origin_node: args.origin_node.clone(),
        topic: args.topic.clone(),
        announcement_id: args.announcement_id.clone(),
        stream_id: args.stream_id.clone(),
        rendition_id: args.rendition.clone(),
        track_name: args.track.clone(),
        profile_id: args.profile.clone(),
        sequence_clock: args.sequence_clock.into(),
    };
    let mut scenario =
        ec_core::sim::foundation_style_system_duplicate_publisher_scenario(seed, &config);
    scenario.media.publisher_source_material = publisher_source_material.clone();
    scenario.publisher_activation_delay_ms = args.publisher_activation_delay_ms;
    scenario.publisher_backfill_delay_ms = args.publisher_backfill_delay_ms;
    scenario
}

fn sim_system_default_control_partitions(
    nodes: &[String],
    publishers: &[String],
) -> Vec<ec_core::sim::SimulationPartition> {
    let mut partitions = Vec::new();
    if let Some(delayed_publisher) = publishers.get(1) {
        partitions.push(ec_core::sim::SimulationPartition::new(
            delayed_publisher,
            0,
            180,
            40,
        ));
    }
    if nodes.iter().any(|node| node == "relay-hel") {
        partitions.push(ec_core::sim::SimulationPartition::new(
            "relay-hel",
            70,
            190,
            55,
        ));
    }
    partitions
}

#[derive(Debug, serde::Serialize)]
struct SimDuplicatePublishersOutput {
    ok: bool,
    invariant: ec_core::sim::DuplicatePublisherInvariantConfig,
    scenario_template: ec_core::sim::DuplicatePublisherScenario,
    campaign: ec_core::sim::DuplicatePublisherCampaignReport,
}

#[derive(Debug, serde::Serialize)]
struct SimDuplicatePublishersFailureCampaignSummary {
    name: String,
    seed_start: ec_core::sim::SimulationSeed,
    iterations: u64,
    passed: u64,
    failed: u64,
}

#[derive(Debug, serde::Serialize)]
struct SimDuplicatePublishersFailureArtifact<'a> {
    artifact_type: &'static str,
    replay_scenario_pointer: &'static str,
    replay_command: String,
    first_failure_seed: ec_core::sim::SimulationSeed,
    first_failure_replay_hint: &'a str,
    shrunk: bool,
    campaign: SimDuplicatePublishersFailureCampaignSummary,
    replay_scenario: &'a ec_core::sim::DuplicatePublisherScenario,
    invariant: &'a ec_core::sim::DuplicatePublisherInvariantReport,
    report: &'a ec_core::sim::DuplicatePublisherSimulationReport,
    shrink_steps: &'a [ec_core::sim::DuplicatePublisherShrinkStep],
}

fn sim_duplicate_publishers_output(
    args: &SimDuplicatePublishersArgs,
) -> Result<SimDuplicatePublishersOutput> {
    if let Some(path) = &args.scenario_json {
        return sim_duplicate_publishers_replay_output(args, path);
    }
    if args.iterations == 0 {
        return Err(anyhow!(
            "sim-duplicate-publishers requires --iterations > 0"
        ));
    }
    if args.sequence_count == 0 {
        return Err(anyhow!(
            "sim-duplicate-publishers requires --sequence-count > 0"
        ));
    }
    if args.transient_drop_per_million > 1_000_000 {
        return Err(anyhow!(
            "--transient-drop-per-million must be <= 1000000, got {}",
            args.transient_drop_per_million
        ));
    }
    let end_sequence = args
        .start_sequence
        .checked_add(args.sequence_count)
        .ok_or_else(|| anyhow!("--start-sequence + --sequence-count overflows u64"))?;
    let publishers = sim_duplicate_publishers_nodes(args)?;
    let partitions = sim_duplicate_publishers_partitions(args, &publishers)?;
    let publisher_outages = sim_duplicate_publishers_publisher_outages(args, &publishers)?;
    let publisher_sequence_offsets = sim_duplicate_publishers_sequence_offsets(args, &publishers)?;
    let publisher_media_time_offsets_ms =
        sim_duplicate_publishers_media_time_offsets(args, &publishers)?;
    let missing_media_timing_publishers =
        sim_duplicate_publishers_missing_media_timing_publishers(args, &publishers)?;
    let publisher_source_material =
        sim_publisher_source_material(&args.publisher_source_material, &publishers)?;
    let encoder_drifts = args
        .encoder_drifts
        .iter()
        .map(|value| parse_sim_encoder_drift(value))
        .collect::<Result<Vec<_>>>()?;

    let invariant = ec_core::sim::DuplicatePublisherInvariantConfig {
        require_source_count: publishers.len() as u64,
        require_duplicate_complete: true,
        require_media_timing: true,
        max_duplicate_complete_ms: if args.max_duplicate_complete_ms == 0 {
            None
        } else {
            Some(args.max_duplicate_complete_ms)
        },
    };

    let build_scenario = |seed| {
        let mut scenario = ec_core::sim::DuplicatePublisherScenario::new(
            seed,
            publishers.clone(),
            &args.stream_id,
            &args.rendition,
            &args.track,
            &args.profile,
            args.start_sequence,
            end_sequence,
        );
        scenario.segment_step_ms = args.segment_step_ms;
        scenario.base_network_delay_ms = args.base_network_delay_ms;
        scenario.max_jitter_ms = args.max_jitter_ms;
        scenario.transient_drop_per_million = args.transient_drop_per_million;
        scenario.backfill_after_ms = args.backfill_after_ms;
        scenario.publisher_sequence_offsets = publisher_sequence_offsets.clone();
        scenario.publisher_media_time_offsets_ms = publisher_media_time_offsets_ms.clone();
        scenario.missing_media_timing_publishers = missing_media_timing_publishers.clone();
        scenario.publisher_source_material = publisher_source_material.clone();
        scenario.partitions = partitions.clone();
        scenario.publisher_outages = publisher_outages.clone();
        scenario.encoder_drifts = encoder_drifts.clone();
        scenario
    };

    let seed_start = ec_core::sim::SimulationSeed::new(args.seed);
    let scenario_template = build_scenario(seed_start);
    let campaign = ec_core::sim::run_duplicate_publisher_campaign(
        "duplicate-publisher-fault-campaign",
        seed_start,
        args.iterations,
        &invariant,
        build_scenario,
    );
    let ok = campaign.all_passed();

    Ok(SimDuplicatePublishersOutput {
        ok,
        invariant,
        scenario_template,
        campaign,
    })
}

fn sim_duplicate_publishers_replay_output(
    args: &SimDuplicatePublishersArgs,
    path: &Path,
) -> Result<SimDuplicatePublishersOutput> {
    let scenario = read_sim_duplicate_publishers_scenario(path)?;
    validate_sim_duplicate_publishers_scenario(&scenario)?;

    let invariant = ec_core::sim::DuplicatePublisherInvariantConfig {
        require_source_count: scenario.publisher_nodes.len() as u64,
        require_duplicate_complete: true,
        require_media_timing: true,
        max_duplicate_complete_ms: if args.max_duplicate_complete_ms == 0 {
            None
        } else {
            Some(args.max_duplicate_complete_ms)
        },
    };
    let seed_start = scenario.seed;
    let scenario_template = scenario.clone();
    let campaign = ec_core::sim::run_duplicate_publisher_campaign(
        "duplicate-publisher-replay-scenario",
        seed_start,
        1,
        &invariant,
        |_| scenario.clone(),
    );
    let ok = campaign.all_passed();

    Ok(SimDuplicatePublishersOutput {
        ok,
        invariant,
        scenario_template,
        campaign,
    })
}

fn read_sim_duplicate_publishers_scenario(
    path: &Path,
) -> Result<ec_core::sim::DuplicatePublisherScenario> {
    let mut bytes = Vec::new();
    if path == Path::new("-") {
        std::io::stdin()
            .read_to_end(&mut bytes)
            .context("failed to read simulation scenario from stdin")?;
    } else {
        bytes = fs::read(path)
            .with_context(|| format!("failed to read simulation scenario {}", path.display()))?;
    }
    serde_json::from_slice::<ec_core::sim::DuplicatePublisherScenario>(&bytes).with_context(|| {
        format!(
            "failed to parse simulation scenario JSON from {}",
            path.display()
        )
    })
}

fn validate_sim_duplicate_publishers_scenario(
    scenario: &ec_core::sim::DuplicatePublisherScenario,
) -> Result<()> {
    if scenario.publisher_nodes.len() < 2 {
        return Err(anyhow!(
            "simulation scenario requires at least two publisher nodes"
        ));
    }
    if scenario.end_sequence <= scenario.start_sequence {
        return Err(anyhow!(
            "simulation scenario end_sequence must be greater than start_sequence"
        ));
    }
    if scenario.transient_drop_per_million > 1_000_000 {
        return Err(anyhow!(
            "simulation scenario transient_drop_per_million must be <= 1000000"
        ));
    }
    for partition in &scenario.partitions {
        if partition.source_node.trim().is_empty() {
            return Err(anyhow!("simulation partition source_node cannot be empty"));
        }
        if partition.end_ms <= partition.start_ms {
            return Err(anyhow!(
                "simulation partition end_ms must be greater than start_ms for {}",
                partition.source_node
            ));
        }
    }
    for outage in &scenario.publisher_outages {
        if outage.source_node.trim().is_empty() {
            return Err(anyhow!(
                "simulation publisher outage source_node cannot be empty"
            ));
        }
        if outage.end_ms <= outage.start_ms {
            return Err(anyhow!(
                "simulation publisher outage end_ms must be greater than start_ms for {}",
                outage.source_node
            ));
        }
    }
    for source_node in scenario.publisher_sequence_offsets.keys() {
        if source_node.trim().is_empty() {
            return Err(anyhow!(
                "simulation publisher sequence offset source_node cannot be empty"
            ));
        }
        if !scenario.publisher_nodes.contains(source_node) {
            return Err(anyhow!(
                "simulation publisher sequence offset references unknown publisher {}",
                source_node
            ));
        }
    }
    for source_node in scenario.publisher_media_time_offsets_ms.keys() {
        if source_node.trim().is_empty() {
            return Err(anyhow!(
                "simulation publisher media time offset source_node cannot be empty"
            ));
        }
        if !scenario.publisher_nodes.contains(source_node) {
            return Err(anyhow!(
                "simulation publisher media time offset references unknown publisher {}",
                source_node
            ));
        }
    }
    for source_node in &scenario.missing_media_timing_publishers {
        if source_node.trim().is_empty() {
            return Err(anyhow!(
                "simulation missing media timing publisher cannot be empty"
            ));
        }
        if !scenario.publisher_nodes.contains(source_node) {
            return Err(anyhow!(
                "simulation missing media timing references unknown publisher {}",
                source_node
            ));
        }
    }
    for (source_node, material_id) in &scenario.publisher_source_material {
        if source_node.trim().is_empty() || material_id.trim().is_empty() {
            return Err(anyhow!(
                "simulation publisher source material source_node and material_id cannot be empty"
            ));
        }
        if !scenario.publisher_nodes.contains(source_node) {
            return Err(anyhow!(
                "simulation publisher source material references unknown publisher {}",
                source_node
            ));
        }
    }
    for drift in &scenario.encoder_drifts {
        if drift.source_node.trim().is_empty() || drift.profile_id.trim().is_empty() {
            return Err(anyhow!(
                "simulation encoder drift source_node and profile_id cannot be empty"
            ));
        }
    }
    Ok(())
}

fn sim_duplicate_publishers_command(args: SimDuplicatePublishersArgs) -> Result<()> {
    let output = sim_duplicate_publishers_output(&args)?;
    if !output.ok {
        if let Some(path) = &args.failure_artifact {
            write_sim_duplicate_publishers_failure_artifact(path, &output)?;
            eprintln!(
                "wrote duplicate publisher simulation failure artifact to {}",
                path.display()
            );
        }
    }
    if args.pretty {
        println!("{}", serde_json::to_string_pretty(&output)?);
    } else {
        println!("{}", serde_json::to_string(&output)?);
    }
    if !output.ok && !args.allow_failure {
        let replay_hint = output
            .campaign
            .first_failure
            .as_ref()
            .map(|failure| failure.replay_hint.as_str())
            .unwrap_or("no replay hint");
        return Err(anyhow!(
            "duplicate publisher simulation invariants failed: {replay_hint}"
        ));
    }
    Ok(())
}

fn write_sim_duplicate_publishers_failure_artifact(
    path: &Path,
    output: &SimDuplicatePublishersOutput,
) -> Result<()> {
    let failure = output
        .campaign
        .first_failure
        .as_ref()
        .ok_or_else(|| anyhow!("duplicate publisher simulation did not fail"))?;
    let (replay_scenario, invariant, report, shrink_steps, shrunk) =
        if let Some(shrunk_failure) = failure.shrunk_failure.as_ref() {
            (
                &shrunk_failure.scenario,
                &shrunk_failure.invariant,
                &shrunk_failure.report,
                shrunk_failure.steps.as_slice(),
                true,
            )
        } else {
            (
                &failure.scenario,
                &failure.invariant,
                &failure.report,
                &[][..],
                false,
            )
        };
    let replay_command = format!(
        "jq '.replay_scenario' {} | ec-node sim-duplicate-publishers --scenario-json - --allow-failure --pretty",
        path.display()
    );
    let artifact = SimDuplicatePublishersFailureArtifact {
        artifact_type: "every.channel.sim.duplicate_publishers.failure.v1",
        replay_scenario_pointer: "/replay_scenario",
        replay_command,
        first_failure_seed: failure.seed,
        first_failure_replay_hint: &failure.replay_hint,
        shrunk,
        campaign: SimDuplicatePublishersFailureCampaignSummary {
            name: output.campaign.name.clone(),
            seed_start: output.campaign.seed_start,
            iterations: output.campaign.iterations,
            passed: output.campaign.passed,
            failed: output.campaign.failed,
        },
        replay_scenario,
        invariant,
        report,
        shrink_steps,
    };
    if let Some(parent) = path
        .parent()
        .filter(|parent| !parent.as_os_str().is_empty())
    {
        fs::create_dir_all(parent).with_context(|| {
            format!(
                "failed to create simulation failure artifact directory {}",
                parent.display()
            )
        })?;
    }
    let bytes = serde_json::to_vec_pretty(&artifact)
        .context("failed to serialize simulation failure artifact")?;
    fs::write(path, bytes).with_context(|| {
        format!(
            "failed to write simulation failure artifact {}",
            path.display()
        )
    })?;
    Ok(())
}

fn sim_duplicate_publishers_nodes(args: &SimDuplicatePublishersArgs) -> Result<Vec<String>> {
    let publishers = if args.publishers.is_empty() {
        vec!["nuc-a".to_string(), "nuc-b".to_string()]
    } else {
        args.publishers
            .iter()
            .map(|publisher| publisher.trim())
            .filter(|publisher| !publisher.is_empty())
            .map(ToString::to_string)
            .collect::<Vec<_>>()
    };
    if publishers.len() < 2 {
        return Err(anyhow!(
            "sim-duplicate-publishers requires at least two --publisher values"
        ));
    }
    Ok(publishers)
}

fn sim_duplicate_publishers_partitions(
    args: &SimDuplicatePublishersArgs,
    publishers: &[String],
) -> Result<Vec<ec_core::sim::SimulationPartition>> {
    if !args.partitions.is_empty() {
        return args
            .partitions
            .iter()
            .map(|value| parse_sim_partition(value))
            .collect::<Result<Vec<_>>>();
    }
    Ok(vec![
        ec_core::sim::SimulationPartition::new(&publishers[1], 120, 520, 140),
        ec_core::sim::SimulationPartition::new(&publishers[0], 940, 1_260, 90),
    ])
}

fn parse_sim_partition(value: &str) -> Result<ec_core::sim::SimulationPartition> {
    let parts = value.split(':').collect::<Vec<_>>();
    if parts.len() != 4 {
        return Err(anyhow!(
            "expected partition NODE:START_MS:END_MS:RELEASE_DELAY_MS, got {value:?}"
        ));
    }
    let source_node = parts[0].trim();
    if source_node.is_empty() {
        return Err(anyhow!("partition source node cannot be empty"));
    }
    let start_ms = parse_sim_u64("partition start_ms", parts[1])?;
    let end_ms = parse_sim_u64("partition end_ms", parts[2])?;
    let release_delay_ms = parse_sim_u64("partition release_delay_ms", parts[3])?;
    if end_ms <= start_ms {
        return Err(anyhow!(
            "partition end_ms must be greater than start_ms for {source_node}"
        ));
    }
    Ok(ec_core::sim::SimulationPartition::new(
        source_node,
        start_ms,
        end_ms,
        release_delay_ms,
    ))
}

fn sim_duplicate_publishers_publisher_outages(
    args: &SimDuplicatePublishersArgs,
    publishers: &[String],
) -> Result<Vec<ec_core::sim::SimulationOutage>> {
    if !args.publisher_outages.is_empty() {
        return args
            .publisher_outages
            .iter()
            .map(|value| parse_sim_publisher_outage(value))
            .collect::<Result<Vec<_>>>();
    }
    Ok(vec![ec_core::sim::SimulationOutage::new(
        &publishers[1],
        1_360,
        1_520,
        220,
    )])
}

fn parse_sim_publisher_outage(value: &str) -> Result<ec_core::sim::SimulationOutage> {
    let parts = value.split(':').collect::<Vec<_>>();
    if parts.len() != 4 {
        return Err(anyhow!(
            "expected publisher outage NODE:START_MS:END_MS:BACKFILL_DELAY_MS, got {value:?}"
        ));
    }
    let source_node = parts[0].trim();
    if source_node.is_empty() {
        return Err(anyhow!("publisher outage source node cannot be empty"));
    }
    let start_ms = parse_sim_u64("publisher outage start_ms", parts[1])?;
    let end_ms = parse_sim_u64("publisher outage end_ms", parts[2])?;
    let backfill_delay_ms = parse_sim_u64("publisher outage backfill_delay_ms", parts[3])?;
    if end_ms <= start_ms {
        return Err(anyhow!(
            "publisher outage end_ms must be greater than start_ms for {source_node}"
        ));
    }
    Ok(ec_core::sim::SimulationOutage::new(
        source_node,
        start_ms,
        end_ms,
        backfill_delay_ms,
    ))
}

fn sim_duplicate_publishers_sequence_offsets(
    args: &SimDuplicatePublishersArgs,
    publishers: &[String],
) -> Result<BTreeMap<String, u64>> {
    let mut offsets = BTreeMap::new();
    for value in &args.publisher_sequence_offsets {
        let (source_node, offset) = parse_sim_publisher_sequence_offset(value)?;
        if !publishers.contains(&source_node) {
            return Err(anyhow!(
                "publisher sequence offset references unknown publisher {source_node}"
            ));
        }
        offsets.insert(source_node, offset);
    }
    Ok(offsets)
}

fn parse_sim_publisher_sequence_offset(value: &str) -> Result<(String, u64)> {
    let parts = value.split(':').collect::<Vec<_>>();
    if parts.len() != 2 {
        return Err(anyhow!(
            "expected publisher sequence offset NODE:SEQUENCE_OFFSET, got {value:?}"
        ));
    }
    let source_node = parts[0].trim();
    if source_node.is_empty() {
        return Err(anyhow!("publisher sequence offset node cannot be empty"));
    }
    let offset = parse_sim_u64("publisher sequence offset", parts[1])?;
    Ok((source_node.to_string(), offset))
}

fn sim_duplicate_publishers_media_time_offsets(
    args: &SimDuplicatePublishersArgs,
    publishers: &[String],
) -> Result<BTreeMap<String, u64>> {
    let mut offsets = BTreeMap::new();
    for value in &args.publisher_media_time_offsets_ms {
        let (source_node, offset) = parse_sim_publisher_media_time_offset(value)?;
        if !publishers.contains(&source_node) {
            return Err(anyhow!(
                "publisher media time offset references unknown publisher {source_node}"
            ));
        }
        offsets.insert(source_node, offset);
    }
    Ok(offsets)
}

fn parse_sim_publisher_media_time_offset(value: &str) -> Result<(String, u64)> {
    let parts = value.split(':').collect::<Vec<_>>();
    if parts.len() != 2 {
        return Err(anyhow!(
            "expected publisher media time offset NODE:OFFSET_MS, got {value:?}"
        ));
    }
    let source_node = parts[0].trim();
    if source_node.is_empty() {
        return Err(anyhow!("publisher media time offset node cannot be empty"));
    }
    let offset = parse_sim_u64("publisher media time offset", parts[1])?;
    Ok((source_node.to_string(), offset))
}

fn sim_publisher_source_material(
    values: &[String],
    publishers: &[String],
) -> Result<BTreeMap<String, String>> {
    let mut material = BTreeMap::new();
    for value in values {
        let (source_node, material_id) = parse_sim_publisher_source_material(value)?;
        if !publishers.contains(&source_node) {
            return Err(anyhow!(
                "publisher source material references unknown publisher {source_node}"
            ));
        }
        material.insert(source_node, material_id);
    }
    Ok(material)
}

fn parse_sim_publisher_source_material(value: &str) -> Result<(String, String)> {
    let parts = value.splitn(2, ':').collect::<Vec<_>>();
    if parts.len() != 2 {
        return Err(anyhow!(
            "expected publisher source material NODE:MATERIAL_ID, got {value:?}"
        ));
    }
    let source_node = parts[0].trim();
    let material_id = parts[1].trim();
    if source_node.is_empty() || material_id.is_empty() {
        return Err(anyhow!(
            "publisher source material node and material id cannot be empty"
        ));
    }
    Ok((source_node.to_string(), material_id.to_string()))
}

fn sim_duplicate_publishers_missing_media_timing_publishers(
    args: &SimDuplicatePublishersArgs,
    publishers: &[String],
) -> Result<BTreeSet<String>> {
    let mut missing = BTreeSet::new();
    for value in &args.missing_media_timing_publishers {
        let source_node = value.trim();
        if source_node.is_empty() {
            return Err(anyhow!(
                "missing media timing publisher node cannot be empty"
            ));
        }
        if !publishers.iter().any(|publisher| publisher == source_node) {
            return Err(anyhow!(
                "missing media timing references unknown publisher {source_node}"
            ));
        }
        missing.insert(source_node.to_string());
    }
    Ok(missing)
}

fn parse_sim_encoder_drift(value: &str) -> Result<ec_core::sim::EncoderDriftFault> {
    let parts = value.split(':').collect::<Vec<_>>();
    if parts.len() != 3 {
        return Err(anyhow!(
            "expected encoder drift NODE:SEQUENCE:PROFILE_ID, got {value:?}"
        ));
    }
    let source_node = parts[0].trim();
    let profile_id = parts[2].trim();
    if source_node.is_empty() || profile_id.is_empty() {
        return Err(anyhow!("encoder drift node and profile id cannot be empty"));
    }
    let sequence = parse_sim_u64("encoder drift sequence", parts[1])?;
    Ok(ec_core::sim::EncoderDriftFault::new(
        source_node,
        sequence,
        profile_id,
    ))
}

fn parse_sim_u64(label: &str, value: &str) -> Result<u64> {
    value
        .trim()
        .parse::<u64>()
        .with_context(|| format!("failed to parse {label} as u64: {value:?}"))
}

fn archive_track_aliases(track_name: &str) -> Vec<&'static str> {
    match track_name {
        WT_PUBLISH_PRIMARY_VIDEO_TRACK => vec![WT_PUBLISH_PRIMARY_VIDEO_TRACK, "video0.m4s"],
        WT_PUBLISH_PRIMARY_AUDIO_TRACK => vec![WT_PUBLISH_PRIMARY_AUDIO_TRACK, "audio0.m4s"],
        "video0.m4s" => vec!["video0.m4s", WT_PUBLISH_PRIMARY_VIDEO_TRACK],
        "audio0.m4s" => vec!["audio0.m4s", WT_PUBLISH_PRIMARY_AUDIO_TRACK],
        _ => Vec::new(),
    }
}

fn archive_index_path_with_aliases(
    manifest_root: &Path,
    broadcast_name: &str,
    track_name: &str,
) -> Option<PathBuf> {
    let aliases = archive_track_aliases(track_name);
    if aliases.is_empty() {
        let path = archive_index_path(manifest_root, broadcast_name, track_name);
        return path.exists().then_some(path);
    }
    aliases
        .into_iter()
        .map(|alias| archive_index_path(manifest_root, broadcast_name, alias))
        .find(|path| path.exists())
}

fn read_archive_records_with_aliases(
    manifest_root: &Path,
    broadcast_name: &str,
    track_name: &str,
) -> Result<Vec<ArchiveIndexRecord>> {
    let aliases = archive_track_aliases(track_name);
    if aliases.is_empty() {
        return read_archive_records(manifest_root, broadcast_name, track_name);
    }
    for alias in aliases {
        let records = read_archive_records(manifest_root, broadcast_name, alias)?;
        if !records.is_empty() {
            return Ok(records);
        }
    }
    Ok(Vec::new())
}

fn dedupe_by_group_sequence(mut records: Vec<ArchiveIndexRecord>) -> Vec<ArchiveIndexRecord> {
    records.sort_by_key(|record| record.group_sequence);
    let mut out: Vec<ArchiveIndexRecord> = Vec::with_capacity(records.len());
    for record in records {
        if let Some(last) = out.last_mut() {
            if last.group_sequence == record.group_sequence {
                *last = record;
                continue;
            }
        }
        out.push(record);
    }
    out
}

fn read_archive_tail_records_from_path(
    path: &Path,
    min_records: usize,
) -> Result<Vec<ArchiveIndexRecord>> {
    if min_records == 0 || !path.exists() {
        return Ok(Vec::new());
    }

    let mut file = File::open(path)
        .with_context(|| format!("failed to open archive index {}", path.display()))?;
    let len = file
        .metadata()
        .with_context(|| format!("failed to stat archive index {}", path.display()))?
        .len();
    let mut offset = len;
    let mut data = Vec::new();
    let mut reached_start = false;

    while offset > 0 {
        let read_len = usize::try_from(offset.min(256 * 1024)).unwrap_or(256 * 1024);
        offset -= read_len as u64;
        file.seek(SeekFrom::Start(offset))
            .with_context(|| format!("failed to seek archive index {}", path.display()))?;
        let mut chunk = vec![0u8; read_len];
        file.read_exact(&mut chunk)
            .with_context(|| format!("failed to read archive index {}", path.display()))?;
        chunk.extend_from_slice(&data);
        data = chunk;
        reached_start = offset == 0;

        let parse_start = if reached_start {
            0
        } else if let Some(pos) = data.iter().position(|byte| *byte == b'\n') {
            pos + 1
        } else {
            continue;
        };
        let complete = &data[parse_start..];
        let complete_lines = complete
            .split(|byte| *byte == b'\n')
            .filter(|line| !trim_ascii_whitespace(line).is_empty())
            .count();
        if complete_lines >= min_records {
            break;
        }
    }

    let parse_start = if reached_start {
        0
    } else {
        data.iter()
            .position(|byte| *byte == b'\n')
            .map(|pos| pos + 1)
            .unwrap_or(data.len())
    };
    let mut records = Vec::new();
    for line in data[parse_start..].split(|byte| *byte == b'\n') {
        if let Some(record) = parse_archive_record_line(path, line) {
            records.push(record);
        }
    }
    if records.len() > min_records {
        records = records.split_off(records.len() - min_records);
    }
    Ok(records)
}

fn read_archive_tail_records_with_aliases(
    manifest_root: &Path,
    broadcast_name: &str,
    track_name: &str,
    min_records: usize,
) -> Result<Vec<ArchiveIndexRecord>> {
    let Some(path) = archive_index_path_with_aliases(manifest_root, broadcast_name, track_name)
    else {
        return Ok(Vec::new());
    };
    read_archive_tail_records_from_path(&path, min_records)
}

fn count_archive_index_lines(path: &Path) -> Result<usize> {
    let mut file = File::open(path)
        .with_context(|| format!("failed to open archive index {}", path.display()))?;
    let mut buf = vec![0u8; 1024 * 1024];
    let mut lines = 0usize;
    let mut saw_non_newline = false;
    let mut ended_with_newline = true;
    loop {
        let read = file
            .read(&mut buf)
            .with_context(|| format!("failed to read archive index {}", path.display()))?;
        if read == 0 {
            break;
        }
        for byte in &buf[..read] {
            if *byte == b'\n' {
                lines += 1;
                ended_with_newline = true;
            } else {
                saw_non_newline = true;
                ended_with_newline = false;
            }
        }
    }
    if saw_non_newline && !ended_with_newline {
        lines += 1;
    }
    Ok(lines)
}

fn archive_track_summary_from_records(records: &[ArchiveIndexRecord]) -> ArchiveTrackSummary {
    ArchiveTrackSummary {
        start_unix_ms: records.iter().map(|record| record.received_unix_ms).min(),
        end_unix_ms: records.iter().map(|record| record.received_unix_ms).max(),
        segments: records.len(),
        total_bytes: Some(records.iter().map(|record| record.size_bytes as u64).sum()),
        total_frames: Some(records.iter().map(|record| record.frame_count).sum()),
        partial_summary: false,
    }
}

fn first_archive_record_from_path(path: &Path) -> Result<Option<ArchiveIndexRecord>> {
    if !path.exists() {
        return Ok(None);
    }
    let file = File::open(path)
        .with_context(|| format!("failed to open archive index {}", path.display()))?;
    let reader = BufReader::new(file);
    for line in reader.split(b'\n') {
        let line =
            line.with_context(|| format!("failed to read archive index {}", path.display()))?;
        if let Some(record) = parse_archive_record_line(path, &line) {
            return Ok(Some(record));
        }
    }
    Ok(None)
}

fn last_archive_record_from_path(path: &Path) -> Result<Option<ArchiveIndexRecord>> {
    let records = read_archive_tail_records_from_path(path, 1)?;
    Ok(records.into_iter().last())
}

fn summarize_archive_track(
    manifest_root: &Path,
    broadcast_name: &str,
    track_name: &str,
    from_ms: Option<u64>,
    limit: Option<usize>,
) -> Result<ArchiveTrackSummary> {
    if from_ms.is_some() || limit.is_some() {
        let records =
            parse_archive_track(manifest_root, broadcast_name, track_name, from_ms, limit)?;
        return Ok(archive_track_summary_from_records(&records));
    }

    let Some(path) = archive_index_path_with_aliases(manifest_root, broadcast_name, track_name)
    else {
        return Ok(ArchiveTrackSummary::default());
    };
    summarize_archive_track_from_path(&path)
}

fn summarize_archive_track_from_path(path: &Path) -> Result<ArchiveTrackSummary> {
    let size = fs::metadata(&path)
        .with_context(|| format!("failed to stat archive index {}", path.display()))?
        .len();
    if size <= WT_ARCHIVE_TIMELINE_FULL_SCAN_MAX_BYTES {
        let records = read_archive_records_from_path(&path)?;
        return Ok(archive_track_summary_from_records(&records));
    }

    let modified = fs::metadata(&path)
        .with_context(|| format!("failed to stat archive index {}", path.display()))?
        .modified()
        .ok();
    let cache = ARCHIVE_TRACK_SUMMARY_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
    if let Ok(cache) = cache.lock() {
        if let Some(entry) = cache.get(path) {
            if entry.size == size && entry.modified == modified {
                return Ok(entry.summary.clone());
            }
        }
    }

    let first = first_archive_record_from_path(&path)?;
    let last = last_archive_record_from_path(&path)?;
    let start_unix_ms = first
        .as_ref()
        .map(|record| record.received_unix_ms)
        .into_iter()
        .chain(last.as_ref().map(|record| record.received_unix_ms))
        .min();
    let end_unix_ms = first
        .as_ref()
        .map(|record| record.received_unix_ms)
        .into_iter()
        .chain(last.as_ref().map(|record| record.received_unix_ms))
        .max();
    let summary = ArchiveTrackSummary {
        start_unix_ms,
        end_unix_ms,
        segments: count_archive_index_lines(&path)?,
        total_bytes: None,
        total_frames: None,
        partial_summary: true,
    };
    if let Ok(mut cache) = cache.lock() {
        cache.insert(
            path.to_path_buf(),
            ArchiveTrackSummaryCacheEntry {
                size,
                modified,
                summary: summary.clone(),
            },
        );
    }
    Ok(summary)
}

fn strip_ladder_rendition_suffix(value: &str) -> (&str, Option<&'static str>) {
    for rendition in WT_LADDER_RENDITIONS {
        let suffix = format!("-{rendition}");
        if value.ends_with(&suffix) {
            return (&value[..value.len() - suffix.len()], Some(*rendition));
        }
    }
    (value, None)
}

fn track_rendition(track_name: &str) -> Option<&'static str> {
    let normalized = track_name.trim_end_matches(".jsonl");
    if let Some((_, candidate)) = normalized.rsplit_once('/') {
        for rendition in WT_LADDER_RENDITIONS {
            if candidate == *rendition {
                return Some(*rendition);
            }
        }
    }
    for rendition in WT_LADDER_RENDITIONS {
        if normalized.ends_with(&format!("_{rendition}"))
            || normalized.ends_with(&format!("-{rendition}"))
        {
            return Some(*rendition);
        }
    }
    None
}

fn archive_track_media_kind(track_name: &str) -> &'static str {
    let lower = track_name.to_ascii_lowercase();
    let head = lower.split('/').next().unwrap_or(lower.as_str());
    if head.contains("init") {
        "init"
    } else if head == WT_PUBLISH_PRIMARY_AUDIO_TRACK
        || head.starts_with("audio")
        || lower.contains("/audio")
    {
        "audio"
    } else if head == WT_PUBLISH_PRIMARY_VIDEO_TRACK
        || head.starts_with("video")
        || lower.contains("/video")
        || lower.ends_with(".m4s")
    {
        "video"
    } else if head.contains("catalog") {
        "catalog"
    } else if head.contains("manifest") {
        "manifest"
    } else {
        "other"
    }
}

fn archive_index_files_for_broadcast(
    manifest_root: &Path,
    broadcast_name: &str,
) -> Result<Vec<PathBuf>> {
    let dir = manifest_root.join(sanitize_path_component(broadcast_name));
    if !dir.exists() {
        return Ok(Vec::new());
    }
    let mut files = Vec::new();
    for entry in fs::read_dir(&dir)
        .with_context(|| format!("failed to read archive manifest dir {}", dir.display()))?
    {
        let entry = entry
            .with_context(|| format!("failed to read archive manifest dir {}", dir.display()))?;
        let path = entry.path();
        if path.extension().and_then(|ext| ext.to_str()) == Some("jsonl") {
            files.push(path);
        }
    }
    files.sort();
    Ok(files)
}

fn archive_index_track_name(path: &Path) -> Result<Option<String>> {
    if let Some(record) = first_archive_record_from_path(path)? {
        return Ok(Some(record.track_name));
    }
    Ok(path
        .file_stem()
        .and_then(|value| value.to_str())
        .map(|value| value.to_string()))
}

fn archive_ladder_broadcast_names(
    manifest_root: &Path,
    broadcast_name: &str,
) -> Result<Vec<String>> {
    let (base_name, requested_rendition) = strip_ladder_rendition_suffix(broadcast_name);
    let mut names = Vec::new();
    let push_name = |names: &mut Vec<String>, value: String| {
        if !names.iter().any(|existing| existing == &value) {
            names.push(value);
        }
    };

    push_name(&mut names, base_name.to_string());
    if let Some(rendition) = requested_rendition {
        push_name(&mut names, format!("{base_name}-{rendition}"));
    }

    if manifest_root.exists() {
        for entry in fs::read_dir(manifest_root).with_context(|| {
            format!(
                "failed to read archive manifest root {}",
                manifest_root.display()
            )
        })? {
            let entry = entry.with_context(|| {
                format!(
                    "failed to read archive manifest root {}",
                    manifest_root.display()
                )
            })?;
            if !entry.file_type().map(|kind| kind.is_dir()).unwrap_or(false) {
                continue;
            }
            let Some(name) = entry.file_name().to_str().map(|value| value.to_string()) else {
                continue;
            };
            let (candidate_base, candidate_rendition) = strip_ladder_rendition_suffix(&name);
            if candidate_base == base_name
                && (candidate_rendition.is_some() || candidate_base == name)
            {
                push_name(&mut names, name);
            }
        }
    }

    names.sort();
    Ok(names)
}

fn retention_coverage(span_seconds: f64, retention_seconds: u64) -> f64 {
    if retention_seconds == 0 {
        return 0.0;
    }
    (span_seconds / retention_seconds as f64).clamp(0.0, 1.0)
}

fn archive_ladder_track_status(
    broadcast_name: &str,
    track_name: String,
    summary: ArchiveTrackSummary,
    broadcast_rendition: Option<&'static str>,
    now_ms: u64,
    retention_seconds: u64,
) -> ArchiveLadderTrackStatus {
    let rendition_id = track_rendition(&track_name)
        .or(broadcast_rendition)
        .unwrap_or(WT_LADDER_PRIMARY_RENDITION)
        .to_string();
    let span_seconds = match (summary.start_unix_ms, summary.end_unix_ms) {
        (Some(start), Some(end)) if end >= start => (end - start) as f64 / 1000.0,
        _ => 0.0,
    };
    let latest_age_ms = summary.end_unix_ms.map(|end| now_ms.saturating_sub(end));
    let media_kind = archive_track_media_kind(&track_name).to_string();
    ArchiveLadderTrackStatus {
        broadcast_name: broadcast_name.to_string(),
        track_name,
        media_kind,
        rendition_id,
        start_unix_ms: summary.start_unix_ms,
        end_unix_ms: summary.end_unix_ms,
        latest_age_ms,
        segments: summary.segments,
        total_bytes: summary.total_bytes,
        total_frames: summary.total_frames,
        span_seconds,
        retention_coverage: retention_coverage(span_seconds, retention_seconds),
        partial_summary: summary.partial_summary,
    }
}

fn summarize_ladder_renditions(
    tracks: &[ArchiveLadderTrackStatus],
    retention_seconds: u64,
) -> Vec<ArchiveLadderRenditionSummary> {
    let mut by_rendition: BTreeMap<String, Vec<&ArchiveLadderTrackStatus>> = BTreeMap::new();
    for track in tracks {
        by_rendition
            .entry(track.rendition_id.clone())
            .or_default()
            .push(track);
    }
    by_rendition
        .into_iter()
        .map(|(rendition_id, tracks)| {
            let start_unix_ms = tracks.iter().filter_map(|track| track.start_unix_ms).min();
            let end_unix_ms = tracks.iter().filter_map(|track| track.end_unix_ms).max();
            let latest_age_ms = tracks.iter().filter_map(|track| track.latest_age_ms).min();
            let segments = tracks.iter().map(|track| track.segments).sum();
            let total_bytes = tracks
                .iter()
                .map(|track| track.total_bytes)
                .try_fold(0u64, |acc, value| value.map(|value| acc + value));
            let total_frames = tracks
                .iter()
                .map(|track| track.total_frames)
                .try_fold(0u64, |acc, value| value.map(|value| acc + value));
            let span_seconds = match (start_unix_ms, end_unix_ms) {
                (Some(start), Some(end)) if end >= start => (end - start) as f64 / 1000.0,
                _ => 0.0,
            };
            ArchiveLadderRenditionSummary {
                rendition_id,
                start_unix_ms,
                end_unix_ms,
                latest_age_ms,
                segments,
                total_bytes,
                total_frames,
                retention_coverage: retention_coverage(span_seconds, retention_seconds),
                track_count: tracks.len(),
            }
        })
        .collect()
}

fn archive_ladder_status(
    manifest_root: &Path,
    broadcast_name: &str,
    now_ms: u64,
    retention_seconds: u64,
) -> Result<ArchiveLadderResponse> {
    let (base_broadcast_name, _) = strip_ladder_rendition_suffix(broadcast_name);
    let mut tracks = Vec::new();
    for candidate in archive_ladder_broadcast_names(manifest_root, broadcast_name)? {
        let (_, broadcast_rendition) = strip_ladder_rendition_suffix(&candidate);
        for path in archive_index_files_for_broadcast(manifest_root, &candidate)? {
            let Some(track_name) = archive_index_track_name(&path)? else {
                continue;
            };
            let summary = summarize_archive_track_from_path(&path)?;
            tracks.push(archive_ladder_track_status(
                &candidate,
                track_name,
                summary,
                broadcast_rendition,
                now_ms,
                retention_seconds,
            ));
        }
    }
    tracks.sort_by(|a, b| {
        a.rendition_id
            .cmp(&b.rendition_id)
            .then_with(|| a.media_kind.cmp(&b.media_kind))
            .then_with(|| a.broadcast_name.cmp(&b.broadcast_name))
            .then_with(|| a.track_name.cmp(&b.track_name))
    });
    Ok(ArchiveLadderResponse {
        broadcast_name: broadcast_name.to_string(),
        base_broadcast_name: base_broadcast_name.to_string(),
        generated_unix_ms: now_ms,
        retention_seconds,
        retention_window_start_unix_ms: (retention_seconds > 0)
            .then_some(now_ms.saturating_sub(retention_seconds.saturating_mul(1000))),
        renditions: summarize_ladder_renditions(&tracks, retention_seconds),
        tracks,
    })
}

fn ladder_rendition_bandwidth(rendition_id: &str) -> u64 {
    match rendition_id {
        "480p" => 1_400_000,
        "720p" => 3_200_000,
        "1080p" => 6_300_000,
        _ => 3_500_000,
    }
}

fn ladder_rendition_resolution(rendition_id: &str) -> Option<(u32, u32)> {
    match rendition_id {
        "480p" => Some((854, 480)),
        "720p" => Some((1280, 720)),
        "1080p" => Some((1920, 1080)),
        _ => None,
    }
}

fn archive_hls_variants_from_ladder(status: &ArchiveLadderResponse) -> Vec<ArchiveHlsVariant> {
    let has_explicit_ladder = status.tracks.iter().any(|track| {
        track.media_kind == "video"
            && track.segments > 0
            && (track_rendition(&track.track_name).is_some()
                || strip_ladder_rendition_suffix(&track.broadcast_name)
                    .1
                    .is_some())
    });
    if !has_explicit_ladder {
        return Vec::new();
    }

    let mut selected: BTreeMap<String, ArchiveHlsVariant> = BTreeMap::new();
    for track in &status.tracks {
        if track.media_kind != "video" || track.segments == 0 {
            continue;
        }
        let candidate = ArchiveHlsVariant {
            broadcast_name: track.broadcast_name.clone(),
            track_name: track.track_name.clone(),
            rendition_id: track.rendition_id.clone(),
            bandwidth: ladder_rendition_bandwidth(&track.rendition_id),
            resolution: ladder_rendition_resolution(&track.rendition_id),
            segments: track.segments,
            latest_age_ms: track.latest_age_ms,
        };
        let replace = selected
            .get(&candidate.rendition_id)
            .map(|existing| {
                candidate.segments > existing.segments
                    || (candidate.segments == existing.segments
                        && candidate.latest_age_ms.unwrap_or(u64::MAX)
                            < existing.latest_age_ms.unwrap_or(u64::MAX))
            })
            .unwrap_or(true);
        if replace {
            selected.insert(candidate.rendition_id.clone(), candidate);
        }
    }
    let mut variants = selected.into_values().collect::<Vec<_>>();
    variants.sort_by(|a, b| {
        a.bandwidth
            .cmp(&b.bandwidth)
            .then_with(|| a.rendition_id.cmp(&b.rendition_id))
    });
    variants
}

fn archive_track_playlist_query(url: &Url, broadcast_name: &str, track_name: &str) -> String {
    let mut parts = vec![
        format!("broadcast={}", urlencoding::encode(broadcast_name)),
        format!("track={}", urlencoding::encode(track_name)),
    ];
    for key in ["from_ms", "limit"] {
        if let Some((_, value)) = url.query_pairs().find(|(k, _)| k == key) {
            parts.push(format!("{key}={}", urlencoding::encode(&value)));
        }
    }
    format!("?{}", parts.join("&"))
}

fn archive_hls_master_playlist(
    manifest_root: &Path,
    broadcast_name: &str,
    req_url: &Url,
    now_ms: u64,
    retention_seconds: u64,
) -> Result<Option<String>> {
    let status = archive_ladder_status(manifest_root, broadcast_name, now_ms, retention_seconds)?;
    let variants = archive_hls_variants_from_ladder(&status);
    if variants.is_empty() {
        return Ok(None);
    }

    let encoded_path_broadcast = urlencoding::encode(broadcast_name);
    let mut out = String::new();
    out.push_str("#EXTM3U\n");
    out.push_str("#EXT-X-VERSION:7\n");
    out.push_str("#EXT-X-INDEPENDENT-SEGMENTS\n");
    for variant in variants {
        let query =
            archive_track_playlist_query(req_url, &variant.broadcast_name, &variant.track_name);
        let resolution = variant
            .resolution
            .map(|(width, height)| format!(",RESOLUTION={width}x{height}"))
            .unwrap_or_default();
        out.push_str(&format!(
            "#EXT-X-STREAM-INF:BANDWIDTH={},CODECS=\"avc1.640028,mp4a.40.2\"{resolution},NAME=\"{}\"\n",
            variant.bandwidth, variant.rendition_id
        ));
        out.push_str(&format!(
            "/archive/{encoded_path_broadcast}/track.m3u8{query}\n"
        ));
    }
    Ok(Some(out))
}

fn default_segment_duration_ms(records: &[ArchiveIndexRecord]) -> u64 {
    if records.len() < 2 {
        return 1000;
    }
    let mut deltas = Vec::with_capacity(records.len().saturating_sub(1));
    for pair in records.windows(2) {
        let current = pair[0].received_unix_ms;
        let next = pair[1].received_unix_ms;
        if next > current {
            deltas.push(next - current);
        }
    }
    if deltas.is_empty() {
        return 1000;
    }
    deltas.sort_unstable();
    deltas[deltas.len() / 2].clamp(200, 10000)
}

fn build_hls_segments(records: &[ArchiveIndexRecord]) -> Vec<ArchiveHlsSegment> {
    if records.is_empty() {
        return Vec::new();
    }
    let fallback_ms = default_segment_duration_ms(records);
    let mut out = Vec::with_capacity(records.len());
    for (idx, record) in records.iter().enumerate() {
        let mut dur_ms = fallback_ms;
        if let Some(next) = records.get(idx + 1) {
            if next.received_unix_ms > record.received_unix_ms {
                dur_ms = (next.received_unix_ms - record.received_unix_ms).clamp(200, 10000);
            }
        }
        out.push(ArchiveHlsSegment {
            sequence: record.group_sequence,
            duration_secs: (dur_ms as f64) / 1000.0,
            hash: record.blake3.clone(),
        });
    }
    out
}

fn latest_init_hash(
    manifest_root: &Path,
    broadcast_name: &str,
    fallback_track: &str,
) -> Result<Option<String>> {
    let mut init_tracks = Vec::new();
    if let Some(rendition) = track_rendition(fallback_track) {
        init_tracks.push(format!("{WT_PUBLISH_INIT_TRACK}/{rendition}"));
    }
    init_tracks.push(WT_PUBLISH_INIT_TRACK.to_string());

    for init_track in init_tracks {
        let mut init_records = dedupe_by_group_sequence(read_archive_records(
            manifest_root,
            broadcast_name,
            &init_track,
        )?);
        if let Some(record) = init_records.pop() {
            return Ok(Some(record.blake3));
        }
    }
    Ok(None)
}

fn parse_limit(url: &Url) -> Option<usize> {
    url.query_pairs()
        .find(|(k, _)| k == "limit")
        .and_then(|(_, v)| v.parse::<usize>().ok())
}

fn parse_from_ms(url: &Url) -> Option<u64> {
    url.query_pairs()
        .find(|(k, _)| k == "from_ms")
        .and_then(|(_, v)| v.parse::<u64>().ok())
}

fn archive_playlist_query(url: &Url) -> String {
    let mut parts = Vec::new();
    for key in ["from_ms", "limit"] {
        if let Some((_, value)) = url.query_pairs().find(|(k, _)| k == key) {
            parts.push(format!("{key}={}", urlencoding::encode(&value)));
        }
    }
    if parts.is_empty() {
        String::new()
    } else {
        format!("?{}", parts.join("&"))
    }
}

fn parse_archive_track(
    manifest_root: &Path,
    broadcast_name: &str,
    track_name: &str,
    from_ms: Option<u64>,
    limit: Option<usize>,
) -> Result<Vec<ArchiveIndexRecord>> {
    let tail_limit = from_ms.is_none().then_some(limit).flatten();
    let mut records = if let Some(max_items) = tail_limit {
        if max_items > 0 {
            read_archive_tail_records_with_aliases(
                manifest_root,
                broadcast_name,
                track_name,
                max_items.saturating_mul(4).max(max_items + 16),
            )?
        } else {
            Vec::new()
        }
    } else {
        read_archive_records_with_aliases(manifest_root, broadcast_name, track_name)?
    };
    records = dedupe_by_group_sequence(records);
    if let Some(start_ms) = from_ms {
        records.retain(|record| record.received_unix_ms >= start_ms);
    }
    if let Some(max_items) = limit {
        if max_items > 0 && records.len() > max_items {
            if from_ms.is_some() {
                records.truncate(max_items);
            } else {
                records = records.split_off(records.len() - max_items);
            }
        }
    }
    Ok(records)
}

fn hls_playlist_for_track(
    broadcast_name: &str,
    track_name: &str,
    init_hash: &str,
    records: &[ArchiveIndexRecord],
) -> String {
    let segments = build_hls_segments(records);
    let target_duration = segments
        .iter()
        .map(|segment| segment.duration_secs.ceil() as u64)
        .max()
        .unwrap_or(2)
        .max(1);
    let media_sequence = segments
        .first()
        .map(|segment| segment.sequence)
        .unwrap_or(0);

    let mut out = String::new();
    out.push_str("#EXTM3U\n");
    out.push_str("#EXT-X-VERSION:7\n");
    out.push_str("#EXT-X-PLAYLIST-TYPE:EVENT\n");
    out.push_str("#EXT-X-INDEPENDENT-SEGMENTS\n");
    out.push_str(&format!("#EXT-X-TARGETDURATION:{target_duration}\n"));
    out.push_str(&format!("#EXT-X-MEDIA-SEQUENCE:{media_sequence}\n"));
    out.push_str(&format!(
        "#EXT-X-MAP:URI=\"/archive/{}/init.mp4?hash={}\"\n",
        urlencoding::encode(broadcast_name),
        init_hash
    ));
    for segment in segments {
        out.push_str(&format!("#EXTINF:{:.3},\n", segment.duration_secs));
        out.push_str(&format!(
            "/archive/{}/segment/{}.m4s?track={}\n",
            urlencoding::encode(broadcast_name),
            segment.hash,
            urlencoding::encode(track_name)
        ));
    }
    out
}

fn validate_blake3_hex(hash: &str) -> bool {
    hash.len() == 64 && hash.bytes().all(|b| b.is_ascii_hexdigit())
}

fn cas_path_for_hash(cas_root: &Path, hash: &str) -> Result<PathBuf> {
    if !validate_blake3_hex(hash) {
        return Err(anyhow!("invalid hash"));
    }
    let hash = hash.to_ascii_lowercase();
    let shard = &hash[0..2];
    Ok(cas_root.join(shard).join(format!("{hash}.bin")))
}

fn archive_origin_object_url(base: &Url, relative_path: &str) -> Result<Url> {
    let mut root = base.clone();
    root.set_query(None);
    root.set_fragment(None);
    if !root.path().ends_with('/') {
        let path = format!("{}/", root.path().trim_end_matches('/'));
        root.set_path(&path);
    }
    root.join(relative_path)
        .with_context(|| format!("failed to build archive origin URL for {relative_path}"))
}

fn archive_origin_object_urls(state: &ArchiveReplayState, hash: &str) -> Result<Vec<Url>> {
    if !validate_blake3_hex(hash) {
        return Err(anyhow!("invalid hash"));
    }
    let hash = hash.to_ascii_lowercase();
    let shard = &hash[0..2];
    let mut urls = Vec::new();
    if let Some(base) = &state.archive_cas_origin_url {
        urls.push(archive_origin_object_url(
            base,
            &format!("{shard}/{hash}.bin"),
        )?);
    }
    if let Some(base) = &state.archive_origin_url {
        urls.push(archive_origin_object_url(
            base,
            &format!("objects/blake3/{shard}/{hash}.bin"),
        )?);
    }
    Ok(urls)
}

fn mark_archive_object_access(state: &ArchiveReplayState, hash: &str) {
    if !validate_blake3_hex(hash) {
        return;
    }
    let hash = hash.to_ascii_lowercase();
    let marker = state
        .cache_access_root
        .join(&hash[0..2])
        .join(format!("{hash}.access"));
    let now_ms = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis()
        .to_string();
    if let Some(parent) = marker.parent() {
        if let Err(err) = fs::create_dir_all(parent) {
            tracing::debug!(path = %parent.display(), err = %err, "failed to create archive cache access marker directory");
            return;
        }
    }
    if let Err(err) = fs::write(&marker, now_ms) {
        tracing::debug!(path = %marker.display(), err = %err, "failed to write archive cache access marker");
    }
}

async fn fetch_archive_origin_object(
    state: &ArchiveReplayState,
    hash: &str,
    dest: &Path,
) -> Result<Option<Vec<u8>>> {
    let expected_hash = hash.to_ascii_lowercase();
    let urls = archive_origin_object_urls(state, &expected_hash)?;
    if urls.is_empty() {
        return Ok(None);
    }

    let mut last_err: Option<anyhow::Error> = None;
    for url in urls {
        let response = match state.http_client.get(url.clone()).send().await {
            Ok(response) => response,
            Err(err) => {
                last_err = Some(anyhow!("GET {url} failed: {err}"));
                continue;
            }
        };
        if response.status() == reqwest::StatusCode::NOT_FOUND {
            continue;
        }
        if !response.status().is_success() {
            last_err = Some(anyhow!("GET {url} returned {}", response.status()));
            continue;
        }
        if response
            .content_length()
            .is_some_and(|len| len > state.archive_origin_max_bytes as u64)
        {
            return Err(anyhow!("archive origin object exceeds size cap: {url}"));
        }
        let bytes = response
            .bytes()
            .await
            .with_context(|| format!("failed to read archive origin body: {url}"))?;
        if bytes.len() > state.archive_origin_max_bytes {
            return Err(anyhow!("archive origin object exceeds size cap: {url}"));
        }
        let actual = blake3::hash(&bytes).to_hex().to_string();
        if actual != expected_hash {
            return Err(anyhow!(
                "archive origin hash mismatch for {url}: expected {expected_hash}, got {actual}"
            ));
        }

        if let Some(parent) = dest.parent() {
            fs::create_dir_all(parent)
                .with_context(|| format!("failed to create CAS shard {}", parent.display()))?;
        }
        let nonce = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        let tmp = dest.with_file_name(format!(
            ".{}.tmp-{}-{nonce}",
            dest.file_name()
                .and_then(|name| name.to_str())
                .unwrap_or("archive-object"),
            std::process::id()
        ));
        fs::write(&tmp, &bytes)
            .with_context(|| format!("failed to write temporary CAS object {}", tmp.display()))?;
        fs::rename(&tmp, dest)
            .with_context(|| format!("failed to promote CAS object {}", dest.display()))?;
        tracing::info!(hash = %expected_hash, origin = %url, path = %dest.display(), "cached archive object from origin");
        return Ok(Some(bytes.to_vec()));
    }

    if let Some(err) = last_err {
        return Err(err);
    }
    Ok(None)
}

async fn read_archive_object(state: &ArchiveReplayState, hash: &str) -> Result<Option<Vec<u8>>> {
    let path = cas_path_for_hash(&state.cas_root, hash)?;
    match fs::read(&path) {
        Ok(bytes) => {
            mark_archive_object_access(state, hash);
            Ok(Some(bytes))
        }
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
            let fetched = fetch_archive_origin_object(state, hash, &path).await?;
            if fetched.is_some() {
                mark_archive_object_access(state, hash);
            }
            Ok(fetched)
        }
        Err(err) => {
            Err(err).with_context(|| format!("failed to read archive object {}", path.display()))
        }
    }
}

fn archive_response(status: u16, content_type: &str, body: Vec<u8>) -> ArchiveHttpResponse {
    ArchiveHttpResponse {
        status,
        content_type: content_type.to_string(),
        body,
    }
}

fn archive_error(status: u16, message: &str) -> ArchiveHttpResponse {
    archive_response(
        status,
        "application/json; charset=utf-8",
        serde_json::json!({ "error": message })
            .to_string()
            .into_bytes(),
    )
}

fn decode_archive_group_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
    let mut pos = 0usize;
    let mut out = Vec::new();
    let mut decoded_any = false;

    while pos < bytes.len() {
        if bytes.len().saturating_sub(pos) < 4 {
            if decoded_any {
                return Err(anyhow!("archive group ended mid-frame header"));
            }
            return Ok(bytes.to_vec());
        }

        let meta_len =
            u32::from_be_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]])
                as usize;
        let meta_start = pos + 4;
        let meta_end = meta_start + meta_len;
        if meta_end > bytes.len() {
            if decoded_any {
                return Err(anyhow!("archive group ended mid-frame metadata"));
            }
            return Ok(bytes.to_vec());
        }

        let meta: ObjectMeta = match serde_json::from_slice(&bytes[meta_start..meta_end]) {
            Ok(meta) => meta,
            Err(err) => {
                if decoded_any {
                    return Err(anyhow!("archive group metadata parse failed: {err:#}"));
                }
                return Ok(bytes.to_vec());
            }
        };
        let data_len = usize::try_from(meta.size_bytes)
            .map_err(|_| anyhow!("archive object size exceeds addressable memory"))?;
        let data_start = meta_end;
        let data_end = data_start + data_len;
        if data_end > bytes.len() {
            if decoded_any {
                return Err(anyhow!("archive group ended mid-frame payload"));
            }
            return Ok(bytes.to_vec());
        }

        out.extend_from_slice(&bytes[data_start..data_end]);
        pos = data_end;
        decoded_any = true;
    }

    Ok(out)
}

fn archive_status_text(status: u16) -> &'static str {
    match status {
        200 => "OK",
        204 => "No Content",
        400 => "Bad Request",
        404 => "Not Found",
        405 => "Method Not Allowed",
        500 => "Internal Server Error",
        502 => "Bad Gateway",
        _ => "OK",
    }
}

async fn handle_archive_http_request(
    state: &ArchiveReplayState,
    method: &str,
    target: &str,
) -> ArchiveHttpResponse {
    if method == "OPTIONS" {
        return archive_response(204, "text/plain; charset=utf-8", Vec::new());
    }
    if method != "GET" && method != "HEAD" {
        return archive_error(405, "method not allowed");
    }

    let req_url = match Url::parse(&format!("http://localhost{target}")) {
        Ok(url) => url,
        Err(_) => return archive_error(400, "invalid url"),
    };
    let path = req_url.path();
    if path == "/archive/healthz" {
        return archive_response(
            200,
            "application/json; charset=utf-8",
            serde_json::json!({ "ok": true }).to_string().into_bytes(),
        );
    }

    let parts = path.trim_start_matches('/').split('/').collect::<Vec<_>>();
    if parts.len() < 3 || parts[0] != "archive" {
        return archive_error(404, "not found");
    }

    let broadcast_name = match urlencoding::decode(parts[1]) {
        Ok(v) => v.to_string(),
        Err(_) => return archive_error(400, "invalid broadcast"),
    };
    if broadcast_name.is_empty() {
        return archive_error(400, "missing broadcast");
    }

    let from_ms = parse_from_ms(&req_url);
    let limit = parse_limit(&req_url);

    match parts[2] {
        "timeline.json" if parts.len() == 3 => {
            let video = match summarize_archive_track(
                &state.manifest_root,
                &broadcast_name,
                WT_PUBLISH_PRIMARY_VIDEO_TRACK,
                from_ms,
                limit,
            ) {
                Ok(records) => records,
                Err(err) => return archive_error(500, &format!("{err:#}")),
            };
            let audio = match summarize_archive_track(
                &state.manifest_root,
                &broadcast_name,
                WT_PUBLISH_PRIMARY_AUDIO_TRACK,
                from_ms,
                limit,
            ) {
                Ok(records) => records,
                Err(err) => return archive_error(500, &format!("{err:#}")),
            };
            let min_ms = video
                .start_unix_ms
                .into_iter()
                .chain(audio.start_unix_ms)
                .min();
            let max_ms = video.end_unix_ms.into_iter().chain(audio.end_unix_ms).max();
            let body = ArchiveTimelineResponse {
                broadcast_name,
                start_unix_ms: min_ms,
                end_unix_ms: max_ms,
                video_segments: video.segments,
                audio_segments: audio.segments,
            };
            match serde_json::to_vec(&body) {
                Ok(buf) => archive_response(200, "application/json; charset=utf-8", buf),
                Err(err) => archive_error(500, &format!("{err:#}")),
            }
        }
        "ladder.json" if parts.len() == 3 => {
            let body = match archive_ladder_status(
                &state.manifest_root,
                &broadcast_name,
                now_unix_ms(),
                state.archive_retention_seconds,
            ) {
                Ok(body) => body,
                Err(err) => return archive_error(500, &format!("{err:#}")),
            };
            match serde_json::to_vec(&body) {
                Ok(buf) => archive_response(200, "application/json; charset=utf-8", buf),
                Err(err) => archive_error(500, &format!("{err:#}")),
            }
        }
        "master.m3u8" if parts.len() == 3 => {
            let playlist = match archive_hls_master_playlist(
                &state.manifest_root,
                &broadcast_name,
                &req_url,
                now_unix_ms(),
                state.archive_retention_seconds,
            ) {
                Ok(Some(playlist)) => playlist,
                Ok(None) => {
                    let encoded = urlencoding::encode(&broadcast_name);
                    let query = archive_playlist_query(&req_url);
                    format!(
                        "#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audio\",NAME=\"main\",DEFAULT=YES,AUTOSELECT=YES,URI=\"/archive/{encoded}/audio.m3u8{query}\"\n#EXT-X-STREAM-INF:BANDWIDTH=3500000,CODECS=\"avc1.640028,mp4a.40.2\",AUDIO=\"audio\"\n/archive/{encoded}/video.m3u8{query}\n"
                    )
                }
                Err(err) => return archive_error(500, &format!("{err:#}")),
            };
            archive_response(
                200,
                "application/vnd.apple.mpegurl; charset=utf-8",
                playlist.into_bytes(),
            )
        }
        "track.m3u8" if parts.len() == 3 => {
            let track_broadcast_name = req_url
                .query_pairs()
                .find(|(k, _)| k == "broadcast")
                .map(|(_, value)| value.to_string())
                .unwrap_or_else(|| broadcast_name.clone());
            let track_name = req_url
                .query_pairs()
                .find(|(k, _)| k == "track")
                .map(|(_, value)| value.to_string())
                .unwrap_or_else(|| WT_PUBLISH_PRIMARY_VIDEO_TRACK.to_string());
            let playlist_limit = limit.or(Some(WT_ARCHIVE_HLS_DEFAULT_LIMIT));
            let init_hash =
                match latest_init_hash(&state.manifest_root, &track_broadcast_name, &track_name) {
                    Ok(Some(hash)) => hash,
                    Ok(None) => return archive_error(404, "missing init segment"),
                    Err(err) => return archive_error(500, &format!("{err:#}")),
                };
            let records = match parse_archive_track(
                &state.manifest_root,
                &track_broadcast_name,
                &track_name,
                from_ms,
                playlist_limit,
            ) {
                Ok(records) => records,
                Err(err) => return archive_error(500, &format!("{err:#}")),
            };
            if records.is_empty() {
                return archive_error(404, "archive track is empty");
            }
            let playlist =
                hls_playlist_for_track(&track_broadcast_name, &track_name, &init_hash, &records);
            archive_response(
                200,
                "application/vnd.apple.mpegurl; charset=utf-8",
                playlist.into_bytes(),
            )
        }
        "video.m3u8" if parts.len() == 3 => {
            let playlist_limit = limit.or(Some(WT_ARCHIVE_HLS_DEFAULT_LIMIT));
            let init_hash = match latest_init_hash(
                &state.manifest_root,
                &broadcast_name,
                WT_PUBLISH_PRIMARY_VIDEO_TRACK,
            ) {
                Ok(Some(hash)) => hash,
                Ok(None) => return archive_error(404, "missing init segment"),
                Err(err) => return archive_error(500, &format!("{err:#}")),
            };
            let records = match parse_archive_track(
                &state.manifest_root,
                &broadcast_name,
                WT_PUBLISH_PRIMARY_VIDEO_TRACK,
                from_ms,
                playlist_limit,
            ) {
                Ok(records) => records,
                Err(err) => return archive_error(500, &format!("{err:#}")),
            };
            if records.is_empty() {
                return archive_error(404, "video archive is empty");
            }
            let playlist = hls_playlist_for_track(
                &broadcast_name,
                WT_PUBLISH_PRIMARY_VIDEO_TRACK,
                &init_hash,
                &records,
            );
            archive_response(
                200,
                "application/vnd.apple.mpegurl; charset=utf-8",
                playlist.into_bytes(),
            )
        }
        "audio.m3u8" if parts.len() == 3 => {
            let playlist_limit = limit.or(Some(WT_ARCHIVE_HLS_DEFAULT_LIMIT));
            let init_hash = match latest_init_hash(
                &state.manifest_root,
                &broadcast_name,
                WT_PUBLISH_PRIMARY_AUDIO_TRACK,
            ) {
                Ok(Some(hash)) => hash,
                Ok(None) => return archive_error(404, "missing init segment"),
                Err(err) => return archive_error(500, &format!("{err:#}")),
            };
            let records = match parse_archive_track(
                &state.manifest_root,
                &broadcast_name,
                WT_PUBLISH_PRIMARY_AUDIO_TRACK,
                from_ms,
                playlist_limit,
            ) {
                Ok(records) => records,
                Err(err) => return archive_error(500, &format!("{err:#}")),
            };
            if records.is_empty() {
                return archive_error(404, "audio archive is empty");
            }
            let playlist = hls_playlist_for_track(
                &broadcast_name,
                WT_PUBLISH_PRIMARY_AUDIO_TRACK,
                &init_hash,
                &records,
            );
            archive_response(
                200,
                "application/vnd.apple.mpegurl; charset=utf-8",
                playlist.into_bytes(),
            )
        }
        "init.mp4" if parts.len() == 3 => {
            let hash = req_url
                .query_pairs()
                .find(|(k, _)| k == "hash")
                .map(|(_, v)| v.to_string())
                .or_else(|| {
                    latest_init_hash(
                        &state.manifest_root,
                        &broadcast_name,
                        WT_PUBLISH_PRIMARY_VIDEO_TRACK,
                    )
                    .ok()
                    .flatten()
                })
                .or_else(|| {
                    latest_init_hash(
                        &state.manifest_root,
                        &broadcast_name,
                        WT_PUBLISH_PRIMARY_AUDIO_TRACK,
                    )
                    .ok()
                    .flatten()
                });
            let Some(hash) = hash else {
                return archive_error(404, "missing init segment");
            };
            if let Err(_) = cas_path_for_hash(&state.cas_root, &hash) {
                return archive_error(400, "invalid hash");
            }
            match read_archive_object(state, &hash).await {
                Ok(Some(bytes)) => match decode_archive_group_bytes(&bytes) {
                    Ok(payload) => archive_response(200, "video/mp4", payload),
                    Err(err) => archive_error(500, &format!("{err:#}")),
                },
                Ok(None) => archive_error(404, "init segment not found"),
                Err(err) => archive_error(502, &format!("{err:#}")),
            }
        }
        "segment" if parts.len() == 4 => {
            let hash_part = parts[3].strip_suffix(".m4s").unwrap_or(parts[3]);
            if let Err(_) = cas_path_for_hash(&state.cas_root, hash_part) {
                return archive_error(400, "invalid hash");
            }
            match read_archive_object(state, hash_part).await {
                Ok(Some(bytes)) => match decode_archive_group_bytes(&bytes) {
                    Ok(payload) => archive_response(200, "video/mp4", payload),
                    Err(err) => archive_error(500, &format!("{err:#}")),
                },
                Ok(None) => archive_error(404, "segment not found"),
                Err(err) => archive_error(502, &format!("{err:#}")),
            }
        }
        _ => archive_error(404, "not found"),
    }
}

async fn handle_archive_http_connection(
    mut stream: tokio::net::TcpStream,
    state: ArchiveReplayState,
) -> Result<()> {
    let mut buf = Vec::with_capacity(4096);
    let mut tmp = [0u8; 2048];
    loop {
        let read = stream
            .read(&mut tmp)
            .await
            .context("failed to read replay request")?;
        if read == 0 {
            return Ok(());
        }
        buf.extend_from_slice(&tmp[..read]);
        if buf.windows(4).any(|w| w == b"\r\n\r\n") {
            break;
        }
        if buf.len() > 16 * 1024 {
            let response = archive_error(400, "request headers too large");
            let head = format!(
                "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET,HEAD,OPTIONS\r\nAccess-Control-Allow-Headers: *\r\nCache-Control: no-store\r\n\r\n",
                response.status,
                archive_status_text(response.status),
                response.content_type,
                response.body.len(),
            );
            stream.write_all(head.as_bytes()).await?;
            return Ok(());
        }
    }

    let req = String::from_utf8_lossy(&buf);
    let mut lines = req.lines();
    let first = lines.next().unwrap_or_default();
    let mut parts = first.split_whitespace();
    let method = parts.next().unwrap_or("");
    let target = parts.next().unwrap_or("/");
    let response = handle_archive_http_request(&state, method, target).await;
    let is_head = method == "HEAD";
    let body_len = if is_head { 0 } else { response.body.len() };
    let head = format!(
        "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET,HEAD,OPTIONS\r\nAccess-Control-Allow-Headers: *\r\nCache-Control: no-store\r\n\r\n",
        response.status,
        archive_status_text(response.status),
        response.content_type,
        body_len
    );
    stream
        .write_all(head.as_bytes())
        .await
        .context("failed to write replay response headers")?;
    if !is_head {
        stream
            .write_all(&response.body)
            .await
            .context("failed to write replay response body")?;
    }
    Ok(())
}

#[derive(Debug, Clone)]
struct WtPublishRelayArgs {
    url: String,
    name: String,
    tls_disable_verify: bool,
    control_announce: bool,
    control_ttl_ms: u64,
    control_interval_ms: u64,
    iroh_secret: Option<String>,
    discovery: Option<String>,
    gossip_peer: Vec<String>,
    control_endpoint_addr_out: Option<PathBuf>,
}

struct WtPublishRelayState {
    session: moq_lite::Session,
    broadcast: moq_lite::BroadcastProducer,
    catalog: moq_mux::catalog::Producer,
    init_track: moq_lite::TrackProducer,
    control_stop: Option<oneshot::Sender<()>>,
}

#[derive(Clone)]
struct ProtocolOverride<S> {
    inner: S,
    protocol: Option<String>,
}

impl<S: web_transport_trait::Session> web_transport_trait::Session for ProtocolOverride<S> {
    type SendStream = S::SendStream;
    type RecvStream = S::RecvStream;
    type Error = S::Error;

    fn accept_uni(
        &self,
    ) -> impl Future<Output = Result<Self::RecvStream, Self::Error>> + web_transport_trait::MaybeSend
    {
        self.inner.accept_uni()
    }

    fn accept_bi(
        &self,
    ) -> impl Future<Output = Result<(Self::SendStream, Self::RecvStream), Self::Error>>
           + web_transport_trait::MaybeSend {
        self.inner.accept_bi()
    }

    fn open_bi(
        &self,
    ) -> impl Future<Output = Result<(Self::SendStream, Self::RecvStream), Self::Error>>
           + web_transport_trait::MaybeSend {
        self.inner.open_bi()
    }

    fn open_uni(
        &self,
    ) -> impl Future<Output = Result<Self::SendStream, Self::Error>> + web_transport_trait::MaybeSend
    {
        self.inner.open_uni()
    }

    fn send_datagram(&self, payload: bytes::Bytes) -> Result<(), Self::Error> {
        self.inner.send_datagram(payload)
    }

    fn recv_datagram(
        &self,
    ) -> impl Future<Output = Result<bytes::Bytes, Self::Error>> + web_transport_trait::MaybeSend
    {
        self.inner.recv_datagram()
    }

    fn max_datagram_size(&self) -> usize {
        self.inner.max_datagram_size()
    }

    fn protocol(&self) -> Option<&str> {
        self.protocol.as_deref().or_else(|| self.inner.protocol())
    }

    fn close(&self, code: u32, reason: &str) {
        self.inner.close(code, reason)
    }

    fn closed(&self) -> impl Future<Output = Self::Error> + web_transport_trait::MaybeSend {
        self.inner.closed()
    }
}

async fn connect_moq_session(
    relay_url: &Url,
    publish: moq_lite::OriginConsumer,
    tls_disable_verify: bool,
) -> Result<moq_lite::Session> {
    let host = relay_url
        .host_str()
        .ok_or_else(|| anyhow!("relay url missing host: {relay_url}"))?
        .to_string();
    let port = relay_url.port().unwrap_or(443);

    let mut roots = rustls::RootCertStore::empty();
    let native = rustls_native_certs::load_native_certs();
    if !native.errors.is_empty() {
        tracing::warn!(
            errors = ?native.errors,
            "some native root certs could not be loaded"
        );
    }
    for cert in native.certs {
        let _ = roots.add(cert);
    }

    let mut tls = rustls::ClientConfig::builder()
        .with_root_certificates(roots)
        .with_no_client_auth();

    if tls_disable_verify {
        #[derive(Debug)]
        struct NoCertificateVerification(Arc<rustls::crypto::CryptoProvider>);

        impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
            fn verify_server_cert(
                &self,
                _end_entity: &rustls::pki_types::CertificateDer<'_>,
                _intermediates: &[rustls::pki_types::CertificateDer<'_>],
                _server_name: &rustls::pki_types::ServerName<'_>,
                _ocsp: &[u8],
                _now: rustls::pki_types::UnixTime,
            ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
                Ok(rustls::client::danger::ServerCertVerified::assertion())
            }

            fn verify_tls12_signature(
                &self,
                message: &[u8],
                cert: &rustls::pki_types::CertificateDer<'_>,
                dss: &rustls::DigitallySignedStruct,
            ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error>
            {
                rustls::crypto::verify_tls12_signature(
                    message,
                    cert,
                    dss,
                    &self.0.signature_verification_algorithms,
                )
            }

            fn verify_tls13_signature(
                &self,
                message: &[u8],
                cert: &rustls::pki_types::CertificateDer<'_>,
                dss: &rustls::DigitallySignedStruct,
            ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error>
            {
                rustls::crypto::verify_tls13_signature(
                    message,
                    cert,
                    dss,
                    &self.0.signature_verification_algorithms,
                )
            }

            fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
                self.0.signature_verification_algorithms.supported_schemes()
            }
        }

        let provider = rustls::crypto::CryptoProvider::get_default()
            .cloned()
            .unwrap_or_else(|| Arc::new(rustls::crypto::ring::default_provider()));
        tls.dangerous()
            .set_certificate_verifier(Arc::new(NoCertificateVerification(provider)));
    }

    tls.alpn_protocols = vec![web_transport_quinn::ALPN.as_bytes().to_vec()];

    let socket = std::net::UdpSocket::bind("[::]:0").context("failed to bind UDP socket")?;

    let mut transport = quinn::TransportConfig::default();
    transport.max_idle_timeout(Some(Duration::from_secs(10).try_into().unwrap()));
    transport.keep_alive_interval(Some(Duration::from_secs(4)));
    transport.mtu_discovery_config(None);

    let transport = Arc::new(transport);
    let runtime = quinn::default_runtime().context("no async runtime")?;
    let endpoint_config = quinn::EndpointConfig::default();
    let endpoint = quinn::Endpoint::new(endpoint_config, None, socket, runtime)
        .context("failed to create QUIC endpoint")?;

    let ip = tokio::net::lookup_host((host.clone(), port))
        .await
        .context("failed DNS lookup")?
        .next()
        .context("no DNS entries")?;

    let quic: quinn::crypto::rustls::QuicClientConfig = tls.try_into()?;
    let mut client_cfg = quinn::ClientConfig::new(Arc::new(quic));
    client_cfg.transport_config(transport);

    tracing::debug!(%ip, %host, %relay_url, "connecting QUIC");
    let connection = endpoint
        .connect_with(client_cfg, ip, &host)?
        .await
        .context("failed QUIC connect")?;

    let mut request = web_transport_quinn::proto::ConnectRequest::new(relay_url.clone());
    for alpn in moq_lite::ALPNS {
        request = request.with_protocol(alpn.to_string());
    }
    let wt = web_transport_quinn::Session::connect(connection, request)
        .await
        .context("failed WebTransport CONNECT")?;

    let client = moq_lite::Client::new().with_publish(publish);

    match client.connect(wt.clone()).await {
        Ok(session) => {
            tracing::info!("connected to relay (native protocol negotiation)");
            return Ok(session);
        }
        Err(err) => {
            tracing::debug!(err = %err, "native MoQ SETUP failed; trying protocol overrides");
        }
    }

    let attempts: [&str; 4] = ["moqt-16", "moqt-15", "moq-00", ""];
    let mut last_err: Option<anyhow::Error> = None;

    for p in attempts {
        let session = ProtocolOverride {
            inner: wt.clone(),
            protocol: (!p.is_empty()).then(|| p.to_string()),
        };

        match client.connect(session).await {
            Ok(session) => {
                tracing::info!(protocol = %p, "connected to relay");
                return Ok(session);
            }
            Err(err) => {
                last_err = Some(anyhow::Error::new(err));
                tracing::debug!(
                    protocol = %p,
                    err = %last_err.as_ref().unwrap(),
                    "MoQ SETUP failed; retrying"
                );
            }
        }
    }

    Err(last_err.unwrap_or_else(|| anyhow!("failed to connect"))).context("failed MoQ SETUP")
}

async fn open_wt_publish_relay(args: &WtPublishRelayArgs) -> Result<WtPublishRelayState> {
    let relay_url =
        Url::parse(&args.url).with_context(|| format!("invalid relay url: {}", args.url))?;
    let relay_url_str = relay_url.to_string();

    let mut control_stop: Option<oneshot::Sender<()>> = None;
    if args.control_announce {
        let secret = parse_iroh_secret(args.iroh_secret.clone())?;
        let discovery = parse_discovery(args.discovery.as_deref())?;
        let endpoint = ec_iroh::build_endpoint(secret, discovery).await?;
        let gossip_peers = parse_gossip_peers(args.gossip_peer.clone());
        let endpoint_addr_json =
            serde_json::to_string(&endpoint.addr()).unwrap_or_else(|_| endpoint.id().to_string());

        let announcement = build_control_announcement(
            args.name.clone(),
            args.name.clone(),
            vec![StreamTransportDescriptor::RelayMoq {
                url: relay_url_str.clone(),
                broadcast_name: args.name.clone(),
                track_name: WT_PUBLISH_PRIMARY_VIDEO_TRACK.to_string(),
            }],
            args.control_ttl_ms,
        );

        match spawn_control_announcer_task(
            endpoint.clone(),
            gossip_peers,
            announcement,
            Duration::from_millis(args.control_interval_ms.max(1000)),
        )
        .await
        {
            Ok(stop_tx) => {
                eprintln!("control endpoint id: {}", endpoint.id());
                eprintln!("control endpoint addr: {}", endpoint_addr_json);

                if let Some(path) = args.control_endpoint_addr_out.as_ref() {
                    if let Some(parent) = path.parent() {
                        fs::create_dir_all(parent).with_context(|| {
                            format!(
                                "failed to create control endpoint addr parent dir: {}",
                                parent.display()
                            )
                        })?;
                    }
                    fs::write(path, format!("{endpoint_addr_json}\n")).with_context(|| {
                        format!(
                            "failed to write control endpoint addr file: {}",
                            path.display()
                        )
                    })?;
                    tracing::info!(
                        path = %path.display(),
                        "wrote control endpoint addr file"
                    );
                }

                tracing::info!(
                    endpoint = %endpoint.id(),
                    endpoint_addr = %endpoint_addr_json,
                    stream = %args.name,
                    "control announce enabled"
                );
                control_stop = Some(stop_tx);
            }
            Err(err) => {
                tracing::warn!("failed to start control announce task: {err:#}");
            }
        }
    }

    let origin = moq_lite::Origin::random().produce();
    let publish = origin.consume();
    let mut broadcast = origin
        .create_broadcast(&args.name)
        .ok_or_else(|| anyhow!("failed to create broadcast: {}", args.name))?;
    let init_track = broadcast.create_track(moq_lite::Track {
        name: WT_PUBLISH_INIT_TRACK.to_string(),
        priority: 0,
    })?;
    let catalog = moq_mux::catalog::Producer::new(&mut broadcast)
        .context("failed to create MoQ media catalog tracks")?;

    tracing::info!(url=%relay_url, name=%args.name, "connecting to relay");
    let session = connect_moq_session(&relay_url, publish, args.tls_disable_verify).await?;

    Ok(WtPublishRelayState {
        session,
        broadcast,
        catalog,
        init_track,
        control_stop,
    })
}

fn top_level_moof_offset(data: &[u8]) -> Result<Option<usize>> {
    let mut offset = 0usize;
    while data.len().saturating_sub(offset) >= 8 {
        let size32 = u32::from_be_bytes([
            data[offset],
            data[offset + 1],
            data[offset + 2],
            data[offset + 3],
        ]);
        let box_type = &data[offset + 4..offset + 8];
        let mut header_len = 8usize;
        let box_size = if size32 == 1 {
            if data.len().saturating_sub(offset) < 16 {
                return Ok(None);
            }
            header_len = 16;
            u64::from_be_bytes([
                data[offset + 8],
                data[offset + 9],
                data[offset + 10],
                data[offset + 11],
                data[offset + 12],
                data[offset + 13],
                data[offset + 14],
                data[offset + 15],
            ])
        } else if size32 == 0 {
            return Ok(None);
        } else {
            u64::from(size32)
        };
        if box_size < header_len as u64 {
            return Err(anyhow!("invalid fMP4 box size"));
        }
        if box_type == b"moof" {
            return Ok(Some(offset));
        }
        let box_size = usize::try_from(box_size).context("fMP4 box too large")?;
        let next = offset
            .checked_add(box_size)
            .ok_or_else(|| anyhow!("fMP4 box offset overflow"))?;
        if data.len() < next {
            return Ok(None);
        }
        offset = next;
    }
    Ok(None)
}

async fn read_fmp4_init_replay_prefix<R: AsyncRead + Unpin>(
    reader: &mut R,
    max_bytes: usize,
) -> Result<(Vec<u8>, Vec<u8>)> {
    let mut buffer = Vec::new();
    let mut chunk = [0u8; 16 * 1024];
    loop {
        let n = reader
            .read(&mut chunk)
            .await
            .context("failed to read fMP4 publisher output")?;
        if n == 0 {
            return Err(anyhow!("fMP4 stream ended before first moof"));
        }
        if buffer.len().saturating_add(n) > max_bytes {
            return Err(anyhow!(
                "fMP4 init prefix exceeded {} bytes before first moof",
                max_bytes
            ));
        }
        buffer.extend_from_slice(&chunk[..n]);
        let Some(moof_offset) = top_level_moof_offset(&buffer)? else {
            continue;
        };
        if moof_offset == 0 {
            return Err(anyhow!("fMP4 stream started with moof and no init segment"));
        }
        return Ok((buffer[..moof_offset].to_vec(), buffer));
    }
}

fn publish_raw_moq_group(
    track: &mut moq_lite::TrackProducer,
    track_name: &str,
    sequence: u64,
    data: Vec<u8>,
) -> Result<()> {
    let mut group_writer = track
        .create_group(moq_lite::Group { sequence })
        .with_context(|| format!("failed to create {track_name} group {sequence}"))?;
    group_writer
        .write_frame(bytes::Bytes::from(data))
        .with_context(|| format!("failed to write {track_name} group {sequence}"))?;
    group_writer
        .finish()
        .with_context(|| format!("failed to finish {track_name} group {sequence}"))?;
    Ok(())
}

async fn prepare_fmp4_import_reader<R: AsyncRead + Unpin>(
    reader: &mut R,
    init_track: &mut moq_lite::TrackProducer,
) -> Result<Vec<u8>> {
    let (init, replay_prefix) =
        read_fmp4_init_replay_prefix(reader, WT_PUBLISH_INIT_CAPTURE_MAX_BYTES).await?;
    if !init.starts_with(b"\0\0\0") || top_level_moof_offset(&init)?.is_some() {
        return Err(anyhow!("captured fMP4 init prefix is invalid"));
    }
    publish_raw_moq_group(init_track, WT_PUBLISH_INIT_TRACK, 0, init)?;
    Ok(replay_prefix)
}

async fn import_fmp4_with_publisher_archive<R: AsyncRead + Unpin>(
    importer: &mut moq_mux::import::Fmp4,
    reader: &mut R,
    publisher_archive: &mut Option<PublisherArchiveWriter>,
) -> Result<()> {
    let mut import_buffer = BytesMut::new();
    let mut chunk = [0u8; 64 * 1024];
    loop {
        let n = reader
            .read(&mut chunk)
            .await
            .context("failed to read fMP4 publisher output")?;
        if n == 0 {
            break;
        }
        if let Some(writer) = publisher_archive.as_mut() {
            writer.observe_bytes(&chunk[..n])?;
        }
        import_buffer.extend_from_slice(&chunk[..n]);
        importer.decode(&mut import_buffer)?;
    }
    if let Some(writer) = publisher_archive.as_ref() {
        writer.finish()?;
    }
    Ok(())
}

fn relay_announced_url(relay_url: &str) -> Result<Url> {
    let mut url =
        Url::parse(relay_url).with_context(|| format!("invalid relay url: {relay_url}"))?;
    url.set_path("/announced");
    url.set_query(None);
    url.set_fragment(None);
    Ok(url)
}

fn announced_body_contains_broadcast(body: &str, broadcast_name: &str) -> bool {
    body.split_whitespace().any(|item| {
        item.strip_prefix("anon/")
            .unwrap_or(item)
            .eq(broadcast_name)
    })
}

async fn relay_announced_watchdog(
    announced_url: Url,
    broadcast_name: String,
    missing_timeout: Duration,
    interval: Duration,
    tls_disable_verify: bool,
) -> Result<()> {
    let client = reqwest::Client::builder()
        .timeout(interval.min(Duration::from_secs(8)))
        .danger_accept_invalid_certs(tls_disable_verify)
        .build()
        .context("failed to build relay announcement watchdog HTTP client")?;
    let mut last_seen = Instant::now();
    let mut missing_logged = false;

    loop {
        let observed = match client.get(announced_url.clone()).send().await {
            Ok(res) if res.status().is_success() => {
                let body = res
                    .text()
                    .await
                    .context("failed to read relay announcement body")?;
                announced_body_contains_broadcast(&body, &broadcast_name)
            }
            Ok(res) => {
                tracing::warn!(
                    status = %res.status(),
                    url = %announced_url,
                    broadcast = %broadcast_name,
                    "relay announcement probe returned non-success"
                );
                false
            }
            Err(err) => {
                tracing::warn!(
                    err = %err,
                    url = %announced_url,
                    broadcast = %broadcast_name,
                    "relay announcement probe failed"
                );
                false
            }
        };

        if observed {
            if missing_logged {
                tracing::info!(
                    url = %announced_url,
                    broadcast = %broadcast_name,
                    "relay announcement restored"
                );
            }
            last_seen = Instant::now();
            missing_logged = false;
        } else {
            if !missing_logged {
                tracing::warn!(
                    url = %announced_url,
                    broadcast = %broadcast_name,
                    timeout_ms = missing_timeout.as_millis(),
                    "relay is not announcing broadcast"
                );
                missing_logged = true;
            }
            if last_seen.elapsed() >= missing_timeout {
                return Err(anyhow!(
                    "relay did not announce broadcast {broadcast_name} at {announced_url} for {}ms",
                    missing_timeout.as_millis()
                ));
            }
        }

        tokio::time::sleep(interval).await;
    }
}

async fn wt_archive_serve(args: WtArchiveServeArgs) -> Result<()> {
    let manifest_root = args
        .manifest_dir
        .unwrap_or_else(|| args.output_dir.join("manifests"));
    let cas_root = args.output_dir.join("objects").join("blake3");
    let cache_access_root = args
        .cache_access_dir
        .unwrap_or_else(|| args.output_dir.join("cache-access").join("blake3"));
    let archive_origin_url = args
        .archive_origin_url
        .as_deref()
        .map(Url::parse)
        .transpose()
        .context("invalid --archive-origin-url")?;
    let archive_cas_origin_url = args
        .archive_cas_origin_url
        .as_deref()
        .map(Url::parse)
        .transpose()
        .context("invalid --archive-cas-origin-url")?;
    let http_client = reqwest::Client::builder()
        .timeout(Duration::from_secs(20))
        .build()
        .context("failed to build archive origin HTTP client")?;
    fs::create_dir_all(&manifest_root)
        .with_context(|| format!("failed to create manifest dir {}", manifest_root.display()))?;
    fs::create_dir_all(&cas_root)
        .with_context(|| format!("failed to create CAS dir {}", cas_root.display()))?;
    fs::create_dir_all(&cache_access_root).with_context(|| {
        format!(
            "failed to create cache access dir {}",
            cache_access_root.display()
        )
    })?;

    let listener = TcpListener::bind(&args.listen)
        .await
        .with_context(|| format!("failed to bind archive replay listener {}", args.listen))?;
    let local = listener
        .local_addr()
        .context("failed to read listen addr")?;
    tracing::info!(
        listen = %local,
        manifest_root = %manifest_root.display(),
        cas_root = %cas_root.display(),
        archive_origin_url = ?archive_origin_url,
        archive_cas_origin_url = ?archive_cas_origin_url,
        "archive replay server listening"
    );

    let state = ArchiveReplayState {
        cas_root,
        manifest_root,
        archive_origin_url,
        archive_cas_origin_url,
        archive_origin_max_bytes: args.archive_origin_max_bytes,
        archive_retention_seconds: args.archive_retention_seconds,
        cache_access_root,
        http_client,
    };

    loop {
        let (stream, peer) = listener.accept().await.context("accept failed")?;
        let state_clone = state.clone();
        tokio::spawn(async move {
            if let Err(err) = handle_archive_http_connection(stream, state_clone).await {
                tracing::debug!(peer = %peer, err = %err, "archive replay request failed");
            }
        });
    }
}

fn publisher_start_boundary_delay_ms(now_ms: u64, boundary_ms: u64) -> u64 {
    if boundary_ms == 0 {
        return 0;
    }
    let remainder = now_ms % boundary_ms;
    if remainder == 0 {
        0
    } else {
        boundary_ms - remainder
    }
}

async fn wait_for_publisher_start_boundary(boundary_ms: u64) {
    let delay_ms = publisher_start_boundary_delay_ms(now_unix_ms(), boundary_ms);
    if delay_ms == 0 {
        return;
    }
    tracing::info!(
        boundary_ms,
        delay_ms,
        "waiting for publisher media start boundary"
    );
    tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}

fn wt_publish_ffmpeg_args(args: &WtPublishArgs, publish_input: &str) -> Vec<OsString> {
    let mut cmd_args = vec![
        OsString::from("-hide_banner"),
        OsString::from("-loglevel"),
        OsString::from("error"),
        OsString::from("-nostats"),
        OsString::from("-fflags"),
        OsString::from("+nobuffer+bitexact"),
        OsString::from("-flags"),
        OsString::from("low_delay"),
        OsString::from("-copyts"),
    ];

    if args.realtime_input {
        cmd_args.push(OsString::from("-re"));
    }

    if let Some(format) = args.input_format.as_deref() {
        cmd_args.push(OsString::from("-f"));
        cmd_args.push(OsString::from(format));
    }

    if args.publisher_wallclock_timestamps {
        cmd_args.push(OsString::from("-use_wallclock_as_timestamps"));
        cmd_args.push(OsString::from("1"));
    }

    cmd_args.push(OsString::from("-i"));
    cmd_args.push(OsString::from(publish_input));

    if args.transcode {
        cmd_args.extend([
            OsString::from("-map"),
            OsString::from("0:v:0"),
            OsString::from("-map"),
            OsString::from("0:a:0?"),
            OsString::from("-c:v"),
            OsString::from("libx264"),
            OsString::from("-vf"),
            OsString::from(args.video_filter.as_str()),
            OsString::from("-preset"),
            OsString::from(args.video_preset.as_str()),
            OsString::from("-tune"),
            OsString::from("zerolatency"),
            OsString::from("-crf"),
            OsString::from(args.video_crf.to_string()),
            OsString::from("-pix_fmt"),
            OsString::from("yuv420p"),
            OsString::from("-profile:v"),
            OsString::from("main"),
            OsString::from("-g"),
            OsString::from(args.gop_frames.to_string()),
            OsString::from("-keyint_min"),
            OsString::from(args.gop_frames.to_string()),
            OsString::from("-force_key_frames"),
            OsString::from(format!(
                "expr:if(isnan(prev_forced_t),gte(t,0),gte(t,prev_forced_t+{:.3}))",
                args.publisher_archive_segment_duration_ms as f64 / 1000.0
            )),
            OsString::from("-sc_threshold"),
            OsString::from("0"),
            OsString::from("-bf"),
            OsString::from("0"),
            OsString::from("-x264-params"),
            OsString::from("open-gop=0:scenecut=0:rc-lookahead=0:sync-lookahead=0:stitchable=1"),
            OsString::from("-threads"),
            OsString::from("1"),
            OsString::from("-c:a"),
            OsString::from("aac"),
            OsString::from("-profile:a"),
            OsString::from("aac_low"),
            OsString::from("-b:a"),
            OsString::from("160k"),
            OsString::from("-ac"),
            OsString::from("2"),
            OsString::from("-ar"),
            OsString::from("48000"),
            OsString::from("-af"),
            OsString::from(ec_chopper::LIVE_AUDIO_RESAMPLE_FILTER),
            OsString::from("-max_muxing_queue_size"),
            OsString::from("2048"),
        ]);
    } else {
        cmd_args.extend([OsString::from("-c"), OsString::from("copy")]);
    }

    cmd_args.extend([
        OsString::from("-avoid_negative_ts"),
        OsString::from("disabled"),
        OsString::from("-f"),
        OsString::from("mp4"),
        OsString::from("-movflags"),
        OsString::from(args.movflags.as_str()),
        OsString::from("pipe:1"),
    ]);

    cmd_args
}

#[derive(Debug, Clone)]
struct StatelessProofEncodeProfile {
    transcode: bool,
    video_filter: String,
    gop_frames: u32,
    video_preset: String,
    video_crf: u8,
    movflags: String,
}

impl StatelessProofEncodeProfile {
    #[cfg(test)]
    fn from_wt_publish_args(args: &WtPublishArgs) -> Self {
        Self {
            transcode: args.transcode,
            video_filter: args.video_filter.clone(),
            gop_frames: args.gop_frames,
            video_preset: args.video_preset.clone(),
            video_crf: args.video_crf,
            movflags: args.movflags.clone(),
        }
    }

    fn from_publisher_proof_segment_args(args: &PublisherProofSegmentArgs) -> Self {
        Self {
            transcode: args.transcode,
            video_filter: args.video_filter.clone(),
            gop_frames: args.gop_frames,
            video_preset: args.video_preset.clone(),
            video_crf: args.video_crf,
            movflags: args.movflags.clone(),
        }
    }

    fn from_publisher_proof_windows_args(args: &PublisherProofWindowsArgs) -> Self {
        Self {
            transcode: args.transcode,
            video_filter: args.video_filter.clone(),
            gop_frames: args.gop_frames,
            video_preset: args.video_preset.clone(),
            video_crf: args.video_crf,
            movflags: args.movflags.clone(),
        }
    }

    fn from_publisher_proof_archive_source_args(args: &PublisherProofArchiveSourceArgs) -> Self {
        Self {
            transcode: args.transcode,
            video_filter: args.video_filter.clone(),
            gop_frames: args.gop_frames,
            video_preset: args.video_preset.clone(),
            video_crf: args.video_crf,
            movflags: args.movflags.clone(),
        }
    }
}

fn stateless_proof_ffmpeg_args(
    profile: &StatelessProofEncodeProfile,
    input_ts: &Path,
    output_mp4: &Path,
) -> Vec<OsString> {
    let mut cmd_args = vec![
        OsString::from("-hide_banner"),
        OsString::from("-loglevel"),
        OsString::from("error"),
        OsString::from("-nostdin"),
        OsString::from("-y"),
        OsString::from("-fflags"),
        OsString::from("+bitexact"),
        OsString::from("-i"),
        input_ts.as_os_str().to_os_string(),
        OsString::from("-map"),
        OsString::from("0:v:0"),
        OsString::from("-map"),
        OsString::from("0:a:0?"),
        OsString::from("-sn"),
        OsString::from("-dn"),
        OsString::from("-map_metadata"),
        OsString::from("-1"),
        OsString::from("-filter_threads"),
        OsString::from("1"),
        OsString::from("-filter_complex_threads"),
        OsString::from("1"),
        OsString::from("-c:v"),
        OsString::from("libx264"),
        OsString::from("-vf"),
        OsString::from(profile.video_filter.as_str()),
        OsString::from("-preset"),
        OsString::from(profile.video_preset.as_str()),
        OsString::from("-tune"),
        OsString::from("zerolatency"),
        OsString::from("-crf"),
        OsString::from(profile.video_crf.to_string()),
        OsString::from("-pix_fmt"),
        OsString::from("yuv420p"),
        OsString::from("-profile:v"),
        OsString::from("main"),
        OsString::from("-g"),
        OsString::from(profile.gop_frames.to_string()),
        OsString::from("-keyint_min"),
        OsString::from(profile.gop_frames.to_string()),
        OsString::from("-force_key_frames"),
        OsString::from("expr:gte(t,0)"),
        OsString::from("-sc_threshold"),
        OsString::from("0"),
        OsString::from("-bf"),
        OsString::from("0"),
        OsString::from("-x264-params"),
        OsString::from("open-gop=0:scenecut=0:rc-lookahead=0:sync-lookahead=0:stitchable=1"),
        OsString::from("-threads"),
        OsString::from("1"),
        OsString::from("-flags:v"),
        OsString::from("+bitexact"),
        OsString::from("-c:a"),
        OsString::from("aac"),
        OsString::from("-profile:a"),
        OsString::from("aac_low"),
        OsString::from("-b:a"),
        OsString::from("160k"),
        OsString::from("-ac"),
        OsString::from("2"),
        OsString::from("-ar"),
        OsString::from("48000"),
        OsString::from("-af"),
        OsString::from(ec_chopper::LIVE_AUDIO_RESAMPLE_FILTER),
        OsString::from("-flags:a"),
        OsString::from("+bitexact"),
        OsString::from("-max_muxing_queue_size"),
        OsString::from("2048"),
        OsString::from("-avoid_negative_ts"),
        OsString::from("make_zero"),
        OsString::from("-f"),
        OsString::from("mp4"),
        OsString::from("-movflags"),
        OsString::from(profile.movflags.as_str()),
        output_mp4.as_os_str().to_os_string(),
    ];

    if !profile.transcode {
        cmd_args = vec![
            OsString::from("-hide_banner"),
            OsString::from("-loglevel"),
            OsString::from("error"),
            OsString::from("-nostdin"),
            OsString::from("-y"),
            OsString::from("-fflags"),
            OsString::from("+bitexact"),
            OsString::from("-i"),
            input_ts.as_os_str().to_os_string(),
            OsString::from("-c"),
            OsString::from("copy"),
            OsString::from("-map_metadata"),
            OsString::from("-1"),
            OsString::from("-avoid_negative_ts"),
            OsString::from("make_zero"),
            OsString::from("-f"),
            OsString::from("mp4"),
            OsString::from("-movflags"),
            OsString::from(profile.movflags.as_str()),
            output_mp4.as_os_str().to_os_string(),
        ];
    }

    cmd_args
}

#[cfg(test)]
fn wt_publish_stateless_proof_ffmpeg_args(
    args: &WtPublishArgs,
    input_ts: &Path,
    output_mp4: &Path,
) -> Vec<OsString> {
    stateless_proof_ffmpeg_args(
        &StatelessProofEncodeProfile::from_wt_publish_args(args),
        input_ts,
        output_mp4,
    )
}

#[derive(Debug, serde::Serialize)]
struct PublisherProofSegmentReport {
    input_ts: PathBuf,
    output_mp4: PathBuf,
    size_bytes: usize,
    init_size_bytes: usize,
    init_blake3: String,
    media_fragment_count: usize,
    media_fragment_blake3: Vec<String>,
}

fn publisher_proof_segment_command(args: PublisherProofSegmentArgs) -> Result<()> {
    if let Some(parent) = args.output_mp4.parent() {
        if !parent.as_os_str().is_empty() {
            fs::create_dir_all(parent).with_context(|| {
                format!(
                    "failed to create proof output directory {}",
                    parent.display()
                )
            })?;
        }
    }

    let profile = StatelessProofEncodeProfile::from_publisher_proof_segment_args(&args);
    let status = Command::new("ffmpeg")
        .args(stateless_proof_ffmpeg_args(
            &profile,
            &args.input_ts,
            &args.output_mp4,
        ))
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::inherit())
        .status()
        .context("failed to run stateless proof ffmpeg")?;
    if !status.success() {
        return Err(anyhow!("stateless proof ffmpeg exited with {status}"));
    }

    let bytes = fs::read(&args.output_mp4)
        .with_context(|| format!("failed to read {}", args.output_mp4.display()))?;
    let split = split_fmp4_init_and_media(&bytes)?;
    let media_fragment_blake3 = split
        .media
        .iter()
        .map(|fragment| blake3::hash(fragment).to_hex().to_string())
        .collect::<Vec<_>>();
    let report = PublisherProofSegmentReport {
        input_ts: args.input_ts,
        output_mp4: args.output_mp4,
        size_bytes: bytes.len(),
        init_size_bytes: split.init.len(),
        init_blake3: blake3::hash(&split.init).to_hex().to_string(),
        media_fragment_count: split.media.len(),
        media_fragment_blake3,
    };

    if args.pretty {
        println!("{}", serde_json::to_string_pretty(&report)?);
    } else {
        println!("{}", serde_json::to_string(&report)?);
    }
    Ok(())
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct PublisherProofWindowReport {
    chunk_index: u64,
    chunk_start_27mhz: Option<u64>,
    chunk_duration_27mhz: u64,
    sync_status: String,
    source_ts: PathBuf,
    source_ts_blake3: String,
    output_mp4: PathBuf,
    size_bytes: usize,
    init_size_bytes: usize,
    init_blake3: String,
    media_fragment_count: usize,
    media_fragment_blake3: Vec<String>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct PublisherProofWindowsReport {
    input_ts: PathBuf,
    output_dir: PathBuf,
    chunk_ms: u64,
    preroll_packets: usize,
    window_count: usize,
    windows: Vec<PublisherProofWindowReport>,
}

fn publisher_proof_windows_report(
    args: &PublisherProofWindowsArgs,
) -> Result<PublisherProofWindowsReport> {
    if args.chunk_ms == 0 {
        return Err(anyhow!("--chunk-ms must be greater than 0"));
    }

    let source_window_dir = args.output_dir.join("source-windows");
    let proof_dir = args.output_dir.join("proof-mp4");
    fs::create_dir_all(&source_window_dir)
        .with_context(|| format!("failed to create {}", source_window_dir.display()))?;
    fs::create_dir_all(&proof_dir)
        .with_context(|| format!("failed to create {}", proof_dir.display()))?;

    let input = File::open(&args.input_ts)
        .with_context(|| format!("failed to open {}", args.input_ts.display()))?;
    let manifest = ec_chopper::chunk_ts_stream_with_preroll(
        input,
        &source_window_dir,
        args.chunk_ms,
        args.max_chunks,
        args.preroll_packets,
    )?;
    let profile = StatelessProofEncodeProfile::from_publisher_proof_windows_args(args);
    let mut chunks = manifest.chunks;
    chunks.sort_by_key(|chunk| chunk.index);
    let mut windows = Vec::with_capacity(chunks.len());

    for chunk in chunks {
        let output_mp4 = proof_dir.join(format!("proof_{:010}.mp4", chunk.index));
        let status = Command::new("ffmpeg")
            .args(stateless_proof_ffmpeg_args(
                &profile,
                &chunk.path,
                &output_mp4,
            ))
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::inherit())
            .status()
            .with_context(|| {
                format!(
                    "failed to run stateless proof ffmpeg for chunk {}",
                    chunk.index
                )
            })?;
        if !status.success() {
            return Err(anyhow!(
                "stateless proof ffmpeg exited with {status} for chunk {}",
                chunk.index
            ));
        }

        let bytes = fs::read(&output_mp4)
            .with_context(|| format!("failed to read {}", output_mp4.display()))?;
        let split = split_fmp4_init_and_media(&bytes)?;
        let media_fragment_blake3 = split
            .media
            .iter()
            .map(|fragment| blake3::hash(fragment).to_hex().to_string())
            .collect::<Vec<_>>();

        windows.push(PublisherProofWindowReport {
            chunk_index: chunk.index,
            chunk_start_27mhz: chunk.timing.chunk_start_27mhz,
            chunk_duration_27mhz: chunk.timing.chunk_duration_27mhz,
            sync_status: chunk.timing.sync_status,
            source_ts_blake3: ec_chopper::hash_file_blake3(&chunk.path)?,
            source_ts: chunk.path,
            output_mp4,
            size_bytes: bytes.len(),
            init_size_bytes: split.init.len(),
            init_blake3: blake3::hash(&split.init).to_hex().to_string(),
            media_fragment_count: split.media.len(),
            media_fragment_blake3,
        });
    }

    Ok(PublisherProofWindowsReport {
        input_ts: args.input_ts.clone(),
        output_dir: args.output_dir.clone(),
        chunk_ms: args.chunk_ms,
        preroll_packets: args.preroll_packets,
        window_count: windows.len(),
        windows,
    })
}

fn publisher_proof_windows_command(args: PublisherProofWindowsArgs) -> Result<()> {
    let report = publisher_proof_windows_report(&args)?;
    if args.pretty {
        println!("{}", serde_json::to_string_pretty(&report)?);
    } else {
        println!("{}", serde_json::to_string(&report)?);
    }
    Ok(())
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct PublisherProofDuplicatePublisherReport {
    publisher: String,
    report: PublisherProofWindowsReport,
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct PublisherProofDuplicateDivergence {
    chunk_index: u64,
    reasons: Vec<String>,
    missing_publishers: Vec<String>,
    source_ts_blake3: BTreeMap<String, String>,
    init_blake3: BTreeMap<String, String>,
    media_fragment_blake3: BTreeMap<String, Vec<String>>,
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct PublisherProofDuplicatesReport {
    input_ts: PathBuf,
    output_dir: PathBuf,
    chunk_ms: u64,
    preroll_packets: usize,
    publisher_count: usize,
    compared_window_count: usize,
    matching_window_count: usize,
    divergent_window_count: usize,
    ok: bool,
    reasons: Vec<String>,
    publishers: Vec<PublisherProofDuplicatePublisherReport>,
    divergences: Vec<PublisherProofDuplicateDivergence>,
}

fn publisher_proof_duplicate_publishers(
    args: &PublisherProofDuplicatesArgs,
) -> Result<Vec<String>> {
    let publishers = if args.publisher.is_empty() {
        vec!["publisher-a".to_string(), "publisher-b".to_string()]
    } else {
        args.publisher.clone()
    };
    if publishers.len() < 2 {
        return Err(anyhow!(
            "publisher-proof-duplicates requires at least two --publisher values"
        ));
    }

    let mut seen = BTreeSet::new();
    for publisher in &publishers {
        if publisher.trim().is_empty() {
            return Err(anyhow!("publisher identity must not be empty"));
        }
        if publisher == "."
            || publisher == ".."
            || publisher.contains('/')
            || publisher.contains('\\')
        {
            return Err(anyhow!(
                "publisher identity {publisher:?} must be a single safe path component"
            ));
        }
        if !seen.insert(publisher.clone()) {
            return Err(anyhow!("publisher identity {publisher:?} is duplicated"));
        }
    }

    Ok(publishers)
}

fn compare_publisher_proof_duplicate_reports(
    publishers: &[String],
    reports: &[PublisherProofDuplicatePublisherReport],
) -> (usize, usize, Vec<PublisherProofDuplicateDivergence>) {
    let mut chunk_indices = BTreeSet::new();
    let mut windows_by_publisher = Vec::new();

    for publisher_report in reports {
        let mut windows = BTreeMap::new();
        for window in &publisher_report.report.windows {
            chunk_indices.insert(window.chunk_index);
            windows.insert(window.chunk_index, window);
        }
        windows_by_publisher.push((publisher_report.publisher.as_str(), windows));
    }

    let mut matching_window_count = 0usize;
    let mut divergences = Vec::new();

    for chunk_index in chunk_indices {
        let mut reasons = Vec::new();
        let mut missing_publishers = Vec::new();
        let mut source_ts_blake3 = BTreeMap::new();
        let mut init_blake3 = BTreeMap::new();
        let mut media_fragment_blake3 = BTreeMap::new();
        let mut source_values = BTreeSet::new();
        let mut init_values = BTreeSet::new();
        let mut media_values = BTreeSet::new();

        for (publisher, windows) in &windows_by_publisher {
            let Some(window) = windows.get(&chunk_index) else {
                missing_publishers.push((*publisher).to_string());
                continue;
            };
            source_values.insert(window.source_ts_blake3.clone());
            init_values.insert(window.init_blake3.clone());
            media_values.insert(window.media_fragment_blake3.clone());
            source_ts_blake3.insert((*publisher).to_string(), window.source_ts_blake3.clone());
            init_blake3.insert((*publisher).to_string(), window.init_blake3.clone());
            media_fragment_blake3.insert(
                (*publisher).to_string(),
                window.media_fragment_blake3.clone(),
            );
            if window.media_fragment_count == 0 {
                reasons.push(format!("{publisher}:media_fragments_empty"));
            }
        }

        if !missing_publishers.is_empty() {
            reasons.push("missing_window".to_string());
        }
        if source_values.len() > 1 {
            reasons.push("source_ts_hash_mismatch".to_string());
        }
        if init_values.len() > 1 {
            reasons.push("init_hash_mismatch".to_string());
        }
        if media_values.len() > 1 {
            reasons.push("media_fragment_hash_mismatch".to_string());
        }

        if reasons.is_empty() && source_ts_blake3.len() == publishers.len() {
            matching_window_count += 1;
        } else {
            divergences.push(PublisherProofDuplicateDivergence {
                chunk_index,
                reasons,
                missing_publishers,
                source_ts_blake3,
                init_blake3,
                media_fragment_blake3,
            });
        }
    }

    (matching_window_count, divergences.len(), divergences)
}

fn publisher_proof_duplicates_report(
    args: &PublisherProofDuplicatesArgs,
) -> Result<PublisherProofDuplicatesReport> {
    if args.chunk_ms == 0 {
        return Err(anyhow!("--chunk-ms must be greater than 0"));
    }

    let publishers = publisher_proof_duplicate_publishers(args)?;
    let mut publisher_reports = Vec::with_capacity(publishers.len());
    for publisher in &publishers {
        let report = publisher_proof_windows_report(&PublisherProofWindowsArgs {
            input_ts: args.input_ts.clone(),
            output_dir: args.output_dir.join(publisher),
            chunk_ms: args.chunk_ms,
            max_chunks: args.max_chunks,
            preroll_packets: args.preroll_packets,
            transcode: args.transcode,
            video_filter: args.video_filter.clone(),
            gop_frames: args.gop_frames,
            video_preset: args.video_preset.clone(),
            video_crf: args.video_crf,
            movflags: args.movflags.clone(),
            pretty: false,
        })?;
        publisher_reports.push(PublisherProofDuplicatePublisherReport {
            publisher: publisher.clone(),
            report,
        });
    }

    let compared_window_count = publisher_reports
        .iter()
        .flat_map(|publisher_report| {
            publisher_report
                .report
                .windows
                .iter()
                .map(|window| window.chunk_index)
        })
        .collect::<BTreeSet<_>>()
        .len();
    let (matching_window_count, divergent_window_count, divergences) =
        compare_publisher_proof_duplicate_reports(&publishers, &publisher_reports);

    let mut reasons = Vec::new();
    if compared_window_count == 0 {
        reasons.push("no_windows".to_string());
    }
    if divergent_window_count > 0 {
        reasons.push("divergent_windows".to_string());
    }
    if matching_window_count != compared_window_count {
        reasons.push("matching_window_count_incomplete".to_string());
    }

    Ok(PublisherProofDuplicatesReport {
        input_ts: args.input_ts.clone(),
        output_dir: args.output_dir.clone(),
        chunk_ms: args.chunk_ms,
        preroll_packets: args.preroll_packets,
        publisher_count: publishers.len(),
        compared_window_count,
        matching_window_count,
        divergent_window_count,
        ok: reasons.is_empty(),
        reasons,
        publishers: publisher_reports,
        divergences,
    })
}

fn publisher_proof_duplicates_command(args: PublisherProofDuplicatesArgs) -> Result<()> {
    let report = publisher_proof_duplicates_report(&args)?;
    if args.pretty {
        println!("{}", serde_json::to_string_pretty(&report)?);
    } else {
        println!("{}", serde_json::to_string(&report)?);
    }
    if args.require_ok && !report.ok {
        return Err(anyhow!(
            "publisher proof duplicate comparison failed: {}",
            report.reasons.join(",")
        ));
    }
    Ok(())
}

#[derive(Debug, serde::Serialize)]
struct PublisherProofCompareInputSummary {
    publisher: String,
    report_path: PathBuf,
    input_ts: PathBuf,
    output_dir: PathBuf,
    chunk_ms: u64,
    preroll_packets: usize,
    window_count: usize,
}

#[derive(Debug, serde::Serialize)]
struct PublisherProofCompareReport {
    publisher_count: usize,
    compared_window_count: usize,
    matching_window_count: usize,
    divergent_window_count: usize,
    ok: bool,
    reasons: Vec<String>,
    inputs: Vec<PublisherProofCompareInputSummary>,
    publishers: Vec<PublisherProofDuplicatePublisherReport>,
    divergences: Vec<PublisherProofDuplicateDivergence>,
}

fn publisher_proof_compare_report(
    args: &PublisherProofCompareArgs,
) -> Result<PublisherProofCompareReport> {
    if args.report.len() < 2 {
        return Err(anyhow!(
            "publisher-proof-compare requires at least two --report NAME=PATH entries"
        ));
    }

    let mut seen_publishers = BTreeSet::new();
    let mut publisher_reports = Vec::with_capacity(args.report.len());
    let mut inputs = Vec::with_capacity(args.report.len());

    for item in &args.report {
        let (publisher, report_path) = parse_named_proof_report_path(item)?;
        validate_publisher_proof_label(&publisher)?;
        if !seen_publishers.insert(publisher.clone()) {
            return Err(anyhow!(
                "publisher proof report {publisher:?} is duplicated"
            ));
        }

        let bytes = fs::read(&report_path)
            .with_context(|| format!("failed to read proof report {}", report_path.display()))?;
        let report =
            serde_json::from_slice::<PublisherProofWindowsReport>(&bytes).with_context(|| {
                format!(
                    "failed to parse proof report JSON {}",
                    report_path.display()
                )
            })?;
        inputs.push(PublisherProofCompareInputSummary {
            publisher: publisher.clone(),
            report_path,
            input_ts: report.input_ts.clone(),
            output_dir: report.output_dir.clone(),
            chunk_ms: report.chunk_ms,
            preroll_packets: report.preroll_packets,
            window_count: report.window_count,
        });
        publisher_reports.push(PublisherProofDuplicatePublisherReport { publisher, report });
    }

    let publishers = publisher_reports
        .iter()
        .map(|report| report.publisher.clone())
        .collect::<Vec<_>>();
    let compared_window_count = publisher_reports
        .iter()
        .flat_map(|publisher_report| {
            publisher_report
                .report
                .windows
                .iter()
                .map(|window| window.chunk_index)
        })
        .collect::<BTreeSet<_>>()
        .len();
    let (matching_window_count, divergent_window_count, divergences) =
        compare_publisher_proof_duplicate_reports(&publishers, &publisher_reports);

    let mut reasons = Vec::new();
    if compared_window_count == 0 {
        reasons.push("no_windows".to_string());
    }
    if inputs
        .iter()
        .map(|input| input.chunk_ms)
        .collect::<BTreeSet<_>>()
        .len()
        > 1
    {
        reasons.push("chunk_ms_mismatch".to_string());
    }
    if inputs
        .iter()
        .map(|input| input.preroll_packets)
        .collect::<BTreeSet<_>>()
        .len()
        > 1
    {
        reasons.push("preroll_packets_mismatch".to_string());
    }
    if divergent_window_count > 0 {
        reasons.push("divergent_windows".to_string());
    }
    if matching_window_count != compared_window_count {
        reasons.push("matching_window_count_incomplete".to_string());
    }

    Ok(PublisherProofCompareReport {
        publisher_count: publishers.len(),
        compared_window_count,
        matching_window_count,
        divergent_window_count,
        ok: reasons.is_empty(),
        reasons,
        inputs,
        publishers: publisher_reports,
        divergences,
    })
}

fn publisher_proof_compare_command(args: PublisherProofCompareArgs) -> Result<()> {
    let report = publisher_proof_compare_report(&args)?;
    if args.pretty {
        println!("{}", serde_json::to_string_pretty(&report)?);
    } else {
        println!("{}", serde_json::to_string(&report)?);
    }
    if args.require_ok && !report.ok {
        return Err(anyhow!(
            "publisher proof report comparison failed: {}",
            report.reasons.join(",")
        ));
    }
    Ok(())
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
struct PublisherProofRemoteTarget {
    publisher: String,
    host: String,
}

#[derive(Debug, serde::Serialize)]
struct PublisherProofRemoteExecutionReport {
    publisher: String,
    host: String,
    remote_root: String,
    remote_input_ts: String,
    remote_output_dir: String,
    local_report_path: PathBuf,
    upload_elapsed_ms: u64,
    proof_elapsed_ms: u64,
    cleanup_elapsed_ms: Option<u64>,
}

#[derive(Debug, serde::Serialize)]
struct PublisherProofRemoteCompareReport {
    input_ts: PathBuf,
    input_ts_blake3: String,
    output_dir: PathBuf,
    remote_count: usize,
    chunk_ms: u64,
    preroll_packets: usize,
    remote_root: String,
    ok: bool,
    reasons: Vec<String>,
    remotes: Vec<PublisherProofRemoteExecutionReport>,
    compare: PublisherProofCompareReport,
}

fn publisher_proof_remote_compare_command(args: PublisherProofRemoteCompareArgs) -> Result<()> {
    let report = publisher_proof_remote_compare_report(&args)?;
    if args.pretty {
        println!("{}", serde_json::to_string_pretty(&report)?);
    } else {
        println!("{}", serde_json::to_string(&report)?);
    }
    if args.require_ok && !report.ok {
        return Err(anyhow!(
            "remote publisher proof comparison failed: {}",
            report.reasons.join(",")
        ));
    }
    Ok(())
}

fn publisher_proof_remote_compare_report(
    args: &PublisherProofRemoteCompareArgs,
) -> Result<PublisherProofRemoteCompareReport> {
    if args.chunk_ms == 0 {
        return Err(anyhow!("--chunk-ms must be greater than 0"));
    }
    if args.remote.len() < 2 {
        return Err(anyhow!(
            "publisher-proof-remote-compare requires at least two --remote NAME=HOST targets"
        ));
    }
    validate_remote_shell_component("remote ec-node", &args.remote_ec_node)?;
    validate_remote_command_arg("video filter", &args.video_filter)?;
    validate_remote_command_arg("video preset", &args.video_preset)?;
    validate_remote_command_arg("movflags", &args.movflags)?;
    for option in &args.ssh_option {
        validate_ssh_option(option)?;
    }

    let targets = parse_publisher_proof_remote_targets(&args.remote)?;
    let remote_root = publisher_proof_remote_root(args)?;
    validate_remote_absolute_tmp_path("remote root", &remote_root)?;

    let reports_dir = args.output_dir.join("reports");
    fs::create_dir_all(&reports_dir)
        .with_context(|| format!("failed to create {}", reports_dir.display()))?;
    let input_ts_blake3 = ec_chopper::hash_file_blake3(&args.input_ts)?;
    let mut executions = Vec::with_capacity(targets.len());

    for target in &targets {
        let execution = run_publisher_proof_remote_target(args, target, &remote_root, &reports_dir)
            .with_context(|| {
                format!(
                    "failed to run remote publisher proof for {} on {}",
                    target.publisher, target.host
                )
            })?;
        executions.push(execution);
    }

    let compare_args = PublisherProofCompareArgs {
        report: executions
            .iter()
            .map(|execution| {
                format!(
                    "{}={}",
                    execution.publisher,
                    execution.local_report_path.display()
                )
            })
            .collect(),
        pretty: false,
        require_ok: false,
    };
    let compare = publisher_proof_compare_report(&compare_args)?;
    let compare_path = args.output_dir.join("compare.json");
    fs::write(&compare_path, serde_json::to_vec_pretty(&compare)?)
        .with_context(|| format!("failed to write {}", compare_path.display()))?;

    let mut reasons = compare.reasons.clone();
    if !compare.ok && reasons.is_empty() {
        reasons.push("remote_compare_failed".to_string());
    }

    Ok(PublisherProofRemoteCompareReport {
        input_ts: args.input_ts.clone(),
        input_ts_blake3,
        output_dir: args.output_dir.clone(),
        remote_count: targets.len(),
        chunk_ms: args.chunk_ms,
        preroll_packets: args.preroll_packets,
        remote_root,
        ok: compare.ok,
        reasons,
        remotes: executions,
        compare,
    })
}

fn run_publisher_proof_remote_target(
    args: &PublisherProofRemoteCompareArgs,
    target: &PublisherProofRemoteTarget,
    remote_root: &str,
    reports_dir: &Path,
) -> Result<PublisherProofRemoteExecutionReport> {
    let remote_dir = format!("{}/{}", remote_root.trim_end_matches('/'), target.publisher);
    let remote_input_ts = format!("{remote_dir}/source.ts");
    let remote_output_dir = format!("{remote_dir}/proof");
    validate_remote_absolute_tmp_path("remote directory", &remote_dir)?;
    validate_remote_absolute_tmp_path("remote input", &remote_input_ts)?;
    validate_remote_absolute_tmp_path("remote output", &remote_output_dir)?;

    let upload_started = Instant::now();
    run_ssh_status(
        &args.ssh_option,
        &target.host,
        &["mkdir", "-p", &remote_dir],
    )?;
    run_scp_status(
        &args.ssh_option,
        &args.input_ts,
        &target.host,
        &remote_input_ts,
    )?;
    let upload_elapsed_ms = elapsed_ms(upload_started.elapsed());

    let proof_started = Instant::now();
    let mut remote_command = publisher_proof_remote_command_args(
        args,
        &args.remote_ec_node,
        &remote_input_ts,
        &remote_output_dir,
    );
    let output = run_ssh_output(&args.ssh_option, &target.host, &remote_command)?;
    let proof_elapsed_ms = elapsed_ms(proof_started.elapsed());
    remote_command.clear();

    let local_report_path = reports_dir.join(format!("{}.json", target.publisher));
    fs::write(&local_report_path, output.stdout)
        .with_context(|| format!("failed to write {}", local_report_path.display()))?;

    let cleanup_elapsed_ms = if args.cleanup_remote {
        let cleanup_started = Instant::now();
        run_ssh_status(&args.ssh_option, &target.host, &["rm", "-rf", &remote_dir])?;
        Some(elapsed_ms(cleanup_started.elapsed()))
    } else {
        None
    };

    Ok(PublisherProofRemoteExecutionReport {
        publisher: target.publisher.clone(),
        host: target.host.clone(),
        remote_root: remote_root.to_string(),
        remote_input_ts,
        remote_output_dir,
        local_report_path,
        upload_elapsed_ms,
        proof_elapsed_ms,
        cleanup_elapsed_ms,
    })
}

fn publisher_proof_remote_command_args(
    args: &PublisherProofRemoteCompareArgs,
    remote_ec_node: &str,
    remote_input_ts: &str,
    remote_output_dir: &str,
) -> Vec<String> {
    let mut command = vec![
        remote_ec_node.to_string(),
        "publisher-proof-windows".to_string(),
        "--input-ts".to_string(),
        remote_input_ts.to_string(),
        "--output-dir".to_string(),
        remote_output_dir.to_string(),
        "--chunk-ms".to_string(),
        args.chunk_ms.to_string(),
        "--preroll-packets".to_string(),
        args.preroll_packets.to_string(),
        "--transcode".to_string(),
        args.transcode.to_string(),
        "--video-filter".to_string(),
        args.video_filter.clone(),
        "--gop-frames".to_string(),
        args.gop_frames.to_string(),
        "--video-preset".to_string(),
        args.video_preset.clone(),
        "--video-crf".to_string(),
        args.video_crf.to_string(),
        "--movflags".to_string(),
        args.movflags.clone(),
    ];
    if let Some(max_chunks) = args.max_chunks {
        command.push("--max-chunks".to_string());
        command.push(max_chunks.to_string());
    }
    command
}

fn parse_publisher_proof_remote_targets(
    values: &[String],
) -> Result<Vec<PublisherProofRemoteTarget>> {
    let mut seen = BTreeSet::new();
    let mut targets = Vec::with_capacity(values.len());
    for value in values {
        let (publisher, host) = value
            .split_once('=')
            .ok_or_else(|| anyhow!("expected NAME=HOST for --remote, got {value:?}"))?;
        let publisher = publisher.trim();
        let host = host.trim();
        validate_publisher_proof_label(publisher)?;
        validate_remote_target_host(host)?;
        if !seen.insert(publisher.to_string()) {
            return Err(anyhow!("remote publisher {publisher:?} is duplicated"));
        }
        targets.push(PublisherProofRemoteTarget {
            publisher: publisher.to_string(),
            host: host.to_string(),
        });
    }
    Ok(targets)
}

fn publisher_proof_remote_root(args: &PublisherProofRemoteCompareArgs) -> Result<String> {
    if let Some(root) = &args.remote_root {
        return Ok(root.trim_end_matches('/').to_string());
    }
    let now_ms = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis();
    Ok(format!(
        "/tmp/every-channel-publisher-proof-{}-{now_ms}",
        std::process::id()
    ))
}

fn validate_remote_target_host(host: &str) -> Result<()> {
    if host.is_empty() {
        return Err(anyhow!("remote host must not be empty"));
    }
    if !host.chars().all(|ch| {
        ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '_' | '@' | ':' | '[' | ']')
    }) {
        return Err(anyhow!("remote host {host:?} has unsupported characters"));
    }
    Ok(())
}

fn validate_remote_shell_component(name: &str, value: &str) -> Result<()> {
    if value.trim().is_empty() {
        return Err(anyhow!("{name} must not be empty"));
    }
    if !value.chars().all(|ch| {
        ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '_' | '/' | ':' | '+' | '=')
    }) {
        return Err(anyhow!("{name} {value:?} has unsupported characters"));
    }
    Ok(())
}

fn validate_remote_command_arg(name: &str, value: &str) -> Result<()> {
    if value.trim().is_empty() {
        return Err(anyhow!("{name} must not be empty"));
    }
    if value.chars().any(|ch| {
        ch.is_whitespace()
            || matches!(
                ch,
                '\'' | '"' | '`' | '$' | ';' | '&' | '|' | '<' | '>' | '\\' | '(' | ')' | '{' | '}'
            )
    }) {
        return Err(anyhow!("{name} {value:?} has unsupported shell characters"));
    }
    Ok(())
}

fn validate_ssh_option(value: &str) -> Result<()> {
    if value.trim().is_empty() {
        return Err(anyhow!("ssh option must not be empty"));
    }
    if value.contains(char::is_whitespace) {
        return Err(anyhow!("ssh option {value:?} must not contain whitespace"));
    }
    Ok(())
}

fn validate_remote_absolute_tmp_path(name: &str, value: &str) -> Result<()> {
    validate_remote_shell_component(name, value)?;
    if !value.starts_with("/tmp/every-channel-") {
        return Err(anyhow!(
            "{name} must live under /tmp/every-channel-* for remote proof safety"
        ));
    }
    Ok(())
}

fn run_ssh_status(ssh_options: &[String], host: &str, remote_args: &[&str]) -> Result<()> {
    let mut command = Command::new("ssh");
    push_ssh_options(&mut command, ssh_options);
    command.arg(host);
    for arg in remote_args {
        command.arg(arg);
    }
    let output = command
        .stdin(Stdio::null())
        .output()
        .with_context(|| format!("failed to run ssh command on {host}"))?;
    ensure_command_success("ssh", host, &output)
}

fn run_ssh_output(
    ssh_options: &[String],
    host: &str,
    remote_args: &[String],
) -> Result<std::process::Output> {
    let mut command = Command::new("ssh");
    push_ssh_options(&mut command, ssh_options);
    command.arg(host);
    for arg in remote_args {
        command.arg(arg);
    }
    let output = command
        .stdin(Stdio::null())
        .output()
        .with_context(|| format!("failed to run ssh command on {host}"))?;
    ensure_command_success("ssh", host, &output)?;
    Ok(output)
}

fn run_scp_status(
    ssh_options: &[String],
    source: &Path,
    host: &str,
    remote_path: &str,
) -> Result<()> {
    let mut command = Command::new("scp");
    push_ssh_options(&mut command, ssh_options);
    command.arg(source);
    command.arg(format!("{host}:{remote_path}"));
    let output = command
        .stdin(Stdio::null())
        .output()
        .with_context(|| format!("failed to copy proof input to {host}"))?;
    ensure_command_success("scp", host, &output)
}

fn push_ssh_options(command: &mut Command, ssh_options: &[String]) {
    for option in ssh_options {
        command.arg("-o").arg(option);
    }
}

fn ensure_command_success(command: &str, host: &str, output: &std::process::Output) -> Result<()> {
    if output.status.success() {
        return Ok(());
    }
    let stderr = String::from_utf8_lossy(&output.stderr);
    let stdout = String::from_utf8_lossy(&output.stdout);
    Err(anyhow!(
        "{command} on {host} exited with {}: stdout={} stderr={}",
        output.status,
        stdout.trim(),
        stderr.trim()
    ))
}

fn elapsed_ms(duration: Duration) -> u64 {
    duration.as_millis().try_into().unwrap_or(u64::MAX)
}

fn parse_named_proof_report_path(value: &str) -> Result<(String, PathBuf)> {
    let (name, path) = value
        .split_once('=')
        .ok_or_else(|| anyhow!("expected NAME=PATH for --report, got {value:?}"))?;
    let name = name.trim();
    let path = path.trim();
    if name.is_empty() || path.is_empty() {
        return Err(anyhow!("expected NAME=PATH for --report, got {value:?}"));
    }
    Ok((name.to_string(), PathBuf::from(path)))
}

fn validate_publisher_proof_label(label: &str) -> Result<()> {
    if label.trim().is_empty() {
        return Err(anyhow!("publisher proof label must not be empty"));
    }
    if label == "." || label == ".." || label.contains('/') || label.contains('\\') {
        return Err(anyhow!(
            "publisher proof label {label:?} must be a single safe path component"
        ));
    }
    Ok(())
}

async fn wt_publish(args: WtPublishArgs) -> Result<()> {
    if args.publisher_archive_segment_duration_ms == 0 {
        return Err(anyhow!(
            "--publisher-archive-segment-duration-ms must be greater than 0"
        ));
    }
    let publish_input = prepare_wt_publish_input(&args).await?;
    let mut publisher_archive_child =
        spawn_publisher_proof_archive_source_child(&args, &publish_input)?;
    let mut publisher_archive = None;

    let mut relay = open_wt_publish_relay(&WtPublishRelayArgs {
        url: args.url.clone(),
        name: args.name.clone(),
        tls_disable_verify: args.tls_disable_verify,
        control_announce: args.control_announce,
        control_ttl_ms: args.control_ttl_ms,
        control_interval_ms: args.control_interval_ms,
        iroh_secret: args.iroh_secret.clone(),
        discovery: args.discovery.clone(),
        gossip_peer: args.gossip_peer.clone(),
        control_endpoint_addr_out: args.control_endpoint_addr_out.clone(),
    })
    .await?;

    // Spawn ffmpeg to generate fMP4 suitable for hang/moq-mux.
    wait_for_publisher_start_boundary(args.publisher_start_boundary_ms).await;
    let mut cmd = TokioCommand::new("ffmpeg");
    cmd.args(wt_publish_ffmpeg_args(&args, &publish_input));
    cmd.stdout(Stdio::piped());
    cmd.stderr(Stdio::inherit());

    tracing::info!(input=%publish_input, "spawning ffmpeg");
    let mut child = cmd.spawn().context("failed to spawn ffmpeg")?;
    let stdout = child
        .stdout
        .take()
        .ok_or_else(|| anyhow!("ffmpeg stdout unavailable"))?;

    if !args.passthrough {
        tracing::debug!(
            "moq-mux 0.4 fMP4 import preserves CMAF fragments; --passthrough=false is accepted for CLI compatibility"
        );
    }
    let mut importer = moq_mux::import::Fmp4::new(relay.broadcast, relay.catalog);

    let mut stdout = stdout;
    let replay_prefix = prepare_fmp4_import_reader(&mut stdout, &mut relay.init_track).await?;
    let replay = std::io::Cursor::new(replay_prefix);
    let mut import_reader = tokio::io::AsyncReadExt::chain(replay, stdout);
    let decode_fut = import_fmp4_with_publisher_archive(
        &mut importer,
        &mut import_reader,
        &mut publisher_archive,
    );
    tokio::pin!(decode_fut);
    let announced_watchdog_url = relay_announced_url(&args.url)?;
    let announced_watchdog_fut = relay_announced_watchdog(
        announced_watchdog_url,
        args.name.clone(),
        Duration::from_millis(args.relay_announced_watchdog_ms),
        Duration::from_millis(args.relay_announced_watchdog_interval_ms.max(1000)),
        args.tls_disable_verify,
    );
    tokio::pin!(announced_watchdog_fut);

    tracing::info!("publishing fMP4 -> moq-mux -> relay");
    let outcome = tokio::select! {
        res = &mut decode_fut => {
            let status = child.wait().await.context("failed to wait for ffmpeg")?;
            match res {
                Ok(()) if status.success() => Ok(()),
                Ok(()) => Err(anyhow!("ffmpeg exited with {status}")),
                Err(err) => Err(err).context("fmp4 ingest failed"),
            }
        }
        _ = relay.session.closed() => {
            let _ = child.kill().await;
            Err(anyhow!("relay session closed"))
        }
        res = &mut announced_watchdog_fut, if args.relay_announced_watchdog_ms > 0 => {
            let _ = child.kill().await;
            match res {
                Ok(()) => Err(anyhow!("relay announcement watchdog exited unexpectedly")),
                Err(err) => Err(err).context("relay announcement watchdog failed"),
            }
        }
        res = async {
            if let Some(child) = publisher_archive_child.as_mut() {
                child
                    .wait()
                    .await
                    .context("failed to wait for publisher source proof archive worker")
            } else {
                std::future::pending::<Result<std::process::ExitStatus>>().await
            }
        }, if publisher_archive_child.is_some() => {
            let _ = child.kill().await;
            match res {
                Ok(status) => Err(anyhow!("publisher source proof archive worker exited with {status}")),
                Err(err) => Err(err),
            }
        }
        _ = tokio::signal::ctrl_c() => {
            tracing::info!("ctrl-c; shutting down");
            let _ = child.kill().await;
            tokio::time::sleep(Duration::from_millis(100)).await;
            Ok(())
        }
    };

    if let Some(child) = publisher_archive_child.as_mut() {
        let _ = child.kill().await;
    }

    if let Some(stop) = relay.control_stop.take() {
        let _ = stop.send(());
    }

    outcome
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct NtscRsPreprocessPlan {
    cli: PathBuf,
    args: Vec<OsString>,
    output: PathBuf,
}

fn ntsc_rs_preprocess_plan(args: &WtPublishArgs) -> Result<Option<NtscRsPreprocessPlan>> {
    let Some(preset) = args.ntsc_rs_preset.as_ref() else {
        return Ok(None);
    };
    if args.input_format.is_some() {
        return Err(anyhow!(
            "--ntsc-rs-preset cannot be combined with --input-format; ntsc-rs writes a normal output file for ffmpeg"
        ));
    }
    let output = args.ntsc_rs_output.as_ref().ok_or_else(|| {
        anyhow!(
            "--ntsc-rs-preset requires --ntsc-rs-output so the preprocessed file path is explicit"
        )
    })?;
    if output.as_os_str().is_empty() {
        return Err(anyhow!("--ntsc-rs-output must not be empty"));
    }
    let output_lossy = output.to_string_lossy();
    if args.input == output_lossy.as_ref() {
        return Err(anyhow!("--ntsc-rs-output must not overwrite --input"));
    }

    Ok(Some(NtscRsPreprocessPlan {
        cli: args.ntsc_rs_cli.clone(),
        args: vec![
            OsString::from("-i"),
            OsString::from(args.input.as_str()),
            OsString::from("-o"),
            output.as_os_str().to_os_string(),
            OsString::from("-p"),
            preset.as_os_str().to_os_string(),
            OsString::from("--overwrite"),
        ],
        output: output.clone(),
    }))
}

async fn prepare_wt_publish_input(args: &WtPublishArgs) -> Result<String> {
    let Some(plan) = ntsc_rs_preprocess_plan(args)? else {
        return Ok(args.input.clone());
    };

    if let Some(parent) = plan.output.parent() {
        if !parent.as_os_str().is_empty() {
            fs::create_dir_all(parent).with_context(|| {
                format!(
                    "failed to create ntsc-rs output directory {}",
                    parent.display()
                )
            })?;
        }
    }

    tracing::info!(
        cli=%plan.cli.display(),
        output=%plan.output.display(),
        "running ntsc-rs preprocessor"
    );
    let status = TokioCommand::new(&plan.cli)
        .args(&plan.args)
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status()
        .await
        .with_context(|| format!("failed to run ntsc-rs CLI {}", plan.cli.display()))?;
    if !status.success() {
        return Err(anyhow!("ntsc-rs CLI exited with {status}"));
    }

    Ok(plan.output.to_string_lossy().into_owned())
}

fn nbc_bootstrap(args: NbcBootstrapArgs) -> Result<()> {
    let chrome_path = resolve_nbc_chrome_path(args.chrome_path.as_deref())?;
    let profile_dir = resolve_nbc_profile_dir(args.profile_dir.as_deref())?;
    let result = bootstrap_nbc_auth(
        chrome_path,
        profile_dir,
        args.source_url,
        args.screenshot_out,
    )?;
    println!(
        "{}",
        serde_json::to_string_pretty(&result).context("failed to encode bootstrap result")?
    );
    Ok(())
}

async fn nbc_wt_publish(args: NbcWtPublishArgs) -> Result<()> {
    let chrome_path = resolve_nbc_chrome_path(args.chrome_path.as_deref())?;
    let profile_dir = resolve_nbc_profile_dir(args.profile_dir.as_deref())?;
    let mut relay = open_wt_publish_relay(&WtPublishRelayArgs {
        url: args.url.clone(),
        name: args.name.clone(),
        tls_disable_verify: args.tls_disable_verify,
        control_announce: args.control_announce,
        control_ttl_ms: args.control_ttl_ms,
        control_interval_ms: args.control_interval_ms,
        iroh_secret: args.iroh_secret.clone(),
        discovery: args.discovery.clone(),
        gossip_peer: args.gossip_peer.clone(),
        control_endpoint_addr_out: args.control_endpoint_addr_out.clone(),
    })
    .await?;

    let mut frame_reader = spawn_nbc_frame_reader(
        chrome_path.clone(),
        profile_dir.clone(),
        args.source_url.clone(),
    )
    .with_context(|| format!("failed to open NBC browser session for {}", args.source_url))?;

    let fps = nbc_capture_fps().max(1);

    let mut cmd = TokioCommand::new("ffmpeg");
    cmd.arg("-hide_banner")
        .arg("-loglevel")
        .arg("error")
        .arg("-nostats")
        .arg("-fflags")
        .arg("+nobuffer")
        .arg("-flags")
        .arg("low_delay")
        .arg("-f")
        .arg("mjpeg")
        .arg("-framerate")
        .arg(fps.to_string())
        .arg("-i")
        .arg("pipe:0")
        .args([
            "-map",
            "0:v:0",
            "-an",
            "-c:v",
            "libx264",
            "-preset",
            "veryfast",
            "-tune",
            "zerolatency",
            "-pix_fmt",
            "yuv420p",
            "-profile:v",
            "main",
            "-g",
        ])
        .arg(args.gop_frames.to_string())
        .arg("-keyint_min")
        .arg(args.gop_frames.to_string())
        .args([
            "-sc_threshold",
            "0",
            "-bf",
            "0",
            "-threads",
            "1",
            "-f",
            "mp4",
            "-movflags",
            args.movflags.as_str(),
            "pipe:1",
        ]);

    cmd.stdin(Stdio::piped());
    cmd.stdout(Stdio::piped());
    cmd.stderr(Stdio::inherit());

    tracing::info!(
        source_url = %args.source_url,
        chrome_path = %chrome_path.display(),
        profile_dir = %profile_dir.display(),
        "spawning ffmpeg for NBC browser capture"
    );
    let mut child = cmd.spawn().context("failed to spawn ffmpeg")?;
    let stdin = child
        .stdin
        .take()
        .ok_or_else(|| anyhow!("ffmpeg stdin unavailable"))?;
    let writer = tokio::task::spawn_blocking(move || -> Result<()> {
        let mut stdin = SyncIoBridge::new(stdin);
        std::io::copy(&mut frame_reader, &mut stdin)
            .context("failed to pipe NBC JPEG frames into ffmpeg")?;
        Ok(())
    });
    let stdout = child
        .stdout
        .take()
        .ok_or_else(|| anyhow!("ffmpeg stdout unavailable"))?;

    if !args.passthrough {
        tracing::debug!(
            "moq-mux 0.4 fMP4 import preserves CMAF fragments; --passthrough=false is accepted for CLI compatibility"
        );
    }
    let mut importer = moq_mux::import::Fmp4::new(relay.broadcast, relay.catalog);

    let mut stdout = stdout;
    let replay_prefix = prepare_fmp4_import_reader(&mut stdout, &mut relay.init_track).await?;
    let replay = std::io::Cursor::new(replay_prefix);
    let mut import_reader = tokio::io::AsyncReadExt::chain(replay, stdout);
    let decode_fut = importer.decode_from(&mut import_reader);
    tokio::pin!(decode_fut);

    tracing::info!("publishing NBC browser capture -> fMP4 -> moq-mux -> relay");
    let outcome = tokio::select! {
        res = &mut decode_fut => {
            let status = child.wait().await.context("failed to wait for ffmpeg")?;
            match writer.await {
                Ok(Ok(())) => {}
                Ok(Err(err)) if !status.success() => {
                    tracing::debug!("NBC frame writer ended after ffmpeg exit: {err:#}");
                }
                Ok(Err(err)) => return Err(err).context("NBC frame writer failed"),
                Err(err) => return Err(anyhow!("NBC frame writer task failed: {err}")),
            }
            match res {
                Ok(()) if status.success() => Ok(()),
                Ok(()) => Err(anyhow!("ffmpeg exited with {status}")),
                Err(err) => Err(err).context("fmp4 ingest failed"),
            }
        }
        _ = relay.session.closed() => {
            let _ = child.kill().await;
            let _ = writer.await;
            Err(anyhow!("relay session closed"))
        }
        _ = tokio::signal::ctrl_c() => {
            tracing::info!("ctrl-c; shutting down");
            let _ = child.kill().await;
            let _ = writer.await;
            tokio::time::sleep(Duration::from_millis(100)).await;
            Ok(())
        }
    };

    if let Some(stop) = relay.control_stop.take() {
        let _ = stop.send(());
    }

    outcome
}
