/**

 * Protocol helpers: constants, merkle, producer shuffle, tx/block hashing.

 * Confirmation / claim-window values mirror ProtocolConstants.kt.

 */

import { sha3_256, bytesToHex, hexToBytes, sign, publicKeyToAddress, isValidSrmdAddress, hmacSha256Hex } from "./wallet_crypto.js";



export const Protocol = {

  // Block time — consensus-locked at 33 seconds forever.

  BLOCK_TIME_START_SECONDS: 33,

  /** @deprecated Block time no longer grows. */

  BLOCK_TIME_MAX_SECONDS: 33,

  /** @deprecated Block time no longer grows. */

  BLOCK_TIME_STEP_SECONDS: 0,

  /** @deprecated Block time no longer grows. */

  BLOCK_TIME_STEP_DAYS: 333,

  SECONDS_PER_DAY: 86400,

  SRMD_DECIMALS: 6,
  SRMD_UNITS: 1_000_000,

  BLOCK_VERSION_MIN: 1,
  BLOCK_VERSION_DECIMAL: 2,
  BLOCK_VERSION: 2,
  SNAPSHOT_HMAC_SECRET: "sarmad-dev-snapshot-secret",
  ACTIVE_HEARTBEAT_HUNT_EXTRA_MINUTES: 20,

  MINER_REWARD: 2 * 1_000_000,
  INVITER_REWARD: 1_000_000,
  STAKING_CONTRACT_REWARD: 1_000_000,
  ACTIVE_NODES_REWARD: 1_000_000,

  ACTIVE_NODES_CLAIM_WINDOW_HOURS: 23,
  ACTIVE_HEARTBEAT_INTERVAL_SECONDS: 20 * 60,
  ACTIVE_HEARTBEAT_CREDIT_MINUTES: 20,

  /** From this height ARSC uses block counting (see ARSC_* below). */
  ARSC_BLOCK_EPOCH_ACTIVATION_HEIGHT: 8000,
  ARSC_BLOCKS_PER_SHARE_CHUNK: 333,
  ARSC_SHARES_PER_CHUNK: 3,
  ARSC_EPOCH_BLOCKS: 3333,

  arscBlockEpochActive(height) {
    return Number(height || 0) >= Protocol.ARSC_BLOCK_EPOCH_ACTIVATION_HEIGHT;
  },

  arscSharesFromEligibleBlocks(eligibleBlocks) {
    const n = Number(eligibleBlocks || 0);
    if (n <= 0) return 0;
    return Math.floor(n / Protocol.ARSC_BLOCKS_PER_SHARE_CHUNK) * Protocol.ARSC_SHARES_PER_CHUNK;
  },

  /** Wallet-to-wallet gas: 0.033% = 33 / 100_000, floored, min 0. */
  GAS_FEE_NUMERATOR: 33,
  GAS_FEE_DENOMINATOR: 100_000,

  transferGasFee(amount) {
    const n = Number(amount) || 0;
    if (n <= 0) return 0;
    return Math.floor((n * Protocol.GAS_FEE_NUMERATOR) / Protocol.GAS_FEE_DENOMINATOR);
  },

  formatSrmd(units) {
    const n = Number(units) || 0;
    const neg = n < 0;
    const mag = Math.abs(n);
    const whole = Math.floor(mag / Protocol.SRMD_UNITS);
    const frac = mag % Protocol.SRMD_UNITS;
    const sign = neg ? "-" : "";
    if (frac === 0) return sign + String(whole);
    return sign + String(whole) + "." + String(frac).padStart(Protocol.SRMD_DECIMALS, "0").replace(/0+$/, "");
  },

  parseSrmd(text) {
    const t = String(text || "").trim().replace(/,/g, "");
    if (!t) return null;
    const neg = t.startsWith("-");
    const s = neg ? t.slice(1) : t;
    if (!s || (s.match(/\./g) || []).length > 1) return null;
    const [w, f = ""] = s.split(".");
    if (!/^\d*$/.test(w) || !/^\d*$/.test(f) || f.length > Protocol.SRMD_DECIMALS) return null;
    const whole = Number(w || "0");
    const frac = Number((f + "000000").slice(0, Protocol.SRMD_DECIMALS));
    if (!Number.isFinite(whole) || !Number.isFinite(frac)) return null;
    const v = whole * Protocol.SRMD_UNITS + frac;
    return neg ? -v : v;
  },

  huntWindowId(networkTs) {
    return Math.floor(Number(networkTs || 0) / Protocol.ACTIVE_HEARTBEAT_INTERVAL_SECONDS);
  },

  huntHmacHex(address, window) {
    return hmacSha256Hex(Protocol.SNAPSHOT_HMAC_SECRET, `SARFEES-HUNT|${address}|${window}`);
  },

  verifyHuntHmac(address, window, sig, networkTs) {
    if (window !== Protocol.huntWindowId(networkTs)) return false;
    const expect = Protocol.huntHmacHex(address, window);
    return String(expect).toLowerCase() === String(sig || "").toLowerCase();
  },

  scaleLegacyAmount(amount, blockVersion) {
    const v = Number(blockVersion || 0);
    const n = Number(amount) || 0;
    if (v < Protocol.BLOCK_VERSION_DECIMAL) return n * Protocol.SRMD_UNITS;
    return n;
  },

  STAKING_MIN_SUBSCRIPTION: 3 * 1_000_000,

  STAKING_MAX_SUBSCRIPTION: 33 * 1_000_000,

  STAKING_DISTRIBUTION_DIVISOR: 60,

  /** UI estimate: (pool/60/allUsersShares)*userShares */
  estimateStakingNextMonth(poolBalance, allUsersShares, userShares) {
    if (allUsersShares <= 0 || userShares <= 0 || poolBalance <= 0) return 0;
    const monthlyPool = poolBalance / Protocol.STAKING_DISTRIBUTION_DIVISOR;
    return (monthlyPool / allUsersShares) * userShares;
  },

  STAKING_ELIGIBILITY_MONTHS: 222,

  STAKING_MONTH_DAYS: 30,

  STAKING_MONTH_SECONDS: 30 * 24 * 60 * 60,

  /** Unclaimed monthly staking payout expires after this many network days. */

  STAKING_CLAIM_DAYS: 29,

  MAX_INVITER_REWARDS_PER_DAY: 333 * 1_000_000,

  /** Referral claimable expires after 7 network days. */

  INVITER_CLAIM_WINDOW_HOURS: 7 * 24,

  /** Minimum claimable balance required before INVITE:CLAIM is accepted. */

  INVITER_MIN_CLAIM: 10 * 1_000_000,

  /** Inviter spendable balance required before a new friend can lock them as referral. */
  INVITER_MIN_BALANCE: 3 * 1_000_000,

  /** Hard cap of invitees per inviter address. */
  MAX_INVITEES_PER_ADDRESS: 33,

  /**
   * From this height a wallet must lock PRODUCER_ADMISSION_BOND to mine.
   * Until then mining is free. Bond does not buy extra turns (time-based issuance).
   */
  PRODUCER_ADMISSION_HEIGHT: 3_333_333,

  PRODUCER_ADMISSION_BOND: 3 * 1_000_000,

  PRODUCER_ADMISSION_EXIT_SECONDS: 33 * 86_400,

  MAX_PRODUCERS_PER_IP: 5,

  /** Per-wallet disk reserve before mining (chain + app data). */
  PRODUCER_STORAGE_RESERVE_BYTES: 1024 * 1024 * 1024,

  /** Per-wallet RAM: 32 MB slabs, min 3 (96 MB) to mine, max 13 (416 MB). */
  PRODUCER_RAM_SLAB_BYTES: 32 * 1024 * 1024,
  PRODUCER_RAM_MIN_SLABS: 3,
  PRODUCER_RAM_MAX_SLABS: 13,
  PRODUCER_RAM_MIN_BYTES: 3 * 32 * 1024 * 1024,
  PRODUCER_RAM_RESERVE_BYTES: 13 * 32 * 1024 * 1024,

  producerAdmissionRequired(chainHeight) {
    return Number(chainHeight || 0) >= Protocol.PRODUCER_ADMISSION_HEIGHT;
  },

  isActiveProducerBond(locked, unbondAt) {
    return Number(locked || 0) >= Protocol.PRODUCER_ADMISSION_BOND && Number(unbondAt || 0) === 0;
  },

  BUILT_IN_CONTRACT_CONFIRMATIONS: 1,

  SMALL_TRANSFER_MAX: 33 * 1_000_000,

  MEDIUM_TRANSFER_MAX: 3333 * 1_000_000,

  SMALL_TRANSFER_CONFIRMATIONS: 3,

  MEDIUM_TRANSFER_CONFIRMATIONS: 6,

  LARGE_TRANSFER_CONFIRMATIONS: 9,

  BURN_ADDRESS: "SRMDBURN000000000000000000000000000",

  FOUNDER_ADDRESS: "SRMDBdpeTwAUKQjYsWsnsDGhNC1merWh6MV3r7uM6N4Yet",

  /** Fee paid to founder when generating an extra address (APK parity). */
  ADDRESS_CREATION_FEE: 1_000_000,

  /**
   * Same secret as ProtocolConstants.COORDINATOR_RESET_TOKEN / VPS SARMAD_RESET_TOKEN.
   * Used only by the founder wallet UI for full network wipe.
   */
  COORDINATOR_RESET_TOKEN:
    "dba3f7600625a30c195fcd251b4d5dcf2f9f1baa3833ea9c305a6950c80b123b",

  STAKING_ADDRESS: "SRMD1STAKINGREWARD0000000000000000000",

  INVITER_ADDRESS: "SRMD1INVITER0000000000000000000000000",

  ACTIVE_ADDRESS: "SRMD1ACTIVENODES00000000000000000000",

  PRODUCER_BOND_ADDRESS: "SRMD1PRODUCERBOND0000000000000000000",

  GENESIS_WALL_CLOCK_EPOCH_SECONDS: 1_785_283_200,

  MAX_MEMPOOL: 3333,

  // 0–3,333,332 → 333 KB; then +333 KB every 3,333,333 blocks

  BLOCK_SIZE_START_BYTES: 333 * 1024,

  BLOCK_SIZE_INCREMENT_BYTES: 333 * 1024,

  BLOCK_SIZE_INTERVAL: 3333333,

  MAX_TRANSACTIONS_PER_BLOCK: 200,

};



/** Mirrors ProtocolConstants.getBlockSizeLimit; phones reject blocks above this. */

export function blockSizeLimit(blockHeight) {

  const increments = Math.floor((Number(blockHeight) || 0) / Protocol.BLOCK_SIZE_INTERVAL);

  return Protocol.BLOCK_SIZE_START_BYTES + increments * Protocol.BLOCK_SIZE_INCREMENT_BYTES;

}



/**

 * Wallet confirmation policy. Mirrors ProtocolConstants.requiredConfirmations:

 * any non-null contract payload → 1 confirmation; plain transfers by amount tier.

 */

export function requiredConfirmations(amount, contractPayload = null) {

  if (contractPayload != null) {

    return Protocol.BUILT_IN_CONTRACT_CONFIRMATIONS;

  }

  const value = Number(amount) || 0;

  if (value <= Protocol.SMALL_TRANSFER_MAX) return Protocol.SMALL_TRANSFER_CONFIRMATIONS;

  if (value <= Protocol.MEDIUM_TRANSFER_MAX) return Protocol.MEDIUM_TRANSFER_CONFIRMATIONS;

  return Protocol.LARGE_TRANSFER_CONFIRMATIONS;

}



/** Confirmations for a tx included at inclusionHeight given tip height. */

export function confirmationDepth(currentHeight, inclusionHeight) {

  if (inclusionHeight == null || !Number.isFinite(Number(inclusionHeight))) return 0;

  return Math.max(0, Number(currentHeight) - Number(inclusionHeight) + 1);

}



/** Approximate remaining finality wait at fixed 33s block time. */

export function remainingFinalitySeconds(confirmations, required) {

  const left = Math.max(0, (Number(required) || 0) - (Number(confirmations) || 0));

  return left * Protocol.BLOCK_TIME_START_SECONDS;

}



/** Mirrors Transaction.sizeBytes on Android so both sides pack blocks the same way. */

export function txSizeBytes(tx) {

  return (

    txBytesForSigning(tx).length +

    (tx.signature || "").length +

    (tx.signerPublicKey || "").length +

    (tx.payload instanceof Uint8Array

      ? tx.payload.length

      : tx.payloadHex

        ? tx.payloadHex.length / 2

        : 0)

  );

}



export const Payload = {

  STAKE_SUBSCRIBE: "STAKE:SUBSCRIBE",

  STAKE_ACTIVATE: "STAKE:ACTIVATE",

  STAKE_CLAIM: "STAKE:CLAIM",

  ACTIVE_HEARTBEAT: "ACTIVE:HEARTBEAT",

  ACTIVE_CLAIM: "ACTIVE:CLAIM",

  COORD_HEARTBEAT: "COORD:HEARTBEAT",

  COORD_CLAIM: "COORD:CLAIM",

  INVITE_CLAIM: "INVITE:CLAIM",
  // Zero-value call that only publishes the sender's inviter on-chain. Mining never
  // carries it, so without this every produced block burns the inviter reward.
  INVITE_REGISTER: "INVITE:REGISTER",

  PRODUCER_JOIN: "PRODUCER:JOIN",
  PRODUCER_LEAVE: "PRODUCER:LEAVE",
  PRODUCER_BOND: "PRODUCER:BOND",
  PRODUCER_UNBOND: "PRODUCER:UNBOND",
  PRODUCER_CLAIM_BOND: "PRODUCER:CLAIM_BOND",

  encode(op) {

    return new TextEncoder().encode(op);

  },

  decode(bytes) {

    if (!bytes || !bytes.length) return null;

    return new TextDecoder().decode(bytes);

  },

  opcode(bytes) {
    const raw = Payload.decode(bytes);
    if (!raw) return null;
    const i = raw.indexOf("|");
    return i < 0 ? raw : raw.slice(0, i);
  },

  payloadHex(bytes) {

    if (!bytes || !bytes.length) return "";

    return bytesToHex(bytes);

  },

};



/** Resolve payload bytes from a tx object (live Uint8Array or wire hex). */

export function txPayloadBytes(tx) {

  if (!tx) return null;

  if (tx.payload instanceof Uint8Array) return tx.payload;

  if (tx.payloadHex) {

    const hex = String(tx.payloadHex);

    if (!hex) return null;

    return hexToBytes(hex);

  }

  return null;

}



/**

 * Effective block time — always 33s (locked). Argument kept for API parity with Kotlin.

 */

export function effectiveBlockTimeSeconds(_networkTs) {

  return Protocol.BLOCK_TIME_START_SECONDS;

}



export function nextBlockTimestamp(prevTs) {

  const bt = effectiveBlockTimeSeconds(prevTs);

  return Math.max(0, Number(prevTs) || 0) + bt;

}



export function txBytesForSigning(tx) {

  const payloadHex = Payload.payloadHex(

    tx.payload instanceof Uint8Array

      ? tx.payload

      : tx.payloadHex

        ? hexToBytes(tx.payloadHex)

        : null

  );

  return new TextEncoder().encode(

    `${tx.fromAddress}|${tx.toAddress}|${tx.amount}|${tx.gasFee}|${tx.timestamp}|${payloadHex}`

  );

}



export function txHash(tx) {

  return bytesToHex(sha3_256(txBytesForSigning(tx)));

}



/** Mirrors Block.sizeBytes on Android. */

export function blockSizeBytes(header, transactions) {

  return (

    headerBytesForHash(header).length +

    (transactions || []).reduce((sum, t) => sum + txSizeBytes(t), 0)

  );

}



export function headerBytesForHash(h) {

  return new TextEncoder().encode(

    `${h.version}|${h.previousBlockHash}|${h.networkTime}|${h.timestamp}|${h.merkleRoot}|${h.difficultyTarget}|${h.nonce}|${h.minerPublicKey}|${h.transactionCount}|${h.founderAddress || ""}`

  );

}



export function blockHash(header) {

  return bytesToHex(sha3_256(headerBytesForHash(header)));

}



/**

 * Genesis block built from the network genesis timestamp the coordinator publishes.

 * Must stay byte-identical to GenesisBlock.createWithTimestamp on Android, otherwise

 * this client would build a different chain instead of joining the network.

 */

export function genesisBlock(timestampSeconds = 0) {

  const ts = Math.max(0, Number(timestampSeconds) || 0);

  const header = {

    // Must match Android GenesisBlock.kt (always version 1). BLOCK_VERSION (2)
    // produced a different genesis hash, so the EXE rejected every live block.
    version: 1,

    previousBlockHash: "0".repeat(64),

    networkTime: ts * 1000,

    timestamp: ts,

    merkleRoot: bytesToHex(sha3_256(new TextEncoder().encode("genesis"))),

    difficultyTarget: "0".repeat(64),

    nonce: 0,

    minerPublicKey: "genesis",

    transactionCount: 0,

    founderAddress: Protocol.FOUNDER_ADDRESS,

  };

  return { height: 0, hash: blockHash(header), header, transactions: [] };

}



export function merkleRoot(txHashes) {

  if (!txHashes.length) return bytesToHex(sha3_256(new Uint8Array(0)));

  let hashes = txHashes.map((h) => 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 cat = new Uint8Array(hashes[i].length + hashes[i + 1].length);

      cat.set(hashes[i], 0);

      cat.set(hashes[i + 1], hashes[i].length);

      next.push(sha3_256(cat));

    }

    hashes = next;

  }

  return bytesToHex(hashes[0]);

}



function seedToState(seedHex) {

  const normalized = String(seedHex || "")

    .toLowerCase()

    .padStart(64, "0")

    .slice(0, 64);

  const bytes = sha3_256(new TextEncoder().encode(normalized));

  let value = 0n;

  for (let i = 0; i < 8; i++) value = (value << 8n) | BigInt(bytes[i]);

  return value === 0n ? 1n : value;

}



function nextState(state) {

  let x = state;

  x ^= x >> 12n;

  x ^= x << 25n;

  x ^= x >> 27n;

  return BigInt.asUintN(64, x * 0x2545f4914f6cdd1dn);

}



export function shuffleOrder(activeAddresses, seedHex) {

  const sorted = [...new Set(activeAddresses)].sort();

  if (sorted.length <= 1) return sorted;

  const order = [...sorted];

  let state = seedToState(seedHex);

  for (let i = order.length - 1; i >= 1; i--) {

    state = nextState(state);

    const j = Number(state % BigInt(i + 1));

    const tmp = order[i];

    order[i] = order[j];

    order[j] = tmp;

  }

  return order;

}



export function nextProducer(snapshot) {

  const order = shuffleOrder(snapshot.activeAddresses, snapshot.seedHex);

  const done = new Set(snapshot.completedAddresses || []);

  return order.find((a) => !done.has(a)) || null;

}

/** True when the VPS cycle list is a subset of the on-chain producer set (or the chain set is still empty). */
export function vpsCycleSetAllowed(chainSet, vpsActive) {
  const chain = Array.isArray(chainSet) ? chainSet : [];
  if (!chain.length) return true;
  const allowed = new Set(chain);
  return (vpsActive || []).every((a) => allowed.has(a));
}



export function createSignedTx(wallet, { toAddress, amount, payload = null, gasFee = null }) {

  const kp = wallet.keyPairs[wallet.currentAddressIndex || 0];

  const fromAddress = publicKeyToAddress(kp.publicKey);

  const hasPayload = payload != null && payload !== "";

  const resolvedGas =

    gasFee != null ? gasFee : hasPayload ? 0 : Protocol.transferGasFee(amount);

  const timestamp = Date.now();

  const payloadBytes =

    payload == null ? null : typeof payload === "string" ? Payload.encode(payload) : payload;

  const tx = {

    fromAddress,

    toAddress,

    amount: Number(amount),

    gasFee: Number(resolvedGas),

    timestamp,

    signature: "",

    signerPublicKey: kp.publicKeyHex,

    payload: payloadBytes,

    payloadHex: payloadBytes ? bytesToHex(payloadBytes) : "",

    inviterAddress: wallet.inviterAddress || null,

  };

  const sig = sign(txBytesForSigning(tx), kp.privateKey);

  tx.signature = bytesToHex(sig);

  return tx;

}



export function produceBlock({

  previousBlockHash,

  previousBlockTimestampSeconds,

  transactions,

  producerPublicKey,

  skipDepth = 0,

}) {

  const timestamp = nextBlockTimestamp(previousBlockTimestampSeconds);

  const hashes = transactions.map((t) => txHash(t));

  const root = merkleRoot(hashes);

  const header = {

    version: Protocol.BLOCK_VERSION,

    previousBlockHash,

    networkTime: timestamp * 1000,

    timestamp,

    merkleRoot: root,

    difficultyTarget: "f".repeat(64),

    nonce: skipDepth,

    minerPublicKey: producerPublicKey,

    transactionCount: transactions.length,

    founderAddress: "",

  };

  return { header, transactions, hash: blockHash(header) };

}



export { isValidSrmdAddress };


