const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const pull = require("../server/node_modules/pull-stream");
const { getConfig } = require("../configs/config-manager.js");
const { config } = require("../server/SSB_server.js");
const sharedState = require("../configs/shared-state.js");

const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));

const MAX_PENDING_EPOCHS = 12;

const DEFAULT_RULES = {
  epochKind: "MONTHLY",
  alpha: 0.2,
  reserveMin: 500,
  capPerEpoch: 2000,
  caps: { cap_user_epoch: 50, floor_user: 1, w_min: 0.2, w_max: 6 },
  graceDays: 30
};

const STORAGE_DIR = process.env.OASIS_BANKING_DIR || path.join(__dirname, "..", "configs");
const EPOCHS_PATH = path.join(STORAGE_DIR, "banking-epochs.json");
const TRANSFERS_PATH = path.join(STORAGE_DIR, "banking-allocations.json");
const ADDR_PATH = path.join(STORAGE_DIR, "wallet-addresses.json");
const ECO_HISTORY_PATH = path.join(STORAGE_DIR, "banking-eco-history.json");
const ECO_HISTORY_MAX = 500;
const ECO_HISTORY_MIN_GAP_MS = 5 * 60 * 1000;

const ECOIN_PER_GRAM_CO2 = 0.1;
const ECOIN_PER_DAY_OF_HISTORY = 0.001;
const ONE_DAY_MS = 86400000;
const ONE_MIB = 1024 * 1024;
const carbonGramsFromBytes = (b) => (Number(b) || 0) / ONE_MIB * 0.095;
const ecoinTaxFromGrams = (g) => (Number(g) || 0) * ECOIN_PER_GRAM_CO2;
const ecoinTaxFromBytes = (b) => ecoinTaxFromGrams(carbonGramsFromBytes(b));
const archTaxFromDays = (days) => Math.max(0, Number(days) || 0) * ECOIN_PER_DAY_OF_HISTORY;

function ensureStoreFiles() {
  if (!fs.existsSync(STORAGE_DIR)) fs.mkdirSync(STORAGE_DIR, { recursive: true });
  if (!fs.existsSync(EPOCHS_PATH)) fs.writeFileSync(EPOCHS_PATH, "[]");
  if (!fs.existsSync(TRANSFERS_PATH)) fs.writeFileSync(TRANSFERS_PATH, "[]");
  if (!fs.existsSync(ADDR_PATH)) fs.writeFileSync(ADDR_PATH, "{}");
  if (!fs.existsSync(ECO_HISTORY_PATH)) fs.writeFileSync(ECO_HISTORY_PATH, "[]");
}

function readEcoHistory() {
  ensureStoreFiles();
  try { return JSON.parse(fs.readFileSync(ECO_HISTORY_PATH, "utf8")) || []; } catch (_) { return []; }
}

function appendEcoHistory(sample) {
  ensureStoreFiles();
  let arr = readEcoHistory();
  if (!Array.isArray(arr)) arr = [];
  const last = arr[arr.length - 1];
  if (last && Number(sample.ts) - Number(last.ts) < ECO_HISTORY_MIN_GAP_MS) return arr;
  arr.push(sample);
  if (arr.length > ECO_HISTORY_MAX) arr = arr.slice(arr.length - ECO_HISTORY_MAX);
  try { fs.writeFileSync(ECO_HISTORY_PATH, JSON.stringify(arr)); } catch (_) {}
  return arr;
}

function epochIdNow() {
  const d = new Date();
  const yyyy = d.getUTCFullYear();
  const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
  return `${yyyy}-${mm}`;
}

async function getAnyWalletAddress() {
  const tryOne = async (method, params = []) => {
    const r = await rpcCall(method, params, "user");
    if (!r) return null;
    if (typeof r === "string" && isValidEcoinAddress(r)) return r;
    if (Array.isArray(r) && r.length && isValidEcoinAddress(r[0])) return r[0];
    if (r && typeof r === "object") {
      const keys = Object.keys(r);
      if (keys.length && isValidEcoinAddress(keys[0])) return keys[0];
      if (r.address && isValidEcoinAddress(r.address)) return r.address;
    }
    return null;
  };
  return await tryOne("getnewaddress")
      || await tryOne("getaddress")
      || await tryOne("getaccountaddress", [""])
      || await tryOne("getaddressesbyaccount", [""])
      || await tryOne("getaddressesbylabel", [""])
      || await tryOne("getaddressesbylabel", ["default"]);
}

async function ensureSelfAddressPublished() {
  const me = config.keys.id;
  const local = readAddrMap();
  const current = typeof local[me] === "string" ? local[me] : (local[me] && local[me].address) || null;
  if (current && isValidEcoinAddress(current)) return { status: "present", address: current };
  const cfg = getWalletCfg("user") || {};
  if (!cfg.url) return { status: "skipped" };
  const addr = await getAnyWalletAddress();
  if (addr && isValidEcoinAddress(addr)) {
    const m = readAddrMap();
    m[me] = addr;
    writeAddrMap(m);
    let ssb = null;
    try {
      if (services?.cooler?.open) ssb = await services.cooler.open();
      else if (global.ssb) ssb = global.ssb;
      else {
        try {
          const srv = require("../server/SSB_server.js");
          ssb = srv?.ssb || srv?.server || srv?.default || null;
        } catch (_) {}
      }
    } catch (_) {}
    if (ssb && ssb.publish) {
      await new Promise((resolve, reject) =>
	   ssb.publish(
	      { type: "wallet", coin: "ECO", address: addr, timestamp: Date.now(), updatedAt: new Date().toISOString() },
	      (err) => err ? reject(err) : resolve()
	    )
      );
    }
    return { status: "published", address: addr };
  }
  return { status: "error" };
}

function readJson(p, d) {
  try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return d; }
}

function writeJson(p, v) {
  fs.writeFileSync(p, JSON.stringify(v, null, 2));
}

async function rpcCall(method, params, kind = "user") {
  const cfg = getWalletCfg(kind);
  if (!cfg?.url) {
    return null;
  }
  const headers = {
    "Content-Type": "application/json",
  };
  if (cfg.user || cfg.pass) {
    headers.authorization = "Basic " + Buffer.from(`${cfg.user}:${cfg.pass}`).toString("base64");
  }
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 1500);
  try {
    const res = await fetch(cfg.url, {
      method: "POST",
      headers: headers,
      body: JSON.stringify({
        jsonrpc: "1.0",
        id: "oasis",
        method: method,
        params: params,
      }),
      signal: controller.signal,
    });
    if (!res.ok) {
      return null;
    }
    const data = await res.json();
    if (data.error) {
      return null;
    }
    return data.result;
  } catch (err) {
    return null;
  } finally {
    clearTimeout(timer);
  }
}

async function safeGetBalance(kind = "user") {
  try {
    const r = await rpcCall("getbalance", [], kind);
    return Number(r) || 0;
  } catch {
    return 0;
  }
}

function readAddrMap() {
  ensureStoreFiles();
  const raw = readJson(ADDR_PATH, {});
  return raw && typeof raw === "object" ? raw : {};
}

function writeAddrMap(m) {
  ensureStoreFiles();
  writeJson(ADDR_PATH, m || {});
}

function getLogLimit() {
  return getConfig().ssbLogStream?.limit || 1000;
}

function isValidEcoinAddress(addr) {
  return typeof addr === "string" && /^E[1-9A-HJ-NP-Za-km-z]{32,34}$/.test(addr);
}

function getWalletCfg(kind) {
  const cfg = getConfig() || {};
  if (kind === "pub") {
    if (!isPubNode()) return null;
    return cfg.wallet || null;
  }
  return cfg.wallet || null;
}

function isPubNode() {
  const pubId = (getConfig() || {}).walletPub?.pubId || "";
  const myId = config?.keys?.id || "";
  return !!pubId && !!myId && pubId === myId;
}

function getConfiguredPubId() {
  return (getConfig() || {}).walletPub?.pubId || "";
}

function resolveUserId(maybeId) {
  const s = String(maybeId || "").trim();
  if (s) return s;
  return config?.keys?.id || "";
}

let FEED_SRC = "none";

module.exports = ({ services } = {}) => {
  const transfersRepo = {
    listAll: async () => { ensureStoreFiles(); return readJson(TRANSFERS_PATH, []); },
    listByTag: async (tag) => { ensureStoreFiles(); return readJson(TRANSFERS_PATH, []).filter(t => (t.tags || []).includes(tag)); },
    findById: async (id) => { ensureStoreFiles(); return readJson(TRANSFERS_PATH, []).find(t => t.id === id) || null; },
    create: async (t) => { ensureStoreFiles(); const all = readJson(TRANSFERS_PATH, []); all.push(t); writeJson(TRANSFERS_PATH, all); },
    markClosed: async (id, txid) => { ensureStoreFiles(); const all = readJson(TRANSFERS_PATH, []); const i = all.findIndex(x => x.id === id); if (i >= 0) { all[i].status = "CLOSED"; all[i].txid = txid; writeJson(TRANSFERS_PATH, all); } }
  };

  const epochsRepo = {
    list: async () => { ensureStoreFiles(); return readJson(EPOCHS_PATH, []); },
    save: async (epoch) => { ensureStoreFiles(); const all = readJson(EPOCHS_PATH, []); const i = all.findIndex(e => e.id === epoch.id); if (i >= 0) all[i] = epoch; else all.push(epoch); writeJson(EPOCHS_PATH, all); },
    get: async (id) => { ensureStoreFiles(); return readJson(EPOCHS_PATH, []).find(e => e.id === id) || null; }
  };

  let ssbInstance;
  async function openSsb() {
    if (ssbInstance) return ssbInstance;
    if (services?.cooler?.open) ssbInstance = await services.cooler.open();
    else if (cooler?.open) ssbInstance = await cooler.open();
    else if (global.ssb) ssbInstance = global.ssb;
    else {
      try {
        const srv = require("../server/SSB_server.js");
        ssbInstance = srv?.ssb || srv?.server || srv?.default || null;
      } catch (_) {
        ssbInstance = null;
      }
    }
    return ssbInstance;
  }

  async function scanLogStream() {
    const ssb = await openSsb();
    if (!ssb) return [];
    return new Promise((resolve, reject) =>
      pull(
        ssb.createLogStream({ limit: getLogLimit(), reverse: true }),
        pull.collect((err, arr) => err ? reject(err) : resolve(arr))
      )
    );
  }

  async function getWalletFromSSB(userId) {
    const msgs = await scanLogStream();
    for (const m of msgs) {
      const v = m.value || {};
      const c = v.content || {};
      if (v.author === userId && c && c.type === "wallet" && c.coin === "ECO" && typeof c.address === "string") {
        return c.address;
      }
    }
    return null;
  }

  async function scanAllWalletsSSB() {
    const latest = {};
    const msgs = await scanLogStream();
    for (const m of msgs) {
      const v = m.value || {};
      const c = v.content || {};
      if (c && c.type === "wallet" && c.coin === "ECO" && typeof c.address === "string") {
        if (!latest[v.author]) latest[v.author] = c.address;
      }
    }
    return latest;
  }

  async function publishSelfAddress(address) {
    const ssb = await openSsb();
    if (!ssb) return false;
    const msg = { type: "wallet", coin: "ECO", address, updatedAt: new Date().toISOString() };
    await new Promise((resolve, reject) => ssb.publish(msg, (err, val) => err ? reject(err) : resolve(val)));
    return true;
  }

  async function listUsers() {
    const addrLocal = readAddrMap();
    const ids = Object.keys(addrLocal);
    if (ids.length > 0) return ids.map(id => ({ id }));
    return [{ id: config.keys.id }];
  }

  async function getUserAddress(userId) {
    const v = readAddrMap()[userId];
    if (v === "__removed__") return null;
    const local = typeof v === "string" ? v : (v && v.address) || null;
    if (local) return local;
    const ssbAddr = await getWalletFromSSB(userId);
    return ssbAddr;
  }

  async function setUserAddress(userId, address, publishIfSelf) {
    if (!userId || !isValidEcoinAddress(address)) return false;
    const m = readAddrMap();
    m[userId] = address;
    writeAddrMap(m);
    if (publishIfSelf && idsEqual(userId, config.keys.id)) await publishSelfAddress(address);
    return true;
  }

  async function addAddress({ userId, address }) {
    if (!userId || !address || !isValidEcoinAddress(address)) return { status: "invalid" };
    const m = readAddrMap();
    const prev = m[userId];
    m[userId] = address;
    writeAddrMap(m);
    if (idsEqual(userId, config.keys.id)) await publishSelfAddress(address);
    return { status: prev ? (prev === address || (prev && prev.address === address) ? "exists" : "updated") : "added" };
  }

  async function removeAddress({ userId }) {
    if (!userId) return { status: "invalid" };
    const m = readAddrMap();
    const hadLocal = !!m[userId] && m[userId] !== "__removed__";
    const ssbAll = await scanAllWalletsSSB();
    if (ssbAll[userId]) {
      m[userId] = "__removed__";
      writeAddrMap(m);
      return { status: "deleted" };
    }
    if (hadLocal) {
      delete m[userId];
      writeAddrMap(m);
      return { status: "deleted" };
    }
    return { status: "not_found" };
  }

  async function listAddressesMerged() {
    const local = readAddrMap();
    const ssbAll = await scanAllWalletsSSB();
    const keys = new Set([...Object.keys(local), ...Object.keys(ssbAll)]);
    const out = [];
    for (const id of keys) {
      if (local[id] === "__removed__") continue;
      if (local[id]) out.push({ id, address: typeof local[id] === "string" ? local[id] : local[id].address, source: "local" });
      else if (ssbAll[id]) out.push({ id, address: ssbAll[id], source: "ssb" });
    }
    return out;
  }

  function idsEqual(a, b) {
    if (!a || !b) return false;
    const A = String(a).trim();
    const B = String(b).trim();
    if (A === B) return true;
    const strip = s => s.replace(/^@/, "").replace(/\.ed25519$/, "");
    return strip(A) === strip(B);
  }

  function inferType(c = {}) {
    if (c.vote) return "vote";
    if (c.votes) return "votes";
    if (c.address && c.coin === "ECO" && c.type === "wallet") return "bankWallet";
    if (c.type === "ubiClaimResult" && c.txid && c.epochId) return "ubiClaimResult";
    if (typeof c.amount !== "undefined" && c.epochId && c.allocationId) return "bankClaim";
    if (typeof c.item_type !== "undefined" && typeof c.status !== "undefined") return "market";
    if (typeof c.goal !== "undefined" && typeof c.progress !== "undefined") return "project";
    if (typeof c.members !== "undefined" && typeof c.isAnonymous !== "undefined") return "tribe";
    if (typeof c.date !== "undefined" && typeof c.location !== "undefined") return "event";
    if (typeof c.priority !== "undefined" && typeof c.status !== "undefined" && c.title) return "task";
    if (typeof c.confirmations !== "undefined" && typeof c.severity !== "undefined") return "report";
    if (typeof c.job_type !== "undefined" && typeof c.status !== "undefined") return "job";
    if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "audio") return "audio";
    if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "video") return "video";
    if (typeof c.url !== "undefined" && c.title && c.key) return "document";
    if (typeof c.text !== "undefined" && typeof c.refeeds !== "undefined") return "feed";
    if (typeof c.text !== "undefined" && typeof c.contentWarning !== "undefined") return "post";
    if (typeof c.contact !== "undefined") return "contact";
    if (typeof c.about !== "undefined") return "about";
    if (typeof c.concept !== "undefined" && typeof c.amount !== "undefined" && c.status) return "transfer";
    return "";
  }

  function normalizeType(a) {
    const t = a.type || a.content?.type || inferType(a.content) || "";
    return String(t).toLowerCase();
  }

  function priorityBump(p) {
    const s = String(p || "").toUpperCase();
    if (s === "HIGH") return 3;
    if (s === "MEDIUM") return 1;
    return 0;
  }

  function severityBump(s) {
    const x = String(s || "").toUpperCase();
    if (x === "CRITICAL") return 6;
    if (x === "HIGH") return 4;
    if (x === "MEDIUM") return 2;
    return 0;
  }

  function scoreMarket(c) {
    const st = String(c.status || "").toUpperCase();
    let s = 5;
    if (st === "SOLD") s += 8;
    else if (st === "ACTIVE") s += 3;
    const bids = Array.isArray(c.auctions_poll) ? c.auctions_poll.length : 0;
    s += Math.min(10, bids);
    return s;
  }

  function scoreProject(c) {
    const st = String(c.status || "ACTIVE").toUpperCase();
    const prog = Number(c.progress || 0);
    let s = 8 + Math.min(10, prog / 10);
    if (st === "FUNDED") s += 10;
    return s;
  }

  function calculateOpinionScore(content) {
    const cats = content?.opinions || {};
    let s = 0;
    for (const k in cats) {
      if (!Object.prototype.hasOwnProperty.call(cats, k)) continue;
      if (k === "interesting" || k === "inspiring") s += 5;
      else if (k === "boring" || k === "spam" || k === "propaganda") s -= 3;
      else s += 1;
    }
    return s;
  }

  async function listAllActions() {
    if (services?.feed?.listAll) {
      const arr = await services.feed.listAll();
      FEED_SRC = "services.feed.listAll";
      return normalizeFeedArray(arr);
    }
    if (services?.activity?.list) {
      const arr = await services.activity.list();
      FEED_SRC = "services.activity.list";
      return normalizeFeedArray(arr);
    }
    if (typeof global.listFeed === "function") {
      const arr = await global.listFeed("all");
      FEED_SRC = "global.listFeed('all')";
      return normalizeFeedArray(arr);
    }
    const ssb = await openSsb();
    if (!ssb || !ssb.createLogStream) {
      FEED_SRC = "none";
      return [];
    }
    const msgs = await scanLogStream();
    FEED_SRC = "ssb.createLogStream";
    return msgs.map(m => {
      const v = m.value || {};
      const c = v.content || {};
      return {
        id: v.key || m.key,
        author: v.author,
        type: (c.type || "").toLowerCase(),
        value: v,
        content: c
      };
    });
  }

  function normalizeFeedArray(arr) {
    if (!Array.isArray(arr)) return [];
    return arr.map(x => {
      const value = x.value || {};
      const content = x.content || value.content || {};
      const author = x.author || value.author || content.author || null;
      const type = (content.type || "").toLowerCase();
      return { id: x.id || value.key || x.key, author, type, value, content };
    });
  }

async function publishKarmaScore(userId, karmaScore) {
  const ssb = await openSsb();
  if (!ssb || !ssb.publish) return false;
  const timestamp = new Date().toISOString();
  const content = { type: "karmaScore", karmaScore, userId, timestamp };
  return new Promise((resolve, reject) => {
    ssb.publish(content, (err, msg) => err ? reject(err) : resolve(msg));
  });
}

async function fetchUserActions(userId) {
  const me = resolveUserId(userId);
  const actions = await listAllActions();
  const authored = actions.filter(a =>
    (a.author && a.author === me) || (a.value?.author && a.value.author === me)
  );
  if (authored.length) return authored;
  return actions.filter(a => {
    const c = a.content || {};
    const fields = [c.author, c.organizer, c.seller, c.about, c.contact];
    return fields.some(f => f && f === me);
  });
}

function scoreFromActions(actions) {
  let score = 0;
  const nowMs = Date.now();
  for (const action of actions) {
    const t = normalizeType(action);
    const c = action.content || {};
    const rawType = String(c.type || "").toLowerCase();
    const ts = action.value?.timestamp;
    const ageDays = ts ? (nowMs - ts) / 86400000 : Infinity;
    const decay = ageDays <= 30 ? 1.0 : ageDays <= 90 ? 0.5 : 0.25;
    if (t === "post") score += 10 * decay;
    else if (t === "comment") score += 5 * decay;
    else if (t === "like") score += 2 * decay;
    else if (t === "image") score += 8 * decay;
    else if (t === "video") score += 12 * decay;
    else if (t === "audio") score += 8 * decay;
    else if (t === "document") score += 6 * decay;
    else if (t === "bookmark") score += 2 * decay;
    else if (t === "feed") score += 6 * decay;
    else if (t === "forum") score += (c.root ? 5 : 10) * decay;
    else if (t === "vote") score += (3 + calculateOpinionScore(c)) * decay;
    else if (t === "votes") score += Math.min(10, Number(c.totalVotes || 0)) * decay;
    else if (t === "market") score += scoreMarket(c) * decay;
    else if (t === "project") score += scoreProject(c) * decay;
    else if (t === "tribe") score += (6 + Math.min(10, Array.isArray(c.members) ? c.members.length * 0.5 : 0)) * decay;
    else if (t === "event") score += (4 + Math.min(10, Array.isArray(c.attendees) ? c.attendees.length : 0)) * decay;
    else if (t === "task") score += (3 + priorityBump(c.priority)) * decay;
    else if (t === "report") score += (4 + (Array.isArray(c.confirmations) ? c.confirmations.length : 0) + severityBump(c.severity)) * decay;
    else if (t === "curriculum") score += 5 * decay;
    else if (t === "aiexchange") score += (Array.isArray(c.ctx) ? Math.min(10, c.ctx.length) : 0) * decay;
    else if (t === "job") score += (4 + (Array.isArray(c.subscribers) ? c.subscribers.length : 0)) * decay;
    else if (t === "bankclaim") score += Math.min(20, Math.log(1 + Math.max(0, Number(c.amount) || 0)) * 5) * decay;
    else if (t === "bankwallet") score += 2 * decay;
    else if (t === "transfer") score += 1 * decay;
    else if (t === "about") score += (1 + ((c.visibilityPrefs && c.visibilityPrefs.fediverseHandle) ? 8 : 0)) * decay;
    else if (t === "contact") score += 1 * decay;
    else if (t === "pub") score += 1 * decay;
    else if (t === "parliamentcandidature" || rawType === "parliamentcandidature") score += 12 * decay;
    else if (t === "parliamentterm" || rawType === "parliamentterm") score += 25 * decay;
    else if (t === "parliamentproposal" || rawType === "parliamentproposal") score += 8 * decay;
    else if (t === "parliamentlaw" || rawType === "parliamentlaw") score += 16 * decay;
    else if (t === "parliamentrevocation" || rawType === "parliamentrevocation") score += 10 * decay;
    else if (t === "courts_case" || t === "courtscase" || rawType === "courts_case") score += 4 * decay;
    else if (t === "courts_evidence" || t === "courtsevidence" || rawType === "courts_evidence") score += 3 * decay;
    else if (t === "courts_answer" || t === "courtsanswer" || rawType === "courts_answer") score += 4 * decay;
    else if (t === "courts_verdict" || t === "courtsverdict" || rawType === "courts_verdict") score += 10 * decay;
    else if (t === "courts_settlement" || t === "courtssettlement" || rawType === "courts_settlement") score += 8 * decay;
    else if (t === "courts_nomination" || t === "courtsnomination" || rawType === "courts_nomination") score += 6 * decay;
    else if (t === "courts_nom_vote" || t === "courtsnomvote" || rawType === "courts_nom_vote") score += 3 * decay;
    else if (t === "courts_public_pref" || t === "courtspublicpref" || rawType === "courts_public_pref") score += 1 * decay;
    else if (t === "courts_mediators" || t === "courtsmediators" || rawType === "courts_mediators") score += 6 * decay;
    else if (t === "courts_open_support" || t === "courtsopensupport" || rawType === "courts_open_support") score += 2 * decay;
    else if (t === "courts_verdict_vote" || t === "courtsverdictvote" || rawType === "courts_verdict_vote") score += 3 * decay;
    else if (t === "courts_judge_assign" || t === "courtsjudgeassign" || rawType === "courts_judge_assign") score += 5 * decay;
    else if (t === "larpjoinhouse") score += 15 * decay;
    else if (t === "larphousepost") score += 6 * decay;
    else if (t === "larptestattempt") score += 3 * decay;
    else if (t === "torrent") score += 6 * decay;
    else if (t === "shop" || t === "shopproduct") score += 6 * decay;
    else if (t === "shop-purchase") score += 2 * decay;
    else if (t === "pad" || t === "padentry") score += 3 * decay;
    else if (t === "calendar" || t === "calendarnote" || t === "calendardate") score += 3 * decay;
    else if (t === "chat") score += 1 * decay;
    else if (t === "gamescore") score += 2 * decay;
    else if (t === "pixelia") score += 2 * decay;
    else if (rawType === "industry") score += 8 * decay;
    else if (rawType === "industryblueprint") score += 6 * decay;
    else if (rawType === "industrybuild") score += 8 * decay;
    else if (rawType === "industrycontribution") score += 4 * decay;
    else if (rawType === "industryallocation") score += 10 * decay;
    else if (rawType === "industryvote") score += 2 * decay;
    else if (rawType === "industrymember") score += 2 * decay;
    else if (rawType === "industryopinion") score += 1 * decay;
  }
  return Math.max(0, Math.round(score));
}

async function getCarbonGramsForUser(userId) {
  const ssb = await openSsb();
  if (!ssb || !userId) return 0;
  return new Promise((resolve) => {
    let bytes = 0;
    pull(
      ssb.createUserStream({ id: userId }),
      pull.drain(
        (m) => { try { bytes += Buffer.byteLength(JSON.stringify(m && m.value), 'utf8'); } catch (_) {} },
        () => resolve((bytes / (1024 * 1024)) * 0.095)
      )
    );
  });
}

async function getBytesForUser(userId) {
  const ssb = await openSsb();
  if (!ssb || !userId) return 0;
  return new Promise((resolve) => {
    let bytes = 0;
    pull(
      ssb.createUserStream({ id: userId }),
      pull.drain(
        (m) => { try { bytes += Buffer.byteLength(JSON.stringify(m && m.value), 'utf8'); } catch (_) {} },
        () => resolve(bytes)
      )
    );
  });
}

let _ecoTaxStatsCache = null;
const ECO_TAX_STATS_TTL_MS = 60 * 1000;

async function calculateEcoTaxStatsInternal() {
  const ssb = await (async () => {
    try { return await openSsb(); } catch (_) { return null; }
  })();
  if (!ssb) return null;
  return new Promise((resolve) => {
    let totalBytes = 0;
    let totalBlocks = 0;
    let maxBlockBytes = 0;
    let oldestTs = Infinity;
    let newestTs = 0;
    pull(
      ssb.createLogStream({}),
      pull.drain(
        (m) => {
          try {
            const v = m && m.value;
            if (!v) return;
            totalBlocks += 1;
            const size = Buffer.byteLength(JSON.stringify(v), 'utf8');
            totalBytes += size;
            if (size > maxBlockBytes) maxBlockBytes = size;
            const ts = Number(v.timestamp || 0);
            if (ts && ts < oldestTs) oldestTs = ts;
            if (ts && ts > newestTs) newestTs = ts;
          } catch (_) {}
        },
        () => {
          const totalGramsCO2 = carbonGramsFromBytes(totalBytes);
          const totalEcoinTax = ecoinTaxFromGrams(totalGramsCO2);
          const spanMs = (newestTs && oldestTs && oldestTs !== Infinity) ? Math.max(1, newestTs - oldestTs) : 1;
          const spanDays = spanMs / 86400000;
          const annualEcoinTax = spanDays > 0 ? totalEcoinTax * (365 / spanDays) : totalEcoinTax;
          const monthlyEcoinTax = annualEcoinTax / 12;
          try { sharedState.setMaxBlockBytes(maxBlockBytes); } catch (_) {}
          const ecoTaxes = {
            lifetime: Number(totalEcoinTax.toFixed(6)),
            annual: Number(annualEcoinTax.toFixed(6)),
            monthly: Number(monthlyEcoinTax.toFixed(6))
          };
          const archLifetime = archTaxFromDays(spanDays);
          const archAnnual = archTaxFromDays(365);
          const archMonthly = archTaxFromDays(365 / 12);
          const archTaxes = {
            lifetime: Number(archLifetime.toFixed(6)),
            annual: Number(archAnnual.toFixed(6)),
            monthly: Number(archMonthly.toFixed(6))
          };
          const byType = { eco: ecoTaxes, arch: archTaxes };
          const totals = {
            lifetimeEcoinTax: Number(Object.values(byType).reduce((s, t) => s + (t.lifetime || 0), 0).toFixed(6)),
            annualEcoinTax: Number(Object.values(byType).reduce((s, t) => s + (t.annual || 0), 0).toFixed(6)),
            monthlyEcoinTax: Number(Object.values(byType).reduce((s, t) => s + (t.monthly || 0), 0).toFixed(6))
          };
          resolve({
            totalBlocks,
            totalBytes,
            maxBlockBytes,
            totalGramsCO2: Number(totalGramsCO2.toFixed(6)),
            totalEcoinTax: ecoTaxes.lifetime,
            annualEcoinTax: ecoTaxes.annual,
            monthlyEcoinTax: ecoTaxes.monthly,
            byType,
            totals,
            spanDays: Number(spanDays.toFixed(3)),
            oldestTs: oldestTs === Infinity ? 0 : oldestTs,
            newestTs,
            ecoinPerGramCO2: ECOIN_PER_GRAM_CO2,
            ecoinPerDayOfHistory: ECOIN_PER_DAY_OF_HISTORY
          });
        }
      )
    );
  });
}

async function calculateEcoTaxStats() {
  const now = Date.now();
  if (_ecoTaxStatsCache && (now - _ecoTaxStatsCache.ts) < ECO_TAX_STATS_TTL_MS) {
    return _ecoTaxStatsCache.value;
  }
  const value = await calculateEcoTaxStatsInternal().catch(() => null);
  if (value) _ecoTaxStatsCache = { ts: now, value };
  return value || {
    totalBlocks: 0, totalBytes: 0, totalGramsCO2: 0,
    totalEcoinTax: 0, annualEcoinTax: 0, monthlyEcoinTax: 0,
    byType: {
      eco: { lifetime: 0, annual: 0, monthly: 0 },
      arch: { lifetime: 0, annual: 0, monthly: 0 }
    },
    totals: { lifetimeEcoinTax: 0, annualEcoinTax: 0, monthlyEcoinTax: 0 },
    spanDays: 0, oldestTs: 0, newestTs: 0,
    ecoinPerGramCO2: ECOIN_PER_GRAM_CO2,
    ecoinPerDayOfHistory: ECOIN_PER_DAY_OF_HISTORY
  };
}

async function getUserEcoinTax(userId) {
  const bytes = await getBytesForUser(userId).catch(() => 0);
  return ecoinTaxFromBytes(bytes);
}

async function getUserFirstBlockTs(userId) {
  const ssb = await openSsb();
  if (!ssb || !userId) return 0;
  return new Promise((resolve) => {
    let first = 0;
    pull(
      ssb.createUserStream({ id: userId, limit: 1 }),
      pull.drain(
        (m) => { if (m && m.value && m.value.timestamp && !first) first = Number(m.value.timestamp); },
        () => resolve(first || 0)
      )
    );
  });
}

async function getUserArchTax(userId) {
  const firstTs = await getUserFirstBlockTs(userId).catch(() => 0);
  if (!firstTs) return 0;
  const stats = await calculateEcoTaxStats().catch(() => null);
  const newestTs = stats && Number.isFinite(stats.newestTs) && stats.newestTs > 0
    ? stats.newestTs
    : Date.now();
  const ageDays = Math.max(0, (newestTs - firstTs) / ONE_DAY_MS);
  return archTaxFromDays(ageDays);
}

let _industryModel = null;
function industryModel() {
  if (_industryModel) return _industryModel;
  if (!services || !services.cooler) return null;
  try { _industryModel = require("./industry_model")({ cooler: services.cooler }); } catch (_) { _industryModel = null; }
  return _industryModel;
}

async function getIndustryBalance(userId) {
  const uid = resolveUserId(userId);
  const industry = industryModel();
  if (!industry) return { received: 0, sent: 0, net: 0, networkTotal: 0 };
  const builds = await industry.listAllBuilds().catch(() => []);
  let production = 0;
  let earned = 0;
  for (const b of (builds || [])) {
    const est = parseFloat(b && b.estTotal);
    if (Number.isFinite(est) && est > 0) production += est;
    const mine = parseFloat(b && b.points && b.points[uid]);
    if (Number.isFinite(mine) && mine > 0) earned += mine;
  }
  return {
    received: Number(earned.toFixed(6)),
    sent: 0,
    net: Number(earned.toFixed(6)),
    networkTotal: Number(production.toFixed(6))
  };
}

async function getUserEngagementScore(userId) {
  const ssb = await openSsb();
  const uid = resolveUserId(userId);
  const actions = await fetchUserActions(uid);
  const rawKarma = scoreFromActions(actions);
  const carbonGrams = await getCarbonGramsForUser(uid).catch(() => 0);
  const karmaScore = Math.max(0, Math.round(rawKarma - carbonGrams));

  const prev = await getLastKarmaScore(uid);
  const lastPublishedTimestamp = await getLastPublishedTimestamp(uid);

  const isSelf = idsEqual(uid, ssb.id);
  const hasSSB = !!(ssb && ssb.publish);

  const changed = (prev === null) || (karmaScore !== prev);
  const nowMs = Date.now();
  const lastMs = lastPublishedTimestamp ? new Date(lastPublishedTimestamp).getTime() : 0;
  const cooldownOk = (nowMs - lastMs) >= 24 * 60 * 60 * 1000;

  if (isSelf && hasSSB && changed && cooldownOk) {
    await publishKarmaScore(uid, karmaScore);
  }
  return karmaScore;
}

async function getLastKarmaScore(userId) {
  const ssb = await openSsb();
  if (!ssb) return null;
  return new Promise((resolve) => {
    const source = ssb.messagesByType
      ? ssb.messagesByType({ type: "karmaScore", reverse: true })
      : ssb.createLogStream && ssb.createLogStream({ reverse: true });
    if (!source) return resolve(null);
    pull(
      source,
      pull.filter(msg => {
        const v = msg.value || msg;
        const c = v.content || {};
        return c && c.type === "karmaScore" && c.userId === userId;
      }),
      pull.take(1),
      pull.collect((err, arr) => {
        if (err || !arr || !arr.length) return resolve(null);
        const v = arr[0].value || arr[0];
        const c = v.content || {};
        resolve(Number(c.karmaScore) || 0);
      })
    );
  });
}

async function getLastPublishedTimestamp(userId) {
  const ssb = await openSsb();
  if (!ssb) return new Date(0).toISOString();
  const fallback = new Date(0).toISOString();
  return new Promise((resolve) => {
    const source = ssb.messagesByType
      ? ssb.messagesByType({ type: "karmaScore", reverse: true })
      : ssb.createLogStream && ssb.createLogStream({ reverse: true });
    if (!source) return resolve(fallback);
    pull(
      source,
      pull.filter(msg => {
        const v = msg.value || msg;
        const c = v.content || {};
        return c && c.type === "karmaScore" && c.userId === userId;
      }),
      pull.take(1),
      pull.collect((err, arr) => {
        if (err || !arr || !arr.length) return resolve(fallback);
        const v = arr[0].value || arr[0];
        const c = v.content || {};
        resolve(c.timestamp || fallback);
      })
    );
  });
}

  function computePoolVars(pubBal, rules) {
    const alphaCap = (rules.alpha ?? DEFAULT_RULES.alpha) * pubBal;
    const available = Math.max(0, pubBal - (rules.reserveMin ?? DEFAULT_RULES.reserveMin));
    const rawMin = Math.min(available, (rules.capPerEpoch ?? DEFAULT_RULES.capPerEpoch), alphaCap);
    const pool = clamp(rawMin, 0, Number.MAX_SAFE_INTEGER);
    return { pubBal, alphaCap, available, rawMin, pool };
  }

  async function computeEpoch({ epochId, userId, rules = DEFAULT_RULES }) {
    const pubBal = await safeGetBalance("pub");
    const pv = computePoolVars(pubBal, rules);
    const addresses = await listAddressesMerged();
    const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address));
    const capUser = rules.caps?.cap_user_epoch ?? DEFAULT_RULES.caps.cap_user_epoch;
    const wMin = rules.caps?.w_min ?? DEFAULT_RULES.caps.w_min;
    const wMax = rules.caps?.w_max ?? DEFAULT_RULES.caps.w_max;
    const floorUbi = rules.caps?.floor_user ?? DEFAULT_RULES.caps.floor_user ?? 1;
    const weights = [];
    for (const entry of eligible) {
      const score = await getUserEngagementScore(entry.id);
      weights.push({ user: entry.id, w: clamp(1 + score / 100, wMin, wMax) });
    }
    if (!weights.length && userId) {
      const score = await getUserEngagementScore(userId);
      weights.push({ user: userId, w: clamp(1 + score / 100, wMin, wMax) });
    }
    const W = weights.reduce((acc, x) => acc + x.w, 0) || 1;
    const allocations = [];
    for (const { user, w } of weights) {
      const gross = Math.max(floorUbi, Math.min(pv.pool * w / W, capUser));
      const surplus = Math.max(0, gross - floorUbi);
      const userEcoTax = await getUserEcoinTax(user).catch(() => 0);
      const userArchTax = await getUserArchTax(user).catch(() => 0);
      const userTotalTax = userEcoTax + userArchTax;
      const taxedSurplus = Math.max(0, surplus - userTotalTax);
      const amount = floorUbi + taxedSurplus;
      allocations.push({
        id: `alloc:${epochId}:${user}`,
        epoch: epochId,
        user,
        weight: Number(w.toFixed(6)),
        gross: Number(gross.toFixed(6)),
        ecoTax: Number((userEcoTax || 0).toFixed(6)),
        archTax: Number((userArchTax || 0).toFixed(6)),
        totalTax: Number((userTotalTax || 0).toFixed(6)),
        amount: Number(amount.toFixed(6))
      });
    }
    const snapshot = JSON.stringify({ epochId, pool: pv.pool, weights, allocations, rules }, null, 2);
    const hash = crypto.createHash("sha256").update(snapshot).digest("hex");
    return { epoch: { id: epochId, pool: Number(pv.pool.toFixed(6)), weightsSum: Number(W.toFixed(6)), rules, hash }, allocations };
  }

  async function executeEpoch({ epochId, rules = DEFAULT_RULES } = {}) {
    const eid = epochId || epochIdNow();
    await expireOldAllocations();
    const existing = await epochsRepo.get(eid);
    if (existing) return { epoch: existing, allocations: await transfersRepo.listByTag(`epoch:${eid}`) };
    const { epoch, allocations } = await computeEpoch({ epochId: eid, userId: config.keys.id, rules });
    await epochsRepo.save(epoch);
    for (const a of allocations) {
      if (a.amount <= 0) continue;
      const record = {
        id: a.id,
        from: config.keys.id,
        to: a.user,
        amount: a.amount,
        concept: `UBI ${eid}`,
        status: "UNCLAIMED",
        createdAt: new Date().toISOString(),
        deadline: new Date(Date.now() + ((rules.graceDays ?? DEFAULT_RULES.graceDays) * 86400000)).toISOString(),
        tags: ["UBI", `epoch:${eid}`],
        opinions: {}
      };
      await transfersRepo.create(record);
      try { await publishUbiAllocation(record); } catch (_) {}
    }
    return { epoch, allocations };
  }

  async function publishBankClaim({ amount, epochId, allocationId, txid }) {
    const ssbClient = await openSsb();
    const content = { type: "bankClaim", amount, epochId, allocationId, txid, timestamp: Date.now() };
    return new Promise((resolve, reject) => ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res)));
  }

  async function claimAllocation({ transferId, claimerId, forcePub = false }) {
    const allocation = await transfersRepo.findById(transferId);
    if (!allocation || (allocation.status !== "UNCLAIMED" && allocation.status !== "UNCONFIRMED")) throw new Error("Invalid allocation or already claimed.");
    if (claimerId && allocation.to !== claimerId) throw new Error("This allocation is not for you.");
    const addr = await getUserAddress(allocation.to);
    if (!addr || !isValidEcoinAddress(addr)) throw new Error("No valid ECOin address registered.");
    const txid = await rpcCall("sendtoaddress", [addr, allocation.amount, `UBI ${allocation.concept || "claim"}`], "pub");
    if (!txid) throw new Error("RPC sendtoaddress failed. Check PUB wallet configuration.");
    await transfersRepo.markClosed(transferId, txid);
    return { txid };
  }

  async function claimUBI(userId) {
    const uid = resolveUserId(userId);
    const epochId = epochIdNow();
    const pubId = getConfiguredPubId();
    if (!pubId) throw new Error("no_pub_configured");
    const alreadyClaimed = await hasClaimedThisMonth(uid);
    if (alreadyClaimed) throw new Error("already_claimed");
    const ssb = await openSsb();
    if (!ssb || !ssb.publish) throw new Error("ssb_unavailable");
    const now = new Date().toISOString();
    const claimContent = { type: "ubiClaim", pubId, epochId, claimedAt: now };
    await new Promise((resolve, reject) => ssb.publish(claimContent, (err, res) => err ? reject(err) : resolve(res)));
    return { status: "claimed_pending", epochId };
  }

  async function updateAllocationStatus(allocationId, status, txid) {
    if (status === "CLOSED") {
      await transfersRepo.markClosed(allocationId, txid);
      return;
    }
    ensureStoreFiles();
    const all = readJson(TRANSFERS_PATH, []);
    const idx = all.findIndex(t => t.id === allocationId);
    if (idx >= 0) {
      all[idx].status = status;
      if (txid) all[idx].txid = txid;
      writeJson(TRANSFERS_PATH, all);
    }
  }

  async function hasClaimedThisMonth(userId) {
    const epochId = epochIdNow();
    const msgs = await scanLogStream();
    for (const m of msgs) {
      const v = m.value || {};
      const c = v.content || {};
      if (c.type === "ubiClaimResult" && c.userId === userId && c.epochId === epochId) return true;
      if (c.type === "ubiClaim" && v.author === userId && c.epochId === epochId) return true;
    }
    return false;
  }

  async function getUbiClaimHistory(userId) {
    const msgs = await scanLogStream();
    let lastClaimedDate = null;
    let totalClaimed = 0;
    let claimCount = 0;
    for (const m of msgs) {
      const v = m.value || {};
      const c = v.content || {};
      if (c.type === "ubiClaimResult" && c.userId === userId) {
        totalClaimed += Number(c.amount) || 0;
        claimCount += 1;
        const d = c.processedAt || null;
        if (d && (!lastClaimedDate || d > lastClaimedDate)) lastClaimedDate = d;
      }
    }
    return { lastClaimedDate, totalClaimed: Number(totalClaimed.toFixed(6)), claimCount };
  }

  async function getUbiAllocationsFromSSB() {
    const pubId = getConfiguredPubId();
    if (!pubId) return [];
    const msgs = await scanLogStream();
    const out = [];
    for (const m of msgs) {
      const v = m.value || {};
      const c = v.content || {};
      if (v.author === pubId && c && c.type === "ubiAllocation") {
        out.push({
          id: c.allocationId,
          from: pubId,
          to: c.to,
          amount: c.amount,
          concept: c.concept,
          epochId: c.epochId,
          status: c.status || "UNCLAIMED",
          createdAt: c.createdAt || new Date().toISOString()
        });
      }
    }
    return out;
  }

  async function publishPubAvailability() {
    if (!isPubNode()) return;
    const balance = await safeGetBalance("pub");
    const floor = Math.max(1, DEFAULT_RULES?.caps?.floor_user ?? 1);
    const available = Number(balance) >= floor;
    const ssb = await openSsb();
    if (!ssb || !ssb.publish) return;
    const content = { type: "pubAvailability", available, coin: "ECO", timestamp: Date.now() };
    await new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res)));
    return available;
  }

  async function getPubAvailabilityFromSSB() {
    const pubId = getConfiguredPubId();
    if (!pubId) return false;
    const msgs = await scanLogStream();
    let latest = null;
    for (const m of msgs) {
      const v = m.value || {};
      const c = v.content || {};
      if (v.author === pubId && c && c.type === "pubAvailability" && c.coin === "ECO") {
        if (!latest || (Number(c.timestamp) || 0) > (Number(latest.timestamp) || 0)) latest = c;
      }
    }
    return !!(latest && latest.available);
  }

  async function listBanking(filter = "overview", userId) {
    const uid = resolveUserId(userId);
    const epochId = epochIdNow();
    let pubBalance = 0;
    let ubiAvailable = false;
    let allocations;
    if (isPubNode()) {
      pubBalance = await safeGetBalance("pub");
      const floor = Math.max(1, DEFAULT_RULES?.caps?.floor_user ?? 1);
      ubiAvailable = Number(pubBalance) >= floor;
      try { await publishPubAvailability(); } catch (_) {}
      const all = await transfersRepo.listByTag("UBI");
      allocations = all.map(t => ({
        id: t.id, concept: t.concept, from: t.from, to: t.to, amount: t.amount, status: t.status,
        createdAt: t.createdAt || t.deadline || new Date().toISOString(), txid: t.txid
      }));
    } else {
      ubiAvailable = await getPubAvailabilityFromSSB();
      allocations = await getUbiAllocationsFromSSB();
    }
    const userBalance = await safeGetBalance("user");
    const industryBal = await getIndustryBalance(uid).catch(() => ({ received: 0, sent: 0, net: 0, networkTotal: 0 }));
    const epochs = await epochsRepo.list();
    let computed = null;
    try { computed = await computeEpoch({ epochId, userId: uid, rules: DEFAULT_RULES }); } catch {}
    const pv = computePoolVars(pubBalance, DEFAULT_RULES);
    const actions = await fetchUserActions(uid);
    const rawScore = scoreFromActions(actions);
    const carbonGramsForScore = await getCarbonGramsForUser(uid).catch(() => 0);
    const engagementScore = Math.max(0, Math.round(rawScore - carbonGramsForScore));
    const poolForEpoch = computed?.epoch?.pool || pv.pool || 0;
    const futureUBI = Number(((engagementScore / 100) * poolForEpoch).toFixed(6));
    const addresses = await listAddressesMerged();
    const alreadyClaimed = await hasClaimedThisMonth(uid);
    const pubId = getConfiguredPubId();
    const userAddress = await getUserAddress(uid);
    const userWalletCfg = getWalletCfg("user") || {};
    const hasValidWallet = !!(userAddress && isValidEcoinAddress(userAddress) && userWalletCfg.url);
    const summary = {
      userBalance,
      industryBalance: industryBal.net,
      industryNetworkTotal: industryBal.networkTotal,
      industryReceived: industryBal.received,
      industrySent: industryBal.sent,
      epochId,
      pool: poolForEpoch,
      weightsSum: computed?.epoch?.weightsSum || 0,
      userEngagementScore: engagementScore,
      futureUBI,
      alreadyClaimed,
      pubId,
      hasValidWallet,
      ubiAvailability: ubiAvailable ? "OK" : "NO_FUNDS"
    };
    const exchange = await calculateEcoinValue();
    const exchangeHistory = readEcoHistory();
    let taxStats = null;
    let userEcoinTax = 0;
    if (filter === 'taxes' || filter === 'exchange' || filter === 'overview') {
      try { taxStats = await calculateEcoTaxStats(); } catch (_) { taxStats = null; }
      try { userEcoinTax = await getUserEcoinTax(uid); } catch (_) { userEcoinTax = 0; }
    }
    const taxRules = { ecoinPerGramCO2: ECOIN_PER_GRAM_CO2, gramsCO2PerMiB: 0.095, ecoinPerDayOfHistory: ECOIN_PER_DAY_OF_HISTORY };
    let userArchTax = 0;
    if (filter === 'taxes' || filter === 'exchange' || filter === 'overview') {
      try { userArchTax = await getUserArchTax(uid); } catch (_) { userArchTax = 0; }
    }
    const userTotalTax = (userEcoinTax || 0) + (userArchTax || 0);
    return {
      summary, allocations, epochs, rules: DEFAULT_RULES, taxRules, addresses, exchange, exchangeHistory, taxStats,
      userEcoinTax: Number((userEcoinTax || 0).toFixed(6)),
      userArchTax: Number((userArchTax || 0).toFixed(6)),
      userTotalTax: Number(userTotalTax.toFixed(6))
    };
  }

  async function getAllocationById(id) {
    const t = await transfersRepo.findById(id);
    if (!t) return null;
    return { id: t.id, concept: t.concept, from: t.from, to: t.to, amount: t.amount, status: t.status, createdAt: t.createdAt || new Date().toISOString(), txid: t.txid };
  }

  async function getEpochById(id) {
    const existing = await epochsRepo.get(id);
    if (existing) return existing;
    const all = await transfersRepo.listAll();
    const filtered = all.filter(t => (t.tags || []).includes(`epoch:${id}`));
    const pool = filtered.reduce((s, t) => s + Number(t.amount || 0), 0);
    return { id, pool, weightsSum: 0, rules: DEFAULT_RULES, hash: "-" };
  }

  async function listEpochAllocations(id) {
    const all = await transfersRepo.listAll();
    return all.filter(t => (t.tags || []).includes(`epoch:${id}`)).map(t => ({
      id: t.id, concept: t.concept, from: t.from, to: t.to, amount: t.amount, status: t.status, createdAt: t.createdAt || new Date().toISOString(), txid: t.txid
    }));
  }

  let genesisTimeCache = null;

  async function getAvgBlockSeconds(blocks) {
    if (!blocks || blocks < 2) return 0;
    try {
      if (!genesisTimeCache) {
        const h1 = await rpcCall("getblockhash", [1]);
        if (!h1) return 0;
        const b1 = await rpcCall("getblock", [h1]);
        genesisTimeCache = b1?.time || null;
        if (!genesisTimeCache) return 0;
      }
      const hCur = await rpcCall("getblockhash", [blocks]);
      if (!hCur) return 0;
      const bCur = await rpcCall("getblock", [hCur]);
      const curTime = bCur?.time || 0;
      if (!curTime) return 0;
      const elapsed = curTime - genesisTimeCache;
      return elapsed > 0 ? elapsed / (blocks - 1) : 0;
    } catch (_) { return 0; }
  }

  async function calculateEcoinValue() {
    const totalSupply = 25500000;
    let circulatingSupply = 0;
    let blocks = 0;
    let blockValueEco = 0;
    let isSynced = false;
    try {
      const info = await rpcCall("getinfo", []);
      circulatingSupply = info?.moneysupply || 0;
      blocks = info?.blocks || 0;
      isSynced = circulatingSupply > 0;
      const mining = await rpcCall("getmininginfo", []);
      blockValueEco = (mining?.blockvalue || 0) / 1e8;
    } catch (_) {}
    const avgSec = await getAvgBlockSeconds(blocks);
    const ecoValuePerHour = avgSec > 0 ? (3600 / avgSec) * blockValueEco : 0;
    const maturity = totalSupply > 0 ? circulatingSupply / totalSupply : 0;
    const ecoTimeMs = maturity * 3600 * 1000;
    const annualIssuance = ecoValuePerHour * 24 * 365;
    const inflationFactor = circulatingSupply > 0 ? (annualIssuance / circulatingSupply) * 100 : 0;
    const inflationMonthly = inflationFactor / 12;
    const result = {
      ecoValue: Number(ecoValuePerHour.toFixed(6)),
      ecoTimeMs: Number(ecoTimeMs.toFixed(3)),
      totalSupply,
      inflationFactor: Number(inflationFactor.toFixed(2)),
      inflationMonthly: Number(inflationMonthly.toFixed(2)),
      currentSupply: circulatingSupply,
      isSynced
    };
    if (isSynced) {
      appendEcoHistory({
        ts: Date.now(),
        ecoValue: result.ecoValue,
        currentSupply: result.currentSupply,
        inflationFactor: result.inflationFactor,
        blockValueEco: Number(blockValueEco.toFixed(8)),
        avgBlockSec: Number((avgSec || 0).toFixed(2)),
        blocks
      });
    }
    return result;
  }

  async function getBankingData(userId) {
    const ecoValue = await calculateEcoinValue();
    const karmaScore = await getUserEngagementScore(userId);
    let estimatedUBI = 0;
    try {
      const pubBal = isPubNode() ? await safeGetBalance("pub") : 0;
      const pv = computePoolVars(pubBal, DEFAULT_RULES);
      const pool = pv.pool || 0;
      const addresses = await listAddressesMerged();
      const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address));
      const wMin = DEFAULT_RULES.caps?.w_min ?? 0.2;
      const wMax = DEFAULT_RULES.caps?.w_max ?? 6;
      const userW = clamp(1 + karmaScore / 100, wMin, wMax);
      const otherUsers = Math.max(0, eligible.length - 1);
      const totalW = Math.max(1, userW + otherUsers);
      const cap = DEFAULT_RULES.caps?.cap_user_epoch ?? 50;
      const floor = DEFAULT_RULES.caps?.floor_user ?? 1;
      estimatedUBI = eligible.length > 0
        ? Math.max(floor, Math.min(pool * (userW / totalW), cap))
        : 0;
    } catch (_) {}
    const uid = resolveUserId(userId);
    const userEcoinTax = await getUserEcoinTax(uid).catch(() => 0);
    const userArchTax = await getUserArchTax(uid).catch(() => 0);
    const userTotalTax = userEcoinTax + userArchTax;
    const estimatedUBIBeforeTax = estimatedUBI;
    const baseFloor = DEFAULT_RULES.caps?.floor_user ?? 1;
    const surplus = Math.max(0, estimatedUBI - baseFloor);
    estimatedUBI = baseFloor + Math.max(0, surplus - userTotalTax);
    const claimHistory = await getUbiClaimHistory(userId).catch(() => ({ lastClaimedDate: null, totalClaimed: 0 }));
    return {
      ecoValue,
      karmaScore,
      estimatedUBI,
      estimatedUBIBeforeTax: Number(estimatedUBIBeforeTax.toFixed(6)),
      userEcoinTax: Number(userEcoinTax.toFixed(6)),
      userArchTax: Number(userArchTax.toFixed(6)),
      userTotalTax: Number(userTotalTax.toFixed(6)),
      lastClaimedDate: claimHistory.lastClaimedDate,
      totalClaimed: claimHistory.totalClaimed
    };
  }

  async function expireOldAllocations() {
    const cutoffMs = MAX_PENDING_EPOCHS * 30 * 86400000;
    const now = Date.now();
    const allocs = await transfersRepo.listAll();
    for (const a of allocs) {
      if ((a.status === "UNCLAIMED" || a.status === "UNCONFIRMED") &&
          (now - new Date(a.createdAt).getTime()) > cutoffMs) {
        await updateAllocationStatus(a.id, "EXPIRED");
      }
    }
  }

  async function publishUbiAllocation(allocation) {
    const ssb = await openSsb();
    if (!ssb) return;
    const epochTag = (allocation.tags || []).find(t => t.startsWith("epoch:"));
    const content = {
      type: "ubiAllocation",
      allocationId: allocation.id,
      to: allocation.to,
      amount: allocation.amount,
      concept: allocation.concept,
      epochId: epochTag ? epochTag.slice(6) : "",
      status: "UNCLAIMED",
      createdAt: allocation.createdAt
    };
    return new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res)));
  }

  async function publishUbiClaim(allocationId, epochId) {
    const ssb = await openSsb();
    if (!ssb) return;
    const content = { type: "ubiClaim", allocationId, epochId, claimedAt: new Date().toISOString() };
    return new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res)));
  }

  async function publishUbiClaimResult(allocationId, epochId, txid, userId, amount) {
    const ssb = await openSsb();
    if (!ssb) return;
    const content = { type: "ubiClaimResult", allocationId, epochId, txid, userId, amount, processedAt: new Date().toISOString() };
    return new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res)));
  }

  async function processPendingClaims() {
    if (!isPubNode()) return;
    const ssb = await openSsb();
    if (!ssb) return;
    const claims = [];
    const results = [];
    await new Promise((resolve, reject) => {
      pull(ssb.messagesByType({ type: "ubiClaim", reverse: false }),
        pull.drain(msg => {
          if (msg.value?.content?.type === "ubiClaim") {
            claims.push({ ...msg.value.content, _author: msg.value.author });
          }
        },
          err => err ? reject(err) : resolve()));
    });
    await new Promise((resolve, reject) => {
      pull(ssb.messagesByType({ type: "ubiClaimResult", reverse: false }),
        pull.drain(msg => { if (msg.value?.content?.type === "ubiClaimResult") results.push(msg.value.content); },
          err => err ? reject(err) : resolve()));
    });
    const processedEpochUser = new Set(results.map(r => `${r.epochId}:${r.userId}`));
    const epochId = epochIdNow();
    for (const claim of claims) {
      const claimantId = claim._author;
      if (!claimantId) continue;
      const claimEpoch = claim.epochId || epochId;
      if (processedEpochUser.has(`${claimEpoch}:${claimantId}`)) continue;
      try {
        const addr = await getUserAddress(claimantId);
        if (!addr || !isValidEcoinAddress(addr)) continue;
        const pubBal = await safeGetBalance("pub");
        if (pubBal <= 0) continue;
        const pv = computePoolVars(pubBal, DEFAULT_RULES);
        const addresses = await listAddressesMerged();
        const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address));
        const karmaScore = await getUserEngagementScore(claimantId);
        const wMin = DEFAULT_RULES.caps.w_min;
        const wMax = DEFAULT_RULES.caps.w_max;
        const capUser = DEFAULT_RULES.caps.cap_user_epoch;
        const userW = clamp(1 + karmaScore / 100, wMin, wMax);
        const totalW = eligible.reduce((acc) => acc + clamp(1, wMin, wMax), 0) || 1;
        const amount = Number(Math.max(1, Math.min(pv.pool * userW / totalW, capUser)).toFixed(6));
        const txid = await rpcCall("sendtoaddress", [addr, amount, `UBI ${claimEpoch}`], "pub");
        if (!txid) continue;
        await publishUbiClaimResult(claim.allocationId || `claim:${claimEpoch}:${claimantId}`, claimEpoch, txid, claimantId, amount);
        await publishBankClaim({ amount, epochId: claimEpoch, allocationId: claim.allocationId || `claim:${claimEpoch}:${claimantId}`, txid });
        const now = new Date().toISOString();
        await new Promise((resolve, reject) => ssb.publish({
          type: "transfer",
          from: config.keys.id,
          to: claimantId,
          concept: `UBI ${claimEpoch} ${claimantId}`,
          amount: String(amount),
          createdAt: now,
          updatedAt: now,
          deadline: null,
          confirmedBy: [config.keys.id],
          status: "UNCONFIRMED",
          tags: ["UBI"],
          opinions: {},
          opinions_inhabitants: [],
          txid
        }, (err, msg) => err ? reject(err) : resolve(msg)));
      } catch (_) {}
    }
  }

  return {
    DEFAULT_RULES,
    isPubNode,
    getConfiguredPubId,
    computeEpoch,
    executeEpoch,
    getUserEngagementScore,
    publishBankClaim,
    publishUbiAllocation,
    publishUbiClaim,
    publishUbiClaimResult,
    publishPubAvailability,
    getPubAvailabilityFromSSB,
    hasClaimedThisMonth,
    getUbiClaimHistory,
    claimUBI,
    processPendingClaims,
    expireOldAllocations,
    claimAllocation,
    listBanking,
    getAllocationById,
    getEpochById,
    listEpochAllocations,
    addAddress,
    removeAddress,
    ensureSelfAddressPublished,
    getUserAddress,
    setUserAddress,
    listAddressesMerged,
    calculateEcoinValue,
    calculateEcoTaxStats,
    getUserEcoinTax,
    getUserArchTax,
    getUserFirstBlockTs,
    getBankingData,
    ECOIN_PER_GRAM_CO2,
    ECOIN_PER_DAY_OF_HISTORY
  };
};
