const crypto = require("crypto");

class CaptionStabilizer {
  constructor(options = {}) {
    this.maxChars = options.maxChars || 80;
    this.fragmentAfterMs = options.fragmentAfterMs || 1000;
    this.now = options.now || Date.now;
    this.tracks = new Map();
  }

  update(sourceUuid, hypothesis, details = {}) {
    const now = this.now();
    const words = tokenize(hypothesis);
    let state = this.tracks.get(sourceUuid);
    if (!state || details.newUtterance) {
      state = { captionId: crypto.randomUUID(), revision: 0, stable: [], previous: [], startedAt: now, tailSince: now };
      this.tracks.set(sourceUuid, state);
    }
    const common = commonPrefix(state.previous, words);
    const agreement = commonPrefix(state.stable, words);
    if (agreement.length < state.stable.length) {
      words.splice(0, agreement.length, ...state.stable.slice(0, agreement.length));
    }
    const stableLimit = details.trailingIncomplete && !details.final
      ? Math.max(state.stable.length, common.length - 1)
      : Math.max(state.stable.length, common.length);
    const agreedCandidate = words.slice(0, stableLimit);
    if (agreedCandidate.length > state.stable.length) state.stable = agreedCandidate;
    const tail = words.slice(state.stable.length);
    if (tail.join(" ") !== state.previous.slice(state.stable.length).join(" ")) state.tailSince = now;
    const incompleteWord = Boolean(details.trailingIncomplete && tail.length && now - state.tailSince >= this.fragmentAfterMs && !details.final);
    if (details.final) state.stable = words;
    state.previous = words;
    state.revision += 1;
    const stableText = clip(state.stable.join(" "), this.maxChars);
    const uncertainText = details.final ? "" : clip(tail.join(" "), Math.max(0, this.maxChars - stableText.length - 1));
    const event = {
      caption_id: state.captionId,
      revision: state.revision,
      stable_text: stableText,
      uncertain_text: uncertainText,
      final: Boolean(details.final),
      incomplete_word: incompleteWord,
      stabilization_ms: now - state.startedAt
    };
    if (details.final) this.tracks.delete(sourceUuid);
    return event;
  }
}

class LatestCaptionGate {
  constructor() { this.revisions = new Map(); }
  accept(event) {
    const key = `${event.session_id}:${event.caption_id}`;
    const current = this.revisions.get(key) || 0;
    if (event.revision <= current) return false;
    this.revisions.set(key, event.revision);
    return true;
  }
  clearSession(sessionId) { for (const key of this.revisions.keys()) if (key.startsWith(`${sessionId}:`)) this.revisions.delete(key); }
}

function tokenize(value) { return String(value || "").trim().split(/\s+/).filter(Boolean); }
function commonPrefix(left, right) { let index = 0; while (index < left.length && index < right.length && left[index] === right[index]) index += 1; return right.slice(0, index); }
function clip(value, max) { if (value.length <= max) return value; return value.slice(0, max).trimEnd(); }

module.exports = { CaptionStabilizer, LatestCaptionGate, tokenize, commonPrefix };
