// The shared, pure orchestration pipeline. Given a decoded PcmAudio and a
// TranscriptionEngine, it:
//   1. loads the model if needed (reporting load progress),
//   2. plans overlapping windows over the audio (chunking.ts),
//   3. transcribes each window via the engine (chunk-local times),
//   4. stitches the per-chunk results into one absolute-time transcript,
//      emitting a growing `partial` after every chunk for live UI.
//
// This module is intentionally free of any platform/native imports so it can be
// exercised end-to-end in vitest with a fake engine (see pipeline.test.ts).

import { WHISPER_SAMPLE_RATE } from '../types';
import type { PcmAudio, Segment, TranscribeOptions } from '../types';
import { planChunks } from './chunking';
import { stitchSegments } from './stitch';
import type { TranscriptionEngine } from './engine';

/** Progress event emitted while transcribing. */
export interface TranscribeProgress {
  /** 'loading' while the model downloads/initializes, then 'transcribing'. */
  stage: 'loading' | 'transcribing';
  /** Fraction in [0, 1] for the current stage. */
  progress: number;
  /**
   * Best-effort transcript so far, in absolute time. Empty during 'loading';
   * grows after each chunk during 'transcribing'.
   */
  partial: Segment[];
}

export interface TranscribeParams {
  audio: PcmAudio;
  options: TranscribeOptions;
  engine: TranscriptionEngine;
  onProgress?: (p: TranscribeProgress) => void;
  /** Abort mid-run; throws DOMException('Aborted','AbortError'). */
  signal?: AbortSignal;
  /** Window length in seconds (default 28; Whisper attends to ~30s). */
  windowSec?: number;
  /** Overlap between adjacent windows in seconds (default 2). */
  overlapSec?: number;
}

/** Default window/overlap, matching planChunks/stitchSegments conventions. */
const DEFAULT_WINDOW_SEC = 28;
const DEFAULT_OVERLAP_SEC = 2;

/**
 * Transcribe a full PcmAudio into absolute-time segments. See module comment.
 */
export async function transcribe(params: TranscribeParams): Promise<Segment[]> {
  const {
    audio,
    options,
    engine,
    onProgress,
    signal,
    windowSec = DEFAULT_WINDOW_SEC,
    overlapSec = DEFAULT_OVERLAP_SEC,
  } = params;

  const { modelId } = options;

  // 1. Load the model if it isn't already resident. Load progress maps onto the
  //    'loading' stage; `partial` is empty because we have no segments yet.
  if (!engine.isModelLoaded(modelId)) {
    await engine.loadModel(modelId, (p) =>
      onProgress?.({ stage: 'loading', progress: p, partial: [] }),
    );
  }

  // 2. Plan overlapping windows. We pin the sample rate to Whisper's 16 kHz;
  //    PcmAudio is guaranteed to already be at that rate by its type.
  const plan = planChunks(audio.samples.length, {
    windowSec,
    overlapSec,
    sampleRate: WHISPER_SAMPLE_RATE,
  });

  // 3. Transcribe each window. `perChunk[i]` holds chunk-local segments for
  //    plan[i]; the stitcher offsets and merges them.
  const perChunk: Segment[][] = [];
  for (let i = 0; i < plan.length; i++) {
    // Cooperative cancellation: check before each (potentially long) chunk.
    if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');

    const chunk = plan[i]!;
    // subarray is a zero-copy view; the engine only reads it, never mutates.
    const chunkAudio: PcmAudio = {
      sampleRate: WHISPER_SAMPLE_RATE,
      samples: audio.samples.subarray(chunk.startSample, chunk.endSample),
    };

    const segs = await engine.transcribeChunk(chunkAudio, options);
    perChunk.push(segs);

    // Emit a growing absolute-time partial: stitch everything decoded so far.
    onProgress?.({
      stage: 'transcribing',
      progress: (i + 1) / plan.length,
      partial: stitchSegments(perChunk, plan.slice(0, i + 1), { overlapSec }),
    });
  }

  // 4. Final stitch over the full plan.
  return stitchSegments(perChunk, plan, { overlapSec });
}
