const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { performance } = require('perf_hooks');

const ROOT = path.resolve(__dirname, '..');
const runId = new Date().toISOString().replace(/[:.]/g, '-');
const defaultDataDir = path.join(ROOT, 'data', `public-attack-sim-${runId}`);

process.env.MAZEX_DATA_DIR ||= defaultDataDir;
process.env.MAZEX_AUTO_BROADCAST ||= '0';
process.env.MAZEX_TRUST_PROXY ||= '1';
process.env.MAZEX_API_RATE_LIMIT ||= '40';
process.env.MAZEX_API_POST_RATE_LIMIT ||= '15';
process.env.MAZEX_API_CRITICAL_RATE_LIMIT ||= '8';
process.env.MAZEX_API_NETWORK_RATE_LIMIT ||= '12';
process.env.MAZEX_API_MAX_BODY_BYTES ||= '8192';
process.env.MAZEX_PEER_CANDIDATE_ERROR_COOLDOWN_THRESHOLD ||= '3';
process.env.MAZEX_PEER_CANDIDATE_ERROR_COOLDOWN_MS ||= '30000';

const {
  appendBase1ConfirmationBlock,
  getBase1Account,
  getBase1ChainSummary,
  getBase1FeeSchedule,
  getBase1NetworkStatus,
  getCurrentBase1Public,
  loadBase1State,
  makeCandidateFromOfficial,
  receiveBase1SolveCandidatePackage,
  resetBase1State,
  submitBase1Candidate,
  submitBase1Transfer,
  verifyBase1Chain,
} = require('../src/lib/base1_state');
const { BASE1_ALGORITHM_VERSION, generateBase1Step } = require('../src/lib/base1_algorithm');
const { deriveNextStepSeedSource } = require('../src/lib/chain_seed');
const { server } = require('../src/server');

const CONFIG = {
  healthFlood: Math.max(1, Math.trunc(Number(process.env.MAZEX_ATTACK_HEALTH_FLOOD || 120))),
  writeFlood: Math.max(1, Math.trunc(Number(process.env.MAZEX_ATTACK_WRITE_FLOOD || 45))),
  invalidCandidateFlood: Math.max(1, Math.trunc(Number(process.env.MAZEX_ATTACK_INVALID_CANDIDATES || 8))),
  keepData: ['1', 'true', 'yes'].includes(String(process.env.MAZEX_ATTACK_KEEP_DATA || '').toLowerCase()),
};

function assertInsideWorkspace(target) {
  const resolved = path.resolve(target);
  const workspace = path.resolve(ROOT);
  assert(resolved.startsWith(workspace + path.sep), `Refusing to touch path outside workspace: ${resolved}`);
  return resolved;
}

function elapsedMs(start) {
  return Number((performance.now() - start).toFixed(2));
}

function currentOfficialCandidate() {
  const state = loadBase1State();
  const step = state.officialStepsByKey[state.currentStepKey];
  assert(step, 'Current official Step is missing');
  return makeCandidateFromOfficial(step);
}

function latestHeadGuard() {
  return getCurrentBase1Public().latest_head_guard;
}

function solveCurrent({ solver = 'attack-solver', chargeFee = true, tag = 'attack-solve' } = {}) {
  const submitFee = getCurrentBase1Public().current.submit_cost;
  const result = submitBase1Candidate({
    solver,
    candidateStep: currentOfficialCandidate(),
    expectedHead: latestHeadGuard(),
    requireExpectedHead: true,
    chargeFee,
    submitFee,
    signedPayloadHash: `${tag}-${Date.now()}-${Math.random()}`,
  });
  assert.strictEqual(result.correct, true, `Solve failed: ${result.code || result.message}`);
  return result;
}

function wrongCandidate() {
  const candidate = currentOfficialCandidate();
  candidate.trees = JSON.parse(JSON.stringify(candidate.trees));
  candidate.trees.u = { type: 'const', value: 999999 };
  return candidate;
}

function attackPrediction(metrics) {
  const started = performance.now();
  resetBase1State();
  const stateBefore = loadBase1State();
  const previousStep = stateBefore.officialStepsByKey[stateBefore.currentStepKey];
  const previousHead = latestHeadGuard();
  const solved = solveCurrent({ solver: 'prediction-solver', chargeFee: false, tag: 'prediction' });
  const stateAfter = loadBase1State();
  const actualNext = stateAfter.officialStepsByKey[stateAfter.currentStepKey];

  const exactSeed = deriveNextStepSeedSource({
    chainId: stateAfter.chainId,
    seasonSeed: stateAfter.seasonSeed,
    algorithm: BASE1_ALGORITHM_VERSION,
    previousStep,
    previousResult: solved.solved,
    previousBlockHash: solved.solve_block.block_hash,
    nextLayerNo: actualNext.layer_no,
    nextStepNo: actualNext.step_no,
  });
  const actualBoundSeed = stateAfter.seedSourcesByKey[stateAfter.currentStepKey];
  assert.strictEqual(
    actualBoundSeed.pre_fragment_source_hash,
    exactSeed.source_hash,
    'Committed pre-fragment seed source did not match the solved Step inputs',
  );
  const exactNext = generateBase1Step({
    layerNo: actualNext.layer_no,
    stepNo: actualNext.step_no,
    seedSource: actualBoundSeed,
    fragmentProfile: actualNext.fragment_generation_profile || null,
  });
  assert.strictEqual(exactNext.formula_hash, actualNext.formula_hash, 'Exact fragment-bound committed inputs did not reproduce next Step');

  const forgedSeed = deriveNextStepSeedSource({
    chainId: stateAfter.chainId,
    seasonSeed: stateAfter.seasonSeed,
    algorithm: BASE1_ALGORITHM_VERSION,
    previousStep,
    previousResult: solved.solved,
    previousBlockHash: '0x' + 'f'.repeat(64),
    nextLayerNo: actualNext.layer_no,
    nextStepNo: actualNext.step_no,
  });
  const forgedNext = generateBase1Step({
    layerNo: actualNext.layer_no,
    stepNo: actualNext.step_no,
    seedSource: forgedSeed,
    fragmentProfile: actualNext.fragment_generation_profile || null,
  });
  assert.notStrictEqual(forgedNext.formula_hash, actualNext.formula_hash, 'Forged previous block hash predicted next Step');

  const publicState = getCurrentBase1Public();
  const serializedPublic = JSON.stringify(publicState);
  assert(!serializedPublic.includes('officialStepsByKey'), 'Public state leaked official step map');
  assert(!serializedPublic.includes('operator_units'), 'Public state leaked operator units');
  assert(!serializedPublic.includes('step_seed'), 'Public state leaked step seed');

  metrics.push({
    module: 'prediction_attack',
    status: 'pass',
    previous_height: previousHead.height,
    actual_next: `${actualNext.layer_no}:${actualNext.step_no}`,
    exact_replay_matches: true,
    forged_block_hash_rejected: true,
    public_state_private_leak: false,
    ms: elapsedMs(started),
  });
}

function attackProtocolReplay(metrics) {
  const started = performance.now();
  const solver = 'protocol-replay-solver';
  const receiver = 'protocol-replay-receiver';
  const fees = getBase1FeeSchedule();

  const oldHead = latestHeadGuard();
  const firstSolve = solveCurrent({ solver, chargeFee: false, tag: 'protocol-first' });
  appendBase1ConfirmationBlock({ producer: 'protocol-confirm-1' });
  appendBase1ConfirmationBlock({ producer: 'protocol-confirm-2' });
  appendBase1ConfirmationBlock({ producer: 'protocol-confirm-3' });

  const staleSolve = submitBase1Candidate({
    solver,
    candidateStep: currentOfficialCandidate(),
    expectedHead: oldHead,
    requireExpectedHead: true,
    chargeFee: true,
    submitFee: getCurrentBase1Public().current.submit_cost,
    signedPayloadHash: 'stale-solve-replay',
  });
  assert.strictEqual(staleSolve.correct, false, 'Stale head solve unexpectedly passed');
  assert(
    ['LATEST_HEAD_MISMATCH', 'STALE_PARENT_BLOCK'].includes(staleSolve.code),
    `Unexpected stale solve code: ${staleSolve.code}`,
  );

  const account = getBase1Account(solver);
  assert(account.confirmed_balance > fees.transfer_fee + 1, 'Solver reward did not become confirmed for replay test');
  const transfer = submitBase1Transfer({
    from: solver,
    to: receiver,
    amount: 1,
    fee: fees.transfer_fee,
    nonce: account.nonce + 1,
    expectedHead: latestHeadGuard(),
    signedPayloadHash: 'transfer-replay-once',
  });
  assert.strictEqual(transfer.ok, true, `Initial transfer failed: ${transfer.code || transfer.message}`);
  const replay = submitBase1Transfer({
    from: solver,
    to: receiver,
    amount: 1,
    fee: fees.transfer_fee,
    nonce: account.nonce + 1,
    expectedHead: latestHeadGuard(),
    signedPayloadHash: 'transfer-replay-twice',
  });
  assert.strictEqual(replay.ok, false, 'Transfer nonce replay unexpectedly passed');
  assert.strictEqual(replay.code, 'TRANSFER_NONCE_MISMATCH', `Unexpected replay code: ${replay.code}`);

  const chain = verifyBase1Chain();
  assert.strictEqual(chain.ok, true, `Chain failed after protocol replay attack: ${chain.code || chain.message}`);

  metrics.push({
    module: 'protocol_replay_attack',
    status: 'pass',
    stale_head_rejected: staleSolve.code,
    nonce_replay_rejected: replay.code,
    solve_block: firstSolve.solve_block.block_hash.slice(0, 18),
    ms: elapsedMs(started),
  });
}

function attackCandidateNetwork(metrics) {
  const started = performance.now();
  const attacker = 'http://attacker.invalid';
  let oversizedCode = null;
  let invalidCount = 0;
  for (let index = 0; index < CONFIG.invalidCandidateFlood; index += 1) {
    const result = receiveBase1SolveCandidatePackage({
      solver: 'candidate-flood-attacker',
      candidateStep: wrongCandidate(),
      expectedHead: latestHeadGuard(),
      source: attacker,
    });
    assert.strictEqual(result.ok, false, 'Invalid candidate package unexpectedly passed');
    invalidCount += 1;
  }

  const oversized = receiveBase1SolveCandidatePackage({
    solver: 'oversized-attacker',
    candidateStep: {
      ...wrongCandidate(),
      oversized_note: 'x'.repeat(300 * 1024),
    },
    expectedHead: latestHeadGuard(),
    source: attacker,
  });
  oversizedCode = oversized.code;
  assert.strictEqual(oversized.code, 'CANDIDATE_PACKAGE_TOO_LARGE', `Unexpected oversized code: ${oversized.code}`);

  const reputationValues = Object.values(getBase1NetworkStatus().reputation || {});
  const attackerRep = reputationValues.find((rep) => Number(rep.invalid_candidate_count || 0) >= invalidCount);
  assert(attackerRep, 'Attacker reputation was not recorded');
  assert.strictEqual(attackerRep.can_broadcast, false, 'Attacker was not downgraded after invalid candidate flood');

  const valid = receiveBase1SolveCandidatePackage({
    solver: 'candidate-valid-peer',
    candidateStep: currentOfficialCandidate(),
    expectedHead: latestHeadGuard(),
    source: 'http://valid-peer.invalid',
  });
  assert.strictEqual(valid.ok, true, `Valid candidate was rejected: ${valid.code || valid.message}`);
  const duplicate = receiveBase1SolveCandidatePackage({
    solver: 'candidate-valid-peer',
    candidateStep: currentOfficialCandidate(),
    expectedHead: latestHeadGuard(),
    answerHash: valid.confirmation.answer_hash,
    source: 'http://valid-peer.invalid',
  });
  assert.strictEqual(duplicate.code, 'DUPLICATE_ANSWER_HASH_DROPPED', `Duplicate answer_hash was not dropped: ${duplicate.code}`);

  metrics.push({
    module: 'candidate_network_attack',
    status: 'pass',
    invalid_candidate_flood: invalidCount,
    oversized_code: oversizedCode,
    attacker_can_broadcast: attackerRep.can_broadcast,
    duplicate_code: duplicate.code,
    ms: elapsedMs(started),
  });
}

async function request(baseUrl, pathName, options = {}) {
  const response = await fetch(`${baseUrl}${pathName}`, options);
  const text = await response.text();
  let body = {};
  try {
    body = text ? JSON.parse(text) : {};
  } catch {
    body = { raw: text };
  }
  return { response, body };
}

async function attackHttpSurface(metrics) {
  const started = performance.now();
  await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
  const { port } = server.address();
  const baseUrl = `http://127.0.0.1:${port}`;
  try {
    const healthFlood = await Promise.all(
      Array.from({ length: CONFIG.healthFlood }, () =>
        request(baseUrl, '/api/health', {
          headers: { 'X-Forwarded-For': '198.51.100.10' },
        }),
      ),
    );
    const health429 = healthFlood.filter((item) => item.response.status === 429).length;
    assert(health429 > 0, 'Health flood did not trigger rate limiting');

    const writeFlood = await Promise.all(
      Array.from({ length: CONFIG.writeFlood }, (_, index) =>
        request(baseUrl, '/api/base1/sample', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'X-Forwarded-For': '198.51.100.11',
          },
          body: JSON.stringify({ userId: 'write-flood', x: index % 20, y: (index * 3) % 20 }),
        }),
      ),
    );
    const write429 = writeFlood.filter((item) => item.response.status === 429).length;
    assert(write429 > 0, 'Write flood did not trigger write rate limiting');

    const oversized = await request(baseUrl, '/api/base1/sample', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Forwarded-For': '198.51.100.12',
      },
      body: JSON.stringify({ payload: 'x'.repeat(10 * 1024) }),
    });
    assert.strictEqual(oversized.response.status, 413, `Oversized body expected 413, got ${oversized.response.status}`);

    const badType = await request(baseUrl, '/api/base1/sample', {
      method: 'POST',
      headers: {
        'Content-Type': 'text/plain',
        'X-Forwarded-For': '198.51.100.13',
      },
      body: '{"x":1,"y":1}',
    });
    assert.strictEqual(badType.response.status, 415, `Bad content-type expected 415, got ${badType.response.status}`);

    const badOrigin = await request(baseUrl, '/api/base1/sample', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'http://evil.example',
        'X-Forwarded-For': '198.51.100.14',
      },
      body: '{"x":1,"y":1}',
    });
    assert.strictEqual(badOrigin.response.status, 403, `Bad origin expected 403, got ${badOrigin.response.status}`);

    const pollution = await request(baseUrl, '/api/base1/sample', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Forwarded-For': '198.51.100.15',
      },
      body: '{"x":1,"y":1,"__proto__":{"polluted":true}}',
    });
    assert.strictEqual(pollution.response.status, 400, `Pollution expected 400, got ${pollution.response.status}`);

    const traversal = await request(baseUrl, '/../src/server.js', {
      headers: { 'X-Forwarded-For': '198.51.100.16' },
    });
    assert([403, 404].includes(traversal.response.status), `Traversal expected 403/404, got ${traversal.response.status}`);

    const remoteWallet = await request(baseUrl, '/api/wallet/create', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Forwarded-For': '198.51.100.17',
      },
      body: '{}',
    });
    assert.strictEqual(remoteWallet.response.status, 403, `Remote wallet expected 403, got ${remoteWallet.response.status}`);

    metrics.push({
      module: 'http_public_surface_attack',
      status: 'pass',
      health_flood: CONFIG.healthFlood,
      health_429: health429,
      write_flood: CONFIG.writeFlood,
      write_429: write429,
      oversized_status: oversized.response.status,
      bad_type_status: badType.response.status,
      bad_origin_status: badOrigin.response.status,
      pollution_status: pollution.response.status,
      traversal_status: traversal.response.status,
      remote_wallet_status: remoteWallet.response.status,
      ms: elapsedMs(started),
    });
  } finally {
    await new Promise((resolve) => server.close(resolve));
  }
}

async function main() {
  const dataDir = assertInsideWorkspace(process.env.MAZEX_DATA_DIR);
  fs.rmSync(dataDir, { recursive: true, force: true });
  fs.mkdirSync(dataDir, { recursive: true });
  const metrics = [];
  const started = performance.now();
  let report;
  try {
    attackPrediction(metrics);
    attackProtocolReplay(metrics);
    attackCandidateNetwork(metrics);
    await attackHttpSurface(metrics);
    report = {
      ok: true,
      run_id: runId,
      note: 'Local authorized simulation of public-network attack patterns; no external host was targeted.',
      config: CONFIG,
      data_dir: dataDir,
      total_ms: elapsedMs(started),
      chain: getBase1ChainSummary(),
      metrics,
    };
    console.log(JSON.stringify(report, null, 2));
    console.table(metrics);
  } catch (error) {
    report = {
      ok: false,
      run_id: runId,
      config: CONFIG,
      data_dir: dataDir,
      total_ms: elapsedMs(started),
      error: {
        message: error.message,
        stack: error.stack,
      },
      metrics,
    };
    console.error(JSON.stringify(report, null, 2));
    process.exitCode = 1;
  } finally {
    if (CONFIG.keepData) {
      fs.writeFileSync(path.join(dataDir, 'public_attack_sim_result.json'), JSON.stringify(report, null, 2), 'utf8');
    } else if (fs.existsSync(dataDir)) {
      fs.rmSync(dataDir, { recursive: true, force: true });
    }
  }
}

main();
