import { spawn } from 'node:child_process';

const DEFAULT_TIMEOUT_MS = 30_000;
const MAX_OUTPUT_BYTES = 256 * 1024;

function truncateOutput(value) {
  const text = value || '';
  if (text.length <= MAX_OUTPUT_BYTES) return text;
  return `${text.slice(0, MAX_OUTPUT_BYTES)}\n... output truncated at ${MAX_OUTPUT_BYTES} bytes ...`;
}

export class SerialCommandQueue {
  constructor({ name = 'command-queue' } = {}) {
    this.name = name;
    this.queue = [];
    this.active = null;
    this.sequence = 0;
  }

  get size() {
    return this.queue.length + (this.active ? 1 : 0);
  }

  enqueue(command, options = {}) {
    const entry = {
      id: ++this.sequence,
      command,
      timeoutMs: options.timeoutMs || DEFAULT_TIMEOUT_MS,
      onStart: options.onStart,
      onDone: options.onDone,
      signal: options.signal,
      queuedAt: Date.now()
    };

    return new Promise((resolve) => {
      entry.resolve = resolve;
      if (entry.signal?.aborted) {
        entry.resolve({ ok: false, status: 499, command, stdout: '', stderr: 'Command cancelled before start.', queuedMs: 0, durationMs: 0, queue: this.name, id: entry.id });
        return;
      }
      this.queue.push(entry);
      this.drain();
    });
  }

  drain() {
    if (this.active) return;
    while (this.queue.length > 0) {
      const entry = this.queue.shift();
      if (entry.signal?.aborted) {
        const result = { ok: false, status: 499, command: entry.command, stdout: '', stderr: 'Command cancelled before start.', queuedMs: Date.now() - entry.queuedAt, durationMs: 0, queue: this.name, id: entry.id };
        try { entry.onDone?.(result); } catch { /* callback best effort */ }
        entry.resolve(result);
        continue;
      }
      this.active = entry;
      this.run(entry);
      return;
    }
  }

  run(entry) {
    const startedAt = Date.now();
    let stdout = '';
    let stderr = '';
    let settled = false;
    let timedOut = false;
    let killTimer = null;

    const finish = (status, error = null) => {
      if (settled) return;
      settled = true;
      clearTimeout(timeoutTimer);
      if (killTimer) clearTimeout(killTimer);
      if (entry.signal) entry.signal.removeEventListener('abort', cancel);

      const cancelled = entry.signal?.aborted && !timedOut;
      const result = {
        ok: status === 0 && !timedOut && !error && !cancelled,
        status: cancelled ? 499 : timedOut ? 124 : (status ?? 1),
        command: entry.command,
        stdout: truncateOutput(stdout),
        stderr: truncateOutput(timedOut ? `${stderr}\nCommand timed out after ${entry.timeoutMs}ms.` : (error ? `${stderr}\n${error.message || String(error)}` : stderr)),
        queuedMs: startedAt - entry.queuedAt,
        durationMs: Date.now() - startedAt,
        queue: this.name,
        id: entry.id
      };

      try { entry.onDone?.(result); } catch { /* callback best effort */ }
      entry.resolve(result);
      this.active = null;
      this.drain();
    };

    try { entry.onStart?.({ id: entry.id, command: entry.command, queuedMs: startedAt - entry.queuedAt, queue: this.name }); } catch { /* callback best effort */ }

    const child = spawn('sh', ['-c', entry.command], {
      env: process.env,
      stdio: ['ignore', 'pipe', 'pipe'],
      detached: true
    });

    const killProcessGroup = (signal) => {
      try {
        process.kill(-child.pid, signal);
      } catch {
        child.kill(signal);
      }
    };

    const cancel = () => {
      if (settled) return;
      timedOut = false;
      stderr += '\nCommand cancelled by caller.';
      killProcessGroup('SIGTERM');
      killTimer = setTimeout(() => killProcessGroup('SIGKILL'), 2_000);
    };

    if (entry.signal?.aborted) {
      cancel();
    } else if (entry.signal) {
      entry.signal.addEventListener('abort', cancel, { once: true });
    }

    const timeoutTimer = setTimeout(() => {
      timedOut = true;
      killProcessGroup('SIGTERM');
      killTimer = setTimeout(() => killProcessGroup('SIGKILL'), 2_000);
    }, entry.timeoutMs);

    child.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
    child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
    child.on('error', (error) => finish(1, error));
    child.on('close', (code, signal) => finish(code ?? (signal ? 1 : 0)));
  }
}

export function createKubectlQueue(options = {}) {
  return new SerialCommandQueue({ name: options.name || 'kubectl' });
}
