const crypto = require("crypto");
const { RollingPcmBuffer, SequenceTracker } = require("./bounded_queue");
const { CaptionStabilizer } = require("../transcription/stabilizer");

class SessionCoordinator {
  constructor(options) {
    this.provider = options.provider;
    this.deliveryFactory = options.deliveryFactory;
    this.log = options.log || { append() {} };
    this.now = options.now || Date.now;
    this.graceMs = options.graceMs || 30000;
    this.benchmarks = options.benchmarks || null;
    this.readinessContext = options.readinessContext || (() => ({}));
    this.sessions = new Map();
    this.stabilizer = new CaptionStabilizer({ now: this.now });
    this.provider.on?.("hypothesis", (event) => Promise.resolve(this.onHypothesis(event)).catch((error) => {
      this.log.append({ kind: "inference_result_error", code: error.code || null, message: error.message });
    }));
    this.provider.on?.("provider_error", (error) => Promise.resolve(this.onProviderError(error)).catch((failure) => {
      this.log.append({ kind: "provider_failure_handler_error", message: failure.message });
    }));
  }
  create(device, send, resumeSessionId = null) {
    const resumable = resumeSessionId && this.sessions.get(resumeSessionId);
    if (resumable && resumable.deviceId === device.id && resumable.graceUntil > this.now()) {
      clearTimeout(resumable.graceTimer);
      resumable.graceTimer = null;
      resumable.graceUntil = 0;
      resumable.connected = true;
      resumable.send = send;
      resumable.delivery = this.deliveryFactory(send);
      this.log.append({ kind: "session", state: "resumed", session_id: resumable.id, device_id: device.id });
      return { session: resumable, resumed: true };
    }
    const session = {
      id: crypto.randomUUID(), deviceId: device.id, connected: true, send,
      state: "idle", mode: null, obs: { streaming: false, recording: false },
      tracks: new Map(), delivery: this.deliveryFactory(send), createdAt: this.now(), graceUntil: 0, graceTimer: null, benchmarkId: null, providerActive: false
    };
    this.sessions.set(session.id, session);
    this.log.append({ kind: "session", state: "created", session_id: session.id, device_id: device.id });
    return { session, resumed: false };
  }
  updateSource(sessionId, input) {
    const session = this.require(sessionId);
    if (!isUuid(input.source_uuid)) throw new Error("Source UUID is invalid.");
    const existing = session.tracks.get(input.source_uuid);
    const track = existing || { sequence: new SequenceTracker(), buffer: new RollingPcmBuffer({ seconds: 5, now: this.now }), lastSpeechAt: 0 };
    Object.assign(track, {
      source_uuid: input.source_uuid, display_name: text(input.display_name, 160) || "OBS source",
      speaker_label: text(input.speaker_label, 80) || null, enabled: input.enabled !== false,
      primary: Boolean(input.primary), single_speaker: input.single_speaker !== false,
      delivery_enabled: input.delivery_enabled !== false, program_active: Boolean(input.program_active),
      source_missing: Boolean(input.source_missing), last_activity_at: this.now()
    });
    if (track.primary) for (const other of session.tracks.values()) other.primary = false;
    session.tracks.set(track.source_uuid, track);
    return serializeTrack(track);
  }
  async updateObsState(sessionId, obs) {
    const session = this.require(sessionId);
    const wasStreaming = session.obs.streaming;
    session.obs = {
      streaming: Boolean(obs.streaming), recording: Boolean(obs.recording), version: text(obs.version, 32) || null,
      bridge_installed: typeof obs.bridge_installed === "boolean" ? obs.bridge_installed : session.obs.bridge_installed,
      bridge_connected: typeof obs.bridge_connected === "boolean" ? obs.bridge_connected : session.obs.bridge_connected,
      bridge_version: obs.bridge_version === undefined ? session.obs.bridge_version : text(obs.bridge_version, 32) || null,
      path_test_valid: typeof obs.path_test_valid === "boolean" ? obs.path_test_valid : session.obs.path_test_valid,
      path_test_at: Number.isFinite(Number(obs.path_test_at)) ? Math.max(0, Number(obs.path_test_at)) : session.obs.path_test_at
    };
    if (!wasStreaming && session.obs.streaming && session.state === "idle" && obs.auto_start !== false) await this.start(sessionId, { mode: "live" });
    if (wasStreaming && !session.obs.streaming && session.state === "running" && session.mode === "live") this.beginGrace(session);
    if (!wasStreaming && session.obs.streaming && session.state === "grace") await this.resume(session);
    return { ...session.obs, state: session.state };
  }
  async start(sessionId, options = {}) {
    const session = this.require(sessionId);
    if (session.state !== "idle") throw Object.assign(new Error("Another transcription activity is still finishing. Try again in a moment."), { code: "SESSION_BUSY" });
    const mode = options.mode === "test" ? "test" : options.mode === "benchmark" ? "benchmark" : "live";
    if (mode === "live" && !session.obs.streaming) throw Object.assign(new Error("Live transcription requires an active OBS stream."), { code: "OBS_NOT_STREAMING" });
    const tracks = Array.from(session.tracks.values()).filter((track) => track.enabled && !track.source_missing);
    if (!tracks.length) throw Object.assign(new Error("Select an available OBS audio source first."), { code: "NO_TRACKS" });
    const providerHealth = await this.provider.health();
    if (!providerHealth.healthy) {
      const reason = providerHealth.last_error?.message ||
        (providerHealth.last_exit ? `The worker last exited with code ${providerHealth.last_exit.code ?? "unknown"}.` : null) ||
        `Worker state is ${providerHealth.state || "unavailable"}.`;
      throw Object.assign(new Error(`Speech recognition is not ready. ${reason}`), { code: "PROVIDER_UNAVAILABLE", details: providerHealth });
    }
    try {
      if (mode === "benchmark" && this.benchmarks) session.benchmarkId = this.benchmarks.start({ sessionId: session.id, deviceId: session.deviceId, source: serializeTrack(tracks[0]) });
      if (mode !== "test") {
        await this.provider.startSession({ id: session.id, mode });
        session.providerActive = true;
        for (const track of tracks) await this.provider.addTrack(session.id, serializeTrack(track));
      }
      await session.delivery.start({ testMode: mode !== "live" });
      session.mode = mode;
      session.state = "running";
      this.log.append({ kind: "session", state: "running", mode, session_id: session.id, tracks: tracks.map((track) => track.source_uuid) });
      if (mode === "test") await session.delivery.deliver(pathTestCaption(session, tracks[0], providerHealth, this.now()));
      return this.status(session.id);
    } catch (error) {
      if (session.providerActive) try { await this.provider.stopSession(session.id); } catch { }
      try { await session.delivery.stop(); } catch { }
      if (session.benchmarkId && this.benchmarks) this.benchmarks.finish(session.id, "failed");
      session.providerActive = false;
      session.benchmarkId = null;
      session.mode = null;
      session.state = "idle";
      throw error;
    }
  }
  async stop(sessionId, reason = "requested") {
    const session = this.require(sessionId);
    clearTimeout(session.graceTimer);
    session.graceTimer = null;
    await session.delivery.stop();
    if (session.providerActive) await this.provider.stopSession(session.id);
    if (session.benchmarkId && this.benchmarks) {
      const benchmarkStatus = reason === "silence_timeout" ? "silence_timeout" : reason === "provider_failed" ? "failed" : reason === "disconnect" || reason === "plugin_shutdown" ? "aborted" : "completed";
      const result = this.benchmarks.finish(session.id, benchmarkStatus);
      if (result) session.send("benchmark_complete", benchmarkCompletion(result), session.id);
      session.benchmarkId = null;
    }
    session.state = "idle";
    session.mode = null;
    session.providerActive = false;
    for (const track of session.tracks.values()) track.buffer.clear();
    this.log.append({ kind: "session", state: "stopped", reason, session_id: session.id });
    if (!session.connected) this.expireLater(session);
    return this.status(session.id);
  }
  async audio(sessionId, frame) {
    const session = this.require(sessionId);
    if (frame.session_id !== session.id) throw new Error("Audio session does not match the negotiated session.");
    const track = session.tracks.get(frame.source_uuid);
    if (!track) return { accepted: false, reason: "unselected_source" };
    const sequence = track.sequence.accept(frame.sequence);
    if (!sequence.accepted) return { accepted: false, reason: "out_of_order" };
    if (sequence.gap) this.log.append({ kind: "audio_gap", session_id: session.id, source_uuid: track.source_uuid, missing_frames: sequence.gap });
    if (session.state !== "running" || !track.enabled || !track.program_active || track.source_missing || frame.muted || !frame.active) return { accepted: false, reason: "inactive" };
    track.buffer.push(frame.pcm, this.now());
    const accepted = await this.provider.pushAudio(session.id, track.source_uuid, frame);
    return { accepted: accepted !== false, gap: sequence.gap, buffer: track.buffer.metrics() };
  }
  disconnect(sessionId) {
    const session = this.sessions.get(sessionId);
    if (!session) return;
    session.connected = false;
    if (session.state === "running" && session.mode === "benchmark") this.stop(session.id, "disconnect").catch(() => {});
    else if (session.state === "running") this.beginGrace(session);
    else this.expireLater(session);
  }
  status(sessionId) {
    const session = this.require(sessionId);
    return { session_id: session.id, state: session.state, mode: session.mode, benchmark_id: session.benchmarkId, obs: { ...session.obs }, grace_until: session.graceUntil || null, tracks: Array.from(session.tracks.values()).map(serializeTrack) };
  }
  summary() {
    const sessions = Array.from(this.sessions.values());
    return {
      total: sessions.length,
      connected: sessions.filter((session) => session.connected).length,
      running: sessions.filter((session) => session.state === "running").length,
      test_running: sessions.filter((session) => session.state === "running" && session.mode === "test").length,
      live_running: sessions.filter((session) => session.state === "running" && session.mode === "live").length
    };
  }
  async readiness() {
    const health = await this.provider.health();
    const fingerprint = crypto.createHash("sha256").update(JSON.stringify({
      contract: "voice-free-path-v1", provider: health.provider || "whisper_cpp_server",
      model: health.model || null, backend: health.backend || null, settings: this.readinessContext()
    })).digest("hex");
    return {
      ready: Boolean(health.healthy && health.model_ready), state: health.state,
      model: health.model || null, backend: health.backend || health.model?.backend || "whisper.cpp",
      fingerprint,
      detail: health.healthy && health.model_ready ? "The speech model is loaded and ready." : "Speech recognition needs attention in Lumi."
    };
  }
  async close() { for (const session of Array.from(this.sessions.values())) await this.stop(session.id, "plugin_shutdown"); this.sessions.clear(); }
  require(id) { const session = this.sessions.get(id); if (!session) throw new Error("Session was not found."); return session; }
  beginGrace(session) {
    clearTimeout(session.graceTimer);
    session.state = "grace";
    session.graceUntil = this.now() + this.graceMs;
    session.delivery.pause();
    session.graceTimer = setTimeout(() => this.stop(session.id, "stream_grace_expired").catch(() => {}), this.graceMs);
    session.graceTimer.unref?.();
    this.log.append({ kind: "session", state: "grace", session_id: session.id, grace_until: session.graceUntil });
  }
  async resume(session) { clearTimeout(session.graceTimer); session.graceTimer = null; session.graceUntil = 0; session.state = "running"; await session.delivery.resume(); this.log.append({ kind: "session", state: "resumed", session_id: session.id }); }
  expireLater(session) { clearTimeout(session.graceTimer); session.graceUntil = this.now() + this.graceMs; session.graceTimer = setTimeout(() => this.sessions.delete(session.id), this.graceMs); session.graceTimer.unref?.(); }
  async onHypothesis(raw) {
    const session = this.sessions.get(raw.session_id);
    const track = session?.tracks.get(raw.track_id || raw.source_uuid);
    if (!session || !track || session.state !== "running") return;
    track.lastSpeechAt = this.now();
    const primary = Array.from(session.tracks.values()).find((candidate) => candidate.primary);
    if (primary && track !== primary && this.now() - primary.lastSpeechAt < 800) return;
    const stable = this.stabilizer.update(track.source_uuid, raw.text, { final: raw.final, newUtterance: raw.new_utterance, trailingIncomplete: raw.incomplete_word });
    const event = {
      session_id: session.id, source_uuid: track.source_uuid,
      speaker_label: speakerLabel(session, track), ...stable,
      audio: { start_us: raw.audio_start_us || 0, end_us: raw.audio_end_us || 0 },
      latency: { capture_ms: raw.capture_ms || 0, network_ms: raw.network_ms || 0, queue_ms: raw.queue_ms || 0, inference_ms: raw.inference_ms || 0, stabilization_ms: stable.stabilization_ms, total_ms: raw.total_ms || 0 },
      model: { id: raw.model_id || "unknown", provider: "whisper.cpp", backend: raw.backend || "unknown" }
    };
    event.analysis = { transcript: String(raw.text || "").slice(0, 8000), words: Array.isArray(raw.words) ? raw.words.slice(0, 500) : [], emitted_at_ms: raw.emitted_at_ms || this.now() };
    if (session.mode === "benchmark") this.benchmarks?.record(session.id, event);
    const result = await session.delivery.deliver(event);
    this.log.append({ kind: "caption", session_id: session.id, source_uuid: track.source_uuid, caption_text: session.mode === "benchmark" ? undefined : event.stable_text, uncertain_text: session.mode === "benchmark" ? undefined : event.uncertain_text, revision: event.revision, final: event.final, delivery: result.disposition, latency: event.latency, model: event.model, benchmark_id: session.benchmarkId });
  }
  async onProviderError(error) {
    const affected = Array.from(this.sessions.values()).filter((session) => session.state === "running" || session.state === "grace");
    for (const session of affected) {
      clearTimeout(session.graceTimer);
      session.graceTimer = null;
      session.graceUntil = 0;
      session.state = "idle";
      session.mode = null;
      session.providerActive = false;
      for (const track of session.tracks.values()) track.buffer.clear();
      try { await session.delivery.stop(); } catch { }
      if (session.benchmarkId && this.benchmarks) {
        const result = this.benchmarks.finish(session.id, "failed");
        if (result) session.send("benchmark_complete", benchmarkCompletion(result), session.id);
        session.benchmarkId = null;
      }
      session.send("error", {
        code: error.code || "PROVIDER_FAILED",
        message: `Speech recognition stopped: ${error.message}`,
        recoverable: true
      }, session.id);
      this.log.append({ kind: "session", state: "inference_failed", session_id: session.id, code: error.code || "PROVIDER_FAILED", message: error.message });
    }
  }
}

function speakerLabel(session, track) { const enabled = Array.from(session.tracks.values()).filter((candidate) => candidate.enabled); return enabled.length === 1 && track.single_speaker ? null : track.speaker_label || track.display_name; }
function serializeTrack(track) { return { source_uuid: track.source_uuid, display_name: track.display_name, speaker_label: track.speaker_label, enabled: track.enabled, primary: track.primary, single_speaker: track.single_speaker, delivery_enabled: track.delivery_enabled, program_active: track.program_active, source_missing: track.source_missing, last_activity_at: track.last_activity_at, sequence: track.sequence?.metrics?.() }; }
function text(value, max) { return String(value || "").trim().slice(0, max); }
function isUuid(value) { return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(value || "")); }
function pathTestCaption(session, track, health, now) {
  return {
    session_id: session.id, source_uuid: track.source_uuid, speaker_label: null,
    caption_id: crypto.randomUUID(), revision: 1, final: true, new_utterance: true,
    stable_text: "Lumi transcription path is ready.", uncertain_text: "", display_seconds: 2,
    audio: { start_us: 0, end_us: 0 },
    latency: { capture_ms: 0, network_ms: 0, queue_ms: 0, inference_ms: 0, stabilization_ms: 0, total_ms: 0 },
    model: { id: health.model || "unknown", provider: health.provider || "whisper_cpp_server", backend: health.backend || "ready" },
    analysis: { transcript: "Lumi transcription path is ready.", words: [], emitted_at_ms: now }
  };
}
function benchmarkCompletion(result) {
  return {
    id: result.id, status: result.status, transcript: result.transcript, stats: result.stats,
    words: result.words.slice(0, 250).map((word) => ({ text: word.text, latency_ms: word.latency_ms, confidence: word.confidence, final: word.final }))
  };
}

module.exports = { SessionCoordinator, serializeTrack };
