import { spawn } from 'child_process';
import type { ChildProcess } from 'child_process';

/**
 * RingBuffer — circular buffer for efficiently capturing process output.
 * Keeps only the most recent `capacity` bytes, discarding older data.
 */
export class RingBuffer {
  private buffer: Buffer;
  private writePos = 0;
  private length = 0;

  constructor(private capacity: number = 65536) {
    this.buffer = Buffer.alloc(capacity);
  }

  /** Write data into the ring buffer. Older data is overwritten when full. */
  write(data: string | Buffer): void {
    const buf = typeof data === 'string' ? Buffer.from(data) : data;

    if (buf.length >= this.capacity) {
      // Data larger than buffer — keep only the last `capacity` bytes
      buf.copy(this.buffer, 0, buf.length - this.capacity);
      this.writePos = 0;
      this.length = this.capacity;
      return;
    }

    // Handle wrap-around: write in up to two chunks
    const firstChunk = Math.min(buf.length, this.capacity - this.writePos);
    buf.copy(this.buffer, this.writePos, 0, firstChunk);
    if (firstChunk < buf.length) {
      buf.copy(this.buffer, 0, firstChunk);
    }

    this.writePos = (this.writePos + buf.length) % this.capacity;
    this.length = Math.min(this.length + buf.length, this.capacity);
  }

  /** Read all buffered data as a UTF-8 string. */
  read(): string {
    if (this.length === 0) {return '';}

    if (this.length < this.capacity) {
      // Buffer not full — data is contiguous, starting at (writePos - length)
      const start = this.writePos - this.length;
      if (start >= 0) {
        // Data sits in a single contiguous region
        return this.buffer.subarray(start, this.writePos).toString('utf-8');
      }
      // Wrapped: data spans from (start + capacity) to end, then 0 to writePos
      const tail = this.buffer.subarray(start + this.capacity);
      const head = this.buffer.subarray(0, this.writePos);
      return Buffer.concat([tail, head]).toString('utf-8');
    }

    // Buffer full — read from writePos (oldest) to end, then 0 to writePos (newest)
    if (this.writePos === 0) {
      // Special case: writePos wrapped to 0, entire buffer is valid in order
      return this.buffer.toString('utf-8');
    }
    const end = this.buffer.subarray(this.writePos);
    const start = this.buffer.subarray(0, this.writePos);
    return Buffer.concat([end, start]).toString('utf-8');
  }

  /** Clear all buffered data. */
  clear(): void {
    this.writePos = 0;
    this.length = 0;
  }

  /** Current amount of data stored (in bytes). */
  get size(): number {
    return this.length;
  }
}

/** A background process managed by the ProcessManager. */
export interface ManagedProcess {
  id: string;
  command: string;
  cwd?: string;
  pid: number;
  status: 'running' | 'exited' | 'killed' | 'error';
  exitCode?: number;
  errorMessage?: string;
  outputBuffer: RingBuffer;
  startedAt: number;
}

export interface ProcessManagerConfig {
  /** Maximum number of concurrent running processes (default 10). */
  maxConcurrent?: number;
  /** Maximum runtime in minutes before a process is killed (default 60). */
  maxRuntimeMinutes?: number;
  /** Ring buffer size in bytes for each process's output (default 65536 / 64KB). */
  bufferSize?: number;
}

/**
 * ProcessManager — manages background processes spawned by an AI agent.
 *
 * Features:
 * - Enforces a concurrent process limit
 * - Captures stdout/stderr into a fixed-size ring buffer per process
 * - Automatically kills processes that exceed a configurable max runtime
 * - Provides graceful shutdown with SIGTERM → SIGKILL escalation
 */
export class ProcessManager {
  private processes = new Map<string, ManagedProcess>();
  private childProcesses = new Map<string, ChildProcess>();
  private nextId = 1;
  private cleanupTimer: ReturnType<typeof setInterval> | null = null;
  private _config: Required<ProcessManagerConfig>;

  constructor(config: ProcessManagerConfig = {}) {
    this._config = {
      maxConcurrent: config.maxConcurrent ?? 10,
      maxRuntimeMinutes: config.maxRuntimeMinutes ?? 60,
      bufferSize: config.bufferSize ?? 65536,
    };

    // Periodic cleanup of expired processes (every 60 seconds)
    this.cleanupTimer = setInterval(() => this._cleanupExpired(), 60_000);
    // Ensure the timer doesn't keep the process alive
    if (this.cleanupTimer.unref) {
      this.cleanupTimer.unref();
    }
  }

  get config(): Required<ProcessManagerConfig> {
    return this._config;
  }

  /** Start a background process. Returns the managed process info. */
  start(command: string, cwd?: string): ManagedProcess {
    // Check concurrent limit
    const running = this.runningCount();
    if (running >= this._config.maxConcurrent) {
      throw new Error(
        `Maximum concurrent processes (${this._config.maxConcurrent}) reached. Kill a running process first.`,
      );
    }

    const id = `proc_${this.nextId++}`;
    const outputBuffer = new RingBuffer(this._config.bufferSize);

    const child = spawn('bash', ['-c', command], {
      cwd,
      stdio: ['ignore', 'pipe', 'pipe'],
      detached: false,
    });

    const proc: ManagedProcess = {
      id,
      command,
      cwd,
      pid: child.pid!,
      status: 'running',
      outputBuffer,
      startedAt: Date.now(),
    };

    this.processes.set(id, proc);
    this.childProcesses.set(id, child);

    // Capture stdout and stderr into the ring buffer
    child.stdout?.on('data', (data: Buffer) => {
      outputBuffer.write(data);
    });
    child.stderr?.on('data', (data: Buffer) => {
      outputBuffer.write(data);
    });

    // Handle process exit
    child.on('exit', (code, signal) => {
      if (signal === 'SIGKILL' || signal === 'SIGTERM') {
        proc.status = 'killed';
      } else {
        proc.status = 'exited';
      }
      proc.exitCode = code ?? undefined;
      this.childProcesses.delete(id);
    });

    // Handle spawn errors (e.g. command not found)
    child.on('error', (err) => {
      proc.status = 'error';
      proc.errorMessage = err.message;
      this.childProcesses.delete(id);
    });

    return proc;
  }

  /** Get a process by ID. */
  get(id: string): ManagedProcess | undefined {
    return this.processes.get(id);
  }

  /** Get recent output from a process's ring buffer. */
  getOutput(id: string): string {
    const proc = this.processes.get(id);
    if (!proc) {throw new Error(`Process ${id} not found`);}
    return proc.outputBuffer.read();
  }

  /** Kill a process by sending a signal. Returns true if signal was sent. */
  kill(id: string, signal: NodeJS.Signals = 'SIGTERM'): boolean {
    const proc = this.processes.get(id);
    if (!proc) {throw new Error(`Process ${id} not found`);}
    if (proc.status !== 'running') {return false;}

    const child = this.childProcesses.get(id);
    if (child) {
      child.kill(signal);
      return true;
    }
    return false;
  }

  /** List all managed processes. */
  list(): ManagedProcess[] {
    return Array.from(this.processes.values());
  }

  /** Count currently running processes. */
  runningCount(): number {
    let count = 0;
    for (const proc of this.processes.values()) {
      if (proc.status === 'running') {count++;}
    }
    return count;
  }

  /** Clean up expired processes that have exceeded the max runtime. */
  private _cleanupExpired(): void {
    const maxAge = this._config.maxRuntimeMinutes * 60_000;
    const now = Date.now();
    for (const [id, proc] of this.processes) {
      if (proc.status === 'running' && (now - proc.startedAt) > maxAge) {
        console.log(`[ProcessManager] Killing expired process ${id} (${proc.command})`);
        this.kill(id, 'SIGKILL');
      }
    }
  }

  /** Shut down all running processes. Called on daemon shutdown. */
  async shutdown(): Promise<void> {
    if (this.cleanupTimer) {
      clearInterval(this.cleanupTimer);
      this.cleanupTimer = null;
    }

    const running = this.list().filter(p => p.status === 'running');
    if (running.length === 0) {return;}

    console.log(`[ProcessManager] Killing ${running.length} running process(es)...`);

    // Send SIGTERM first for graceful shutdown
    for (const proc of running) {
      this.kill(proc.id, 'SIGTERM');
    }

    // Wait up to 5 seconds for graceful shutdown, then escalate to SIGKILL
    await new Promise<void>((resolve) => {
      const timeout = setTimeout(() => {
        for (const proc of running) {
          if (proc.status === 'running') {
            this.kill(proc.id, 'SIGKILL');
          }
        }
        resolve();
      }, 5000);
      timeout.unref?.();

      // Check every 200ms if all processes have exited
      const check = setInterval(() => {
        if (running.every(p => p.status !== 'running')) {
          clearTimeout(timeout);
          clearInterval(check);
          resolve();
        }
      }, 200);
      check.unref?.();
    });
  }
}
