/**
 * Equal-hub mesh: persist known hub baseUrls and fan-out new blocks/txs.
 * Peers are learned from directory /v1/coordinators and coordinator-announce — no hardcoded seeds.
 */
const fs = require("fs");
const path = require("path");
const { URL } = require("url");
const { dataPath } = require("./data_dir");

const STORE = dataPath("hub_peers.json");
const GOSSIP_HEADER = "x-sarmad-hub-gossip";
const DEFAULT_TTL_MS = 30 * 60 * 1000;

/** @type {Map<string, { baseUrl: string, address: string, lastSeenMs: number, source: string }>} */
const peers = new Map();

function normalizeBase(url) {
  return String(url || "")
    .trim()
    .replace(/\/$/, "");
}

function load() {
  try {
    const raw = JSON.parse(fs.readFileSync(STORE, "utf8"));
    const list = Array.isArray(raw) ? raw : raw.peers || [];
    const now = Date.now();
    for (const row of list) {
      const baseUrl = normalizeBase(row.baseUrl || row.url);
      if (!baseUrl || !/^https?:\/\//i.test(baseUrl)) continue;
      peers.set(baseUrl, {
        baseUrl,
        address: String(row.address || ""),
        lastSeenMs: Number(row.lastSeenMs) || now,
        source: String(row.source || "file"),
      });
    }
  } catch (_) {}
}

function save() {
  const list = [...peers.values()].map((p) => ({
    baseUrl: p.baseUrl,
    address: p.address || "",
    lastSeenMs: p.lastSeenMs,
    source: p.source,
  }));
  try {
    // tmp+rename: a crash mid-write would otherwise leave an unparsable peer list.
    const tmp = `${STORE}.tmp-${process.pid}`;
    fs.writeFileSync(tmp, JSON.stringify({ peers: list }, null, 2), "utf8");
    fs.renameSync(tmp, STORE);
  } catch (_) {}
}

function rememberPeer(baseUrl, opts = {}) {
  const url = normalizeBase(baseUrl);
  if (!url || !/^https?:\/\//i.test(url)) return false;
  // Never peer with loopback — other hubs cannot reach it.
  try {
    const u = new URL(url);
    if (u.hostname === "127.0.0.1" || u.hostname === "localhost") return false;
  } catch (_) {
    return false;
  }
  const prev = peers.get(url);
  peers.set(url, {
    baseUrl: url,
    address: String(opts.address || (prev && prev.address) || ""),
    lastSeenMs: Date.now(),
    source: String(opts.source || (prev && prev.source) || "learn"),
  });
  save();
  return true;
}

function forgetStale(ttlMs = DEFAULT_TTL_MS) {
  const now = Date.now();
  let changed = false;
  for (const [url, row] of peers.entries()) {
    if (now - (row.lastSeenMs || 0) > ttlMs) {
      peers.delete(url);
      changed = true;
    }
  }
  if (changed) save();
}

function listPeers(selfBaseUrl, selfAddress) {
  forgetStale();
  const self = normalizeBase(selfBaseUrl);
  const selfAddr = String(selfAddress || "");
  return [...peers.values()]
    .filter((p) => {
      if (!p.baseUrl || p.baseUrl === self) return false;
      if (selfAddr && p.address && p.address === selfAddr) return false;
      return true;
    })
    .map((p) => p.baseUrl);
}

function isGossipRequest(req) {
  const v = req && req.headers && req.headers[GOSSIP_HEADER];
  return v === "1" || v === "true";
}

function postJson(baseUrl, pathname, bodyObj, headers = {}) {
  return new Promise((resolve) => {
    let url;
    try {
      url = new URL(pathname, baseUrl.endsWith("/") ? baseUrl : baseUrl + "/");
    } catch (_) {
      resolve({ ok: false, error: "bad url" });
      return;
    }
    const data = JSON.stringify(bodyObj);
    const lib = url.protocol === "https:" ? require("https") : require("http");
    const req = lib.request(
      {
        hostname: url.hostname,
        port: url.port || (url.protocol === "https:" ? 443 : 80),
        path: url.pathname + url.search,
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "Content-Length": Buffer.byteLength(data),
          [GOSSIP_HEADER]: "1",
          "x-sarmad-hub-from": normalizeBase(headers.fromBase || "") || undefined,
          ...headers,
        },
        timeout: 12_000,
      },
      (res) => {
        res.resume();
        resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode });
      }
    );
    req.on("error", (e) => resolve({ ok: false, error: e && e.message }));
    req.on("timeout", () => {
      req.destroy();
      resolve({ ok: false, error: "timeout" });
    });
    req.write(data);
    req.end();
  });
}

function getJson(baseUrl, pathname) {
  return new Promise((resolve) => {
    let url;
    try {
      url = new URL(pathname, baseUrl.endsWith("/") ? baseUrl : baseUrl + "/");
    } catch (_) {
      resolve(null);
      return;
    }
    const lib = url.protocol === "https:" ? require("https") : require("http");
    const req = lib.get(
      {
        hostname: url.hostname,
        port: url.port || (url.protocol === "https:" ? 443 : 80),
        path: url.pathname + url.search,
        timeout: 12_000,
      },
      (res) => {
        const chunks = [];
        res.on("data", (c) => chunks.push(c));
        res.on("end", () => {
          try {
            resolve(JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"));
          } catch (_) {
            resolve(null);
          }
        });
      }
    );
    req.on("error", () => resolve(null));
    req.on("timeout", () => {
      req.destroy();
      resolve(null);
    });
  });
}

/**
 * Learn peers from a directory /v1/coordinators response and remember the directory itself.
 */
async function refreshFromDirectory(directoryUrl) {
  const dir = normalizeBase(directoryUrl);
  if (!dir) return [];
  rememberPeer(dir, { source: "directory" });
  const json = await getJson(dir, "/v1/coordinators");
  const list = (json && json.coordinators) || [];
  for (const c of list) {
    if (c && c.baseUrl) rememberPeer(c.baseUrl, { address: c.address || "", source: "coordinators" });
  }
  return listPeers();
}

/**
 * Fan-out a newly stored block to all known hubs (fire-and-forget).
 */
function gossipBlock(selfBaseUrl, reportBody, log = console.log, selfAddress) {
  const targets = listPeers(selfBaseUrl, selfAddress);
  if (!targets.length) return;
  const body = {
    cycleId: reportBody.cycleId || 0,
    producerAddress: reportBody.producerAddress || "",
    blockHash: reportBody.blockHash,
    height: reportBody.height,
    tipTimestamp: reportBody.tipTimestamp,
    block: reportBody.block,
  };
  for (const base of targets) {
    postJson(base, "/v1/hub/block", body).then((r) => {
      if (!r.ok) {
        log(`[hub-gossip] block h=${body.height} → ${base} failed: ${r.status || r.error}`);
      }
    });
  }
}

function gossipTx(selfBaseUrl, tx, log = console.log, selfAddress) {
  const targets = listPeers(selfBaseUrl, selfAddress);
  if (!targets.length || !tx) return;
  for (const base of targets) {
    postJson(base, "/v1/hub/tx", { tx }).then((r) => {
      if (!r.ok) {
        log(`[hub-gossip] tx → ${base} failed: ${r.status || r.error}`);
      }
    });
  }
}

function gossipHeartbeat(selfBaseUrl, heartbeatBody, log = console.log, selfAddress) {
  const targets = listPeers(selfBaseUrl, selfAddress);
  if (!targets.length) return;
  for (const base of targets) {
    postJson(base, "/v1/hub/heartbeat", heartbeatBody).then((r) => {
      if (!r.ok) {
        log(`[hub-gossip] heartbeat → ${base} failed: ${r.status || r.error}`);
      }
    });
  }
}

function resetPeers() {
  peers.clear();
  try {
    if (fs.existsSync(STORE)) fs.unlinkSync(STORE);
  } catch (_) {}
}

load();

module.exports = {
  GOSSIP_HEADER,
  rememberPeer,
  listPeers,
  isGossipRequest,
  refreshFromDirectory,
  gossipBlock,
  gossipTx,
  gossipHeartbeat,
  resetPeers,
  peersSnapshot: () => [...peers.values()],
};

