"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.LiveAttributedTranscriptionSession = void 0;
function samplesToMs(samples, sampleRate) {
  return samples / sampleRate * 1000;
}
function cloneSegment(segment) {
  return {
    ...segment
  };
}
const DEFAULT_MAX_STORED_EVENTS = 1000;
// Sherpa ONNX streaming examples commonly add ~0.6s tail padding before
// draining final results so transducer/paraformer models get right context.
const DEFAULT_FLUSH_TAIL_PADDING_MS = 660;
function cloneEvent(event) {
  switch (event.type) {
    case 'speaker_event':
      return {
        ...event,
        event: {
          ...event.event
        }
      };
    case 'transcript_speaker_update':
      return {
        ...event,
        affectedSegmentIds: [...event.affectedSegmentIds]
      };
    case 'turn_finalized':
      return {
        ...event,
        segmentIds: [...event.segmentIds]
      };
    case 'transcript_segment_removed':
      return {
        ...event
      };
    default:
      return {
        ...event
      };
  }
}

/**
 * Live attributed transcription composer.
 *
 * This class owns the replayable event contract that joins online ASR output
 * with `LiveSpeakerTurnSession` speaker-turn events. It intentionally keeps
 * initialization/model loading outside the class so apps can decide which ASR,
 * VAD, and Speaker ID models to use and can reuse already-initialized services.
 */
class LiveAttributedTranscriptionSession {
  listeners = new Set();
  emittedEvents = [];
  emittedEventCount = 0;
  segments = new Map();
  segmentOrder = [];
  turnSpeakers = new Map();
  turnSegments = new Map();
  pendingFinalTurns = [];
  nextSample = 0;
  nextSegmentIndex = 1;
  currentSegmentId = null;
  currentSegmentStartSample = 0;
  resetAsrBeforeNextChunk = false;
  skipAsrForCurrentChunk = false;
  asrHasAcceptedAudio = false;
  released = false;
  constructor(config) {
    if (!Number.isFinite(config.sampleRate) || config.sampleRate <= 0) {
      throw new Error('LiveAttributedTranscriptionSession requires a positive sampleRate');
    }
    this.config = config;
    if (config.onEvent) {
      this.listeners.add(config.onEvent);
    }
    this.unsubscribeSpeakerEvents = config.speakerTurns.onEvent(event => {
      this.handleSpeakerEvent(event);
    });
  }
  onEvent(listener) {
    this.listeners.add(listener);
    return () => {
      this.listeners.delete(listener);
    };
  }
  getState() {
    return {
      released: this.released,
      nextSample: this.nextSample,
      activeTurnId: this.activeTurnId,
      activeSegmentId: this.currentSegmentId ?? undefined,
      segments: this.getOrderedSegments(),
      events: this.emittedEvents.map(cloneEvent)
    };
  }
  getSummary() {
    const segments = this.getOrderedSegments();
    return {
      eventCount: this.emittedEventCount,
      segmentCount: segments.length,
      finalSegmentCount: segments.filter(segment => segment.final).length,
      speakerAttributedSegmentCount: segments.filter(segment => Boolean(segment.speakerId)).length,
      durationMs: samplesToMs(this.nextSample, this.config.sampleRate)
    };
  }
  async acceptChunk(chunk) {
    this.ensureNotReleased();
    const sampleRate = chunk.sampleRate ?? this.config.sampleRate;
    if (sampleRate !== this.config.sampleRate) {
      throw new Error(`LiveAttributedTranscriptionSession expected sampleRate ${this.config.sampleRate}, received ${sampleRate}`);
    }
    const startSample = chunk.startSample ?? this.nextSample;
    if (startSample !== this.nextSample) {
      throw new Error(`LiveAttributedTranscriptionSession received non-contiguous startSample ${startSample}; next expected sample is ${this.nextSample}`);
    }
    const samples = Array.from(chunk.samples);
    const endSample = startSample + samples.length;
    this.skipAsrForCurrentChunk = false;
    this.processingChunkEndSample = endSample;
    try {
      await this.config.speakerTurns.acceptChunk({
        samples,
        sampleRate,
        startSample
      });
    } finally {
      this.processingChunkEndSample = undefined;
    }

    // Speaker-turn state has consumed this chunk from here onward. Keep the
    // composer clock monotonic even if ASR reset/accept/result handling fails.
    this.nextSample = endSample;
    if (this.skipAsrForCurrentChunk) {
      this.skipAsrForCurrentChunk = false;
      return;
    }
    if (this.resetAsrBeforeNextChunk) {
      let reset;
      try {
        reset = await this.config.asr.resetStream();
      } catch (error) {
        this.emit({
          type: 'error',
          source: 'asr',
          error: error instanceof Error ? error.message : 'ASR failed while resetting online stream at speaker boundary'
        });
        return;
      }
      if (!reset.success) {
        this.emit({
          type: 'error',
          source: 'asr',
          error: reset.error ?? 'ASR failed while resetting online stream at speaker boundary'
        });
        return;
      }
      this.resetAsrBeforeNextChunk = false;
      this.asrHasAcceptedAudio = false;
    }
    let accepted;
    try {
      accepted = await this.config.asr.acceptWaveform(sampleRate, samples);
    } catch (error) {
      this.emit({
        type: 'error',
        source: 'asr',
        error: error instanceof Error ? error.message : 'ASR failed while accepting waveform'
      });
      return;
    }
    if (!accepted.success) {
      this.emit({
        type: 'error',
        source: 'asr',
        error: accepted.error ?? 'ASR failed while accepting waveform'
      });
      return;
    }
    this.asrHasAcceptedAudio = true;
    try {
      const result = await this.config.asr.getResult();
      const text = (result.text ?? '').trim();
      if (text.length > 0) {
        this.emitPartial(text, startSample, endSample);
      }
      const endpoint = await this.config.asr.isEndpoint();
      if (endpoint.isEndpoint) {
        await this.finalizeCurrentSegment(endSample);
        let reset;
        try {
          reset = await this.config.asr.resetStream();
        } catch (resetError) {
          this.resetAsrBeforeNextChunk = true;
          this.emit({
            type: 'error',
            source: 'asr',
            error: resetError instanceof Error ? resetError.message : 'ASR failed while resetting online stream'
          });
          return;
        }
        if (reset.success) {
          this.asrHasAcceptedAudio = false;
        } else {
          this.resetAsrBeforeNextChunk = true;
          this.emit({
            type: 'error',
            source: 'asr',
            error: reset.error ?? 'ASR failed while resetting online stream'
          });
        }
      }
    } catch (error) {
      this.emit({
        type: 'error',
        source: 'asr',
        error: error instanceof Error ? error.message : 'ASR failed while reading online stream state'
      });
      return;
    }
    this.emitPendingFinalTurns();
  }
  async flush() {
    this.ensureNotReleased();
    let shouldResetAsr = Boolean(this.currentSegmentId) || this.asrHasAcceptedAudio;
    try {
      await this.config.speakerTurns.flush();
    } catch (error) {
      this.emit({
        type: 'error',
        source: 'speaker_turn',
        error: error instanceof Error ? error.message : 'Speaker turn flush failed'
      });
    }
    // Speaker turn flushing can synchronously finalize/discard turns and mutate
    // the active ASR segment through handleSpeakerEvent(), so recompute before
    // deciding whether the native ASR stream needs a reset.
    shouldResetAsr = shouldResetAsr || Boolean(this.currentSegmentId) || this.asrHasAcceptedAudio;
    await this.drainAsrTailPadding();
    await this.finalizeCurrentSegment(this.nextSample);
    this.emitPendingFinalTurns();
    if (!shouldResetAsr) {
      return;
    }
    let reset;
    try {
      reset = await this.config.asr.resetStream();
    } catch (error) {
      this.emit({
        type: 'error',
        source: 'asr',
        error: error instanceof Error ? error.message : 'ASR failed while resetting online stream'
      });
      return;
    }
    if (!reset.success) {
      this.emit({
        type: 'error',
        source: 'asr',
        error: reset.error ?? 'ASR failed while resetting online stream'
      });
      return;
    }
    this.asrHasAcceptedAudio = false;
  }
  async drainAsrTailPadding() {
    if (!this.currentSegmentId && !this.asrHasAcceptedAudio) {
      return;
    }
    const paddingMs = this.config.flushTailPaddingMs ?? DEFAULT_FLUSH_TAIL_PADDING_MS;
    const tailSamples = Math.round(paddingMs / 1000 * this.config.sampleRate);
    if (paddingMs > 0 && tailSamples > 0) {
      let accepted;
      try {
        accepted = await this.config.asr.acceptWaveform(this.config.sampleRate, new Array(tailSamples).fill(0));
      } catch (error) {
        this.emit({
          type: 'error',
          source: 'asr',
          error: error instanceof Error ? error.message : 'ASR failed while flushing tail padding'
        });
        return;
      }
      if (!accepted.success) {
        this.emit({
          type: 'error',
          source: 'asr',
          error: accepted.error ?? 'ASR failed while flushing tail padding'
        });
        return;
      }
    }
    if (this.config.asr.finishInput) {
      let finished;
      try {
        finished = await this.config.asr.finishInput();
      } catch (error) {
        this.emit({
          type: 'error',
          source: 'asr',
          error: error instanceof Error ? error.message : 'ASR failed while finishing online stream input'
        });
        return;
      }
      if (!finished.success) {
        this.emit({
          type: 'error',
          source: 'asr',
          error: finished.error ?? 'ASR failed while finishing online stream input'
        });
        return;
      }
    }
    try {
      const result = await this.config.asr.getResult();
      const text = (result.text ?? '').trim();
      if (text.length > 0) {
        this.emitPartial(text, this.currentSegmentStartSample, this.nextSample);
      }
      // Querying endpoint advances/clears ASR endpoint state after synthetic tail padding;
      // finalization remains driven by the explicit flush boundary below.
      await this.config.asr.isEndpoint();
    } catch (error) {
      this.emit({
        type: 'error',
        source: 'asr',
        error: error instanceof Error ? error.message : 'ASR failed while flushing online stream'
      });
    }
  }
  async reset() {
    this.ensureNotReleased();
    this.config.speakerTurns.reset();
    this.emittedEvents.splice(0, this.emittedEvents.length);
    this.emittedEventCount = 0;
    this.segments.clear();
    this.segmentOrder.splice(0, this.segmentOrder.length);
    this.turnSpeakers.clear();
    this.turnSegments.clear();
    this.pendingFinalTurns.splice(0, this.pendingFinalTurns.length);
    this.nextSample = 0;
    this.nextSegmentIndex = 1;
    this.currentSegmentId = null;
    this.currentSegmentStartSample = 0;
    this.processingChunkEndSample = undefined;
    this.resetAsrBeforeNextChunk = false;
    this.skipAsrForCurrentChunk = false;
    this.activeTurnId = undefined;
    this.asrHasAcceptedAudio = false;
    let reset;
    try {
      reset = await this.config.asr.resetStream();
    } catch (error) {
      this.emit({
        type: 'error',
        source: 'asr',
        error: error instanceof Error ? error.message : 'ASR failed while resetting online stream'
      });
      return;
    }
    if (!reset.success) {
      this.emit({
        type: 'error',
        source: 'asr',
        error: reset.error ?? 'ASR failed while resetting online stream'
      });
    }
  }
  release() {
    if (this.released) {
      return;
    }
    this.released = true;
    this.unsubscribeSpeakerEvents();
    this.config.speakerTurns.release();
    this.emit({
      type: 'released'
    });
    this.listeners.clear();
  }
  handleSpeakerEvent(event) {
    if (this.released) {
      return;
    }
    if (event.type === 'speech_start') {
      if (this.closeCurrentSegmentForTurnBoundary(event.startSample, event.turnId)) {
        this.resetAsrBeforeNextChunk = true;
      }
      this.activeTurnId = event.turnId;
      this.ensureCurrentSegment(event.startSample);
    }
    if (event.type === 'speaker_resolved') {
      this.turnSpeakers.set(event.turnId, {
        speakerId: event.speakerId,
        confidence: event.confidence
      });
      const affectedSegmentIds = this.applySpeakerToTurnSegments(event.turnId, event.speakerId);
      if (affectedSegmentIds.length > 0) {
        this.emit({
          type: 'transcript_speaker_update',
          turnId: event.turnId,
          speakerId: event.speakerId,
          confidence: event.confidence,
          affectedSegmentIds
        });
      }
    }
    if (event.type === 'turn_final') {
      // Convert raw speaker-turn completion into transcript-aware turn_finalized events
      // once every attached ASR segment has been finalized.
      this.pendingFinalTurns.push(event);
      if (this.activeTurnId === event.turnId) {
        this.activeTurnId = undefined;
      }
      return;
    }
    if (event.type === 'turn_discarded') {
      if (this.activeTurnId === event.turnId) {
        this.activeTurnId = undefined;
      }
      for (const segmentId of Array.from(this.turnSegments.get(event.turnId) ?? [])) {
        const segment = this.segments.get(segmentId);
        if (segment) {
          this.emit({
            type: 'transcript_segment_removed',
            segmentId: segment.segmentId,
            turnId: segment.turnId,
            reason: 'turn_discarded',
            text: segment.text,
            final: segment.final
          });
        }
        this.removeSegment(segmentId);
        if (this.currentSegmentId === segmentId) {
          this.currentSegmentId = null;
          this.currentSegmentStartSample = this.processingChunkEndSample ?? this.nextSample;
        }
      }
      this.turnSegments.delete(event.turnId);
      this.turnSpeakers.delete(event.turnId);
      // LiveSpeakerTurnSession emits turn_discarded synchronously inside
      // acceptChunk; use that invariant to prevent this same chunk from
      // recreating a stale ASR segment for audio we already discarded.
      this.skipAsrForCurrentChunk = true;
      this.resetAsrBeforeNextChunk = true;
      return;
    }
    if (event.type === 'error') {
      this.emit({
        type: 'error',
        source: 'speaker_turn',
        error: event.error
      });
      return;
    }
    this.emit({
      type: 'speaker_event',
      event
    });
  }
  emitPartial(text, startSample, endSample) {
    const segment = this.ensureCurrentSegment(startSample);
    const unchanged = segment.text === text && !segment.final;
    segment.text = text;
    segment.endSample = endSample;
    segment.endMs = samplesToMs(endSample, this.config.sampleRate);
    if (this.activeTurnId) {
      segment.turnId = this.activeTurnId;
      this.attachSegmentToTurn(segment.segmentId, this.activeTurnId);
      const speaker = this.turnSpeakers.get(this.activeTurnId);
      if (speaker) {
        segment.speakerId = speaker.speakerId;
      }
    }
    if (!unchanged || this.config.emitUnchangedPartials) {
      this.emit({
        type: 'partial_transcript',
        segmentId: segment.segmentId,
        turnId: segment.turnId,
        text: segment.text,
        startMs: segment.startMs,
        endMs: segment.endMs,
        startSample: segment.startSample,
        endSample: segment.endSample,
        speakerId: segment.speakerId
      });
    }
  }
  closeCurrentSegmentForTurnBoundary(boundarySample, nextTurnId) {
    const segmentId = this.currentSegmentId;
    if (!segmentId) {
      this.currentSegmentStartSample = boundarySample;
      return false;
    }
    const segment = this.segments.get(segmentId);
    if (!segment || segment.final || segment.turnId === nextTurnId) {
      return false;
    }
    if (segment.text.trim().length > 0) {
      this.finalizeSegment(segment, boundarySample);
    } else {
      this.removeSegment(segment.segmentId);
    }
    this.currentSegmentId = null;
    this.currentSegmentStartSample = boundarySample;
    return true;
  }
  async finalizeCurrentSegment(endSample) {
    const segmentId = this.currentSegmentId;
    if (!segmentId) {
      return;
    }
    const segment = this.segments.get(segmentId);
    if (!segment || segment.final) {
      this.currentSegmentId = null;
      this.currentSegmentStartSample = endSample;
      return;
    }
    if (segment.text.trim().length === 0) {
      this.removeSegment(segmentId);
      this.currentSegmentId = null;
      this.currentSegmentStartSample = endSample;
      return;
    }
    this.finalizeSegment(segment, endSample);
    this.currentSegmentId = null;
    this.currentSegmentStartSample = endSample;
  }
  finalizeSegment(segment, endSample) {
    segment.final = true;
    segment.endSample = endSample;
    segment.endMs = samplesToMs(endSample, this.config.sampleRate);
    if (segment.turnId) {
      const speaker = this.turnSpeakers.get(segment.turnId);
      if (speaker) {
        segment.speakerId = speaker.speakerId;
      }
    }
    this.emit({
      type: 'final_transcript',
      segmentId: segment.segmentId,
      turnId: segment.turnId,
      text: segment.text,
      startMs: segment.startMs,
      endMs: segment.endMs,
      startSample: segment.startSample,
      endSample: segment.endSample,
      speakerId: segment.speakerId
    });
  }
  ensureCurrentSegment(startSample) {
    if (this.currentSegmentId) {
      const existing = this.segments.get(this.currentSegmentId);
      if (existing && !existing.final) {
        return existing;
      }
    }
    const segmentId = `segment_${this.nextSegmentIndex}`;
    this.nextSegmentIndex += 1;
    const effectiveStartSample = Math.max(this.currentSegmentStartSample, startSample);
    const segment = {
      segmentId,
      turnId: this.activeTurnId,
      text: '',
      final: false,
      startSample: effectiveStartSample,
      endSample: effectiveStartSample,
      startMs: samplesToMs(effectiveStartSample, this.config.sampleRate),
      endMs: samplesToMs(effectiveStartSample, this.config.sampleRate)
    };
    const speaker = segment.turnId ? this.turnSpeakers.get(segment.turnId) : undefined;
    if (speaker) {
      segment.speakerId = speaker.speakerId;
    }
    this.segments.set(segmentId, segment);
    this.segmentOrder.push(segmentId);
    this.currentSegmentId = segmentId;
    if (segment.turnId) {
      this.attachSegmentToTurn(segmentId, segment.turnId);
    }
    return segment;
  }
  removeSegment(segmentId) {
    this.segments.delete(segmentId);
    const index = this.segmentOrder.indexOf(segmentId);
    if (index >= 0) {
      this.segmentOrder.splice(index, 1);
    }
    for (const segmentIds of this.turnSegments.values()) {
      segmentIds.delete(segmentId);
    }
  }
  attachSegmentToTurn(segmentId, turnId) {
    if (!segmentId || !turnId) {
      return;
    }
    let segmentIds = this.turnSegments.get(turnId);
    if (!segmentIds) {
      segmentIds = new Set();
      this.turnSegments.set(turnId, segmentIds);
    }
    segmentIds.add(segmentId);
  }
  applySpeakerToTurnSegments(turnId, speakerId) {
    const segmentIds = this.turnSegments.get(turnId);
    if (!segmentIds) {
      return [];
    }
    const affected = [];
    for (const segmentId of segmentIds) {
      const segment = this.segments.get(segmentId);
      if (!segment || segment.speakerId === speakerId) {
        continue;
      }
      segment.speakerId = speakerId;
      affected.push(segmentId);
    }
    return affected;
  }
  emitPendingFinalTurns() {
    if (this.pendingFinalTurns.length === 0) {
      return;
    }
    const stillPending = [];
    for (const event of this.pendingFinalTurns) {
      const segmentIds = Array.from(this.turnSegments.get(event.turnId) ?? new Set());
      const hasUnfinalizedSegments = segmentIds.some(segmentId => {
        const segment = this.segments.get(segmentId);
        return segment ? !segment.final : false;
      });
      if (hasUnfinalizedSegments) {
        stillPending.push(event);
        continue;
      }
      const text = segmentIds.map(segmentId => this.segments.get(segmentId)?.text ?? '').filter(Boolean).join(' ').trim();
      this.emit({
        type: 'turn_finalized',
        turnId: event.turnId,
        startMs: event.startMs,
        endMs: event.endMs,
        startSample: event.startSample,
        endSample: event.endSample,
        speakerId: event.speakerId,
        segmentIds,
        text
      });
      this.turnSegments.delete(event.turnId);
      this.turnSpeakers.delete(event.turnId);
    }
    this.pendingFinalTurns.splice(0, this.pendingFinalTurns.length, ...stillPending);
  }
  getOrderedSegments() {
    return this.segmentOrder.map(segmentId => this.segments.get(segmentId)).filter(segment => Boolean(segment)).map(cloneSegment);
  }
  emit(event) {
    this.emittedEventCount += 1;
    const storedEvent = cloneEvent(event);
    this.emittedEvents.push(storedEvent);
    const maxStoredEvents = Math.max(0, Math.floor(this.config.maxStoredEvents ?? DEFAULT_MAX_STORED_EVENTS));
    if (this.emittedEvents.length > maxStoredEvents) {
      this.emittedEvents.splice(0, this.emittedEvents.length - maxStoredEvents);
    }
    for (const listener of this.listeners) {
      listener(cloneEvent(storedEvent));
    }
  }
  ensureNotReleased() {
    if (this.released) {
      throw new Error('LiveAttributedTranscriptionSession has been released');
    }
  }
}
exports.LiveAttributedTranscriptionSession = LiveAttributedTranscriptionSession;
//# sourceMappingURL=LiveAttributedTranscriptionSession.js.map