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', 'head-discovery-sample');
const PORT_A = 3300;
const PORT_B = 3301;
const PORT_C = 3302;

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 startNode(port, name, extraEnv = {}) {
  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',
      ...extraEnv,
    },
    stdio: ['ignore', 'pipe', 'pipe'],
    windowsHide: true,
  });
}

async function main() {
  fs.rmSync(SAMPLE_DIR, { recursive: true, force: true });
  const nodeA = startNode(PORT_A, 'seed-a');
  const nodeB = startNode(PORT_B, 'client-b', {
    MAZEX_SEED_PEERS: `http://localhost:${PORT_A}`,
  });
  const nodeC = startNode(PORT_C, 'far-c');
  const children = [nodeA, nodeB, nodeC];
  try {
    await Promise.all([waitForHealth(PORT_A), waitForHealth(PORT_B), waitForHealth(PORT_C)]);
    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_C}/api/base1/reset`, { method: 'POST', body: '{}' });
    await jsonFetch(`http://localhost:${PORT_A}/api/base1/node/peers`, {
      method: 'POST',
      body: JSON.stringify({ url: `http://localhost:${PORT_C}`, name: 'far-c' }),
    });
    await jsonFetch(`http://localhost:${PORT_B}/api/base1/node/bootstrap`, {
      method: 'POST',
      body: JSON.stringify({ seeds: [`http://localhost:${PORT_A}`] }),
    });

    const solve = await jsonFetch(`http://localhost:${PORT_C}/api/base1/demo-solve`, {
      method: 'POST',
      body: JSON.stringify({ solver: 'head-discovery-solver' }),
    });
    if (!solve.correct) throw new Error(`demo solve failed: ${solve.message}`);

    const discovery = await jsonFetch(`http://localhost:${PORT_B}/api/base1/node/head-query`, {
      method: 'POST',
      body: JSON.stringify({ ttl: 2, maxPeers: 8, timeoutMs: 3000 }),
    });
    if (!discovery.ok || !discovery.stronger_than_local) {
      throw new Error('Expected node-b to discover a stronger verified head through seed-a');
    }
    if (!String(discovery.best.source || '').includes(String(PORT_C))) {
      throw new Error(`Expected best head to come from far-c through chain query, got ${discovery.best.source}`);
    }

    const sync = await jsonFetch(`http://localhost:${PORT_B}/api/base1/node/sync-peer`, {
      method: 'POST',
      body: JSON.stringify({ url: `http://localhost:${PORT_C}` }),
    });
    if (!sync.ok || !sync.blocks?.adopted) {
      throw new Error(`Expected node-b to adopt discovered head: ${sync.blocks?.status || sync.snapshot?.status}`);
    }

    const headA = await jsonFetch(`http://localhost:${PORT_C}/api/base1/node/head`);
    const headB = await jsonFetch(`http://localhost:${PORT_B}/api/base1/node/head`);
    if (headA.head.head_hash !== headB.head.head_hash) throw new Error('Expected nodes to share the same head after sync');

    console.table([
      {
        node: 'far-c',
        layer: headA.head.current_layer,
        step: headA.head.current_step,
        height: headA.head.height,
        head: headA.head.head_hash.slice(0, 16),
      },
      {
        node: 'client-b',
        layer: headB.head.current_layer,
        step: headB.head.current_step,
        height: headB.head.height,
        head: headB.head.head_hash.slice(0, 16),
      },
      {
        node: 'discovery',
        layer: discovery.best.head.current_layer,
        step: discovery.best.head.current_step,
        height: discovery.best.head.height,
        head: discovery.best.head.head_hash.slice(0, 16),
      },
    ]);
  } finally {
    for (const child of children) child.kill();
  }
}

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