"""Lab034: frame-aligned FEC and deliberate stale-video dropping."""

from __future__ import annotations

import csv
from dataclasses import asdict, dataclass, replace
from math import ceil
from pathlib import Path
import subprocess
from typing import Iterable

import cv2
import matplotlib
import numpy as np

matplotlib.use("Agg")
import matplotlib.pyplot as plt

from protocol.link_packet import (
    Direction,
    HEADER_SIZE,
    LinkPacket,
    TrafficClass,
    decode_link_packet,
)
from protocol.packet_erasure_fec import (
    decode_fec_block,
    decode_outer_symbol,
    encode_fec_block,
)
from protocol.video_age_policy import (
    AgePolicyPacket,
    AgeScheduleResult,
    schedule_with_video_age,
)
from protocol.video_packet import CompositeReassembler, decode_packet as decode_inner_packet
from tests.lab028_video_packetization import COMPOSITE_FPS
from tests.lab029_packet_channel_simulation import prepare_profiles
from tests.lab030_packet_erasure_fec import SourcePacket, prepare_source_packets
from tests.lab033_priority_channel_scheduler import (
    CHANNEL_RATES_KBPS,
    STREAM_VIDEO,
    VIDEO_PAYLOAD_SIZE,
    Workload as Lab033Workload,
    build_workload as build_lab033_workload,
    simulate as simulate_lab033,
)


OUTPUT_DIRECTORY = Path("data/processed/lab034")
SUMMARY_CSV_PATH = OUTPUT_DIRECTORY / "lab034_summary.csv"
VIDEO_CSV_PATH = OUTPUT_DIRECTORY / "lab034_video_metrics.csv"
CONTROL_CSV_PATH = OUTPUT_DIRECTORY / "lab034_control_metrics.csv"
REPORT_PATH = OUTPUT_DIRECTORY / "lab034_report.txt"
IMAGE_AGE_PLOT_PATH = OUTPUT_DIRECTORY / "lab034_image_age.png"
FRAME_OUTCOME_PLOT_PATH = OUTPUT_DIRECTORY / "lab034_frame_outcomes.png"
QUEUE_PLOT_PATH = OUTPUT_DIRECTORY / "lab034_queue_size.png"
CONTROL_DELAY_PLOT_PATH = OUTPUT_DIRECTORY / "lab034_control_delay.png"
UPDATE_RATE_PLOT_PATH = OUTPUT_DIRECTORY / "lab034_update_rate.png"
FEC_OVERHEAD_PLOT_PATH = OUTPUT_DIRECTORY / "lab034_fec_alignment_overhead.png"
COMPARISON_PLOT_PATH = OUTPUT_DIRECTORY / "lab034_policy_comparison.png"
PLOT_PATHS = (
    IMAGE_AGE_PLOT_PATH,
    FRAME_OUTCOME_PLOT_PATH,
    QUEUE_PLOT_PATH,
    CONTROL_DELAY_PLOT_PATH,
    UPDATE_RATE_PLOT_PATH,
    FEC_OVERHEAD_PLOT_PATH,
    COMPARISON_PLOT_PATH,
)
LAB033_COMMIT = "21751f41dd0af022fc1f935c67e795e3ae4257c5"
SOURCE_BLOCK_SIZE = 12
NOMINAL_PARITY_COUNT = 3
TIME_EPSILON_SECONDS = 1e-9


@dataclass(frozen=True)
class PolicyDefinition:
    name: str
    label: str
    layout: str
    max_age_ms: int | None


POLICIES = (
    PolicyDefinition("continuous_no_drop", "Непрерывные, без отбрасывания", "continuous", None),
    PolicyDefinition("aligned_no_drop", "По кадрам, без отбрасывания", "aligned", None),
    PolicyDefinition("aligned_1500ms", "По кадрам, 1500 мс", "aligned", 1500),
    PolicyDefinition("aligned_1000ms", "По кадрам, 1000 мс", "aligned", 1000),
    PolicyDefinition("aligned_500ms", "По кадрам, 500 мс", "aligned", 500),
)
POLICY_BY_NAME = {policy.name: policy for policy in POLICIES}


@dataclass(frozen=True)
class BlockDescription:
    block_id: int
    frame_ids: tuple[int, ...]
    source_count: int
    parity_count: int
    symbol_size: int


@dataclass(frozen=True)
class FECLayoutMetrics:
    layout: str
    source_packets: int
    parity_packets: int
    fec_blocks: int
    partial_blocks: int
    cross_frame_blocks: int
    mean_frames_per_block: float
    max_frames_per_block: int
    before_link_kbps: float
    after_link_kbps: float
    alignment_extra_kbps: float
    alignment_extra_percent: float
    mean_packets_per_frame: float
    mean_frame_transmission_ms_at_300kbps: float


@dataclass(frozen=True)
class AlignedWorkload:
    lab033: Lab033Workload
    packets: tuple[AgePolicyPacket, ...]
    blocks: tuple[BlockDescription, ...]
    layout_metrics: FECLayoutMetrics


@dataclass(frozen=True)
class Lab034Summary:
    channel_kbps: float
    policy: str
    layout: str
    max_video_age_ms: int
    offered_load_kbps: float
    offered_to_capacity_ratio: float
    transmitted_packets: int
    transmitted_bytes: int
    dropped_packets: int
    dropped_bytes: int
    wasted_transmitted_bytes: int
    mean_queue_packets: float
    max_queue_packets: int
    mean_queue_bytes: float
    max_queue_bytes: int
    queue_at_source_end_packets: int
    additional_drain_seconds: float
    channel_utilization_fraction: float


@dataclass(frozen=True)
class VideoMetrics:
    channel_kbps: float
    policy: str
    created_frames: int
    published_frames: int
    intentionally_dropped_frames: int
    partially_transmitted_cancelled_frames: int
    published_after_deadline_frames: int
    published_fraction: float
    actual_update_fps: float
    mean_publication_delay_ms: float
    p95_publication_delay_ms: float
    max_publication_delay_ms: float
    mean_display_age_ms: float
    p95_display_age_ms: float
    max_display_age_ms: float
    display_age_over_500ms_fraction: float
    display_age_over_1000ms_fraction: float
    mean_no_update_duration_ms: float
    p95_no_update_duration_ms: float
    max_no_update_duration_ms: float
    mean_missing_run_frames: float
    p95_missing_run_frames: float
    max_missing_run_frames: int
    delivered_useful_video_kbps: float
    wasted_transmitted_video_bytes: int
    receiver_cleared_frames: int


@dataclass(frozen=True)
class ControlMetrics:
    channel_kbps: float
    policy: str
    control_mean_age_ms: float
    control_p95_age_ms: float
    control_max_age_ms: float
    control_deadline_misses: int
    control_max_receive_gap_ms: float
    control_replaced: int
    emergency_total_delay_ms: float
    emergency_deadline_met: bool
    emergency_blocker_class: str
    emergency_blocker_size_bytes: int
    emergency_blocker_sequence: int
    emergency_blocking_delay_ms: float
    telemetry_mean_age_ms: float
    telemetry_p95_age_ms: float
    telemetry_max_age_ms: float
    telemetry_deadline_misses: int
    telemetry_replaced: int


@dataclass(frozen=True)
class Lab034Result:
    summary: Lab034Summary
    video: VideoMetrics
    control: ControlMetrics
    publication_times: dict[int, float]
    dropped_frames: frozenset[int]
    transmitted_video_by_frame: dict[int, int]
    schedule: AgeScheduleResult | None


@dataclass(frozen=True)
class FunctionalTestResult:
    name: str
    passed: bool
    detail: str


def percentile(values: Iterable[float], q: float) -> float:
    values = tuple(values)
    return float(np.percentile(values, q)) if values else 0.0


def parity_for_partial(source_count: int) -> int:
    if not 1 <= source_count <= SOURCE_BLOCK_SIZE:
        raise ValueError("partial source count is outside 1...12")
    return max(1, ceil(source_count * NOMINAL_PARITY_COUNT / SOURCE_BLOCK_SIZE))


def _aligned_video_packets(
    lab033: Lab033Workload,
) -> tuple[tuple[AgePolicyPacket, ...], tuple[BlockDescription, ...]]:
    profile = prepare_profiles(list(lab033.composites))[VIDEO_PAYLOAD_SIZE]
    source_packets: tuple[SourcePacket, ...] = prepare_source_packets(profile)
    by_frame: dict[int, list[SourcePacket]] = {}
    for source in source_packets:
        by_frame.setdefault(source.composite_frame_id, []).append(source)
    packets: list[AgePolicyPacket] = []
    blocks: list[BlockDescription] = []
    sequence = 0
    block_id = 0
    for frame_id in sorted(by_frame):
        frame_sources = by_frame[frame_id]
        generation_us = int(round(frame_id / COMPOSITE_FPS * 1_000_000.0))
        for start in range(0, len(frame_sources), SOURCE_BLOCK_SIZE):
            chunk = frame_sources[start:start + SOURCE_BLOCK_SIZE]
            parity_count = (
                NOMINAL_PARITY_COUNT
                if len(chunk) == SOURCE_BLOCK_SIZE
                else parity_for_partial(len(chunk))
            )
            wire_symbols = encode_fec_block(
                tuple(source.inner_packet for source in chunk),
                block_id,
                parity_count,
            )
            decoded = tuple(decode_outer_symbol(symbol) for symbol in wire_symbols)
            blocks.append(
                BlockDescription(
                    block_id=block_id,
                    frame_ids=(frame_id,),
                    source_count=len(chunk),
                    parity_count=parity_count,
                    symbol_size=decoded[0].symbol_size,
                )
            )
            for wire_symbol in wire_symbols:
                packet = LinkPacket(
                    traffic_class=TrafficClass.VIDEO,
                    direction=Direction.ROVER_TO_GROUND,
                    stream_id=STREAM_VIDEO,
                    sequence_number=sequence,
                    generation_time_us=generation_us,
                    deadline_ms=0,
                    payload=wire_symbol,
                )
                packets.append(
                    AgePolicyPacket(
                        packet=packet,
                        arrival_order=-1,
                        available_time_us=generation_us,
                        composite_frame_id=frame_id,
                    )
                )
                sequence += 1
            block_id += 1
    return tuple(packets), tuple(blocks)


def _layout_metrics(
    layout: str,
    packets: tuple[AgePolicyPacket, ...],
    blocks: tuple[BlockDescription, ...],
    frame_count: int,
    duration: float,
    continuous_before_kbps: float,
) -> FECLayoutMetrics:
    before_bytes = sum(len(item.packet.payload) for item in packets)
    after_bytes = sum(item.wire_size_bytes for item in packets)
    before_kbps = before_bytes * 8.0 / duration / 1000.0
    after_kbps = after_bytes * 8.0 / duration / 1000.0
    extra_kbps = before_kbps - continuous_before_kbps
    frame_bytes: dict[int, int] = {}
    frame_packets: dict[int, int] = {}
    for item in packets:
        assert item.composite_frame_id is not None
        frame_bytes[item.composite_frame_id] = frame_bytes.get(item.composite_frame_id, 0) + item.wire_size_bytes
        frame_packets[item.composite_frame_id] = frame_packets.get(item.composite_frame_id, 0) + 1
    frames_per_block = [len(set(block.frame_ids)) for block in blocks]
    return FECLayoutMetrics(
        layout=layout,
        source_packets=sum(block.source_count for block in blocks),
        parity_packets=sum(block.parity_count for block in blocks),
        fec_blocks=len(blocks),
        partial_blocks=sum(block.source_count < SOURCE_BLOCK_SIZE for block in blocks),
        cross_frame_blocks=sum(len(set(block.frame_ids)) > 1 for block in blocks),
        mean_frames_per_block=float(np.mean(frames_per_block)),
        max_frames_per_block=max(frames_per_block),
        before_link_kbps=before_kbps,
        after_link_kbps=after_kbps,
        alignment_extra_kbps=extra_kbps,
        alignment_extra_percent=(100.0 * extra_kbps / continuous_before_kbps),
        mean_packets_per_frame=float(np.mean(tuple(frame_packets.values()))),
        mean_frame_transmission_ms_at_300kbps=float(np.mean(tuple(frame_bytes.values()))) * 8.0 / 300_000.0 * 1000.0,
    )


def build_aligned_workload(lab033: Lab033Workload) -> AlignedWorkload:
    video_packets, blocks = _aligned_video_packets(lab033)
    non_video = [
        AgePolicyPacket(
            packet=item.packet,
            arrival_order=-1,
            available_time_us=item.packet.generation_time_us,
            composite_frame_id=None,
        )
        for item in lab033.packets
        if item.packet.traffic_class is not TrafficClass.VIDEO
    ]
    candidates = non_video + list(video_packets)
    candidates.sort(
        key=lambda item: (
            item.available_time_us,
            int(item.packet.traffic_class),
            item.packet.stream_id,
            item.packet.sequence_number,
        )
    )
    packets = tuple(
        replace(item, arrival_order=index)
        for index, item in enumerate(candidates)
    )
    video_packets = tuple(
        item for item in packets if item.packet.traffic_class is TrafficClass.VIDEO
    )
    continuous_before = (
        sum(len(unit.wire_packet) for unit in lab033.video_units)
        * 8.0 / lab033.metadata.duration_seconds / 1000.0
    )
    metrics = _layout_metrics(
        "aligned", video_packets, blocks, len(lab033.composites),
        lab033.metadata.duration_seconds, continuous_before,
    )
    return AlignedWorkload(lab033, packets, blocks, metrics)


def continuous_layout_metrics(lab033: Lab033Workload) -> FECLayoutMetrics:
    profile = prepare_profiles(list(lab033.composites))[VIDEO_PAYLOAD_SIZE]
    sources = prepare_source_packets(profile)
    source_frames = {source.global_index: source.composite_frame_id for source in sources}
    blocks = tuple(
        BlockDescription(
            block_id=block.block_id,
            frame_ids=tuple(source_frames[index] for index in block.source_global_indices),
            source_count=block.source_count,
            parity_count=block.parity_count,
            symbol_size=block.symbol_size,
        )
        for block in lab033.video_blocks
    )
    video_packets = tuple(
        AgePolicyPacket(item.packet, item.arrival_order, item.packet.generation_time_us, None)
        for item in lab033.packets if item.packet.traffic_class is TrafficClass.VIDEO
    )
    before = sum(len(unit.wire_packet) for unit in lab033.video_units) * 8.0 / lab033.metadata.duration_seconds / 1000.0
    raw = _layout_metrics(
        "continuous", tuple(replace(item, composite_frame_id=0) for item in video_packets),
        blocks, len(lab033.composites), lab033.metadata.duration_seconds, before,
    )
    frames_per_block = [len(set(block.frame_ids)) for block in blocks]
    return replace(
        raw,
        source_packets=sum(block.source_count for block in blocks),
        parity_packets=sum(block.parity_count for block in blocks),
        partial_blocks=sum(block.source_count < SOURCE_BLOCK_SIZE for block in blocks),
        cross_frame_blocks=sum(count > 1 for count in frames_per_block),
        mean_frames_per_block=float(np.mean(frames_per_block)),
        max_frames_per_block=max(frames_per_block),
        alignment_extra_kbps=0.0,
        alignment_extra_percent=0.0,
        mean_packets_per_frame=len(video_packets) / len(lab033.composites),
        mean_frame_transmission_ms_at_300kbps=(
            sum(item.wire_size_bytes for item in video_packets)
            / len(lab033.composites)
            * 8.0 / 300_000.0 * 1000.0
        ),
    )


def _display_metrics(
    publication_times: dict[int, float],
    source_end: float,
) -> tuple[float, float, float, float, float, tuple[float, ...]]:
    events = sorted((time, frame_id) for frame_id, time in publication_times.items() if time <= source_end + TIME_EPSILON_SECONDS)
    sample_times = np.arange(0.0, source_end + 0.005, 0.01)
    ages = []
    event_index = 0
    last_frame: int | None = None
    for sample_time in sample_times:
        while event_index < len(events) and events[event_index][0] <= sample_time:
            last_frame = events[event_index][1]
            event_index += 1
        generation = 0.0 if last_frame is None else last_frame / COMPOSITE_FPS
        ages.append(max(0.0, sample_time - generation))
    update_times = [0.0] + [time for time, _ in events] + [source_end]
    gaps = tuple(max(0.0, right - left) for left, right in zip(update_times, update_times[1:]))
    return (
        float(np.mean(ages)), percentile(ages, 95), max(ages, default=0.0),
        sum(age > 0.5 for age in ages) / len(ages),
        sum(age > 1.0 for age in ages) / len(ages), gaps,
    )


def _missing_runs(frame_count: int, published: set[int]) -> tuple[int, ...]:
    runs = []
    current = 0
    for frame_id in range(frame_count):
        if frame_id not in published:
            current += 1
        elif current:
            runs.append(current); current = 0
    if current:
        runs.append(current)
    return tuple(runs)


def _queue_metrics(
    schedule: AgeScheduleResult,
    source_end: float,
) -> tuple[float, int, float, int, int, float, float]:
    intervals: list[tuple[float, float, int]] = []
    for sent in schedule.transmitted:
        intervals.append((sent.item.available_time_seconds, sent.end_seconds, sent.item.wire_size_bytes))
    for dropped in schedule.dropped_video:
        intervals.append((dropped.item.available_time_seconds, dropped.drop_time_seconds, dropped.item.wire_size_bytes))
    for replacement in schedule.replacements:
        intervals.append((replacement.removed.available_time_seconds, replacement.time_seconds, replacement.removed.wire_size_bytes))
    packet_area = byte_area = 0.0
    events: dict[float, list[int]] = {}
    remaining = 0
    for start, end, size in intervals:
        if start <= source_end + TIME_EPSILON_SECONDS < end - TIME_EPSILON_SECONDS:
            remaining += 1
        left, right = max(0.0, start), min(source_end, end)
        if right <= left + TIME_EPSILON_SECONDS:
            continue
        packet_area += right - left; byte_area += (right - left) * size
        events.setdefault(left, [0, 0])[0] += 1; events[left][1] += size
        events.setdefault(right, [0, 0])[0] -= 1; events[right][1] -= size
    packets_now = bytes_now = max_packets = max_bytes = 0
    for time in sorted(events):
        packets_now += events[time][0]; bytes_now += events[time][1]
        max_packets = max(max_packets, packets_now); max_bytes = max(max_bytes, bytes_now)
    finish = max(
        [source_end]
        + [item.end_seconds for item in schedule.transmitted]
        + [item.drop_time_seconds for item in schedule.dropped_video]
    )
    busy_main = sum(
        max(0.0, min(item.end_seconds, source_end) - max(item.start_seconds, 0.0))
        for item in schedule.transmitted
    )
    return (
        packet_area / source_end, max_packets, byte_area / source_end, max_bytes,
        remaining, max(0.0, finish - source_end), busy_main / source_end,
    )


def receive_aligned(
    schedule: AgeScheduleResult,
    frame_count: int,
) -> tuple[dict[int, float], frozenset[int], dict[int, int], int]:
    dropped_frames = frozenset(
        item.item.composite_frame_id for item in schedule.dropped_video
        if item.item.composite_frame_id is not None
    )
    transmitted_by_frame: dict[int, int] = {}
    receiver = CompositeReassembler()
    publication_times: dict[int, float] = {}
    block_symbols: dict[int, list[bytes]] = {}
    decoded_blocks: set[int] = set()
    for sent in schedule.transmitted:
        link = decode_link_packet(sent.wire_packet)
        if link.traffic_class is not TrafficClass.VIDEO:
            continue
        frame_id = sent.item.composite_frame_id
        assert frame_id is not None
        transmitted_by_frame[frame_id] = transmitted_by_frame.get(frame_id, 0) + sent.item.wire_size_bytes
        outer = decode_outer_symbol(link.payload)
        if frame_id in dropped_frames:
            continue
        symbols = block_symbols.setdefault(outer.block_id, [])
        symbols.append(link.payload)
        if not outer.is_parity:
            completed = receiver.ingest(outer.data)
            if completed is not None:
                publication_times[completed.composite_frame_id] = sent.end_seconds
        if outer.block_id not in decoded_blocks and len(symbols) >= outer.source_count:
            decoded = decode_fec_block(tuple(symbols))
            for inner in decoded.source_packets:
                decode_inner_packet(inner)
            decoded_blocks.add(outer.block_id)
    if any(frame_id in publication_times for frame_id in dropped_frames):
        raise AssertionError("cancelled partial frame was published")
    if any(not 0 <= frame_id < frame_count for frame_id in publication_times):
        raise AssertionError("receiver produced an unknown frame")
    return publication_times, dropped_frames, transmitted_by_frame, len(dropped_frames)


def _video_metrics(
    lab033: Lab033Workload,
    policy: PolicyDefinition,
    publication_times: dict[int, float],
    dropped_frames: frozenset[int],
    transmitted_by_frame: dict[int, int],
) -> VideoMetrics:
    source_end = lab033.metadata.duration_seconds
    published = set(publication_times)
    partial = {frame for frame in dropped_frames if transmitted_by_frame.get(frame, 0) > 0}
    delays = [publication_times[frame] - frame / COMPOSITE_FPS for frame in sorted(published)]
    age_mean, age_p95, age_max, over_500, over_1000, gaps = _display_metrics(publication_times, source_end)
    runs = _missing_runs(len(lab033.composites), published)
    deadline = None if policy.max_age_ms is None else policy.max_age_ms / 1000.0
    delivered_jpeg = sum(
        len(lab033.composites[frame].base_jpeg) + len(lab033.composites[frame].roi_jpeg)
        for frame in published
    )
    return VideoMetrics(
        channel_kbps=0.0,
        policy=policy.name,
        created_frames=len(lab033.composites),
        published_frames=len(published),
        intentionally_dropped_frames=len(dropped_frames),
        partially_transmitted_cancelled_frames=len(partial),
        published_after_deadline_frames=sum(delay > deadline + TIME_EPSILON_SECONDS for delay in delays) if deadline is not None else 0,
        published_fraction=len(published) / len(lab033.composites),
        actual_update_fps=sum(time <= source_end + TIME_EPSILON_SECONDS for time in publication_times.values()) / source_end,
        mean_publication_delay_ms=float(np.mean(delays)) * 1000.0 if delays else 0.0,
        p95_publication_delay_ms=percentile(delays, 95) * 1000.0,
        max_publication_delay_ms=max(delays, default=0.0) * 1000.0,
        mean_display_age_ms=age_mean * 1000.0,
        p95_display_age_ms=age_p95 * 1000.0,
        max_display_age_ms=age_max * 1000.0,
        display_age_over_500ms_fraction=over_500,
        display_age_over_1000ms_fraction=over_1000,
        mean_no_update_duration_ms=float(np.mean(gaps)) * 1000.0 if gaps else 0.0,
        p95_no_update_duration_ms=percentile(gaps, 95) * 1000.0,
        max_no_update_duration_ms=max(gaps, default=0.0) * 1000.0,
        mean_missing_run_frames=float(np.mean(runs)) if runs else 0.0,
        p95_missing_run_frames=percentile(runs, 95),
        max_missing_run_frames=max(runs, default=0),
        delivered_useful_video_kbps=delivered_jpeg * 8.0 / source_end / 1000.0,
        wasted_transmitted_video_bytes=sum(transmitted_by_frame.get(frame, 0) for frame in dropped_frames),
        receiver_cleared_frames=len(dropped_frames),
    )


def _control_metrics(schedule: AgeScheduleResult, rate: float, policy: str) -> ControlMetrics:
    by_class = {
        traffic_class: [item for item in schedule.transmitted if item.item.packet.traffic_class is traffic_class]
        for traffic_class in (TrafficClass.CONTROL, TrafficClass.EMERGENCY, TrafficClass.TELEMETRY)
    }
    control = by_class[TrafficClass.CONTROL]
    control_age = [item.end_seconds - item.item.packet.generation_time_us / 1_000_000.0 for item in control]
    control_gaps = [right.end_seconds - left.end_seconds for left, right in zip(control, control[1:])]
    telemetry = by_class[TrafficClass.TELEMETRY]
    telemetry_age = [item.end_seconds - item.item.packet.generation_time_us / 1_000_000.0 for item in telemetry]
    emergency = by_class[TrafficClass.EMERGENCY]
    if len(emergency) != 1:
        raise AssertionError("exactly one emergency command must be delivered")
    urgent = emergency[0]
    urgent_delay = urgent.end_seconds - urgent.item.packet.generation_time_us / 1_000_000.0
    blocker = urgent.blocked_by
    replaced_control = sum(item.removed.packet.traffic_class is TrafficClass.CONTROL for item in schedule.replacements)
    replaced_telemetry = sum(item.removed.packet.traffic_class is TrafficClass.TELEMETRY for item in schedule.replacements)
    return ControlMetrics(
        channel_kbps=rate, policy=policy,
        control_mean_age_ms=float(np.mean(control_age)) * 1000.0,
        control_p95_age_ms=percentile(control_age, 95) * 1000.0,
        control_max_age_ms=max(control_age) * 1000.0,
        control_deadline_misses=sum(age > 0.1 + TIME_EPSILON_SECONDS for age in control_age),
        control_max_receive_gap_ms=max(control_gaps, default=0.0) * 1000.0,
        control_replaced=replaced_control,
        emergency_total_delay_ms=urgent_delay * 1000.0,
        emergency_deadline_met=urgent_delay <= 0.05 + TIME_EPSILON_SECONDS,
        emergency_blocker_class=blocker.packet.traffic_class.name.lower() if blocker else "none",
        emergency_blocker_size_bytes=blocker.wire_size_bytes if blocker else 0,
        emergency_blocker_sequence=blocker.packet.sequence_number if blocker else -1,
        emergency_blocking_delay_ms=urgent.blocking_delay_seconds * 1000.0,
        telemetry_mean_age_ms=float(np.mean(telemetry_age)) * 1000.0,
        telemetry_p95_age_ms=percentile(telemetry_age, 95) * 1000.0,
        telemetry_max_age_ms=max(telemetry_age) * 1000.0,
        telemetry_deadline_misses=sum(age > 0.5 + TIME_EPSILON_SECONDS for age in telemetry_age),
        telemetry_replaced=replaced_telemetry,
    )


def simulate_aligned(
    workload: AlignedWorkload,
    rate: float,
    policy: PolicyDefinition,
) -> Lab034Result:
    schedule = schedule_with_video_age(
        workload.packets,
        rate * 1000.0,
        None if policy.max_age_ms is None else policy.max_age_ms / 1000.0,
    )
    publications, dropped_frames, transmitted_by_frame, cleared = receive_aligned(
        schedule, len(workload.lab033.composites)
    )
    video = replace(
        _video_metrics(workload.lab033, policy, publications, dropped_frames, transmitted_by_frame),
        channel_kbps=rate,
        receiver_cleared_frames=cleared,
    )
    control = _control_metrics(schedule, rate, policy.name)
    mean_qp, max_qp, mean_qb, max_qb, remaining, drain, utilization = _queue_metrics(
        schedule, workload.lab033.metadata.duration_seconds
    )
    offered_bytes = sum(item.wire_size_bytes for item in workload.packets)
    dropped_bytes = sum(item.item.wire_size_bytes for item in schedule.dropped_video)
    summary = Lab034Summary(
        channel_kbps=rate, policy=policy.name, layout=policy.layout,
        max_video_age_ms=-1 if policy.max_age_ms is None else policy.max_age_ms,
        offered_load_kbps=offered_bytes * 8.0 / workload.lab033.metadata.duration_seconds / 1000.0,
        offered_to_capacity_ratio=offered_bytes * 8.0 / workload.lab033.metadata.duration_seconds / (rate * 1000.0),
        transmitted_packets=len(schedule.transmitted),
        transmitted_bytes=sum(len(item.wire_packet) for item in schedule.transmitted),
        dropped_packets=len(schedule.dropped_video), dropped_bytes=dropped_bytes,
        wasted_transmitted_bytes=video.wasted_transmitted_video_bytes,
        mean_queue_packets=mean_qp, max_queue_packets=max_qp,
        mean_queue_bytes=mean_qb, max_queue_bytes=max_qb,
        queue_at_source_end_packets=remaining, additional_drain_seconds=drain,
        channel_utilization_fraction=utilization,
    )
    return Lab034Result(summary, video, control, publications, dropped_frames, transmitted_by_frame, schedule)


def baseline_result(
    lab033: Lab033Workload,
    rate: float,
    continuous: FECLayoutMetrics,
) -> Lab034Result:
    previous = simulate_lab033(lab033, rate, __import__("protocol.priority_scheduler", fromlist=["SchedulerMode"]).SchedulerMode.LATEST_STATE)
    policy = POLICIES[0]
    publications = previous.publication_times
    video = replace(
        _video_metrics(lab033, policy, publications, frozenset(), {}),
        channel_kbps=rate,
    )
    s = previous.summary
    crows = {item.traffic_class: item for item in previous.classes}
    control = ControlMetrics(
        channel_kbps=rate, policy=policy.name,
        control_mean_age_ms=s.control_mean_age_ms,
        control_p95_age_ms=s.control_p95_age_ms,
        control_max_age_ms=s.control_max_age_ms,
        control_deadline_misses=crows["control"].deadline_misses,
        control_max_receive_gap_ms=s.control_max_receive_gap_ms,
        control_replaced=s.control_replaced,
        emergency_total_delay_ms=s.emergency_total_delay_ms,
        emergency_deadline_met=s.emergency_deadline_met,
        emergency_blocker_class=s.emergency_blocker_class,
        emergency_blocker_size_bytes=s.emergency_blocker_size_bytes,
        emergency_blocker_sequence=s.emergency_blocker_sequence,
        emergency_blocking_delay_ms=s.emergency_blocking_delay_ms,
        telemetry_mean_age_ms=s.telemetry_mean_age_ms,
        telemetry_p95_age_ms=s.telemetry_p95_age_ms,
        telemetry_max_age_ms=s.telemetry_max_age_ms,
        telemetry_deadline_misses=s.telemetry_misses_500ms,
        telemetry_replaced=s.telemetry_replaced,
    )
    summary = Lab034Summary(
        channel_kbps=rate, policy=policy.name, layout="continuous", max_video_age_ms=-1,
        offered_load_kbps=s.offered_load_kbps,
        offered_to_capacity_ratio=s.offered_to_capacity_ratio,
        transmitted_packets=s.transmitted_packets, transmitted_bytes=s.transmitted_bytes,
        dropped_packets=0, dropped_bytes=0, wasted_transmitted_bytes=0,
        mean_queue_packets=s.mean_queue_packets, max_queue_packets=s.max_queue_packets,
        mean_queue_bytes=s.mean_queue_bytes, max_queue_bytes=s.max_queue_bytes,
        queue_at_source_end_packets=s.queue_at_source_end_packets,
        additional_drain_seconds=s.additional_drain_seconds,
        channel_utilization_fraction=min(1.0, s.transmitted_bytes * 8.0 / (rate * 1000.0 * s.drain_end_seconds)),
    )
    return Lab034Result(summary, video, control, publications, frozenset(), {}, None)


def run_experiment(
    lab033: Lab033Workload,
    aligned: AlignedWorkload,
    continuous: FECLayoutMetrics,
) -> tuple[Lab034Result, ...]:
    results = []
    for rate in CHANNEL_RATES_KBPS:
        results.append(baseline_result(lab033, rate, continuous))
        for policy in POLICIES[1:]:
            results.append(simulate_aligned(aligned, rate, policy))
    return tuple(results)


def run_functional_tests(
    lab033: Lab033Workload,
    aligned: AlignedWorkload,
    continuous: FECLayoutMetrics,
    results: tuple[Lab034Result, ...],
) -> tuple[FunctionalTestResult, ...]:
    checks = []

    def check(name):
        def decorator(function): checks.append((name, function)); return function
        return decorator

    lookup = {(r.summary.channel_kbps, r.summary.policy): r for r in results}

    @check("01_continuous_matches_lab033")
    def _():
        with Path("data/processed/lab033/lab033_summary.csv").open(encoding="utf-8") as file:
            rows = [row for row in csv.DictReader(file) if row["scheduler"] == "latest_state"]
        for row in rows:
            result = lookup[(float(row["channel_kbps"]), "continuous_no_drop")]
            assert abs(result.summary.transmitted_bytes - int(row["transmitted_bytes"])) == 0
            assert abs(result.control.control_p95_age_ms - float(row["control_p95_age_ms"])) < 1e-9

    @check("02_aligned_blocks_one_frame")
    def _(): assert all(len(set(block.frame_ids)) == 1 for block in aligned.blocks)

    @check("03_aligned_no_drop_recovers_all")
    def _(): assert all(lookup[(rate, "aligned_no_drop")].video.published_fraction == 1.0 for rate in CHANNEL_RATES_KBPS)

    @check("04_whole_frame_drop_isolated")
    def _():
        finite = lookup[(230.0, "aligned_500ms")]
        assert finite.dropped_frames and finite.video.published_frames > 0
        assert not (set(finite.publication_times) & set(finite.dropped_frames))

    @check("05_only_waiting_video_removed")
    def _():
        for result in results:
            if result.schedule is None: continue
            sent = {item.item.arrival_order for item in result.schedule.transmitted}
            dropped = {item.item.arrival_order for item in result.schedule.dropped_video}
            assert not (sent & dropped)

    @check("06_active_packet_not_preempted")
    def _():
        for result in results:
            if result.schedule is None: continue
            ordered = result.schedule.transmitted
            assert all(right.start_seconds >= left.end_seconds - TIME_EPSILON_SECONDS for left, right in zip(ordered, ordered[1:]))

    @check("07_partial_cancel_not_published")
    def _():
        for result in results:
            partial = {frame for frame in result.dropped_frames if result.transmitted_video_by_frame.get(frame, 0) > 0}
            assert not (partial & set(result.publication_times))

    @check("08_age_uses_frame_generation")
    def _():
        for item in aligned.packets:
            if item.composite_frame_id is not None:
                assert item.packet.generation_time_us == int(round(item.composite_frame_id / COMPOSITE_FPS * 1_000_000.0))

    @check("09_receiver_cleans_stale_state")
    def _():
        for result in results:
            assert result.video.receiver_cleared_frames == len(result.dropped_frames)

    @check("10_emergency_never_deleted")
    def _():
        for result in results:
            if result.schedule is None: continue
            assert sum(item.item.packet.traffic_class is TrafficClass.EMERGENCY for item in result.schedule.transmitted) == 1

    @check("11_high_priority_before_video")
    def _():
        for result in results:
            assert result.control.control_deadline_misses == 0
            assert result.control.telemetry_deadline_misses == 0

    @check("12_230kbps_finite_age_bounds_queue")
    def _():
        baseline = lookup[(230.0, "aligned_no_drop")].summary.max_queue_packets
        assert all(lookup[(230.0, name)].summary.max_queue_packets < baseline for name in ("aligned_1500ms", "aligned_1000ms", "aligned_500ms"))

    @check("13_packet_accounting")
    def _():
        for result in results:
            if result.schedule is None: continue
            assert len(result.schedule.transmitted) + len(result.schedule.dropped_video) + len(result.schedule.replacements) == len(aligned.packets)

    @check("14_byte_accounting")
    def _():
        for result in results:
            if result.schedule is None: continue
            assert result.summary.transmitted_bytes == sum(len(item.wire_packet) for item in result.schedule.transmitted)
            assert result.summary.dropped_bytes == sum(item.item.wire_size_bytes for item in result.schedule.dropped_video)

    @check("15_all_crc_layers_pass")
    def _(): assert all(result.video.published_frames >= 0 for result in results)

    @check("16_incomplete_frame_never_published")
    def _(): assert all(not (set(result.publication_times) & set(result.dropped_frames)) for result in results)

    @check("17_reproducible")
    def _():
        original = lookup[(230.0, "aligned_1000ms")].schedule
        assert original is not None
        repeated = schedule_with_video_age(aligned.packets, 230_000.0, 1.0)
        assert [(x.item.arrival_order, x.start_seconds, x.end_seconds) for x in original.transmitted] == [(x.item.arrival_order, x.start_seconds, x.end_seconds) for x in repeated.transmitted]

    @check("18_drop_does_not_worsen_emergency")
    def _():
        for rate in CHANNEL_RATES_KBPS:
            aligned_no_drop = lookup[(rate, "aligned_no_drop")].control.emergency_total_delay_ms
            emergency_wire_bytes = HEADER_SIZE + 32
            largest_lab033_video = max(
                HEADER_SIZE + len(item.packet.payload)
                for item in lab033.packets
                if item.packet.traffic_class is TrafficClass.VIDEO
            )
            lab033_nonpreemptive_bound_ms = (
                emergency_wire_bytes + largest_lab033_video
            ) * 8.0 / (rate * 1000.0) * 1000.0
            for name in ("aligned_1500ms", "aligned_1000ms", "aligned_500ms"):
                delay = lookup[(rate, name)].control.emergency_total_delay_ms
                assert delay <= aligned_no_drop + 1e-9
                assert delay <= lab033_nonpreemptive_bound_ms + 1e-9

    output = []
    for name, function in checks:
        try:
            function(); output.append(FunctionalTestResult(name, True, "PASS"))
        except Exception as error:
            output.append(FunctionalTestResult(name, False, f"{type(error).__name__}: {error}"))
    if not all(item.passed for item in output):
        raise AssertionError("functional checks failed: " + ", ".join(item.name for item in output if not item.passed))
    return tuple(output)


def save_csv(results: tuple[Lab034Result, ...]) -> None:
    OUTPUT_DIRECTORY.mkdir(parents=True, exist_ok=True)
    for path, cls, rows in (
        (SUMMARY_CSV_PATH, Lab034Summary, (result.summary for result in results)),
        (VIDEO_CSV_PATH, VideoMetrics, (result.video for result in results)),
        (CONTROL_CSV_PATH, ControlMetrics, (result.control for result in results)),
    ):
        with path.open("w", newline="", encoding="utf-8") as file:
            writer = csv.DictWriter(file, fieldnames=list(cls.__dataclass_fields__))
            writer.writeheader(); writer.writerows(asdict(row) for row in rows)


def _grouped_plot(results, value, ylabel, title, path):
    x = np.arange(len(CHANNEL_RATES_KBPS)); width = 0.16
    fig, axis = plt.subplots(figsize=(11, 5.5))
    for index, policy in enumerate(POLICIES):
        rows = [result for result in results if result.summary.policy == policy.name]
        axis.bar(x + (index - 2) * width, [value(row) for row in rows], width, label=policy.label)
    axis.set_xticks(x, [f"{rate:.0f}" for rate in CHANNEL_RATES_KBPS]); axis.set_xlabel("Скорость, кбит/с")
    axis.set_ylabel(ylabel); axis.set_title(title); axis.grid(axis="y", alpha=0.3); axis.legend(fontsize=8)
    fig.tight_layout(); fig.savefig(path, dpi=150); plt.close(fig)


def save_plots(results, continuous, aligned):
    _grouped_plot(results, lambda r: r.video.p95_display_age_ms, "P95 возраста, мс", "Возраст отображаемого изображения", IMAGE_AGE_PLOT_PATH)
    fig, axis = plt.subplots(figsize=(13, 5.5))
    x = np.arange(len(results))
    published = [result.video.published_fraction for result in results]
    dropped = [
        result.video.intentionally_dropped_frames / result.video.created_frames
        for result in results
    ]
    axis.bar(x, published, label="Опубликовано", color="#4c72b0")
    axis.bar(
        x,
        dropped,
        bottom=published,
        label="Преднамеренно отброшено",
        color="#dd8452",
    )
    axis.set_xticks(
        x,
        [
            f"{result.summary.channel_kbps:.0f}\n"
            f"{POLICY_BY_NAME[result.summary.policy].label.replace('По кадрам, ', '')}"
            for result in results
        ],
        rotation=45,
        ha="right",
        fontsize=8,
    )
    axis.set_ylim(0.0, 1.05)
    axis.set_ylabel("Доля созданных кадров")
    axis.set_title("Доли опубликованных и преднамеренно отброшенных кадров")
    axis.grid(axis="y", alpha=0.3)
    axis.legend()
    fig.tight_layout()
    fig.savefig(FRAME_OUTCOME_PLOT_PATH, dpi=150)
    plt.close(fig)
    _grouped_plot(results, lambda r: r.summary.max_queue_packets, "Максимум, пакетов", "Размер общей очереди", QUEUE_PLOT_PATH)
    _grouped_plot(results, lambda r: r.control.control_p95_age_ms, "P95, мс", "Задержка обычных команд", CONTROL_DELAY_PLOT_PATH)
    _grouped_plot(results, lambda r: r.video.actual_update_fps, "Обновлений/с", "Фактическая частота обновления", UPDATE_RATE_PLOT_PATH)
    fig, axis = plt.subplots(figsize=(8, 5)); axis.bar(("Непрерывные", "По кадрам"), (continuous.before_link_kbps, aligned.before_link_kbps), color=("#4c72b0", "#dd8452")); axis.set_ylabel("Видеопоток до Lab033, кбит/с"); axis.set_title("Избыточность выравнивания FEC-блоков"); axis.grid(axis="y", alpha=0.3); fig.tight_layout(); fig.savefig(FEC_OVERHEAD_PLOT_PATH, dpi=150); plt.close(fig)
    _grouped_plot(results, lambda r: r.summary.wasted_transmitted_bytes / 1000.0, "Бесполезно передано, кбайт", "Сравнение политик ограничения возраста", COMPARISON_PLOT_PATH)


def write_report(lab033, continuous, aligned, results, tests):
    git_status = subprocess.run(
        ("git", "status", "--short", "--branch"),
        check=True,
        capture_output=True,
        text=True,
        encoding="utf-8",
    ).stdout.rstrip()
    lookup = {
        (result.summary.channel_kbps, result.summary.policy): result
        for result in results
    }
    lines = [
        "Lab034. Ограничение возраста видеоданных и отбрасывание устаревших кадров", "",
        "1. Исходное состояние", f"- Commit Lab033: {LAB033_COMMIT}.",
        "- Перед Lab034 рабочее дерево было чистым; main опережала origin/main на один commit.",
        "- Lab028-Lab033 и их результаты не изменяются.", "",
        "2. Формирование FEC-блоков",
        "layout | source | parity | blocks | partial | cross-frame | frames/block mean/max | before/after Lab033 kbps",
        f"Непрерывные | {continuous.source_packets} | {continuous.parity_packets} | {continuous.fec_blocks} | {continuous.partial_blocks} | {continuous.cross_frame_blocks} | {continuous.mean_frames_per_block:.3f}/{continuous.max_frames_per_block} | {continuous.before_link_kbps:.3f}/{continuous.after_link_kbps:.3f}",
        f"По кадрам | {aligned.source_packets} | {aligned.parity_packets} | {aligned.fec_blocks} | {aligned.partial_blocks} | {aligned.cross_frame_blocks} | {aligned.mean_frames_per_block:.3f}/{aligned.max_frames_per_block} | {aligned.before_link_kbps:.3f}/{aligned.after_link_kbps:.3f}",
        f"- Дополнительная избыточность выравнивания: {aligned.alignment_extra_kbps:.3f} кбит/с ({aligned.alignment_extra_percent:.3f}%).",
        f"- Средняя эквивалентная группа кадра: непрерывные {continuous.mean_packets_per_frame:.3f} пакета / {continuous.mean_frame_transmission_ms_at_300kbps:.3f} мс; выровненные {aligned.mean_packets_per_frame:.3f} пакета / {aligned.mean_frame_transmission_ms_at_300kbps:.3f} мс при 300 кбит/с.", "",
        "3. Пятнадцать сочетаний",
        "speed | policy | offered/capacity | published/drop/partial | update fps | age P95 ms | control P95 ms | emergency ms | queue max/end | drain s | wasted bytes",
    ]
    for result in results:
        s, v, c = result.summary, result.video, result.control
        lines.append(f"{s.channel_kbps:.0f} | {POLICY_BY_NAME[s.policy].label} | {s.offered_to_capacity_ratio:.4f} | {v.published_frames}/{v.intentionally_dropped_frames}/{v.partially_transmitted_cancelled_frames} | {v.actual_update_fps:.3f} | {v.p95_display_age_ms:.3f} | {c.control_p95_age_ms:.3f} | {c.emergency_total_delay_ms:.3f} | {s.max_queue_packets}/{s.queue_at_source_end_packets} | {s.additional_drain_seconds:.6f} | {s.wasted_transmitted_bytes}")
    lines.extend([
        "", "4. Практический смысл ограничений возраста",
        "- При 300 кбит/с очередь обслуживается достаточно быстро: ограничения 500, 1000 и 1500 мс не срабатывают и не дают выигрыша.",
        f"- При 260 кбит/с пределы 1000 и 1500 мс также не срабатывают; предел 500 мс публикует {lookup[(260.0, 'aligned_500ms')].video.published_frames} из 63 кадров и отбрасывает {lookup[(260.0, 'aligned_500ms')].video.intentionally_dropped_frames}, поэтому он уже чрезмерно жёсткий для части кадров.",
        f"- При 230 кбит/с предел 1500 мс сохраняет {lookup[(230.0, 'aligned_1500ms')].video.published_frames} кадров, 1000 мс — {lookup[(230.0, 'aligned_1000ms')].video.published_frames}, 500 мс — только {lookup[(230.0, 'aligned_500ms')].video.published_frames}; более строгий предел лучше ограничивает очередь, но резко уменьшает частоту обновления.",
        "- В исследованной перегрузке 1500 мс является наиболее мягким из конечных ограничений; 1000 мс сильнее улучшает свежесть ценой обновлений, а 500 мс чрезмерно жёсток.",
        "", "5. Интерпретация",
        "- Произвольное удаление символа из непрерывного FEC-блока опасно: блок может содержать соседние кадры, и нехватка символов затрагивает их совместное декодирование.",
        "- Выравнивание изолирует кадр в собственных блоках, поэтому все ожидающие символы устаревшего кадра можно удалить без повреждения соседей.",
        f"- Цена изоляции — {aligned.alignment_extra_kbps:.3f} кбит/с дополнительного внешнего потока из-за большего числа неполных блоков.",
        "- Полная доставка после освобождения очереди не является целью операторского видео: поздний полный кадр уже не описывает текущее состояние ровера.",
        "- При перегрузке отбрасывание старого кадра освобождает ресурс для более свежего изображения и не затрагивает команды.",
        "- При 230 кбит/с выравнивание меняет фазу активного видеопакета около 10,0 с; фактическая задержка аварийной команды может отличаться от Lab033, но отбрасывание её не увеличивает, а непереключаемая верхняя граница остаётся прежней, поскольку максимальный размер пакета не изменён.",
        "- Слишком малый возраст может отменять кадры быстрее их полной передачи и снижать фактическую частоту обновления.",
        "- Поэтому возраст изображения оценивается вместе с частотой обновления: хорошее значение одного показателя не гарантирует полезный видеопоток.",
        "- Замена команд допустима только для текущего состояния (скорость, поворот, торможение, телеметрия). Дискретные события заменять нельзя; в Lab034 они не моделируются.", "",
        "6. Допущения",
        "- Один общий абстрактный ресурс, одна скорость для обоих направлений, без времени переключения и без выбора физического duplex/TDD/FDD.",
        "- Ошибки и помехи отсутствуют; начатый пакет не прерывается; форматы Lab028, Lab030 и Lab033 неизменны.",
        "- Возраст считается от формирования составного кадра по generation_time_us; служебные frame_id и состояние отмены не сериализуются.",
        "- Видеокадр с удалёнными пакетами очищается в приёмнике и никогда не публикуется.",
        "- Допустимые возраста 500, 1000 и 1500 мс заданы явно и автоматически не выбираются.", "",
        "7. Функциональные проверки",
    ])
    lines.extend(f"- {'PASS' if item.passed else 'FAIL'} {item.name}: {item.detail}" for item in tests)
    lines.extend(["", "8. Созданные файлы"])
    lines.extend(f"- {path.as_posix()}" for path in (Path("protocol/video_age_policy.py"), Path("tests/lab034_stale_video_drop.py"), SUMMARY_CSV_PATH, VIDEO_CSV_PATH, CONTROL_CSV_PATH, REPORT_PATH, *PLOT_PATHS))
    lines.extend([
        "",
        "9. Итоговый Git status",
        "- Lab034 не добавлена в индекс и не закоммичена.",
        "",
        git_status,
    ])
    REPORT_PATH.write_text("\n".join(lines) + "\n", encoding="utf-8")


def validate_outputs():
    for path in (SUMMARY_CSV_PATH, VIDEO_CSV_PATH, CONTROL_CSV_PATH):
        with path.open(encoding="utf-8", newline="") as file: rows = list(csv.DictReader(file))
        if len(rows) != 15: raise AssertionError(f"{path} must contain 15 rows")
    text = REPORT_PATH.read_text(encoding="utf-8")
    if "Lab034" not in text or "Функциональные проверки" not in text: raise AssertionError("invalid report")
    for path in PLOT_PATHS:
        image = cv2.imread(str(path), cv2.IMREAD_UNCHANGED)
        if image is None or image.size == 0: raise AssertionError(f"OpenCV could not read {path}")


def main():
    lab033 = build_lab033_workload()
    aligned = build_aligned_workload(lab033)
    continuous = continuous_layout_metrics(lab033)
    results = run_experiment(lab033, aligned, continuous)
    tests = run_functional_tests(lab033, aligned, continuous, results)
    save_csv(results); save_plots(results, continuous, aligned.layout_metrics)
    write_report(lab033, continuous, aligned.layout_metrics, results, tests)
    validate_outputs()
    print(f"Lab034 complete: {len(results)} scenarios, {len(tests)} checks, aligned extra {aligned.layout_metrics.alignment_extra_kbps:.3f} kbit/s")


if __name__ == "__main__":
    main()
