/**
 * Dictation local-speech worker process.
 *
 * Hosts the sherpa-onnx native inference (Parakeet STT) in a separate process
 * so ONNX decoding never blocks the main OpenChamber server. Communicates
 * with the parent over child_process IPC (advanced serialization, so Buffers
 * survive the trip as Uint8Array).
 *
 * Request/response protocol (parent -> worker):
 *   { type: 'session.create', requestId, sessionId, modelsDir, modelId }
 *   { type: 'session.append', requestId, sessionId, audio }
 *   { type: 'session.commit' | 'session.clear' | 'session.close', requestId, sessionId }
 * Worker -> parent:
 *   { type: 'response', requestId, ok, result?, error? }
 *   { type: 'session.committed' | 'session.transcript' | 'session.error', sessionId, ... }
 */

import {
  SherpaOfflineRecognizerEngine,
  SherpaRealtimeTranscriptionSession,
} from './sherpa-recognizer.js';
import { SherpaTtsEngine } from './sherpa-tts.js';
import { getLocalSttModelDir, getLocalSttModelSpec } from './model-catalog.js';
import { pcm16ToWav } from '../audio.js';
import path from 'path';

process.title = 'OpenChamber Dictation';

const engines = new Map();
const ttsEngines = new Map();
const sessions = new Map();
let ipcClosing = false;

function sendToParent(message) {
  if (ipcClosing || !process.connected || !process.send) {
    return;
  }
  try {
    process.send(message, (error) => {
      if (error) {
        ipcClosing = true;
      }
    });
  } catch {
    ipcClosing = true;
  }
}

function sendOk(requestId, result) {
  sendToParent({ type: 'response', requestId, ok: true, ...(result !== undefined ? { result } : {}) });
}

function getEngine(modelsDir, modelId) {
  const key = `${modelsDir}:${modelId}`;
  const existing = engines.get(key);
  if (existing) {
    return existing;
  }
  const modelDir = getLocalSttModelDir(modelsDir, modelId);
  const spec = getLocalSttModelSpec(modelId);
  const created = new SherpaOfflineRecognizerEngine({
    type: spec.type,
    encoder: path.join(modelDir, spec.files.encoder),
    decoder: path.join(modelDir, spec.files.decoder),
    ...(spec.files.joiner ? { joiner: path.join(modelDir, spec.files.joiner) } : {}),
    tokens: path.join(modelDir, spec.files.tokens),
    numThreads: 2,
  });
  engines.set(key, created);
  return created;
}

function cleanupSession(sessionId) {
  const session = sessions.get(sessionId);
  sessions.delete(sessionId);
  try {
    session?.close();
  } catch {
    // ignore
  }
}

function toBuffer(audio) {
  if (Buffer.isBuffer(audio)) {
    return audio;
  }
  if (audio instanceof Uint8Array) {
    return Buffer.from(audio.buffer, audio.byteOffset, audio.byteLength);
  }
  if (audio && typeof audio === 'object' && audio.type === 'Buffer' && Array.isArray(audio.data)) {
    return Buffer.from(audio.data);
  }
  throw new Error('Unsupported audio payload in dictation worker');
}

function getTtsEngine(modelsDir, modelId) {
  const key = `${modelsDir}:${modelId}`;
  const existing = ttsEngines.get(key);
  if (existing) {
    return existing;
  }
  const spec = getLocalSttModelSpec(modelId);
  const created = new SherpaTtsEngine({
    modelDir: getLocalSttModelDir(modelsDir, modelId),
    files: spec.files,
    numThreads: 2,
  });
  ttsEngines.set(key, created);
  return created;
}

async function handleRequest(message) {
  switch (message.type) {
    case 'tts.synthesize': {
      const engine = getTtsEngine(message.modelsDir, message.modelId);
      const { pcm16, sampleRate } = engine.synthesize(message.text, {
        speakerId: message.speakerId,
        speed: message.speed,
      });
      sendOk(message.requestId, {
        audio: pcm16ToWav(pcm16, sampleRate),
        format: 'audio/wav',
      });
      return;
    }
    case 'session.create': {
      cleanupSession(message.sessionId);
      const engine = getEngine(message.modelsDir, message.modelId);
      const session = new SherpaRealtimeTranscriptionSession({ engine });
      session.on('committed', (payload) => {
        sendToParent({ type: 'session.committed', sessionId: message.sessionId, payload });
      });
      session.on('transcript', (payload) => {
        sendToParent({ type: 'session.transcript', sessionId: message.sessionId, payload });
      });
      session.on('error', (err) => {
        sendToParent({
          type: 'session.error',
          sessionId: message.sessionId,
          error: err instanceof Error ? err.message : String(err),
        });
      });
      await session.connect();
      sessions.set(message.sessionId, session);
      sendOk(message.requestId, { requiredSampleRate: session.requiredSampleRate });
      return;
    }
    case 'session.append': {
      sessions.get(message.sessionId)?.appendPcm16(toBuffer(message.audio));
      sendOk(message.requestId);
      return;
    }
    case 'session.commit': {
      sessions.get(message.sessionId)?.commit();
      sendOk(message.requestId);
      return;
    }
    case 'session.clear': {
      sessions.get(message.sessionId)?.clear();
      sendOk(message.requestId);
      return;
    }
    case 'session.close': {
      cleanupSession(message.sessionId);
      sendOk(message.requestId);
      return;
    }
    default: {
      throw new Error(`Unknown dictation worker request: ${message?.type}`);
    }
  }
}

process.on('message', (message) => {
  void handleRequest(message).catch((error) => {
    sendToParent({
      type: 'response',
      requestId: message?.requestId,
      ok: false,
      error: error instanceof Error ? error.message : 'Dictation worker request failed',
    });
  });
});

process.once('disconnect', () => {
  ipcClosing = true;
  for (const sessionId of Array.from(sessions.keys())) {
    cleanupSession(sessionId);
  }
  for (const engine of engines.values()) {
    engine.free();
  }
  for (const tts of ttsEngines.values()) {
    tts.free();
  }
  process.exit(0);
});
