const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');

const ROOT = path.resolve(__dirname, '..');
const NODE = process.execPath;
const SERVER = path.join(ROOT, 'src', 'server.js');
const SAMPLE_DIR = path.join(ROOT, 'data', 'submit-preflight-sample');
const PORT_A = 3400;
const PORT_B = 3401;

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

async function jsonFetch(url, options = {}) {
  const response = await fetch(url, {
    headers: { 'Content-Type': 'application/json' },
    ...options,
  });
  const data = await response.json().catch(() => ({}));
  if (!response.ok || data.error) throw new Error(`${url} failed: ${data.error || data.message || response.status}`);
  return data;
}

async function waitForHealth(port) {
  for (let index = 0; index < 50; index += 1) {
    try {
      return await jsonFetch(`http://localhost:${port}/api/health`);
    } catch {
      await sleep(100);
    }
  }
  throw new Error(`Node ${port} did not become healthy`);
}

function assertInsideWorkspace(target) {
  const resolved = path.resolve(target);
  const workspace = path.resolve(ROOT);
  if (!resolved.startsWith(workspace + path.sep)) {
    throw new Error(`Refusing to remove path outside workspace: ${resolved}`);
  }
}

function startNode(port, name) {
  const dataDir = path.join(SAMPLE_DIR, name);
  fs.mkdirSync(dataDir, { recursive: true });
  return spawn(NODE, [SERVER], {
    cwd: ROOT,
    env: {
      ...process.env,
      PORT: String(port),
      MAZEX_DATA_DIR: dataDir,
      MAZEX_AUTO_BROADCAST: '0',
      MAZEX_SYNC_INTERVAL_MS: '0',
    },
    stdio: ['ignore', 'pipe', 'pipe'],
    windowsHide: true,
  });
}

async function main() {
  assertInsideWorkspace(SAMPLE_DIR);
  fs.rmSync(SAMPLE_DIR, { recursive: true, force: true });
  const children = [startNode(PORT_A, 'node-a'), startNode(PORT_B, 'node-b')];
  try {
    await Promise.all([waitForHealth(PORT_A), waitForHealth(PORT_B)]);
    await jsonFetch(`http://localhost:${PORT_A}/api/base1/reset`, { method: 'POST', body: '{}' });
    await jsonFetch(`http://localhost:${PORT_B}/api/base1/reset`, { method: 'POST', body: '{}' });
    await jsonFetch(`http://localhost:${PORT_B}/api/base1/node/peers`, {
      method: 'POST',
      body: JSON.stringify({ url: `http://localhost:${PORT_A}`, name: 'node-a' }),
    });

    const beforeB = await jsonFetch(`http://localhost:${PORT_B}/api/base1/current`);
    const solveA = await jsonFetch(`http://localhost:${PORT_A}/api/base1/demo-solve`, {
      method: 'POST',
      body: JSON.stringify({ solver: 'preflight-solver-a' }),
    });
    if (!solveA.correct) throw new Error(`node-a demo solve failed: ${solveA.message}`);

    const preflightB = await jsonFetch(`http://localhost:${PORT_B}/api/base1/preflight`, {
      method: 'POST',
      body: JSON.stringify({ ttl: 1, maxPeers: 8, timeoutMs: 3000 }),
    });
    if (!preflightB.changed) throw new Error('Expected node-b preflight to sync the stronger node-a head');
    if (!preflightB.sync?.blocks?.adopted) throw new Error(`Expected node-b to adopt blocks: ${preflightB.sync?.blocks?.status}`);

    const afterB = await jsonFetch(`http://localhost:${PORT_B}/api/base1/current`);
    const witnessB = await jsonFetch(`http://localhost:${PORT_B}/api/base1/witnesses?limit=10`);
    if (beforeB.latest_head_guard.head_hash === afterB.latest_head_guard.head_hash) {
      throw new Error('Expected node-b head to change after preflight sync');
    }
    if (afterB.latest_head_guard.head_hash !== solveA.next.latest_head_guard.head_hash) {
      throw new Error('Expected node-b head to match node-a after preflight');
    }
    if (!witnessB.witnesses.some((item) => item.reason === 'peer_blocks_replayed_and_adopted')) {
      throw new Error('Expected adopted peer block witness to be recorded');
    }

    console.table([
      {
        node: 'node-a',
        phase: 'after solve',
        layer: solveA.next.current.layer_no,
        step: solveA.next.current.step_no,
        height: solveA.next.chain.height,
        head: solveA.next.chain.head_hash.slice(0, 16),
      },
      {
        node: 'node-b',
        phase: 'after preflight',
        layer: afterB.current.layer_no,
        step: afterB.current.step_no,
        height: afterB.chain.height,
        head: afterB.chain.head_hash.slice(0, 16),
      },
      {
        node: 'node-b',
        phase: 'witnesses',
        layer: witnessB.witnesses.length,
        step: '-',
        height: preflightB.sync.blocks.accepted_count,
        head: preflightB.after.head_hash.slice(0, 16),
      },
    ]);
  } finally {
    for (const child of children) child.kill();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
