import { spawn, type ChildProcess } from 'node:child_process';
import { join } from 'node:path';
import { resolveNodeBin, resolveServerEntry } from './paths';
import { buildChildEnv } from './env';

export interface DaemonHandle {
  url: string;
  token: string;
  stop(): Promise<void>;
}

interface Handshake {
  token: string;
  url: string;
}

/**
 * Lance le daemon en process enfant (Node bundlé) et attend son handshake sur le fd 3
 * (ARBORETUM_EMIT_TOKEN_FD=3 côté serveur) : la réception du JSON {token,url} prouve que le
 * serveur écoute (le handshake est écrit après app.listen). Arrêt propre : SIGTERM puis SIGKILL.
 */
export function startDaemon(opts: {
  dataDir: string;
  port: number;
  onLog?: (line: string) => void;
  onExit?: (code: number | null) => void;
}): Promise<DaemonHandle> {
  const node = resolveNodeBin();
  const entry = resolveServerEntry();
  const dbPath = join(opts.dataDir, 'arboretum.db');
  const env = buildChildEnv({ XDG_DATA_HOME: opts.dataDir, ARBORETUM_EMIT_TOKEN_FD: '3' });

  const child: ChildProcess = spawn(node, [entry, '--port', String(opts.port), '--db', dbPath], {
    env,
    stdio: ['ignore', 'pipe', 'pipe', 'pipe'],
  });

  child.stdout?.on('data', (d: Buffer) => opts.onLog?.(d.toString()));
  child.stderr?.on('data', (d: Buffer) => opts.onLog?.(d.toString()));

  let stopped = false;
  const stop = (): Promise<void> =>
    new Promise((resolve) => {
      if (stopped || child.exitCode !== null) return resolve();
      stopped = true;
      const killTimer = setTimeout(() => child.kill('SIGKILL'), 3000);
      child.once('exit', () => {
        clearTimeout(killTimer);
        resolve();
      });
      child.kill('SIGTERM');
    });

  return new Promise<DaemonHandle>((resolve, reject) => {
    let buf = '';
    let settled = false;
    const fd3 = child.stdio[3] as NodeJS.ReadableStream | null;
    const timer = setTimeout(() => {
      if (settled) return;
      settled = true;
      void stop();
      reject(new Error('daemon handshake timeout'));
    }, 30000);

    child.once('exit', (code) => {
      opts.onExit?.(code);
      if (!settled) {
        settled = true;
        clearTimeout(timer);
        reject(new Error(`daemon exited before handshake (code ${code ?? 'null'})`));
      }
    });

    fd3?.on('data', (chunk: Buffer) => {
      if (settled) return;
      buf += chunk.toString();
      const nl = buf.indexOf('\n');
      if (nl < 0) return;
      settled = true;
      clearTimeout(timer);
      try {
        const hs = JSON.parse(buf.slice(0, nl)) as Handshake;
        resolve({ url: hs.url, token: hs.token, stop });
      } catch (err) {
        void stop();
        reject(err instanceof Error ? err : new Error(String(err)));
      }
    });
  });
}
