// -----------------------------------------------------------------------------
// Minimal, correct Ethereum hexary Merkle-Patricia trie BUILDER + proof generator,
// plus EIP-2718 receipt encoding, for the message-inbox tests.
//
// This is an INDEPENDENT implementation from the Solidity MerklePatriciaProof
// verifier: the builder here constructs the trie and the on-chain verifier walks it.
// A passing test means the two agree, which is a real cross-check on both.
//
// Receipt values are always > 32 bytes (a receipt carries a 256-byte logsBloom), so
// every trie node is > 32 bytes and referenced by hash — no inline-node case arises.
// -----------------------------------------------------------------------------
const { RLP } = require("@ethereumjs/rlp");
const { keccak256 } = require("ethers");

const toBuf = (x) => {
  if (Buffer.isBuffer(x)) return x;
  if (typeof x === "string" && x.startsWith("0x")) return Buffer.from(x.slice(2), "hex");
  return Buffer.from(x);
};
function keccakBuf(b) {
  return Buffer.from(keccak256("0x" + toBuf(b).toString("hex")).slice(2), "hex");
}
function rlpEncode(x) {
  return Buffer.from(RLP.encode(x));
}
function rlpDecode(b) {
  return RLP.decode(Uint8Array.from(b));
}

function bytesToNibbles(b) {
  const out = [];
  for (const x of b) {
    out.push(x >> 4);
    out.push(x & 0x0f);
  }
  return out;
}
function hpEncode(nibbles, isLeaf) {
  const odd = nibbles.length % 2 === 1;
  const flags = (isLeaf ? 2 : 0) | (odd ? 1 : 0);
  const bytes = [];
  let start = 0;
  if (odd) {
    bytes.push((flags << 4) | nibbles[0]);
    start = 1;
  } else {
    bytes.push(flags << 4);
  }
  for (let i = start; i < nibbles.length; i += 2) {
    bytes.push((nibbles[i] << 4) | nibbles[i + 1]);
  }
  return Buffer.from(bytes);
}

// Build a trie over [{key: Buffer, value: Buffer}]. Returns { root, nodes } where
// nodes maps hex(hash)->rlp Buffer.
function buildTrie(entries) {
  const nodes = {};
  function store(items) {
    const rlp = rlpEncode(items);
    const h = keccakBuf(rlp);
    nodes[h.toString("hex")] = rlp;
    return h; // referenced by 32-byte hash (all our nodes are > 32 bytes)
  }
  function commonPrefix(pairs) {
    let cp = pairs[0].nibbles.slice();
    for (const p of pairs) {
      let i = 0;
      while (i < cp.length && i < p.nibbles.length && cp[i] === p.nibbles[i]) i++;
      cp = cp.slice(0, i);
    }
    return cp;
  }
  function build(pairs) {
    if (pairs.length === 1) {
      return store([hpEncode(pairs[0].nibbles, true), pairs[0].value]);
    }
    const cp = commonPrefix(pairs);
    if (cp.length > 0) {
      const stripped = pairs.map((p) => ({ nibbles: p.nibbles.slice(cp.length), value: p.value }));
      const child = build(stripped);
      return store([hpEncode(cp, false), child]);
    }
    // branch
    const slots = new Array(16).fill(Buffer.alloc(0));
    let branchValue = Buffer.alloc(0);
    const groups = {};
    for (const p of pairs) {
      if (p.nibbles.length === 0) {
        branchValue = p.value;
      } else {
        const nib = p.nibbles[0];
        (groups[nib] = groups[nib] || []).push({ nibbles: p.nibbles.slice(1), value: p.value });
      }
    }
    for (let nib = 0; nib < 16; nib++) {
      if (groups[nib]) slots[nib] = build(groups[nib]);
    }
    return store([...slots, branchValue]);
  }
  const pairs = entries.map((e) => ({ nibbles: bytesToNibbles(toBuf(e.key)), value: toBuf(e.value) }));
  const root = build(pairs);
  return { root, nodes };
}

// Collect the proof node set (rlp Buffers) along `key` from `root`.
function getProof(root, nodes, key) {
  const path = bytesToNibbles(toBuf(key));
  const out = [];
  let cur = root;
  let idx = 0;
  for (let hop = 0; hop < 512; hop++) {
    const rlp = nodes[toBuf(cur).toString("hex")];
    if (!rlp) break;
    out.push(rlp);
    const items = rlpDecode(rlp).map((x) => Buffer.from(x));
    if (items.length === 2) {
      const enc = items[0];
      const isLeaf = (enc[0] >> 4) & 0x2;
      const odd = (enc[0] >> 4) & 0x1;
      let fragLen = (enc.length - 1) * 2 + (odd ? 1 : 0);
      if (isLeaf) break;
      idx += fragLen;
      cur = items[1];
    } else {
      if (idx >= path.length) break;
      const nib = path[idx];
      idx += 1;
      const child = items[nib];
      if (!child || child.length === 0) break;
      cur = child;
    }
  }
  return out;
}

// rlp(transactionIndex) — the receipts-trie key.
function txIndexKey(i) {
  if (i === 0) return Buffer.from([0x80]);
  return rlpEncode(i);
}

// Build an EIP-2718 type-2 receipt value: 0x02 || rlp([status, cumGas, bloom, logs]).
// log = [address(20), [topics...], data].
function encodeReceipt(type, { status, cumulativeGas, logs }) {
  const bloom = Buffer.alloc(256, 0);
  const rlpLogs = logs.map((l) => [toBuf(l.address), l.topics.map(toBuf), toBuf(l.data)]);
  const body = rlpEncode([status, cumulativeGas, bloom, rlpLogs]);
  if (type === 0) return body; // legacy: value is the rlp list directly
  return Buffer.concat([Buffer.from([type]), body]); // typed
}

module.exports = { buildTrie, getProof, txIndexKey, encodeReceipt, keccakBuf, rlpEncode, bytesToNibbles };
