// In-memory cache of the most recent relay-reported credit balance + tier,
// keyed by credit-key so each distinct (installId, license) pair has its
// own snapshot. Multi-mode introduces multiple identities in one Recap
// process (operator's pool + each paid user's license), and a single
// global snapshot would let one user's relay ping clobber another's
// cached view.
//
// Credit-key shape mirrors the relay's own keying (Path 3 license-keyed
// credits): `lic:<sha256(licenseKey).slice(0, 16)>` for paid licenses,
// `inst:<installId>` for unlicensed / Core. Stable per identity, doesn't
// require parsing the license payload — we hash the raw LIC1 string,
// which works as a local cache key independent of the relay's exact
// fingerprint formula.
//
// Not persisted to disk — relay is the source of truth. On a fresh boot
// the map is empty until each request triggers its first probe.

import { createHash } from "crypto";

// Map<creditKey, snapshot>. snapshot shape:
//   { creditsRemaining, tier, lastUpdated, lastError }
const snapshots = new Map();

// LRU-ish defensive cap so a worst-case (e.g. a buggy probe spinning up
// thousands of distinct identities) can't grow the map unbounded. In
// normal use we expect on the order of (1 operator + N paid users)
// rows, well under this ceiling.
const MAX_SNAPSHOTS = 5000;

const EMPTY_SNAPSHOT = Object.freeze({
  creditsRemaining: null,
  tier: null,
  lastUpdated: null,
  lastError: null,
});

// computeCreditKey({ installId, licenseKey, userId }) — derives the stable
// per-identity key used by both the provider (when recording responses)
// and the /api/relay/status handler (when looking up the right snapshot
// for the request). Priority: cloud userId (core-decoupling `user:<id>`)
// → license (hashed raw key) → install_id. Returns null if all three are
// missing (caller should treat as "no cache, never hit" and skip the
// lookup).
export function computeCreditKey({ installId, licenseKey, userId } = {}) {
  // Cloud (core-decoupling) identity: keyed by the Recaps account id, to
  // mirror the relay's `user:<id>` pool.
  const uid = (userId || "").trim();
  if (uid) return "user:" + uid;
  const lic = (licenseKey || "").trim();
  if (lic) {
    return "lic:" + createHash("sha256").update(lic).digest("hex").slice(0, 16);
  }
  const id = (installId || "").trim();
  if (id) return "inst:" + id;
  return null;
}

function getOrCreate(creditKey) {
  let s = snapshots.get(creditKey);
  if (!s) {
    if (snapshots.size >= MAX_SNAPSHOTS) {
      // Evict the oldest entry (Map iteration is insertion-ordered).
      const oldest = snapshots.keys().next().value;
      if (oldest != null) snapshots.delete(oldest);
    }
    s = { creditsRemaining: null, tier: null, lastUpdated: null, lastError: null };
    snapshots.set(creditKey, s);
  }
  return s;
}

// Called by the relay provider on every response (including 4xx with the
// standard envelope). `envelope` is the parsed JSON shape:
//   { credits_remaining, tier, ... }
// `creditKey` is the per-identity key from computeCreditKey(); required
// in multi-mode, optional in single-mode (falls through to a synthetic
// "operator" bucket — see fallback below).
export function updateRelayState(envelope, creditKey) {
  if (!envelope || typeof envelope !== "object") return;
  const key = creditKey || "operator";
  const s = getOrCreate(key);
  if (typeof envelope.credits_remaining === "number") {
    s.creditsRemaining = envelope.credits_remaining;
  }
  if (typeof envelope.tier === "string") {
    s.tier = envelope.tier;
  }
  s.lastUpdated = Date.now();
  s.lastError = null;
}

// Record a relay error (network failure, 5xx without envelope, etc.).
// Same keying as updateRelayState — the error sticks to the identity
// that hit it, so an admin's tile doesn't flash "relay unreachable"
// because a tenant's probe failed.
export function recordRelayError(message, creditKey) {
  const key = creditKey || "operator";
  const s = getOrCreate(key);
  s.lastError = (message || "Unknown relay error").slice(0, 300);
  s.lastUpdated = Date.now();
}

// getRelayState(creditKey) — fetch the snapshot for an identity. Returns
// the EMPTY_SNAPSHOT (with nulls) if the identity has never recorded
// anything yet. Caller can branch on creditsRemaining === null vs
// numeric to decide whether to probe.
export function getRelayState(creditKey) {
  const key = creditKey || "operator";
  const s = snapshots.get(key);
  return s ? { ...s } : { ...EMPTY_SNAPSHOT };
}

// Test/teardown helper. Clears every cached snapshot. Used by tests + a
// debugging admin endpoint if we ever need one. Not used in production.
export function resetRelayState() {
  snapshots.clear();
}
