//! WebSocket handler for streaming speech-to-text with sliding window optimization.

use axum::{
    extract::{ws::Message, ws::WebSocket, State, WebSocketUpgrade},
    response::Response,
};
use futures::{SinkExt, StreamExt};
use tokio::sync::mpsc;
use uuid::Uuid;

use crate::audio::{resample_and_mixdown, TARGET_CHANNELS, TARGET_SAMPLE_RATE};
use crate::db::models::{TranscriptEntry, UpdateFileRequest};
use crate::db::repository;
use crate::listen::{align_speakers, samples_per_chunk, DialogueSegment, TimestampMode};
use crate::server::messages::{
    AudioEncoding, ClientMessage, ServerMessage, StartMessage, TranscriptMessage,
};
use crate::server::state::{MlModels, SharedState};

/// Chunk size in milliseconds for triggering transcription processing.
const STREAM_CHUNK_MS: u32 = 5_000;

/// Maximum window size in seconds for sliding window processing.
const MAX_WINDOW_SECONDS: f32 = 30.0;

/// Maximum window size in samples at 16kHz.
const MAX_WINDOW_SAMPLES: usize = (MAX_WINDOW_SECONDS as usize) * (TARGET_SAMPLE_RATE as usize);

/// EOU chunk size in samples (160ms at 16kHz).
const EOU_CHUNK_SIZE: usize = 2560;

/// Context overlap in seconds to keep when trimming finalized audio.
const CONTEXT_OVERLAP_SECONDS: f32 = 2.0;

/// WebSocket upgrade handler for STT streaming.
///
/// This endpoint accepts WebSocket connections for real-time speech-to-text
/// transcription with speaker diarization.
#[utoipa::path(
    get,
    path = "/api/v1/listen",
    responses(
        (status = 101, description = "WebSocket connection established"),
    ),
    tag = "Listen"
)]
pub async fn websocket_handler(
    ws: WebSocketUpgrade,
    State(state): State<SharedState>,
) -> Response {
    ws.on_upgrade(|socket| handle_socket(socket, state))
}

async fn handle_socket(socket: WebSocket, state: SharedState) {
    let session_id = Uuid::new_v4().to_string();
    tracing::info!(session_id = %session_id, "New WebSocket connection");

    // Split socket for concurrent read/write
    let (mut sender, mut receiver) = socket.split();

    // Channel for sending responses back to client
    let (response_tx, mut response_rx) = mpsc::channel::<ServerMessage>(32);

    // Spawn task to forward responses to WebSocket
    let sender_task = tokio::spawn(async move {
        while let Some(msg) = response_rx.recv().await {
            let json = match serde_json::to_string(&msg) {
                Ok(j) => j,
                Err(e) => {
                    tracing::error!("Failed to serialize message: {}", e);
                    continue;
                }
            };
            if sender.send(Message::Text(json.into())).await.is_err() {
                break;
            }
        }
    });

    // Lazy-load ML models on first Listen connection
    let ml_models = match state.get_ml_models().await {
        Ok(models) => models,
        Err(e) => {
            tracing::error!(session_id = %session_id, error = %e, "Failed to load ML models");
            let _ = response_tx
                .send(ServerMessage::Error {
                    code: "MODEL_LOAD_ERROR".into(),
                    message: format!("Failed to load ML models: {}", e),
                })
                .await;
            drop(response_tx);
            let _ = sender_task.await;
            return;
        }
    };

    // Send ready message
    let _ = response_tx
        .send(ServerMessage::Ready {
            session_id: session_id.clone(),
        })
        .await;

    // Audio format state
    let mut audio_format: Option<StartMessage> = None;

    // Main audio buffer for transcription (accumulates resampled 16kHz mono audio)
    let mut audio_buffer: Vec<f32> = Vec::new();

    // EOU detection buffer (resampled audio for utterance detection)
    let mut eou_buffer: Vec<f32> = Vec::new();
    let mut last_eou_text: String = String::new();
    let mut utterance_ended: bool = false;

    // Tracking state
    let mut last_sent_end_time: f32 = 0.0;
    let mut last_processed_len: usize = 0;
    let mut audio_offset: f32 = 0.0; // Time offset from trimmed audio
    let mut finalized_segments: Vec<DialogueSegment> = Vec::new();

    // File persistence state
    let mut file_id: Option<Uuid> = None;
    let mut transcript_entries: Vec<TranscriptEntry> = Vec::new();
    let mut transcript_counter: u32 = 0;

    // Auth state (set when Start message includes valid auth_token and contract_id)
    let mut authenticated_owner_id: Option<Uuid> = None;
    let mut target_contract_id: Option<Uuid> = None;

    // Reset Sortformer state for new session
    {
        let mut sortformer = ml_models.sortformer.lock().await;
        sortformer.reset_state();
    }

    // Process incoming messages
    while let Some(msg_result) = receiver.next().await {
        let msg = match msg_result {
            Ok(m) => m,
            Err(e) => {
                tracing::error!("WebSocket error: {}", e);
                break;
            }
        };

        match msg {
            Message::Text(text) => {
                // Parse JSON control messages
                match serde_json::from_str::<ClientMessage>(&text) {
                    Ok(ClientMessage::Start(start)) => {
                        tracing::info!(
                            session_id = %session_id,
                            sample_rate = start.sample_rate,
                            channels = start.channels,
                            encoding = ?start.encoding,
                            contract_id = ?start.contract_id,
                            has_auth = start.auth_token.is_some(),
                            "Session started"
                        );

                        // Validate auth and contract if provided
                        if let (Some(token), Some(contract_id_str)) = (&start.auth_token, &start.contract_id) {
                            // Parse contract ID
                            if let Ok(contract_id) = Uuid::parse_str(contract_id_str) {
                                // Validate JWT token
                                if let Some(ref verifier) = state.jwt_verifier {
                                    match verifier.verify(token) {
                                        Ok(claims) => {
                                            authenticated_owner_id = Some(claims.sub);
                                            target_contract_id = Some(contract_id);
                                            tracing::info!(
                                                session_id = %session_id,
                                                owner_id = %claims.sub,
                                                contract_id = %contract_id,
                                                "Authenticated session - transcripts will be saved to contract"
                                            );
                                        }
                                        Err(e) => {
                                            tracing::warn!(
                                                session_id = %session_id,
                                                error = %e,
                                                "Invalid auth token - transcripts will not be saved"
                                            );
                                        }
                                    }
                                } else {
                                    tracing::debug!(
                                        session_id = %session_id,
                                        "No JWT verifier configured - transcripts will not be saved"
                                    );
                                }
                            } else {
                                tracing::warn!(
                                    session_id = %session_id,
                                    contract_id = contract_id_str,
                                    "Invalid contract ID format"
                                );
                            }
                        }

                        audio_format = Some(start);
                        audio_buffer.clear();
                        eou_buffer.clear();
                        last_eou_text.clear();
                        utterance_ended = false;
                        last_sent_end_time = 0.0;
                        last_processed_len = 0;
                        audio_offset = 0.0;
                        finalized_segments.clear();
                        file_id = None;
                        authenticated_owner_id = authenticated_owner_id; // Keep from above
                        target_contract_id = target_contract_id; // Keep from above

                        // Reset models for new session
                        let mut sortformer = ml_models.sortformer.lock().await;
                        sortformer.reset_state();
                    }
                    Ok(ClientMessage::Stop(stop)) => {
                        tracing::info!(
                            session_id = %session_id,
                            reason = ?stop.reason,
                            audio_buffer_len = audio_buffer.len(),
                            "Session stopped by client"
                        );

                        if audio_format.is_some() {
                            if !audio_buffer.is_empty() {
                                tracing::debug!(
                                    session_id = %session_id,
                                    samples = audio_buffer.len(),
                                    "Processing final audio buffer"
                                );

                                // Process remaining audio with sliding window
                                match process_audio_window(&audio_buffer, audio_offset, ml_models).await {
                                    Ok(segments) => {
                                        tracing::debug!(
                                            session_id = %session_id,
                                            total_segments = segments.len(),
                                            finalized_count = finalized_segments.len(),
                                            last_sent_end = last_sent_end_time,
                                            "Final transcription complete"
                                        );

                                        // Combine finalized segments with new segments
                                        let mut all_segments = finalized_segments.clone();

                                        // Add segments from current window that weren't finalized
                                        for seg in &segments {
                                            // Adjust timestamps with offset
                                            let adjusted_seg = DialogueSegment {
                                                speaker: seg.speaker.clone(),
                                                start: seg.start + audio_offset,
                                                end: seg.end + audio_offset,
                                                text: seg.text.clone(),
                                            };

                                            // Only add if not already finalized
                                            if !finalized_segments.iter().any(|f|
                                                (f.start - adjusted_seg.start).abs() < 0.1 &&
                                                f.text == adjusted_seg.text
                                            ) {
                                                all_segments.push(adjusted_seg);
                                            }
                                        }

                                        // Sort by start time
                                        all_segments.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap());

                                        // Send any NEW segments as interim first
                                        for seg in &all_segments {
                                            if seg.end > last_sent_end_time {
                                                let _ = response_tx
                                                    .send(ServerMessage::Transcript(TranscriptMessage {
                                                        speaker: seg.speaker.clone(),
                                                        start: seg.start,
                                                        end: seg.end,
                                                        text: seg.text.clone(),
                                                        is_final: false,
                                                    }))
                                                    .await;
                                            }
                                        }

                                        // Send ALL segments as final
                                        for seg in &all_segments {
                                            let _ = response_tx
                                                .send(ServerMessage::Transcript(TranscriptMessage {
                                                    speaker: seg.speaker.clone(),
                                                    start: seg.start,
                                                    end: seg.end,
                                                    text: seg.text.clone(),
                                                    is_final: true,
                                                }))
                                                .await;
                                        }
                                    }
                                    Err(e) => {
                                        tracing::error!(
                                            session_id = %session_id,
                                            error = %e,
                                            "Final transcription failed"
                                        );
                                        let _ = response_tx
                                            .send(ServerMessage::Error {
                                                code: "TRANSCRIPTION_ERROR".into(),
                                                message: e.to_string(),
                                            })
                                            .await;
                                    }
                                }
                            }
                        }

                        let _ = response_tx
                            .send(ServerMessage::Stopped {
                                reason: stop.reason.unwrap_or_else(|| "client_requested".into()),
                            })
                            .await;
                        break;
                    }
                    Err(e) => {
                        tracing::warn!(session_id = %session_id, error = %e, "Failed to parse message");
                        let _ = response_tx
                            .send(ServerMessage::Error {
                                code: "PARSE_ERROR".into(),
                                message: format!("Failed to parse message: {}", e),
                            })
                            .await;
                    }
                }
            }
            Message::Binary(data) => {
                let Some(ref format) = audio_format else {
                    let _ = response_tx
                        .send(ServerMessage::Error {
                            code: "NO_FORMAT".into(),
                            message: "Received audio before start message".into(),
                        })
                        .await;
                    continue;
                };

                // Decode binary audio data to f32 samples
                let samples = decode_audio_chunk(&data, format);

                // Resample to 16kHz mono for all processing
                let resampled = if format.sample_rate != TARGET_SAMPLE_RATE || format.channels != TARGET_CHANNELS {
                    resample_and_mixdown(&samples, format.sample_rate, format.channels)
                } else {
                    samples
                };

                audio_buffer.extend(&resampled);
                eou_buffer.extend(&resampled);

                // Process EOU detection in 160ms chunks
                while eou_buffer.len() >= EOU_CHUNK_SIZE {
                    let chunk: Vec<f32> = eou_buffer.drain(..EOU_CHUNK_SIZE).collect();

                    let mut eou = ml_models.parakeet_eou.lock().await;
                    if let Ok(text) = eou.transcribe(&chunk, false) {
                        // Detect utterance boundary (sentence-ending punctuation)
                        if !text.is_empty() && text != last_eou_text {
                            if last_eou_text.ends_with('.')
                                || last_eou_text.ends_with('?')
                                || last_eou_text.ends_with('!')
                            {
                                utterance_ended = true;
                                tracing::debug!(
                                    session_id = %session_id,
                                    "Utterance boundary detected via EOU"
                                );
                            }
                            last_eou_text = text;
                        }
                    }
                }

                // Calculate if we should process (utterance ended OR enough new audio)
                let chunk_samples = samples_per_chunk(TARGET_SAMPLE_RATE, STREAM_CHUNK_MS);
                let new_audio_len = audio_buffer.len() - last_processed_len;
                let should_process = utterance_ended || new_audio_len >= chunk_samples;

                if should_process {
                    tracing::debug!(
                        session_id = %session_id,
                        total_samples = audio_buffer.len(),
                        new_samples = new_audio_len,
                        utterance_ended = utterance_ended,
                        audio_offset = audio_offset,
                        "Processing audio with sliding window"
                    );

                    match process_audio_window(&audio_buffer, audio_offset, ml_models).await {
                        Ok(segments) => {
                            tracing::debug!(
                                session_id = %session_id,
                                total_segments = segments.len(),
                                last_sent_end = last_sent_end_time,
                                "Transcription produced segments"
                            );

                            // Send segments with adjusted timestamps
                            for seg in &segments {
                                let adjusted_start = seg.start + audio_offset;
                                let adjusted_end = seg.end + audio_offset;
                                if adjusted_end > last_sent_end_time {
                                    // Create file on first transcript if authenticated with contract
                                    if file_id.is_none() {
                                        if let (Some(owner_id), Some(contract_id), Some(pool)) =
                                            (authenticated_owner_id, target_contract_id, &state.db_pool)
                                        {
                                            let create_req = crate::db::models::CreateFileRequest {
                                                contract_id,
                                                name: None, // Auto-generated
                                                description: Some("Live transcription".to_string()),
                                                transcript: vec![],
                                                location: None,
                                                body: vec![],
                                                repo_file_path: None,
                                                contract_phase: None, // Will be looked up from contract
                                            };
                                            match repository::create_file_for_owner(pool, owner_id, create_req).await {
                                                Ok(file) => {
                                                    file_id = Some(file.id);
                                                    tracing::info!(
                                                        session_id = %session_id,
                                                        file_id = %file.id,
                                                        contract_id = %contract_id,
                                                        "Created file for session in contract"
                                                    );
                                                }
                                                Err(e) => {
                                                    tracing::warn!(
                                                        session_id = %session_id,
                                                        error = %e,
                                                        "Failed to create file for session"
                                                    );
                                                }
                                            }
                                        }
                                    }

                                    // Track transcript entry
                                    transcript_counter += 1;
                                    transcript_entries.push(TranscriptEntry {
                                        id: format!("{}-{}", session_id, transcript_counter),
                                        speaker: seg.speaker.clone(),
                                        start: adjusted_start,
                                        end: adjusted_end,
                                        text: seg.text.clone(),
                                        is_final: false,
                                    });

                                    let _ = response_tx
                                        .send(ServerMessage::Transcript(TranscriptMessage {
                                            speaker: seg.speaker.clone(),
                                            start: adjusted_start,
                                            end: adjusted_end,
                                            text: seg.text.clone(),
                                            is_final: false,
                                        }))
                                        .await;
                                    last_sent_end_time = adjusted_end;
                                }
                            }

                            // If utterance ended, finalize and trim
                            if utterance_ended && segments.len() > 1 {
                                // Finalize all but the last segment
                                let to_finalize = &segments[..segments.len() - 1];
                                for seg in to_finalize {
                                    finalized_segments.push(DialogueSegment {
                                        speaker: seg.speaker.clone(),
                                        start: seg.start + audio_offset,
                                        end: seg.end + audio_offset,
                                        text: seg.text.clone(),
                                    });
                                }

                                // Trim audio buffer
                                if let Some(last_finalized) = to_finalize.last() {
                                    let trim_to_time = (last_finalized.end - CONTEXT_OVERLAP_SECONDS).max(0.0);
                                    let trim_samples = (trim_to_time * TARGET_SAMPLE_RATE as f32) as usize;

                                    if trim_samples > 0 && trim_samples < audio_buffer.len() {
                                        audio_buffer.drain(..trim_samples);
                                        audio_offset += trim_to_time;
                                        tracing::debug!(
                                            session_id = %session_id,
                                            trimmed_samples = trim_samples,
                                            new_offset = audio_offset,
                                            remaining_samples = audio_buffer.len(),
                                            "Trimmed audio buffer after finalization"
                                        );
                                    }
                                }
                            }

                            last_processed_len = audio_buffer.len();
                            utterance_ended = false;
                        }
                        Err(e) => {
                            tracing::error!(session_id = %session_id, error = %e, "Transcription error");
                            let _ = response_tx
                                .send(ServerMessage::Error {
                                    code: "TRANSCRIPTION_ERROR".into(),
                                    message: e.to_string(),
                                })
                                .await;
                        }
                    }
                }
            }
            Message::Close(_) => {
                tracing::info!(session_id = %session_id, "WebSocket closed by client");
                break;
            }
            _ => {}
        }
    }

    // Save final transcript to file if we have one
    if let Some(fid) = file_id {
        if let Some(ref pool) = state.db_pool {
            // Deduplicate transcript entries before saving
            let deduplicated = deduplicate_transcripts(&transcript_entries);

            // Mark all entries as final
            let final_entries: Vec<TranscriptEntry> = deduplicated
                .into_iter()
                .map(|mut entry| {
                    entry.is_final = true;
                    entry
                })
                .collect();

            match repository::update_file(pool, fid, UpdateFileRequest {
                name: None,
                description: None,
                transcript: Some(final_entries.clone()),
                summary: None,
                body: None,
                version: None, // Internal update, skip version check
                repo_file_path: None,
            }).await {
                Ok(_) => {
                    tracing::info!(
                        session_id = %session_id,
                        file_id = %fid,
                        original_count = transcript_entries.len(),
                        deduplicated_count = final_entries.len(),
                        "Saved final transcript to file"
                    );
                }
                Err(e) => {
                    tracing::error!(
                        session_id = %session_id,
                        file_id = %fid,
                        error = %e,
                        "Failed to save final transcript to file"
                    );
                }
            }
        }
    }

    // Cleanup
    drop(response_tx);
    let _ = sender_task.await;
    tracing::info!(session_id = %session_id, "WebSocket connection closed");
}

/// Decode binary audio chunk to f32 samples based on encoding format.
fn decode_audio_chunk(data: &[u8], format: &StartMessage) -> Vec<f32> {
    match format.encoding {
        AudioEncoding::Pcm32f => data
            .chunks_exact(4)
            .map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
            .collect(),
        AudioEncoding::Pcm16 | AudioEncoding::Raw => data
            .chunks_exact(2)
            .map(|chunk| {
                let sample = i16::from_le_bytes([chunk[0], chunk[1]]);
                sample as f32 / 32768.0
            })
            .collect(),
    }
}

/// Deduplicate transcript entries by removing entries with similar times and text.
///
/// Entries are considered duplicates if any of these are true:
/// - Start times are within 1.5 seconds AND text is similar (same, substring, or high overlap)
/// - Time ranges overlap significantly AND text is similar
/// - Text is identical regardless of timing
fn deduplicate_transcripts(entries: &[TranscriptEntry]) -> Vec<TranscriptEntry> {
    if entries.is_empty() {
        return vec![];
    }

    // Sort by start time
    let mut sorted: Vec<TranscriptEntry> = entries.to_vec();
    sorted.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap_or(std::cmp::Ordering::Equal));

    let mut result: Vec<TranscriptEntry> = Vec::new();

    for entry in sorted {
        // Normalize text for comparison
        let entry_text_normalized = normalize_text(&entry.text);

        // Check if this entry is a duplicate of any existing entry
        let duplicate_idx = result.iter().position(|existing| {
            let existing_text_normalized = normalize_text(&existing.text);

            // Check if same speaker
            let same_speaker = existing.speaker == entry.speaker;

            // Check if start times are identical or very close
            let start_identical = (existing.start - entry.start).abs() < 0.1;
            let start_close = (existing.start - entry.start).abs() < 1.5;

            // Check if time ranges overlap
            let time_overlap = existing.start < entry.end && entry.start < existing.end;

            // Check various text similarity conditions
            let text_identical = existing_text_normalized == entry_text_normalized;
            let text_contained = existing_text_normalized.contains(&entry_text_normalized)
                || entry_text_normalized.contains(&existing_text_normalized);
            let text_similar = text_similarity(&existing_text_normalized, &entry_text_normalized) > 0.7;

            // Duplicate conditions:
            // 1. Same speaker + identical start time (different end times = same segment refined)
            // 2. Same speaker + close start + similar text
            // 3. Same speaker + overlapping time + similar text
            // 4. Identical text (likely a re-transcription)
            (same_speaker && start_identical)
                || (same_speaker && start_close && (text_identical || text_contained || text_similar))
                || (same_speaker && time_overlap && (text_identical || text_contained))
                || text_identical
        });

        match duplicate_idx {
            Some(idx) => {
                // If the new entry has longer text, update the existing one
                if entry.text.len() > result[idx].text.len() {
                    result[idx].text = entry.text.clone();
                    result[idx].end = result[idx].end.max(entry.end);
                } else {
                    // Extend end time if needed
                    result[idx].end = result[idx].end.max(entry.end);
                }
            }
            None => {
                result.push(entry);
            }
        }
    }

    // Second pass: merge adjacent segments with same speaker and similar text
    let mut merged: Vec<TranscriptEntry> = Vec::new();
    for entry in result {
        if let Some(last) = merged.last_mut() {
            // Check if this should be merged with the previous entry
            let same_speaker = last.speaker == entry.speaker;
            let adjacent = (entry.start - last.end).abs() < 0.5;
            let text_overlap = normalize_text(&last.text).contains(&normalize_text(&entry.text))
                || normalize_text(&entry.text).contains(&normalize_text(&last.text));

            if same_speaker && adjacent && text_overlap {
                // Merge: keep longer text, extend time range
                if entry.text.len() > last.text.len() {
                    last.text = entry.text;
                }
                last.end = last.end.max(entry.end);
                continue;
            }
        }
        merged.push(entry);
    }

    // Reassign IDs to be sequential
    for (i, entry) in merged.iter_mut().enumerate() {
        let parts: Vec<&str> = entry.id.split('-').collect();
        if let Some(session_prefix) = parts.first() {
            entry.id = format!("{}-{}", session_prefix, i + 1);
        }
    }

    merged
}

/// Normalize text for comparison by lowercasing and collapsing whitespace.
fn normalize_text(text: &str) -> String {
    text.to_lowercase()
        .split_whitespace()
        .collect::<Vec<_>>()
        .join(" ")
}

/// Calculate text similarity as a ratio of shared words.
fn text_similarity(a: &str, b: &str) -> f32 {
    if a.is_empty() || b.is_empty() {
        return 0.0;
    }

    let words_a: std::collections::HashSet<&str> = a.split_whitespace().collect();
    let words_b: std::collections::HashSet<&str> = b.split_whitespace().collect();

    let intersection = words_a.intersection(&words_b).count();
    let union = words_a.union(&words_b).count();

    if union == 0 {
        0.0
    } else {
        intersection as f32 / union as f32
    }
}

/// Process audio using sliding window through STT and streaming diarization models.
///
/// Only processes the last MAX_WINDOW_SECONDS of audio to maintain constant
/// processing time regardless of total audio length.
async fn process_audio_window(
    samples: &[f32],
    _audio_offset: f32,
    ml_models: &MlModels,
) -> Result<Vec<DialogueSegment>, Box<dyn std::error::Error + Send + Sync>> {
    // Apply sliding window - only process the last 30 seconds
    let window_start = samples.len().saturating_sub(MAX_WINDOW_SAMPLES);
    let window = &samples[window_start..];

    tracing::trace!(
        total_samples = samples.len(),
        window_samples = window.len(),
        window_start = window_start,
        "Using sliding window for processing"
    );

    // Acquire model locks and run inference
    let mut parakeet = ml_models.parakeet.lock().await;
    let mut sortformer = ml_models.sortformer.lock().await;

    // Run streaming diarization (maintains speaker cache across calls)
    let diarization_segments =
        sortformer.diarize_streaming(window.to_vec(), TARGET_SAMPLE_RATE, TARGET_CHANNELS)?;

    // Run transcription
    let transcription = parakeet.transcribe_samples(
        window.to_vec(),
        TARGET_SAMPLE_RATE,
        TARGET_CHANNELS,
        Some(TimestampMode::Sentences),
    )?;

    // Align speakers with transcription
    let aligned = align_speakers(&transcription.tokens, &diarization_segments);

    // Adjust timestamps for window offset within the buffer
    let window_offset = window_start as f32 / TARGET_SAMPLE_RATE as f32;
    let adjusted: Vec<DialogueSegment> = aligned
        .into_iter()
        .map(|seg| DialogueSegment {
            speaker: seg.speaker,
            start: seg.start + window_offset,
            end: seg.end + window_offset,
            text: seg.text,
        })
        .collect();

    Ok(adjusted)
}
