const { EventEmitter } = require("events");
const { spawn } = require("child_process");
const { BoundedQueue } = require("../sessions/bounded_queue");

class TranscriptionProvider extends EventEmitter {
  async loadModel() { throw new Error("Model loading is not implemented."); }
  async benchmark() { throw new Error("Benchmarking is not implemented."); }
  async startSession() { throw new Error("Session start is not implemented."); }
  async addTrack() { throw new Error("Track add is not implemented."); }
  async pushAudio() { throw new Error("Audio input is not implemented."); }
  async removeTrack() { throw new Error("Track removal is not implemented."); }
  async stopSession() { throw new Error("Session stop is not implemented."); }
  async health() { return { healthy: false, state: "unavailable" }; }
}

class WhisperWorkerSupervisor extends EventEmitter {
  constructor(options = {}) {
    super();
    this.executable = options.executable;
    this.args = options.args || [];
    this.spawn = options.spawn || spawn;
    this.maxRestarts = options.maxRestarts ?? 3;
    this.restartWindowMs = options.restartWindowMs || 60000;
    this.queue = new BoundedQueue({ maxItems: options.maxQueuedPackets || 250, maxBytes: options.maxQueuedBytes || 5 * 32000, maxAgeMs: 5000 });
    this.child = null;
    this.state = "stopped";
    this.stopping = false;
    this.restartTimes = [];
    this.stdoutBuffer = "";
    this.lastExit = null;
    this.lastError = null;
    this.lastDiagnostic = null;
    this.startedAt = null;
  }
  start() {
    if (this.child) return;
    if (!this.executable) throw coded("WORKER_NOT_CONFIGURED", "The whisper.cpp worker is not installed. Build or install the Lumi transcription runtime, then retry.");
    this.stopping = false;
    this.state = "starting";
    const child = this.spawn(this.executable, this.args, { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
    this.child = child;
    this.startedAt = Date.now();
    child.stdout.on("data", (chunk) => this.onStdout(chunk));
    child.stderr.on("data", (chunk) => this.onDiagnostic(String(chunk).trim().slice(0, 1000)));
    child.stdin.on("error", (error) => this.onIoError("stdin", error));
    child.stdout.on("error", (error) => this.onIoError("stdout", error));
    child.stderr.on("error", (error) => this.onIoError("stderr", error));
    child.on("error", (error) => this.onProcessError(error));
    child.on("exit", (code, signal) => this.onExit(code, signal));
    child.stdin.on("drain", () => this.flush());
    this.state = "running";
    this.emit("state", { state: this.state });
    this.flush();
  }
  send(metadata, pcm = null) {
    const body = Buffer.from(JSON.stringify(metadata), "utf8");
    const audio = pcm ? Buffer.from(pcm) : Buffer.alloc(0);
    if (body.length > 64 * 1024 || audio.length > 5 * 32000) throw new Error("Worker packet exceeds its limit.");
    const header = Buffer.alloc(8);
    header.writeUInt32LE(body.length, 0);
    header.writeUInt32LE(audio.length, 4);
    const packet = Buffer.concat([header, body, audio]);
    const accepted = this.queue.push(packet, { bytes: packet.length, capturedAt: metadata.captured_at || Date.now() });
    if (accepted) this.flush();
    return accepted;
  }
  flush() {
    if (!this.child || this.state !== "running") return;
    let packet;
    try {
      while ((packet = this.queue.shift())) if (!this.child.stdin.write(packet)) break;
    } catch (error) { this.onIoError("stdin", error); }
  }
  async stop() {
    this.stopping = true;
    this.queue.clear();
    const child = this.child;
    if (!child) { this.state = "stopped"; return; }
    this.sendDirect({ type: "shutdown" });
    await new Promise((resolve) => {
      const timer = setTimeout(() => { child.kill(); resolve(); }, 2000);
      child.once("exit", () => { clearTimeout(timer); resolve(); });
    });
    this.child = null;
    this.state = "stopped";
  }
  sendDirect(metadata) { if (!this.child) return false; const body = Buffer.from(JSON.stringify(metadata)); const header = Buffer.alloc(8); header.writeUInt32LE(body.length, 0); return this.child.stdin.write(Buffer.concat([header, body])); }
  onStdout(chunk) {
    this.stdoutBuffer += String(chunk);
    if (this.stdoutBuffer.length > 256 * 1024) this.stdoutBuffer = this.stdoutBuffer.slice(-64 * 1024);
    let index;
    while ((index = this.stdoutBuffer.indexOf("\n")) >= 0) {
      const line = this.stdoutBuffer.slice(0, index).trim();
      this.stdoutBuffer = this.stdoutBuffer.slice(index + 1);
      if (!line) continue;
      try { this.emit("message", JSON.parse(line)); }
      catch { this.onDiagnostic("Whisper worker emitted malformed output."); }
    }
  }
  onDiagnostic(message) {
    if (!message) return;
    this.lastDiagnostic = { message, at: Date.now() };
    this.emit("diagnostic", { level: "warning", message });
  }
  onIoError(stream, error) {
    if (this.stopping) return;
    this.lastError = { stream, code: error.code || null, message: error.message, at: Date.now() };
    this.emit("error", Object.assign(new Error(`Whisper worker ${stream} failed: ${error.message}`), { code: error.code, stream }));
  }
  onProcessError(error) {
    this.lastError = { stream: "process", code: error.code || null, message: error.message, at: Date.now() };
    this.emit("error", error);
  }
  onExit(code, signal) {
    this.child = null;
    this.lastExit = { code, signal: signal || null, at: Date.now() };
    if (this.stopping) { this.state = "stopped"; return; }
    this.state = "failed";
    this.emit("state", { state: this.state });
    this.emit("crash", { code, signal, at: this.lastExit.at });
    const now = Date.now();
    this.restartTimes = this.restartTimes.filter((time) => now - time < this.restartWindowMs);
    if (this.restartTimes.length >= this.maxRestarts) return;
    this.restartTimes.push(now);
    setTimeout(() => { try { this.start(); } catch (error) { this.emit("error", error); } }, 250).unref?.();
  }
  health() {
    return {
      healthy: this.state === "running", state: this.state, queue: this.queue.metrics(),
      restarts_in_window: this.restartTimes.length, started_at: this.startedAt,
      last_exit: this.lastExit, last_error: this.lastError, last_diagnostic: this.lastDiagnostic
    };
  }
}

class WhisperCppServerProvider extends TranscriptionProvider {
  constructor(supervisor) {
    super(); this.worker = supervisor; this.model = null; this.desiredModel = null; this.modelReady = false; this.loading = false; this.recovering = false; this.sessions = new Set();
    supervisor.on("message", (message) => this.emit(message.type || "message", message));
    supervisor.on("crash", (event) => {
      this.modelReady = false;
      this.sessions.clear();
      this.emit("provider_error", Object.assign(new Error(`Whisper worker exited unexpectedly${event.code == null ? "" : ` with code ${event.code}`}.`), { code: "WORKER_CRASHED", details: event }));
    });
    supervisor.on("error", (error) => this.emit("provider_error", error));
    supervisor.on("state", ({ state }) => {
      if (state === "running" && this.desiredModel && !this.modelReady && !this.loading && !this.recovering) this.recoverModel();
    });
  }
  async loadModel(model) {
    this.desiredModel = model;
    this.loading = true;
    try {
      this.worker.start();
      const result = await this.requestModel(model);
      this.model = model;
      this.modelReady = true;
      return { ...(await this.health()), loaded: result };
    } finally { this.loading = false; }
  }
  async requestModel(model) {
    const loaded = new Promise((resolve, reject) => {
      const timer = setTimeout(() => { cleanup(); reject(new Error("The whisper.cpp worker did not confirm model loading in time.")); }, 30000);
      const onLoaded = (event) => { cleanup(); resolve(event); };
      const onError = (event) => { cleanup(); reject(Object.assign(new Error(event.message || "The model could not be loaded."), { code: event.code })); };
      const onProviderError = (error) => { cleanup(); reject(error); };
      const cleanup = () => { clearTimeout(timer); this.off("model_loaded", onLoaded); this.off("model_error", onError); this.off("provider_error", onProviderError); };
      this.once("model_loaded", onLoaded); this.once("model_error", onError);
      this.once("provider_error", onProviderError);
    });
    this.worker.send({ type: "load_model", model });
    return loaded;
  }
  recoverModel() {
    this.recovering = true;
    this.requestModel(this.desiredModel).then((result) => {
      this.model = this.desiredModel;
      this.modelReady = true;
      this.emit("recovered", { model_id: result.model_id || this.model?.id || null, backend: result.backend || null });
    }).catch((error) => this.emit("provider_error", Object.assign(error, { code: error.code || "MODEL_RECOVERY_FAILED" })))
      .finally(() => { this.recovering = false; });
  }
  async benchmark(options = {}) { this.worker.send({ type: "benchmark", options }); return { accepted: true, model: this.model?.id || null }; }
  async startSession(session) {
    if (!this.modelReady) throw coded("PROVIDER_UNAVAILABLE", "The speech model is not ready in the worker.");
    this.sessions.add(session.id); this.worker.send({ type: "start_session", session });
  }
  async addTrack(sessionId, track) { this.worker.send({ type: "add_track", session_id: sessionId, track }); }
  async pushAudio(sessionId, trackId, frame) { return this.worker.send({ type: "audio", session_id: sessionId, track_id: trackId, sequence: frame.sequence, capture_timestamp_us: frame.capture_timestamp_us, captured_at: Math.floor(frame.capture_timestamp_us / 1000) }, frame.pcm); }
  async removeTrack(sessionId, trackId) { this.worker.send({ type: "remove_track", session_id: sessionId, track_id: trackId }); }
  async stopSession(sessionId) {
    this.sessions.delete(sessionId);
    const stopped = new Promise((resolve) => {
      const timer = setTimeout(() => { cleanup(); resolve(false); }, 5000);
      const onStopped = (event) => { if (event.session_id !== sessionId) return; cleanup(); resolve(true); };
      const cleanup = () => { clearTimeout(timer); this.off("session_stopped", onStopped); };
      this.on("session_stopped", onStopped);
    });
    this.worker.send({ type: "stop_session", session_id: sessionId });
    await stopped;
  }
  async health() {
    const worker = this.worker.health();
    return {
      ...worker, provider: "whisper_cpp_server", model: this.model?.id || this.desiredModel?.id || null,
      model_ready: this.modelReady, sessions: this.sessions.size,
      healthy: worker.healthy && this.modelReady,
      state: !worker.healthy ? worker.state : this.modelReady ? "running" : this.recovering || this.loading ? "loading_model" : "model_unavailable"
    };
  }
  async stop() { this.sessions.clear(); this.modelReady = false; await this.worker.stop(); }
}

function coded(code, message) { return Object.assign(new Error(message), { code }); }

module.exports = { TranscriptionProvider, WhisperWorkerSupervisor, WhisperCppServerProvider };
