/**
 * services/rdap.js
 *
 * RDAP (Registration Data Access Protocol) is the modern, JSON-based
 * replacement for WHOIS. It's the authoritative source for:
 *   - registrar name
 *   - registrar abuse contact email
 *   - registrant country
 *   - nameservers
 *
 * Bootstrap: IANA publishes the RDAP bootstrap registry. For each TLD we
 * know which RDAP server to query. We cache the result per-domain.
 *
 * Free. No API key. Standardised by RFC 7480-7485.
 *
 * Returns: { registrar, registrar_abuse_email, registrant_country, status, nameservers, raw }
 *          or null if the TLD has no RDAP service (rare).
 */

'use strict';

const BOOTSTRAP = 'https://data.iana.org/rdap/dns.json';
let bootstrapCache = null;
let bootstrapAt    = 0;
const BOOTSTRAP_TTL = 24 * 60 * 60 * 1000; // 24h

const perDomainCache = new Map(); // domain → { at, data }
const PER_DOMAIN_TTL = 6 * 60 * 60 * 1000; // 6h

async function getBootstrap() {
  if (bootstrapCache && (Date.now() - bootstrapAt) < BOOTSTRAP_TTL) return bootstrapCache;
  try {
    const res = await fetch(BOOTSTRAP, { headers: { 'user-agent': 'NCII-Shield/1.0 (RDAP)' } });
    if (!res.ok) throw new Error(`bootstrap ${res.status}`);
    bootstrapCache = await res.json();
    bootstrapAt    = Date.now();
    return bootstrapCache;
  } catch (e) {
    console.warn('[rdap] bootstrap fetch failed:', e.message);
    return bootstrapCache || { services: [[], [], []] };
  }
}

function findServer(bootstrap, tld) {
  for (const entry of bootstrap.services || []) {
    const [tlds, servers] = entry;
    if (tlds.includes(tld.toLowerCase())) return servers[0];
  }
  return null;
}

function tldOf(domain) {
  const parts = domain.toLowerCase().split('.');
  if (parts.length < 2) return null;
  return parts[parts.length - 1];
}

function extractEmails(text) {
  if (!text) return [];
  return [...new Set((text.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g) || [])
    .filter(e => !e.includes('.' + 'png') && e.length < 80))];
}

async function lookup(domain) {
  domain = String(domain || '').toLowerCase().replace(/^www\./, '').trim();
  if (!domain || !/^[a-z0-9.-]+\.[a-z]{2,}$/.test(domain)) return null;

  const cached = perDomainCache.get(domain);
  if (cached && (Date.now() - cached.at) < PER_DOMAIN_TTL) return cached.data;

  const tld = tldOf(domain);
  if (!tld) return null;
  const boot = await getBootstrap();
  const server = findServer(boot, tld);
  if (!server) {
    const out = { domain, registrar: null, registrar_abuse_email: null, error: 'no-rdap-server-for-tld' };
    perDomainCache.set(domain, { at: Date.now(), data: out });
    return out;
  }

  try {
    const url = `${server.replace(/\/$/, '')}/domain/${encodeURIComponent(domain)}`;
    const res = await fetch(url, {
      headers: { 'accept': 'application/rdap+json', 'user-agent': 'NCII-Shield/1.0' },
      redirect: 'follow',
    });
    if (res.status === 404) {
      const out = { domain, registrar: null, registrar_abuse_email: null, error: 'not-registered' };
      perDomainCache.set(domain, { at: Date.now(), data: out });
      return out;
    }
    if (!res.ok) {
      const out = { domain, registrar: null, registrar_abuse_email: null, error: `http-${res.status}` };
      perDomainCache.set(domain, { at: Date.now(), data: out });
      return out;
    }
    const data = await res.json();
    let out = parseRdap(data);
    // If we don't have a real abuse email, follow the "related" link to the
    // registrar's own RDAP server (the standard RDAP referral pattern).
    if (!out.registrar_abuse_email && out._related) {
      const follow = await fetchRelatedAbuse(out._related);
      if (follow) {
        out = {
          ...out,
          registrar: follow.registrar || out.registrar,
          registrar_abuse_email: follow.registrar_abuse_email,
        };
      }
    }
    // Final fallback: hard-coded map of well-known registrar IDs → abuse@
    if (!out.registrar_abuse_email) {
      const fallback = registrarAbuseFallback(out.registrar_handle, out.registrar);
      if (fallback) out.registrar_abuse_email = fallback;
    }
    delete out._related; // internal
    perDomainCache.set(domain, { at: Date.now(), data: out });
    return out;
  } catch (e) {
    console.warn('[rdap] lookup failed for', domain, ':', e.message);
    return { domain, registrar: null, registrar_abuse_email: null, error: e.message };
  }
}

function parseRdap(d) {
  // 1. Registrar entity
  const registrarEntity = (d.entities || []).find(e => (e.roles || []).includes('registrar'));
  // The registrar "handle" is the IANA registrar ID (e.g. "2487" for Internet.bs)
  // — this is what we use to look up the abuse@ fallback.
  const registrarHandle = registrarEntity ? registrarEntity.handle : null;
  // The human-friendly name (if the registry provides it via vCard fn)
  const registrarName = registrarEntity ? (registrarEntity.vcard?.['fn']?.[0] || null) : null;
  // Compose: prefer the human name; fall back to the handle
  const registrar = registrarName || registrarHandle;

  // 2. Registrar abuse email: look in the registrar entity for a vCard EMAIL.
  let registrar_abuse_email = null;
  if (registrarEntity) {
    const abuseVcard = (registrarEntity.vcard?.['email'] || []).find(e => (e[0] || '').toLowerCase().includes('abuse'));
    if (abuseVcard) registrar_abuse_email = abuseVcard[3];
    if (!registrar_abuse_email) {
      const any = (registrarEntity.vcard?.['email'] || [])[0];
      if (any) registrar_abuse_email = any[3];
    }
  }
  // 3. Registrant country (only for non-gdps-redacted records)
  const registrant = (d.entities || []).find(e => (e.roles || []).includes('registrant'));
  const registrant_country = registrant?.vcard?.['adr']?.[0]?.[6] || null;

  // 4. Status flags
  const status = (d.status || []).map(s => typeof s === 'string' ? s : s.status || '').filter(Boolean);

  // 5. Nameservers
  const nameservers = (d.nameservers || []).map(n => n.ldhName).filter(Boolean);

  // 6. The "related" link points to the registrar's own RDAP server. If
  //    this server didn't return a real abuse email, we'll fetch that.
  const related = (d.links || []).find(l => l.rel === 'related' && l.href && l.href.includes('/rdap'));

  return {
    registrar,
    registrar_handle: registrarHandle,        // IANA ID (e.g. "2487")
    registrar_abuse_email,
    registrant_country,
    status,
    nameservers,
    _related: related ? related.href : null,
  };
}

async function fetchRelatedAbuse(relatedUrl) {
  try {
    const r = await fetch(relatedUrl, { headers: { 'accept': 'application/rdap+json' } });
    if (!r.ok) return null;
    const data = await r.json();
    // The related endpoint is usually the registrar's own RDAP "entity" record.
    // vCard is at the top level here.
    const emails = (data.vcard?.['email'] || []).map(e => e[3]).filter(Boolean);
    const abuse = emails.find(e => /abuse/i.test(e)) || emails[0] || null;
    return {
      registrar: data.vcard?.['fn']?.[0] || data.handle || null,
      registrar_abuse_email: abuse,
    };
  } catch (e) { return null; }
}

// Hard-coded fallback for well-known registrars whose RDAP doesn't expose
// abuse emails but who maintain a published abuse@ contact. This is
// authoritative as of 2026 — if a registrar changes their address, the
// admin can edit /etc/registrar_abuse.json.
const REGISTRAR_ABUSE_FALLBACK = {
  // IANA registrar IDs (taken from RDAP handle) → abuse email
  '292':     'abuse@markmonitor.com',     // MarkMonitor
  '2487':    'abuse@internet.bs',          // Internet.bs
  '264':     'abuse@name.com',             // Name.com
  '144':     'abuse@godaddy.com',          // GoDaddy
  '146':     'abuse@godaddy.com',
  '48':      'abuse@enom.com',             // eNom
  '69':      'abuse@tucows.com',           // Tucows
  '376':     'abuse@namecheap.com',        // Namecheap
  '1068':    'abuse@namecheap.com',
  '472':     'abuse@dynadot.com',          // Dynadot
  '228':     'abuse@dreamhost.com',
  '625':     'abuse@namesilo.com',
  '1479':    'abuse@namebright.com',
  '1599':    'abuse@publicdomainregistry.com',
  '1111':    'abuse@101domain.com',
  '303':     'abuse@webnames.ca',
  '81':      'abuse@gandi.net',
  '1715':    'abuse@squarespace.domains',
  '1495':    'abuse@key-systems.net',
  '168':     'abuse@enom.com',
  '609':     'abuse@sav.com',
  '440':     'abuse@wildwestdomains.com',
  '299':     'abuse@cscglobal.com',
  '955':     'abuse@alibaba-inc.com',
  '1861':    'abuse@porkbun.com',         // Porkbun LLC
  // Common-name fallback (in case the RDAP returns the name instead of ID)
  'markmonitor':               'abuse@markmonitor.com',
  'internet.bs':               'abuse@internet.bs',
  'name.com':                  'abuse@name.com',
  'godaddy':                   'abuse@godaddy.com',
  'enom':                      'abuse@enom.com',
  'tucows':                    'abuse@tucows.com',
  'namecheap':                 'abuse@namecheap.com',
  'dynadot':                   'abuse@dynadot.com',
  'dreamhost':                 'abuse@dreamhost.com',
  'namesilo':                  'abuse@namesilo.com',
  'gandi':                     'abuse@gandi.net',
  'squarespace':               'abuse@squarespace.domains',
  'key-systems':               'abuse@key-systems.net',
  'cscglobal':                 'abuse@cscglobal.com',
  'alibaba':                   'abuse@alibaba-inc.com',
  'sav.com':                   'abuse@sav.com',
  'porkbun':                   'abuse@porkbun.com',
};

function registrarAbuseFallback(handle, registrarName) {
  if (handle && REGISTRAR_ABUSE_FALLBACK[handle]) return REGISTRAR_ABUSE_FALLBACK[handle];
  if (registrarName) {
    const key = registrarName.toLowerCase().split(/[\s,]+/)[0];
    if (REGISTRAR_ABUSE_FALLBACK[key]) return REGISTRAR_ABUSE_FALLBACK[key];
  }
  return null;
}

module.exports = { lookup, registrarAbuseFallback };
