const fs = require("fs");

// Local ROA Store — validates prefixes without RIPE Stat API calls.
// Parses ~400k ROAs from the same Cloudflare RPKI feed used for ASPA.
// Uses sorted arrays + binary search for O(log n) lookups (~0.1ms per prefix)
const roaStore = {
  v4Entries: [],    // [{start, end, asn, prefixLen, maxLen}] sorted by start
  v6Entries: [],    // [{prefixHex, prefixLen, asn, maxLen}] sorted by prefixHex
  ready: false,
  count: 0,
  lastBuild: 0,

  // Parse IPv4 prefix string to 32-bit unsigned integer
  _ipv4ToUint32(ip) {
    const parts = ip.split(".");
    return ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0;
  },

  // Build ROA store from Cloudflare feed roas array
  build(roas) {
    const v4 = [];
    const v6 = [];
    for (let i = 0; i < roas.length; i++) {
      const r = roas[i];
      const asn = typeof r.asn === "string" ? parseInt(r.asn.replace("AS", "")) : Number(r.asn);
      const prefix = r.prefix;
      const maxLen = r.maxLength || r.maxPrefixLength || 0;
      if (!prefix || !asn) continue;

      const slashIdx = prefix.indexOf("/");
      if (slashIdx < 0) continue;
      const prefixLen = parseInt(prefix.substring(slashIdx + 1));
      const addr = prefix.substring(0, slashIdx);

      if (prefix.indexOf(":") >= 0) {
        // IPv6 — store as zero-padded hex string for sorting
        const expanded = this._expandIPv6(addr);
        if (expanded) {
          v6.push({ prefixHex: expanded, prefixLen, asn, maxLen: maxLen || prefixLen });
        }
      } else {
        // IPv4 — store as numeric range
        const start = this._ipv4ToUint32(addr);
        const hostBits = 32 - prefixLen;
        const end = (start | ((1 << hostBits) - 1)) >>> 0;
        v4.push({ start, end, asn, prefixLen, maxLen: maxLen || prefixLen });
      }
    }

    // Sort for binary search
    v4.sort((a, b) => a.start - b.start || a.prefixLen - b.prefixLen);
    v6.sort((a, b) => a.prefixHex < b.prefixHex ? -1 : a.prefixHex > b.prefixHex ? 1 : a.prefixLen - b.prefixLen);

    this.v4Entries = v4;
    this.v6Entries = v6;
    this.count = v4.length + v6.length;
    this.ready = true;
    this.lastBuild = Date.now();
  },

  // Expand IPv6 address to 32-char hex for reliable sorting
  _expandIPv6(addr) {
    try {
      let groups = addr.split("::");
      let left = groups[0] ? groups[0].split(":") : [];
      let right = groups.length > 1 && groups[1] ? groups[1].split(":") : [];
      const missing = 8 - left.length - right.length;
      const mid = [];
      for (let i = 0; i < missing; i++) mid.push("0000");
      const all = [...left, ...mid, ...right];
      return all.map(g => g.padStart(4, "0")).join("");
    } catch (_e) {
      return null;
    }
  },

  // Validate a prefix against the local ROA store
  // Returns: {prefix, status: "valid"|"invalid"|"not_found", validating_roas: N}
  validate(asn, prefix) {
    if (!this.ready) return null; // Signal caller to use fallback

    const asnNum = typeof asn === "string" ? parseInt(asn.replace("AS", "")) : Number(asn);
    const slashIdx = prefix.indexOf("/");
    if (slashIdx < 0) return { prefix, status: "not_found", validating_roas: 0 };
    const prefixLen = parseInt(prefix.substring(slashIdx + 1));
    const addr = prefix.substring(0, slashIdx);

    if (prefix.indexOf(":") >= 0) {
      return this._validateV6(asnNum, addr, prefixLen, prefix);
    }
    return this._validateV4(asnNum, addr, prefixLen, prefix);
  },

  _validateV4(asn, addr, prefixLen, prefix) {
    const queryStart = this._ipv4ToUint32(addr);
    const entries = this.v4Entries;

    // Binary search: find rightmost entry where start <= queryStart
    let lo = 0, hi = entries.length - 1;
    let insertionPoint = -1;
    while (lo <= hi) {
      const mid = (lo + hi) >>> 1;
      if (entries[mid].start <= queryStart) {
        insertionPoint = mid;
        lo = mid + 1;
      } else {
        hi = mid - 1;
      }
    }

    // Scan backwards from insertion point to find covering ROAs
    // ROAs are sorted by start, so we scan back while start could still cover our prefix
    const matched = [];
    const unmatchedAs = [];
    for (let i = insertionPoint; i >= 0; i--) {
      const e = entries[i];
      // If this ROA's network start is too far left, no more matches possible
      if (queryStart - e.start > 0x01000000) break; // heuristic: skip if > /8 away
      // Check if query prefix is contained within this ROA
      if (e.start <= queryStart && queryStart <= e.end && prefixLen >= e.prefixLen) {
        if (prefixLen <= e.maxLen) {
          if (e.asn === asn) {
            matched.push(e);
          } else {
            unmatchedAs.push(e);
          }
        }
        // prefixLen > maxLen → too specific, invalid if ASN matches
        else if (e.asn === asn) {
          unmatchedAs.push(e); // ASN matches but length exceeds maxLen
        }
      }
    }

    if (matched.length > 0) return { prefix, status: "valid", validating_roas: matched.length };
    if (unmatchedAs.length > 0) return { prefix, status: "invalid", validating_roas: unmatchedAs.length };
    return { prefix, status: "not_found", validating_roas: 0 };
  },

  _validateV6(asn, addr, prefixLen, prefix) {
    const queryHex = this._expandIPv6(addr);
    if (!queryHex) return { prefix, status: "not_found", validating_roas: 0 };
    const entries = this.v6Entries;

    // Binary search for approximate position
    let lo = 0, hi = entries.length - 1;
    let insertionPoint = -1;
    while (lo <= hi) {
      const mid = (lo + hi) >>> 1;
      if (entries[mid].prefixHex <= queryHex) {
        insertionPoint = mid;
        lo = mid + 1;
      } else {
        hi = mid - 1;
      }
    }

    const matched = [];
    const unmatchedAs = [];
    // Scan backwards from insertion point
    for (let i = insertionPoint; i >= 0 && i > insertionPoint - 500; i--) {
      const e = entries[i];
      // Check if query prefix is covered by this ROA entry
      // A covering ROA has a shorter or equal prefix length and its network prefix matches
      if (e.prefixLen <= prefixLen) {
        // Compare the first prefixLen bits (in hex chars: prefixLen/4 chars, rounded up)
        const hexChars = Math.ceil(e.prefixLen / 4);
        if (queryHex.substring(0, hexChars) === e.prefixHex.substring(0, hexChars)) {
          if (prefixLen <= e.maxLen) {
            if (e.asn === asn) matched.push(e);
            else unmatchedAs.push(e);
          } else if (e.asn === asn) {
            unmatchedAs.push(e);
          }
        }
      }
      // Stop if we're too far away
      if (queryHex.substring(0, 4) !== e.prefixHex.substring(0, 4)) break;
    }

    if (matched.length > 0) return { prefix, status: "valid", validating_roas: matched.length };
    if (unmatchedAs.length > 0) return { prefix, status: "invalid", validating_roas: unmatchedAs.length };
    return { prefix, status: "not_found", validating_roas: 0 };
  },

  // Persist to disk for fast restart
  saveToDisk(filePath) {
    try {
      const data = JSON.stringify({
        ts: this.lastBuild,
        v4Count: this.v4Entries.length,
        v6Count: this.v6Entries.length,
        v4: this.v4Entries,
        v6: this.v6Entries,
      });
      fs.writeFileSync(filePath, data);
      console.log("[ROA] Saved " + this.count + " ROAs to disk");
    } catch (e) {
      console.warn("[ROA] Disk save failed:", e.message);
    }
  },

  // Load from disk cache (returns true if loaded)
  loadFromDisk(filePath) {
    try {
      if (!fs.existsSync(filePath)) return false;
      const raw = fs.readFileSync(filePath, "utf8");
      const data = JSON.parse(raw);
      // Only use if less than 6 hours old
      if (Date.now() - data.ts > 6 * 60 * 60 * 1000) return false;
      this.v4Entries = data.v4;
      this.v6Entries = data.v6;
      this.count = data.v4Count + data.v6Count;
      this.lastBuild = data.ts;
      this.ready = true;
      console.log("[ROA] Loaded " + this.count + " ROAs from disk cache");
      return true;
    } catch (e) {
      console.warn("[ROA] Disk load failed:", e.message);
      return false;
    }
  },
};

module.exports = { roaStore };
