import { spawn } from 'node:child_process';
import { PKG_ROOT, WORKSPACE, pickExistingDir } from '../../paths.js';
import { redactToolOutput } from '../safety.js';

const DEFAULT_TIMEOUT_MS = 120000;
const MAX_TIMEOUT_MS = 600000;
const MAX_SCRIPT_CHARS = 20000;
const DEFAULT_MAX_OUTPUT_CHARS = 50000;

function maxOutputChars() {
  const value = Number(process.env.MADARX_TOOL_OUTPUT_MAX_BYTES || DEFAULT_MAX_OUTPUT_CHARS);
  if (!Number.isFinite(value) || value <= 0) return DEFAULT_MAX_OUTPUT_CHARS;
  return Math.min(DEFAULT_MAX_OUTPUT_CHARS, Math.floor(value));
}

function cap(text) {
  return redactToolOutput(String(text || '').slice(0, maxOutputChars()));
}

function appendCapped(current, chunk) {
  const limit = maxOutputChars();
  if (current.length >= limit) return current;
  return current + String(chunk).slice(0, limit - current.length);
}

function browserHarnessEnv() {
  const allowed = [
    'PATH',
    'HOME',
    'USER',
    'LOGNAME',
    'TMPDIR',
    'LANG',
    'LC_ALL',
    'BU_CDP_URL',
    'BU_CDP_PORT',
    'BU_CHROME_PROFILE',
  ];
  return Object.fromEntries(allowed.filter(key => process.env[key]).map(key => [key, process.env[key]]));
}

function signalChildTree(child, signal) {
  try {
    if (child.pid && process.platform !== 'win32') {
      process.kill(-child.pid, signal);
      return;
    }
  } catch (error) {
    if (error?.code !== 'ESRCH') throw error;
    return;
  }
  child.kill(signal);
}

export function browserHarnessRunPreflight(input = {}) {
  const script = String(input.script || '').trim();
  if (!script) throw new Error('script is required');
  if (script.length > MAX_SCRIPT_CHARS) {
    throw new Error(`script is too large: ${script.length} chars; max ${MAX_SCRIPT_CHARS}`);
  }
  const timeoutMs = Math.max(
    1000,
    Math.min(MAX_TIMEOUT_MS, Number(input.timeout_ms || DEFAULT_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS),
  );
  const command = String(process.env.MADARX_BROWSER_HARNESS_BIN || 'browser-harness-hermes');
  return { command, script, timeoutMs };
}

export async function browserHarnessRun(input = {}) {
  const { command, script, timeoutMs } = browserHarnessRunPreflight(input);
  const started = Date.now();

  return await new Promise(resolve => {
    let stdout = '';
    let stderr = '';
    let timedOut = false;
    let settled = false;
    let killTimer = null;
    const child = spawn(command, [], {
      // The harness reads its script from stdin, so cwd only needs to exist.
      // ROOT is the build-time-baked path (absent in packaged installs) and
      // made spawn throw ENOENT before the binary even resolved. Use a dir that
      // exists at runtime.
      cwd: pickExistingDir(PKG_ROOT, WORKSPACE),
      shell: false,
      detached: process.platform !== 'win32',
      stdio: ['pipe', 'pipe', 'pipe'],
      env: browserHarnessEnv(),
    });

    const timer = setTimeout(() => {
      timedOut = true;
      signalChildTree(child, 'SIGTERM');
      killTimer = setTimeout(() => {
        if (!settled) signalChildTree(child, 'SIGKILL');
      }, 2000);
    }, timeoutMs);

    child.stdout.on('data', chunk => { stdout = appendCapped(stdout, chunk); });
    child.stderr.on('data', chunk => { stderr = appendCapped(stderr, chunk); });

    child.on('error', error => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      if (killTimer) clearTimeout(killTimer);
      resolve({
        ok: false,
        tool: 'browser_harness_run',
        status: 'failed',
        command,
        error: error.message,
        stdout: cap(stdout),
        stderr: cap(stderr),
        duration_ms: Date.now() - started,
      });
    });

    child.on('close', (code, signal) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      if (killTimer) clearTimeout(killTimer);
      const status = timedOut ? 'timeout' : signal ? 'failed' : code === 0 ? 'complete' : 'failed';
      resolve({
        ok: code === 0 && !signal && !timedOut,
        tool: 'browser_harness_run',
        status,
        command,
        exit_code: code,
        signal,
        stdout: cap(stdout),
        stderr: cap(stderr),
        duration_ms: Date.now() - started,
      });
    });

    child.stdin.end(`${script}\n`);
  });
}
