/**
 * Browser/EXE block validation mirroring Android BlockValidator (+ optional producer).
 */
import {
  Protocol,
  blockSizeBytes,
  blockSizeLimit,
  blockHash,
  merkleRoot,
  txHash,
  txBytesForSigning,
  effectiveBlockTimeSeconds,
  nextProducer,
  shuffleOrder,
} from "./protocol.js";
import { verify, hexToBytes, publicKeyToAddress } from "./wallet_crypto.js";

const PROPAGATION_TOLERANCE_SECONDS = 3;

function fail(message) {
  return { valid: false, message };
}

function ok(extra = {}) {
  return { valid: true, ...extra };
}

export function validateBlockTimestamp(header, previousTip) {
  const ts = Number(header.timestamp);
  if (!Number.isFinite(ts)) return fail("Invalid block timestamp");
  if (!previousTip || !previousTip.timestamp) {
    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 = effectiveBlockTimeSeconds(prevTs);
  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} block { header, transactions, hash? }
 * @param {{ hash: string, timestamp: number, height?: number }|null} tip
 * @param {number} height
 */
export function validateBlockStructure(block, tip, height) {
  if (!block || !block.header) return fail("invalid block");
  const header = block.header;
  const transactions = Array.isArray(block.transactions) ? block.transactions : [];

  if (
    Number(header.version) < Protocol.BLOCK_VERSION_MIN ||
    Number(header.version) > Protocol.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 > Protocol.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 = validateBlockTimestamp(header, tip && tip.hash ? tip : null);
  if (!tsCheck.valid) return tsCheck;

  if (tip && tip.hash && 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(block.hash || "").trim();
  if (claimed && claimed !== computedHash) return fail("Block hash mismatch");

  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})`);
    const pk = hexToBytes(pkHex);
    if (!pk.length) return fail(`Invalid signer public key (tx ${i})`);
    if (publicKeyToAddress(pk) !== 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 = txBytesForSigning(tx);
    let good = false;
    try {
      good = verify(data, sig, pk);
    } catch {
      good = false;
    }
    if (!good) return fail(`Invalid transaction signature (tx ${i})`);
  }

  return ok({ computedHash });
}

/**
 * Mirror ProducerValidator (allowedSkipDepth default 0).
 */
export function validateProducer(block, snapshot, allowedSkipDepth = 0) {
  const header = block && block.header;
  if (!header) return fail("invalid block");
  const pubKey = String(header.minerPublicKey || "");
  if (!pubKey) return fail("Missing producer public key");
  let address;
  try {
    address = publicKeyToAddress(hexToBytes(pubKey));
  } catch {
    return fail("Invalid producer public key");
  }
  const active = [...new Set(snapshot.activeAddresses || [])];
  const completed = new Set(snapshot.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}`);
  }
  const order = shuffleOrder(active, snapshot.seedHex);
  const remaining = order.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}`);
  }
  return ok({ producerAddress: address });
}

export function validateIncomingBlock(block, tip, height, snapshot = null) {
  const struct = validateBlockStructure(block, tip, height);
  if (!struct.valid) return struct;
  if (snapshot) {
    const prod = validateProducer(block, snapshot, 0);
    if (!prod.valid) return prod;
  }
  return struct;
}

export { nextProducer };
