import { spawn } from 'node:child_process';

const READY_MARKER = 'net_LANServer is enabled. Not sending heartbeats';

export class DedicatedSupervisor {
  constructor(config, log = console) {
    this.config = config;
    this.log = log;
    this.child = null;
    this.timer = null;
    this.stopping = false;
    this.restartAttempt = 0;
    this.startedAt = null;
    this.readyAt = null;
    this.ready = false;
    this.lastExit = null;
  }

  start() {
    if (!this.config.enabled || this.stopping || this.child || this.timer) return;
    this.#spawn();
  }

  #spawn() {
    if (this.stopping) return;
    this.startedAt = Date.now();
    let child;
    try {
      child = spawn(this.config.command, this.config.args, {
        cwd: this.config.cwd,
        env: process.env,
        stdio: ['ignore', 'pipe', 'pipe'],
      });
    } catch (error) {
      this.lastExit = { at: new Date().toISOString(), error: error.message };
      this.#schedule();
      return;
    }
    this.child = child;
    this.ready = false;
    this.readyAt = null;
    let readinessBuffer = '';
    const inspectReadiness = (chunk) => {
      if (this.ready) return;
      const combined = `${readinessBuffer}${chunk}`;
      if (!combined.includes(READY_MARKER)) {
        readinessBuffer = combined.slice(-(READY_MARKER.length - 1));
        return;
      }
      this.ready = true;
      this.readyAt = Date.now();
      this.log.info?.('OpenQ4 dedicated finished loading the authoritative map');
    };
    child.stdout?.on('data', (chunk) => {
      inspectReadiness(chunk);
      process.stdout.write(`[openq4ded] ${chunk}`);
    });
    child.stderr?.on('data', (chunk) => {
      inspectReadiness(chunk);
      process.stderr.write(`[openq4ded] ${chunk}`);
    });
    let finalized = false;
    const finalize = (code, signal, error = null) => {
      if (finalized) return;
      finalized = true;
      const runtimeMs = Date.now() - this.startedAt;
      this.child = null;
      this.ready = false;
      this.readyAt = null;
      this.lastExit = {
        at: new Date().toISOString(),
        code,
        signal,
        runtimeMs,
        ...(error ? { error: error.message } : {}),
      };
      if (runtimeMs >= this.config.stableMs) this.restartAttempt = 0;
      if (!this.stopping) this.#schedule();
    };
    child.once('error', (error) => finalize(null, null, error));
    child.once('exit', (code, signal) => finalize(code, signal));
    this.log.info?.(`OpenQ4 dedicated spawned pid=${child.pid}`);
  }

  #schedule() {
    if (this.stopping || this.timer) return;
    this.restartAttempt += 1;
    const delay = Math.min(this.config.backoffMaxMs,
      this.config.backoffBaseMs * 2 ** Math.min(this.restartAttempt - 1, 10));
    this.log.warn?.(`OpenQ4 dedicated restart in ${delay}ms (attempt ${this.restartAttempt})`);
    this.timer = setTimeout(() => {
      this.timer = null;
      this.#spawn();
    }, delay);
    this.timer.unref?.();
  }

  status() {
    return {
      enabled: this.config.enabled,
      running: !!this.child,
      ready: !!this.child && this.ready,
      pid: this.child?.pid || null,
      command: this.config.enabled ? this.config.command : null,
      startedAt: this.startedAt ? new Date(this.startedAt).toISOString() : null,
      readyAt: this.readyAt ? new Date(this.readyAt).toISOString() : null,
      restartAttempt: this.restartAttempt,
      restartPending: !!this.timer,
      lastExit: this.lastExit,
    };
  }

  async stop() {
    this.stopping = true;
    if (this.timer) clearTimeout(this.timer);
    this.timer = null;
    const child = this.child;
    if (!child) return;
    await new Promise((resolve) => {
      let finished = false;
      let killer = null;
      const done = () => {
        if (finished) return;
        finished = true;
        if (killer) clearTimeout(killer);
        resolve();
      };
      child.once('exit', done);
      try { child.kill('SIGTERM'); } catch { done(); return; }
      killer = setTimeout(() => {
        try { child.kill('SIGKILL'); } catch {}
        done();
      }, this.config.stopTimeoutMs);
      killer.unref?.();
    });
  }
}
