// -----------------------------------------------------------------------------
// Test helpers for the Ethereum-interop light client.
//
// These build a SYNTHETIC (but structurally spec-correct) Altair light-client
// update using REAL BLS12-381 cryptography from @noble/curves. The BLS signatures
// are genuine; hash-to-curve, aggregation and the pairing check are verified on
// chain against AERE's LIVE EIP-2537 precompiles (Prague). The SSZ roots and Merkle
// branches are computed here with the SAME algorithm the Solidity contract uses, so
// a passing test means the two independent implementations agree byte-for-byte.
//
// HONEST LABEL: this is not a captured Ethereum-mainnet update. The DST used is
// noble's default RFC-9380 "_NUL_" tag (mainnet sync uses "_POP_"); the genesis
// validators root, fork version and headers are chosen by the test. It exercises the
// exact on-chain verification pipeline; it does not assert mainnet byte-compatibility.
// -----------------------------------------------------------------------------
const { bls12_381: bls } = require("@noble/curves/bls12-381");
const { sha256 } = require("@noble/hashes/sha256");

const DST = Buffer.from("BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_", "ascii");
const COMMITTEE_SIZE = 512;
const ZERO32 = Buffer.alloc(32);

// ---- byte utils -------------------------------------------------------------
const hex = (b) => "0x" + Buffer.from(b).toString("hex");
const cat = (...a) => Buffer.concat(a.map((x) => Buffer.from(x)));
function shaPair(a, b) {
  return Buffer.from(sha256(cat(a, b)));
}

// ---- EIP-2537 point encodings ----------------------------------------------
const PAD16 = Buffer.alloc(16);
function f48(v) {
  return Buffer.from(v.toString(16).padStart(96, "0"), "hex");
}
function encG1(P) {
  P = P.toAffine();
  return cat(PAD16, f48(P.x), PAD16, f48(P.y)); // 128 bytes
}
function encG2(P) {
  P = P.toAffine();
  return cat(PAD16, f48(P.x.c0), PAD16, f48(P.x.c1), PAD16, f48(P.y.c0), PAD16, f48(P.y.c1)); // 256
}

// ---- SSZ ---------------------------------------------------------------------
function uint64Chunk(v) {
  const b = Buffer.alloc(32);
  let x = BigInt(v);
  for (let i = 0; i < 8; i++) {
    b[i] = Number(x & 0xffn);
    x >>= 8n;
  }
  return b;
}
function beaconHeaderRoot(h) {
  const l01 = shaPair(uint64Chunk(h.slot), uint64Chunk(h.proposerIndex));
  const l23 = shaPair(h.parentRoot, h.stateRoot);
  const l45 = shaPair(h.bodyRoot, ZERO32);
  const l67 = shaPair(ZERO32, ZERO32);
  const a = shaPair(l01, l23);
  const b = shaPair(l45, l67);
  return shaPair(a, b);
}
function merkleize(leaves) {
  let n = leaves.length;
  let layer = leaves.slice();
  while (n > 1) {
    const next = [];
    for (let i = 0; i < n; i += 2) next.push(shaPair(layer[i], layer[i + 1]));
    layer = next;
    n = next.length;
  }
  return layer[0];
}
function pubkeyLeaf(compressed48) {
  const c0 = compressed48.slice(0, 32);
  const c1 = cat(compressed48.slice(32, 48), Buffer.alloc(16));
  return shaPair(c0, c1);
}
function syncCommitteeRoot(compressedPubkeys, aggCompressed) {
  const leaves = compressedPubkeys.map(pubkeyLeaf);
  const pubkeysRoot = merkleize(leaves);
  return shaPair(pubkeysRoot, pubkeyLeaf(aggCompressed));
}
function computeDomain(domainType4, forkVersion4, gvr) {
  const forkVersionChunk = cat(forkVersion4, Buffer.alloc(28));
  const forkDataRoot = shaPair(forkVersionChunk, gvr);
  return cat(domainType4, forkDataRoot.slice(0, 28)); // 32 bytes
}

// ---- sparse merkle tree with gindex overrides (for state-root branches) -----
// Builds a depth-`depth` binary tree; `assign` maps generalized-index -> 32-byte
// value (overriding computed hashes). Returns { root, branch(gindex) }.
function buildTree(depth, assign) {
  const size = 1 << (depth + 1);
  const val = new Array(size).fill(null);
  const leafStart = 1 << depth;
  for (let g = leafStart; g < size; g++) val[g] = ZERO32;
  for (const [g, v] of Object.entries(assign)) val[Number(g)] = Buffer.from(v);
  for (let g = leafStart - 1; g >= 1; g--) {
    if (assign[g] !== undefined) continue; // keep override
    val[g] = shaPair(val[2 * g], val[2 * g + 1]);
  }
  function branch(gindex) {
    const out = [];
    let g = gindex;
    while (g > 1) {
      out.push(val[g ^ 1]); // sibling
      g = g >> 1;
    }
    return out;
  }
  return { root: val[1], val, branch };
}

// ---- committee generation ----------------------------------------------------
// Deterministic committee of 512 real BLS keypairs from a seed.
function makeCommittee(seedText) {
  const sks = [];
  const pointsG1 = [];
  const compressed = [];
  const uncompressed = [];
  const R = bls.fields.Fr.ORDER;
  let seed = Buffer.from(sha256(Buffer.from(seedText, "ascii")));
  for (let i = 0; i < COMMITTEE_SIZE; i++) {
    seed = Buffer.from(sha256(seed));
    // reduce to a valid scalar in [1, r): sk = (seed mod (r-1)) + 1
    const scalar = (BigInt("0x" + seed.toString("hex")) % (R - 1n)) + 1n;
    const sk = Buffer.from(scalar.toString(16).padStart(64, "0"), "hex");
    const pk = bls.getPublicKey(sk); // compressed 48
    const P = bls.G1.ProjectivePoint.fromHex(Buffer.from(pk).toString("hex"));
    sks.push(sk);
    pointsG1.push(P);
    compressed.push(Buffer.from(pk));
    uncompressed.push(encG1(P));
  }
  // aggregate pubkey of the whole committee
  let agg = bls.G1.ProjectivePoint.ZERO;
  for (const P of pointsG1) agg = agg.add(P);
  const aggCompressed = Buffer.from(agg.toRawBytes(true)); // 48
  const aggUncompressed = encG1(agg);
  const root = syncCommitteeRoot(compressed, aggCompressed);
  return { sks, pointsG1, compressed, uncompressed, aggCompressed, aggUncompressed, root };
}

// Sign a 32-byte message with a participant subset; returns aggregate sig (G2 point)
// and the participation bitfield (64-byte SSZ Bitvector[512]).
function signSubset(committee, message, participantIndices) {
  const bits = Buffer.alloc(COMMITTEE_SIZE / 8);
  let aggSig = bls.G2.ProjectivePoint.ZERO;
  for (const i of participantIndices) {
    bits[i >> 3] |= 1 << (i & 7);
    const sig = bls.sign(message, committee.sks[i]); // compressed G2
    const S = bls.G2.ProjectivePoint.fromHex(Buffer.from(sig).toString("hex"));
    aggSig = aggSig.add(S);
  }
  return { bits, aggSigUncompressed: encG2(aggSig) };
}

module.exports = {
  bls,
  DST,
  COMMITTEE_SIZE,
  ZERO32,
  hex,
  cat,
  shaPair,
  encG1,
  encG2,
  uint64Chunk,
  beaconHeaderRoot,
  merkleize,
  pubkeyLeaf,
  syncCommitteeRoot,
  computeDomain,
  buildTree,
  makeCommittee,
  signSubset,
};
