/**
 * Relay-side block validation mirroring Android BlockValidator + ProducerValidator.
 * Rejects forged / malformed tips before relay_store.storeBlock.
 *
 * Soft account rules (invalid transfer skipped, block still applied) match phones —
 * those are NOT a reject gate here, or relay tip would diverge from honest nodes.
 */
const path = require("path");
const crypto = require("crypto");

const BLOCK_VERSION_MIN = 1;
const BLOCK_VERSION = 2;
const BLOCK_TIME_SECONDS = 33;
const PROPAGATION_TOLERANCE_SECONDS = 3;
const BLOCK_SIZE_START_BYTES = 333 * 1024;
const BLOCK_SIZE_INCREMENT_BYTES = 333 * 1024;
const BLOCK_SIZE_INTERVAL = 3_333_333;
const MAX_TRANSACTIONS_PER_BLOCK = 200;

let walletCrypto = null;

function nodeSha3_256(bytes) {
  return new Uint8Array(crypto.createHash("sha3-256").update(Buffer.from(bytes)).digest());
}

function loadWalletCrypto() {
  if (walletCrypto) return walletCrypto;
  const candidates = [
    path.join(__dirname, "..", "coordinator_exe", "exe_wallet.js"),
    path.join(__dirname, "exe_wallet.js"),
  ];
  for (const p of candidates) {
    try {
      const mod = require(p);
      walletCrypto = {
        ...mod,
        sha3_256: typeof mod.sha3_256 === "function" ? mod.sha3_256 : nodeSha3_256,
      };
      return walletCrypto;
    } catch (_) {}
  }
  // Fallback: local noble packages (npm install in vps_scheduler).
  try {
    const { sha3_256 } = require("@noble/hashes/sha3");
    const { ml_dsa87 } = require("@noble/post-quantum/ml-dsa");
    walletCrypto = {
      sha3_256,
      verify: (data, signature, publicKey) => ml_dsa87.verify(publicKey, data, signature),
      publicKeyToAddress(publicKey) {
        const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
        const hash = sha3_256(publicKey);
        if (!hash.length) return "SRMD";
        const bytes = Uint8Array.from(hash);
        let count = 0;
        for (const b of bytes) {
          if (b === 0) count++;
          else break;
        }
        const temp = new Uint8Array(bytes.length * 2);
        let j = temp.length;
        let startAt = count;
        while (startAt < bytes.length) {
          let carry = bytes[startAt];
          let i = 0;
          let k = temp.length - 1;
          while (k >= 0 && (carry !== 0 || i < j - (temp.length - bytes.length))) {
            carry += 256 * temp[k];
            temp[k] = carry % 58;
            carry = (carry / 58) | 0;
            k--;
            i++;
          }
          startAt++;
        }
        let result = "";
        for (const b of temp) {
          if (b !== 0 || result.length) result += ALPHABET[b];
        }
        for (let i = 0; i < count; i++) result = ALPHABET[0] + result;
        return "SRMD" + result;
      },
      hexToBytes(hex) {
        const clean = String(hex || "").replace(/\s+/g, "");
        if (!clean.length || clean.length % 2 !== 0) return new Uint8Array(0);
        return Uint8Array.from(clean.match(/.{1,2}/g).map((h) => parseInt(h, 16)));
      },
      bytesToHex(bytes) {
        return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
      },
    };
    return walletCrypto;
  } catch (e) {
    throw new Error(
      "block_validator: wallet crypto unavailable (need coordinator_exe/exe_wallet.js or @noble deps): " +
        (e && e.message)
    );
  }
}

function sha3Hex(bytes) {
  const wc = loadWalletCrypto();
  return wc.bytesToHex(wc.sha3_256(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)));
}

function hexToBytes(hex) {
  return loadWalletCrypto().hexToBytes(hex);
}

function bytesToHex(bytes) {
  return loadWalletCrypto().bytesToHex(bytes);
}

function blockSizeLimit(height) {
  const increments = Math.floor((Number(height) || 0) / BLOCK_SIZE_INTERVAL);
  return BLOCK_SIZE_START_BYTES + increments * BLOCK_SIZE_INCREMENT_BYTES;
}

function payloadHexFromTx(tx) {
  if (!tx) return "";
  if (tx.payloadHex != null && String(tx.payloadHex).length) return String(tx.payloadHex);
  if (tx.payload instanceof Uint8Array && tx.payload.length) return bytesToHex(tx.payload);
  if (typeof tx.payload === "string" && tx.payload.length) {
    // Some wire forms send utf8 opcode; Android signs hex of UTF-8 bytes via ContractPayload.
    return bytesToHex(Buffer.from(tx.payload, "utf8"));
  }
  return "";
}

function txBytesForSigning(tx) {
  const payloadHex = payloadHexFromTx(tx);
  return Buffer.from(
    `${tx.fromAddress}|${tx.toAddress}|${tx.amount}|${tx.gasFee}|${tx.timestamp}|${payloadHex}`,
    "utf8"
  );
}

function txHash(tx) {
  return sha3Hex(txBytesForSigning(tx));
}

function txSizeBytes(tx) {
  const payloadLen =
    tx.payload instanceof Uint8Array
      ? tx.payload.length
      : tx.payloadHex
        ? String(tx.payloadHex).length / 2
        : 0;
  return (
    txBytesForSigning(tx).length +
    String(tx.signature || "").length +
    String(tx.signerPublicKey || "").length +
    payloadLen
  );
}

function headerBytesForHash(h) {
  return Buffer.from(
    `${h.version}|${h.previousBlockHash}|${h.networkTime}|${h.timestamp}|${h.merkleRoot}|${h.difficultyTarget}|${h.nonce}|${h.minerPublicKey}|${h.transactionCount}|${h.founderAddress || ""}`,
    "utf8"
  );
}

function blockHash(header) {
  return sha3Hex(headerBytesForHash(header));
}

function blockSizeBytes(header, transactions) {
  return headerBytesForHash(header).length + (transactions || []).reduce((s, t) => s + txSizeBytes(t), 0);
}

function merkleRoot(txHashes) {
  const wc = loadWalletCrypto();
  if (!txHashes.length) return sha3Hex(new Uint8Array(0));
  let hashes = txHashes.map((h) => wc.sha3_256(hexToBytes(h)));
  while (hashes.length > 1) {
    if (hashes.length % 2 !== 0) hashes = hashes.concat([hashes[hashes.length - 1]]);
    const next = [];
    for (let i = 0; i < hashes.length; i += 2) {
      const a = Buffer.from(hashes[i]);
      const b = Buffer.from(hashes[i + 1]);
      next.push(wc.sha3_256(Buffer.concat([a, b])));
    }
    hashes = next;
  }
  return bytesToHex(hashes[0]);
}

function ok() {
  return { valid: true };
}

function fail(message) {
  return { valid: false, message: String(message || "invalid block") };
}

/**
 * Mirror NetworkClock.validateBlockTimestamp + fixed 33s block time.
 * @param {object} header
 * @param {{ timestamp: number, hash?: string }|null} previousTip
 */
function validateTimestamp(header, previousTip) {
  const ts = Number(header.timestamp);
  if (!Number.isFinite(ts)) return fail("Invalid block timestamp");
  if (!previousTip || previousTip.height === 0 || previousTip.timestamp == null) {
    // First stored block after genesis: only require positive / finite ts (genesis itself not stored).
    if (ts <= 0) return fail("Invalid genesis-following timestamp");
    return ok();
  }
  const prevTs = Number(previousTip.timestamp);
  if (ts <= prevTs) return fail("Invalid block timestamp (not after previous)");
  const elapsed = ts - prevTs;
  const expected = BLOCK_TIME_SECONDS;
  const maxAllowed = expected + PROPAGATION_TOLERANCE_SECONDS;
  if (elapsed > maxAllowed) {
    return fail(`Invalid block timestamp (elapsed ${elapsed} > max ${maxAllowed})`);
  }
  if (elapsed < expected) {
    return fail(`Invalid block timestamp (elapsed ${elapsed} < expected ${expected})`);
  }
  return ok();
}

/**
 * @param {object} blockBody  { header, transactions, hash? }
 * @param {{ height: number, hash: string, timestamp: number }} tip  current relay tip
 * @param {number} height  proposed height
 */
function validateBlockStructure(blockBody, tip, height) {
  if (!blockBody || !blockBody.header) return fail("invalid block");
  const header = blockBody.header;
  const transactions = Array.isArray(blockBody.transactions) ? blockBody.transactions : [];

  if (Number(header.version) < BLOCK_VERSION_MIN || Number(header.version) > BLOCK_VERSION) return fail("Invalid block version");
  if (String(header.difficultyTarget || "").length !== 64) return fail("Invalid difficulty target format");
  if (String(header.merkleRoot || "").length !== 64) return fail("Invalid merkle root format");

  if (transactions.length !== Number(header.transactionCount)) {
    return fail("Transaction count mismatch");
  }
  if (transactions.length > MAX_TRANSACTIONS_PER_BLOCK) {
    return fail(`Too many transactions (${transactions.length})`);
  }

  const size = blockSizeBytes(header, transactions);
  const limit = blockSizeLimit(height);
  if (size > limit) return fail(`Block size ${size} exceeds limit ${limit}`);

  const tsCheck = validateTimestamp(header, tip && tip.height > 0 ? tip : null);
  if (!tsCheck.valid) return tsCheck;

  if (tip && tip.height > 0 && String(header.previousBlockHash) !== String(tip.hash)) {
    return fail("previous hash does not match tip");
  }

  const hashes = transactions.map((tx) => txHash(tx));
  const root = merkleRoot(hashes);
  if (root !== String(header.merkleRoot)) return fail("Merkle root mismatch");

  const computedHash = blockHash(header);
  const claimed = String(blockBody.hash || "").trim();
  if (claimed && claimed !== computedHash) return fail("Block hash mismatch");

  // Per-tx ML-DSA-87 + address binding (BlockValidator)
  const wc = loadWalletCrypto();
  for (let i = 0; i < transactions.length; i++) {
    const tx = transactions[i];
    const pkHex = String(tx.signerPublicKey || "");
    if (!pkHex) return fail(`Missing signer public key (tx ${i})`);
    let pk;
    try {
      pk = hexToBytes(pkHex);
    } catch (_) {
      return fail(`Invalid signer public key (tx ${i})`);
    }
    if (!pk.length) return fail(`Invalid signer public key (tx ${i})`);
    const expectedAddress = wc.publicKeyToAddress(pk);
    if (expectedAddress !== String(tx.fromAddress || "")) {
      return fail(`from_address does not match signer public key (tx ${i})`);
    }
    const sig = hexToBytes(String(tx.signature || ""));
    if (!sig.length) return fail(`Invalid transaction signature (tx ${i})`);
    const data = new Uint8Array(txBytesForSigning(tx));
    let good = false;
    try {
      good = wc.verify(data, sig, pk);
    } catch (_) {
      good = false;
    }
    if (!good) return fail(`Invalid transaction signature (tx ${i})`);
  }

  return { valid: true, computedHash, minerPublicKey: String(header.minerPublicKey || "") };
}

/**
 * Mirror ProducerValidator.validate.
 * @param {object} blockBody
 * @param {{ activeAddresses: string[], completedAddresses: string[], seedHex?: string, remaining?: string[] }} cycleCtx
 * @param {number} allowedSkipDepth
 */
function validateProducer(blockBody, cycleCtx, allowedSkipDepth = 0) {
  const header = blockBody && blockBody.header;
  if (!header) return fail("invalid block");
  const pubKey = String(header.minerPublicKey || "");
  if (!pubKey) return fail("Missing producer public key");
  const wc = loadWalletCrypto();
  let address;
  try {
    address = wc.publicKeyToAddress(hexToBytes(pubKey));
  } catch (_) {
    return fail("Invalid producer public key");
  }
  const active = cycleCtx.activeAddresses || [];
  const completed = new Set(cycleCtx.completedAddresses || []);
  if (!active.includes(address)) return fail("Producer not in active cycle set");
  if (completed.has(address)) return fail("Producer already completed this cycle");

  const skipDepth = Math.max(0, Number(header.nonce) || 0);
  if (skipDepth > allowedSkipDepth) {
    return fail(`Skip depth ${skipDepth} exceeds allowed ${allowedSkipDepth}`);
  }

  let remaining = cycleCtx.remaining;
  if (!Array.isArray(remaining)) {
    // Caller should pass remainingOrder(); fall back to active minus completed (unsorted — only for fail-closed).
    remaining = active.filter((a) => !completed.has(a));
  }
  if (skipDepth >= remaining.length) return fail(`No producer available at skip depth ${skipDepth}`);
  const expected = remaining[skipDepth];
  if (expected !== address) {
    return fail(`Unexpected producer: got ${address} expected ${expected}`);
  }

  const claimed = String(cycleCtx.claimedProducerAddress || "").trim();
  if (claimed && claimed !== address) {
    return fail(`producerAddress does not match miner public key (${claimed} vs ${address})`);
  }

  return { valid: true, producerAddress: address };
}

/**
 * Full live ingest check: structure + producer.
 */
function validateForRelayIngest(blockBody, tip, height, cycleCtx, opts = {}) {
  const strictProducer = opts.strictProducer !== false;
  const allowedSkip = Number(opts.allowedSkipDepth != null ? opts.allowedSkipDepth : 0);
  const struct = validateBlockStructure(blockBody, tip, height);
  if (!struct.valid) return struct;
  if (strictProducer) {
    const prod = validateProducer(blockBody, cycleCtx, allowedSkip);
    if (!prod.valid) return prod;
    return { valid: true, computedHash: struct.computedHash, producerAddress: prod.producerAddress };
  }
  return { valid: true, computedHash: struct.computedHash };
}

/** Shape-only + sig check for mempool (optional hardening). */
function validateMempoolTx(tx) {
  if (!tx || !tx.signature || !tx.fromAddress || !tx.toAddress) return fail("invalid tx");
  const amount = Number(tx.amount);
  if (!Number.isFinite(amount) || amount < 0) return fail("invalid tx");
  const wc = loadWalletCrypto();
  const pk = hexToBytes(String(tx.signerPublicKey || ""));
  if (!pk.length) return fail("missing signer public key");
  if (wc.publicKeyToAddress(pk) !== String(tx.fromAddress)) return fail("from_address mismatch");
  const sig = hexToBytes(String(tx.signature || ""));
  if (!sig.length) return fail("bad signature");
  try {
    if (!wc.verify(new Uint8Array(txBytesForSigning(tx)), sig, pk)) return fail("bad signature");
  } catch (_) {
    return fail("bad signature");
  }
  return ok();
}

module.exports = {
  validateBlockStructure,
  validateProducer,
  validateForRelayIngest,
  validateMempoolTx,
  blockHash,
  txHash,
  merkleRoot,
  blockSizeLimit,
  BLOCK_TIME_SECONDS,
  PROPAGATION_TOLERANCE_SECONDS,
};
