from __future__ import annotations

from copy import deepcopy
from dataclasses import dataclass
from fractions import Fraction
from typing import Any, Mapping

from timed_text import TimedTextDocument
from timebase import RationalTimebase


PREMIERE_TICKS_PER_SECOND = 254_016_000_000


@dataclass(frozen=True)
class BoundaryEdge:
    cue_index: int
    cue_id: str
    edge: str


@dataclass(frozen=True)
class CutAlignmentMove:
    edges: tuple[BoundaryEdge, ...]
    original_ms: int
    original_frame_index: int
    cut_frame_index: int | None
    target_ms: int | None
    distance_frames: int | None
    status: str
    reason: str


@dataclass(frozen=True)
class CutAlignmentResult:
    document: TimedTextDocument
    moves: tuple[CutAlignmentMove, ...]

    @property
    def applied_count(self) -> int:
        return sum(move.status == "APPLIED" for move in self.moves)


@dataclass(frozen=True)
class CutManifestSelection:
    sequence_name: str
    reference_track_index: int
    reference_track_name: str
    reference_track_muted: bool
    timebase: RationalTimebase
    srt_zero_sequence_seconds: str
    srt_zero_sequence_frame: int
    item_count: int
    eligible_item_count: int
    sequence_cut_frame_indices: tuple[int, ...]
    cut_frame_indices: tuple[int, ...]
    cuts: tuple[dict[str, Any], ...]
    skipped_adjacencies: tuple[dict[str, Any], ...]

    def to_report_dict(self) -> dict[str, Any]:
        return {
            "sequenceName": self.sequence_name,
            "referenceTrack": {
                "index": self.reference_track_index,
                "label": f"V{self.reference_track_index + 1}",
                "name": self.reference_track_name,
                "muted": self.reference_track_muted,
            },
            "timebase": self.timebase.to_json(),
            "srtZeroMapping": {
                "sequenceSeconds": self.srt_zero_sequence_seconds,
                "sequenceFrame": self.srt_zero_sequence_frame,
            },
            "itemCount": self.item_count,
            "eligibleItemCount": self.eligible_item_count,
            "eligibleCutCount": len(self.cut_frame_indices),
            "cuts": list(self.cuts),
            "skippedAdjacencies": list(self.skipped_adjacencies),
        }


@dataclass(frozen=True)
class CaptionTimingItem:
    item_id: str
    item_index: int
    start_seconds: Fraction
    end_seconds: Fraction


@dataclass(frozen=True)
class CaptionTimingSelection:
    sequence_name: str
    caption_track_index: int
    caption_track_name: str
    timebase: RationalTimebase
    items: tuple[CaptionTimingItem, ...]


@dataclass(frozen=True)
class DirectCaptionBoundaryEdge:
    item_position: int
    item_id: str
    edge: str


@dataclass(frozen=True)
class DirectCaptionAlignmentMove:
    edges: tuple[DirectCaptionBoundaryEdge, ...]
    original_seconds: Fraction
    original_frame_index: int
    cut_frame_index: int | None
    target_seconds: Fraction | None
    distance_frames: int | None
    status: str
    reason: str


@dataclass(frozen=True)
class DirectCaptionAlignmentResult:
    items: tuple[CaptionTimingItem, ...]
    moves: tuple[DirectCaptionAlignmentMove, ...]

    @property
    def applied_count(self) -> int:
        return sum(move.status == "APPLIED" for move in self.moves)


@dataclass(frozen=True)
class _BoundaryAtom:
    order: int
    edges: tuple[BoundaryEdge, ...]
    original_ms: int


@dataclass(frozen=True)
class _DirectBoundaryAtom:
    order: int
    edges: tuple[DirectCaptionBoundaryEdge, ...]
    original_seconds: Fraction


def _require_non_negative_integer(value: int, label: str) -> int:
    if isinstance(value, bool) or not isinstance(value, int) or value < 0:
        raise ValueError(f"{label} must be a non-negative integer.")
    return value


def _require_mapping(value: Any, label: str) -> Mapping[str, Any]:
    if not isinstance(value, Mapping):
        raise ValueError(f"{label} must be an object.")
    return value


def _require_non_negative_fraction(value: Any, label: str) -> Fraction:
    if isinstance(value, bool):
        raise ValueError(f"{label} must be a non-negative number.")
    try:
        result = Fraction(str(value))
    except (ValueError, ZeroDivisionError) as exc:
        raise ValueError(f"{label} must be a non-negative number.") from exc
    if result < 0:
        raise ValueError(f"{label} must be a non-negative number.")
    return result


def fraction_seconds_text(value: Fraction) -> str:
    """Return a stable decimal seconds value suitable for Premiere's numeric DSL input."""

    return format(float(value), ".15g")


def caption_timing_selection(value: Any) -> CaptionTimingSelection:
    manifest = _require_mapping(value, "Caption timing manifest")
    unknown = set(manifest) - {
        "version",
        "sequenceName",
        "sequenceTimebaseTicks",
        "captionTrackIndex",
        "captionTrackName",
        "items",
    }
    if unknown:
        raise ValueError(
            "Caption timing manifest contains unknown field(s): "
            + ", ".join(sorted(str(item) for item in unknown))
            + "."
        )
    if manifest.get("version") != 1:
        raise ValueError("Caption timing manifest version must be 1.")
    sequence_name = str(manifest.get("sequenceName", "")).strip()
    if not sequence_name:
        raise ValueError("Caption timing manifest requires the active sequence name.")
    ticks = _require_non_negative_integer(
        manifest.get("sequenceTimebaseTicks"), "Sequence timebase ticks"
    )
    if ticks == 0:
        raise ValueError("Sequence timebase ticks must be greater than zero.")
    track_index = _require_non_negative_integer(
        manifest.get("captionTrackIndex"), "Caption-track index"
    )
    raw_items = manifest.get("items")
    if not isinstance(raw_items, list):
        raise ValueError("Caption timing manifest items must be an array.")

    items: list[CaptionTimingItem] = []
    ids: set[str] = set()
    indices: set[int] = set()
    for position, raw_item in enumerate(raw_items, start=1):
        item = _require_mapping(raw_item, f"Caption timing item {position}")
        unknown_item = set(item) - {"id", "index", "startSeconds", "endSeconds"}
        if unknown_item:
            raise ValueError(
                f"Caption timing item {position} contains unknown field(s): "
                + ", ".join(sorted(str(key) for key in unknown_item))
                + "."
            )
        item_id = str(item.get("id", "")).strip()
        if not item_id:
            raise ValueError(f"Caption timing item {position} requires an id.")
        if item_id in ids:
            raise ValueError(f"Caption timing item id '{item_id}' is duplicated.")
        ids.add(item_id)
        item_index = _require_non_negative_integer(
            item.get("index"), f"Caption timing item {position} index"
        )
        if item_index in indices:
            raise ValueError(f"Caption timing item index {item_index} is duplicated.")
        indices.add(item_index)
        start = _require_non_negative_fraction(
            item.get("startSeconds"), f"Caption timing item {position} start"
        )
        end = _require_non_negative_fraction(
            item.get("endSeconds"), f"Caption timing item {position} end"
        )
        if end <= start:
            raise ValueError(
                f"Caption timing item {position} must have a positive duration."
            )
        items.append(CaptionTimingItem(item_id, item_index, start, end))

    items.sort(key=lambda item: (item.start_seconds, item.end_seconds, item.item_index))
    for previous, current in zip(items, items[1:]):
        if current.start_seconds < previous.end_seconds:
            raise ValueError(
                "Caption timing manifest contains overlapping items "
                f"'{previous.item_id}' and '{current.item_id}'."
            )
    return CaptionTimingSelection(
        sequence_name=sequence_name,
        caption_track_index=track_index,
        caption_track_name=str(manifest.get("captionTrackName", "")).strip(),
        timebase=RationalTimebase(PREMIERE_TICKS_PER_SECOND, ticks),
        items=tuple(items),
    )


def cut_manifest_selection(value: Any) -> CutManifestSelection:
    manifest = _require_mapping(value, "Cut manifest")
    unknown = set(manifest) - {
        "version",
        "sequenceName",
        "sequenceTimebaseTicks",
        "srtZeroSequenceSeconds",
        "referenceTrackIndex",
        "referenceTrackName",
        "referenceTrackMuted",
        "items",
    }
    if unknown:
        raise ValueError(
            "Cut manifest contains unknown field(s): "
            + ", ".join(sorted(str(item) for item in unknown))
            + "."
        )
    if manifest.get("version") != 1:
        raise ValueError("Cut manifest version must be 1.")
    sequence_name = str(manifest.get("sequenceName", "")).strip()
    if not sequence_name:
        raise ValueError("Cut manifest requires the active sequence name.")
    ticks = _require_non_negative_integer(
        manifest.get("sequenceTimebaseTicks"), "Sequence timebase ticks"
    )
    if ticks == 0:
        raise ValueError("Sequence timebase ticks must be greater than zero.")
    timebase = RationalTimebase(PREMIERE_TICKS_PER_SECOND, ticks)
    origin_seconds = str(manifest.get("srtZeroSequenceSeconds", "0")).strip()
    origin_frame = timebase.seconds_to_nearest_frame_index(origin_seconds)
    track_index = _require_non_negative_integer(
        manifest.get("referenceTrackIndex"), "Reference video-track index"
    )
    track_muted = manifest.get("referenceTrackMuted", False)
    if not isinstance(track_muted, bool):
        raise ValueError("Reference video-track muted state must be true or false.")
    items_value = manifest.get("items")
    if not isinstance(items_value, list):
        raise ValueError("Cut manifest items must be an array.")

    normalized_items: list[dict[str, Any]] = []
    item_ids: set[str] = set()
    for position, raw_item in enumerate(items_value, start=1):
        item = _require_mapping(raw_item, f"Cut manifest item {position}")
        unknown_item = set(item) - {"id", "startSeconds", "endSeconds", "disabled"}
        if unknown_item:
            raise ValueError(
                f"Cut manifest item {position} contains unknown field(s): "
                + ", ".join(sorted(str(key) for key in unknown_item))
                + "."
            )
        item_id = str(item.get("id", "")).strip()
        if not item_id:
            raise ValueError(f"Cut manifest item {position} requires an id.")
        if item_id in item_ids:
            raise ValueError(f"Cut manifest item id '{item_id}' is duplicated.")
        item_ids.add(item_id)
        disabled = item.get("disabled", False)
        if not isinstance(disabled, bool):
            raise ValueError(
                f"Cut manifest item {position} disabled state must be true or false."
            )
        start_seconds = str(item.get("startSeconds", "")).strip()
        end_seconds = str(item.get("endSeconds", "")).strip()
        start_frame = timebase.seconds_to_nearest_frame_index(start_seconds)
        end_frame = timebase.seconds_to_nearest_frame_index(end_seconds)
        if end_frame <= start_frame:
            raise ValueError(
                f"Cut manifest item {position} must have a positive frame duration."
            )
        normalized_items.append(
            {
                "id": item_id,
                "startFrame": start_frame,
                "endFrame": end_frame,
                "disabled": disabled,
            }
        )

    eligible_items = sorted(
        (item for item in normalized_items if not item["disabled"]),
        key=lambda item: (item["startFrame"], item["endFrame"], item["id"]),
    )
    cuts: list[dict[str, Any]] = []
    skipped: list[dict[str, Any]] = []
    sequence_cut_indices: set[int] = set()
    if track_muted:
        skipped.append(
            {
                "reason": "REFERENCE_TRACK_MUTED",
                "message": "The chosen video track is muted, so it supplies no visible cuts.",
            }
        )
    else:
        for left, right in zip(eligible_items, eligible_items[1:]):
            left_end = int(left["endFrame"])
            right_start = int(right["startFrame"])
            if left_end != right_start:
                skipped.append(
                    {
                        "leftItemId": left["id"],
                        "rightItemId": right["id"],
                        "leftEndSequenceFrame": left_end,
                        "rightStartSequenceFrame": right_start,
                        "reason": "ITEM_GAP" if left_end < right_start else "ITEM_OVERLAP",
                    }
                )
                continue
            sequence_cut_indices.add(left_end)
            srt_frame = left_end - origin_frame
            if srt_frame < 0:
                skipped.append(
                    {
                        "leftItemId": left["id"],
                        "rightItemId": right["id"],
                        "sequenceFrame": left_end,
                        "reason": "CUT_BEFORE_SRT_ZERO",
                    }
                )
                continue
            cuts.append(
                {
                    "leftItemId": left["id"],
                    "rightItemId": right["id"],
                    "sequenceFrame": left_end,
                    "srtFrame": srt_frame,
                }
            )

    cut_indices = tuple(sorted({int(item["srtFrame"]) for item in cuts}))
    return CutManifestSelection(
        sequence_name=sequence_name,
        reference_track_index=track_index,
        reference_track_name=str(manifest.get("referenceTrackName", "")).strip(),
        reference_track_muted=track_muted,
        timebase=timebase,
        srt_zero_sequence_seconds=origin_seconds,
        srt_zero_sequence_frame=origin_frame,
        item_count=len(normalized_items),
        eligible_item_count=len(eligible_items),
        sequence_cut_frame_indices=tuple(sorted(sequence_cut_indices)),
        cut_frame_indices=cut_indices,
        cuts=tuple(cuts),
        skipped_adjacencies=tuple(skipped),
    )


def _boundary_atoms(document: TimedTextDocument) -> list[_BoundaryAtom]:
    atoms: list[_BoundaryAtom] = []
    order = 0
    for cue_index, cue in enumerate(document.cues):
        if cue_index == 0:
            atoms.append(
                _BoundaryAtom(
                    order,
                    (BoundaryEdge(cue_index, cue.id, "START"),),
                    cue.start_ms,
                )
            )
            order += 1

        next_cue = document.cues[cue_index + 1] if cue_index + 1 < len(document.cues) else None
        if next_cue is None:
            atoms.append(
                _BoundaryAtom(
                    order,
                    (BoundaryEdge(cue_index, cue.id, "END"),),
                    cue.end_ms,
                )
            )
            continue

        if cue.end_ms == next_cue.start_ms:
            atoms.append(
                _BoundaryAtom(
                    order,
                    (
                        BoundaryEdge(cue_index, cue.id, "END"),
                        BoundaryEdge(cue_index + 1, next_cue.id, "START"),
                    ),
                    cue.end_ms,
                )
            )
            order += 1
            continue

        atoms.append(
            _BoundaryAtom(
                order,
                (BoundaryEdge(cue_index, cue.id, "END"),),
                cue.end_ms,
            )
        )
        order += 1
        atoms.append(
            _BoundaryAtom(
                order,
                (BoundaryEdge(cue_index + 1, next_cue.id, "START"),),
                next_cue.start_ms,
            )
        )
        order += 1
    return atoms


def _apply_atom(document: TimedTextDocument, atom: _BoundaryAtom, target_ms: int) -> None:
    for edge in atom.edges:
        cue = document.cues[edge.cue_index]
        if edge.edge == "START":
            cue.start_ms = target_ms
        else:
            cue.end_ms = target_ms


def _safety_failure(
    document: TimedTextDocument,
    *,
    affected_cue_indices: set[int],
    timebase: RationalTimebase,
    min_duration_ms: int | None,
    min_gap_ms: int | None,
    min_duration_frames: int | None,
    min_gap_frames: int | None,
) -> str | None:
    for cue_index in sorted(affected_cue_indices):
        cue = document.cues[cue_index]
        if min_duration_frames is not None:
            duration_is_safe = _srt_span_meets_frame_minimum(
                cue.start_ms,
                cue.end_ms,
                timebase=timebase,
                minimum_frames=min_duration_frames,
            )
        else:
            duration_is_safe = cue.end_ms - cue.start_ms >= int(min_duration_ms or 0)
        if not duration_is_safe:
            return "WOULD_SHORTEN_CAPTION_BELOW_MINIMUM"
        if cue.words and (
            cue.start_ms > cue.words[0].start_ms or cue.end_ms < cue.words[-1].end_ms
        ):
            return "WOULD_EXCLUDE_WORD_TIMING"
    affected_gap_indices = {
        gap_index
        for cue_index in affected_cue_indices
        for gap_index in (cue_index - 1, cue_index)
        if 0 <= gap_index < len(document.cues) - 1
    }
    for gap_index in sorted(affected_gap_indices):
        previous = document.cues[gap_index]
        current = document.cues[gap_index + 1]
        gap = current.start_ms - previous.end_ms
        if gap < 0:
            return "WOULD_OVERLAP_CAPTIONS"
        if min_gap_frames is not None:
            gap_is_safe = _srt_span_meets_frame_minimum(
                previous.end_ms,
                current.start_ms,
                timebase=timebase,
                minimum_frames=min_gap_frames,
            )
        else:
            gap_is_safe = gap >= int(min_gap_ms or 0)
        if not gap_is_safe:
            return "WOULD_REDUCE_GAP_BELOW_MINIMUM"
    return None


def _srt_span_meets_frame_minimum(
    start_ms: int,
    end_ms: int,
    *,
    timebase: RationalTimebase,
    minimum_frames: int,
) -> bool:
    """Compare an SRT span in sequence-frame terms without phase rounding errors.

    When both millisecond timestamps are canonical frame-grid timestamps, compare
    their frame indices. This accepts a one-frame span that serializes to 41 ms at
    some 24000/1001 phases. For arbitrary non-frame-aligned source timestamps,
    retain the conservative exact-duration comparison.
    """

    if minimum_frames == 0:
        return True
    if timebase.is_frame_aligned(start_ms) and timebase.is_frame_aligned(end_ms):
        return (
            timebase.nearest_frame_index(end_ms)
            - timebase.nearest_frame_index(start_ms)
            >= minimum_frames
        )
    return Fraction(end_ms - start_ms, 1000) >= (
        minimum_frames * timebase.frame_duration_seconds
    )


def _direct_boundary_atoms(
    items: tuple[CaptionTimingItem, ...],
) -> list[_DirectBoundaryAtom]:
    atoms: list[_DirectBoundaryAtom] = []
    order = 0
    for item_position, item in enumerate(items):
        if item_position == 0:
            atoms.append(
                _DirectBoundaryAtom(
                    order,
                    (DirectCaptionBoundaryEdge(item_position, item.item_id, "START"),),
                    item.start_seconds,
                )
            )
            order += 1
        next_item = items[item_position + 1] if item_position + 1 < len(items) else None
        if next_item is None:
            atoms.append(
                _DirectBoundaryAtom(
                    order,
                    (DirectCaptionBoundaryEdge(item_position, item.item_id, "END"),),
                    item.end_seconds,
                )
            )
            continue
        if item.end_seconds == next_item.start_seconds:
            atoms.append(
                _DirectBoundaryAtom(
                    order,
                    (
                        DirectCaptionBoundaryEdge(item_position, item.item_id, "END"),
                        DirectCaptionBoundaryEdge(
                            item_position + 1, next_item.item_id, "START"
                        ),
                    ),
                    item.end_seconds,
                )
            )
            order += 1
            continue
        atoms.append(
            _DirectBoundaryAtom(
                order,
                (DirectCaptionBoundaryEdge(item_position, item.item_id, "END"),),
                item.end_seconds,
            )
        )
        order += 1
        atoms.append(
            _DirectBoundaryAtom(
                order,
                (
                    DirectCaptionBoundaryEdge(
                        item_position + 1, next_item.item_id, "START"
                    ),
                ),
                next_item.start_seconds,
            )
        )
        order += 1
    return atoms


def _apply_direct_atom(
    items: list[CaptionTimingItem], atom: _DirectBoundaryAtom, target: Fraction
) -> None:
    for edge in atom.edges:
        item = items[edge.item_position]
        items[edge.item_position] = CaptionTimingItem(
            item.item_id,
            item.item_index,
            target if edge.edge == "START" else item.start_seconds,
            target if edge.edge == "END" else item.end_seconds,
        )


def _direct_safety_failure(
    items: list[CaptionTimingItem],
    *,
    affected_item_positions: set[int],
    minimum_duration_seconds: Fraction,
    minimum_gap_seconds: Fraction,
) -> str | None:
    for position in sorted(affected_item_positions):
        item = items[position]
        if item.end_seconds - item.start_seconds < minimum_duration_seconds:
            return "WOULD_SHORTEN_CAPTION_BELOW_MINIMUM"
    affected_gap_positions = {
        gap_position
        for position in affected_item_positions
        for gap_position in (position - 1, position)
        if 0 <= gap_position < len(items) - 1
    }
    for gap_position in sorted(affected_gap_positions):
        previous = items[gap_position]
        current = items[gap_position + 1]
        gap = current.start_seconds - previous.end_seconds
        if gap < 0:
            return "WOULD_OVERLAP_CAPTIONS"
        if gap < minimum_gap_seconds:
            return "WOULD_REDUCE_GAP_BELOW_MINIMUM"
    return None


def _alignment_safety_values(
    *,
    min_duration_ms: int | None,
    min_gap_ms: int | None,
    min_duration_frames: int | None,
    min_gap_frames: int | None,
) -> tuple[int | None, int | None, int | None, int | None]:
    if min_duration_ms is not None and min_duration_frames is not None:
        raise ValueError(
            "Choose minimum caption duration in either frames or milliseconds, not both."
        )
    if min_gap_ms is not None and min_gap_frames is not None:
        raise ValueError(
            "Choose minimum caption gap in either frames or milliseconds, not both."
        )

    if min_duration_frames is not None:
        duration_frames = _require_non_negative_integer(
            min_duration_frames, "Minimum caption duration in frames"
        )
        if duration_frames == 0:
            raise ValueError("Minimum caption duration must be at least 1 frame.")
        duration_ms = None
    else:
        duration_ms = _require_non_negative_integer(
            1 if min_duration_ms is None else min_duration_ms,
            "Minimum caption duration",
        )
        if duration_ms == 0:
            raise ValueError("Minimum caption duration must be at least 1 ms.")
        duration_frames = None

    if min_gap_frames is not None:
        gap_frames = _require_non_negative_integer(
            min_gap_frames, "Minimum caption gap in frames"
        )
        gap_ms = None
    else:
        gap_ms = _require_non_negative_integer(
            0 if min_gap_ms is None else min_gap_ms,
            "Minimum caption gap",
        )
        gap_frames = None

    return duration_ms, gap_ms, duration_frames, gap_frames


def align_caption_timing_to_cut_frames(
    selection: CaptionTimingSelection,
    *,
    cut_frame_indices: list[int],
    max_distance_frames: int,
    min_duration_ms: int | None = None,
    min_gap_ms: int | None = None,
    min_duration_frames: int | None = None,
    min_gap_frames: int | None = None,
) -> DirectCaptionAlignmentResult:
    """Plan exact direct Caption Track timing moves in sequence frame space."""

    max_distance = _require_non_negative_integer(
        max_distance_frames, "Maximum cut distance"
    )
    timebase = selection.timebase
    minimum_duration_ms, minimum_gap_ms, minimum_duration_frames, minimum_gap_frames = (
        _alignment_safety_values(
            min_duration_ms=min_duration_ms,
            min_gap_ms=min_gap_ms,
            min_duration_frames=min_duration_frames,
            min_gap_frames=min_gap_frames,
        )
    )
    minimum_duration_seconds = (
        minimum_duration_frames * timebase.frame_duration_seconds
        if minimum_duration_frames is not None
        else Fraction(int(minimum_duration_ms or 0), 1000)
    )
    minimum_gap_seconds = (
        minimum_gap_frames * timebase.frame_duration_seconds
        if minimum_gap_frames is not None
        else Fraction(int(minimum_gap_ms or 0), 1000)
    )
    cuts = sorted(
        {
            _require_non_negative_integer(index, "Cut frame index")
            for index in cut_frame_indices
        }
    )
    atoms = _direct_boundary_atoms(selection.items)
    output = list(selection.items)
    planned: dict[int, DirectCaptionAlignmentMove] = {}
    candidates: list[tuple[int, int, int, int]] = []

    for atom in atoms:
        original_frame = timebase.seconds_to_nearest_frame_index(
            atom.original_seconds
        )
        if not cuts:
            planned[atom.order] = DirectCaptionAlignmentMove(
                atom.edges,
                atom.original_seconds,
                original_frame,
                None,
                None,
                None,
                "UNCHANGED",
                "NO_ELIGIBLE_CUTS",
            )
            continue
        cut = min(cuts, key=lambda value: (abs(value - original_frame), value))
        distance = abs(cut - original_frame)
        target = cut * timebase.frame_duration_seconds
        if distance > max_distance:
            planned[atom.order] = DirectCaptionAlignmentMove(
                atom.edges,
                atom.original_seconds,
                original_frame,
                cut,
                target,
                distance,
                "UNCHANGED",
                "NO_CUT_WITHIN_THRESHOLD",
            )
            continue
        candidates.append((distance, atom.order, cut, original_frame))

    used_cuts: set[int] = set()
    atoms_by_order = {atom.order: atom for atom in atoms}
    tolerance_seconds = Fraction(1, 2000)
    for distance, order, cut, original_frame in sorted(candidates):
        atom = atoms_by_order[order]
        target = cut * timebase.frame_duration_seconds
        if distance == 0 and abs(atom.original_seconds - target) <= tolerance_seconds:
            planned[order] = DirectCaptionAlignmentMove(
                atom.edges,
                atom.original_seconds,
                original_frame,
                cut,
                atom.original_seconds,
                0,
                "UNCHANGED",
                "ALREADY_ALIGNED",
            )
            used_cuts.add(cut)
            continue
        if cut in used_cuts:
            planned[order] = DirectCaptionAlignmentMove(
                atom.edges,
                atom.original_seconds,
                original_frame,
                cut,
                target,
                distance,
                "SKIPPED",
                "CUT_ALREADY_USED",
            )
            continue
        candidate_items = list(output)
        _apply_direct_atom(candidate_items, atom, target)
        failure = _direct_safety_failure(
            candidate_items,
            affected_item_positions={edge.item_position for edge in atom.edges},
            minimum_duration_seconds=minimum_duration_seconds,
            minimum_gap_seconds=minimum_gap_seconds,
        )
        if failure:
            planned[order] = DirectCaptionAlignmentMove(
                atom.edges,
                atom.original_seconds,
                original_frame,
                cut,
                target,
                distance,
                "SKIPPED",
                failure,
            )
            continue
        output = candidate_items
        used_cuts.add(cut)
        planned[order] = DirectCaptionAlignmentMove(
            atom.edges,
            atom.original_seconds,
            original_frame,
            cut,
            target,
            distance,
            "APPLIED",
            "ALIGNED_TO_CUT",
        )
    return DirectCaptionAlignmentResult(
        tuple(output), tuple(planned[index] for index in range(len(atoms)))
    )


def align_document_to_cut_frames(
    document: TimedTextDocument,
    *,
    cut_frame_indices: list[int],
    timebase: RationalTimebase,
    max_distance_frames: int,
    min_duration_ms: int | None = None,
    min_gap_ms: int | None = None,
    min_duration_frames: int | None = None,
    min_gap_frames: int | None = None,
) -> CutAlignmentResult:
    """Align nearby cue boundaries to reviewed timeline cut frames.

    ``cut_frame_indices`` are SRT-relative frame indices under ``timebase``.
    Mapping active-sequence times into that SRT-relative space is the caller's
    responsibility.

    A zero-gap end/start pair is one caption-change boundary and moves together.
    Positive-gap edges remain independent. Competing boundaries are considered by
    nearest distance first; one cut cannot attract two independent boundaries.
    If the nearest cut is already used or unsafe, the boundary remains unchanged;
    the planner deliberately does not fall back to a second-nearest cut.
    """

    max_distance = _require_non_negative_integer(
        max_distance_frames, "Maximum cut distance"
    )
    minimum_duration_ms, minimum_gap_ms, minimum_duration_frames, minimum_gap_frames = (
        _alignment_safety_values(
            min_duration_ms=min_duration_ms,
            min_gap_ms=min_gap_ms,
            min_duration_frames=min_duration_frames,
            min_gap_frames=min_gap_frames,
        )
    )
    cuts = sorted(
        {
            _require_non_negative_integer(index, "Cut frame index")
            for index in cut_frame_indices
        }
    )
    document.validate(allow_overlaps=False)
    output = deepcopy(document)
    atoms = _boundary_atoms(document)
    planned: dict[int, CutAlignmentMove] = {}
    candidates: list[tuple[int, int, int, int]] = []

    for atom in atoms:
        original_frame = timebase.nearest_frame_index(atom.original_ms)
        if not cuts:
            planned[atom.order] = CutAlignmentMove(
                atom.edges,
                atom.original_ms,
                original_frame,
                None,
                None,
                None,
                "UNCHANGED",
                "NO_ELIGIBLE_CUTS",
            )
            continue
        cut = min(cuts, key=lambda value: (abs(value - original_frame), value))
        distance = abs(cut - original_frame)
        if distance > max_distance:
            planned[atom.order] = CutAlignmentMove(
                atom.edges,
                atom.original_ms,
                original_frame,
                cut,
                timebase.frame_index_to_ms(cut),
                distance,
                "UNCHANGED",
                "NO_CUT_WITHIN_THRESHOLD",
            )
            continue
        candidates.append((distance, atom.order, cut, original_frame))

    used_cuts: set[int] = set()
    atoms_by_order = {atom.order: atom for atom in atoms}
    for distance, order, cut, original_frame in sorted(candidates):
        atom = atoms_by_order[order]
        target_ms = timebase.frame_index_to_ms(cut)
        if distance == 0 and timebase.is_frame_aligned(atom.original_ms):
            planned[order] = CutAlignmentMove(
                atom.edges,
                atom.original_ms,
                original_frame,
                cut,
                atom.original_ms,
                0,
                "UNCHANGED",
                "ALREADY_ALIGNED",
            )
            used_cuts.add(cut)
            continue
        if cut in used_cuts:
            planned[order] = CutAlignmentMove(
                atom.edges,
                atom.original_ms,
                original_frame,
                cut,
                target_ms,
                distance,
                "SKIPPED",
                "CUT_ALREADY_USED",
            )
            continue

        candidate_document = deepcopy(output)
        _apply_atom(candidate_document, atom, target_ms)
        failure = _safety_failure(
            candidate_document,
            affected_cue_indices={edge.cue_index for edge in atom.edges},
            timebase=timebase,
            min_duration_ms=minimum_duration_ms,
            min_gap_ms=minimum_gap_ms,
            min_duration_frames=minimum_duration_frames,
            min_gap_frames=minimum_gap_frames,
        )
        if failure:
            planned[order] = CutAlignmentMove(
                atom.edges,
                atom.original_ms,
                original_frame,
                cut,
                target_ms,
                distance,
                "SKIPPED",
                failure,
            )
            continue
        output = candidate_document
        used_cuts.add(cut)
        planned[order] = CutAlignmentMove(
            atom.edges,
            atom.original_ms,
            original_frame,
            cut,
            target_ms,
            distance,
            "APPLIED",
            "ALIGNED_TO_CUT",
        )

    output.validate(allow_overlaps=False)
    return CutAlignmentResult(output, tuple(planned[index] for index in range(len(atoms))))


def cut_alignment_report(
    result: CutAlignmentResult,
    selection: CutManifestSelection,
    *,
    status: str,
    input_path: str,
    output_path: str,
    output_written: bool,
    max_distance_frames: int,
    min_duration_ms: int | None,
    min_gap_ms: int | None,
    timing_metadata: str,
    min_duration_frames: int | None = None,
    min_gap_frames: int | None = None,
) -> dict[str, Any]:
    summary = cut_alignment_summary(result)
    counts: dict[str, int] = {}
    reason_counts: dict[str, int] = {}
    movements: list[dict[str, Any]] = []
    for move in result.moves:
        counts[move.status] = counts.get(move.status, 0) + 1
        reason_counts[move.reason] = reason_counts.get(move.reason, 0) + 1
        movement_ms = (
            move.target_ms - move.original_ms if move.target_ms is not None else None
        )
        movement_frames = (
            float(
                Fraction(
                    movement_ms * selection.timebase.numerator,
                    1000 * selection.timebase.denominator,
                )
            )
            if movement_ms is not None
            else None
        )
        movements.append(
            {
                "edges": [
                    {"cueId": edge.cue_id, "edge": edge.edge}
                    for edge in move.edges
                ],
                "originalMs": move.original_ms,
                "originalFrameIndex": move.original_frame_index,
                "cutFrameIndex": move.cut_frame_index,
                "targetMs": move.target_ms,
                "distanceFrames": move.distance_frames,
                "movementMs": movement_ms,
                "movementFrames": movement_frames,
                "status": move.status,
                "reason": move.reason,
            }
        )
    return {
        "version": 1,
        "status": status,
        "input": input_path,
        "output": output_path,
        "outputWritten": output_written,
        "cueCount": len(result.document.cues),
        "movementCounts": counts,
        "movementReasonCounts": reason_counts,
        **summary,
        "settings": {
            "maximumDistanceFrames": max_distance_frames,
            "minimumCaptionDurationMs": min_duration_ms,
            "minimumCaptionGapMs": min_gap_ms,
            "minimumCaptionDurationFrames": min_duration_frames,
            "minimumCaptionGapFrames": min_gap_frames,
        },
        "sequenceMapping": selection.to_report_dict(),
        "timingMetadata": timing_metadata,
        "movements": movements,
    }


def cut_alignment_summary(result: CutAlignmentResult) -> dict[str, int]:
    return {
        "movedCount": sum(move.status == "APPLIED" for move in result.moves),
        "alreadyAlignedCount": sum(
            move.reason == "ALREADY_ALIGNED" for move in result.moves
        ),
        "outsideSnapRangeCount": sum(
            move.reason == "NO_CUT_WITHIN_THRESHOLD" for move in result.moves
        ),
        "reviewSkipCount": sum(move.status == "SKIPPED" for move in result.moves),
        "noEligibleCutBoundaryCount": sum(
            move.reason == "NO_ELIGIBLE_CUTS" for move in result.moves
        ),
    }


def direct_caption_alignment_plan(
    result: DirectCaptionAlignmentResult,
    caption_selection: CaptionTimingSelection,
    cut_selection: CutManifestSelection,
    *,
    max_distance_frames: int,
    min_duration_ms: int | None,
    min_gap_ms: int | None,
    min_duration_frames: int | None = None,
    min_gap_frames: int | None = None,
) -> dict[str, Any]:
    if caption_selection.sequence_name != cut_selection.sequence_name:
        raise ValueError(
            "Caption Track and reference cuts must come from the same active sequence."
        )
    if caption_selection.timebase != cut_selection.timebase:
        raise ValueError(
            "Caption Track and reference cuts must use the same sequence frame rate."
        )
    moved_count = sum(move.status == "APPLIED" for move in result.moves)
    already_count = sum(move.reason == "ALREADY_ALIGNED" for move in result.moves)
    outside_count = sum(
        move.reason == "NO_CUT_WITHIN_THRESHOLD" for move in result.moves
    )
    review_count = sum(move.status == "SKIPPED" for move in result.moves)
    if not cut_selection.sequence_cut_frame_indices:
        status = "NO_ELIGIBLE_CUTS"
    elif moved_count == 0:
        status = "NO_SAFE_CHANGES" if review_count else "NO_CHANGES"
    else:
        status = "READY_WITH_WARNINGS" if review_count else "READY"

    actions: list[dict[str, Any]] = []
    movements: list[dict[str, Any]] = []
    for move in result.moves:
        target_text = (
            fraction_seconds_text(move.target_seconds)
            if move.target_seconds is not None
            else None
        )
        movement_frames = (
            float(
                (move.target_seconds - move.original_seconds)
                * caption_selection.timebase.frames_per_second
            )
            if move.target_seconds is not None
            else None
        )
        movement = {
            "edges": [],
            "originalSeconds": fraction_seconds_text(move.original_seconds),
            "originalFrameIndex": move.original_frame_index,
            "cutFrameIndex": move.cut_frame_index,
            "targetSeconds": target_text,
            "distanceFrames": move.distance_frames,
            "movementFrames": movement_frames,
            "status": move.status,
            "reason": move.reason,
        }
        for edge in move.edges:
            item = caption_selection.items[edge.item_position]
            edge_value = {
                "itemId": edge.item_id,
                "itemIndex": item.item_index,
                "edge": edge.edge,
            }
            movement["edges"].append(edge_value)
            if move.status == "APPLIED":
                actions.append(
                    {
                        **edge_value,
                        "originalSeconds": fraction_seconds_text(
                            move.original_seconds
                        ),
                        "targetSeconds": target_text,
                        "targetFrameIndex": move.cut_frame_index,
                    }
                )
        movements.append(movement)

    def item_json(item: CaptionTimingItem) -> dict[str, Any]:
        return {
            "id": item.item_id,
            "index": item.item_index,
            "startSeconds": fraction_seconds_text(item.start_seconds),
            "endSeconds": fraction_seconds_text(item.end_seconds),
        }

    return {
        "version": 1,
        "mode": "DIRECT_CAPTION_TRACK",
        "status": status,
        "sequenceName": caption_selection.sequence_name,
        "timebase": caption_selection.timebase.to_json(),
        "captionTrack": {
            "index": caption_selection.caption_track_index,
            "label": f"Caption {caption_selection.caption_track_index + 1}",
            "name": caption_selection.caption_track_name,
        },
        "referenceTrack": cut_selection.to_report_dict()["referenceTrack"],
        "eligibleCutCount": len(cut_selection.sequence_cut_frame_indices),
        "settings": {
            "maximumDistanceFrames": max_distance_frames,
            "minimumCaptionDurationMs": min_duration_ms,
            "minimumCaptionGapMs": min_gap_ms,
            "minimumCaptionDurationFrames": min_duration_frames,
            "minimumCaptionGapFrames": min_gap_frames,
        },
        "movedCount": moved_count,
        "alreadyAlignedCount": already_count,
        "outsideSnapRangeCount": outside_count,
        "reviewSkipCount": review_count,
        "actions": actions,
        "originalItems": [item_json(item) for item in caption_selection.items],
        "expectedItems": [item_json(item) for item in result.items],
        "movements": movements,
        "skippedTimelineAdjacencies": [
            item
            for item in cut_selection.skipped_adjacencies
            if item.get("reason") != "CUT_BEFORE_SRT_ZERO"
        ],
    }


def verify_direct_caption_alignment(
    plan_value: Any,
    actual_selection: CaptionTimingSelection,
    *,
    tolerance_seconds: Fraction = Fraction(1, 1_000_000),
) -> dict[str, Any]:
    plan = _require_mapping(plan_value, "Direct Caption Track alignment plan")
    if plan.get("version") != 1 or plan.get("mode") != "DIRECT_CAPTION_TRACK":
        raise ValueError("Direct Caption Track alignment plan is not supported.")
    if str(plan.get("sequenceName", "")) != actual_selection.sequence_name:
        raise ValueError("The active sequence changed after the alignment preview.")
    track = _require_mapping(plan.get("captionTrack"), "Planned Caption Track")
    if track.get("index") != actual_selection.caption_track_index:
        raise ValueError("The selected Caption Track changed after the alignment preview.")
    if RationalTimebase.from_json(plan.get("timebase")) != actual_selection.timebase:
        raise ValueError("The sequence frame rate changed after the alignment preview.")
    expected_raw = plan.get("expectedItems")
    if not isinstance(expected_raw, list):
        raise ValueError("Direct Caption Track alignment plan has no expected item list.")

    mismatches: list[dict[str, Any]] = []
    if len(expected_raw) != len(actual_selection.items):
        mismatches.append(
            {
                "code": "ITEM_COUNT_CHANGED",
                "expected": len(expected_raw),
                "actual": len(actual_selection.items),
            }
        )
    for position, expected_raw_item in enumerate(expected_raw):
        if position >= len(actual_selection.items):
            break
        expected = _require_mapping(
            expected_raw_item, f"Expected Caption Track item {position + 1}"
        )
        actual = actual_selection.items[position]
        if expected.get("id") != actual.item_id or expected.get("index") != actual.item_index:
            mismatches.append(
                {
                    "code": "ITEM_IDENTITY_CHANGED",
                    "position": position,
                    "expectedId": expected.get("id"),
                    "actualId": actual.item_id,
                    "expectedIndex": expected.get("index"),
                    "actualIndex": actual.item_index,
                }
            )
            continue
        for edge, actual_value in (
            ("start", actual.start_seconds),
            ("end", actual.end_seconds),
        ):
            expected_value = _require_non_negative_fraction(
                expected.get(f"{edge}Seconds"),
                f"Expected item {position + 1} {edge}",
            )
            if abs(expected_value - actual_value) > tolerance_seconds:
                mismatches.append(
                    {
                        "code": "TIMING_NOT_APPLIED",
                        "position": position,
                        "itemId": actual.item_id,
                        "edge": edge.upper(),
                        "expectedSeconds": fraction_seconds_text(expected_value),
                        "actualSeconds": fraction_seconds_text(actual_value),
                    }
                )
    return {
        "version": 1,
        "mode": "DIRECT_CAPTION_TRACK",
        "status": "VERIFIED" if not mismatches else "VERIFICATION_FAILED",
        "sequenceName": actual_selection.sequence_name,
        "captionTrack": {
            "index": actual_selection.caption_track_index,
            "label": f"Caption {actual_selection.caption_track_index + 1}",
            "name": actual_selection.caption_track_name,
        },
        "plannedMoveCount": plan.get("movedCount", 0),
        "verifiedItemCount": len(actual_selection.items),
        "mismatches": mismatches,
    }


def direct_caption_alignment_overview(report: Mapping[str, Any]) -> str:
    status = str(report.get("status", ""))
    track = _require_mapping(report.get("captionTrack"), "Caption Track")
    track_text = str(track.get("label", "Caption Track"))
    if track.get("name"):
        track_text += f" — {track.get('name')}"
    if status == "VERIFIED":
        return (
            f"Aligned {report.get('plannedMoveCount', 0)} caption change(s) on "
            f"{track_text}. Premiere timing was read back and verified."
        )
    if status == "VERIFICATION_FAILED":
        return (
            "Premiere did not apply every planned caption timing change. The "
            "Caption Track may be locked or Premiere may have refused an edit. "
            "Use Undo once if any timing changed, then review the detailed report."
        )
    if status == "NO_ELIGIBLE_CUTS":
        return "No adjacent clip cuts were found on the chosen video track."
    if status == "NO_SAFE_CHANGES":
        return "Caption changes were near cuts, but none could be moved safely."
    if status == "NO_CHANGES":
        return "No caption changes need to move with the selected snap range."
    return (
        f"Ready to move {report.get('movedCount', 0)} caption change(s) on "
        f"{track_text}. Review and confirm before changing the timeline."
    )


def _minimum_setting_text(settings: Mapping[str, Any], stem: str) -> str:
    frames = settings.get(f"{stem}Frames")
    if frames is not None:
        return f"{frames} frame(s)"
    return f"{settings.get(f'{stem}Ms')} ms"


def direct_caption_alignment_report_markdown(report: Mapping[str, Any]) -> str:
    track = _require_mapping(report.get("captionTrack"), "Caption Track")
    lines = [
        "# Direct Caption Track alignment report",
        "",
        f"- Status: `{report.get('status', '')}`",
        f"- Active sequence: {report.get('sequenceName') or 'Unnamed sequence'}",
        (
            f"- Caption Track: {track.get('label', '')}"
            + (f" — {track.get('name')}" if track.get("name") else "")
        ),
    ]
    if "movedCount" in report:
        reference = _require_mapping(report.get("referenceTrack"), "Reference track")
        settings = _require_mapping(report.get("settings"), "Alignment settings")
        lines.extend(
            [
                (
                    f"- Reference video track: {reference.get('label', '')}"
                    + (f" — {reference.get('name')}" if reference.get("name") else "")
                ),
                f"- Eligible cuts found: {report.get('eligibleCutCount', 0)}",
                f"- Snap range: {settings.get('maximumDistanceFrames')} frame(s)",
                "- Minimum caption duration preserved: "
                + _minimum_setting_text(settings, "minimumCaptionDuration"),
                "- Minimum gap preserved: "
                + _minimum_setting_text(settings, "minimumCaptionGap"),
                f"- Caption changes proposed: {report.get('movedCount', 0)}",
                f"- Caption changes already on a cut: {report.get('alreadyAlignedCount', 0)}",
                f"- Caption changes outside the snap range: {report.get('outsideSnapRangeCount', 0)}",
                f"- Caption changes skipped for review: {report.get('reviewSkipCount', 0)}",
                "",
                "## Caption changes",
                "",
            ]
        )
        movements = report.get("movements")
        if isinstance(movements, list) and movements:
            for move in movements:
                edges = ", ".join(
                    f"{edge.get('itemId')}.{str(edge.get('edge', '')).lower()}"
                    for edge in move.get("edges", [])
                )
                target = move.get("targetSeconds")
                lines.append(
                    f"- **{move.get('status')} — {move.get('reason')}**: {edges}; "
                    f"{move.get('originalSeconds')} s → "
                    + (f"{target} s" if target is not None else "no cut")
                )
        else:
            lines.append("No caption boundaries were available for alignment.")
    else:
        mismatches = report.get("mismatches")
        lines.extend(["", "## Post-change verification", ""])
        if isinstance(mismatches, list) and mismatches:
            for mismatch in mismatches:
                if mismatch.get("code") == "TIMING_NOT_APPLIED":
                    lines.append(
                        f"- `{mismatch.get('code')}`: {mismatch.get('itemId')}."
                        f"{str(mismatch.get('edge', '')).lower()} expected "
                        f"{mismatch.get('expectedSeconds')} s, found "
                        f"{mismatch.get('actualSeconds')} s."
                    )
                else:
                    lines.append(f"- `{mismatch.get('code')}`")
        else:
            lines.append("Every Caption Track item matched the reviewed timing plan.")
    return "\n".join(lines) + "\n"


def cut_alignment_report_markdown(report: Mapping[str, Any]) -> str:
    mapping = _require_mapping(report.get("sequenceMapping"), "Sequence mapping")
    track = _require_mapping(mapping.get("referenceTrack"), "Reference track")
    zero = _require_mapping(mapping.get("srtZeroMapping"), "SRT zero mapping")
    settings = _require_mapping(report.get("settings"), "Alignment settings")
    lines = [
        "# Caption alignment report",
        "",
        f"- Status: `{report.get('status', '')}`",
        f"- Source SRT: `{report.get('input', '')}`",
        f"- New SRT: `{report.get('output', '')}`",
        f"- New SRT written: {'yes' if report.get('outputWritten') else 'no'}",
        f"- Active sequence: {mapping.get('sequenceName') or 'Unnamed sequence'}",
        (
            f"- Reference video track: {track.get('label', '')}"
            + (f" — {track.get('name')}" if track.get("name") else "")
        ),
        (
            "- Time mapping: SRT 00:00:00,000 corresponds to sequence "
            f"time {zero.get('sequenceSeconds')} s (frame {zero.get('sequenceFrame')})"
        ),
        f"- Eligible cuts found: {mapping.get('eligibleCutCount', 0)}",
        f"- Snap range: {settings.get('maximumDistanceFrames')} frame(s)",
        "- Minimum caption duration preserved: "
        + _minimum_setting_text(settings, "minimumCaptionDuration"),
        "- Minimum gap preserved: "
        + _minimum_setting_text(settings, "minimumCaptionGap"),
        f"- Caption changes moved: {report.get('movedCount', 0)}",
        f"- Caption changes already on a cut: {report.get('alreadyAlignedCount', 0)}",
        f"- Caption changes outside the snap range: {report.get('outsideSnapRangeCount', 0)}",
        f"- Caption changes within range but skipped for review: {report.get('reviewSkipCount', 0)}",
        f"- Timing metadata: {report.get('timingMetadata', '')}",
        "",
        "## Caption changes",
        "",
    ]
    movements = report.get("movements")
    if not isinstance(movements, list) or not movements:
        lines.append("No caption boundaries were available for alignment.")
    else:
        for move in movements:
            edges = ", ".join(
                f"{edge.get('cueId')}.{str(edge.get('edge', '')).lower()}"
                for edge in move.get("edges", [])
            )
            target = move.get("targetMs")
            target_text = f"{target} ms" if target is not None else "no cut"
            lines.append(
                f"- **{move.get('status')} — {move.get('reason')}**: {edges}; "
                f"{move.get('originalMs')} ms → {target_text}"
                + (
                    f" ({move.get('movementMs'):+d} ms, "
                    f"{move.get('movementFrames'):+.3f} frames)"
                    if move.get("movementMs") is not None
                    else ""
                )
            )
    skipped = mapping.get("skippedAdjacencies")
    if isinstance(skipped, list) and skipped:
        lines.extend(["", "## Timeline boundaries not used", ""])
        for item in skipped:
            details: list[str] = []
            if item.get("leftItemId"):
                details.append(
                    f"{item.get('leftItemId')} ends at sequence frame "
                    f"{item.get('leftEndSequenceFrame')}"
                )
            if item.get("rightItemId"):
                details.append(
                    f"{item.get('rightItemId')} starts at sequence frame "
                    f"{item.get('rightStartSequenceFrame')}"
                )
            if item.get("sequenceFrame") is not None:
                details.append(f"sequence frame {item.get('sequenceFrame')}")
            lines.append(
                f"- `{item.get('reason')}`"
                + (f": {'; '.join(details)}" if details else "")
            )
    return "\n".join(lines) + "\n"


def cut_alignment_overview(report: Mapping[str, Any]) -> str:
    status = str(report.get("status", ""))
    if report.get("outputWritten"):
        lead = f"Created aligned SRT: {report.get('output', '')}"
    elif status == "NO_ELIGIBLE_CUTS":
        lead = "No adjacent clip cuts were found on the chosen video track. No SRT was created."
    elif status == "SKIPPED_EXISTS":
        lead = "The output SRT already exists and was left unchanged."
    elif status == "NO_SAFE_CHANGES":
        lead = "Caption changes were near cuts, but none could be moved safely. No SRT was created."
    else:
        lead = "No caption changes were close enough to move. No SRT was created."
    return (
        f"{lead}\n\n"
        f"Moved: {report.get('movedCount', 0)}\n"
        f"Already on a cut: {report.get('alreadyAlignedCount', 0)}\n"
        f"Outside the selected snap range: {report.get('outsideSnapRangeCount', 0)}\n"
        f"Within range but skipped for review: {report.get('reviewSkipCount', 0)}"
    )
