// shutter-crypto.js
//
// REAL threshold-BLS (BLS12-381) helpers for AereShutterMempoolV2, the
// application-level Shutter-style anti-MEV encrypted mempool PoC on AERE
// chain 2800. Uses @noble/curves (already in the repo node_modules).
//
// Scheme (Boldyreva threshold BLS + hashed-ElGamal encryption):
//   - Committee master secret s, Shamir-shared (t,N) over the scalar field Fr.
//   - Group public key  PK  = s * g2            (G2, the "encryption pubkey").
//   - Keyper pubkeys     P_i = s_i * g2          (G2, published VSS commitments).
//   - Epoch identity     H1  = hashToCurve(epoch tag) in G1.
//   - Epoch decrypt key  DK  = s * H1            (G1, threshold signature).
//   - Keyper share       sig_i = s_i * H1        (G1).
//   - DK reconstruction  DK = sum_{i in S} lambda_i * sig_i   (Lagrange at 0).
//
// On-chain the pairing precompile (EIP-2537, addr 0x0f, VERIFIED live on 2800)
// checks:
//   share valid : e(sig_i, g2) == e(H1, P_i)   <=> e(sig_i,g2)*e(-H1,P_i)==1
//   DK    valid : e(DK,   g2) == e(H1, PK)     <=> e(DK,  g2)*e(-H1,PK )==1
//
// Encryption (hashed-ElGamal to the epoch), decryptable only with DK:
//   gt = e(H1, PK) in GT ; pick random r ; U = r*g2 ;
//   mask = keccak(gt^r) ; C = plaintext XOR mask ; ciphertext = (U, C).
//   Decrypt: gt^r = e(DK, U) ; mask = keccak(e(DK,U)) ; plaintext = C XOR mask.
//   (The GT->bytes masking is off-chain; on-chain we bind the plaintext to a
//    keccak commitment. DK correctness itself is pairing-verified on-chain.)

const { bls12_381 } = require("@noble/curves/bls12-381");
const { keccak_256 } = require("@noble/hashes/sha3");
const { randomBytes } = require("crypto");

const G1 = bls12_381.G1.ProjectivePoint;
const G2 = bls12_381.G2.ProjectivePoint;
const Fr = bls12_381.fields.Fr;
const Fp2 = bls12_381.fields.Fp2;
const Fp12 = bls12_381.fields.Fp12;
const R = Fr.ORDER;
const DST = "AERE-SHUTTER-V2-BLS12381G1-XMD:KECCAK-256_SSWU_RO_"; // documented epoch-identity DST

/* ----------------------------- serialization ----------------------------- */
function fpTo64Hex(x) {
  // 48-byte big-endian field element, left-padded to 64 bytes (EIP-2537).
  const h = x.toString(16).padStart(96, "0");
  return "00".repeat(16) + h;
}
function g1Hex(P) {
  const a = P.toAffine();
  return "0x" + fpTo64Hex(a.x) + fpTo64Hex(a.y); // 128 bytes
}
function g2Hex(P) {
  const a = P.toAffine();
  // EIP-2537 Fp2 ordering: (c0, c1) for x then y.
  return (
    "0x" +
    fpTo64Hex(a.x.c0) + fpTo64Hex(a.x.c1) +
    fpTo64Hex(a.y.c0) + fpTo64Hex(a.y.c1)
  ); // 256 bytes
}
// EIP-2537 -> noble point parsers (64-byte fields, last 48 bytes are the value)
function chunkVal(chunk) { return BigInt("0x" + chunk.slice(32)); }
function eip2537ToG1(h) {
  h = h.startsWith("0x") ? h.slice(2) : h;
  return G1.fromAffine({ x: chunkVal(h.slice(0, 128)), y: chunkVal(h.slice(128, 256)) });
}
function eip2537ToG2(h) {
  h = h.startsWith("0x") ? h.slice(2) : h;
  const xc0 = chunkVal(h.slice(0, 128)), xc1 = chunkVal(h.slice(128, 256));
  const yc0 = chunkVal(h.slice(256, 384)), yc1 = chunkVal(h.slice(384, 512));
  return G2.fromAffine({ x: Fp2.fromBigTuple([xc0, xc1]), y: Fp2.fromBigTuple([yc0, yc1]) });
}
function randScalar() {
  // uniform-ish nonzero scalar in [1, R-1]
  let x = 0n;
  while (x === 0n) x = BigInt("0x" + randomBytes(48).toString("hex")) % R;
  return x;
}

/* --------------------------- polynomial / shamir -------------------------- */
function evalPoly(coeffs, x) {
  // Horner in Fr
  let acc = 0n;
  for (let i = coeffs.length - 1; i >= 0; i--) acc = Fr.add(Fr.mul(acc, x), coeffs[i]);
  return acc;
}
// Lagrange coefficient lambda_i for interpolation AT 0 over index set S (1-based x = index).
function lagrangeAtZero(indices, i) {
  let num = 1n, den = 1n;
  const xi = BigInt(i);
  for (const j of indices) {
    if (j === i) continue;
    const xj = BigInt(j);
    num = Fr.mul(num, Fr.sub(0n, xj)); // (0 - xj)
    den = Fr.mul(den, Fr.sub(xi, xj)); // (xi - xj)
  }
  return Fr.mul(num, Fr.inv(den));
}

/* ------------------------------ committee setup --------------------------- */
// Trusted-dealer Shamir setup for the PoC. Returns committee material.
function setupCommittee(t, N) {
  const coeffs = [randScalar()]; // a0 = master secret s
  for (let k = 1; k < t; k++) coeffs.push(randScalar());
  const s = coeffs[0];
  const shares = []; // {index, secret}
  const pubkeys = []; // P_i hex (G2), index-aligned to keypers[i] (i=0..N-1 -> x=i+1)
  for (let i = 1; i <= N; i++) {
    const si = evalPoly(coeffs, BigInt(i));
    shares.push({ index: i, secret: si });
    pubkeys.push(g2Hex(G2.BASE.multiply(si)));
  }
  const PKpoint = G2.BASE.multiply(s);
  const PK = g2Hex(PKpoint); // group pubkey (encryption key)
  return { t, N, s, shares, pubkeys, PK, PKpoint, g2Gen: g2Hex(G2.BASE) };
}

/* ------------------------------- epoch identity --------------------------- */
function epochIdentity(epochId) {
  const tag = Buffer.from(`AERE-SHUTTER-EPOCH:${epochId}`, "utf8");
  const H1 = bls12_381.G1.hashToCurve(tag, { DST }); // RFC9380 hash-to-curve, cofactor-cleared
  const P = G1.fromHex(H1.toHex(true)); // normalize to ProjectivePoint
  return { H1: g1Hex(P), H1neg: g1Hex(P.negate()), point: P };
}

/* ------------------------------- shares / DK ------------------------------ */
function makeShare(secret_si, epochPoint) {
  return g1Hex(epochPoint.multiply(secret_si)); // sig_i = s_i * H1  (G1)
}
function reconstructDK(epochPoint, committee, subsetIndices) {
  // subsetIndices are 1-based keyper x-coords (length >= t)
  let acc = G1.ZERO;
  for (const i of subsetIndices) {
    const share = committee.shares.find((s) => s.index === i);
    const lam = lagrangeAtZero(subsetIndices, i);
    const sig_i = epochPoint.multiply(share.secret); // = s_i*H1
    acc = acc.add(sig_i.multiply(lam));
  }
  return { DK: g1Hex(acc), point: acc }; // should equal s*H1
}

/* ------------------------------- encryption ------------------------------- */
function fp12Bytes(z) {
  // deterministic flatten of an Fp12 element to bytes (12 * 48-byte coords).
  const flat = [
    z.c0.c0.c0, z.c0.c0.c1, z.c0.c1.c0, z.c0.c1.c1, z.c0.c2.c0, z.c0.c2.c1,
    z.c1.c0.c0, z.c1.c0.c1, z.c1.c1.c0, z.c1.c1.c1, z.c1.c2.c0, z.c1.c2.c1,
  ];
  return Buffer.concat(flat.map((x) => Buffer.from(x.toString(16).padStart(96, "0"), "hex")));
}
function maskFromGT(gt, len) {
  // expand keccak(gt-bytes || counter) to len bytes
  const base = fp12Bytes(gt);
  const out = Buffer.alloc(len);
  let off = 0, ctr = 0;
  while (off < len) {
    const blk = Buffer.from(keccak_256(Buffer.concat([base, Buffer.from([ctr & 0xff])])));
    const n = Math.min(32, len - off);
    blk.copy(out, off, 0, n);
    off += n; ctr++;
  }
  return out;
}
// Encrypt plaintext (Buffer) to the epoch. Returns ciphertext bytes = U(256) || C(len).
function encrypt(committee, epochId, plaintext) {
  const { point: H1 } = epochIdentity(epochId);
  const PKpt = committee.PKpoint || eip2537ToG2(committee.PK);
  const r = randScalar();
  const U = G2.BASE.multiply(r); // ephemeral
  const gt = bls12_381.pairing(H1, PKpt); // e(H1, PK)
  const gtr = Fp12.pow(gt, r); // gt^r
  const mask = maskFromGT(gtr, plaintext.length);
  const C = Buffer.from(plaintext.map((b, i) => b ^ mask[i]));
  const Ubuf = Buffer.from(g2Hex(U).slice(2), "hex");
  return { ciphertext: "0x" + Buffer.concat([Ubuf, C]).toString("hex"), U: g2Hex(U) };
}
// Decrypt with reconstructed DK point. ciphertext hex -> plaintext Buffer.
function decrypt(dkPoint, ciphertextHex) {
  const buf = Buffer.from(ciphertextHex.slice(2), "hex");
  const Ubuf = buf.subarray(0, 256);
  const C = buf.subarray(256);
  const U = eip2537ToG2(Ubuf.toString("hex"));
  const gtr = bls12_381.pairing(dkPoint, U); // e(DK, U) == gt^r
  const mask = maskFromGT(gtr, C.length);
  return Buffer.from(C.map((b, i) => b ^ mask[i]));
}

/* ----------------------------- commit / merkle ---------------------------- */
const { AbiCoder, keccak256, solidityPacked } = require("ethers");
const abi = AbiCoder.defaultAbiCoder();

function commitmentOf(plaintextHex, openingHex) {
  return keccak256(abi.encode(["bytes", "bytes32"], [plaintextHex, openingHex]));
}
// Merkle over ordered positions. leaf = keccak(bytes.concat(keccak(abi.encode(epochId,pos,commitment)))).
function leafOf(epochId, pos, commitment) {
  const inner = keccak256(abi.encode(["uint64", "uint256", "bytes32"], [epochId, pos, commitment]));
  return keccak256(solidityPacked(["bytes32"], [inner]));
}
function hashPair(a, b) {
  const [x, y] = a.toLowerCase() <= b.toLowerCase() ? [a, b] : [b, a]; // OZ sorted pairs
  return keccak256(solidityPacked(["bytes32", "bytes32"], [x, y]));
}
// Build an OZ-compatible sorted-pair Merkle tree; returns {root, proofs[]}.
function buildMerkle(leaves) {
  if (leaves.length === 1) return { root: leaves[0], proofs: [[]] };
  let layer = leaves.slice();
  const layers = [layer];
  while (layer.length > 1) {
    const next = [];
    for (let i = 0; i < layer.length; i += 2) {
      if (i + 1 === layer.length) next.push(layer[i]);
      else next.push(hashPair(layer[i], layer[i + 1]));
    }
    layer = next;
    layers.push(layer);
  }
  const root = layers[layers.length - 1][0];
  const proofs = leaves.map((_, idx) => {
    const proof = [];
    let index = idx;
    for (let l = 0; l < layers.length - 1; l++) {
      const cur = layers[l];
      const pair = index ^ 1;
      if (pair < cur.length) proof.push(cur[pair]);
      index = Math.floor(index / 2);
    }
    return proof;
  });
  return { root, proofs };
}

module.exports = {
  G1, G2, Fr, DST,
  fpTo64Hex, g1Hex, g2Hex, randScalar, eip2537ToG1, eip2537ToG2,
  setupCommittee, epochIdentity, makeShare, reconstructDK,
  encrypt, decrypt, commitmentOf, leafOf, buildMerkle,
};
