/**
 * Ops sweep harness v2 — drives each op's API directly and records
 * http / ok / duration / cause. Re-runnable after every fix.
 *
 * Runs ON the app VM against 127.0.0.1:3000 so the session token never leaves
 * the box and Cloudflare is out of the path.
 *
 * v2 exists because v1's numbers could not be trusted. Two effects corrupt a
 * naive sweep, and both are findings in their own right:
 *
 *  1. "agent offline" false-negatives. isAgentConnected() is last_poll_at within
 *     30s, and last_poll_at is only written when the agent STARTS a long-poll.
 *     While it executes work it is not polling, so a sweep that keeps the agent
 *     busy makes every following op 503 in ~10ms. v1 recorded 5 such 503s; all 5
 *     passed when re-run alone. So: retry once after letting the agent poll, and
 *     RECORD that a retry was needed rather than hiding it.
 *  2. Ops that report ok:false with an empty body. v1 filed adnodemgrctl_status
 *     as failed; driven alone it returns ok:true, exit 0, "The Node Manager is
 *     running". Capture a body excerpt on every failure so a bare failure is
 *     visibly distinct from a real one.
 *
 * Usage: node sweep2.js <connection_id> <email> <op1,op2,...>
 */
const crypto = require('crypto');
const pool = require('./db');

const BASE = 'http://127.0.0.1:3000';
const SETTLE_MS = 27000;   // > the agent's 25s long-poll hold, so last_poll_at refreshes

const sleep = ms => new Promise(r => setTimeout(r, ms));

function mintToken(userId) {
  const payload = Buffer.from(JSON.stringify({ userId })).toString('base64url');
  const sig = crypto.createHmac('sha256', process.env.SESSION_SECRET).update(payload).digest('base64url');
  return `${payload}.${sig}`;
}

function classify(httpStatus, body, netErr) {
  if (netErr) return { ok: false, cause: 'transport: ' + netErr };
  const raw = JSON.stringify(body || {});
  if (httpStatus === 503 && /offline|not connected/i.test(raw)) return { ok: false, cause: 'AGENT-OFFLINE false negative', retryable: true };
  if (httpStatus === 401 || httpStatus === 403) return { ok: false, cause: 'auth/permission' };
  if (httpStatus === 402) return { ok: false, cause: 'tier gate' };
  if (httpStatus === 409) return { ok: false, cause: 'conflict — another job running', retryable: true };
  if (httpStatus === 404) return { ok: false, cause: 'route missing (404)' };
  if (/NO_WLS_PWD|NO_APPS_PWD|NO_PASSWORDS/.test(raw)) return { ok: false, cause: 'MISSING CREDENTIAL (not a code defect)' };
  if (/DELIVERY_ATTEMPTS_EXCEEDED/.test(raw))          return { ok: false, cause: 'result undeliverable' };
  if (/RESULT_TOO_LARGE/.test(raw))                    return { ok: false, cause: 'result too large' };
  if (/timed out|ETIMEDOUT/i.test(raw))                return { ok: false, cause: 'agent timeout', retryable: true };
  if (httpStatus >= 500) return { ok: false, cause: 'server error: ' + String((body && (body.error || body.detail)) || '').slice(0, 70) };
  if (httpStatus !== 200) return { ok: false, cause: 'http ' + httpStatus };

  // 200 does not mean success — the proxy reports its own ok flag.
  if (body && body.ok === false) {
    const tail = String(body.stdout || body.output || body.error || '').replace(/\s+/g, ' ').trim().slice(0, 90);
    return { ok: false, cause: tail ? 'op failed: ' + tail : 'op reported ok:false WITH NO DIAGNOSTIC', retryable: !tail };
  }
  if (body && body.job_id) return { ok: true, cause: 'background job ' + body.job_id };
  return { ok: true, cause: '' };
}

async function once(token, connectionId, opKey) {
  const t0 = Date.now();
  try {
    const res = await fetch(`${BASE}/api/ebs-ops/middleware-run`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
      body: JSON.stringify({ connection_id: connectionId, op_key: opKey }),
    });
    const text = await res.text();
    let body; try { body = JSON.parse(text); } catch { body = { raw: text.slice(0, 200) }; }
    return { http: res.status, ms: Date.now() - t0, body, ...classify(res.status, body) };
  } catch (e) {
    return { http: 0, ms: Date.now() - t0, body: null, ...classify(0, null, e.message) };
  }
}

(async () => {
  const [connectionId, email, opsCsv] = process.argv.slice(2);
  if (!process.env.SESSION_SECRET) { console.error('SESSION_SECRET not in env'); process.exit(2); }
  const { rows } = await pool.query('SELECT id FROM users WHERE email = :p1', { p1: email });
  if (!rows.length) { console.error('no such user: ' + email); process.exit(2); }
  const token = mintToken(rows[0].id);

  const ops = opsCsv.split(',').map(s => s.trim()).filter(Boolean);
  const results = [];

  for (const op of ops) {
    let r = await once(token, Number(connectionId), op);
    let retried = false;
    if (!r.ok && r.retryable) {
      await sleep(SETTLE_MS);              // let the agent re-poll
      const r2 = await once(token, Number(connectionId), op);
      retried = true;
      r = { ...r2, firstAttempt: r.cause };
    }
    results.push({ op, ...r, retried });
    console.log([
      r.ok ? 'PASS' : 'FAIL',
      String(r.http).padEnd(3),
      (r.ms + 'ms').padEnd(8),
      op.padEnd(24),
      retried ? '[retried: ' + r.firstAttempt + '] ' : '',
      r.cause,
    ].join('  '));
    if (!r.ok && r.body) console.log('       body: ' + JSON.stringify(r.body).slice(0, 300));
  }

  const pass = results.filter(r => r.ok).length;
  const flaky = results.filter(r => r.retried && r.ok).length;
  console.log(`\n=== ${pass}/${results.length} ok  (${flaky} needed a retry after a false "agent offline") ===`);
  process.exit(0);
})().catch(e => { console.error('HARNESS ERROR', e); process.exit(2); });
