import { spawn, ChildProcess } from 'child_process';
import { join } from 'path';
import { fileURLToPath } from 'url';
import { randomUUID } from 'crypto';
import { readFileSync, rmSync, readdirSync, statSync, unlinkSync, mkdirSync } from 'fs';
import { piperService as httpPiperService } from './piper.http.service.js';
import { getPiperUrl, getPiperModel } from '../config.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = join(__filename, '..');
const SCRIPT_DIR = join(__dirname, '..');

const TTS_DIR = join('/tmp', 'quibot-audio', 'tts-piper');
mkdirSync(TTS_DIR, { recursive: true });

// ─── type-guard for JSON messages from piper-worker ───
type PiperMsg =
  | { type: 'ready' }
  | { type: 'init_ok' }
  | { type: 'init_error'; error: string }
  | { type: 'synthesized'; outPath: string; bytes?: number; msgId: string }
  | { type: 'error'; text: string; msgId: string };

class PiperLocalService {
  private proc: ChildProcess | null = null;
  private pendingInit: Promise<void> | null = null;
  private cleanupTimer: ReturnType<typeof setInterval> | null = null;

  // ── spawn + stdout parser (simple: pure JSON on stdout, WAV on disk) ──

  private setupStdout(): void {
    if (!this.proc?.stdout) return;
    let buf = '';
    this.proc.stdout.on('data', (chunk: Buffer) => {
      buf += chunk.toString();
      while (true) {
        const nl = buf.indexOf('\n');
        if (nl === -1) break;
        const line = buf.slice(0, nl).trim();
        buf = buf.slice(nl + 1);
        if (!line) continue;
        this.handleLine(line);
      }
    });
  }

  private handleLine(line: string): void {
    console.log('[RX]', line);
  let msg: PiperMsg;

  try {
    msg = JSON.parse(line) as PiperMsg;
  } catch {
    console.warn('[piper-svc] Invalid JSON:', line);
    return;
  }

  switch (msg.type) {
    case 'ready':
      break;

    case 'init_ok':
      this.resolveInit();
      break;

    case 'init_error':
      this.resolveInitError(new Error(msg.error));
      break;

    case 'synthesized':
      this.resolveResponse(msg.msgId, msg.outPath);
      break;

    case 'error':
      this.rejectResponse(
        msg.msgId,
        new Error(msg.text)
      );
      break;
  }
}

  private async writeStdin(line: string): Promise<void> {
  if (!this.proc?.stdin) {
    throw new Error('piper-svc: stdin unavailable');
  }

  console.log('[TX]', line);

  this.proc.stdin.write(line + '\n');
}

  // ── pending-init promises (separate from synth to avoid clearing respMap on init failure) ──
  private initResolve: (() => void) | null = null;
private initReject: ((e: Error) => void) | null = null;

  private resolveInit(): void {
  if (!this.pendingInit) return;

  this.initResolve?.();

  this.pendingInit = null;
  this.initResolve = null;
  this.initReject = null;
}

private rejectResponse(msgId: string, err: Error): void {
  const entry = this.respMap.get(msgId);

  this.respMap.delete(msgId);

  if (entry) {
    entry.reject(err);
  }
}

private resolveInitError(err: Error): void {
  if (!this.pendingInit) return;

  this.initReject?.(err);

  this.pendingInit = null;
  this.initResolve = null;
  this.initReject = null;
}

  // ── pending synth responses (separate from init so init failure doesn't clear them) ──
  private respMap = new Map<string, {
    resolve: (wavPath: string) => void;
    reject: (e: Error) => void;
  }>();

  private resolveResponse(msgId: string, wavPath: string): void {
    const entry = this.respMap.get(msgId);
    this.respMap.delete(msgId);
    if (entry?.resolve) entry.resolve(wavPath);
  }

  // ── public spawn / initWav / synthWav ──

  private async _spawn(): Promise<void> {
    if (this.proc) return;

    const workerPath = join(SCRIPT_DIR, 'piper-worker.py');
    const venv = process.env.VIRTUAL_ENV || join(SCRIPT_DIR, '..', '.venv', 'bin', 'python3');
    this.proc = spawn(venv, [workerPath], { stdio: ['pipe', 'pipe', 'pipe'] });

    this.setupStdout();

    if (this.proc.stderr) {
      this.proc.stderr.on('data', (chunk: Buffer) => {
        console.error(
          '[piper-worker]',
          chunk.toString().trim()
        );
      });
    }

    this.proc.on('exit', () => {
      console.log('[piper-svc] Process exited, rejecting all pending synths');
      // reject all pending (new format: {resolve, reject})
      for (const [, entry] of this.respMap) entry.reject(new Error('piper process exited'));
      this.respMap.clear();
      if (this.pendingInit) { this.initReject(new Error('piper process exited')); this.pendingInit = null; }
    });

    // ── cleanup old WAV files every 5 min ──
    this.cleanupTimer = setInterval(() => {
      try {
        const now = Date.now();
        for (const entry of readdirSync(TTS_DIR)) {
          const fp = join(TTS_DIR, entry);
          try {
            const s = statSync(fp);
            if (s.isFile() && now - s.mtimeMs > 300_000) unlinkSync(fp);
          } catch { /* ignore */ }
        }
      } catch { /* ignore */ }
    }, 5 * 60 * 1000);
  }

  async initWav(): Promise<void> {
    if (!this.proc) await this._spawn();
    if (this.pendingInit) return this.pendingInit;

   this.pendingInit = new Promise<void>((resolve, reject) => {
      this.initResolve = resolve;
      this.initReject = reject;
    });
    const modelPath = getPiperModel() || join('/tmp', 'quibot-piper-models', 'ca_ES-upc_ona-medium.onnx');
    const cfgPath = modelPath.replace(/\.onnx$/, '.onnx.json');
    await this.writeStdin(
      JSON.stringify({ type: 'init', model: modelPath, config: cfgPath }),
    );
    return this.pendingInit;
  }

  /** Synthesize with local Piper subprocess (primary) and HTTP Piper fallback */
  async synthesize(params: { text: string }): Promise<Buffer> {
    try {
      await this.initWav();
      return await this.synthWav(params.text);
    } catch (localErr) {
      const url = getPiperUrl();
      if (url) {
        console.log(`[tts] Local Piper failed: ${localErr instanceof Error ? localErr.message : localErr}. Falling back to remote.`);
        return await httpPiperService.synthesize({ ...params, lang: 'ca' });
      }
      throw localErr;
    }
  }

  async synthWav(text: string): Promise<Buffer> {
    await this.initWav();
    if (!this.proc) await this._spawn(); // auto-spawn; init runs concurrently
    const msgId = randomUUID() + '-' + Date.now();
    const outPath = join(TTS_DIR, `${msgId}.wav`);

    return new Promise((resolve, reject) => {
      let cleared = false;
      const timer = setTimeout(() => {
        if (cleared) return;
        cleared = true;
        this.respMap.delete(msgId);
        reject(new Error('piper-svc: synthesis timed out after 120s'));
      }, 120_000);

      this.respMap.set(msgId, {
        resolve: (wavPath: string) => {
          if (cleared) return;
          cleared = true;
          clearTimeout(timer);
          console.log(`[piper-svc] Synthesized ${wavPath} (${Date.now()})`);
          try {
            const buf = readFileSync(wavPath);
            rmSync(wavPath, { force: true });
            resolve(buf);
          } catch (err: unknown) {
            reject(err instanceof Error ? err : new Error('read WAV failed'));
          }
        },
        reject: (e: Error) => {
          if (cleared) return;
          cleared = true;
          clearTimeout(timer);
          this.respMap.delete(msgId);
          reject(e);
        },
      });

      console.log(`[piper-svc] synthesize ${text.substring(0, 40)}... (msgId=${msgId})`);
      this.writeStdin(
        JSON.stringify({ type: 'synthesize', text, msgId, outPath }),
      ).catch((e) => {
        if (cleared) return;
        cleared = true;
        this.respMap.delete(msgId);
        reject(e instanceof Error ? e : new Error(String(e)));
      });
    });
  }

  // ── shutdown ──

  async shutdown(): Promise<void> {
    if (this.cleanupTimer) {
      clearInterval(this.cleanupTimer);
      this.cleanupTimer = null;
    }
    if (!this.proc) return;
    const p = this.proc; this.proc = null;
    await new Promise<void>((res) => {
      p.on('exit', res);
      setTimeout(res, 3000);
      if (!p.killed) { try { p.stdin?.end(); } catch {} p.kill('SIGTERM'); }
    });
  }
}

// ─── singleton ────────────────────────────────────────────────

export const piperService = new PiperLocalService();
