import { spawn, type ChildProcessWithoutNullStreams } from '../../../comm/child_process.js';
import * as path from 'path';
import {
  SubagentParentMessage,
  SubagentTaskEnvelope,
  SubagentProxyToolCallEvent,
  SubagentWorkerEvent,
  SubagentWorkerEventHandler,
  SubagentWorkerResult,
} from './process_protocol.js';

export interface SubagentProcessRunnerOptions {
  nodePath?: string;
  nodeArgs?: string[];
  workerPath?: string;
  workerArgs?: string[];
  cwd?: string;
  env?: NodeJS.ProcessEnv;
  timeoutMs?: number;
}

export type SubagentProxyToolExecutor = (
  event: SubagentProxyToolCallEvent
) => Promise<string>;

export class SubagentProcessRunner {
  private readonly processes = new Map<string, ChildProcessWithoutNullStreams>();

  constructor(private readonly options: SubagentProcessRunnerOptions = {}) {}

  getActiveRunIds(): string[] {
    return [...this.processes.keys()];
  }

  cancel(runId: string, signal: NodeJS.Signals = 'SIGTERM'): boolean {
    const child = this.processes.get(runId);
    if (!child || child.killed) {
      return false;
    }

    return child.kill(signal);
  }

  run(
    envelope: SubagentTaskEnvelope,
    onEvent?: SubagentWorkerEventHandler,
    onProxyToolCall?: SubagentProxyToolExecutor
  ): Promise<SubagentWorkerResult> {
    if (!envelope.runId) {
      return Promise.reject(new Error('Subagent process envelope requires runId'));
    }
    if (this.processes.has(envelope.runId)) {
      return Promise.reject(new Error(`Subagent process already exists for runId ${envelope.runId}`));
    }

    const workerPath = this.options.workerPath ?? this.resolveDefaultWorkerPath();
    const child = spawn(
      this.options.nodePath ?? process.execPath,
      [
        ...(this.options.nodeArgs ?? []),
        workerPath,
        ...(this.options.workerArgs ?? []),
      ],
      {
        cwd: this.options.cwd,
        env: {
          ...process.env,
          ...(this.options.env ?? {}),
        },
        stdio: 'pipe',
      }
    );

    this.processes.set(envelope.runId, child);

    return new Promise<SubagentWorkerResult>((resolve, reject) => {
      let stdoutBuffer = '';
      let stderrBuffer = '';
      let settled = false;
      const timeoutMs = envelope.timeoutMs ?? this.options.timeoutMs;
      const timeout = timeoutMs && timeoutMs > 0
        ? setTimeout(() => {
            settle('reject', new Error(`Subagent process timed out after ${timeoutMs}ms`));
            child.kill('SIGKILL');
          }, timeoutMs)
        : null;

      const cleanup = () => {
        if (timeout) {
          clearTimeout(timeout);
        }
        if (!child.stdin.destroyed) {
          child.stdin.end();
        }
        this.processes.delete(envelope.runId);
      };

      const settle = (mode: 'resolve' | 'reject', value: SubagentWorkerResult | Error) => {
        if (settled) {
          return;
        }

        settled = true;
        cleanup();
        if (mode === 'resolve') {
          resolve(value as SubagentWorkerResult);
        } else {
          reject(value);
        }
      };

      const writeParentMessage = (message: SubagentParentMessage) => {
        if (child.stdin.destroyed || child.killed) {
          return;
        }
        child.stdin.write(`${JSON.stringify(message)}\n`, 'utf8');
      };

      const handleProxyToolCall = async (event: SubagentProxyToolCallEvent) => {
        if (!onProxyToolCall) {
          writeParentMessage({
            type: 'proxy_tool_error',
            runId: envelope.runId,
            requestId: event.requestId,
            error: 'Subagent worker requested main-process proxy execution, but no proxy executor is configured',
          });
          return;
        }

        try {
          const result = await onProxyToolCall(event);
          writeParentMessage({
            type: 'proxy_tool_result',
            runId: envelope.runId,
            requestId: event.requestId,
            result,
          });
        } catch (error) {
          writeParentMessage({
            type: 'proxy_tool_error',
            runId: envelope.runId,
            requestId: event.requestId,
            error: this.toErrorMessage(error),
          });
        }
      };

      const parseLine = (line: string) => {
        const trimmed = line.trim();
        if (!trimmed) {
          return;
        }

        let event: SubagentWorkerEvent;
        try {
          event = JSON.parse(trimmed) as SubagentWorkerEvent;
        } catch (error) {
          settle(
            'reject',
            new Error(`Invalid subagent worker JSONL event: ${this.toErrorMessage(error)}; line=${trimmed}`)
          );
          child.kill('SIGKILL');
          return;
        }

        if (event.runId !== envelope.runId) {
          settle(
            'reject',
            new Error(`Subagent worker emitted runId ${event.runId}, expected ${envelope.runId}`)
          );
          child.kill('SIGKILL');
          return;
        }

        onEvent?.(event);
        if (event.type === 'proxy_tool_call') {
          void handleProxyToolCall(event);
        } else if (event.type === 'run_end') {
          settle('resolve', event.result);
        } else if (event.type === 'run_error') {
          settle('reject', new Error(event.error));
        }
      };

      child.stdout.on('data', chunk => {
        stdoutBuffer += chunk.toString('utf8');
        const lines = stdoutBuffer.split(/\r?\n/);
        stdoutBuffer = lines.pop() ?? '';
        lines.forEach(parseLine);
      });

      child.stderr.on('data', chunk => {
        stderrBuffer += chunk.toString('utf8');
      });

      child.on('error', error => {
        settle('reject', error);
      });

      child.on('close', code => {
        if (stdoutBuffer.trim()) {
          parseLine(stdoutBuffer);
          stdoutBuffer = '';
        }

        if (settled) {
          return;
        }

        const stderr = stderrBuffer.trim();
        if (code === 0) {
          settle('reject', new Error('Subagent process exited without run_end event'));
        } else {
          settle(
            'reject',
            new Error(
              `Subagent process exited with code ${code}${stderr ? `: ${stderr}` : ''}`
            )
          );
        }
      });

      child.stdin.write(`${JSON.stringify(envelope)}\n`, 'utf8');
    });
  }

  private resolveDefaultWorkerPath(): string {
    return path.join(__dirname, 'worker.js');
  }

  private toErrorMessage(error: unknown): string {
    if (error instanceof Error) {
      return error.message;
    }
    if (typeof error === 'string') {
      return error;
    }
    return 'Unknown error';
  }
}
