#!/usr/bin/env node
'use strict';

const { execFileSync } = require('child_process');
const {
  CTM_PROCESS_NAME,
  WALLE_PROCESS_NAME,
  legacyProcessTitle,
  processTitle,
} = require('../claude-task-manager/lib/process-title');

const DEFAULT_MAX_AGE_MIN = 24 * 60;
const PROTECTED_PORTS = new Set([3456, 3457]);

function parseArgs(argv) {
  const opts = {
    kill: false,
    stale: false,
    maxAgeMin: DEFAULT_MAX_AGE_MIN,
    port: null,
    wallePort: null,
    json: false,
  };
  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (arg === '--kill') opts.kill = true;
    else if (arg === '--stale') opts.stale = true;
    else if (arg === '--json') opts.json = true;
    else if (arg === '--max-age-min') opts.maxAgeMin = Number(argv[++i] || DEFAULT_MAX_AGE_MIN);
    else if (arg === '--port') opts.port = Number(argv[++i] || 0) || null;
    else if (arg === '--wall-e-port') opts.wallePort = Number(argv[++i] || 0) || null;
    else if (arg === '--help' || arg === '-h') opts.help = true;
  }
  return opts;
}

function parseElapsedToSeconds(value) {
  const text = String(value || '').trim();
  if (!text) return 0;
  const daySplit = text.split('-');
  let days = 0;
  let rest = text;
  if (daySplit.length === 2) {
    days = Number(daySplit[0]) || 0;
    rest = daySplit[1];
  }
  const parts = rest.split(':').map(n => Number(n) || 0);
  if (parts.length === 3) return days * 86400 + parts[0] * 3600 + parts[1] * 60 + parts[2];
  if (parts.length === 2) return days * 86400 + parts[0] * 60 + parts[1];
  return days * 86400 + (parts[0] || 0);
}

function findExecutable(candidates) {
  for (const file of candidates) {
    try {
      execFileSync(file, ['-v'], { stdio: 'ignore', timeout: 1000 });
      return file;
    } catch {}
  }
  return candidates[0];
}

function runText(file, args, timeout = 3000) {
  try {
    return execFileSync(file, args, { encoding: 'utf8', timeout, maxBuffer: 8 * 1024 * 1024 });
  } catch {
    return '';
  }
}

function listProcesses() {
  const output = runText('ps', ['-A', '-o', 'pid=,ppid=,etime=,pcpu=,command=']);
  const rows = [];
  for (const line of output.split(/\r?\n/)) {
    const trimmed = line.trim();
    if (!trimmed) continue;
    const match = trimmed.match(/^(\d+)\s+(\d+)\s+(\S+)\s+([0-9.]+)\s+(.+)$/);
    if (!match) continue;
    rows.push({
      pid: Number(match[1]),
      ppid: Number(match[2]),
      etime: match[3],
      ageSeconds: parseElapsedToSeconds(match[3]),
      cpu: Number(match[4]) || 0,
      command: match[5].trim(),
    });
  }
  return rows;
}

function lsofBinary() {
  return findExecutable(['lsof', '/usr/sbin/lsof', '/usr/bin/lsof']);
}

function pidsListeningOnPort(port, lsof = lsofBinary()) {
  if (!port || PROTECTED_PORTS.has(Number(port))) return [];
  const output = runText(lsof, [`-tiTCP:${port}`, '-sTCP:LISTEN', '-nP']);
  return [...new Set(output.split(/\s+/).map(n => Number(n)).filter(Number.isInteger))];
}

function processDetails(pid, lsof = lsofBinary()) {
  const output = runText(lsof, ['-Pan', '-p', String(pid)], 2000);
  const ports = [];
  const paths = [];
  for (const line of output.split(/\r?\n/)) {
    const portMatch = line.match(/TCP\s+\S+:(\d+)\s+\(LISTEN\)/);
    if (portMatch) ports.push(Number(portMatch[1]));
    const pathMatch = line.match(/\s(\/(?:private\/)?tmp\/(?:walle|ctm)-[^ ]+)/);
    if (pathMatch) paths.push(pathMatch[1]);
  }
  return { ports: [...new Set(ports)], paths: [...new Set(paths)] };
}

function isProtectedProcess(proc, details = {}) {
  if (/^(ctm|walle)-primary\b/.test(proc.command)) return true;
  if (proc.command === CTM_PROCESS_NAME || proc.command === WALLE_PROCESS_NAME) return true;
  return (details.ports || []).some(port => PROTECTED_PORTS.has(Number(port)));
}

function titleKind(command) {
  const ctmName = CTM_PROCESS_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const walleName = WALLE_PROCESS_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  if (/^ctm-dev-\d+\b/.test(command)) return 'ctm-dev';
  if (new RegExp(`^${ctmName}\\s+dev-\\d+\\b`).test(command)) return 'ctm-dev';
  if (/^walle-dev-\d+\b/.test(command)) return 'walle-dev';
  if (new RegExp(`^${walleName}\\s+dev-\\d+\\b`).test(command)) return 'walle-dev';
  if (/^ctm-(?:badge-test|render-test|e2e|test)(?:[-\w]*)?\b/.test(command)) return 'ctm-test';
  if (new RegExp(`^${ctmName}\\s+(?:badge-test|render-test|e2e|test)(?:[-\\w]*)?\\b`).test(command)) return 'ctm-test';
  if (/^walle-(?:staging|render-test|e2e|test)(?:[-\w]*)?\b/.test(command)) return 'walle-test';
  if (new RegExp(`^${walleName}\\s+(?:staging|render-test|e2e|test)(?:[-\\w]*)?\\b`).test(command)) return 'walle-test';
  return null;
}

function commandMatchesExactPort(command, port, wallePort) {
  if (PROTECTED_PORTS.has(Number(port)) || PROTECTED_PORTS.has(Number(wallePort))) return false;
  if (port && (
    command === legacyProcessTitle('ctm', `dev-${port}`)
    || command === processTitle('ctm', `dev-${port}`)
    || command === legacyProcessTitle('walle', `dev-${port}`)
    || command === processTitle('walle', `dev-${port}`)
  )) return true;
  if (wallePort && (
    command === legacyProcessTitle('walle', `dev-${Number(wallePort) - 1}`)
    || command === processTitle('walle', `dev-${Number(wallePort) - 1}`)
  )) return true;
  return false;
}

function tempBacked(details) {
  return (details.paths || []).some(p =>
    /\/(?:private\/)?tmp\/(?:walle-dev|walle-staging|walle-e2e|ctm-dev|ctm-worktree|ctm-render-test|ctm-e2e)(?:[-/]|$)/.test(p)
  );
}

function collectTargets(procs, opts, detailsByPid = new Map()) {
  const maxAgeSeconds = Math.max(0, Number(opts.maxAgeMin || DEFAULT_MAX_AGE_MIN)) * 60;
  const exactPortPids = new Set([
    ...pidsListeningOnPort(opts.port).filter(pid => Number.isInteger(pid)),
    ...pidsListeningOnPort(opts.wallePort).filter(pid => Number.isInteger(pid)),
  ]);

  const targets = [];
  const kept = [];

  for (const proc of procs) {
    const kind = titleKind(proc.command);
    const maybeExact = exactPortPids.has(proc.pid) || commandMatchesExactPort(proc.command, opts.port, opts.wallePort);
    if (!kind && !maybeExact) continue;

    const details = detailsByPid.get(proc.pid) || processDetails(proc.pid);
    detailsByPid.set(proc.pid, details);

    if (isProtectedProcess(proc, details)) {
      kept.push({ ...proc, reason: 'protected primary CTM/Wall-E' });
      continue;
    }

    if (maybeExact) {
      targets.push({ ...proc, reason: 'matching requested dev port', details });
      continue;
    }

    if (!opts.stale) {
      kept.push({ ...proc, reason: `${kind} candidate; stale sweep disabled`, details });
      continue;
    }

    const oldEnough = proc.ageSeconds >= maxAgeSeconds;
    const orphaned = proc.ppid === 1 && tempBacked(details);
    if (oldEnough || orphaned) {
      targets.push({
        ...proc,
        reason: oldEnough ? `stale ${kind} older than ${opts.maxAgeMin}m` : `orphaned temp-backed ${kind}`,
        details,
      });
    } else {
      kept.push({ ...proc, reason: `${kind} below stale threshold`, details });
    }
  }

  return { targets, kept };
}

function killTargets(targets) {
  const killedByPid = new Map();
  for (const proc of targets) {
    try {
      process.kill(proc.pid, 'SIGTERM');
      killedByPid.set(proc.pid, { ...proc, signal: 'SIGTERM' });
    } catch {}
  }
  const deadline = Date.now() + 1500;
  while (Date.now() < deadline) {
    const alive = targets.filter(proc => {
      try { process.kill(proc.pid, 0); return true; } catch { return false; }
    });
    if (alive.length === 0) return [...killedByPid.values()];
  }
  for (const proc of targets) {
    try {
      process.kill(proc.pid, 0);
      process.kill(proc.pid, 'SIGKILL');
      const previous = killedByPid.get(proc.pid);
      killedByPid.set(proc.pid, { ...(previous || proc), signal: previous ? 'SIGTERM+SIGKILL' : 'SIGKILL' });
    } catch {}
  }
  return [...killedByPid.values()];
}

function formatReport(result, opts) {
  if (opts.json) return JSON.stringify(result, null, 2);
  const action = opts.kill ? 'Cleaned' : 'Would clean';
  const lines = [`${action} ${result.killed.length || result.targets.length} CTM/Wall-E dev process(es); kept ${result.kept.length}.`];
  const visibleTargets = opts.kill ? result.killed : result.targets;
  if (visibleTargets.length > 0) {
    lines.push('');
    lines.push(opts.kill ? 'Killed:' : 'Targets:');
    for (const p of visibleTargets) {
      const ports = p.details && p.details.ports && p.details.ports.length ? ` ports=${p.details.ports.join(',')}` : '';
      lines.push(`  PID ${p.pid} ${p.command} (${p.cpu}% CPU, age ${p.etime}) - ${p.reason}${ports}`);
    }
  }
  if (result.kept.length > 0) {
    lines.push('');
    lines.push('Kept:');
    for (const p of result.kept) {
      lines.push(`  PID ${p.pid} ${p.command} - ${p.reason}`);
    }
  }
  return lines.join('\n');
}

function main(argv = process.argv.slice(2)) {
  const opts = parseArgs(argv);
  if (opts.help) {
    console.log([
      'Usage: node bin/ctm-dev-cleanup.js [--kill] [--stale] [--max-age-min N] [--port N --wall-e-port N] [--json]',
      '',
      `Dry-runs by default. Never targets ${CTM_PROCESS_NAME}/${WALLE_PROCESS_NAME} or ports 3456/3457.`,
    ].join('\n'));
    return 0;
  }
  const detailsByPid = new Map();
  const { targets, kept } = collectTargets(listProcesses(), opts, detailsByPid);
  const killed = opts.kill ? killTargets(targets) : [];
  const result = { targets, killed, kept };
  console.log(formatReport(result, opts));
  return 0;
}

if (require.main === module) {
  process.exitCode = main();
}

module.exports = {
  collectTargets,
  commandMatchesExactPort,
  formatReport,
  isProtectedProcess,
  killTargets,
  parseArgs,
  parseElapsedToSeconds,
  tempBacked,
  titleKind,
};
