'use strict';

const { spawn } = require('node:child_process');
const { execFileSync } = require('node:child_process');

const DEFAULT_MAX_OUTPUT = 512 * 1024;

function sanitizeEnv(env, { deleteKeys = [], deletePrefixes = [] } = {}) {
  const next = { ...env };
  for (const key of deleteKeys) delete next[key];
  for (const key of Object.keys(next)) {
    if (deletePrefixes.some(prefix => key.startsWith(prefix))) delete next[key];
  }
  return next;
}

function runCommand(command, args, opts = {}) {
  const {
    cwd,
    env,
    timeoutMs = 300000,
    maxOutputBytes = DEFAULT_MAX_OUTPUT,
    stdin = null,
  } = opts;
  const start = Date.now();

  return new Promise((resolve) => {
    let proc;
    try {
      proc = spawn(command, args, {
        cwd: cwd || process.cwd(),
        env: env || process.env,
        stdio: ['pipe', 'pipe', 'pipe'],
      });
    } catch (err) {
      return resolve({
        success: false,
        output: '',
        stderr: err.message,
        exitCode: -1,
        timedOut: false,
        signal: null,
        latencyMs: Date.now() - start,
      });
    }

    let stdout = '';
    let stderr = '';
    let timedOut = false;
    const timer = timeoutMs > 0 ? setTimeout(() => {
      timedOut = true;
      try { proc.kill('SIGTERM'); } catch {}
    }, timeoutMs) : null;

    proc.stdout.on('data', (chunk) => {
      if (stdout.length < maxOutputBytes) stdout += chunk;
    });
    proc.stderr.on('data', (chunk) => {
      if (stderr.length < maxOutputBytes) stderr += chunk;
    });
    proc.on('error', (err) => {
      if (timer) clearTimeout(timer);
      resolve({
        success: false,
        output: stdout,
        stderr: err.message,
        exitCode: -1,
        timedOut,
        signal: null,
        latencyMs: Date.now() - start,
      });
    });
    proc.on('close', (code, signal) => {
      if (timer) clearTimeout(timer);
      resolve({
        success: code === 0 && !timedOut,
        output: stdout,
        stderr,
        exitCode: Number.isInteger(code) ? code : -1,
        timedOut,
        signal: signal || null,
        latencyMs: Date.now() - start,
      });
    });

    if (stdin != null) proc.stdin.end(stdin);
    else proc.stdin.end();
  });
}

function safeGitOutput(cwd, args, maxBuffer = 2 * 1024 * 1024) {
  try {
    return execFileSync('git', args, {
      cwd,
      timeout: 10000,
      encoding: 'utf8',
      maxBuffer,
    });
  } catch {
    return '';
  }
}

function isSafeRelativePath(relPath) {
  return relPath && !relPath.startsWith('/') && !relPath.split(/[\\/]+/).includes('..');
}

function getChangedFiles(cwd) {
  const stdout = safeGitOutput(cwd, ['status', '--porcelain=v1']);
  const files = [];
  for (const rawLine of stdout.split('\n')) {
    if (!rawLine) continue;
    let rel = rawLine.slice(3);
    const renameIdx = rel.indexOf(' -> ');
    if (renameIdx !== -1) rel = rel.slice(renameIdx + 4);
    rel = rel.replace(/^"|"$/g, '');
    if (isSafeRelativePath(rel)) files.push(rel);
  }
  return [...new Set(files)];
}

function getDiff(cwd, files = []) {
  const safeFiles = files.filter(isSafeRelativePath);
  const args = safeFiles.length > 0 ? ['diff', '--', ...safeFiles] : ['diff'];
  return safeGitOutput(cwd, args, 1024 * 1024);
}

function parseJsonLines(text) {
  const events = [];
  for (const line of String(text || '').split('\n')) {
    const trimmed = line.trim();
    if (!trimmed) continue;
    try {
      events.push(JSON.parse(trimmed));
    } catch {
      // Ignore non-JSON progress lines; CLI versions can mix stderr/stdout.
    }
  }
  return events;
}

function parseJsonObject(text) {
  if (!text || typeof text !== 'string') return null;
  try {
    return JSON.parse(text);
  } catch {
    const fence = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
    if (fence) {
      try { return JSON.parse(fence[1].trim()); } catch {}
    }
    return null;
  }
}

module.exports = {
  DEFAULT_MAX_OUTPUT,
  sanitizeEnv,
  runCommand,
  safeGitOutput,
  isSafeRelativePath,
  getChangedFiles,
  getDiff,
  parseJsonLines,
  parseJsonObject,
};
