from __future__ import annotations

import json
import re
import sys
from dataclasses import dataclass
from difflib import SequenceMatcher
from pathlib import Path
from typing import Any, Iterable

from model import Passage, Token
from session import plan_session_order


PROFILE_VERSION = "offline-repeated-takes-v0.4"
CONTRACT_VERSION = 1
MIN_MATCHED_TOKENS = 4
MIN_INFORMATIVE_TOKENS = 3
FULL_SIMILARITY = 0.77
CHARACTER_FUZZY_SIMILARITY = 0.80
PARTIAL_SIMILARITY = 0.72
PARTIAL_COVERAGE = 0.72
PAUSE_BOUNDARY_MS = 900
DEFAULT_GROUP_GAP_MS = 750
MIN_GROUP_GAP_MS = 250
MISSING_REFERENCE_SLOT_MS = 1000

def _explicit_asset_path(option: str) -> Path | None:
    try:
        option_index = sys.argv.index(option)
    except ValueError:
        return None
    if option_index + 1 >= len(sys.argv):
        raise RuntimeError(f"{option} requires a folder path.")
    return Path(sys.argv[option_index + 1]).resolve()


_ASSET_ROOT = Path(__file__).resolve().parent
_SRT_TOOLKIT = _explicit_asset_path("--srt-toolkit-dir") or (
    _ASSET_ROOT.parent / "srt-caption-toolkit"
)
_LEXICAL_ALIGNMENT = _explicit_asset_path("--lexical-alignment-dir") or (
    _ASSET_ROOT.parent / "lexical-alignment"
)


def require_srt_toolkit(path: Path) -> None:
    required = (
        "transcript.py",
        "caption_profile.py",
        "language.py",
        "timed_text.py",
    )
    missing = [name for name in required if not (path / name).is_file()]
    if missing:
        raise RuntimeError(
            "Repeated-take alignment requires the complete sibling "
            f"'srt-caption-toolkit' asset; missing: {', '.join(missing)}."
        )


require_srt_toolkit(_SRT_TOOLKIT)
if str(_SRT_TOOLKIT) not in sys.path:
    sys.path.insert(0, str(_SRT_TOOLKIT))


def require_lexical_alignment(path: Path) -> None:
    if not (path / "lexical_alignment.py").is_file():
        raise RuntimeError(
            "Repeated-take alignment requires the complete sibling "
            "'lexical-alignment' asset; missing: lexical_alignment.py."
        )


require_lexical_alignment(_LEXICAL_ALIGNMENT)
if str(_LEXICAL_ALIGNMENT) not in sys.path:
    sys.path.insert(0, str(_LEXICAL_ALIGNMENT))

from transcript import (  # noqa: E402
    SENTENCE_END_RE,
    extract_transcript_words,
    iter_transcript_word_records,
)
from lexical_alignment import (  # noqa: E402
    anchored_candidate_indices,
    build_informative_index,
    local_window_alignments,
    measure_alignment,
    normalize_word,
)


WORD_RE = re.compile(r"[^\W_]+(?:['’\-][^\W_]+)*", re.UNICODE)
HYPHEN_RE = re.compile(r"[-‐‑‒–—]")
LOW_INFORMATION_WORDS = {
    "a", "an", "and", "are", "as", "at", "be", "but", "der", "die", "das",
    "ein", "eine", "er", "for", "halt", "i", "ich", "im", "in", "is", "it",
    "ja", "me", "mit", "of", "on", "or", "so", "the", "to", "und", "was",
    "we", "what", "you", "äh", "ähm", "hm", "know", "mean", "du", "weißt",
    "meine",
}
NUMBER_WORDS = {
    "zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
    "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
    "sixteen", "seventeen", "eighteen", "nineteen", "twenty", "thirty",
    "forty", "fifty", "sixty", "seventy", "eighty", "ninety", "hundred",
    "null", "eins", "ein", "eine", "zwei", "drei", "vier", "fünf", "sechs",
    "sieben", "acht", "neun", "zehn", "elf", "zwölf", "dreizehn", "vierzehn",
    "fünfzehn", "sechzehn", "siebzehn", "achtzehn", "neunzehn", "zwanzig",
    "dreißig", "vierzig", "fünfzig", "sechzig", "siebzig", "achtzig",
    "neunzig", "hundert",
}


@dataclass(frozen=True)
class MatchEvidence:
    matched_tokens: int
    similarity: float
    character_similarity: float
    reference_coverage: float
    occurrence_coverage: float
    reference_start: int
    occurrence_start: int
    anchor_size: int
    match_class: str


def parse_premiere_transcript(
    transcript: dict[str, Any], *, source_id: str, source_order: int
) -> list[Token]:
    """Parse Premiere JSON with the shared SRT toolkit timing rules."""
    timed_words = extract_transcript_words(transcript)
    raw_words = [record for _, _, record in iter_transcript_word_records(transcript)]
    if len(timed_words) != len(raw_words):
        raise ValueError("Premiere transcript word mapping is inconsistent.")
    return [
        Token(
            source_id=source_id,
            source_order=source_order,
            token_id=f"{source_id}-w{index:06d}",
            raw_text=timed.text,
            normalized=normalize_word(timed.text),
            start_ms=timed.start_ms,
            end_ms=timed.end_ms,
            sentence_end=bool(SENTENCE_END_RE.search(timed.text)),
        )
        for index, timed in enumerate(timed_words)
    ]


def segment_tokens(tokens: Iterable[Token]) -> list[Passage]:
    """Split only within a source; source boundaries can never be crossed."""
    passages: list[Passage] = []
    current: list[Token] = []
    for token in tokens:
        if current and token.source_id != current[-1].source_id:
            passages.append(_passage(current, len(passages)))
            current = []
        if current and token.start_ms - current[-1].end_ms >= PAUSE_BOUNDARY_MS:
            passages.append(_passage(current, len(passages)))
            current = []
        current.append(token)
        if token.sentence_end:
            passages.append(_passage(current, len(passages)))
            current = []
    if current:
        passages.append(_passage(current, len(passages)))
    return [passage for passage in passages if passage.normalized]


def _passage(tokens: list[Token], index: int) -> Passage:
    first = tokens[0]
    return Passage(
        passage_id=f"{first.source_id}-p{index:06d}",
        source_id=first.source_id,
        source_order=first.source_order,
        tokens=tuple(tokens),
    )


def reference_passages(text: str) -> list[tuple[str, tuple[str, ...]]]:
    units: list[tuple[str, tuple[str, ...]]] = []
    chunks = re.split(r"(?:\r?\n)+|(?<=[.!?])\s+", text.strip())
    for chunk in chunks:
        spoken_chunk = HYPHEN_RE.sub(" ", chunk)
        words = tuple(
            normalize_word(match.group(0))
            for match in WORD_RE.finditer(spoken_chunk)
        )
        words = tuple(word for word in words if word)
        if words:
            units.append((chunk.strip(), words))
    return units


def compare_tokens(reference: tuple[str, ...], occurrence: tuple[str, ...]) -> MatchEvidence:
    metrics = measure_alignment(reference, occurrence)
    matched = metrics.matched_tokens
    reference_coverage = metrics.reference_coverage
    occurrence_coverage = metrics.occurrence_coverage
    similarity = metrics.token_similarity
    character_similarity = metrics.character_similarity
    match_class = "REJECT_LOW_SIMILARITY"
    informative_reference = sum(token not in LOW_INFORMATION_WORDS for token in reference)
    informative_occurrence = sum(token not in LOW_INFORMATION_WORDS for token in occurrence)
    terminal_conflict = (
        bool(reference)
        and bool(occurrence)
        and reference[-1] != occurrence[-1]
        and reference[-1] not in LOW_INFORMATION_WORDS
        and occurrence[-1] not in LOW_INFORMATION_WORDS
        and SequenceMatcher(a=reference[-1], b=occurrence[-1], autojunk=False).ratio() < 0.55
        and min(reference_coverage, occurrence_coverage) >= 0.75
    )
    changed_reference: list[str] = []
    changed_occurrence: list[str] = []
    for tag, reference_start, reference_end, occurrence_start, occurrence_end in (
        metrics.opcodes
    ):
        if tag != "equal":
            changed_reference.extend(reference[reference_start:reference_end])
            changed_occurrence.extend(occurrence[occurrence_start:occurrence_end])
    informative_changed_reference = [
        token for token in changed_reference if token not in LOW_INFORMATION_WORDS
    ]
    informative_changed_occurrence = [
        token for token in changed_occurrence if token not in LOW_INFORMATION_WORDS
    ]
    replace_opcodes = [
        opcode for opcode in metrics.opcodes if opcode[0] == "replace"
    ]
    single_informative_substitution_conflict = (
        len(reference) == len(occurrence)
        and len(replace_opcodes) == 1
        and replace_opcodes[0][2] - replace_opcodes[0][1] == 1
        and replace_opcodes[0][4] - replace_opcodes[0][3] == 1
        and reference[replace_opcodes[0][1]] not in LOW_INFORMATION_WORDS
        and occurrence[replace_opcodes[0][3]] not in LOW_INFORMATION_WORDS
        and SequenceMatcher(
            a=reference[replace_opcodes[0][1]],
            b=occurrence[replace_opcodes[0][3]],
            autojunk=False,
        ).ratio()
        < 0.55
        and min(reference_coverage, occurrence_coverage) >= 0.75
    )
    has_changed_number = (
        {
            token
            for token in informative_changed_reference
            if token.isdigit() or token in NUMBER_WORDS
        }
        != {
            token
            for token in informative_changed_occurrence
            if token.isdigit() or token in NUMBER_WORDS
        }
        and any(
            token.isdigit() or token in NUMBER_WORDS
            for token in informative_changed_reference + informative_changed_occurrence
        )
    )
    changed_number_conflict = (
        min(reference_coverage, occurrence_coverage) >= 0.75
        and has_changed_number
    )
    supported_short_pickup = (
        max(reference_coverage, occurrence_coverage) >= 0.99
        and max(informative_reference, informative_occurrence) >= 4
        and metrics.anchor_size >= MIN_MATCHED_TOKENS
    )
    if matched < MIN_MATCHED_TOKENS:
        match_class = "REJECT_TOO_SHORT"
    elif (
        min(informative_reference, informative_occurrence) < MIN_INFORMATIVE_TOKENS
        and not supported_short_pickup
    ):
        match_class = "REJECT_TOO_COMMON"
    elif (
        terminal_conflict
        or single_informative_substitution_conflict
        or changed_number_conflict
    ):
        match_class = "AMBIGUOUS"
    elif similarity >= FULL_SIMILARITY and min(reference_coverage, occurrence_coverage) >= 0.72:
        match_class = "ACCEPT_FULL"
    elif (
        character_similarity >= CHARACTER_FUZZY_SIMILARITY
        and matched >= 5
        and min(reference_coverage, occurrence_coverage) >= 0.5
        and metrics.anchor_size >= 3
    ):
        match_class = "ACCEPT_FULL"
    elif (
        max(reference_coverage, occurrence_coverage) >= 0.84
        and metrics.anchor_size >= MIN_MATCHED_TOKENS
        and (
            similarity >= PARTIAL_SIMILARITY
            or min(reference_coverage, occurrence_coverage) < PARTIAL_COVERAGE
        )
    ):
        match_class = "ACCEPT_PARTIAL"
    return MatchEvidence(
        matched_tokens=matched,
        similarity=round(similarity, 6),
        character_similarity=round(character_similarity, 6),
        reference_coverage=round(reference_coverage, 6),
        occurrence_coverage=round(occurrence_coverage, 6),
        reference_start=metrics.reference_start,
        occurrence_start=metrics.occurrence_start,
        anchor_size=metrics.anchor_size,
        match_class=match_class,
    )


def _accepted(evidence: MatchEvidence) -> bool:
    return evidence.match_class in {"ACCEPT_FULL", "ACCEPT_PARTIAL"}


def _overall_similarity(evidence: MatchEvidence) -> float:
    return max(evidence.similarity, evidence.character_similarity)


def _build_informative_word_index(
    passages: list[Passage],
) -> dict[str, set[int]]:
    return build_informative_index(
        (passage.normalized for passage in passages),
        LOW_INFORMATION_WORDS,
    )


def _anchored_candidate_indices(
    words: tuple[str, ...],
    index: dict[str, set[int]],
    *,
    maximum_anchor_words: int = 64,
) -> set[int]:
    return anchored_candidate_indices(
        words,
        index,
        LOW_INFORMATION_WORDS,
        maximum_anchor_words=maximum_anchor_words,
    )


def _cluster_self_discovered(
    passages: list[Passage],
) -> list[tuple[list[Passage], list[dict[str, Any]]]]:
    # A longest-passage star admits complementary head/tail pickups through one
    # well-supported full delivery without arbitrary Union-Find chain merging.
    remaining = set(range(len(passages)))
    ordered: list[tuple[list[Passage], list[dict[str, Any]]]] = []
    representatives = sorted(
        range(len(passages)),
        key=lambda index: (
            -len(passages[index].normalized),
            passages[index].source_order,
            passages[index].start_ms,
        ),
    )
    word_index = _build_informative_word_index(passages)
    for representative_index in representatives:
        if representative_index not in remaining:
            continue
        representative = passages[representative_index]
        group = [representative]
        ambiguities: list[dict[str, Any]] = []
        remaining.remove(representative_index)
        candidate_indices = _anchored_candidate_indices(
            representative.normalized, word_index
        )
        for candidate_index in sorted(remaining.intersection(candidate_indices)):
            candidate = passages[candidate_index]
            evidence = compare_tokens(representative.normalized, candidate.normalized)
            if _accepted(evidence):
                group.append(candidate)
            elif evidence.match_class == "AMBIGUOUS":
                ambiguities.append(_ambiguity_record(candidate, evidence))
        remaining.difference_update(
            index for index, passage in enumerate(passages) if passage in group
        )
        ordered.append((group, ambiguities))
    ordered.sort(
        key=lambda item: min(
            (passage.source_order, passage.start_ms) for passage in item[0]
        )
    )
    return ordered


def _reference_groups(
    reference_text: str, passages: list[Passage]
) -> list[dict[str, Any]]:
    units = reference_passages(reference_text)
    raw_assignments: list[list[tuple[Passage, MatchEvidence]]] = [
        [] for _ in units
    ]
    ambiguities: list[list[dict[str, Any]]] = [[] for _ in units]
    word_index = _build_informative_word_index(passages)
    for unit_index, (_, normalized) in enumerate(units):
        candidate_indices = _anchored_candidate_indices(normalized, word_index)
        for passage_index in sorted(candidate_indices):
            passage = passages[passage_index]
            accepted, ambiguous = _local_reference_windows(
                normalized, passage, unit_index
            )
            raw_assignments[unit_index].extend(accepted)
            ambiguities[unit_index].extend(
                _ambiguity_record(candidate, evidence)
                for candidate, evidence in ambiguous
            )
    assignments: list[list[Passage]] = [[] for _ in units]
    accepted_across_units: list[tuple[int, Passage]] = []
    ranked = [
        (unit_index, passage, evidence)
        for unit_index, candidates in enumerate(raw_assignments)
        for passage, evidence in candidates
    ]
    ranked.sort(
        key=lambda item: (
            -item[2].matched_tokens,
            -item[2].reference_coverage,
            -(1 if item[2].match_class == "ACCEPT_FULL" else 0),
            -_overall_similarity(item[2]),
            -len(units[item[0]][1]),
            item[0],
            item[1].source_order,
            item[1].start_ms,
            item[1].end_ms,
        )
    )
    for unit_index, passage, _ in ranked:
        conflicts = any(
            other_unit != unit_index
            and other.source_id == passage.source_id
            and _source_time_overlaps(other, passage)
            for other_unit, other in accepted_across_units
        )
        if conflicts:
            continue
        assignments[unit_index].append(passage)
        accepted_across_units.append((unit_index, passage))
    for assigned in assignments:
        assigned.sort(key=lambda item: (item.source_order, item.start_ms, item.end_ms))
    return [
        {
            "referenceUnitId": f"reference-{index:04d}",
            "text": text,
            "normalized": normalized,
            "passages": assignments[index],
            "ambiguities": ambiguities[index],
        }
        for index, (text, normalized) in enumerate(units)
    ]


def _local_reference_windows(
    reference: tuple[str, ...], passage: Passage, unit_index: int
) -> tuple[
    list[tuple[Passage, MatchEvidence]],
    list[tuple[Passage, MatchEvidence]],
]:
    """Find independent local matches so one ASR sentence can serve many units."""
    tokens = passage.normalized_tokens
    if not tokens:
        return [], []
    whole_passage_evidence = compare_tokens(
        reference, tuple(token.normalized for token in tokens)
    )
    if (
        whole_passage_evidence.match_class == "ACCEPT_FULL"
        and whole_passage_evidence.reference_coverage >= 0.95
        and whole_passage_evidence.occurrence_coverage >= 0.95
    ):
        return [
            (
                Passage(
                    passage_id=f"{passage.passage_id}-r{unit_index:04d}-o000",
                    source_id=passage.source_id,
                    source_order=passage.source_order,
                    tokens=tuple(tokens),
                ),
                whole_passage_evidence,
            )
        ], []
    candidates: list[
        tuple[tuple[int, float, float, int, float, int], int, int, MatchEvidence]
    ] = []
    ambiguous: list[
        tuple[tuple[float, float, int, float, int], int, int, MatchEvidence]
    ] = []
    for window in local_window_alignments(
        reference,
        tuple(token.normalized for token in tokens),
        LOW_INFORMATION_WORDS,
        minimum_window_tokens=MIN_MATCHED_TOKENS,
    ):
        start = window.start
        end = window.end
        evidence = compare_tokens(
            reference, tuple(token.normalized for token in tokens[start:end])
        )
        if _accepted(evidence):
            candidates.append(
                (
                    (
                        1 if evidence.match_class == "ACCEPT_FULL" else 0,
                        evidence.reference_coverage,
                        _overall_similarity(evidence),
                        evidence.matched_tokens,
                        evidence.occurrence_coverage,
                        -abs((end - start) - len(reference)),
                    ),
                    start,
                    end,
                    evidence,
                )
            )
        elif evidence.match_class == "AMBIGUOUS":
            ambiguous.append(
                (
                    (
                        evidence.reference_coverage,
                        _overall_similarity(evidence),
                        evidence.matched_tokens,
                        evidence.occurrence_coverage,
                        -abs((end - start) - len(reference)),
                    ),
                    start,
                    end,
                    evidence,
                )
            )

    selected: list[tuple[int, int, MatchEvidence]] = []
    for _, start, end, evidence in sorted(candidates, reverse=True):
        masked_by_conflict = evidence.reference_coverage < 0.999 and any(
            ambiguous_evidence.matched_tokens >= evidence.matched_tokens
            and start < ambiguous_end
            and ambiguous_start < end
            and (
                min(end, ambiguous_end) - max(start, ambiguous_start)
            )
            / max(end - start, 1)
            >= 0.8
            for _, ambiguous_start, ambiguous_end, ambiguous_evidence in ambiguous
        )
        if masked_by_conflict:
            continue
        if any(start < selected_end and selected_start < end for selected_start, selected_end, _ in selected):
            continue
        selected.append((start, end, evidence))
    selected.sort(key=lambda item: item[0])
    accepted_passages = [
        (
            Passage(
                passage_id=f"{passage.passage_id}-r{unit_index:04d}-o{index:03d}",
                source_id=passage.source_id,
                source_order=passage.source_order,
                tokens=tuple(tokens[start:end]),
            ),
            evidence,
        )
        for index, (start, end, evidence) in enumerate(selected)
    ]
    if accepted_passages:
        return accepted_passages, []

    best_ambiguous: list[tuple[Passage, MatchEvidence]] = []
    if ambiguous:
        _, start, end, evidence = max(ambiguous)
        best_ambiguous.append(
            (
                Passage(
                    passage_id=f"{passage.passage_id}-r{unit_index:04d}-amb",
                    source_id=passage.source_id,
                    source_order=passage.source_order,
                    tokens=tuple(tokens[start:end]),
                ),
                evidence,
            )
        )
    return [], best_ambiguous


def _source_time_overlaps(first: Passage, second: Passage) -> bool:
    overlap_ms = min(first.end_ms, second.end_ms) - max(
        first.start_ms, second.start_ms
    )
    return overlap_ms > 0


def _ambiguity_record(
    passage: Passage, evidence: MatchEvidence
) -> dict[str, Any]:
    return {
        "sourceId": passage.source_id,
        "sourceStartMs": passage.start_ms,
        "sourceEndMs": passage.end_ms,
        "similarity": evidence.similarity,
        "characterSimilarity": evidence.character_similarity,
        "referenceCoverage": evidence.reference_coverage,
        "occurrenceCoverage": evidence.occurrence_coverage,
    }


def _canonical(group: list[Passage]) -> Passage:
    return max(group, key=lambda item: (len(item.normalized), item.source_order, item.start_ms))


def _reference_offset_ms(
    reference: tuple[str, ...], canonical: Passage, reference_index: int
) -> int:
    matcher = SequenceMatcher(a=reference, b=canonical.normalized, autojunk=False)
    for block in matcher.get_matching_blocks():
        if block.a <= reference_index < block.a + block.size:
            canonical_index = block.b + reference_index - block.a
            return canonical.token_for_normalized_index(canonical_index).start_ms - canonical.start_ms
    fraction = reference_index / max(len(reference), 1)
    return int(round(fraction * (canonical.end_ms - canonical.start_ms)))


def _group_record(
    group_index: int,
    text: str,
    reference: tuple[str, ...],
    passages: list[Passage],
    timeline_start_ms: int,
    *,
    source_durations_ms: dict[str, int],
    handle_before_ms: int,
    handle_after_ms: int,
    pass_through_singleton: bool = False,
    reference_unit_id: str = "",
) -> dict[str, Any]:
    canonical = _canonical(passages)
    chronological = sorted(
        passages,
        key=lambda item: (item.source_order, item.start_ms, item.end_ms, item.passage_id),
    )
    occurrence_drafts: list[dict[str, Any]] = []
    for recording_order, passage in enumerate(chronological):
        source_in_ms = max(0, passage.start_ms - handle_before_ms)
        source_duration_ms = source_durations_ms[passage.source_id]
        source_out_ms = min(
            source_duration_ms,
            passage.end_ms + handle_after_ms,
        )
        if source_out_ms <= source_in_ms:
            raise ValueError(
                f"Expanded source range is empty for {passage.passage_id}."
            )
        evidence = compare_tokens(reference, passage.normalized)
        reference_start = evidence.reference_start
        occurrence_start = evidence.occurrence_start
        offset_ms = (
            _reference_offset_ms(reference, canonical, reference_start)
            if reference_start < len(reference)
            else 0
        )
        anchor_start = passage.token_for_normalized_index(occurrence_start)
        anchor_end = passage.token_for_normalized_index(
            occurrence_start + max(evidence.anchor_size - 1, 0)
        )
        relative_sequence_start = offset_ms - (anchor_start.start_ms - source_in_ms)
        track_index = len(chronological) - recording_order - 1
        output_match_class = (
            "SINGLETON_PASS_THROUGH" if pass_through_singleton else evidence.match_class
        )
        occurrence_drafts.append(
            {
                "occurrenceId": f"g{group_index:04d}-o{recording_order:04d}",
                "passageId": passage.passage_id,
                "recordingOrder": recording_order,
                "sourceId": passage.source_id,
                "sourceOrder": passage.source_order,
                "sourceInMs": source_in_ms,
                "sourceOutMs": source_out_ms,
                "contentInMs": passage.start_ms,
                "contentOutMs": passage.end_ms,
                "speechInMs": anchor_start.start_ms,
                "speechOutMs": anchor_end.end_ms,
                "_relativeSequenceStartMs": relative_sequence_start,
                "videoTrackIndex": track_index,
                "audioTrackIndex": track_index,
                "matchClass": output_match_class,
                "similarity": _overall_similarity(evidence),
                "tokenSimilarity": evidence.similarity,
                "characterSimilarity": evidence.character_similarity,
                "referenceCoverage": evidence.reference_coverage,
                "occurrenceCoverage": evidence.occurrence_coverage,
                "matchedTokens": evidence.matched_tokens,
                "isPartial": output_match_class == "ACCEPT_PARTIAL",
            }
        )
    shift_ms = max(
        0,
        -min(item["_relativeSequenceStartMs"] for item in occurrence_drafts),
    )
    max_group_end_ms = 0
    occurrences: list[dict[str, Any]] = []
    for draft in occurrence_drafts:
        relative_start = draft.pop("_relativeSequenceStartMs") + shift_ms
        draft["sequenceStartMs"] = timeline_start_ms + relative_start
        max_group_end_ms = max(
            max_group_end_ms,
            relative_start + draft["sourceOutMs"] - draft["sourceInMs"],
        )
        occurrences.append(draft)
    record = {
        "groupId": f"group-{group_index:04d}",
        "groupOrder": group_index,
        "text": text,
        "timelineStartMs": timeline_start_ms,
        "durationMs": max(max_group_end_ms, 1),
        "alignmentShiftMs": shift_ms,
        "occurrences": occurrences,
    }
    if reference_unit_id:
        record["referenceUnitId"] = reference_unit_id
    return record


def _safe_excerpt(text: str, limit: int = 120) -> str:
    compact = " ".join(text.split())
    return compact if len(compact) <= limit else compact[: limit - 1].rstrip() + "…"


def _difference_summary(
    reference: tuple[str, ...], passage: Passage
) -> tuple[str, int, int]:
    occurrence = passage.normalized
    opcodes = [
        opcode
        for opcode in SequenceMatcher(
            a=reference, b=occurrence, autojunk=False
        ).get_opcodes()
        if opcode[0] != "equal"
    ]
    fragments: list[str] = []
    changed_ranges: list[tuple[int, int]] = []
    point_positions: list[int] = []
    tokens = passage.normalized_tokens
    for tag, reference_start, reference_end, occurrence_start, occurrence_end in opcodes:
        reference_words = " ".join(reference[reference_start:reference_end])
        occurrence_words = " ".join(occurrence[occurrence_start:occurrence_end])
        if tag == "replace":
            fragments.append(
                f"replace '{_safe_excerpt(reference_words, 48)}' with "
                f"'{_safe_excerpt(occurrence_words, 48)}'"
            )
        elif tag == "delete":
            fragments.append(f"missing '{_safe_excerpt(reference_words, 64)}'")
        else:
            fragments.append(f"added '{_safe_excerpt(occurrence_words, 64)}'")
        if occurrence_start < occurrence_end:
            changed_ranges.append(
                (
                    tokens[occurrence_start].start_ms,
                    tokens[occurrence_end - 1].end_ms,
                )
            )
        else:
            point_positions.append(
                tokens[occurrence_start].start_ms
                if occurrence_start < len(tokens)
                else passage.end_ms
            )
    if changed_ranges:
        marker_start = min(start for start, _ in changed_ranges)
        marker_end = max(end for _, end in changed_ranges)
        if point_positions:
            marker_start = min(marker_start, min(point_positions))
            marker_end = max(marker_end, max(point_positions))
    else:
        marker_start = min(point_positions, default=passage.start_ms)
        marker_end = marker_start
    summary = "; ".join(fragments[:3])
    if len(fragments) > 3:
        summary += f"; plus {len(fragments) - 3} more change(s)"
    return summary or "normalized wording differs", marker_start, marker_end


def _source_markers_for_group(
    group: dict[str, Any],
    reference: tuple[str, ...],
    passages: list[Passage],
    first_marker_index: int,
) -> list[dict[str, Any]]:
    passages_by_id = {passage.passage_id: passage for passage in passages}
    markers: list[dict[str, Any]] = []
    for occurrence in group["occurrences"]:
        passage = passages_by_id[occurrence["passageId"]]
        if occurrence["matchClass"] == "ACCEPT_PARTIAL":
            status = "PARTIAL"
            reason_codes = ["ACCEPTED_PARTIAL"]
            marker_start_ms = passage.start_ms
            marker_end_ms = passage.end_ms
            difference, _, _ = _difference_summary(reference, passage)
        elif reference == passage.normalized:
            status = "EXACT"
            reason_codes = ["EXACT_REFERENCE_MATCH"]
            marker_start_ms = passage.start_ms
            marker_end_ms = passage.end_ms
            difference = "normalized spoken wording matches the reference"
        else:
            status = "WORDING"
            reason_codes = ["NON_IDENTICAL_WORDING"]
            difference, marker_start_ms, marker_end_ms = _difference_summary(
                reference, passage
            )
        color_index = {"EXACT": 2, "WORDING": 3, "PARTIAL": 1}[status]
        marker_index = first_marker_index + len(markers)
        markers.append(
            {
                "sourceMarkerId": f"source-marker-{marker_index:04d}",
                "sourceId": occurrence["sourceId"],
                "occurrenceId": occurrence["occurrenceId"],
                "groupId": group["groupId"],
                "referenceUnitId": group["referenceUnitId"],
                "status": status,
                "reasonCodes": reason_codes,
                "markerName": f"AA-RT · {status}",
                "markerComment": (
                    f"status={status} | difference={difference} | "
                    f"reference='{_safe_excerpt(group['text'])}' | "
                    f"take='{_safe_excerpt(passage.text)}' | "
                    "automation-agent:repeated-takes"
                ),
                "sourceStartMs": marker_start_ms,
                "durationMs": max(marker_end_ms - marker_start_ms, 0),
                "colorIndex": color_index,
            }
        )
    return markers


def _diagnostic(
    diagnostic_index: int,
    *,
    severity: str,
    reason_codes: list[str],
    marker_name: str,
    marker_comment: str,
    sequence_start_ms: int,
    duration_ms: int,
    group_id: str = "",
    reference_unit_id: str = "",
) -> dict[str, Any]:
    return {
        "diagnosticId": f"diagnostic-{diagnostic_index:04d}",
        "severity": severity,
        "reasonCodes": reason_codes,
        "markerName": marker_name,
        "markerComment": marker_comment,
        "sequenceStartMs": sequence_start_ms,
        "durationMs": duration_ms,
        "groupId": group_id,
        "referenceUnitId": reference_unit_id,
    }


def _group_diagnostic(
    diagnostic_index: int,
    group: dict[str, Any],
    passages: list[Passage],
    ambiguities: list[dict[str, Any]],
    *,
    include_accepted_variation: bool = True,
) -> dict[str, Any] | None:
    reason_codes: list[str] = []
    if ambiguities:
        reason_codes.append("AMBIGUOUS_CANDIDATE_EXCLUDED")
    if include_accepted_variation and any(
        item["matchClass"] == "ACCEPT_PARTIAL" for item in group["occurrences"]
    ):
        reason_codes.append("ACCEPTED_PARTIAL")
    if include_accepted_variation and len(
        {passage.normalized for passage in passages}
    ) > 1:
        reason_codes.append("NON_IDENTICAL_WORDING")
    if not reason_codes:
        return None
    severity = "WARNING" if ambiguities else "REVIEW"
    marker_name = (
        "WARNING · Ambiguous candidate excluded"
        if ambiguities
        else "REVIEW · Partial or fuzzy takes"
    )
    similarities = [item["similarity"] for item in group["occurrences"]]
    source_ranges = ", ".join(
        f"{item['sourceId']}@{item['sourceInMs'] / 1000:.3f}–"
        f"{item['sourceOutMs'] / 1000:.3f}s"
        for item in group["occurrences"]
    )
    if ambiguities:
        source_ranges += "; excluded: " + ", ".join(
            f"{item['sourceId']}@{item['sourceStartMs'] / 1000:.3f}–"
            f"{item['sourceEndMs'] / 1000:.3f}s"
            for item in ambiguities
        )
    comment = (
        f"Reasons: {', '.join(reason_codes)}. "
        f"Review '{_safe_excerpt(group['text'])}'. "
        f"Accepted takes: {len(group['occurrences'])}; "
        f"minimum similarity: {min(similarities):.3f}. "
        f"Source ranges: {source_ranges}."
    )
    return _diagnostic(
        diagnostic_index,
        severity=severity,
        reason_codes=reason_codes,
        marker_name=marker_name,
        marker_comment=comment,
        sequence_start_ms=group["timelineStartMs"],
        duration_ms=group["durationMs"],
        group_id=group["groupId"],
        reference_unit_id=str(group.get("referenceUnitId", "")),
    )


def analyze_session(manifest: dict[str, Any]) -> dict[str, Any]:
    if manifest.get("contractVersion") != CONTRACT_VERSION:
        raise ValueError(f"Unsupported session contractVersion: {manifest.get('contractVersion')!r}")
    sources = manifest.get("sources")
    if not isinstance(sources, list) or not sources:
        raise ValueError("Session manifest must contain at least one source.")
    expected_orders = list(range(len(sources)))
    actual_orders = [source.get("sourceOrder") for source in sources]
    order_mode = "PREMIERE_SELECTION_API"
    order_warnings: list[dict[str, Any]] = []
    if actual_orders != expected_orders:
        if not all(isinstance(source.get("selectionIndex"), int) for source in sources):
            raise ValueError(
                "sources must either have contiguous sourceOrder values or a "
                "selection-ordered selectionIndex manifest."
            )
        order_result = plan_session_order(
            {"contractVersion": CONTRACT_VERSION, "sources": sources}
        )
        sources_by_id = {source["sourceId"]: source for source in sources}
        sources = [
            {
                **sources_by_id[ordered["sourceId"]],
                "sourceOrder": ordered["sourceOrder"],
            }
            for ordered in order_result["sources"]
        ]
        order_mode = order_result["orderMode"]
        order_warnings = order_result["warnings"]
    def seconds_to_ms(name: str, default_seconds: float, minimum_seconds: float) -> int:
        value = manifest.get(name, default_seconds)
        if isinstance(value, bool):
            raise ValueError(f"{name} must be a finite number.")
        try:
            seconds = float(value)
        except (TypeError, ValueError) as error:
            raise ValueError(f"{name} must be a finite number.") from error
        if not (seconds >= minimum_seconds and seconds < 3600):
            raise ValueError(
                f"{name} must be at least {minimum_seconds:g} and below 3600 seconds."
            )
        return int(round(seconds * 1000))

    handle_before_ms = seconds_to_ms("handleBeforeSeconds", 0.0, 0.0)
    handle_after_ms = seconds_to_ms("handleAfterSeconds", 0.0, 0.0)
    group_gap_ms = seconds_to_ms(
        "groupGapSeconds", MIN_GROUP_GAP_MS / 1000, MIN_GROUP_GAP_MS / 1000
    )

    all_passages: list[Passage] = []
    source_durations_ms: dict[str, int] = {}
    for source in sources:
        transcript_path = Path(str(source.get("transcriptPath", "")))
        with transcript_path.open("r", encoding="utf-8") as handle:
            transcript = json.load(handle)
        tokens = parse_premiere_transcript(
            transcript,
            source_id=str(source["sourceId"]),
            source_order=int(source["sourceOrder"]),
        )
        passages = segment_tokens(tokens)
        all_passages.extend(passages)
        transcript_end_ms = max((token.end_ms for token in tokens), default=0)
        raw_duration = source.get("durationSeconds", "")
        if raw_duration in ("", None):
            duration_ms = transcript_end_ms + handle_after_ms
        else:
            try:
                duration_ms = int(round(float(raw_duration) * 1000))
            except (TypeError, ValueError) as error:
                raise ValueError(
                    f"durationSeconds must be numeric for {source['sourceId']}."
                ) from error
        if duration_ms < transcript_end_ms:
            raise ValueError(
                f"durationSeconds ends before the transcript for {source['sourceId']}."
            )
        source_durations_ms[str(source["sourceId"])] = duration_ms

    reference_text = str(manifest.get("referenceText", "")).strip()
    reference_units: list[dict[str, Any]] = []
    self_groups: list[tuple[list[Passage], list[dict[str, Any]]]] = []
    if reference_text:
        normalized_reference = tuple(
            normalized
            for normalized in (normalize_word(word) for word in WORD_RE.findall(reference_text))
            if normalized
        )
        informative_reference = [
            word for word in normalized_reference if word not in LOW_INFORMATION_WORDS
        ]
        if (
            len(normalized_reference) < MIN_MATCHED_TOKENS
            or len(informative_reference) < MIN_INFORMATIVE_TOKENS
        ):
            raise ValueError(
                "The optional reference is too short to align safely. "
                "Provide at least four words including three informative words, "
                "or leave it empty for self-discovery."
            )
        reference_units = _reference_groups(reference_text, all_passages)
        mode = "REFERENCE_GUIDED"
    else:
        self_groups = _cluster_self_discovered(all_passages)
        mode = "SELF_DISCOVERY"

    groups: list[dict[str, Any]] = []
    diagnostics: list[dict[str, Any]] = []
    source_markers: list[dict[str, Any]] = []
    timeline_units: list[dict[str, Any]] = []
    cursor_ms = 0
    if mode == "REFERENCE_GUIDED":
        for unit in reference_units:
            passages = unit["passages"]
            if passages:
                record = _group_record(
                    len(groups),
                    unit["text"],
                    unit["normalized"],
                    passages,
                    cursor_ms,
                    source_durations_ms=source_durations_ms,
                    handle_before_ms=handle_before_ms,
                    handle_after_ms=handle_after_ms,
                    reference_unit_id=unit["referenceUnitId"],
                )
                groups.append(record)
                source_markers.extend(
                    _source_markers_for_group(
                        record,
                        unit["normalized"],
                        passages,
                        len(source_markers),
                    )
                )
                duration_ms = record["durationMs"]
                group_id = record["groupId"]
                diagnostic = _group_diagnostic(
                    len(diagnostics),
                    record,
                    passages,
                    unit["ambiguities"],
                    include_accepted_variation=False,
                )
                if diagnostic:
                    diagnostics.append(diagnostic)
            else:
                duration_ms = MISSING_REFERENCE_SLOT_MS
                group_id = ""
                reason_codes = ["NO_MATCHING_TAKE"]
                if unit["ambiguities"]:
                    reason_codes.append("AMBIGUOUS_CANDIDATE_EXCLUDED")
                diagnostics.append(
                    _diagnostic(
                        len(diagnostics),
                        severity="MISSING",
                        reason_codes=reason_codes,
                        marker_name="MISSING · No matching take",
                        marker_comment=(
                            f"Reasons: {', '.join(reason_codes)}. "
                            "No accepted take was found for reference passage "
                            f"'{_safe_excerpt(unit['text'])}'."
                        ),
                        sequence_start_ms=cursor_ms,
                        duration_ms=duration_ms,
                        reference_unit_id=unit["referenceUnitId"],
                    )
                )
            timeline_units.append(
                {
                    "referenceUnitId": unit["referenceUnitId"],
                    "text": unit["text"],
                    "timelineStartMs": cursor_ms,
                    "durationMs": duration_ms,
                    "groupId": group_id,
                }
            )
            cursor_ms += duration_ms + group_gap_ms
    else:
        for passages, ambiguities in self_groups:
            canonical = _canonical(passages)
            record = _group_record(
                len(groups),
                canonical.text,
                canonical.normalized,
                passages,
                cursor_ms,
                source_durations_ms=source_durations_ms,
                handle_before_ms=handle_before_ms,
                handle_after_ms=handle_after_ms,
                pass_through_singleton=len(passages) == 1,
            )
            groups.append(record)
            diagnostic = _group_diagnostic(
                len(diagnostics), record, passages, ambiguities
            )
            if diagnostic:
                diagnostics.append(diagnostic)
            cursor_ms += record["durationMs"] + group_gap_ms
    unmatched = [
        unit["text"] for unit in reference_units if not unit["passages"]
    ]
    return {
        "contractVersion": CONTRACT_VERSION,
        "profileVersion": PROFILE_VERSION,
        "mode": mode,
        "sourceOrder": [source["sourceId"] for source in sources],
        "orderMode": order_mode,
        "orderWarnings": order_warnings,
        "settings": {
            "handleBeforeMs": handle_before_ms,
            "handleAfterMs": handle_after_ms,
            "groupGapMs": group_gap_ms,
        },
        "groups": groups,
        "referenceTimelineUnits": timeline_units,
        "diagnostics": diagnostics,
        "sourceMarkers": source_markers,
        "summary": {
            "groupCount": len(groups),
            "repeatedGroupCount": sum(len(group["occurrences"]) >= 2 for group in groups),
            "singletonGroupCount": sum(len(group["occurrences"]) == 1 for group in groups),
            "occurrenceCount": sum(len(group["occurrences"]) for group in groups),
            "maxTrackCount": max(
                (len(group["occurrences"]) for group in groups),
                default=0,
            ),
            "unmatchedReferencePassages": unmatched,
            "diagnosticCount": len(diagnostics),
            "reviewMarkerCount": sum(
                item["severity"] == "REVIEW" for item in diagnostics
            ),
            "warningMarkerCount": sum(
                item["severity"] == "WARNING" for item in diagnostics
            ),
            "missingMarkerCount": sum(
                item["severity"] == "MISSING" for item in diagnostics
            ),
            "sourceMarkerCount": len(source_markers),
            "exactTakeMarkerCount": sum(
                item["status"] == "EXACT" for item in source_markers
            ),
            "wordingTakeMarkerCount": sum(
                item["status"] == "WORDING" for item in source_markers
            ),
            "partialTakeMarkerCount": sum(
                item["status"] == "PARTIAL" for item in source_markers
            ),
        },
    }
