/**
 * DashIQ V2 — data.jsx
 * API layer. Single source of truth for all network access.
 *
 * Load context: <script type="text/babel" src="src/data.jsx">
 * No bundler. No imports. Exposes everything on window.DASHIQ.
 *
 * NOTE (embed): TODO — validate e.origin against marketing site domains
 * before promotion to prod (see app.jsx embed-mode notes).
 */

// ─── 1. BASE URL (one declaration, never repeated) ──────────────────────────

const API_BASE = (typeof window !== 'undefined' && window.DASHIQ_API)
  || 'https://dashiq-api.collabiqcore.com';

// ─── 2. IN-MEMORY CACHE ─────────────────────────────────────────────────────

const DEFAULT_TTL_MS = 30000; // 30 seconds
const MAX_CACHE_ENTRIES = 50;

const _store = {}; // { [key]: { value, expiresAt } }

const cache = {
  /**
   * Get a cached value. Returns undefined if missing or expired.
   */
  get(key) {
    const entry = _store[key];
    if (!entry) return undefined;
    if (Date.now() > entry.expiresAt) {
      delete _store[key];
      return undefined;
    }
    return entry.value;
  },

  /**
   * Store a value with a TTL. Evicts oldest entry if cap reached.
   */
  set(key, value, ttlMs) {
    const ttl = (typeof ttlMs === 'number' && ttlMs > 0) ? ttlMs : DEFAULT_TTL_MS;
    // Evict oldest if at capacity
    const keys = Object.keys(_store);
    if (keys.length >= MAX_CACHE_ENTRIES) {
      let oldest = keys[0];
      for (let i = 1; i < keys.length; i++) {
        if (_store[keys[i]].expiresAt < _store[oldest].expiresAt) oldest = keys[i];
      }
      delete _store[oldest];
    }
    _store[key] = { value, expiresAt: Date.now() + ttl };
  },

  /**
   * Clear one entry (by key) or all entries (no argument).
   * Also exposed as bust(pattern) for regex-based eviction.
   */
  clear(key) {
    if (key === undefined) {
      const ks = Object.keys(_store);
      for (let i = 0; i < ks.length; i++) delete _store[ks[i]];
    } else {
      delete _store[key];
    }
  },

  /**
   * Evict all cache entries whose key matches a RegExp or string pattern.
   */
  bust(pattern) {
    const re = (pattern instanceof RegExp) ? pattern : new RegExp(pattern);
    const ks = Object.keys(_store);
    for (let i = 0; i < ks.length; i++) {
      if (re.test(ks[i])) delete _store[ks[i]];
    }
  },

  /**
   * Diagnostic: return count of live (non-expired) entries.
   */
  stats() {
    const now = Date.now();
    let live = 0;
    const ks = Object.keys(_store);
    for (let i = 0; i < ks.length; i++) {
      if (_store[ks[i]].expiresAt > now) live++;
    }
    return { total: ks.length, live };
  },
};

// ─── 3. IN-FLIGHT DEDUPLICATION MAP ─────────────────────────────────────────

const _inflight = {}; // { [cacheKey]: Promise }

// ─── 4. CACHE KEY BUILDER ────────────────────────────────────────────────────

/**
 * Build a deterministic cache key from method + path + sorted params + body hash.
 * Sorting params ensures GET /foo?b=2&a=1 === GET /foo?a=1&b=2.
 */
function _cacheKey(method, path, params, body) {
  let key = method.toUpperCase() + ' ' + path;
  if (params && typeof params === 'object') {
    const sorted = Object.keys(params)
      .filter(k => params[k] !== null && params[k] !== undefined && params[k] !== '')
      .sort()
      .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
      .join('&');
    if (sorted) key += '?' + sorted;
  }
  if (body) {
    try { key += '|' + JSON.stringify(body); } catch (_) {}
  }
  return key;
}

// ─── 5. URL BUILDER ──────────────────────────────────────────────────────────

/**
 * Construct a full URL from path + params object.
 * Only appends params with non-empty, non-null, non-undefined values.
 */
function _buildURL(path, params) {
  let url = API_BASE + path;
  if (params && typeof params === 'object') {
    const qs = Object.keys(params)
      .filter(k => params[k] !== null && params[k] !== undefined && params[k] !== '')
      .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
      .join('&');
    if (qs) url += '?' + qs;
  }
  return url;
}

// ─── 6. CORE REQUEST HELPER ──────────────────────────────────────────────────

/**
 * Generic fetcher.
 *
 * @param {string} method    - 'GET' | 'POST' | 'PUT' | 'DELETE'
 * @param {string} path      - API path e.g. '/v2/orgs'
 * @param {object} [params]  - Query parameters (GET) — omit for POST
 * @param {object} [body]    - Request body (POST/PUT)
 * @param {object} [opts]    - { timeoutMs, noCache }
 * @returns {Promise<any>}
 *
 * Behavior:
 * - 30s default timeout via AbortController
 * - Non-2xx → throws Error with { status, url, body }
 * - 401/403 → throws (caller renders ErrorCard)
 * - NO automatic retry
 * - NO auth headers (open API)
 * - GETs are cached (DEFAULT_TTL_MS); POSTs are never cached
 * - Concurrent identical GETs are deduplicated via _inflight
 */
function request(method, path, params, body, opts) {
  const _opts = opts || {};
  const timeoutMs = (typeof _opts.timeoutMs === 'number' && _opts.timeoutMs > 0)
    ? _opts.timeoutMs
    : 30000;
  const isGet = method.toUpperCase() === 'GET';
  const ck = _cacheKey(method, path, params, body);

  // Cache read (GET only, skip if noCache flag)
  if (isGet && !_opts.noCache) {
    const cached = cache.get(ck);
    if (cached !== undefined) return Promise.resolve(cached);
    // Deduplicate concurrent identical requests
    if (_inflight[ck]) return _inflight[ck];
  }

  // Build URL
  const url = _buildURL(path, isGet ? params : null);

  // Fetch options
  const fetchOpts = {
    method: method.toUpperCase(),
    mode: 'cors',
    headers: {},
  };
  if (body !== undefined && body !== null && !isGet) {
    fetchOpts.headers['Content-Type'] = 'application/json';
    try { fetchOpts.body = JSON.stringify(body); } catch (_) { fetchOpts.body = '{}'; }
  }

  // AbortController for timeout
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
  fetchOpts.signal = ctrl.signal;

  const promise = fetch(url, fetchOpts)
    .then(function(res) {
      clearTimeout(timer);
      if (!res.ok) {
        return res.text().then(function(text) {
          const err = new Error('API ' + res.status + ': ' + path);
          err.status = res.status;
          err.url = url;
          err.body = text;
          throw err;
        });
      }
      return res.json();
    })
    .catch(function(e) {
      clearTimeout(timer);
      if (e.name === 'AbortError') {
        const err = new Error('API timeout (' + (timeoutMs / 1000) + 's): ' + path);
        err.status = 0;
        err.url = url;
        err.body = null;
        throw err;
      }
      throw e;
    })
    .then(function(data) {
      // Cache successful GET responses
      if (isGet) {
        cache.set(ck, data, DEFAULT_TTL_MS);
        delete _inflight[ck];
      }
      return data;
    })
    .catch(function(e) {
      if (isGet) delete _inflight[ck];
      throw e;
    });

  if (isGet) _inflight[ck] = promise;
  return promise;
}

// ─── 7. CONVENIENCE SHORTHANDS ───────────────────────────────────────────────

function get(path, params, opts) {
  return request('GET', path, params, undefined, opts);
}

function post(path, body, opts) {
  return request('POST', path, undefined, body, opts);
}

// ─── 8. RUNTIME BASE URL OVERRIDE ────────────────────────────────────────────

/**
 * Optional runtime override — allows host page to redirect requests after load.
 * Clears the cache when called so stale entries from the old base are not served.
 */
function setApiBase(url) {
  if (typeof url === 'string' && url) {
    // Reassign the module-level constant by mutating the DASHIQ object after construction.
    // Callers always read window.DASHIQ.API_BASE — this keeps parity.
    window.DASHIQ.API_BASE = url.replace(/\/$/, '');
    cache.clear();
  }
}

// ─── 9. ENDPOINT HELPERS ─────────────────────────────────────────────────────

/** Boot check */
function health() {
  return get('/v2/health');
}

/** Org autocomplete — used by anchor + comparison + Compete org search.
 *  @param {{ q?: string, limit?: number, category?: string }} [params]
 *  @param {{ timeoutMs?: number, noCache?: boolean }} [opts]
 *
 *  DEFECT-001 fix: previously took `(opts)` and forwarded it as the 3rd
 *  arg to `get()` (the `opts` slot), so callers passing `{ q: 'sanofi' }`
 *  ended up with `params=undefined`. Now correctly takes `(params, opts)`.
 */
function fetchOrgs(params, opts) {
  return get('/v2/orgs', params, opts);
}

// DEFECT-013: removed unused /api/v1/* wrappers (fetchDrugs, fetchTargets).
// Both were defined here and exported on window.DASHIQ but had ZERO call-sites
// across the V2 frontend (verified via grep -rn '\bfetchDrugs\b\|DASHIQ\.fetchDrugs').
// Backend /v2/drugs and /v2/targets return 404 (verified via Railway probe
// 2026-05-08); per CTK's "no allow-listing" rule, dead-code wrappers for
// non-existent v2 endpoints are removed instead of being placed on a permanent
// allow-list. If autocomplete UI is added back, wire it against /v2/orgs which
// already covers the org-name search use case.

/** Therapeutic area filter — WhitespaceIQ + CompeteIQ. DEFECT-012: migrated
 *  from /api/v1/therapeutic-areas (5 TAs, deprecated) to /v2/therapeutic-areas
 *  (7 TAs incl. RARE_DISEASE, INFECTIOUS_DISEASE). */
function fetchTherapeuticAreas(params, opts) {
  return get('/v2/therapeutic-areas', params, opts);
}

/**
 * PartnerIQ data.
 * @param {{ org_name?: string, org_b?: string, deal_type?: string, limit?: number }} p
 */
function searchStrategy(p) {
  const params = {};
  if (p) {
    if (p.org_name !== undefined && p.org_name !== null) params.org_name = p.org_name;
    if (p.org_b !== undefined && p.org_b !== null) params.org_b = p.org_b;
    if (p.deal_type !== undefined && p.deal_type !== null) params.deal_type = p.deal_type;
    if (p.limit !== undefined && p.limit !== null) params.limit = p.limit;
  }
  return get('/v2/partner-iq/search', params, { timeoutMs: 30000 });
}

/**
 * CompeteIQ Landscape data.
 * @param {{ disease?: string, org?: string, limit?: number, offset?: number }} p
 */
function rankCompete(p) {
  const params = {};
  if (p) {
    if (p.disease !== undefined && p.disease !== null) params.disease = p.disease;
    if (p.org !== undefined && p.org !== null) params.org = p.org;
    if (p.limit !== undefined && p.limit !== null) params.limit = p.limit;
    if (p.offset !== undefined && p.offset !== null) params.offset = p.offset;
  }
  return get('/v2/compete-iq/rank', params);
}

/**
 * CompeteIQ disease picker.
 * @param {{ limit?: number }} p
 */
function fetchCompeteDiseases(p) {
  const params = { limit: (p && p.limit != null) ? p.limit : 2000 };
  return get('/v2/compete-iq/diseases', params);
}

/**
 * CompeteIQ Drug Profile.
 * @param {{ drug_name?: string, limit?: number }} p
 */
function fetchDrugProfiles(p) {
  const params = {};
  if (p) {
    // Forward known filters from the OpenAPI spec: drug_name, target, modality,
    // indication_tier, min_phase, org_name, moa, disease, limit, offset, q.
    const allowed = ['drug_name', 'target', 'modality', 'indication_tier',
                     'min_phase', 'org_name', 'moa', 'disease', 'limit', 'offset', 'q'];
    allowed.forEach(function(k) {
      if (p[k] !== undefined && p[k] !== null && p[k] !== '') params[k] = p[k];
    });
  }
  return get('/v2/drug-profiles', params);
}

/**
 * Drug name autocomplete — large dataset.
 * @param {{ limit?: number }} p
 */
function fetchDrugProfileNames(p) {
  const params = { limit: (p && p.limit != null) ? p.limit : 20000 };
  return get('/v2/drug-profiles/names', params);
}

/**
 * WhitespaceIQ scatter plot data.
 * @param {{ ta?: string, limit?: number }} p
 */
function fetchWhitespaceScatter(p) {
  const params = {};
  if (p) {
    if (p.ta !== undefined && p.ta !== null) params.ta = p.ta;
    if (p.limit !== undefined && p.limit !== null) params.limit = p.limit;
  }
  return get('/v2/whitespace-iq/scatter', params);
}

/**
 * WhitespaceIQ opportunity table.
 * @param {{ ta?: string, limit?: number }} p
 */
function fetchWhitespaceOpportunities(p) {
  const params = {};
  if (p) {
    if (p.ta !== undefined && p.ta !== null) params.ta = p.ta;
    if (p.limit !== undefined && p.limit !== null) params.limit = p.limit;
  }
  return get('/v2/whitespace-iq/opportunities', params);
}

/** WhitespaceIQ TA filter */
function fetchWhitespaceTAs() {
  return get('/v2/whitespace-iq/tas');
}

/**
 * WhitespaceIQ disease detail.
 * @param {string|number} id
 */
function fetchWhitespaceDisease(id) {
  return get('/v2/whitespace-iq/disease/' + encodeURIComponent(id));
}

/**
 * Disruption signals — two lookup modes:
 *
 *   Per-disease: { disease_id } or { disease_name }
 *     -> GET /v2/disruption-signals
 *     -> { disease_id, disease_name, signals: { signal_type: {strength, partner_frame, ...} } }
 *
 *   Per-pair: { org_a, org_b }
 *     -> GET /v2/disruption-signals/pair
 *     -> { pair, diseases_in_aggregate, signals: { signal_type: {strength, n_contributing} } }
 *
 * Pair mode aggregates the 5 disruption signal strengths across the pair's
 * top shared diseases. Powers the PartnerIQ Why-now triangle: each partner
 * row has a different shared-disease list -> different aggregate ->
 * different triangle shape. Dynamic per row, derived from existing data
 * (no ETL change required).
 *
 * Backend: backend_v2/routes/disruption.py
 * Spec: docs/DISRUPTION_SIGNALS_TECHNICAL_SPECIFICATION.md
 *
 * Empty signals object on lookup miss — caller should degrade gracefully.
 */
function fetchDisruptionSignals(p) {
  if (!p) return Promise.resolve({ signals: {} });
  // Pair mode
  if (p.org_a && p.org_b) {
    return get('/v2/disruption-signals/pair', {
      org_a: String(p.org_a),
      org_b: String(p.org_b),
    });
  }
  // Per-disease mode
  const params = {};
  if (p.disease_id != null && p.disease_id !== '') params.disease_id = String(p.disease_id);
  if (p.disease_name) params.disease_name = String(p.disease_name);
  if (!params.disease_id && !params.disease_name) return Promise.resolve({ signals: {} });
  return get('/v2/disruption-signals', params);
}

// DEFECT-013: removed unused /api/v1/disruption/scores, /api/v1/user-prefs,
// /api/v1/cache wrappers. Same disposition as fetchDrugs / fetchTargets above —
// defined here, exported on window.DASHIQ, but ZERO call-sites in the V2
// frontend (verified via grep). Backend /v2/* equivalents return 404 per the
// 2026-05-08 Railway probe. Per CTK's "where v2 endpoints exist, migrate;
// where they don't and the UI doesn't need them, remove the call. No
// allow-listing." rule, the dead-code wrappers are removed; if any of these
// surfaces becomes a real V2 feature later (disruption-scores preset on
// CompeteIQ, server-side user-prefs, server-side client-cache), reintroduce
// against the v2 endpoint that ships at that time.

// ─── 10. PRELOAD ─────────────────────────────────────────────────────────────

/**
 * Fire 5 prefetch requests on DOMContentLoaded.
 * Uses Promise.allSettled so one failure never blocks the others.
 * Returns the settled promises array for callers that want diagnostics.
 */
function preload() {
  return Promise.allSettled([
    fetchOrgs(),
    fetchTherapeuticAreas(),
    searchStrategy({ org_name: '', limit: 50 }),
    rankCompete({ limit: 50 }),
    fetchWhitespaceOpportunities({ limit: 50 }),
  ]);
}

// ─── 11. POSTMESSAGE HELPERS ─────────────────────────────────────────────────

/**
 * Allowlist of marketing-site origins that may embed the dashboard. The
 * runtime can extend it via window.DASHIQ_ALLOWED_ORIGINS for staging tests.
 * Mirror of the list in app.jsx — kept here so data.jsx remains standalone.
 */
var EMBED_ORIGIN_ALLOWLIST = (Array.isArray(window.DASHIQ_ALLOWED_ORIGINS) ? window.DASHIQ_ALLOWED_ORIGINS : [])
  .concat([
    'https://collabiqcore.com',
    'https://www.collabiqcore.com',
    'https://staging.collabiqcore.com',
  ]);

/**
 * Post {type:'COLLABIQ_READY'} to the parent frame.
 * No-op when not in an iframe (window.parent === window).
 * Iterates the allowlist; the browser silently drops mismatched targets.
 */
function postReady() {
  if (typeof window === 'undefined') return;
  if (window.parent === window) return;
  EMBED_ORIGIN_ALLOWLIST.forEach(function(origin) {
    try { window.parent.postMessage({ type: 'COLLABIQ_READY' }, origin); } catch (_) {}
  });
}

/**
 * Register a listener for {type:'COLLABIQ_ACTIVATE'} from the parent frame.
 * No-op when not embedded. Origin-pinned to the allowlist so unknown
 * frames can't trigger activation.
 * @param {function} cb — called with the full MessageEvent when activated
 */
function onActivate(cb) {
  if (typeof window === 'undefined') return;
  if (window.parent === window) return;
  window.addEventListener('message', function(e) {
    if (!e || EMBED_ORIGIN_ALLOWLIST.indexOf(e.origin) === -1) return;
    if (e.data && e.data.type === 'COLLABIQ_ACTIVATE') {
      if (typeof cb === 'function') cb(e);
    }
  });
}

// ─── 12. ASSEMBLE AND EXPOSE ─────────────────────────────────────────────────

const DASHIQ = {
  // Config
  API_BASE: API_BASE,
  setApiBase: setApiBase,

  // Low-level
  request: request,
  get: get,
  post: post,

  // Cache
  cache: cache,

  // Endpoint helpers
  health: health,
  fetchOrgs: fetchOrgs,
  fetchTherapeuticAreas: fetchTherapeuticAreas,
  searchStrategy: searchStrategy,
  rankCompete: rankCompete,
  fetchCompeteDiseases: fetchCompeteDiseases,
  fetchDrugProfiles: fetchDrugProfiles,
  fetchDrugProfileNames: fetchDrugProfileNames,
  fetchWhitespaceScatter: fetchWhitespaceScatter,
  fetchWhitespaceOpportunities: fetchWhitespaceOpportunities,
  fetchWhitespaceTAs: fetchWhitespaceTAs,
  fetchWhitespaceDisease: fetchWhitespaceDisease,
  fetchDisruptionSignals: fetchDisruptionSignals,
  // DEFECT-013: dropped fetchDrugs / fetchTargets / fetchDisruption /
  // fetchUserPrefs / saveUserPrefs / fetchClientCache / saveClientCache
  // exports — all wrapped /api/v1/* endpoints with no V2 equivalent and
  // no current call-sites. Reintroduce against /v2/* if the surface lights up.

  // Preload
  preload: preload,

  // Host handshake
  postReady: postReady,
  onActivate: onActivate,
};

Object.assign(window, { DASHIQ });

// ─── 13. DOMCONTENTLOADED PRELOAD WIRE-UP ────────────────────────────────────

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', function() { window.DASHIQ.preload(); });
} else {
  window.DASHIQ.preload();
}
