/**
 * ARSC presence counter for EXE operators (approximate until full sync).
 * Tracks COORD heartbeats as +20 minutes; hours = floor(minutes/60).
 * On-chain settle credits claimable; COORD:CLAIM must be submitted within 23h or burn.
 *
 * Credits are applied at most once per (blockHeight, fromAddress, op).
 */
const fs = require("fs");
const path = require("path");
const { dataPath } = require("./data_dir");

const STORE = dataPath("coord_rewards.json");
const CREDIT_MINUTES = 20;

/** @type {Map<string, { minutes: number, earned: number, referralClaimable: number, inviterAddress: string }>} */
const byAddress = new Map();
/** @type {Set<string>} height|from|op already credited */
const creditedHb = new Set();

function load() {
  try {
    const raw = JSON.parse(fs.readFileSync(STORE, "utf8"));
    for (const [addr, row] of Object.entries(raw.byAddress || raw || {})) {
      if (addr === "creditedHb" || addr === "byAddress") continue;
      const minutes =
        row.minutes != null
          ? Number(row.minutes) || 0
          : (Number(row.claimable) || 0) * CREDIT_MINUTES;
      byAddress.set(addr, {
        minutes,
        earned: Number(row.earned) || 0,
        referralClaimable: Number(row.referralClaimable) || 0,
        inviterAddress: String(row.inviterAddress || ""),
        lastHbNetworkTs: Number(row.lastHbNetworkTs) || 0,
      });
    }
    if (Array.isArray(raw.creditedHb)) {
      for (const k of raw.creditedHb) creditedHb.add(String(k));
    }
  } catch (_) {}
}

function save() {
  const obj = { byAddress: {}, creditedHb: [...creditedHb].slice(-5000) };
  for (const [addr, row] of byAddress.entries()) obj.byAddress[addr] = row;
  try {
    fs.writeFileSync(STORE, JSON.stringify(obj, null, 2), "utf8");
  } catch (_) {}
}

function row(addr) {
  let r = byAddress.get(addr);
  if (!r) {
    r = { minutes: 0, earned: 0, referralClaimable: 0, inviterAddress: "", lastHbNetworkTs: 0 };
    byAddress.set(addr, r);
  }
  if (r.lastHbNetworkTs == null) r.lastHbNetworkTs = 0;
  return r;
}

function payloadOp(tx) {
  if (!tx) return "";
  if (tx.payloadHex) {
    try {
      return Buffer.from(String(tx.payloadHex), "hex").toString("utf8");
    } catch (_) {
      return "";
    }
  }
  if (typeof tx.payload === "string") return tx.payload;
  return "";
}

function observeBlock(blockDto) {
  const height = Number(blockDto && blockDto.height) || 0;
  const networkTs = Number(
    (blockDto && blockDto.header && blockDto.header.timestamp) ||
      (blockDto && blockDto.tipTimestamp) ||
      0
  );
  const txs = (blockDto && blockDto.transactions) || [];
  let changed = false;
  for (const tx of txs) {
    const op = payloadOp(tx).split("|")[0];
    const from = String(tx.fromAddress || "");
    if (!from.startsWith("SRMD")) continue;
    if (op === "COORD:HEARTBEAT" || op === "ACTIVE:HEARTBEAT") {
      const key = `${height}|${from}|${op}`;
      if (creditedHb.has(key)) continue;
      // Match on-chain rule: at most one credit per address per 20 network minutes.
      const r = row(from);
      const last = Number(r.lastHbNetworkTs) || 0;
      if (last > 0 && networkTs > 0 && networkTs - last < 20 * 60) {
        continue;
      }
      creditedHb.add(key);
      const hunt = payloadOp(tx).includes("|HUNT|");
      r.minutes += CREDIT_MINUTES + (hunt ? CREDIT_MINUTES : 0);
      if (networkTs > 0) r.lastHbNetworkTs = networkTs;
      changed = true;
    } else if (op === "COORD:CLAIM" || op === "ACTIVE:CLAIM") {
      changed = true;
    } else if (op === "INVITE:REGISTER") {
      const inv = String(tx.inviterAddress || "").trim();
      if (inv.startsWith("SRMD")) {
        const r = row(from);
        r.inviterAddress = inv;
        changed = true;
      }
    } else if (op === "INVITE:CLAIM") {
      const r = row(from);
      r.referralClaimable = 0;
      changed = true;
    }
  }
  if (changed) save();
}

function getRewards(address) {
  const r = byAddress.get(address) || {
    minutes: 0,
    earned: 0,
    referralClaimable: 0,
    inviterAddress: "",
  };
  const hours = Math.floor((r.minutes || 0) / 60);
  const rem = (r.minutes || 0) % 60;
  return {
    /** Full hours this epoch (approximate local counter). */
    claimable: hours,
    hours,
    minutes: r.minutes || 0,
    minutesRemainder: rem,
    earned: r.earned,
    referralClaimable: r.referralClaimable,
    inviterAddress: r.inviterAddress || null,
  };
}

function resetRewards() {
  byAddress.clear();
  creditedHb.clear();
  try {
    if (fs.existsSync(STORE)) fs.unlinkSync(STORE);
  } catch (_) {}
}

/**
 * Cap presence minutes to what tip network-time can justify (stops duplicate-credit leftovers).
 * At most one +20m credit per 20 network minutes of tipTimestamp.
 */
function sanitizeAgainstTip(tipTimestampSeconds) {
  const tipTs = Math.max(0, Number(tipTimestampSeconds) || 0);
  const maxCredits = tipTs > 0 ? Math.floor(tipTs / (20 * 60)) + 1 : 0;
  const maxMinutes = maxCredits * CREDIT_MINUTES;
  let changed = false;
  for (const r of byAddress.values()) {
    if ((r.minutes || 0) > maxMinutes) {
      r.minutes = maxMinutes;
      changed = true;
    }
  }
  if (changed) save();
  return { maxMinutes, tipTs };
}

load();

module.exports = {
  observeBlock,
  getRewards,
  resetRewards,
  sanitizeAgainstTip,
  CREDIT_MINUTES,
};

