"""Deterministic narration scheduling for render-time segment concatenation.

Rather than pre-concatenating segments into one file, the render stage
concatenates the processed segments directly. From narration.json + the cached
per-segment transcripts this module computes two things:

  1. an ordered segment schedule (processed file, skip, take, and offset in the
     combined timeline) — this drives the ffmpeg concat at render time; and
  2. the merged word-level transcript, with every word re-timed into the
     combined timeline — this drives slide alignment, exactly what
     re-transcribing a pre-concatenated narration file used to produce, but derived
     deterministically (no re-transcription, no separate combined file).

The processed files share framerate and format and are uncompressed, so the
computed offsets match the concatenated audio timeline sample-for-sample.
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Optional

from .models import VideoSource
from .preprocessor import get_preprocessed_path
from .transcriber import TranscribedWord, load_transcript


@dataclass
class NarrationSegment:
    """One narration segment placed on the combined render timeline."""

    seg_id: str
    source_path: Path          # processed file to concatenate
    skip: float                # seconds trimmed from the segment's start
    take: Optional[float]      # seconds kept from `skip` (None → to end)
    duration: float            # effective seconds contributed to the timeline
    offset: float              # start time of this segment in the combined timeline


def segment_order(narration: dict) -> list[str]:
    """Natural sort of segment ids (S2 before S10, s1-15 before s16-end)."""
    return sorted(
        narration.keys(),
        key=lambda s: [int(t) if t.isdigit() else t.lower() for t in re.split(r"(\d+)", s)],
    )


def build_narration_schedule(
    narration: dict[str, VideoSource],
    narration_dir: Path,
    get_duration: Callable[[Path], float],
    transcripts_dir: Optional[Path] = None,
    verbose: bool = False,
) -> tuple[list[NarrationSegment], list[TranscribedWord]]:
    """Return (ordered segments with offsets, merged transcript in timeline).

    Args:
        narration:      seg_id -> VideoSource (from parse_narration).
        narration_dir:  base dir the processed files resolve against
                        (media/narration; output_file is like processed/…mov).
        get_duration:   callable(Path) -> float (ffprobe duration), used only
                        when a segment has no explicit take.
        transcripts_dir: where per-segment {seg_id}.json transcripts live
                        (defaults to narration_dir/transcripts).
    """
    if transcripts_dir is None:
        transcripts_dir = narration_dir / "transcripts"

    segments: list[NarrationSegment] = []
    merged: list[TranscribedWord] = []
    offset = 0.0

    for seg_id in segment_order(narration):
        vs = narration[seg_id]
        source_path = get_preprocessed_path(narration_dir, vs)

        skip = vs.skip or 0.0
        take = vs.take
        if take is not None:
            eff = max(0.0, take)
        else:
            full_dur = get_duration(source_path) if source_path.exists() else 0.0
            eff = max(0.0, full_dur - skip)
        seg_end = skip + eff  # kept window end in the segment's own timeline

        segments.append(
            NarrationSegment(
                seg_id=seg_id,
                source_path=source_path,
                skip=skip,
                take=take,
                duration=eff,
                offset=offset,
            )
        )

        # Re-time this segment's transcript into the combined timeline: keep only
        # words inside [skip, seg_end], subtract skip, and add the running offset.
        tpath = transcripts_dir / f"{seg_id}.json"
        if tpath.exists():
            kept = 0
            for w in load_transcript(tpath):
                if w.end <= skip or w.start >= seg_end:
                    continue  # entirely outside the kept window
                new_start = max(w.start, skip) - skip + offset
                new_end = min(w.end, seg_end) - skip + offset
                merged.append(
                    TranscribedWord(word=w.word, start=round(new_start, 3), end=round(new_end, 3))
                )
                kept += 1
            if verbose:
                print(f"  {seg_id}: +{kept} words  (skip={skip:.2f}s take={eff:.2f}s → offset {offset:.2f}s)")
        elif verbose:
            print(f"  ⚠ {seg_id}: no transcript at {tpath} — words missing from merged transcript")

        offset += eff

    return segments, merged
