const fs = require('fs');
const path = require('path');
const http = require('http');
const https = require('https');
const { execSync } = require('child_process');

const DEFAULT_RESTART_HEALTH_TIMEOUT_MS = 15000;
const DEFAULT_RESTART_HEALTH_INTERVAL_MS = 250;

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function readEngineControl(minionsHome) {
  try {
    return JSON.parse(fs.readFileSync(path.join(minionsHome, 'engine', 'control.json'), 'utf8'));
  } catch {
    return null;
  }
}

function normalizePid(pid) {
  const n = Number(pid);
  return Number.isInteger(n) && n > 0 ? n : null;
}

function isProcessAlive(pid) {
  const n = normalizePid(pid);
  if (!n || n === process.pid) return false;
  try {
    if (process.platform === 'win32') {
      const out = execSync(`tasklist /FI "PID eq ${n}" /NH`, {
        encoding: 'utf8',
        windowsHide: true,
        timeout: 3000,
      });
      return new RegExp(`\\b${n}\\b`).test(out) && out.toLowerCase().includes('node');
    }
    process.kill(n, 0);
    return true;
  } catch {
    return false;
  }
}

function isPortListening(port) {
  const n = Number(port);
  if (!Number.isInteger(n) || n <= 0) return false;
  try {
    if (process.platform === 'win32') {
      const out = execSync(`netstat -ano -p TCP`, {
        encoding: 'utf8', windowsHide: true, timeout: 3000, maxBuffer: 4 * 1024 * 1024,
      });
      const re = new RegExp(`\\s127\\.0\\.0\\.1:${n}\\s+\\S+\\s+LISTENING`, 'i');
      const re6 = new RegExp(`\\s\\[::1?\\]:${n}\\s+\\S+\\s+LISTENING`, 'i');
      return re.test(out) || re6.test(out);
    }
    try {
      const out = execSync(`lsof -nP -iTCP:${n} -sTCP:LISTEN`, {
        encoding: 'utf8', timeout: 3000,
      });
      if (/\bLISTEN\b/i.test(out)) return true;
    } catch {}
    const out = execSync(`ss -ltn 'sport = :${n}' 2>/dev/null || netstat -ltn 2>/dev/null || netstat -an -p tcp 2>/dev/null`, {
      encoding: 'utf8', timeout: 3000, shell: true,
    });
    return new RegExp(`(?:[:.])${n}\\b[^\\n]*\\b(?:LISTEN|LISTENING)\\b`, 'i').test(out);
  } catch { return false; }
}

function httpGetJson(url, timeoutMs = 1000) {
  return new Promise(resolve => {
    let settled = false;
    const parsed = new URL(url);
    const client = parsed.protocol === 'https:' ? https : http;
    const req = client.get(parsed, { timeout: timeoutMs }, res => {
      let body = '';
      res.setEncoding('utf8');
      res.on('data', chunk => { body += chunk; });
      res.on('end', () => {
        if (settled) return;
        settled = true;
        let json = null;
        try { json = body ? JSON.parse(body) : null; }
        catch (err) {
          resolve({ ok: false, statusCode: res.statusCode, error: err, body });
          return;
        }
        resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, json, body });
      });
    });
    req.on('timeout', () => {
      if (settled) return;
      settled = true;
      req.destroy();
      resolve({ ok: false, error: new Error(`timed out after ${timeoutMs}ms`) });
    });
    req.on('error', err => {
      if (settled) return;
      settled = true;
      resolve({ ok: false, error: err });
    });
  });
}

async function checkRestartHealth(options = {}) {
  const {
    minionsHome,
    dashboardUrl,
    dashboardPid,
    dashboardPort = 7331,
    readControl = readEngineControl,
    isProcessAlive: isAlive = isProcessAlive,
    isPortListening: portCheck = isPortListening,
    httpGetJson: getJson = httpGetJson,
  } = options;

  const control = readControl(minionsHome);
  const pid = normalizePid(control && control.pid);
  const engineAlive = pid ? isAlive(pid) : false;
  const engineOk = control && control.state === 'running' && engineAlive;

  // Two strategies for the dashboard check:
  //   1) Process + port-listening (preferred from `minions restart` since
  //      `bin/minions.js` knows the dashboard PID it just spawned). This avoids
  //      a roundtrip through the dashboard's Node event loop, which can be
  //      blocked for 15-25s during a cold `getStatus()` rebuild while still
  //      being healthy.
  //   2) HTTP `/api/health` — legacy path retained for tests that mock
  //      httpGetJson + dashboardUrl, and for external probes that genuinely
  //      want a response-level check.
  // Strategy 1 wins when `dashboardPid` is supplied; otherwise we fall back to
  // HTTP using the default URL or whatever the caller injected.
  let dashboardOk;
  let dashboardDetail;
  let dashboardKind;
  let dashboardSnapshot;
  if (dashboardPid != null && !dashboardUrl) {
    dashboardKind = 'process';
    const dpid = normalizePid(dashboardPid);
    const dashAlive = dpid ? isAlive(dpid) : false;
    const portOpen = portCheck(dashboardPort);
    dashboardOk = !!(dashAlive && portOpen);
    dashboardDetail = `pid=${dpid || 'none'} alive=${dashAlive ? 'yes' : 'no'} port=${dashboardPort} listening=${portOpen ? 'yes' : 'no'}`;
    dashboardSnapshot = { kind: 'process', pid: dpid, alive: dashAlive, port: dashboardPort, listening: portOpen };
  } else {
    dashboardKind = 'http';
    const url = dashboardUrl || `http://127.0.0.1:${dashboardPort}/api/health`;
    const dashboard = await getJson(url, 3000);
    const dashboardStatus = dashboard && dashboard.json && dashboard.json.status;
    dashboardOk = !!(dashboard && dashboard.ok && dashboardStatus === 'healthy');
    dashboardDetail = dashboard && dashboard.error
      ? dashboard.error.message
      : dashboard && dashboard.statusCode
        ? `HTTP ${dashboard.statusCode}${dashboardStatus ? `, status=${dashboardStatus}` : ''}`
        : 'no response';
    dashboardSnapshot = { kind: 'http', url, ok: !!(dashboard && dashboard.ok), statusCode: dashboard && dashboard.statusCode, status: dashboardStatus };
  }

  const errors = [];
  if (!engineOk) {
    const state = control && control.state ? control.state : 'unknown';
    const pidLabel = pid || 'none';
    errors.push(`Engine is not healthy (state=${state}, pid=${pidLabel}, alive=${engineAlive ? 'yes' : 'no'})`);
  }
  if (!dashboardOk) {
    const where = dashboardKind === 'http'
      ? (dashboardUrl || `http://127.0.0.1:${dashboardPort}/api/health`)
      : `dashboard pid=${normalizePid(dashboardPid) || 'none'} port=${dashboardPort}`;
    errors.push(`Dashboard failed health check at ${where} (${dashboardDetail})`);
  }

  return {
    ok: engineOk && dashboardOk,
    engine: { state: control && control.state, pid, alive: engineAlive },
    dashboard: dashboardSnapshot,
    errors,
  };
}

async function waitForRestartHealth(options = {}) {
  const timeoutMs = options.timeoutMs ?? DEFAULT_RESTART_HEALTH_TIMEOUT_MS;
  const intervalMs = options.intervalMs ?? DEFAULT_RESTART_HEALTH_INTERVAL_MS;
  const maxAttempts = normalizePid(options.maxAttempts);
  const started = Date.now();
  let attempts = 0;
  let last = null;

  while (true) {
    attempts++;
    last = await checkRestartHealth(options);
    last.attempts = attempts;
    last.elapsedMs = Date.now() - started;
    if (last.ok) return last;
    if (maxAttempts && attempts >= maxAttempts) break;
    const remainingMs = timeoutMs - (Date.now() - started);
    if (!maxAttempts && remainingMs <= 0) break;
    await sleep(maxAttempts ? intervalMs : Math.min(intervalMs, remainingMs));
  }

  return last || {
    ok: false,
    attempts,
    elapsedMs: Date.now() - started,
    errors: ['Restart health check did not run'],
  };
}

function formatRestartHealthError(result) {
  const elapsed = typeof result.elapsedMs === 'number' ? `${result.elapsedMs}ms` : 'unknown time';
  const attempts = result.attempts || 0;
  const details = (result.errors || ['Unknown restart verification failure']).map(err => `  - ${err}`).join('\n');
  return `\n  ERROR: Restart verification failed after ${elapsed} (${attempts} attempt${attempts === 1 ? '' : 's'}).\n${details}\n`;
}

module.exports = {
  DEFAULT_RESTART_HEALTH_TIMEOUT_MS,
  DEFAULT_RESTART_HEALTH_INTERVAL_MS,
  checkRestartHealth,
  waitForRestartHealth,
  formatRestartHealthError,
  _private: {
    httpGetJson,
    isProcessAlive,
    isPortListening,
    readEngineControl,
    normalizePid,
  },
};
