// NETWORK metadata client for Phase 4 enrichment.
//
// PRIVACY: every request below sends ONLY the extracted citation id (DOI /
// arXiv id / ISBN) or the topic string — never any transcript text. Detection
// already happened locally; this module just resolves a bare identifier to
// public metadata.
//
// All endpoints are CORS-friendly, legitimate, metadata-only APIs (Crossref,
// OpenAlex, Open Library, Wikipedia REST). Their responses include the
// `Access-Control-Allow-Origin` header, so they are readable even under our
// COEP `require-corp` / COOP `same-origin` isolation (CORS responses satisfy
// require-corp without needing CORP headers). We never fetch or proxy any
// shadow-library content here — those are search-URL only and live in links.ts.
//
// Every function is defensive: ~8s AbortController timeout, try/catch that
// resolves to `undefined`, and never throws. Results are memoised in an
// in-memory Map for the session.

import type { Citation, RefMetadata } from './types';

/** Per-session metadata cache, keyed by `${kind}:${value}` / `wiki:${topic}`. */
const cache = new Map<string, RefMetadata | undefined>();
const wikiCache = new Map<
  string,
  { title: string; extract: string; url: string } | undefined
>();

const TIMEOUT_MS = 8000;

/** GET JSON with an abort timeout. Returns parsed JSON or undefined on any
 *  failure (network, non-2xx, timeout, parse). Never throws. */
async function fetchJson(url: string): Promise<unknown | undefined> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
  try {
    const res = await fetch(url, {
      method: 'GET',
      headers: { Accept: 'application/json' },
      signal: controller.signal,
    });
    if (!res.ok) return undefined;
    return (await res.json()) as unknown;
  } catch {
    return undefined;
  } finally {
    clearTimeout(timer);
  }
}

/** Narrow an unknown to a plain record for safe property access. */
function asRecord(v: unknown): Record<string, unknown> | undefined {
  return typeof v === 'object' && v !== null
    ? (v as Record<string, unknown>)
    : undefined;
}

function asString(v: unknown): string | undefined {
  return typeof v === 'string' && v.length > 0 ? v : undefined;
}

function asNumber(v: unknown): number | undefined {
  return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
}

// ---------------------------------------------------------------------------
// Crossref (DOI)
// ---------------------------------------------------------------------------

function parseCrossref(json: unknown, value: string): RefMetadata | undefined {
  const root = asRecord(json);
  const message = asRecord(root?.message);
  if (!message) return undefined;

  const titleArr = Array.isArray(message.title) ? message.title : [];
  const title = asString(titleArr[0]);

  const authors: string[] = [];
  if (Array.isArray(message.author)) {
    for (const a of message.author) {
      const rec = asRecord(a);
      if (!rec) continue;
      const given = asString(rec.given);
      const family = asString(rec.family);
      const name = [given, family].filter(Boolean).join(' ').trim();
      if (name) authors.push(name);
    }
  }

  // Year: prefer published, then published-print/online, then issued.
  const year =
    crossrefYear(message.published) ??
    crossrefYear(message['published-print']) ??
    crossrefYear(message['published-online']) ??
    crossrefYear(message.issued);

  return {
    title,
    authors: authors.length ? authors : undefined,
    year,
    source: 'Crossref',
    url: `https://doi.org/${value}`,
  };
}

/** Crossref date parts live at `<field>['date-parts'][0][0]`. */
function crossrefYear(field: unknown): number | undefined {
  const rec = asRecord(field);
  const parts = rec?.['date-parts'];
  if (!Array.isArray(parts)) return undefined;
  const first = parts[0];
  if (!Array.isArray(first)) return undefined;
  return asNumber(first[0]);
}

// ---------------------------------------------------------------------------
// OpenAlex (arXiv, and DOI fallback)
// ---------------------------------------------------------------------------

function parseOpenAlex(
  json: unknown,
  source: string,
  url: string,
): RefMetadata | undefined {
  const work = asRecord(json);
  if (!work) return undefined;

  const title = asString(work.title) ?? asString(work.display_name);

  const authors: string[] = [];
  if (Array.isArray(work.authorships)) {
    for (const a of work.authorships) {
      const rec = asRecord(a);
      const author = asRecord(rec?.author);
      const name = asString(author?.display_name);
      if (name) authors.push(name);
    }
  }

  const year = asNumber(work.publication_year);

  // OpenAlex returns an error object (with no title) for unknown ids.
  if (!title && !authors.length && year === undefined) return undefined;

  return {
    title,
    authors: authors.length ? authors : undefined,
    year,
    source,
    url,
  };
}

// ---------------------------------------------------------------------------
// Open Library (ISBN)
// ---------------------------------------------------------------------------

function parseOpenLibrary(
  json: unknown,
  value: string,
): RefMetadata | undefined {
  const book = asRecord(json);
  if (!book) return undefined;

  const title = asString(book.title);

  // Open Library's /isbn/{}.json gives `by_statement` (a free-text credit)
  // rather than structured authors; surface it as a single author when present.
  const byStatement = asString(book.by_statement);
  const authors = byStatement ? [byStatement] : undefined;

  // publish_date is free-text (e.g. "March 2004"); pull a 4-digit year out.
  let year: number | undefined;
  const publishDate = asString(book.publish_date);
  if (publishDate) {
    const m = publishDate.match(/\b(\d{4})\b/);
    if (m) year = asNumber(Number(m[1]));
  }

  if (!title && !authors && year === undefined) return undefined;

  return {
    title,
    authors,
    year,
    source: 'Open Library',
    url: `https://openlibrary.org/isbn/${value}`,
  };
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/**
 * Resolve a detected citation to public metadata.
 *
 * Sends ONLY the citation's identifier (DOI / arXiv id / ISBN) to a
 * metadata-only API — never any transcript text. Returns `undefined` on any
 * failure or for `author-year` citations (the UI offers search links instead).
 * Never throws.
 */
export async function resolveCitation(
  c: Citation,
): Promise<RefMetadata | undefined> {
  const key = `${c.kind}:${c.value}`;
  if (cache.has(key)) return cache.get(key);

  let result: RefMetadata | undefined;

  switch (c.kind) {
    case 'doi': {
      // Crossref is the authoritative DOI registry and is CORS-enabled.
      const json = await fetchJson(
        `https://api.crossref.org/works/${encodeURIComponent(c.value)}`,
      );
      result = parseCrossref(json, c.value);
      // Fallback to OpenAlex if Crossref had nothing (e.g. DataCite DOI).
      if (!result) {
        const alt = await fetchJson(
          `https://api.openalex.org/works/doi:${encodeURIComponent(c.value)}`,
        );
        result = parseOpenAlex(alt, 'OpenAlex', `https://doi.org/${c.value}`);
      }
      break;
    }
    case 'arxiv': {
      // OpenAlex resolves arXiv ids over CORS, avoiding arXiv's
      // non-CORS Atom export API.
      const json = await fetchJson(
        `https://api.openalex.org/works/arxiv:${encodeURIComponent(c.value)}`,
      );
      result = parseOpenAlex(
        json,
        'OpenAlex',
        `https://arxiv.org/abs/${c.value}`,
      );
      break;
    }
    case 'isbn': {
      const json = await fetchJson(
        `https://openlibrary.org/isbn/${encodeURIComponent(c.value)}.json`,
      );
      result = parseOpenLibrary(json, c.value);
      break;
    }
    case 'author-year':
      // No reliable id to resolve; UI falls back to search links.
      result = undefined;
      break;
  }

  cache.set(key, result);
  return result;
}

/**
 * Fetch a one-paragraph Wikipedia summary for a topic.
 *
 * Sends ONLY the topic string to Wikipedia's REST summary endpoint (CORS-
 * enabled) — never any transcript text. Returns `undefined` on 404 / any
 * failure. Never throws.
 */
export async function wikipediaSummary(
  topic: string,
): Promise<{ title: string; extract: string; url: string } | undefined> {
  const key = `wiki:${topic}`;
  if (wikiCache.has(key)) return wikiCache.get(key);

  let result: { title: string; extract: string; url: string } | undefined;

  const json = await fetchJson(
    `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(
      topic,
    )}`,
  );
  const root = asRecord(json);
  if (root) {
    const title = asString(root.title);
    const extract = asString(root.extract);
    const contentUrls = asRecord(root.content_urls);
    const desktop = asRecord(contentUrls?.desktop);
    const url = asString(desktop?.page);
    if (title && extract && url) {
      result = { title, extract, url };
    }
  }

  wikiCache.set(key, result);
  return result;
}
