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', 'time-order-submit-sample');
const PORT_A = 3500;
const PORT_B = 3501;

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 < 60; 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',
      MAZEX_TIME_BUCKET_SECONDS: '10',
    },
    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_A}/api/base1/node/peers`, {
      method: 'POST',
      body: JSON.stringify({ url: `http://localhost:${PORT_B}`, name: 'node-b', priorityRelay: true }),
    });
    await jsonFetch(`http://localhost:${PORT_B}/api/base1/node/peers`, {
      method: 'POST',
      body: JSON.stringify({ url: `http://localhost:${PORT_A}`, name: 'node-a', priorityRelay: true }),
    });

    const timeSync = await jsonFetch(`http://localhost:${PORT_A}/api/base1/time-sync`, {
      method: 'POST',
      body: JSON.stringify({ maxPeers: 8, timeoutMs: 3000 }),
    });
    if (!timeSync.ok || !timeSync.time_proof?.proof_hash) throw new Error('Expected node-a time sync proof');
    if (timeSync.sample_count < 2) throw new Error('Expected node-a to include peer time sample');

    const solve = await jsonFetch(`http://localhost:${PORT_A}/api/base1/demo-solve`, {
      method: 'POST',
      body: JSON.stringify({ solver: 'time-order-solver' }),
    });
    if (!solve.correct) throw new Error(`Expected demo solve to pass: ${solve.message}`);
    if (!solve.solve_receipt?.receipt_hash) throw new Error('Expected solve receipt');
    if (solve.solve_receipt.receipt_order !== 1) throw new Error(`Expected first receipt order, got ${solve.solve_receipt.receipt_order}`);
    if (!solve.step_lock?.lock_hash) throw new Error('Expected finalized step lock');
    if (solve.step_lock.status !== 'finalized') throw new Error(`Expected finalized lock, got ${solve.step_lock.status}`);
    if (solve.network_confirmation?.status !== 'confirmed') {
      throw new Error(`Expected network confirmation, got ${solve.network_confirmation?.status}`);
    }
    if (solve.network_confirmation.confirmation_count < solve.network_confirmation.required_confirmation_count) {
      throw new Error('Expected confirmation threshold to be reached before solve block is written');
    }
    if (!solve.network_confirmation.peer_results.some((peer) => peer.accepted)) {
      throw new Error('Expected at least one peer to verify the solve candidate');
    }

    const receiptsA = await jsonFetch(`http://localhost:${PORT_A}/api/base1/solve-receipts?limit=5`);
    if (!receiptsA.receipts.some((receipt) => receipt.receipt_hash === solve.solve_receipt.receipt_hash)) {
      throw new Error('Expected receipt API to expose solve receipt');
    }

    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 discover and sync node-a head');
    if (!preflightB.time_sync?.time_proof?.proof_hash) throw new Error('Expected preflight time sync proof');

    const currentB = await jsonFetch(`http://localhost:${PORT_B}/api/base1/current`);
    if (currentB.latest_head_guard.head_hash !== solve.next.latest_head_guard.head_hash) {
      throw new Error('Expected node-b to sync solved head after preflight');
    }

    console.table([
      {
        node: 'node-a',
        phase: 'time-sync',
        bucket: timeSync.time_proof.time_bucket,
        samples: timeSync.sample_count,
        head: timeSync.time_proof.latest_head_hash.slice(0, 16),
      },
      {
        node: 'node-a',
        phase: 'solve',
        bucket: solve.solve_receipt.network_time_bucket,
        samples: `${solve.network_confirmation.confirmation_count}/${solve.network_confirmation.required_confirmation_count}`,
        head: solve.next.latest_head_guard.head_hash.slice(0, 16),
      },
      {
        node: 'node-b',
        phase: 'preflight-sync',
        bucket: preflightB.time_sync.time_proof.time_bucket,
        samples: preflightB.time_sync.sample_count,
        head: currentB.latest_head_guard.head_hash.slice(0, 16),
      },
    ]);
  } finally {
    for (const child of children) child.kill();
  }
}

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