#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import math
import os
import sys
from pathlib import Path
from typing import Any


SCRIPT_DIR = Path(__file__).resolve().parent
sys.dont_write_bytecode = True
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from analysis import (
    analysis_review_form,
    analysis_settings_report,
    resolve_analysis,
    standard_analysis_profile,
    validate_frame_alignment,
)
from caption_profile import CaptionProfile
from condensation import (
    condensation_preparation_report,
    condensation_report_markdown,
    merge_condensation_responses,
    plan_condensation_requests,
    prepare_condensation,
)
from cut_alignment import (
    align_caption_timing_to_cut_frames,
    align_document_to_cut_frames,
    caption_timing_selection,
    cut_alignment_report,
    cut_alignment_report_markdown,
    cut_alignment_overview,
    cut_alignment_summary,
    cut_manifest_selection,
    direct_caption_alignment_overview,
    direct_caption_alignment_plan,
    direct_caption_alignment_report_markdown,
    verify_direct_caption_alignment,
)
from portable_profile import (
    PortableCaptionProfile,
    discover_profiles,
    profile_filename,
    read_portable_profile,
    write_portable_profile_atomic,
)
from operations import (
    Finding,
    form_sentence_groups,
    global_reflow,
    line_only_reflow,
    validate_document,
)
from reporting import (
    analysis_metrics,
    analysis_overview,
    findings_to_json,
    findings_to_markdown,
    review_marker_document,
    write_json,
)
from reference_caption_builder import (
    build_reference_caption_document,
    reference_caption_build_report,
    reference_caption_build_report_markdown,
)
from reference_sequence_alignment import (
    align_reference_script_to_sequence_manifest,
    reference_sequence_report,
    reference_sequence_report_markdown,
)
from reference_script_alignment import reference_review_marker_document
from sequence import sequence_manifest_to_document, sequence_report_markdown
from srt_io import SrtParseError, parse_srt, serialize_srt
from timing_sidecar import (
    apply_sidecar,
    create_sidecar,
    expected_sidecar_path,
    install_output_pair,
    load_and_validate_sidecar,
)
from timed_text import TimingFidelity
from timebase import COMMON_FRAME_RATES, RationalTimebase
from language import normalize_language_tag
from transcript import detect_transcript_language, transcript_to_document
from translation import (
    LinguisticContext,
    merge_translation_responses,
    plan_translation_requests,
    parse_language_list,
    parse_language_specs,
    parse_protected_terms,
    resolve_document_protected_terms,
    safe_output_stem,
    translated_output_name,
)
from workflow_profiles import (
    add_measurement_results,
    resolve_condensation_profile,
    resolve_reference_profile,
    resolve_reflow_profile,
    standard_workflow_profile,
    workflow_review_form,
    workflow_settings_report,
)


def ab_path(value: str) -> Path:
    text = str(value)
    if text.startswith("::HOME::"):
        text = str(Path.home()) + text[len("::HOME::") :]
    elif text.startswith("file://"):
        text = text[7:]
    elif text.startswith("file:"):
        text = text[5:]
    return Path(text).expanduser()


def load_json(path: Path) -> Any:
    return json.loads(path.read_text(encoding="utf-8-sig"))


def boolean_argument(value: Any) -> bool:
    return str(value).strip().casefold() in {"1", "true", "yes", "on"}


def same_path(left: Path, right: Path) -> bool:
    try:
        if left.exists() and right.exists() and os.path.samefile(left, right):
            return True
    except OSError:
        pass
    return os.path.normcase(str(left.resolve())) == os.path.normcase(str(right.resolve()))


def profile_from_args(args: argparse.Namespace) -> CaptionProfile:
    profile = CaptionProfile(
        max_units_per_line=args.max_units_per_line,
        max_lines=args.max_lines,
        max_cps=args.max_cps,
        min_duration_ms=args.min_duration_ms,
        max_duration_ms=args.max_duration_ms,
        preferred_gap_ms=args.preferred_gap_ms,
    )
    profile.validate()
    return profile


def profile_for_target(args: argparse.Namespace, target_language: str) -> CaptionProfile:
    base = profile_from_args(args)
    language = normalize_language_tag(target_language).split("-", 1)[0]
    if language not in {"ja", "ko", "zh"}:
        return base
    units = int(getattr(args, "cjk_max_units_per_line", 0))
    cps = float(getattr(args, "cjk_max_cps", 0))
    profile = CaptionProfile(
        max_units_per_line=units if units > 0 else base.max_units_per_line,
        max_lines=base.max_lines,
        max_cps=cps if cps > 0 else base.max_cps,
        min_duration_ms=base.min_duration_ms,
        max_duration_ms=base.max_duration_ms,
        preferred_gap_ms=base.preferred_gap_ms,
    )
    profile.validate()
    return profile


def add_profile_arguments(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--max-units-per-line", type=int, default=42)
    parser.add_argument("--max-lines", type=int, default=2)
    parser.add_argument("--max-cps", type=float, default=17.0)
    parser.add_argument("--min-duration-ms", type=int, default=1000)
    parser.add_argument("--max-duration-ms", type=int, default=7000)
    parser.add_argument("--preferred-gap-ms", type=int, default=80)


def add_workflow_profile_arguments(
    parser: argparse.ArgumentParser,
    *,
    include_cps: bool,
) -> None:
    parser.add_argument("--caption-profile", default="")
    parser.add_argument("--max-units-per-line", type=int)
    parser.add_argument("--max-lines", type=int)
    if include_cps:
        parser.add_argument("--max-cps", type=float)
    parser.add_argument("--min-duration-ms", type=int)
    parser.add_argument("--max-duration-ms", type=int)


def _selected_workflow_profile(args: argparse.Namespace) -> PortableCaptionProfile:
    profile_path = str(getattr(args, "caption_profile", "") or "").strip()
    return (
        read_portable_profile(ab_path(profile_path))
        if profile_path
        else standard_workflow_profile()
    )


def _workflow_reviewed_values(
    args: argparse.Namespace,
    *,
    include_cps: bool,
) -> dict[str, Any]:
    values = {
        "layout.maxUnitsPerLine": getattr(args, "max_units_per_line", None),
        "layout.maxLines": getattr(args, "max_lines", None),
        "timing.minDurationMs": getattr(args, "min_duration_ms", None),
        "timing.maxDurationMs": getattr(args, "max_duration_ms", None),
    }
    if include_cps:
        values["readability.maxCps"] = getattr(args, "max_cps", None)
    return values


def _workflow_profile_path(args: argparse.Namespace) -> Path | None:
    value = str(getattr(args, "caption_profile", "") or "").strip()
    return ab_path(value) if value else None


def _resolved_reflow_from_args(args: argparse.Namespace):
    return resolve_reflow_profile(
        _selected_workflow_profile(args),
        reviewed_values=_workflow_reviewed_values(args, include_cps=False),
    )


def _resolved_condensation_from_args(args: argparse.Namespace):
    return resolve_condensation_profile(
        _selected_workflow_profile(args),
        reviewed_values=_workflow_reviewed_values(args, include_cps=True),
    )


def _resolved_reference_from_args(args: argparse.Namespace):
    return resolve_reference_profile(
        _selected_workflow_profile(args),
        reviewed_values=_workflow_reviewed_values(args, include_cps=True),
    )


def add_analysis_profile_arguments(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--caption-profile")
    parser.add_argument("--max-units-per-line")
    parser.add_argument("--max-lines")
    parser.add_argument("--max-cps")
    parser.add_argument("--min-duration-ms")
    parser.add_argument("--max-duration-ms")
    parser.add_argument("--gap-mode")
    parser.add_argument("--min-gap-ms")
    parser.add_argument("--frame-rate")
    parser.add_argument("--custom-frame-rate-numerator")
    parser.add_argument("--custom-frame-rate-denominator")
    parser.add_argument("--min-timestamp-gap-frames")
    parser.add_argument("--align-to-frame-grid")


def _analysis_timebase_from_args(args: argparse.Namespace) -> RationalTimebase | None:
    frame_rate = str(args.frame_rate or "").strip()
    if not frame_rate:
        return None
    if frame_rate == "Custom":
        if (
            _optional_int(
                args.custom_frame_rate_numerator,
                "Custom frame-rate numerator",
                minimum=1,
            )
            is None
            or _optional_int(
                args.custom_frame_rate_denominator,
                "Custom frame-rate denominator",
                minimum=1,
            )
            is None
        ):
            raise ValueError(
                "A custom delivery frame rate requires a numerator and denominator."
            )
        return RationalTimebase(
            int(args.custom_frame_rate_numerator),
            int(args.custom_frame_rate_denominator),
        )
    if frame_rate == "Not specified":
        return None
    return RationalTimebase.from_label(frame_rate)


def _analysis_profile_and_resolution(args: argparse.Namespace):
    selected_profile = (
        read_portable_profile(ab_path(args.caption_profile))
        if args.caption_profile
        else standard_analysis_profile()
    )
    reviewed_values = {
        "layout.maxUnitsPerLine": _optional_int(
            args.max_units_per_line,
            "Maximum text length per line",
            minimum=1,
        ),
        "layout.maxLines": _optional_int(
            args.max_lines,
            "Maximum lines per caption",
            minimum=1,
        ),
        "readability.maxCps": _optional_float(
            args.max_cps,
            "Maximum reading speed",
            minimum_exclusive=0,
        ),
        "timing.minDurationMs": _optional_int(
            args.min_duration_ms,
            "Minimum caption duration",
            minimum=1,
        ),
        "timing.maxDurationMs": _optional_int(
            args.max_duration_ms,
            "Maximum caption duration",
            minimum=1,
        ),
    }
    suppressed_profile_keys: set[str] = set()
    gap_mode = str(args.gap_mode or "").strip()
    if not gap_mode and str(args.min_gap_ms or "").strip():
        gap_mode = "Milliseconds"
    if not gap_mode and str(args.min_timestamp_gap_frames or "").strip():
        gap_mode = "Frames"
    if gap_mode == "General editing preference (80 ms)":
        suppressed_profile_keys.update(
            {"timing.minGapMs", "timing.minTimestampGapFrames"}
        )
    elif gap_mode in {
        "Milliseconds",
        "Required gap in milliseconds (enter value next)",
    }:
        reviewed_values["timing.minGapMs"] = _optional_int(
            args.min_gap_ms,
            "Minimum caption gap",
            minimum=0,
        )
    elif gap_mode in {
        "Frames",
        "Required gap in frames (enter value next)",
    }:
        reviewed_values["timing.minTimestampGapFrames"] = _optional_int(
            args.min_timestamp_gap_frames,
            "Minimum caption gap in frames",
            minimum=0,
        )
    elif gap_mode:
        raise ValueError(f"Unknown caption-gap mode: {gap_mode}.")
    timebase = _analysis_timebase_from_args(args)
    if timebase is not None:
        reviewed_values["timing.timebase"] = timebase.to_json()
    elif str(args.frame_rate or "").strip() == "Not specified":
        suppressed_profile_keys.update(
            {
                "timing.timebase",
                "timing.minTimestampGapFrames",
                "timing.alignToFrameGrid",
            }
        )
    if args.align_to_frame_grid is not None:
        reviewed_values["timing.alignToFrameGrid"] = _optional_boolean(
            args.align_to_frame_grid,
            "Frame alignment",
        )
    return resolve_analysis(
        selected_profile,
        reviewed_values=reviewed_values,
        suppressed_profile_keys=suppressed_profile_keys,
    )


def _optional_int(value: str | None, field: str, *, minimum: int) -> int | None:
    if value is None or not str(value).strip():
        return None
    try:
        parsed = int(str(value).strip())
    except ValueError as exc:
        raise ValueError(f"{field} must be a whole number.") from exc
    if parsed < minimum:
        raise ValueError(f"{field} must be at least {minimum}.")
    return parsed


def _optional_float(
    value: str | None,
    field: str,
    *,
    minimum_exclusive: float,
) -> float | None:
    if value is None or not str(value).strip():
        return None
    try:
        parsed = float(str(value).strip())
    except ValueError as exc:
        raise ValueError(f"{field} must be a number.") from exc
    if not math.isfinite(parsed) or parsed <= minimum_exclusive:
        raise ValueError(f"{field} must be greater than {minimum_exclusive:g}.")
    return parsed


def _optional_boolean(value: str | None, field: str) -> bool | None:
    if value is None or not str(value).strip():
        return None
    normalized = str(value).strip().casefold()
    if normalized in {
        "1",
        "true",
        "yes",
        "on",
        "check frame boundaries",
        "report caption timings that fall between frames",
    }:
        return True
    if normalized in {
        "0",
        "false",
        "no",
        "off",
        "do not check frame boundaries",
    }:
        return False
    raise ValueError(f"{field} must be true or false.")


def _form_value(settings: dict[str, Any], key: str) -> Any:
    return settings[key] if key in settings else ""


def _timebase_form_values(settings: dict[str, Any]) -> tuple[str, Any, Any]:
    raw_timebase = settings.get("timing.timebase")
    if raw_timebase is None:
        return "Not specified", "", ""
    timebase = RationalTimebase.from_json(raw_timebase)
    for label, values in COMMON_FRAME_RATES.items():
        if timebase == RationalTimebase(*values):
            return label, "", ""
    return "Custom", timebase.numerator, timebase.denominator


def _gap_mode_form_value(settings: dict[str, Any]) -> str:
    if "timing.minTimestampGapFrames" in settings:
        return "Frames"
    if "timing.minGapMs" in settings:
        return "Milliseconds"
    return "Not specified"


def _alignment_form_value(settings: dict[str, Any]) -> str:
    value = settings.get("timing.alignToFrameGrid")
    if value is True:
        return "Adjust to frame boundaries"
    if value is False:
        return "Do not adjust to frame boundaries"
    return "Not specified"


def command_profile_discover(args: argparse.Namespace) -> int:
    folder = ab_path(args.folder)
    profiles, issues = discover_profiles(folder)
    name_counts: dict[str, int] = {}
    for item in profiles:
        key = item.profile.name.casefold()
        name_counts[key] = name_counts.get(key, 0) + 1
    entries = []
    for item in profiles:
        duplicate = name_counts[item.profile.name.casefold()] > 1
        label = (
            f"{item.profile.name} ({item.path.name})"
            if duplicate
            else item.profile.name
        )
        if args.label_prefix:
            label = f"{args.label_prefix}{label}"
        entries.append(
            {
                "label": label,
                "name": item.profile.name,
                "path": str(item.path),
            }
        )
    result = {
        "version": 1,
        "folder": str(folder),
        "profiles": entries,
        "issues": [
            {"path": str(issue.path), "message": issue.message} for issue in issues
        ],
    }
    write_json(ab_path(args.output), result)
    if args.names_output:
        names_output = ab_path(args.names_output)
        names_output.parent.mkdir(parents=True, exist_ok=True)
        names_output.write_text(
            "\n".join(item["label"] for item in entries),
            encoding="utf-8",
            newline="\n",
        )
    if args.paths_output:
        paths_output = ab_path(args.paths_output)
        paths_output.parent.mkdir(parents=True, exist_ok=True)
        paths_output.write_text(
            "\n".join(item["path"] for item in entries),
            encoding="utf-8",
            newline="\n",
        )
    if args.issues_message_output:
        issues_message_output = ab_path(args.issues_message_output)
        issues_message_output.parent.mkdir(parents=True, exist_ok=True)
        issue_lines: list[str] = []
        if issues:
            count = len(issues)
            noun = "profile file" if count == 1 else "profile files"
            issue_lines.extend(
                [
                    "",
                    "",
                    f"{count} saved caption {noun} could not be read and "
                    "is not available in this list:"
                    if count == 1
                    else f"{count} saved caption {noun} could not be read and "
                    "are not available in this list:",
                ]
            )
            for issue in issues[:3]:
                issue_lines.append(f"- {issue.path.name}: {issue.message}")
            if count > 3:
                issue_lines.append(f"- …and {count - 3} more.")
            issue_lines.append(
                "Review or replace these files in the Caption Profiles folder "
                "to use them again."
            )
        issues_message_output.write_text(
            "\n".join(issue_lines),
            encoding="utf-8",
            newline="\n",
        )
    print(
        f"RESULT|status=DISCOVERED|profiles={len(entries)}|issues={len(issues)}"
        f"|output={ab_path(args.output)}"
    )
    return 0


def command_profile_inspect(args: argparse.Namespace) -> int:
    source = ab_path(args.input)
    profile = read_portable_profile(source)
    settings = profile.settings
    frame_rate, custom_numerator, custom_denominator = _timebase_form_values(settings)
    result = {
        "version": 1,
        "sourcePath": str(source),
        "profile": profile.to_json(),
        "form": {
            "name": profile.name,
            "notes": profile.notes,
            "maxUnitsPerLine": _form_value(settings, "layout.maxUnitsPerLine"),
            "maxLines": _form_value(settings, "layout.maxLines"),
            "maxCps": _form_value(settings, "readability.maxCps"),
            "minDurationMs": _form_value(settings, "timing.minDurationMs"),
            "maxDurationMs": _form_value(settings, "timing.maxDurationMs"),
            "minGapMs": _form_value(settings, "timing.minGapMs"),
            "gapMode": _gap_mode_form_value(settings),
            "frameRate": frame_rate,
            "customFrameRateNumerator": custom_numerator,
            "customFrameRateDenominator": custom_denominator,
            "minTimestampGapFrames": _form_value(
                settings, "timing.minTimestampGapFrames"
            ),
            "alignToFrameGrid": _alignment_form_value(settings),
        },
    }
    write_json(ab_path(args.output), result)
    if args.form_folder:
        form_folder = ab_path(args.form_folder)
        form_folder.mkdir(parents=True, exist_ok=True)
        for field, value in result["form"].items():
            text = str(value)
            (form_folder / f"{field}.txt").write_text(
                text,
                encoding="utf-8",
                newline="\n",
            )
    print(f"RESULT|status=VALID|name={profile.name}|output={ab_path(args.output)}")
    return 0


def command_analysis_prefill(args: argparse.Namespace) -> int:
    profile_path = str(args.caption_profile or "").strip()
    selected_profile = (
        read_portable_profile(ab_path(profile_path))
        if profile_path
        else standard_analysis_profile()
    )
    form = analysis_review_form(resolve_analysis(selected_profile))
    result = {
        "version": 1,
        "profilePath": str(ab_path(profile_path)) if profile_path else "",
        "form": form,
    }
    write_json(ab_path(args.output), result)
    form_folder = ab_path(args.form_folder)
    form_folder.mkdir(parents=True, exist_ok=True)
    for field, value in form.items():
        (form_folder / f"{field}.txt").write_text(
            str(value),
            encoding="utf-8",
            newline="\n",
        )
    print(
        f"RESULT|status=READY|profile={form['profileName']}"
        f"|output={ab_path(args.output)}"
    )
    return 0


def command_workflow_profile_prefill(args: argparse.Namespace) -> int:
    selected_profile = _selected_workflow_profile(args)
    if args.workflow == "reflow":
        resolution = resolve_reflow_profile(selected_profile)
    elif args.workflow == "condensation":
        resolution = resolve_condensation_profile(selected_profile)
    elif args.workflow == "reference":
        resolution = resolve_reference_profile(selected_profile)
    else:
        raise ValueError(f"Unknown caption workflow: {args.workflow}.")
    form = workflow_review_form(resolution)
    result = {
        "version": 1,
        "workflow": args.workflow,
        "profilePath": (
            str(_workflow_profile_path(args))
            if _workflow_profile_path(args) is not None
            else ""
        ),
        "form": form,
        "captionSettings": workflow_settings_report(
            resolution,
            profile_path=_workflow_profile_path(args),
        ),
    }
    write_json(ab_path(args.output), result)
    form_folder = ab_path(args.form_folder)
    form_folder.mkdir(parents=True, exist_ok=True)
    for field, value in form.items():
        (form_folder / f"{field}.txt").write_text(
            str(value),
            encoding="utf-8",
            newline="\n",
        )
    print(
        f"RESULT|status=READY|workflow={args.workflow}"
        f"|profile={form['profileName']}|output={ab_path(args.output)}"
    )
    return 0


def _profile_from_editor_args(args: argparse.Namespace) -> PortableCaptionProfile:
    settings: dict[str, Any] = {}
    optional_values = (
        ("layout.maxUnitsPerLine", _optional_int(args.max_units_per_line, "Maximum units per line", minimum=1)),
        ("layout.maxLines", _optional_int(args.max_lines, "Maximum lines", minimum=1)),
        ("readability.maxCps", _optional_float(args.max_cps, "Maximum reading speed", minimum_exclusive=0)),
        ("timing.minDurationMs", _optional_int(args.min_duration_ms, "Minimum duration", minimum=1)),
        ("timing.maxDurationMs", _optional_int(args.max_duration_ms, "Maximum duration", minimum=1)),
        ("timing.minGapMs", _optional_int(args.min_gap_ms, "Minimum gap in milliseconds", minimum=0)),
        (
            "timing.minTimestampGapFrames",
            _optional_int(
                args.min_timestamp_gap_frames,
                "Minimum timestamp gap in frames",
                minimum=0,
            ),
        ),
    )
    for key, value in optional_values:
        if value is not None:
            settings[key] = value

    frame_rate = str(args.frame_rate or "").strip()
    if frame_rate and frame_rate != "Not specified":
        if frame_rate == "Custom":
            numerator = _optional_int(
                args.custom_frame_rate_numerator,
                "Custom frame-rate numerator",
                minimum=1,
            )
            denominator = _optional_int(
                args.custom_frame_rate_denominator,
                "Custom frame-rate denominator",
                minimum=1,
            )
            if numerator is None or denominator is None:
                raise ValueError(
                    "Custom delivery frame rate requires numerator and denominator."
                )
            timebase = RationalTimebase(numerator, denominator)
        else:
            timebase = RationalTimebase.from_label(frame_rate)
        settings["timing.timebase"] = timebase.to_json()

    align_value = str(args.align_to_frame_grid or "").strip()
    if align_value and align_value != "Not specified":
        if align_value == "Adjust to frame boundaries":
            settings["timing.alignToFrameGrid"] = True
        elif align_value == "Do not adjust to frame boundaries":
            settings["timing.alignToFrameGrid"] = False
        else:
            parsed_alignment = _optional_boolean(
                align_value,
                "Frame alignment",
            )
            assert parsed_alignment is not None
            settings["timing.alignToFrameGrid"] = parsed_alignment

    return PortableCaptionProfile.from_json(
        {
            "version": 1,
            "name": args.name,
            "notes": args.notes or "",
            "settings": settings,
        }
    )


def _profile_output_path(args: argparse.Namespace, profile: PortableCaptionProfile) -> Path:
    if args.output:
        return ab_path(args.output)
    return ab_path(args.output_folder) / profile_filename(profile.name)


def _profile_summary(profile: PortableCaptionProfile) -> str:
    settings = profile.settings

    def shown(key: str, suffix: str = "") -> str:
        value = settings.get(key)
        return f"{value}{suffix}" if value is not None else "Not set"

    frame_rate, custom_numerator, custom_denominator = _timebase_form_values(settings)
    if frame_rate == "Custom":
        frame_rate = f"{custom_numerator}/{custom_denominator} fps"
    alignment = _alignment_form_value(settings)
    if "timing.minTimestampGapFrames" in settings:
        gap = f"{settings['timing.minTimestampGapFrames']} frame steps"
    elif "timing.minGapMs" in settings:
        gap = f"{settings['timing.minGapMs']} ms"
    else:
        gap = "Not set"
    return "\n".join(
        [
            f"Profile: {profile.name}",
            f"Notes: {profile.notes or 'Not set'}",
            f"Maximum text length per line: {shown('layout.maxUnitsPerLine')}",
            f"Maximum lines per caption: {shown('layout.maxLines')}",
            f"Maximum reading speed: {shown('readability.maxCps', ' characters per second')}",
            f"Minimum caption duration: {shown('timing.minDurationMs', ' ms')}",
            f"Maximum caption duration: {shown('timing.maxDurationMs', ' ms')}",
            f"Delivery frame rate: {frame_rate}",
            f"Frame alignment: {alignment}",
            f"Minimum caption gap: {gap}",
        ]
    )


def command_profile_preview(args: argparse.Namespace) -> int:
    profile = _profile_from_editor_args(args)
    output = _profile_output_path(args, profile)
    summary_output = ab_path(args.summary_output)
    target_output = ab_path(args.target_output)
    summary_output.parent.mkdir(parents=True, exist_ok=True)
    target_output.parent.mkdir(parents=True, exist_ok=True)
    summary_output.write_text(_profile_summary(profile), encoding="utf-8", newline="\n")
    target_output.write_text(str(output), encoding="utf-8", newline="\n")
    print(f"RESULT|status=VALID|name={profile.name}|path={output}")
    return 0


def command_profile_save(args: argparse.Namespace) -> int:
    profile = _profile_from_editor_args(args)
    output = _profile_output_path(args, profile)
    write_portable_profile_atomic(
        output,
        profile,
        replace=boolean_argument(args.replace),
    )
    result = {
        "version": 1,
        "status": "SAVED",
        "path": str(output),
        "profile": profile.to_json(),
    }
    if args.result_json:
        write_json(ab_path(args.result_json), result)
    print(f"RESULT|status=SAVED|name={profile.name}|path={output}")
    return 0


def write_report_pair(
    json_path: Path | None,
    markdown_path: Path | None,
    findings: list[Finding],
    *,
    title: str,
    status: str,
    timing_fidelity: str,
    extra: dict[str, Any] | None = None,
) -> None:
    report = findings_to_json(
        findings, status=status, timing_fidelity=timing_fidelity, extra=extra
    )
    if json_path is not None:
        write_json(json_path, report)
    if markdown_path is not None:
        markdown_path.parent.mkdir(parents=True, exist_ok=True)
        markdown_path.write_text(findings_to_markdown(report, title=title), encoding="utf-8")


def command_transcript_to_srt(args: argparse.Namespace) -> int:
    transcript = load_json(ab_path(args.input))
    if not isinstance(transcript, dict):
        raise ValueError("Transcript JSON root must be an object.")
    detected_language, language_issue = detect_transcript_language(transcript)
    findings = []
    if language_issue is not None:
        findings.append(
            Finding(
                "WARNING",
                "TRANSCRIPT_LANGUAGE_UNAVAILABLE",
                language_issue + " The output filename will not claim a language.",
            )
        )
    document, warnings = transcript_to_document(
        transcript,
        profile=profile_from_args(args),
        language=detected_language,
        source_name=args.source_name,
    )
    serialized = serialize_srt(document)
    if args.output_folder:
        if not args.run_dir:
            raise ValueError("--run-dir is required with --output-folder.")
        stem = safe_output_stem(args.source_name or "transcript")
        suffix = f".{detected_language}" if detected_language else ""
        output = ab_path(args.output_folder) / f"{stem}{suffix}.srt"
        run_dir = ab_path(args.run_dir)
        run_dir.mkdir(parents=True, exist_ok=True)
        prepared_srt = run_dir / "prepared.srt"
        prepared_srt.write_text(serialized, encoding="utf-8", newline="\n")
        prepared_sidecar: Path | None = None
        if boolean_argument(args.write_timing):
            prepared_sidecar = run_dir / "prepared.timing.json"
            write_json(prepared_sidecar, create_sidecar(document, serialized.encode("utf-8")))
        installation = install_output_pair(
            prepared_srt,
            output,
            prepared_sidecar=prepared_sidecar,
            overwrite=boolean_argument(args.overwrite),
        )
    else:
        if not args.output:
            raise ValueError("Either --output or --output-folder is required.")
        output = ab_path(args.output)
        output.parent.mkdir(parents=True, exist_ok=True)
        output.write_text(serialized, encoding="utf-8", newline="\n")
        if args.timing_sidecar:
            sidecar = create_sidecar(document, serialized.encode("utf-8"))
            write_json(ab_path(args.timing_sidecar), sidecar)
        installation = {"status": "SRT_WRITTEN", "srt": str(output)}
    findings.extend(
        Finding("WARNING", "GENERATION_WARNING", warning) for warning in warnings
    )
    write_report_pair(
        ab_path(args.report_json) if args.report_json else None,
        ab_path(args.report_markdown) if args.report_markdown else None,
        findings,
        title="Transcript to SRT report",
        status=installation["status"],
        timing_fidelity=document.timing_fidelity.value,
        extra={
            "cueCount": len(document.cues),
            "output": str(output),
            "detectedLanguage": detected_language,
            "languageStatus": "DETECTED" if detected_language else "UNAVAILABLE",
            "installation": installation,
        },
    )
    print(
        f"RESULT|status={installation['status']}|cues={len(document.cues)}"
        f"|languageStatus={'DETECTED' if detected_language else 'UNAVAILABLE'}"
        f"|language={detected_language}|output={output}"
    )
    return 0


def _load_srt(args: argparse.Namespace, *, strict: bool = True):
    input_path = ab_path(args.input)
    return input_path, parse_srt(
        input_path.read_text(encoding="utf-8-sig"),
        language=getattr(args, "language", ""),
        source_name=input_path.stem,
        strict=strict,
    )


def command_validate(args: argparse.Namespace) -> int:
    input_path, (document, parse_diagnostics) = _load_srt(args, strict=False)
    analysis = _analysis_profile_and_resolution(args)
    findings = [
        Finding(item.severity, item.code, item.message) for item in parse_diagnostics
    ] + validate_document(
        document,
        analysis.processing_profile,
        explicit_gap_limit=analysis.explicit_gap_limit,
        gap_timebase=analysis.timebase,
        required_gap_frames=analysis.required_gap_frames,
    )
    if (
        analysis.timebase is not None
        and analysis.resolved_profile.effective_settings.get(
            "timing.alignToFrameGrid"
        ) is True
    ):
        findings.extend(validate_frame_alignment(document, analysis.timebase))
    status = "FAILED" if any(item.severity == "ERROR" for item in findings) else (
        "SUCCEEDED_WITH_WARNINGS" if findings else "SUCCEEDED"
    )
    caption_settings = analysis_settings_report(
        analysis,
        findings,
        profile_path=(
            ab_path(args.caption_profile) if args.caption_profile else None
        ),
    )
    write_report_pair(
        ab_path(args.report_json),
        ab_path(args.report_markdown),
        findings,
        title="SRT validation report",
        status=status,
        timing_fidelity=document.timing_fidelity.value,
        extra={
            "input": str(input_path),
            "cueCount": len(document.cues),
            "captionSettings": caption_settings,
            "analysisMetrics": analysis_metrics(
                document,
                findings,
                timebase=analysis.timebase,
            ),
        },
    )
    if args.overview_text:
        overview_path = ab_path(args.overview_text)
        overview_path.parent.mkdir(parents=True, exist_ok=True)
        overview_path.write_text(
            analysis_overview(
                document,
                findings,
                analysis.processing_profile,
                caption_settings=caption_settings,
                timebase=analysis.timebase,
            ),
            encoding="utf-8",
            newline="\n",
        )
    if args.review_srt:
        review_path = ab_path(args.review_srt)
        review_path.parent.mkdir(parents=True, exist_ok=True)
        review_path.write_text(
            serialize_srt(
                review_marker_document(
                    document, findings, source_identity=str(input_path)
                )
            ),
            encoding="utf-8",
            newline="\n",
        )
    print(f"RESULT|status={status}|cues={len(document.cues)}|findings={len(findings)}")
    return 2 if status == "FAILED" else 0


def _load_sequence_manifest_with_transcripts(path: Path) -> dict[str, Any]:
    manifest = load_json(path)
    if not isinstance(manifest, dict):
        raise ValueError("Sequence manifest JSON root must be an object.")
    raw_items = manifest.get("items")
    if isinstance(raw_items, list):
        resolved_items = []
        for item in raw_items:
            if (
                isinstance(item, dict)
                and not isinstance(item.get("transcript"), dict)
                and isinstance(item.get("transcriptPath"), str)
            ):
                transcript = load_json(ab_path(item["transcriptPath"]))
                if not isinstance(transcript, dict):
                    raise ValueError("Sequence item transcriptPath must resolve to a JSON object.")
                item = {**item, "transcript": transcript}
            resolved_items.append(item)
        manifest = {**manifest, "items": resolved_items}
    return manifest


def command_sequence_manifest_to_srt(args: argparse.Namespace) -> int:
    manifest = _load_sequence_manifest_with_transcripts(ab_path(args.input))
    chosen_track_index = (
        args.chosen_track_index
        if args.chosen_track_index is not None and args.chosen_track_index >= 0
        else None
    )
    document, findings, sequence_report = sequence_manifest_to_document(
        manifest,
        profile=profile_from_args(args),
        chosen_track_index=chosen_track_index,
    )
    if args.output_folder:
        stem = safe_output_stem(str(manifest.get("sequenceName", "")) or "active-sequence")
        language = str(sequence_report.get("detectedLanguage", ""))
        suffix = f".{language}" if language else ""
        output = ab_path(args.output_folder) / f"{stem}{suffix}.srt"
    elif args.output:
        output = ab_path(args.output)
    else:
        raise ValueError("Either --output or --output-folder is required.")
    generation_succeeded = document is not None and sequence_report["status"] in {
        "SUCCEEDED",
        "SUCCEEDED_WITH_WARNINGS",
    }
    output_written = False
    output_sidecar: str | None = None
    installation: dict[str, Any] | None = None
    if generation_succeeded and args.output_folder:
        if not args.run_dir:
            raise ValueError("--run-dir is required with --output-folder.")
        run_dir = ab_path(args.run_dir)
        run_dir.mkdir(parents=True, exist_ok=True)
        serialized = serialize_srt(document)
        prepared_srt = run_dir / "prepared-sequence.srt"
        prepared_srt.write_text(serialized, encoding="utf-8", newline="\n")
        prepared_sidecar: Path | None = None
        if boolean_argument(args.write_timing):
            prepared_sidecar = run_dir / "prepared-sequence.timing.json"
            write_json(prepared_sidecar, create_sidecar(document, serialized.encode("utf-8")))
        installation = install_output_pair(
            prepared_srt,
            output,
            prepared_sidecar=prepared_sidecar,
            overwrite=boolean_argument(args.overwrite),
        )
        output_written = installation["status"] in {
            "SRT_WRITTEN",
            "SRT_AND_TIMING_WRITTEN",
            "SRT_WRITTEN_TIMING_METADATA_FAILED",
        }
        if output_written and isinstance(installation.get("sidecar"), str):
            output_sidecar = str(installation["sidecar"])
    elif generation_succeeded:
        output.parent.mkdir(parents=True, exist_ok=True)
        serialized = serialize_srt(document)
        output.write_text(serialized, encoding="utf-8", newline="\n")
        output_written = True
        installation = {"status": "SRT_WRITTEN", "srt": str(output)}
        if boolean_argument(args.write_timing):
            sidecar_path = expected_sidecar_path(output)
            write_json(sidecar_path, create_sidecar(document, serialized.encode("utf-8")))
            output_sidecar = str(sidecar_path)
    status = (
        str(installation["status"])
        if installation is not None and installation["status"] == "SKIPPED_EXISTS"
        else sequence_report["status"]
    )
    report_json = ab_path(args.report_json)
    report_markdown = ab_path(args.report_markdown)
    write_report_pair(
        report_json,
        report_markdown,
        findings,
        title="Active-sequence source-transcript SRT report",
        status=status,
        timing_fidelity=(
            document.timing_fidelity.value if document is not None else "WORD_ALIGNED_EXACT"
        ),
        extra={
            **{key: value for key, value in sequence_report.items() if key != "status"},
            "sequenceStatus": sequence_report["status"],
            "output": str(output),
            "outputWritten": output_written,
            "outputTimingSidecar": output_sidecar,
            "installation": installation,
        },
    )
    with report_markdown.open("a", encoding="utf-8") as handle:
        handle.write("\n" + sequence_report_markdown(sequence_report))
    print(
        f"RESULT|status={status}|cues={len(document.cues) if document else 0}"
        f"|outputWritten={str(output_written).lower()}|output={output}"
    )
    if status == "SKIPPED_EXISTS":
        return 0
    return 0 if output_written else 2


REFERENCE_SEQUENCE_STATUS_MESSAGES = {
    "UNRESOLVED_CONFLICTS": (
        "Overlapping spoken transcript regions require editorial resolution before the approved script can be timed safely."
    ),
    "UNSUPPORTED_ITEMS": (
        "At least one in-scope Timeline clip could not be mapped reliably. This can happen while Premiere is still transcribing, when transcript timing is incomplete, or when a clip uses Reverse Speed or a non-1x speed. Review the item list and retry after transcription finishes."
    ),
    "NO_MAPPED_SPEECH": (
        "No usable source-transcript words were found in the selected active-sequence scope."
    ),
}


def _write_reference_sequence_run_reports(
    *,
    report_json_path: Path,
    report_markdown_path: Path,
    status: str,
    status_message: str,
    output: Path,
    output_written: bool,
    output_timing_sidecar: str | None,
    installation: dict[str, Any] | None,
    sequence_result: Any,
    build: Any,
    caption_settings: dict[str, Any],
    review_markers: dict[str, Any],
) -> None:
    report = {
        "version": 1,
        "status": status,
        "statusMessage": status_message,
        "output": str(output),
        "outputWritten": output_written,
        "outputTimingSidecar": output_timing_sidecar,
        "installation": installation,
        "sequenceAndAlignment": reference_sequence_report(sequence_result),
        "captionBuild": (
            reference_caption_build_report(build) if build is not None else None
        ),
        "captionSettings": caption_settings,
        "reviewMarkers": review_markers,
    }
    write_json(report_json_path, report)
    lines = [
        "# Create SRT From Reference Script Run Report",
        "",
        f"- Result: `{status}`",
        f"- SRT created: {'yes' if output_written else 'no'}",
        f"- Intended output: `{output}`",
        "",
        status_message,
        "",
        reference_sequence_report_markdown(sequence_result).rstrip(),
    ]
    if build is not None:
        lines.extend(
            [
                "",
                reference_caption_build_report_markdown(
                    build, heading_level=2
                ).rstrip(),
            ]
        )
    lines.extend(
        [
            "",
            "## Caption settings",
            "",
            "- Selected settings: "
            + str(caption_settings.get("profileName", "Standard settings")),
        ]
    )
    for item in caption_settings.get("effective", []):
        if isinstance(item, dict):
            lines.append(
                f"- {item.get('label')}: {item.get('displayValue')} "
                f"({item.get('sourceLabel')})"
            )
    not_checked = caption_settings.get("notChecked", [])
    if isinstance(not_checked, list) and not_checked:
        lines.append(
            "- Stored profile settings not used by this tool: "
            + ", ".join(
                str(item.get("label"))
                for item in not_checked
                if isinstance(item, dict)
            )
        )
    profile_result = caption_settings.get("profileRuleResult")
    if profile_result:
        lines.append(f"- Profile use: {profile_result}")
    lines.extend(
        [
            "",
            "## Optional sequence review markers",
            "",
            f"- Marker choice: {review_markers['modeLabel']}",
            f"- Review markers prepared: {review_markers['markerCount']}",
            f"- Wording differences included: {review_markers['wordingFindingCount']}",
            "- Estimated timing regions included: "
            + str(review_markers["estimatedTimingRegionCount"]),
            "- Findings without a safe Timeline position: "
            + str(review_markers["unplacedFindingCount"]),
        ]
    )
    report_markdown_path.parent.mkdir(parents=True, exist_ok=True)
    report_markdown_path.write_text(
        "\n".join(lines).rstrip() + "\n", encoding="utf-8", newline="\n"
    )


def _command_reference_sequence_to_srt(
    args: argparse.Namespace,
    *,
    prepared_srt: Path,
    prepared_timing: Path,
) -> int:
    manifest = _load_sequence_manifest_with_transcripts(ab_path(args.input))
    reference_path = ab_path(args.reference_text_file)
    reference_text = reference_path.read_text(encoding="utf-8-sig")
    if not reference_text.strip():
        raise ValueError("The approved reference text file is empty.")
    chosen_track_index = (
        args.chosen_track_index
        if args.chosen_track_index is not None and args.chosen_track_index >= 0
        else None
    )
    profile_resolution = _resolved_reference_from_args(args)
    profile = profile_resolution.processing_profile
    caption_settings = workflow_settings_report(
        profile_resolution,
        profile_path=_workflow_profile_path(args),
    )
    sequence_result = align_reference_script_to_sequence_manifest(
        reference_text,
        manifest,
        profile=profile,
        chosen_track_index=chosen_track_index,
    )
    build = (
        build_reference_caption_document(
            sequence_result.alignment,
            profile=profile,
            language=str(sequence_result.sequence_report.get("detectedLanguage", "")),
            source_name=str(manifest.get("sequenceName", "")),
            sentence_end_min_fill=args.sentence_end_min_fill / 100.0,
            comma_min_fill=args.comma_min_fill / 100.0,
        )
        if sequence_result.alignment is not None
        else None
    )
    if args.output_folder:
        stem = safe_output_stem(str(manifest.get("sequenceName", "")) or "active-sequence")
        output = ab_path(args.output_folder) / f"{stem}.reference-script.srt"
    elif args.output:
        output = ab_path(args.output)
    else:
        raise ValueError("Either --output or --output-folder is required.")
    report_json_path = ab_path(args.report_json)
    report_markdown_path = ab_path(args.report_markdown)
    marker_mode_labels = {
        "NONE": "None",
        "WORDING": "Wording differences only",
        "WORDING_AND_ESTIMATED": "Wording differences and estimated timing",
    }
    marker_summary: dict[str, Any] = {
        "mode": args.review_markers_mode,
        "modeLabel": marker_mode_labels[args.review_markers_mode],
        "markerCount": 0,
        "wordingFindingCount": 0,
        "estimatedTimingRegionCount": 0,
        "unplacedFindingCount": 0,
    }
    if args.review_srt:
        review_srt_path = ab_path(args.review_srt)
        review_srt_path.unlink(missing_ok=True)
        if (
            args.review_markers_mode != "NONE"
            and sequence_result.alignment is not None
            and sequence_result.alignment.can_generate_srt
        ):
            marker_document = reference_review_marker_document(
                sequence_result.alignment,
                include_estimated_timing=(
                    args.review_markers_mode == "WORDING_AND_ESTIMATED"
                ),
            )
            review_srt_path.parent.mkdir(parents=True, exist_ok=True)
            review_srt_path.write_text(
                serialize_srt(marker_document), encoding="utf-8", newline="\n"
            )
            metadata = marker_document.metadata
            marker_summary.update(
                {
                    "markerCount": int(metadata["markerCount"]),
                    "wordingFindingCount": int(metadata["wordingFindingCount"]),
                    "estimatedTimingRegionCount": int(
                        metadata["estimatedTimingRegionCount"]
                    ),
                    "unplacedFindingCount": int(
                        metadata["unplacedWordingFindingCount"]
                    )
                    + int(metadata["unplacedEstimatedTimingRegionCount"]),
                }
            )
    output_written = False
    output_sidecar: str | None = None
    installation: dict[str, Any] | None = None
    if build is None:
        status = sequence_result.status
        status_message = REFERENCE_SEQUENCE_STATUS_MESSAGES.get(
            status,
            "The active-sequence transcript selection is not safe to use for reference-script timing. Review the report details.",
        )
    elif build.document is None:
        status = build.status
        status_message = build.status_message
    else:
        prepared_srt.parent.mkdir(parents=True, exist_ok=True)
        serialized = serialize_srt(build.document)
        prepared_srt.write_text(serialized, encoding="utf-8", newline="\n")
        prepared_sidecar: Path | None = None
        if (
            boolean_argument(args.write_timing)
            and build.document.timing_fidelity
            in {
                TimingFidelity.WORD_ALIGNED_EXACT,
                TimingFidelity.WORD_ALIGNED_ESTIMATED,
            }
        ):
            prepared_sidecar = prepared_timing
            write_json(
                prepared_sidecar,
                create_sidecar(build.document, serialized.encode("utf-8")),
            )
        installation = install_output_pair(
            prepared_srt,
            output,
            prepared_sidecar=prepared_sidecar,
            overwrite=boolean_argument(args.overwrite),
        )
        output_written = installation["status"] in {
            "SRT_WRITTEN",
            "SRT_AND_TIMING_WRITTEN",
            "SRT_WRITTEN_TIMING_METADATA_FAILED",
        }
        if isinstance(installation.get("sidecar"), str):
            output_sidecar = str(installation["sidecar"])
        if installation["status"] == "SKIPPED_EXISTS":
            status = "SKIPPED_EXISTS"
            status_message = (
                "The intended SRT already exists. It was left unchanged because overwrite was not enabled."
            )
        else:
            status = build.status
            status_message = build.status_message
            if not boolean_argument(args.write_timing):
                status_message += (
                    " Word-timing metadata was not written because it was disabled for this run."
                )
            elif (
                installation["status"] == "SRT_AND_TIMING_WRITTEN"
                and build.document.timing_fidelity
                == TimingFidelity.WORD_ALIGNED_EXACT
            ):
                status_message += " Exact word-timing metadata was written."
            elif (
                installation["status"] == "SRT_AND_TIMING_WRITTEN"
                and build.document.timing_fidelity
                == TimingFidelity.WORD_ALIGNED_ESTIMATED
            ):
                status_message += (
                    " Word-timing metadata was written with "
                    f"{build.estimated_timing_token_count} estimated or near-match "
                    f"word window(s). Review them in {report_markdown_path}."
                )
            elif build.document.timing_fidelity not in {
                TimingFidelity.WORD_ALIGNED_EXACT,
                TimingFidelity.WORD_ALIGNED_ESTIMATED,
            }:
                status_message += (
                    " Word-timing metadata was not written because safe word windows "
                    "could not be represented for the complete SRT."
                )
            if installation["status"] == "SRT_WRITTEN_TIMING_METADATA_FAILED":
                status = "SUCCEEDED_WITH_WARNINGS"
                status_message += " The SRT was written, but its timing metadata could not be installed."
    _write_reference_sequence_run_reports(
        report_json_path=report_json_path,
        report_markdown_path=report_markdown_path,
        status=status,
        status_message=status_message,
        output=output,
        output_written=output_written,
        output_timing_sidecar=output_sidecar,
        installation=installation,
        sequence_result=sequence_result,
        build=build,
        caption_settings=caption_settings,
        review_markers=marker_summary,
    )
    if args.overview_text:
        overview_path = ab_path(args.overview_text)
        overview_path.parent.mkdir(parents=True, exist_ok=True)
        overview_lines = [status_message]
        if output_written:
            overview_lines.extend(["", f"Created SRT: {output}"])
        elif status == "SKIPPED_EXISTS":
            overview_lines.extend(["", f"Existing SRT: {output}"])
        else:
            overview_lines.extend(["", "No SRT was created."])
        if args.review_markers_mode != "NONE":
            overview_lines.extend(
                [
                    "",
                    f"Review markers prepared: {marker_summary['markerCount']}",
                    "Wording differences included: "
                    + str(marker_summary["wordingFindingCount"]),
                    "Estimated timing regions included: "
                    + str(marker_summary["estimatedTimingRegionCount"]),
                    "Findings without a safe Timeline position: "
                    + str(marker_summary["unplacedFindingCount"]),
                ]
            )
        overview_path.write_text(
            "\n".join(overview_lines).rstrip() + "\n",
            encoding="utf-8",
            newline="\n",
        )
    print(
        f"RESULT|status={status}|cues={len(build.document.cues) if build and build.document else 0}"
        f"|outputWritten={str(output_written).lower()}|reviewMarkers={marker_summary['markerCount']}|output={output}"
    )
    return 0 if output_written or status == "SKIPPED_EXISTS" else 2


def command_reference_sequence_to_srt(args: argparse.Namespace) -> int:
    run_dir = ab_path(args.run_dir)
    prepared_srt = run_dir / "prepared-reference-script.srt"
    prepared_timing = run_dir / "prepared-reference-script.timing.json"
    # A run directory may be reused after an interrupted or exact-timing run.
    # Remove both preparation files before and after every status path so stale
    # word timing can never be mistaken for the current result.
    prepared_srt.unlink(missing_ok=True)
    prepared_timing.unlink(missing_ok=True)
    try:
        return _command_reference_sequence_to_srt(
            args,
            prepared_srt=prepared_srt,
            prepared_timing=prepared_timing,
        )
    finally:
        prepared_srt.unlink(missing_ok=True)
        prepared_timing.unlink(missing_ok=True)


def command_sequence_init_manifest(args: argparse.Namespace) -> int:
    output = ab_path(args.output)
    manifest = {
        "version": 1,
        "sequenceName": args.sequence_name,
        "frameDurationSeconds": args.frame_duration_seconds,
        "sequenceStartSeconds": args.sequence_start_seconds,
        "sequenceEndSeconds": args.sequence_end_seconds,
        "items": [],
    }
    write_json(output, manifest)
    print(f"RESULT|status=SUCCEEDED|items=0|output={output}")
    return 0


def command_sequence_add_item(args: argparse.Namespace) -> int:
    manifest_path = ab_path(args.manifest)
    manifest = load_json(manifest_path)
    if not isinstance(manifest, dict):
        raise ValueError("Sequence manifest JSON root must be an object.")
    items = manifest.get("items")
    if not isinstance(items, list):
        raise ValueError("Sequence manifest does not contain an items array.")
    item: dict[str, Any] = {
        "id": args.id,
        "trackIndex": args.track_index,
        "trackName": args.track_name,
        "itemName": args.item_name,
        "projectItemRef": args.project_item_ref,
        "timelineStartSeconds": args.timeline_start_seconds,
        "timelineEndSeconds": args.timeline_end_seconds,
        "sourceInSeconds": args.source_in_seconds,
        "sourceOutSeconds": args.source_out_seconds,
        "speed": args.speed,
        "reversed": boolean_argument(args.reversed),
        "disabled": boolean_argument(args.disabled),
        "trackMuted": boolean_argument(args.track_muted),
    }
    if args.transcript_path:
        item["transcriptPath"] = str(ab_path(args.transcript_path))
    if args.discovery_warning:
        item["discoveryWarning"] = args.discovery_warning
    items.append(item)
    write_json(manifest_path, manifest)
    print(f"RESULT|status=SUCCEEDED|items={len(items)}|output={manifest_path}")
    return 0


def command_cut_manifest_init(args: argparse.Namespace) -> int:
    output = ab_path(args.output)
    manifest = {
        "version": 1,
        "sequenceName": args.sequence_name,
        "sequenceTimebaseTicks": args.sequence_timebase_ticks,
        "srtZeroSequenceSeconds": str(args.srt_zero_sequence_seconds),
        "referenceTrackIndex": args.reference_track_index,
        "referenceTrackName": args.reference_track_name,
        "referenceTrackMuted": boolean_argument(args.reference_track_muted),
        "items": [],
    }
    cut_manifest_selection(manifest)
    write_json(output, manifest)
    print(f"RESULT|status=SUCCEEDED|items=0|output={output}")
    return 0


def command_cut_manifest_add_item(args: argparse.Namespace) -> int:
    manifest_path = ab_path(args.manifest)
    manifest = load_json(manifest_path)
    if not isinstance(manifest, dict):
        raise ValueError("Cut manifest JSON root must be an object.")
    items = manifest.get("items")
    if not isinstance(items, list):
        raise ValueError("Cut manifest does not contain an items array.")
    items.append(
        {
            "id": args.id,
            "startSeconds": str(args.start_seconds),
            "endSeconds": str(args.end_seconds),
            "disabled": boolean_argument(args.disabled),
        }
    )
    cut_manifest_selection(manifest)
    write_json(manifest_path, manifest)
    print(f"RESULT|status=SUCCEEDED|items={len(items)}|output={manifest_path}")
    return 0


def command_caption_timing_manifest_init(args: argparse.Namespace) -> int:
    output = ab_path(args.output)
    manifest = {
        "version": 1,
        "sequenceName": args.sequence_name,
        "sequenceTimebaseTicks": args.sequence_timebase_ticks,
        "captionTrackIndex": args.caption_track_index,
        "captionTrackName": args.caption_track_name,
        "items": [],
    }
    caption_timing_selection(manifest)
    write_json(output, manifest)
    print(f"RESULT|status=SUCCEEDED|items=0|output={output}")
    return 0


def command_caption_timing_manifest_add_item(args: argparse.Namespace) -> int:
    manifest_path = ab_path(args.manifest)
    manifest = load_json(manifest_path)
    if not isinstance(manifest, dict):
        raise ValueError("Caption timing manifest JSON root must be an object.")
    items = manifest.get("items")
    if not isinstance(items, list):
        raise ValueError("Caption timing manifest does not contain an items array.")
    items.append(
        {
            "id": args.id,
            "index": args.index,
            "startSeconds": str(args.start_seconds),
            "endSeconds": str(args.end_seconds),
        }
    )
    caption_timing_selection(manifest)
    write_json(manifest_path, manifest)
    print(f"RESULT|status=SUCCEEDED|items={len(items)}|output={manifest_path}")
    return 0


def command_plan_direct_caption_alignment(args: argparse.Namespace) -> int:
    caption_selection = caption_timing_selection(
        load_json(ab_path(args.caption_manifest))
    )
    cut_selection = cut_manifest_selection(load_json(ab_path(args.cut_manifest)))
    report_min_duration_ms = (
        None
        if args.min_duration_frames is not None
        else (1 if args.min_duration_ms is None else args.min_duration_ms)
    )
    report_min_gap_ms = (
        None
        if args.min_gap_frames is not None
        else (0 if args.min_gap_ms is None else args.min_gap_ms)
    )
    result = align_caption_timing_to_cut_frames(
        caption_selection,
        cut_frame_indices=list(cut_selection.sequence_cut_frame_indices),
        max_distance_frames=args.max_distance_frames,
        min_duration_ms=report_min_duration_ms,
        min_gap_ms=report_min_gap_ms,
        min_duration_frames=args.min_duration_frames,
        min_gap_frames=args.min_gap_frames,
    )
    plan = direct_caption_alignment_plan(
        result,
        caption_selection,
        cut_selection,
        max_distance_frames=args.max_distance_frames,
        min_duration_ms=report_min_duration_ms,
        min_gap_ms=report_min_gap_ms,
        min_duration_frames=args.min_duration_frames,
        min_gap_frames=args.min_gap_frames,
    )
    output = ab_path(args.output)
    write_json(output, plan)
    if args.actions_tsv:
        actions_path = ab_path(args.actions_tsv)
        actions_path.parent.mkdir(parents=True, exist_ok=True)
        action_lines = ["itemIndex\tedge\ttargetSeconds"]
        action_lines.extend(
            f"{item['itemIndex']}\t{item['edge']}\t{item['targetSeconds']}"
            for item in plan["actions"]
        )
        actions_path.write_text("\n".join(action_lines) + "\n", encoding="utf-8")
    if args.summary_tsv:
        summary_path = ab_path(args.summary_tsv)
        summary_path.parent.mkdir(parents=True, exist_ok=True)
        summary_path.write_text(
            "status\tmovedCount\n"
            f"{plan['status']}\t{plan['movedCount']}\n",
            encoding="utf-8",
        )
    if args.report_markdown:
        markdown = ab_path(args.report_markdown)
        markdown.parent.mkdir(parents=True, exist_ok=True)
        markdown.write_text(
            direct_caption_alignment_report_markdown(plan), encoding="utf-8"
        )
    if args.overview_text:
        overview = ab_path(args.overview_text)
        overview.parent.mkdir(parents=True, exist_ok=True)
        overview.write_text(direct_caption_alignment_overview(plan), encoding="utf-8")
    print(
        f"RESULT|status={plan['status']}|captions={len(caption_selection.items)}"
        f"|cuts={len(cut_selection.sequence_cut_frame_indices)}|moved={plan['movedCount']}"
        f"|alreadyAligned={plan['alreadyAlignedCount']}"
        f"|reviewSkipped={plan['reviewSkipCount']}"
        f"|outsideRange={plan['outsideSnapRangeCount']}|output={output}"
    )
    return 0


def command_verify_direct_caption_alignment(args: argparse.Namespace) -> int:
    plan = load_json(ab_path(args.plan))
    actual = caption_timing_selection(load_json(ab_path(args.caption_manifest)))
    report = verify_direct_caption_alignment(plan, actual)
    output = ab_path(args.output)
    write_json(output, report)
    if args.report_markdown:
        markdown = ab_path(args.report_markdown)
        markdown.parent.mkdir(parents=True, exist_ok=True)
        markdown.write_text(
            direct_caption_alignment_report_markdown(report), encoding="utf-8"
        )
    if args.overview_text:
        overview = ab_path(args.overview_text)
        overview.parent.mkdir(parents=True, exist_ok=True)
        overview.write_text(
            direct_caption_alignment_overview(report), encoding="utf-8"
        )
    print(
        f"RESULT|status={report['status']}"
        f"|plannedMoves={report['plannedMoveCount']}"
        f"|mismatches={len(report['mismatches'])}|output={output}"
    )
    return 0 if report["status"] == "VERIFIED" else 2


def command_align_to_cuts(args: argparse.Namespace) -> int:
    input_path, (document, parse_diagnostics) = _load_srt(args)
    output_path = ab_path(args.output)
    if same_path(input_path, output_path):
        raise ValueError(
            "In-place cut alignment is not supported. Choose a separate output file so the source remains recoverable."
        )
    selection = cut_manifest_selection(load_json(ab_path(args.cut_manifest)))
    report_min_duration_ms = (
        None
        if args.min_duration_frames is not None
        else (1 if args.min_duration_ms is None else args.min_duration_ms)
    )
    report_min_gap_ms = (
        None
        if args.min_gap_frames is not None
        else (0 if args.min_gap_ms is None else args.min_gap_ms)
    )
    sidecar, sidecar_reason = load_and_validate_sidecar(input_path)
    if sidecar is not None:
        document = apply_sidecar(document, sidecar)
    result = align_document_to_cut_frames(
        document,
        cut_frame_indices=list(selection.cut_frame_indices),
        timebase=selection.timebase,
        max_distance_frames=args.max_distance_frames,
        min_duration_ms=report_min_duration_ms,
        min_gap_ms=report_min_gap_ms,
        min_duration_frames=args.min_duration_frames,
        min_gap_frames=args.min_gap_frames,
    )

    overwrite = boolean_argument(args.overwrite)
    output_written = False
    installation: dict[str, Any] | None = None
    timing_metadata = sidecar_reason
    summary = cut_alignment_summary(result)
    if output_path.exists() and not overwrite:
        status = "SKIPPED_EXISTS"
    elif not selection.cut_frame_indices:
        status = "NO_ELIGIBLE_CUTS"
    elif result.applied_count == 0:
        status = "NO_SAFE_CHANGES" if summary["reviewSkipCount"] else "NO_CHANGES"
    else:
        run_dir = ab_path(args.run_dir)
        run_dir.mkdir(parents=True, exist_ok=True)
        prepared_srt = run_dir / "prepared-cut-aligned.srt"
        serialized = serialize_srt(result.document)
        prepared_srt.write_text(serialized, encoding="utf-8", newline="\n")
        prepared_sidecar: Path | None = None
        if result.document.timing_fidelity.value == "WORD_ALIGNED_EXACT":
            prepared_sidecar = run_dir / "prepared-cut-aligned.timing.json"
            write_json(
                prepared_sidecar,
                create_sidecar(result.document, serialized.encode("utf-8")),
            )
            timing_metadata = "Exact word timing accepted and refreshed for the aligned SRT."
        installation = install_output_pair(
            prepared_srt,
            output_path,
            prepared_sidecar=prepared_sidecar,
            overwrite=overwrite,
        )
        output_written = installation["status"] != "SKIPPED_EXISTS"
        status = (
            "SUCCEEDED_WITH_WARNINGS"
            if summary["reviewSkipCount"]
            else "SUCCEEDED"
        )

    report = cut_alignment_report(
        result,
        selection,
        status=status,
        input_path=str(input_path),
        output_path=str(output_path),
        output_written=output_written,
        max_distance_frames=args.max_distance_frames,
        min_duration_ms=report_min_duration_ms,
        min_gap_ms=report_min_gap_ms,
        min_duration_frames=args.min_duration_frames,
        min_gap_frames=args.min_gap_frames,
        timing_metadata=timing_metadata,
    )
    report["parseDiagnostics"] = [
        {
            "severity": item.severity,
            "code": item.code,
            "message": item.message,
            "block": item.block,
        }
        for item in parse_diagnostics
    ]
    report["installation"] = installation
    report_json = ab_path(args.report_json)
    report_markdown = ab_path(args.report_markdown)
    write_json(report_json, report)
    report_markdown.parent.mkdir(parents=True, exist_ok=True)
    report_markdown.write_text(
        cut_alignment_report_markdown(report), encoding="utf-8"
    )
    if args.overview_text:
        overview_text = ab_path(args.overview_text)
        overview_text.parent.mkdir(parents=True, exist_ok=True)
        overview_text.write_text(cut_alignment_overview(report), encoding="utf-8")
    print(
        f"RESULT|status={status}|cues={len(result.document.cues)}"
        f"|cuts={len(selection.cut_frame_indices)}|moved={report['movedCount']}"
        f"|alreadyAligned={report['alreadyAlignedCount']}"
        f"|reviewSkipped={report['reviewSkipCount']}"
        f"|outsideRange={report['outsideSnapRangeCount']}"
        f"|outputWritten={str(output_written).lower()}"
        f"|output={output_path}"
    )
    return 0


def command_reflow(args: argparse.Namespace) -> int:
    input_path, (document, parse_diagnostics) = _load_srt(args, strict=False)
    reflow_resolution = _resolved_reflow_from_args(args)
    processing_profile = reflow_resolution.processing_profile
    caption_settings = workflow_settings_report(
        reflow_resolution,
        profile_path=_workflow_profile_path(args),
    )
    output = ab_path(args.output)
    if same_path(input_path, output):
        raise ValueError(
            "In-place reflow is not supported. Choose a separate output file so the source remains recoverable."
        )
    if output.exists() and not boolean_argument(args.overwrite):
        write_report_pair(
            ab_path(args.report_json),
            ab_path(args.report_markdown),
            [],
            title="SRT reflow report",
            status="SKIPPED_EXISTS",
            timing_fidelity=document.timing_fidelity.value,
            extra={
                "input": str(input_path),
                "output": str(output),
                "mode": "GLOBAL_REFLOW",
                "outputWritten": False,
                "captionSettings": caption_settings,
            },
        )
        print(f"RESULT|status=SKIPPED_EXISTS|cues={len(document.cues)}|output={output}")
        return 0
    fatal_parse_findings = [
        Finding(item.severity, item.code, item.message)
        for item in parse_diagnostics
        if item.severity == "ERROR"
    ]
    if fatal_parse_findings:
        write_report_pair(
            ab_path(args.report_json),
            ab_path(args.report_markdown),
            fatal_parse_findings,
            title="SRT reflow report",
            status="FAILED",
            timing_fidelity=document.timing_fidelity.value,
            extra={
                "input": str(input_path),
                "mode": "GLOBAL_REFLOW",
                "outputWritten": False,
                "captionSettings": caption_settings,
            },
        )
        print(
            f"RESULT|status=FAILED|cues={len(document.cues)}|outputWritten=false"
        )
        return 2
    source_errors = [
        finding
        for finding in validate_document(document, processing_profile)
        if finding.severity == "ERROR"
    ]
    if source_errors:
        write_report_pair(
            ab_path(args.report_json),
            ab_path(args.report_markdown),
            source_errors,
            title="SRT reflow report",
            status="FAILED",
            timing_fidelity=document.timing_fidelity.value,
            extra={
                "input": str(input_path),
                "mode": "GLOBAL_REFLOW",
                "outputWritten": False,
                "captionSettings": caption_settings,
            },
        )
        print(
            f"RESULT|status=FAILED|cues={len(document.cues)}|outputWritten=false"
        )
        return 2
    sidecar, sidecar_status = load_and_validate_sidecar(
        input_path, expected_language=args.language or None
    )
    sidecar_used = False
    if sidecar is not None:
        document = apply_sidecar(document, sidecar)
        sidecar_used = True
    result, reflow_findings = global_reflow(
        document,
        processing_profile,
        sentence_end_min_fill=args.sentence_end_min_fill / 100.0,
        comma_min_fill=args.comma_min_fill / 100.0,
    )
    result_findings = validate_document(
        result,
        processing_profile,
        explicit_gap_limit=any(
            key in reflow_resolution.measured_only_keys
            for key in ("timing.minGapMs", "timing.minTimestampGapFrames")
        ),
        gap_timebase=reflow_resolution.gap_timebase,
        required_gap_frames=reflow_resolution.required_gap_frames,
    )
    if "readability.maxCps" not in reflow_resolution.measured_only_keys:
        result_findings = [
            finding
            for finding in result_findings
            if finding.code != "CPS_TOO_HIGH"
        ]
    measured_finding_codes: set[str] = set()
    if "readability.maxCps" in reflow_resolution.measured_only_keys:
        measured_finding_codes.add("CPS_TOO_HIGH")
    if any(
        key in reflow_resolution.measured_only_keys
        for key in ("timing.minGapMs", "timing.minTimestampGapFrames")
    ):
        measured_finding_codes.add("GAP_BELOW_PROFILE_LIMIT")
    result_findings = [
        Finding("WARNING", item.code, item.message, item.cue_ids)
        if item.code in measured_finding_codes
        else item
        for item in result_findings
    ]
    findings = [
        Finding(item.severity, item.code, item.message) for item in parse_diagnostics
    ] + reflow_findings + result_findings
    add_measurement_results(caption_settings, result_findings)
    output.parent.mkdir(parents=True, exist_ok=True)
    serialized = serialize_srt(result)
    output.write_text(serialized, encoding="utf-8", newline="\n")
    output_sidecar: str | None = None
    output_sidecar_path = expected_sidecar_path(output)
    if result.timing_fidelity in {
        TimingFidelity.WORD_ALIGNED_EXACT,
        TimingFidelity.WORD_ALIGNED_ESTIMATED,
    }:
        write_json(output_sidecar_path, create_sidecar(result, serialized.encode("utf-8")))
        output_sidecar = str(output_sidecar_path)
    else:
        # An overwrite may replace a previously word-aligned output with a
        # lower-fidelity result. Never leave its now-invalid hash-bound sidecar
        # beside the new SRT.
        output_sidecar_path.unlink(missing_ok=True)
    status = "FAILED" if any(item.severity == "ERROR" for item in findings) else (
        "SUCCEEDED_WITH_WARNINGS" if findings else "SUCCEEDED"
    )
    write_report_pair(
        ab_path(args.report_json),
        ab_path(args.report_markdown),
        findings,
        title="SRT reflow report",
        status=status,
        timing_fidelity=result.timing_fidelity.value,
        extra={
            "input": str(input_path),
            "output": str(output),
            "mode": "GLOBAL_REFLOW",
            "cueBoundariesChanged": True,
            "sentenceEndMinimumFillPercent": args.sentence_end_min_fill,
            "commaMinimumFillPercent": args.comma_min_fill,
            "timingSidecarStatus": sidecar_status,
            "timingSidecarUsed": sidecar_used,
            "outputTimingSidecar": output_sidecar,
            "captionSettings": caption_settings,
        },
    )
    print(f"RESULT|status={status}|cues={len(result.cues)}|output={output}")
    return 2 if status == "FAILED" else 0


def command_plan_translation(args: argparse.Namespace) -> int:
    input_path, (document, _) = _load_srt(args)
    target_profile = profile_for_target(args, args.target_language)
    planned_output: Path | None = None
    if args.output_folder:
        planned_output = ab_path(args.output_folder) / translated_output_name(
            str(input_path), args.target_language
        )
    batches = plan_translation_requests(
        document,
        target_language=args.target_language,
        mode=args.mode,
        context=LinguisticContext(
            project_context=args.project_context,
            known_names_and_terminology=args.known_names,
            task_instructions=args.translation_instructions,
            protected_terms=args.protected_terms,
        ),
        profile=target_profile,
        max_groups_per_batch=args.max_groups_per_batch,
        target_language_name=args.target_language_name,
        approach=args.approach,
    )
    state = {
        "version": 1,
        "input": str(ab_path(args.input)),
        "targetLanguage": args.target_language,
        "mode": args.mode.upper(),
        "approach": args.approach.upper(),
        "translationInstructions": args.translation_instructions,
        "protectedTerms": args.protected_terms,
        "profile": {
            "maxUnitsPerLine": target_profile.max_units_per_line,
            "maxLines": target_profile.max_lines,
            "maxCps": target_profile.max_cps,
            "minDurationMs": target_profile.min_duration_ms,
            "maxDurationMs": target_profile.max_duration_ms,
            "preferredGapMs": target_profile.preferred_gap_ms,
        },
        "batches": [],
        "skipOutput": bool(
            planned_output is not None
            and planned_output.exists()
            and not boolean_argument(args.overwrite)
        ),
        "plannedOutput": str(planned_output) if planned_output is not None else "",
    }
    if args.work_dir:
        work_dir = ab_path(args.work_dir)
        request_dir = work_dir / "requests"
        response_dir = work_dir / "responses"
        request_dir.mkdir(parents=True, exist_ok=True)
        response_dir.mkdir(parents=True, exist_ok=True)
        rows: list[str] = []
        for batch in ([] if state["skipOutput"] else batches):
            batch_id = batch["batchId"]
            request_path = request_dir / f"{batch_id}.json"
            response_path = response_dir / f"{batch_id}.response.json"
            write_json(request_path, batch)
            expected_ids = ",".join(group["id"] for group in batch["groups"])
            state["batches"].append(
                {
                    "id": batch_id,
                    "requestPath": str(request_path),
                    "responsePath": str(response_path),
                    "expectedGroupIds": [group["id"] for group in batch["groups"]],
                }
            )
            rows.append(
                "\t".join([batch_id, str(request_path), str(response_path), expected_ids])
            )
        output = work_dir / "state.json"
        write_json(output, state)
        if rows:
            print("\n".join(rows))
        plan_status = "SKIPPED_EXISTS" if state["skipOutput"] else "SUCCEEDED"
        print(
            f"PLAN|status={plan_status}|batches={len(state['batches'])}|state={output}",
            file=sys.stderr,
        )
    else:
        if not args.output:
            raise ValueError("Either --output or --work-dir is required.")
        output = ab_path(args.output)
        state["batches"] = batches
        write_json(output, state)
        print(f"RESULT|status=SUCCEEDED|batches={len(batches)}|output={output}")
    return 0


def condensed_output_name(source_name: str) -> str:
    return f"{safe_output_stem(Path(source_name).stem)}.condensed.srt"


def _profile_snapshot(profile: CaptionProfile) -> dict[str, object]:
    return {
        "maxUnitsPerLine": profile.max_units_per_line,
        "maxLines": profile.max_lines,
        "maxCps": profile.max_cps,
        "minDurationMs": profile.min_duration_ms,
        "maxDurationMs": profile.max_duration_ms,
        "preferredGapMs": profile.preferred_gap_ms,
    }


def _profile_from_snapshot(value: object) -> CaptionProfile:
    if not isinstance(value, dict):
        raise ValueError("Condensation state has no valid caption-settings snapshot.")
    profile = CaptionProfile(
        max_units_per_line=int(value["maxUnitsPerLine"]),
        max_lines=int(value["maxLines"]),
        max_cps=float(value["maxCps"]),
        min_duration_ms=int(value["minDurationMs"]),
        max_duration_ms=int(value["maxDurationMs"]),
        preferred_gap_ms=int(value.get("preferredGapMs", 80)),
    )
    profile.validate()
    return profile


def command_plan_condensation(args: argparse.Namespace) -> int:
    input_path, (document, _) = _load_srt(args)
    profile_resolution = _resolved_condensation_from_args(args)
    profile = profile_resolution.processing_profile
    caption_settings = workflow_settings_report(
        profile_resolution,
        profile_path=_workflow_profile_path(args),
    )
    output = (
        ab_path(args.output_folder) / condensed_output_name(str(input_path))
        if args.output_folder
        else None
    )
    skip_output = bool(
        output is not None and output.exists() and not boolean_argument(args.overwrite)
    )
    try:
        plan = plan_condensation_requests(
            document,
            profile=profile,
            context=LinguisticContext(
                project_context=args.project_context,
                known_names_and_terminology=args.known_names,
                task_instructions=args.condensation_instructions,
                protected_terms=args.protected_terms,
            ),
            max_groups_per_batch=args.max_groups_per_batch,
        )
    except ValueError as exc:
        if "must not overlap" in str(exc).casefold():
            raise ValueError(
                "This SRT contains overlapping captions. Run Analyze SRT Readability And "
                "Timing to review the overlaps before condensation."
            ) from exc
        raise
    state: dict[str, object] = {
        "version": 1,
        "input": str(input_path),
        "plannedOutput": str(output) if output is not None else "",
        "skipOutput": skip_output,
        "condensationInstructions": args.condensation_instructions,
        "protectedTerms": args.protected_terms,
        "profile": _profile_snapshot(profile),
        "captionSettings": caption_settings,
        "batches": [],
        "preparation": condensation_preparation_report(plan.preparation),
        "unusedProtectedTerms": list(plan.unused_protected_terms),
    }
    if args.work_dir:
        work_dir = ab_path(args.work_dir)
        request_dir = work_dir / "requests"
        response_dir = work_dir / "responses"
        request_dir.mkdir(parents=True, exist_ok=True)
        response_dir.mkdir(parents=True, exist_ok=True)
        rows: list[str] = []
        batch_state: list[dict[str, object]] = []
        for batch in (() if skip_output else plan.batches):
            batch_id = str(batch["batchId"])
            request_path = request_dir / f"{batch_id}.json"
            response_path = response_dir / f"{batch_id}.response.json"
            write_json(request_path, batch)
            expected_ids = [str(group["id"]) for group in batch["groups"]]
            batch_state.append(
                {
                    "id": batch_id,
                    "requestPath": str(request_path),
                    "responsePath": str(response_path),
                    "expectedGroupIds": expected_ids,
                }
            )
            rows.append(
                "\t".join(
                    [batch_id, str(request_path), str(response_path), ",".join(expected_ids)]
                )
            )
        state["batches"] = batch_state
        state_path = work_dir / "state.json"
        write_json(state_path, state)
        if rows:
            print("\n".join(rows))
        print(
            f"PLAN|status={'SKIPPED_EXISTS' if skip_output else 'SUCCEEDED'}"
            f"|batches={len(batch_state)}|state={state_path}",
            file=sys.stderr,
        )
    else:
        if not args.output:
            raise ValueError("Either --output or --work-dir is required.")
        state["batches"] = list(plan.batches)
        state_path = ab_path(args.output)
        write_json(state_path, state)
        print(
            f"RESULT|status={'SKIPPED_EXISTS' if skip_output else 'SUCCEEDED'}"
            f"|batches={len(plan.batches)}|output={state_path}"
        )
    return 0


def command_merge_condensation(args: argparse.Namespace) -> int:
    workflow_ai_attempted: bool | None = None
    workflow_ai_used = False
    caption_settings: dict[str, Any]
    processing_profile: CaptionProfile
    if args.state:
        state_path = ab_path(args.state)
        state = load_json(state_path)
        if not isinstance(state, dict):
            raise ValueError("Condensation state must be an object.")
        args.input = str(state["input"])
        args.condensation_instructions = str(
            state.get("condensationInstructions", "")
        )
        args.protected_terms = str(state.get("protectedTerms", ""))
        processing_profile = _profile_from_snapshot(state.get("profile"))
        raw_caption_settings = state.get("captionSettings")
        if not isinstance(raw_caption_settings, dict):
            raise ValueError("Condensation state has no caption-settings report.")
        caption_settings = raw_caption_settings
        if state.get("skipOutput"):
            print(
                f"RESULT|status=SKIPPED_EXISTS|cues=0|output={state.get('plannedOutput', '')}"
            )
            return 0
        batch_state = state.get("batches")
        if not isinstance(batch_state, list):
            raise ValueError("Condensation state has no batches array.")
        workflow_ai_attempted = bool(batch_state)
        responses: list[dict[str, object]] = []
        for batch in batch_state:
            if not isinstance(batch, dict) or not isinstance(
                batch.get("responsePath"), str
            ):
                raise ValueError("Condensation state batch is invalid.")
            response_path = Path(batch["responsePath"])
            if not response_path.exists():
                continue
            response = load_json(response_path)
            if not isinstance(response, dict):
                raise ValueError("Condensation response root must be an object.")
            responses.append(response)
        workflow_ai_used = bool(responses)
    else:
        if not args.responses:
            raise ValueError("Either --responses or --state is required.")
        value = load_json(ab_path(args.responses))
        raw_responses = value.get("responses") if isinstance(value, dict) else None
        if not isinstance(raw_responses, list) or not all(
            isinstance(response, dict) for response in raw_responses
        ):
            raise ValueError('Responses JSON must contain a "responses" array of objects.')
        responses = raw_responses
        workflow_ai_used = bool(responses)
        profile_resolution = _resolved_condensation_from_args(args)
        processing_profile = profile_resolution.processing_profile
        caption_settings = workflow_settings_report(
            profile_resolution,
            profile_path=_workflow_profile_path(args),
        )

    input_path, (document, _) = _load_srt(args)
    sidecar, sidecar_status = load_and_validate_sidecar(
        input_path,
        expected_language=args.language or None,
    )
    if sidecar is not None:
        document = apply_sidecar(document, sidecar)
    if workflow_ai_attempted is None:
        workflow_ai_attempted = bool(
            prepare_condensation(
                document,
                profile=processing_profile,
            ).workflow_ai_group_ids
        )
    try:
        merged = merge_condensation_responses(
            document,
            responses,
            profile=processing_profile,
            protected_terms=args.protected_terms,
        )
    except ValueError as exc:
        if "must not overlap" in str(exc).casefold():
            raise ValueError(
                "This SRT contains overlapping captions. Run Analyze SRT Readability And "
                "Timing to review the overlaps before condensation."
            ) from exc
        raise

    if args.output_folder:
        output = ab_path(args.output_folder) / condensed_output_name(str(input_path))
    elif args.output:
        output = ab_path(args.output)
    else:
        raise ValueError("Either --output or --output-folder is required.")
    if same_path(input_path, output):
        raise ValueError(
            "In-place condensation is not supported. Choose a separate output so the source remains recoverable."
        )
    run_dir = ab_path(args.state).parent if args.state else output.parent
    run_dir.mkdir(parents=True, exist_ok=True)
    prepared_srt = run_dir / "prepared-condensation.srt"
    serialized = serialize_srt(merged.document)
    prepared_srt.write_text(serialized, encoding="utf-8", newline="\n")
    prepared_sidecar: Path | None = None
    if merged.document.timing_fidelity.value == "WORD_ALIGNED_EXACT":
        prepared_sidecar = run_dir / "prepared-condensation.timing.json"
        write_json(
            prepared_sidecar,
            create_sidecar(merged.document, serialized.encode("utf-8")),
        )
    try:
        installation = install_output_pair(
            prepared_srt,
            output,
            prepared_sidecar=prepared_sidecar,
            overwrite=boolean_argument(args.overwrite),
        )
    finally:
        prepared_srt.unlink(missing_ok=True)
        if prepared_sidecar is not None:
            prepared_sidecar.unlink(missing_ok=True)

    report = condensation_preparation_report(
        merged,
        workflow_ai_used=workflow_ai_used,
        workflow_ai_attempted=workflow_ai_attempted,
    )
    report.update(
        {
            "input": str(input_path),
            "output": str(output),
            "installation": installation,
            "timingSidecarStatus": sidecar_status,
            "outputTimingSidecar": (
                str(expected_sidecar_path(output))
                if merged.document.timing_fidelity.value == "WORD_ALIGNED_EXACT"
                else None
            ),
            "captionSettings": caption_settings,
        }
    )
    report_json_path = ab_path(args.report_json) if args.report_json else None
    report_markdown_path = (
        ab_path(args.report_markdown) if args.report_markdown else None
    )
    if args.report_folder:
        report_folder = ab_path(args.report_folder)
        report_stem = safe_output_stem(input_path.stem)
        report_json_path = report_json_path or (
            report_folder / f"{report_stem}.condensation-report.json"
        )
        report_markdown_path = report_markdown_path or (
            report_folder / f"{report_stem}.condensation-report.md"
        )
    if report_json_path is not None:
        write_json(report_json_path, report)
    if report_markdown_path is not None:
        markdown_path = report_markdown_path
        markdown_path.parent.mkdir(parents=True, exist_ok=True)
        markdown_path.write_text(
            condensation_report_markdown(report),
            encoding="utf-8",
        )
    print(
        f"RESULT|status={report['status']}|cues={len(merged.document.cues)}"
        f"|rewritten={report['counts']['REWRITTEN']}"
        f"|redistributed={report['counts']['REDISTRIBUTED']}"
        f"|unresolved={report['counts']['UNRESOLVED']}|output={output}"
    )
    return 0 if report["status"] in {"COMPLETED", "PARTIAL"} else 2


def command_translation_preflight(args: argparse.Namespace) -> int:
    output_folder = ab_path(args.output_folder)
    languages = parse_language_specs(args.languages)
    if args.input_files_json:
        input_files_path = ab_path(args.input_files_json)
        raw_files = json.loads(input_files_path.read_text(encoding="utf-8-sig"))
        if not isinstance(raw_files, list):
            raise ValueError("The selected-files JSON must contain a list of file paths.")
        files = []
        seen_paths: set[str] = set()
        for raw_path in raw_files:
            if not isinstance(raw_path, str) or not raw_path.strip():
                raise ValueError("Every selected SRT path must be a non-empty string.")
            source = ab_path(raw_path)
            if source.suffix.casefold() != ".srt":
                raise ValueError(f"The selected file is not an .srt file: {source}")
            identity = str(source).casefold()
            if identity not in seen_paths:
                seen_paths.add(identity)
                files.append(source)
    else:
        input_folder = ab_path(args.input_folder)
        files = sorted(
            (
                path
                for path in input_folder.iterdir()
                if path.is_file() and path.suffix.casefold() == ".srt"
            ),
            key=lambda path: path.name.casefold(),
        )
    if not files:
        raise ValueError("No SRT files were selected for translation.")
    profile = profile_from_args(args)
    valid_files = 0
    invalid_files = 0
    sentence_groups = 0
    estimated_calls = 0
    skipped_outputs = 0
    overwrite = boolean_argument(args.overwrite)
    for source in files:
        try:
            document, _ = parse_srt(
                source.read_text(encoding="utf-8-sig"),
                source_name=source.stem,
            )
        except (OSError, ValueError, SrtParseError):
            invalid_files += 1
            continue
        groups = len(form_sentence_groups(document, profile))
        valid_files += 1
        sentence_groups += groups
        batches = math.ceil(groups / max(1, args.max_groups_per_batch))
        for language in languages:
            output = output_folder / translated_output_name(str(source), language.tag)
            if output.exists() and not overwrite:
                skipped_outputs += 1
            else:
                estimated_calls += batches
    context_sections = [
        label
        for label, supplied in (
            ("project context", bool(args.project_context.strip())),
            ("known names/terminology", bool(args.known_names.strip())),
            ("translation instructions", bool(args.translation_instructions.strip())),
            ("protected text", bool(args.protected_terms.strip())),
        )
        if supplied
    ]
    print(
        "\n".join(
            [
                "Translation plan",
                "",
                f"SRT files: {len(files)} ({valid_files} ready; {invalid_files} need attention)",
                f"Target languages: {len(languages)} ({', '.join(spec.tag for spec in languages)})",
                f"Sentence groups to translate: {sentence_groups}",
                f"Expected Workflow AI requests: {estimated_calls}",
                f"Existing translations skipped: {skipped_outputs}",
                f"Output folder: {output_folder}",
                f"Overwrite existing files: {overwrite}",
                f"Translation approach: {args.approach.upper()}",
                (
                    f"Caption layout: {profile.max_units_per_line} text units per line, "
                    f"{profile.max_lines} line(s), {profile.max_cps:g} characters per second"
                ),
                "Language-specific layout: "
                + "; ".join(
                    (
                        f"{spec.tag}={target_profile.max_units_per_line} text units per line, "
                        f"{target_profile.max_cps:g} characters per second"
                    )
                    for spec in languages
                    for target_profile in [profile_for_target(args, spec.tag)]
                ),
                (
                    "Context supplied: " + ", ".join(context_sections)
                    if context_sections
                    else "Context supplied: none"
                ),
            ]
        )
    )
    return 0


def command_merge_translation(args: argparse.Namespace) -> int:
    if args.state:
        state = load_json(ab_path(args.state))
        if not isinstance(state, dict):
            raise ValueError("Translation state must be an object.")
        args.input = str(state["input"])
        args.target_language = str(state["targetLanguage"])
        args.mode = str(state["mode"])
        args.approach = str(state.get("approach", "FAITHFUL"))
        args.translation_instructions = str(state.get("translationInstructions", ""))
        args.protected_terms = str(state.get("protectedTerms", ""))
        if state.get("skipOutput"):
            print(
                f"RESULT|status=SKIPPED_EXISTS|cues=0|output={state.get('plannedOutput', '')}"
            )
            return 0
        profile_value = state.get("profile", {})
        if isinstance(profile_value, dict):
            args.max_units_per_line = int(profile_value.get("maxUnitsPerLine", args.max_units_per_line))
            args.max_lines = int(profile_value.get("maxLines", args.max_lines))
            args.max_cps = float(profile_value.get("maxCps", args.max_cps))
            args.min_duration_ms = int(profile_value.get("minDurationMs", args.min_duration_ms))
            args.max_duration_ms = int(profile_value.get("maxDurationMs", args.max_duration_ms))
            args.preferred_gap_ms = int(profile_value.get("preferredGapMs", args.preferred_gap_ms))
        batch_state = state.get("batches")
        if not isinstance(batch_state, list):
            raise ValueError("Translation state has no batches array.")
        responses = []
        for batch in batch_state:
            if not isinstance(batch, dict) or not isinstance(batch.get("responsePath"), str):
                raise ValueError("Translation state batch is invalid.")
            response = load_json(Path(batch["responsePath"]))
            if not isinstance(response, dict):
                raise ValueError("Translation response root must be an object.")
            responses.append(response)
    else:
        if not args.responses:
            raise ValueError("Either --responses or --state is required.")
        value = load_json(ab_path(args.responses))
        responses = value.get("responses") if isinstance(value, dict) else None
        if not isinstance(responses, list):
            raise ValueError('Responses JSON must contain a "responses" array.')
    _, (document, _) = _load_srt(args)
    result, warnings = merge_translation_responses(
        document,
        responses,
        target_language=args.target_language,
        mode=args.mode,
        profile=profile_from_args(args),
        approach=args.approach,
        protected_terms=args.protected_terms,
    )
    if args.output_folder:
        output = ab_path(args.output_folder) / translated_output_name(args.input, result.language)
        prepared = ab_path(args.state).parent / "prepared-translation.srt" if args.state else output.parent / ".prepared-translation.srt"
        prepared.parent.mkdir(parents=True, exist_ok=True)
        prepared.write_text(serialize_srt(result), encoding="utf-8", newline="\n")
        try:
            installation = install_output_pair(
                prepared,
                output,
                prepared_sidecar=None,
                overwrite=boolean_argument(args.overwrite),
            )
        finally:
            prepared.unlink(missing_ok=True)
    else:
        if not args.output:
            raise ValueError("Either --output or --output-folder is required.")
        output = ab_path(args.output)
        output.parent.mkdir(parents=True, exist_ok=True)
        output.write_text(serialize_srt(result), encoding="utf-8", newline="\n")
        installation = {"status": "SRT_WRITTEN", "srt": str(output)}
    findings = [Finding("WARNING", "TRANSLATION_WARNING", warning) for warning in warnings]
    findings.extend(validate_document(result, profile_from_args(args)))
    status = "FAILED" if any(item.severity == "ERROR" for item in findings) else (
        "SUCCEEDED_WITH_WARNINGS" if findings else "SUCCEEDED"
    )
    approach_labels = {
        "FAITHFUL": "Faithful translation",
        "TRANSCREATION": "Marketing transcreation",
        "CUSTOM": "Follow my instructions",
    }
    normalized_approach = args.approach.upper()
    requested_protected_terms = parse_protected_terms(args.protected_terms)
    resolved_protected_terms, unused_protected_terms = resolve_document_protected_terms(
        document, profile_from_args(args), args.protected_terms
    )
    translation_task = {
        "approach": normalized_approach,
        "approachLabel": approach_labels[normalized_approach],
        "editorInstructions": args.translation_instructions.strip(),
        "requestedProtectedTerms": requested_protected_terms,
        "protectedTerms": resolved_protected_terms,
        "unusedProtectedTerms": unused_protected_terms,
        "humanTargetMarketReviewRequired": normalized_approach in {"TRANSCREATION", "CUSTOM"},
    }
    write_report_pair(
        ab_path(args.report_json) if args.report_json else None,
        ab_path(args.report_markdown) if args.report_markdown else None,
        findings,
        title="SRT translation report",
        status=installation["status"] if status != "FAILED" else status,
        timing_fidelity=result.timing_fidelity.value,
        extra={
            "output": str(output),
            "targetLanguage": result.language,
            "mode": args.mode.upper(),
            "translationTask": translation_task,
            "installation": installation,
        },
    )
    if args.output_folder:
        write_report_pair(
            output.parent / f"{output.name}.translation-report.json",
            output.parent / f"{output.name}.translation-report.md",
            findings,
            title="SRT translation report",
            status=installation["status"] if status != "FAILED" else status,
            timing_fidelity=result.timing_fidelity.value,
            extra={
                "output": str(output),
                "targetLanguage": result.language,
                "mode": args.mode.upper(),
                "translationTask": translation_task,
                "installation": installation,
            },
        )
    print(f"RESULT|status={installation['status']}|cues={len(result.cues)}|output={output}")
    return 2 if status == "FAILED" else 0


def command_normalize_languages(args: argparse.Namespace) -> int:
    languages = parse_language_specs(args.languages)
    print("\n".join(f"{spec.tag}\t{spec.display_name}" for spec in languages))
    print(f"LANGUAGES|status=SUCCEEDED|count={len(languages)}", file=sys.stderr)
    return 0


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Automation Agent SRT caption toolkit")
    subparsers = parser.add_subparsers(dest="command", required=True)

    profile_discover_parser = subparsers.add_parser("profile-discover")
    profile_discover_parser.add_argument("--folder", required=True)
    profile_discover_parser.add_argument("--output", required=True)
    profile_discover_parser.add_argument("--names-output")
    profile_discover_parser.add_argument("--paths-output")
    profile_discover_parser.add_argument("--issues-message-output")
    profile_discover_parser.add_argument("--label-prefix", default="")
    profile_discover_parser.set_defaults(handler=command_profile_discover)

    profile_inspect_parser = subparsers.add_parser("profile-inspect")
    profile_inspect_parser.add_argument("--input", required=True)
    profile_inspect_parser.add_argument("--output", required=True)
    profile_inspect_parser.add_argument("--form-folder")
    profile_inspect_parser.set_defaults(handler=command_profile_inspect)

    analysis_prefill_parser = subparsers.add_parser("analysis-prefill")
    analysis_prefill_parser.add_argument("--caption-profile", default="")
    analysis_prefill_parser.add_argument("--output", required=True)
    analysis_prefill_parser.add_argument("--form-folder", required=True)
    analysis_prefill_parser.set_defaults(handler=command_analysis_prefill)

    workflow_prefill_parser = subparsers.add_parser("workflow-profile-prefill")
    workflow_prefill_parser.add_argument(
        "--workflow",
        choices=["reflow", "condensation", "reference"],
        required=True,
    )
    workflow_prefill_parser.add_argument("--caption-profile", default="")
    workflow_prefill_parser.add_argument("--output", required=True)
    workflow_prefill_parser.add_argument("--form-folder", required=True)
    workflow_prefill_parser.set_defaults(handler=command_workflow_profile_prefill)

    profile_save_parser = subparsers.add_parser("profile-save")
    profile_save_destination = profile_save_parser.add_mutually_exclusive_group(
        required=True
    )
    profile_save_destination.add_argument("--output")
    profile_save_destination.add_argument("--output-folder")
    profile_save_parser.add_argument("--result-json")
    profile_save_parser.add_argument("--replace", nargs="?", const="true", default="false")
    profile_save_parser.add_argument("--name", required=True)
    profile_save_parser.add_argument("--notes", default="")
    profile_save_parser.add_argument("--max-units-per-line")
    profile_save_parser.add_argument("--max-lines")
    profile_save_parser.add_argument("--max-cps")
    profile_save_parser.add_argument("--min-duration-ms")
    profile_save_parser.add_argument("--max-duration-ms")
    profile_save_parser.add_argument("--min-gap-ms")
    profile_save_parser.add_argument("--frame-rate", default="")
    profile_save_parser.add_argument("--custom-frame-rate-numerator")
    profile_save_parser.add_argument("--custom-frame-rate-denominator")
    profile_save_parser.add_argument("--min-timestamp-gap-frames")
    profile_save_parser.add_argument("--align-to-frame-grid")
    profile_save_parser.set_defaults(handler=command_profile_save)

    profile_preview_parser = subparsers.add_parser("profile-preview")
    profile_preview_destination = profile_preview_parser.add_mutually_exclusive_group(
        required=True
    )
    profile_preview_destination.add_argument("--output")
    profile_preview_destination.add_argument("--output-folder")
    profile_preview_parser.add_argument("--summary-output", required=True)
    profile_preview_parser.add_argument("--target-output", required=True)
    profile_preview_parser.add_argument("--name", required=True)
    profile_preview_parser.add_argument("--notes", default="")
    profile_preview_parser.add_argument("--max-units-per-line")
    profile_preview_parser.add_argument("--max-lines")
    profile_preview_parser.add_argument("--max-cps")
    profile_preview_parser.add_argument("--min-duration-ms")
    profile_preview_parser.add_argument("--max-duration-ms")
    profile_preview_parser.add_argument("--min-gap-ms")
    profile_preview_parser.add_argument("--frame-rate", default="")
    profile_preview_parser.add_argument("--custom-frame-rate-numerator")
    profile_preview_parser.add_argument("--custom-frame-rate-denominator")
    profile_preview_parser.add_argument("--min-timestamp-gap-frames")
    profile_preview_parser.add_argument("--align-to-frame-grid")
    profile_preview_parser.set_defaults(handler=command_profile_preview)

    transcript_parser = subparsers.add_parser("transcript-to-srt")
    transcript_parser.add_argument("--input", required=True)
    transcript_parser.add_argument("--output")
    transcript_parser.add_argument("--output-folder")
    transcript_parser.add_argument("--run-dir")
    transcript_parser.add_argument("--timing-sidecar")
    transcript_parser.add_argument("--write-timing", nargs="?", const="true", default="false")
    transcript_parser.add_argument("--overwrite", nargs="?", const="true", default="false")
    transcript_parser.add_argument("--report-json")
    transcript_parser.add_argument("--report-markdown")
    transcript_parser.add_argument("--source-name", default="")
    add_profile_arguments(transcript_parser)
    transcript_parser.set_defaults(handler=command_transcript_to_srt)

    sequence_parser = subparsers.add_parser("sequence-manifest-to-srt")
    sequence_parser.add_argument("--input", required=True)
    sequence_parser.add_argument("--output")
    sequence_parser.add_argument("--output-folder")
    sequence_parser.add_argument("--run-dir")
    sequence_parser.add_argument("--report-json", required=True)
    sequence_parser.add_argument("--report-markdown", required=True)
    sequence_parser.add_argument("--chosen-track-index", type=int)
    sequence_parser.add_argument("--write-timing", nargs="?", const="true", default="false")
    sequence_parser.add_argument("--overwrite", nargs="?", const="true", default="false")
    add_profile_arguments(sequence_parser)
    sequence_parser.set_defaults(handler=command_sequence_manifest_to_srt)

    reference_sequence_parser = subparsers.add_parser("reference-sequence-to-srt")
    reference_sequence_parser.add_argument("--input", required=True)
    reference_sequence_parser.add_argument("--reference-text-file", required=True)
    reference_sequence_output = reference_sequence_parser.add_mutually_exclusive_group(
        required=True
    )
    reference_sequence_output.add_argument("--output")
    reference_sequence_output.add_argument("--output-folder")
    reference_sequence_parser.add_argument("--run-dir", required=True)
    reference_sequence_parser.add_argument("--report-json", required=True)
    reference_sequence_parser.add_argument("--report-markdown", required=True)
    reference_sequence_parser.add_argument("--overview-text")
    reference_sequence_parser.add_argument("--review-srt")
    reference_sequence_parser.add_argument(
        "--review-markers-mode",
        choices=["NONE", "WORDING", "WORDING_AND_ESTIMATED"],
        default="NONE",
    )
    reference_sequence_parser.add_argument("--chosen-track-index", type=int)
    reference_sequence_parser.add_argument(
        "--sentence-end-min-fill", type=float, default=60.0
    )
    reference_sequence_parser.add_argument(
        "--comma-min-fill", type=float, default=80.0
    )
    reference_sequence_parser.add_argument(
        "--write-timing", nargs="?", const="true", default="true"
    )
    reference_sequence_parser.add_argument(
        "--overwrite", nargs="?", const="true", default="false"
    )
    add_workflow_profile_arguments(reference_sequence_parser, include_cps=True)
    reference_sequence_parser.set_defaults(handler=command_reference_sequence_to_srt)

    sequence_init_parser = subparsers.add_parser("sequence-init-manifest")
    sequence_init_parser.add_argument("--output", required=True)
    sequence_init_parser.add_argument("--sequence-name", required=True)
    sequence_init_parser.add_argument("--frame-duration-seconds", type=float, required=True)
    sequence_init_parser.add_argument("--sequence-start-seconds", type=float, required=True)
    sequence_init_parser.add_argument("--sequence-end-seconds", type=float, required=True)
    sequence_init_parser.set_defaults(handler=command_sequence_init_manifest)

    sequence_item_parser = subparsers.add_parser("sequence-add-item")
    sequence_item_parser.add_argument("--manifest", required=True)
    sequence_item_parser.add_argument("--id", required=True)
    sequence_item_parser.add_argument("--track-index", type=int, required=True)
    sequence_item_parser.add_argument("--track-name", default="")
    sequence_item_parser.add_argument("--item-name", default="")
    sequence_item_parser.add_argument("--project-item-ref", default="")
    sequence_item_parser.add_argument("--timeline-start-seconds", type=float, required=True)
    sequence_item_parser.add_argument("--timeline-end-seconds", type=float, required=True)
    sequence_item_parser.add_argument("--source-in-seconds", type=float, required=True)
    sequence_item_parser.add_argument("--source-out-seconds", type=float, required=True)
    sequence_item_parser.add_argument("--speed", type=float, required=True)
    sequence_item_parser.add_argument("--reversed", required=True)
    sequence_item_parser.add_argument("--disabled", required=True)
    sequence_item_parser.add_argument("--track-muted", required=True)
    sequence_item_parser.add_argument("--transcript-path")
    sequence_item_parser.add_argument("--discovery-warning", default="")
    sequence_item_parser.set_defaults(handler=command_sequence_add_item)

    cut_init_parser = subparsers.add_parser("cut-manifest-init")
    cut_init_parser.add_argument("--output", required=True)
    cut_init_parser.add_argument("--sequence-name", required=True)
    cut_init_parser.add_argument("--sequence-timebase-ticks", type=int, required=True)
    cut_init_parser.add_argument("--srt-zero-sequence-seconds", required=True)
    cut_init_parser.add_argument("--reference-track-index", type=int, required=True)
    cut_init_parser.add_argument("--reference-track-name", default="")
    cut_init_parser.add_argument("--reference-track-muted", required=True)
    cut_init_parser.set_defaults(handler=command_cut_manifest_init)

    cut_item_parser = subparsers.add_parser("cut-manifest-add-item")
    cut_item_parser.add_argument("--manifest", required=True)
    cut_item_parser.add_argument("--id", required=True)
    cut_item_parser.add_argument("--start-seconds", required=True)
    cut_item_parser.add_argument("--end-seconds", required=True)
    cut_item_parser.add_argument("--disabled", required=True)
    cut_item_parser.set_defaults(handler=command_cut_manifest_add_item)

    caption_init_parser = subparsers.add_parser("caption-timing-manifest-init")
    caption_init_parser.add_argument("--output", required=True)
    caption_init_parser.add_argument("--sequence-name", required=True)
    caption_init_parser.add_argument("--sequence-timebase-ticks", type=int, required=True)
    caption_init_parser.add_argument("--caption-track-index", type=int, required=True)
    caption_init_parser.add_argument("--caption-track-name", default="")
    caption_init_parser.set_defaults(handler=command_caption_timing_manifest_init)

    caption_item_parser = subparsers.add_parser("caption-timing-manifest-add-item")
    caption_item_parser.add_argument("--manifest", required=True)
    caption_item_parser.add_argument("--id", required=True)
    caption_item_parser.add_argument("--index", type=int, required=True)
    caption_item_parser.add_argument("--start-seconds", required=True)
    caption_item_parser.add_argument("--end-seconds", required=True)
    caption_item_parser.set_defaults(handler=command_caption_timing_manifest_add_item)

    direct_plan_parser = subparsers.add_parser("plan-direct-caption-alignment")
    direct_plan_parser.add_argument("--caption-manifest", required=True)
    direct_plan_parser.add_argument("--cut-manifest", required=True)
    direct_plan_parser.add_argument("--output", required=True)
    direct_plan_parser.add_argument("--actions-tsv", default="")
    direct_plan_parser.add_argument("--summary-tsv", default="")
    direct_plan_parser.add_argument("--report-markdown", default="")
    direct_plan_parser.add_argument("--overview-text", default="")
    direct_plan_parser.add_argument("--max-distance-frames", type=int, required=True)
    direct_duration_group = direct_plan_parser.add_mutually_exclusive_group()
    direct_duration_group.add_argument("--min-duration-ms", type=int)
    direct_duration_group.add_argument("--min-duration-frames", type=int)
    direct_gap_group = direct_plan_parser.add_mutually_exclusive_group()
    direct_gap_group.add_argument("--min-gap-ms", type=int)
    direct_gap_group.add_argument("--min-gap-frames", type=int)
    direct_plan_parser.set_defaults(handler=command_plan_direct_caption_alignment)

    direct_verify_parser = subparsers.add_parser("verify-direct-caption-alignment")
    direct_verify_parser.add_argument("--plan", required=True)
    direct_verify_parser.add_argument("--caption-manifest", required=True)
    direct_verify_parser.add_argument("--output", required=True)
    direct_verify_parser.add_argument("--report-markdown", default="")
    direct_verify_parser.add_argument("--overview-text", default="")
    direct_verify_parser.set_defaults(handler=command_verify_direct_caption_alignment)

    align_parser = subparsers.add_parser("align-to-cuts")
    align_parser.add_argument("--input", required=True)
    align_parser.add_argument("--cut-manifest", required=True)
    align_parser.add_argument("--output", required=True)
    align_parser.add_argument("--run-dir", required=True)
    align_parser.add_argument("--report-json", required=True)
    align_parser.add_argument("--report-markdown", required=True)
    align_parser.add_argument("--overview-text", default="")
    align_parser.add_argument("--max-distance-frames", type=int, required=True)
    align_duration_group = align_parser.add_mutually_exclusive_group()
    align_duration_group.add_argument("--min-duration-ms", type=int)
    align_duration_group.add_argument("--min-duration-frames", type=int)
    align_gap_group = align_parser.add_mutually_exclusive_group()
    align_gap_group.add_argument("--min-gap-ms", type=int)
    align_gap_group.add_argument("--min-gap-frames", type=int)
    align_parser.add_argument("--language", default="")
    align_parser.add_argument("--overwrite", nargs="?", const="true", default="false")
    align_parser.set_defaults(handler=command_align_to_cuts)

    validate_parser = subparsers.add_parser("validate")
    validate_parser.add_argument("--input", required=True)
    validate_parser.add_argument("--report-json", required=True)
    validate_parser.add_argument("--report-markdown", required=True)
    validate_parser.add_argument("--overview-text")
    validate_parser.add_argument("--review-srt")
    validate_parser.add_argument("--language", default="")
    add_analysis_profile_arguments(validate_parser)
    validate_parser.set_defaults(handler=command_validate)

    reflow_parser = subparsers.add_parser("reflow")
    reflow_parser.add_argument("--input", required=True)
    reflow_parser.add_argument("--output", required=True)
    reflow_parser.add_argument("--report-json", required=True)
    reflow_parser.add_argument("--report-markdown", required=True)
    reflow_parser.add_argument("--language", default="")
    reflow_parser.add_argument("--sentence-end-min-fill", type=float, default=60.0)
    reflow_parser.add_argument("--comma-min-fill", type=float, default=80.0)
    reflow_parser.add_argument("--overwrite", nargs="?", const="true", default="false")
    add_workflow_profile_arguments(reflow_parser, include_cps=False)
    reflow_parser.set_defaults(handler=command_reflow)

    plan_parser = subparsers.add_parser("plan-translation")
    plan_parser.add_argument("--input", required=True)
    plan_parser.add_argument("--output")
    plan_parser.add_argument("--work-dir")
    plan_parser.add_argument("--output-folder")
    plan_parser.add_argument("--overwrite", nargs="?", const="true", default="false")
    plan_parser.add_argument("--target-language", required=True)
    plan_parser.add_argument("--target-language-name", default="")
    plan_parser.add_argument("--cjk-max-units-per-line", type=int, default=0)
    plan_parser.add_argument("--cjk-max-cps", type=float, default=0)
    plan_parser.add_argument("--mode", choices=["PRESERVE_CUES", "OPTIMIZE_TARGET"], default="PRESERVE_CUES")
    plan_parser.add_argument("--approach", choices=["FAITHFUL", "TRANSCREATION", "CUSTOM"], default="FAITHFUL")
    plan_parser.add_argument("--project-context", default="")
    plan_parser.add_argument("--known-names", default="")
    plan_parser.add_argument("--translation-instructions", default="")
    plan_parser.add_argument("--protected-terms", default="")
    plan_parser.add_argument("--max-groups-per-batch", type=int, default=25)
    plan_parser.add_argument("--language", default="")
    add_profile_arguments(plan_parser)
    plan_parser.set_defaults(handler=command_plan_translation)

    preflight_parser = subparsers.add_parser("translation-preflight")
    preflight_inputs = preflight_parser.add_mutually_exclusive_group(required=True)
    preflight_inputs.add_argument("--input-folder")
    preflight_inputs.add_argument("--input-files-json")
    preflight_parser.add_argument("--output-folder", required=True)
    preflight_parser.add_argument("--languages", required=True)
    preflight_parser.add_argument("--overwrite", nargs="?", const="true", default="false")
    preflight_parser.add_argument("--max-groups-per-batch", type=int, default=25)
    preflight_parser.add_argument("--project-context", default="")
    preflight_parser.add_argument("--known-names", default="")
    preflight_parser.add_argument("--translation-instructions", default="")
    preflight_parser.add_argument("--protected-terms", default="")
    preflight_parser.add_argument("--approach", choices=["FAITHFUL", "TRANSCREATION", "CUSTOM"], default="FAITHFUL")
    preflight_parser.add_argument("--cjk-max-units-per-line", type=int, default=0)
    preflight_parser.add_argument("--cjk-max-cps", type=float, default=0)
    add_profile_arguments(preflight_parser)
    preflight_parser.set_defaults(handler=command_translation_preflight)

    merge_parser = subparsers.add_parser("merge-translation")
    merge_parser.add_argument("--input")
    merge_parser.add_argument("--responses")
    merge_parser.add_argument("--state")
    merge_parser.add_argument("--output")
    merge_parser.add_argument("--output-folder")
    merge_parser.add_argument("--overwrite", nargs="?", const="true", default="false")
    merge_parser.add_argument("--target-language")
    merge_parser.add_argument("--mode", choices=["PRESERVE_CUES", "OPTIMIZE_TARGET"], default="PRESERVE_CUES")
    merge_parser.add_argument("--approach", choices=["FAITHFUL", "TRANSCREATION", "CUSTOM"], default="FAITHFUL")
    merge_parser.add_argument("--translation-instructions", default="")
    merge_parser.add_argument("--protected-terms", default="")
    merge_parser.add_argument("--report-json")
    merge_parser.add_argument("--report-markdown")
    merge_parser.add_argument("--language", default="")
    add_profile_arguments(merge_parser)
    merge_parser.set_defaults(handler=command_merge_translation)

    condensation_plan_parser = subparsers.add_parser("plan-condensation")
    condensation_plan_parser.add_argument("--input", required=True)
    condensation_plan_parser.add_argument("--output")
    condensation_plan_parser.add_argument("--work-dir")
    condensation_plan_parser.add_argument("--output-folder")
    condensation_plan_parser.add_argument(
        "--overwrite", nargs="?", const="true", default="false"
    )
    condensation_plan_parser.add_argument("--project-context", default="")
    condensation_plan_parser.add_argument("--known-names", default="")
    condensation_plan_parser.add_argument("--condensation-instructions", default="")
    condensation_plan_parser.add_argument("--protected-terms", default="")
    condensation_plan_parser.add_argument(
        "--max-groups-per-batch", type=int, default=25
    )
    condensation_plan_parser.add_argument("--language", default="")
    add_workflow_profile_arguments(condensation_plan_parser, include_cps=True)
    condensation_plan_parser.set_defaults(handler=command_plan_condensation)

    condensation_merge_parser = subparsers.add_parser("merge-condensation")
    condensation_merge_parser.add_argument("--input")
    condensation_merge_parser.add_argument("--responses")
    condensation_merge_parser.add_argument("--state")
    condensation_merge_parser.add_argument("--output")
    condensation_merge_parser.add_argument("--output-folder")
    condensation_merge_parser.add_argument(
        "--overwrite", nargs="?", const="true", default="false"
    )
    condensation_merge_parser.add_argument("--condensation-instructions", default="")
    condensation_merge_parser.add_argument("--protected-terms", default="")
    condensation_merge_parser.add_argument("--report-json")
    condensation_merge_parser.add_argument("--report-markdown")
    condensation_merge_parser.add_argument("--report-folder")
    condensation_merge_parser.add_argument("--language", default="")
    add_workflow_profile_arguments(condensation_merge_parser, include_cps=True)
    condensation_merge_parser.set_defaults(handler=command_merge_condensation)

    languages_parser = subparsers.add_parser("normalize-languages")
    languages_parser.add_argument("--languages", required=True)
    languages_parser.set_defaults(handler=command_normalize_languages)
    return parser


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()
    try:
        return int(args.handler(args))
    except (OSError, ValueError, json.JSONDecodeError, SrtParseError) as exc:
        print(f"ERROR|type={type(exc).__name__}|message={str(exc)[:1000]}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
