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

const ROOT = path.resolve(__dirname, '..');
const NODE = process.execPath;
const WORK_DIR = path.join(ROOT, 'data', 'two-copy-p2p-stress');
const COPY_DIR = path.join(WORK_DIR, 'project-copy');
const ORIGINAL_DATA_DIR = path.join(WORK_DIR, 'node-original-data');
const COPY_DATA_DIR = path.join(WORK_DIR, 'node-copy-data');
const PORT_A = Math.trunc(Number(process.env.MAZEX_TWO_COPY_PORT_A || 3900));
const PORT_B = Math.trunc(Number(process.env.MAZEX_TWO_COPY_PORT_B || 3901));
const P2P_A = Math.trunc(Number(process.env.MAZEX_TWO_COPY_P2P_A || 4900));
const P2P_B = Math.trunc(Number(process.env.MAZEX_TWO_COPY_P2P_B || 4901));
const ROUNDS = Math.max(1, Math.trunc(Number(process.env.MAZEX_TWO_COPY_ROUNDS || 20)));
const SAMPLES_PER_ROUND = Math.max(0, Math.trunc(Number(process.env.MAZEX_TWO_COPY_SAMPLES || 5)));

const EXCLUDED_TOP_LEVEL = new Set([
  '.git',
  'data',
  'node_modules',
  '.next',
  'dist',
  'coverage',
]);

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 || data.ok === false) {
    throw new Error(`${url} failed: ${data.error || data.message || data.code || response.status}`);
  }
  return data;
}

function gitShortHash() {
  try {
    return execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: ROOT, encoding: 'utf8' }).trim();
  } catch {
    return 'unknown';
  }
}

function copyProject(sourceRoot, targetRoot) {
  fs.rmSync(targetRoot, { recursive: true, force: true });
  fs.mkdirSync(targetRoot, { recursive: true });

  function copyItem(source, target, relative = '') {
    const name = path.basename(source);
    const top = relative.split(path.sep).filter(Boolean)[0] || name;
    if (EXCLUDED_TOP_LEVEL.has(top)) return;
    const stat = fs.statSync(source);
    if (stat.isDirectory()) {
      fs.mkdirSync(target, { recursive: true });
      for (const child of fs.readdirSync(source)) {
        copyItem(path.join(source, child), path.join(target, child), path.join(relative, child));
      }
      return;
    }
    if (stat.isFile()) {
      fs.mkdirSync(path.dirname(target), { recursive: true });
      fs.copyFileSync(source, target);
    }
  }

  for (const entry of fs.readdirSync(sourceRoot)) {
    copyItem(path.join(sourceRoot, entry), path.join(targetRoot, entry), entry);
  }

  const manifest = {
    type: 'mazexchain.project_copy',
    source: sourceRoot,
    copy: targetRoot,
    copied_at: new Date().toISOString(),
    git_head: gitShortHash(),
    excluded_top_level: Array.from(EXCLUDED_TOP_LEVEL),
  };
  fs.writeFileSync(path.join(targetRoot, 'COPY_MANIFEST.json'), JSON.stringify(manifest, null, 2), 'utf8');
  return manifest;
}

function startNode({ cwd, dataDir, httpPort, p2pPort, name }) {
  fs.mkdirSync(dataDir, { recursive: true });
  const child = spawn(NODE, [path.join(cwd, 'src', 'server.js')], {
    cwd,
    env: {
      ...process.env,
      PORT: String(httpPort),
      MAZEX_DATA_DIR: dataDir,
      MAZEX_AUTO_BROADCAST: '0',
      MAZEX_SYNC_INTERVAL_MS: '0',
      MAZEX_P2P_ENABLED: '1',
      MAZEX_P2P_PORT: String(p2pPort),
      MAZEX_API_RATE_LIMIT: '10000',
      MAZEX_API_POST_RATE_LIMIT: '10000',
      MAZEX_P2P_MAX_MESSAGES_PER_WINDOW: '20000',
      MAZEX_P2P_MAX_PEERS: '16',
    },
    stdio: ['ignore', 'pipe', 'pipe'],
    windowsHide: true,
  });
  child.stdout.on('data', (chunk) => process.stdout.write(`[${name}] ${chunk}`));
  child.stderr.on('data', (chunk) => process.stderr.write(`[${name} ERR] ${chunk}`));
  return child;
}

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

async function waitForSameHead({ minHeight = 0, timeoutMs = 15000 } = {}) {
  const started = Date.now();
  let last = null;
  while (Date.now() - started < timeoutMs) {
    const [a, b] = await Promise.all([
      jsonFetch(`http://localhost:${PORT_A}/api/base1/chain/summary`),
      jsonFetch(`http://localhost:${PORT_B}/api/base1/chain/summary`),
    ]);
    last = { a, b };
    if (a.head_hash === b.head_hash && a.height >= minHeight && b.height >= minHeight) {
      return { a, b, sync_ms: Date.now() - started };
    }
    await sleep(200);
  }
  throw new Error(`Nodes did not converge. Last heads: ${last?.a?.head_hash} / ${last?.b?.head_hash}`);
}

async function connectP2P() {
  const aToB = await jsonFetch(`http://localhost:${PORT_A}/api/base1/p2p/connect`, {
    method: 'POST',
    body: JSON.stringify({ peer: `tcp://127.0.0.1:${P2P_B}` }),
  });
  const bToA = await jsonFetch(`http://localhost:${PORT_B}/api/base1/p2p/connect`, {
    method: 'POST',
    body: JSON.stringify({ peer: `tcp://127.0.0.1:${P2P_A}` }),
  });
  if (!aToB.ok || !bToA.ok) throw new Error('P2P two-way connect failed');
  await sleep(1000);
  const [aStatus, bStatus] = await Promise.all([
    jsonFetch(`http://localhost:${PORT_A}/api/base1/p2p/status`),
    jsonFetch(`http://localhost:${PORT_B}/api/base1/p2p/status`),
  ]);
  if (aStatus.peer_count < 1 || bStatus.peer_count < 1) {
    throw new Error(`Expected both copies to have peers, got ${aStatus.peer_count}/${bStatus.peer_count}`);
  }
  return { aStatus, bStatus };
}

async function sampleOn(port, round) {
  if (!SAMPLES_PER_ROUND) return null;
  return jsonFetch(`http://localhost:${port}/api/base1/sample-batch`, {
    method: 'POST',
    body: JSON.stringify({
      userId: `two-copy-sampler-${round}`,
      count: SAMPLES_PER_ROUND,
    }),
  });
}

async function solveOn({ label, port, round }) {
  await sampleOn(port, round);
  const result = await jsonFetch(`http://localhost:${port}/api/base1/demo-solve`, {
    method: 'POST',
    body: JSON.stringify({ solver: `two-copy-${label}-solver-${round}` }),
  });
  if (!result.correct) throw new Error(`${label} solve failed at round ${round}: ${result.code || result.message}`);
  await jsonFetch(`http://localhost:${port}/api/base1/p2p/broadcast-head`, {
    method: 'POST',
    body: '{}',
  });
  return result;
}

async function main() {
  fs.rmSync(WORK_DIR, { recursive: true, force: true });
  fs.mkdirSync(WORK_DIR, { recursive: true });
  const manifest = copyProject(ROOT, COPY_DIR);

  const children = [
    startNode({ cwd: ROOT, dataDir: ORIGINAL_DATA_DIR, httpPort: PORT_A, p2pPort: P2P_A, name: 'original' }),
    startNode({ cwd: COPY_DIR, dataDir: COPY_DATA_DIR, httpPort: PORT_B, p2pPort: P2P_B, name: 'copy' }),
  ];

  const perRound = [];
  const started = Date.now();
  try {
    await Promise.all([
      waitForHealth(PORT_A, 'original'),
      waitForHealth(PORT_B, 'copy'),
    ]);
    await Promise.all([
      jsonFetch(`http://localhost:${PORT_A}/api/base1/reset`, { method: 'POST', body: '{}' }),
      jsonFetch(`http://localhost:${PORT_B}/api/base1/reset`, { method: 'POST', body: '{}' }),
    ]);
    await connectP2P();
    await waitForSameHead({ minHeight: 0 });

    for (let round = 1; round <= ROUNDS; round += 1) {
      const active = round % 2 === 1
        ? { label: 'original', port: PORT_A }
        : { label: 'copy', port: PORT_B };
      const before = await waitForSameHead();
      const solveStarted = Date.now();
      const solve = await solveOn({ ...active, round });
      const after = await waitForSameHead({ minHeight: before.a.height + 2, timeoutMs: 20000 });
      perRound.push({
        round,
        solver_side: active.label,
        solve_ms: Date.now() - solveStarted,
        sync_ms: after.sync_ms,
        height: after.a.height,
        work: after.a.cumulative_work_score,
        reward: solve.reward_amount,
        submit_fee: solve.submit_fee?.amount ?? 0,
        head: after.a.head_hash.slice(0, 18),
      });
    }

    const [headA, headB, p2pA, p2pB, securityA, securityB] = await Promise.all([
      jsonFetch(`http://localhost:${PORT_A}/api/base1/chain/summary`),
      jsonFetch(`http://localhost:${PORT_B}/api/base1/chain/summary`),
      jsonFetch(`http://localhost:${PORT_A}/api/base1/p2p/status`),
      jsonFetch(`http://localhost:${PORT_B}/api/base1/p2p/status`),
      jsonFetch(`http://localhost:${PORT_A}/api/base1/security/status`),
      jsonFetch(`http://localhost:${PORT_B}/api/base1/security/status`),
    ]);

    if (headA.head_hash !== headB.head_hash) throw new Error('Final heads diverged');
    const totalMs = Date.now() - started;
    const averageSyncMs = Math.round(perRound.reduce((sum, row) => sum + row.sync_ms, 0) / perRound.length);
    console.table(perRound);
    console.table([
      {
        copy: 'original',
        http: PORT_A,
        p2p: P2P_A,
        peers: p2pA.peer_count,
        height: headA.height,
        work: headA.cumulative_work_score,
        head: headA.head_hash.slice(0, 18),
        api_limit: securityA.api_rate_limit,
      },
      {
        copy: 'project-copy',
        http: PORT_B,
        p2p: P2P_B,
        peers: p2pB.peer_count,
        height: headB.height,
        work: headB.cumulative_work_score,
        head: headB.head_hash.slice(0, 18),
        api_limit: securityB.api_rate_limit,
      },
      {
        copy: 'summary',
        http: '',
        p2p: '',
        peers: '',
        height: ROUNDS,
        work: `${totalMs} ms`,
        head: `avg sync ${averageSyncMs} ms`,
        api_limit: '',
      },
    ]);
    console.log(JSON.stringify({
      ok: true,
      copy_dir: COPY_DIR,
      original_data_dir: ORIGINAL_DATA_DIR,
      copy_data_dir: COPY_DATA_DIR,
      copied_from_git_head: manifest.git_head,
      rounds: ROUNDS,
      samples_per_round: SAMPLES_PER_ROUND,
      final_height: headA.height,
      final_work: headA.cumulative_work_score,
      final_head: headA.head_hash,
      total_ms: totalMs,
      average_sync_ms: averageSyncMs,
    }, null, 2));
  } finally {
    for (const child of children) child.kill();
  }
}

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