#!/usr/bin/env bun
// Pull peer-reviewed evidence from PubMed (NCBI E-utilities — free, no auth)
// for each pilot SKU's primary active ingredient. Writes structured records
// to data/evidence/<ingredient>.json for use by the Scientific Studies
// generator and the FAQ "How long until I notice a difference?" template.
//
// E-utilities docs: https://www.ncbi.nlm.nih.gov/books/NBK25500/
// Rate limit: 3 req/sec without an API key (we space requests at 400ms).
//
// Usage: bun scripts/ingest-pubmed-evidence.ts

import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'fs'
import { join } from 'path'

const ROOT = join(import.meta.dir, '..')
const OUT_DIR = join(ROOT, 'data', 'evidence')
const TOOL = 'jv-content-pipeline'
const EMAIL = 'omair@quikcue.com'
const PER_INGREDIENT_LIMIT = 8

type Ingredient = {
  handle: string
  searchTerm: string
  skus: string[]
  filters: string
}

const INGREDIENTS: Ingredient[] = [
  {
    handle: 'cholecalciferol',
    searchTerm: '(cholecalciferol OR "vitamin D3") AND (supplementation OR adults)',
    skus: ['JV-D1000', 'JV-D4000'],
    filters: '+AND+humans[Filter]+AND+("randomized+controlled+trial"[Publication+Type]+OR+"meta-analysis"[Publication+Type]+OR+"systematic+review"[Publication+Type])'
  },
  {
    handle: 'menaquinone-7',
    searchTerm: '("menaquinone" OR "vitamin K2" OR "MK-7") AND (bone OR cardiovascular)',
    skus: ['JV-D4000'],
    filters: '+AND+humans[Filter]+AND+("randomized+controlled+trial"[Publication+Type]+OR+"meta-analysis"[Publication+Type])'
  },
  {
    handle: 'curcumin',
    searchTerm: '(curcumin OR "Curcuma longa") AND (bioavailability OR piperine OR joint)',
    skus: ['JV-TURMERIC500'],
    filters: '+AND+humans[Filter]+AND+("randomized+controlled+trial"[Publication+Type]+OR+"meta-analysis"[Publication+Type])'
  },
  {
    handle: 'piperine-bioperine',
    searchTerm: '(piperine OR BioPerine) AND (bioavailability OR absorption)',
    skus: ['JV-TURMERIC500'],
    filters: '+AND+humans[Filter]'
  },
  {
    handle: 'calcium-citrate',
    searchTerm: '"calcium citrate" AND (absorption OR bone OR supplementation)',
    skus: ['JV-CALCIT'],
    filters: '+AND+humans[Filter]+AND+("randomized+controlled+trial"[Publication+Type]+OR+"meta-analysis"[Publication+Type])'
  },
  {
    handle: 'marine-collagen-peptides',
    searchTerm: '("marine collagen" OR "collagen peptides" OR "hydrolyzed collagen") AND (skin OR joint OR bone)',
    skus: ['JV-COLLAGEN400'],
    filters: '+AND+humans[Filter]+AND+("randomized+controlled+trial"[Publication+Type]+OR+"meta-analysis"[Publication+Type])'
  }
]

async function eutil<T>(url: string, attempt = 0): Promise<T> {
  const res = await fetch(url)
  if (res.status === 429 || res.status >= 500) {
    if (attempt >= 3) throw new Error(`eutil failed after retries: ${res.status}`)
    await new Promise(r => setTimeout(r, 1500 * (attempt + 1)))
    return eutil<T>(url, attempt + 1)
  }
  if (!res.ok) throw new Error(`eutil ${res.status}: ${res.statusText}`)
  return (await res.json()) as T
}

async function eutilText(url: string, attempt = 0): Promise<string> {
  const res = await fetch(url)
  if (res.status === 429 || res.status >= 500) {
    if (attempt >= 3) throw new Error(`eutilText failed after retries: ${res.status}`)
    await new Promise(r => setTimeout(r, 1500 * (attempt + 1)))
    return eutilText(url, attempt + 1)
  }
  if (!res.ok) throw new Error(`eutilText ${res.status}: ${res.statusText}`)
  return await res.text()
}

type ESearchResult = { esearchresult: { idlist: string[]; count: string } }
type ESummaryResult = { result: Record<string, any> }

function buildSearchUrl(ing: Ingredient): string {
  const q = encodeURIComponent(ing.searchTerm)
  return `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=${q}${ing.filters}&retmax=${PER_INGREDIENT_LIMIT}&sort=relevance&retmode=json&tool=${TOOL}&email=${EMAIL}`
}

function buildSummaryUrl(ids: string[]): string {
  return `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&id=${ids.join(',')}&retmode=json&tool=${TOOL}&email=${EMAIL}`
}

function buildFetchUrl(ids: string[]): string {
  // MEDLINE format — structured tagged fields, far easier to parse than abstract-text
  return `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id=${ids.join(',')}&rettype=medline&retmode=text&tool=${TOOL}&email=${EMAIL}`
}

// Parse MEDLINE-tagged records. Each field starts with `TAG - value` at column 0
// and continues on indented lines until the next `TAG -`. PMID, TI (title), and
// AB (abstract) are the fields we need; we also capture FAU (full author) for safety.
function parseAbstracts(raw: string): Map<string, string> {
  const out = new Map<string, string>()
  // Records separated by blank lines; each starts with `PMID-` (note: hyphen).
  // We rely on the fact that field continuations are indented 6 spaces.
  const records = raw.split(/\n\s*\n(?=PMID-)/)
  for (const rec of records) {
    const fields: Record<string, string> = {}
    let currentTag: string | null = null
    for (const line of rec.split('\n')) {
      const m = line.match(/^([A-Z]{2,4})\s*-\s?(.*)$/)
      if (m) {
        currentTag = m[1]
        if (fields[currentTag]) fields[currentTag] += ' ' + m[2]
        else fields[currentTag] = m[2]
      } else if (currentTag && /^\s{2,}\S/.test(line)) {
        fields[currentTag] += ' ' + line.trim()
      }
    }
    const pmid = (fields.PMID || '').trim()
    if (!pmid) continue
    const abstract = (fields.AB || '').replace(/\s+/g, ' ').trim().slice(0, 1800)
    if (abstract) out.set(pmid, abstract)
  }
  return out
}

async function ingestIngredient(ing: Ingredient) {
  console.log(`\n[${ing.handle}] searching: "${ing.searchTerm}"`)
  const searchRes = await eutil<ESearchResult>(buildSearchUrl(ing))
  const ids = searchRes.esearchresult?.idlist || []
  console.log(`  hits: ${searchRes.esearchresult?.count}, taking top ${ids.length}`)
  if (ids.length === 0) {
    writeFileSync(
      join(OUT_DIR, `${ing.handle}.json`),
      JSON.stringify({ handle: ing.handle, searchTerm: ing.searchTerm, generatedAt: new Date().toISOString(), studies: [], note: 'no hits' }, null, 2)
    )
    return
  }
  await new Promise(r => setTimeout(r, 400))
  const summary = await eutil<ESummaryResult>(buildSummaryUrl(ids))
  await new Promise(r => setTimeout(r, 400))
  const fetchRaw = await eutilText(buildFetchUrl(ids))
  const abstracts = parseAbstracts(fetchRaw)

  const studies = ids.map(id => {
    const s = summary.result?.[id] || {}
    return {
      pmid: id,
      title: s.title || '',
      journal: s.fulljournalname || s.source || '',
      year: (s.pubdate || '').slice(0, 4),
      firstAuthor: s.authors?.[0]?.name || s.sortfirstauthor || '',
      authorCount: s.authors?.length || 0,
      publicationTypes: (s.pubtype || []).join(', '),
      doi: s.elocationid?.replace(/^doi:\s*/i, '') || '',
      url: `https://pubmed.ncbi.nlm.nih.gov/${id}/`,
      abstract: abstracts.get(id) || ''
    }
  })

  const doc = {
    handle: ing.handle,
    searchTerm: ing.searchTerm,
    filtersApplied: ing.filters.replace(/^\+AND\+/, ''),
    skus: ing.skus,
    generatedAt: new Date().toISOString(),
    generatedBy: 'scripts/ingest-pubmed-evidence.ts',
    source: 'PubMed E-utilities',
    licence: 'NCBI public domain (USA federal work)',
    totalHits: parseInt(searchRes.esearchresult?.count || '0', 10),
    studies
  }
  const outFile = join(OUT_DIR, `${ing.handle}.json`)
  writeFileSync(outFile, JSON.stringify(doc, null, 2))
  console.log(`  wrote ${outFile} — ${studies.length} studies (${studies.filter(s => s.abstract).length} with abstract)`)
}

async function main() {
  mkdirSync(OUT_DIR, { recursive: true })
  for (const ing of INGREDIENTS) {
    try {
      await ingestIngredient(ing)
    } catch (e: any) {
      console.error(`[${ing.handle}] FAILED: ${e.message}`)
    }
    await new Promise(r => setTimeout(r, 400))
  }
  console.log(`\nDone. Output: ${OUT_DIR}`)
}

await main()
