#!/usr/bin/env bun
// Generate elite-grade first-draft content for the 4 new PDP section types per
// the 21 May + 1 June 2026 Loom briefs from Umar. Inputs:
//   - data/pipeline/pilot-skus.json                              (lock + phase schedule)
//   - data/content/brand-voice/elite-faq-templates.json          (brand-voice FAQs)
//   - data/evidence/<ingredient>.json                            (PubMed meta-analyses + RCTs)
//   - data/intelligence/<sku>/{claims-evidence,competitor-comparison,
//                              conversion-blockers,review-time-mentions,
//                              audience-profile}.json            (per-SKU intelligence)
//   - data/sources/jv-product-truth/<sku>.json                   (product DB truth, if present)
//   - data/content/products/<sku>.json                           (existing product content)
// Outputs:
//   - data/content/pilot-extended/<sku>.json                     (per-SKU drafts with QA scores)
//   - data/content/pilot-extended/_summary.json                  (batch QA roll-up)
//
// Usage: bun scripts/generate-pilot-content.ts [SKU1 SKU2 ...]

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

const ROOT = join(import.meta.dir, '..')
const PILOT_FILE = join(ROOT, 'data', 'pipeline', 'pilot-skus.json')
const ELITE_FAQ_FILE = join(ROOT, 'data', 'content', 'brand-voice', 'elite-faq-templates.json')
const VOICE_GUIDE_PATH = 'data/content/brand-voice/voice-guide.md'
const INTEL_DIR = join(ROOT, 'data', 'intelligence')
const EVIDENCE_DIR = join(ROOT, 'data', 'evidence')
const PRODUCT_TRUTH_DIR = join(ROOT, 'data', 'sources', 'jv-product-truth')
const PRODUCTS_DIR = join(ROOT, 'data', 'content', 'products')
const OUT_DIR = join(ROOT, 'data', 'content', 'pilot-extended')

// Per-SKU primary-active-ingredient → evidence file mapping
const INGREDIENT_MAP: Record<string, string[]> = {
  'JV-D1000': ['cholecalciferol'],
  'JV-D4000': ['cholecalciferol', 'menaquinone-7'],
  'JV-TURMERIC500': ['curcumin', 'piperine-bioperine'],
  'JV-CALCIT': ['calcium-citrate'],
  'JV-COLLAGEN400': ['marine-collagen-peptides']
}

// Forbidden phrases per voice-guide.md
const FORBIDDEN_PHRASES = [
  'game-changer', 'game changer', 'life-changing', 'miracle', 'transforms your',
  'doctor-formulated', 'clinically proven', 'scientifically proven',
  'boosts', 'supercharges', 'fights',
  'detox', 'cleanse',
  'powerhouse', 'potent', 'pure ', 'premium ',
  'amazing', 'incredible', 'revolutionary',
  '[draft]', '[draft ', 'draft template', 'lorem ipsum', 'todo', 'tbd'
]

// Engineering / internal jargon that must never reach customer-facing copy
const ENGINEERING_JARGON = [
  'contamination', 'extraction', 'tagging', 'sku', 'pdp', 'metafield',
  'top3', 'minority', 'cohort', 'cross-product', 'evaluator',
  'expectation management', 'compliance gate', 'source-backed',
  'workstream', 'snippet', 'datapoint'
]

type PilotFaq = { question: string; answer: string; evidenceTier?: string; complianceNotes?: string }
type Pilot = {
  sku: string
  name: string
  category: string
  rationale: string
  timelineCadence: 'weeks' | 'months' | string
  phases: string[]
  productSpecificFaqs?: PilotFaq[]
}
type Pilots = { skus: Pilot[] }

type Claim = {
  handle?: string
  heading?: string
  description?: string
  // claims-library.json uses source_url / source_title (not url / title)
  citation?: {
    source_title?: string
    source_url?: string
    study_or_regulatory_reference?: string
    evidence_type?: string
  }
  stat_1_value?: string | number
  stat_1_name?: string
  stat_2_value?: string | number
  stat_2_name?: string
  stat_3_value?: string | number
  stat_3_name?: string
  disclaimer?: string
  status?: string
  skus?: string[]
}
type ClaimsEvidence = { sku: string; name?: string; n?: number; claims?: Claim[]; complianceGate?: string }

type Competitor = {
  rank?: number; brand?: string; domain?: string; url?: string; status?: string
  signals?: string[]; whitespace?: string
  priceSignal?: string; format?: string; dose?: string; claims?: string[]
}
type ComparisonRow = { rank?: number; attribute?: string; justVitamins?: string; competitorEvidence?: string; recommendation?: string }
type CompetitorComparison = { sku: string; name?: string; competitors?: Competitor[]; comparison?: ComparisonRow[]; jvSignals?: { rank?: number; signal?: string }[] }

type Blocker = { rank?: number; blocker?: string; evidence?: string; fix?: string; confidence?: string }
type ConversionBlockers = { sku: string; blockers?: Blocker[] }

type TimeMention = { reviewIdx: number; stars: number; days: number; matchedPhrase: string; sentence: string }
type ReviewTimeMentions = { sku: string; totalMentions?: number; phases?: { label: string; windowDays: number[]; mentionCount: number; topMentions: TimeMention[] }[] }

type PubMedStudy = { pmid: string; title: string; journal: string; year: string; firstAuthor: string; doi: string; url: string; abstract: string }
type Evidence = { handle: string; searchTerm: string; totalHits?: number; studies?: PubMedStudy[] }

type EliteFaqTemplate = { questionHandle: string; question: string; answer: string; evidenceTier: string; complianceNotes: string }
type EliteFaqLib = { standardFaqs: Record<string, EliteFaqTemplate[]> }

// Shape produced by scripts/sync-jv-product-truth.ts — see that file.
type ProductTruth = {
  sku?: string
  title?: string
  handle?: string
  format?: string
  strength?: string
  strapline?: string
  tags?: string  // Shopify comma-joined tags (includes dietary like "Vegan,Vegetarian,Gluten Free")
  recommendedIntake?: string
  ingredients?: string  // rich text JSON
  allergens?: string
  warning?: string
  legalCategory?: string
  dimensions?: string
  variants?: Array<{
    sku?: string
    option?: string
    price?: string
    compareAtPrice?: string
    barcode?: string
    inventoryQty?: string
    weight?: string
    weightUnit?: string
  }>
}

function readJson<T = any>(p: string, fallback: T | null = null): T | null {
  if (!existsSync(p)) return fallback
  try { return JSON.parse(readFileSync(p, 'utf8')) as T } catch { return fallback }
}

function slug(s: string): string {
  return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60)
}

function brandHandle(brand: string): string {
  return slug(brand || 'competitor')
}

function loadEvidence(skuInfo: Pilot): { ingredients: string[]; evidence: Record<string, Evidence | null> } {
  const ingredients = INGREDIENT_MAP[skuInfo.sku] || []
  const out: Record<string, Evidence | null> = {}
  for (const ing of ingredients) {
    out[ing] = readJson<Evidence>(join(EVIDENCE_DIR, `${ing}.json`))
  }
  return { ingredients, evidence: out }
}

// ============================================================================
// Quality checks — run inline as we build content
// ============================================================================
type QaCheck = { pass: boolean; field: string; reason?: string }

function qaText(field: string, text: string): QaCheck[] {
  const checks: QaCheck[] = []
  if (!text || !text.trim()) {
    checks.push({ pass: false, field, reason: 'empty' })
    return checks
  }
  const lower = text.toLowerCase()
  for (const ph of FORBIDDEN_PHRASES) {
    if (lower.includes(ph)) {
      checks.push({ pass: false, field, reason: `forbidden phrase: "${ph}"` })
    }
  }
  for (const j of ENGINEERING_JARGON) {
    // word-boundary match to avoid hits in URLs / handles
    const re = new RegExp(`\\b${j.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}\\b`, 'i')
    if (re.test(text)) {
      checks.push({ pass: false, field, reason: `engineering jargon: "${j}"` })
    }
  }
  if (/\b(treats|cures|prevents disease|reverses)\b/i.test(text)) {
    checks.push({ pass: false, field, reason: 'medical claim (treats/cures/prevents)' })
  }
  // Catch unresolved placeholder markers
  if (/\[.{2,40}\]/.test(text)) {
    checks.push({ pass: false, field, reason: 'unresolved [...] placeholder' })
  }
  // Catch repeated template scaffolds (same opener used >=2 times in a single block;
  // detected at block-level via qaScaffold below — flag here only the obviously templated)
  if (/^\s*\[draft/i.test(text)) {
    checks.push({ pass: false, field, reason: 'draft prefix' })
  }
  if (checks.length === 0) checks.push({ pass: true, field })
  return checks
}

// Block-level check: identical opening clause across multiple sub-fields ⇒ scaffold
function qaScaffoldRepetition(field: string, texts: string[]): QaCheck[] {
  if (texts.length < 2) return []
  const openings = texts.map(t => (t || '').trim().split(/[,.]/)[0].toLowerCase().slice(0, 60))
  const counts: Record<string, number> = {}
  for (const o of openings) counts[o] = (counts[o] || 0) + 1
  const worst = Object.entries(counts).sort((a, b) => b[1] - a[1])[0]
  if (worst && worst[1] >= 2 && worst[0].length > 20) {
    return [{ pass: false, field, reason: `scaffold repetition: opener "${worst[0]}..." repeats ${worst[1]}×` }]
  }
  return [{ pass: true, field }]
}

function scoreArtefact(checks: QaCheck[]): { score: number; failed: QaCheck[] } {
  const total = checks.length
  const failed = checks.filter(c => !c.pass)
  const score = total === 0 ? 0 : Math.round((1 - failed.length / total) * 100)
  return { score, failed }
}

// ============================================================================
// SCIENTIFIC STUDY
// ============================================================================
function generateScientificStudy(skuInfo: Pilot, claimsEv: ClaimsEvidence | null, evidence: Record<string, Evidence | null>, ingredients: string[]) {
  const qa: QaCheck[] = []
  const efsaClaim = (claimsEv?.claims || []).find(c => c.citation?.source_url && c.heading)
  const pubmedStudies: PubMedStudy[] = []
  for (const ing of ingredients) {
    const ev = evidence[ing]
    if (ev?.studies && ev.studies.length > 0) {
      // Prefer recent meta-analyses / systematic reviews
      const sorted = ev.studies.slice().sort((a, b) => Number(b.year || '0') - Number(a.year || '0'))
      pubmedStudies.push(sorted[0])
    }
  }

  let handle: string, headline: string, body_copy: string, link_url: string, link_label: string, status: string, sources: string[]

  if (efsaClaim) {
    // Tier 1: source-backed claim from claims-evidence (EFSA / UK NHC)
    handle = `${slug(skuInfo.sku)}-${efsaClaim.handle || 'scientific-study'}`.slice(0, 60)
    // Polish multi-slash claim wording into a single readable headline:
    // "Vitamin D contributes to normal immune function / bones / muscle function"
    //  → "Vitamin D contributes to normal immune function, bones, and muscle function"
    const rawHeading = efsaClaim.heading || `${skuInfo.name} — what the research says`
    headline = rawHeading
      .replace(/\s*\/\s*/g, ', ')
      .replace(/, ([^,]+)$/, ', and $1')
      .replace(/\s+,/g, ',')
    // Build a clean, compliance-precise body
    // 1. The regulated claim itself (authorised wording is the headline)
    // 2. A short, honest framing line
    // 3. Optional supporting research note that does NOT make a new claim
    // Polish journal names: drop trailing ": NLM-abbrev" suffixes ("Phytotherapy research : PTR")
    const cleanJournal = (j: string) => (j || '').replace(/\s*:\s*[A-Z]{2,6}$/i, '').trim()
    const supporting = pubmedStudies.length > 0
      ? ` Independent published research on this ingredient — including a recent ${pubmedStudies[0].year} article in ${cleanJournal(pubmedStudies[0].journal)} — continues to investigate dose, form, and population response.`
      : ''
    body_copy = `${headline}. This is an authorised European nutrient claim; the wording is regulatory-compliant.${supporting}`.trim()
    link_url = efsaClaim.citation?.source_url || ''
    link_label = efsaClaim.citation?.source_title || 'Source'
    status = 'source_backed_draft'
    sources = [`data/intelligence/${skuInfo.sku}/claims-evidence.json#${efsaClaim.handle}`, link_url].filter(Boolean)
  } else if (pubmedStudies.length > 0) {
    // Tier 2: no regulated claim, but PubMed evidence exists — frame as experience/research summary
    const top = pubmedStudies[0]
    const ingName = ingredients[0].replace(/-/g, ' ')
    handle = `${slug(skuInfo.sku)}-research-summary`
    headline = `What the research on ${ingName} suggests`
    body_copy = `${ingName.charAt(0).toUpperCase() + ingName.slice(1)} continues to be studied in adult populations. The dose and form in this product are guided by the consensus across published research. No clinical-effect claim is made on this page until human-reviewed compliance language is in place — please refer to the linked study for primary source.`
    link_url = top.url
    link_label = `${top.journal} (${top.year})`
    status = 'needs_compliance_review'
    sources = [`data/evidence/${ingredients[0]}.json#PMID:${top.pmid}`, top.url]
  } else {
    handle = `${slug(skuInfo.sku)}-scientific-study-placeholder`
    headline = 'Scientific evidence (pending)'
    body_copy = 'This block is held back until the citation chain is complete. PubMed and EFSA / UK NHC register lookups are required for this ingredient before any clinical-sounding claim is published on the page.'
    link_url = ''
    link_label = ''
    status = 'needs_evidence'
    sources = [`data/intelligence/${skuInfo.sku}/claims-evidence.json (empty)`, `data/evidence/${ingredients[0] || 'unknown'}.json`]
  }

  // Stat tiles: prefer regulator-precise wording from the matched EFSA claim
  // (stat_1/2/3_value+name are curated regulatory labels — these are perfect for
  // the Scientific Studies tiles). Only fall back to empty if no claim was matched.
  const stats: { value: string; label: string; source?: string }[] = []
  if (efsaClaim) {
    for (let i = 1; i <= 3; i++) {
      const v = (efsaClaim as any)[`stat_${i}_value`]
      const n = (efsaClaim as any)[`stat_${i}_name`]
      if (v && n) stats.push({ value: String(v), label: String(n), source: `claims-library.json#${efsaClaim.handle}.stat_${i}` })
    }
  }

  qa.push(...qaText('scientificStudy.headline', headline))
  qa.push(...qaText('scientificStudy.body_copy', body_copy))

  return {
    handle, name_internal: `${skuInfo.name} — ${headline}`, headline, body_copy,
    link_url, link_label, stats, image: `${slug(skuInfo.sku)}-scientific-study.png`,
    status, sources,
    pubmedReferences: pubmedStudies.slice(0, 3).map(s => ({
      pmid: s.pmid, year: s.year, journal: s.journal, title: s.title, url: s.url
    })),
    qa: scoreArtefact(qa)
  }
}

// ============================================================================
// TIMELINE
// ============================================================================
function generateTimelineBlock(skuInfo: Pilot, claimsEv: ClaimsEvidence | null, tm: ReviewTimeMentions | null, evidence: Record<string, Evidence | null>, ingredients: string[]) {
  const studyClaim = claimsEv?.claims?.find(c => c.citation?.source_url)
  const cadenceWord = skuInfo.timelineCadence === 'weeks' ? 'weeks' : 'months'
  // Phase-position openers per cadence — phase 1 sets expectation, mid phases describe build,
  // final phase frames cumulative or maintenance.
  const openers = {
    months: [
      'The first month is groundwork.',
      'By the second month, the picture starts to fill in.',
      'Month three is where many customers describe the strongest sense of routine paying off.',
      'From here, it becomes a maintenance routine.'
    ],
    weeks: [
      'The first fortnight is about baseline.',
      'Weeks three to four are when blood levels and routine catch up.',
      'By month two, most adults reach the recommended range.',
      'From here, it is a year-round maintenance routine — especially through low-sun months.'
    ]
  }
  const cadenceKey = skuInfo.timelineCadence === 'weeks' ? 'weeks' : 'months'
  const phases = skuInfo.phases.map((label, i) => {
    const bucket = tm?.phases?.find(p => p.label === label)
    const top = (bucket?.topMentions || []).slice(0, 2)
    const qa: QaCheck[] = []
    let body: string
    let phaseStatus: string
    const phaseSources: string[] = []
    const opener = openers[cadenceKey][Math.min(i, openers[cadenceKey].length - 1)]
    if (top.length > 0) {
      const quote = top[0]
      const cleaned = quote.sentence.replace(/\s+/g, ' ').replace(/\.+$/, '').trim()
      const stars = quote.stars
      const days = quote.days
      // Vary the framing line by quote tone heuristic + phase position
      const isQuick = days <= 14
      const isSlow = days >= 60
      const framing =
        i === 0 && isQuick
          ? 'Some customers do feel something quickly — like this one'
          : i === 0
          ? 'Most customers describe these first weeks as cumulative rather than immediate — one review captures it'
          : isSlow
          ? 'By this point reviews like this become more frequent'
          : 'Customer reviews from this window often sound like this'
      const closer =
        i === 0
          ? 'The job of this phase is consistency: same time each day, with food where possible.'
          : i === skuInfo.phases.length - 1
          ? 'Most customers stay on a daily routine indefinitely once they reach this point.'
          : 'Keep the daily routine steady through this window.'
      body = `${opener} ${framing}: "${cleaned}" (${stars}-star review). ${closer}`
      phaseStatus = 'source_backed_draft'
      phaseSources.push(
        `data/intelligence/${skuInfo.sku}/_raw_reviews.json[${quote.reviewIdx}]`,
        `data/intelligence/${skuInfo.sku}/review-time-mentions.json#${label}`
      )
    } else {
      const ev = evidence[ingredients[0]]
      if (ev?.studies && ev.studies.length > 0) {
        const top = ev.studies[0]
        body = `${opener} Few customer reviews mention this exact window — published research (${top.journal}, ${top.year}) suggests consistent daily intake is what produces measurable effects over time. The routine matters more than the dose.`
        phaseStatus = 'evidence_grounded_draft'
        phaseSources.push(`data/evidence/${ingredients[0]}.json#PMID:${top.pmid}`)
      } else {
        body = `${opener} Customer reviews in this window are sparse; the safest framing is that daily consistency, more than dose escalation, is what drives change for this product class.`
        phaseStatus = 'category_default'
        phaseSources.push(`data/pipeline/pilot-skus.json#${skuInfo.sku}.phases`)
      }
    }
    qa.push(...qaText(`timelineBlock.phases[${i}].body_copy`, body))
    return {
      handle: `${slug(skuInfo.sku)}-phase-${i + 1}`,
      phase_label: label,
      body_copy: body,
      status: phaseStatus,
      sources: phaseSources,
      qa: scoreArtefact(qa)
    }
  })

  const blockHandle = `${slug(skuInfo.sku)}-results-timeline`
  const intro = `A look at your first few ${cadenceWord}, drawn from customer reviews and supported by published research on this ingredient.`
  const blockQa: QaCheck[] = []
  blockQa.push(...qaText('timelineBlock.intro_copy', intro))
  // Block-level scaffold check: if every phase opens the same way, flag it
  blockQa.push(...qaScaffoldRepetition('timelineBlock.phases.openings', phases.map((p: any) => p.body_copy || '')))
  return {
    handle: blockHandle,
    name_internal: `${skuInfo.name} — Results Timeline`,
    headline: 'Results',
    subtitle: 'What you can expect',
    intro_copy: intro,
    study_link_label: studyClaim?.citation?.source_title || 'Independent scientific study',
    study_link_url: studyClaim?.citation?.source_url || '',
    phases,
    status: phases.every(p => p.status === 'source_backed_draft') ? 'source_backed_draft' : 'mixed_evidence_draft',
    sources: [`data/intelligence/${skuInfo.sku}/review-time-mentions.json`, ...(studyClaim?.citation?.source_url ? [studyClaim.citation.source_url] : [])],
    qa: scoreArtefact(blockQa)
  }
}

// ============================================================================
// COMPARISON
// ============================================================================
function generateComparisonTable(skuInfo: Pilot, cc: CompetitorComparison | null, productTruth: ProductTruth | null) {
  const truth = productTruth || {}
  if (!cc || ((!cc.comparison || cc.comparison.length === 0) && (!cc.competitors || cc.competitors.length === 0))) {
    return {
      handle: `${slug(skuInfo.sku)}-comparison-placeholder`,
      name_internal: `${skuInfo.name} — Comparison (needs data)`,
      section_title: 'How we compare', subtitle: '',
      columns: [], rows: [],
      status: 'needs_input',
      sources: [`data/intelligence/${skuInfo.sku}/competitor-comparison.json (missing or empty)`],
      qa: { score: 0, failed: [{ pass: false, field: 'comparison', reason: 'no source data' }] }
    }
  }

  const competitors = (cc!.competitors || []).slice(0, 3)
  const columns = [
    { handle: 'column-jv', name: 'Just Vitamins', is_us: true, accent_colour: '#1a2b4d', image: '' },
    ...competitors.map(c => ({
      handle: `column-${brandHandle(c.brand || 'competitor')}`,
      name: c.brand || 'Competitor', is_us: false, accent_colour: '#9aa0a6', image: ''
    }))
  ]

  let rows: any[]
  let status: string
  // Path A: synthesised comparison rows (D1000, TURMERIC500)
  if (cc!.comparison && cc!.comparison.length > 0) {
    rows = cc!.comparison.slice(0, 8).map((row, i) => ({
      handle: `${slug(skuInfo.sku)}-cmp-row-${i + 1}`,
      feature_label: row.attribute || `Feature ${i + 1}`,
      values: {
        '0': { mark: 'text', value: row.justVitamins || '', note: row.recommendation || '' },
        '1': { mark: 'text', value: row.competitorEvidence || '', note: '' }
      },
      status: 'source_backed_draft',
      sources: [`data/intelligence/${skuInfo.sku}/competitor-comparison.json#comparison[${i}]`]
    }))
    status = 'source_backed_draft'
  } else {
    // Path B: attribute matrix from per-competitor {format, dose, priceSignal, claims}
    // JV column filled from product truth
    // Pull JV attributes from the synced product truth (sync-jv-product-truth.ts).
    const jvFormat = truth.format || ''
    const jvStrength = truth.strength || ''
    const firstVariantPrice = truth.variants?.[0]?.price
    const jvPrice = firstVariantPrice ? `£${firstVariantPrice} (${truth.variants?.[0]?.option || 'one pack'})` : ''
    // Shopify tags column carries dietary / certification tags (e.g. "Vegan, Vegetarian, Gluten Free")
    const jvCerts = (truth.tags || '')
      .split(',')
      .map(t => t.trim())
      .filter(t => /vegan|vegetarian|gluten|halal|kosher|organic|dairy|nut|soya|gelatin|allergen|wheat|made in|british|uk/i.test(t))
      .join(', ')
    // jvApplicable=false means the attribute is intentionally competitor-only (e.g.
    // "Opportunity" — about competitor whitespace, not JV's own value). Those rows
    // do NOT count as a "missing JV column" gap.
    const attrSpecs: { label: string; jvValue: string; getter: (c: Competitor) => string; jvApplicable: boolean }[] = [
      { label: 'Format', jvValue: jvFormat, getter: c => c.format || '', jvApplicable: true },
      { label: 'Dose / strength', jvValue: jvStrength, getter: c => c.dose || '', jvApplicable: true },
      { label: 'Price signal', jvValue: jvPrice, getter: c => c.priceSignal || '', jvApplicable: true },
      { label: 'Certifications', jvValue: jvCerts, getter: c => (c.claims || []).filter(x => /vegan|vegetarian|gluten|halal|kosher|organic/i.test(x)).join(', '), jvApplicable: true },
      { label: 'Brand claim', jvValue: 'EFSA-authorised wording only', getter: c => (c.claims || [])[0] || '', jvApplicable: true },
      { label: 'Opportunity (competitor whitespace)', jvValue: '', getter: c => c.whitespace || '', jvApplicable: false }
    ]
    rows = attrSpecs
      .map((spec, i) => {
        const values: Record<string, { mark: string; value: string; note: string }> = {
          '0': { mark: 'text', value: spec.jvValue, note: spec.jvApplicable && !spec.jvValue ? 'Fill from product truth' : '' }
        }
        let hasCompetitor = false
        competitors.forEach((c, ci) => {
          const v = spec.getter(c)
          if (v) hasCompetitor = true
          values[String(ci + 1)] = { mark: 'text', value: v, note: '' }
        })
        if (!hasCompetitor && !spec.jvValue) return null
        const rowStatus = !spec.jvApplicable
          ? 'source_backed_draft'
          : (spec.jvValue ? 'source_backed_draft' : 'needs_product_truth')
        return {
          handle: `${slug(skuInfo.sku)}-cmp-row-${i + 1}`,
          feature_label: spec.label, values,
          status: rowStatus,
          sources: [
            `data/intelligence/${skuInfo.sku}/competitor-comparison.json#competitors[]`,
            productTruth ? `data/sources/jv-product-truth/${skuInfo.sku}.json` : `(no product truth yet)`
          ]
        }
      })
      .filter(Boolean)
    status = rows.some(r => r.status === 'needs_product_truth') ? 'mixed_evidence_draft' : 'source_backed_draft'
  }

  const tableQa: QaCheck[] = []
  for (const r of rows) {
    const jvVal = (r.values as any)['0']?.value || ''
    if (jvVal) tableQa.push(...qaText(`comparison.row[${r.feature_label}].jv`, jvVal))
  }

  return {
    handle: `${slug(skuInfo.sku)}-comparison`,
    name_internal: `${skuInfo.name} — How we compare`,
    section_title: 'How we compare', subtitle: '',
    columns, rows, status,
    sources: [`data/intelligence/${skuInfo.sku}/competitor-comparison.json`],
    qa: scoreArtefact(tableQa)
  }
}

// ============================================================================
// FAQ
// ============================================================================
function generateFaqBlock(skuInfo: Pilot, cb: ConversionBlockers | null, eliteFaqs: EliteFaqLib | null) {
  // Map blocker keywords → customer-voice question + brand-voice answer template.
  // The answer is *not* the blocker.fix string verbatim (that's engineering text);
  // it's a Wild-Nutrition-tone template that gets the evidence cited as a candid note.
  const customerVoiceMap: { keywords: RegExp; question: string; answerTemplate: string }[] = [
    {
      keywords: /\b(break|broken|damaged|crushed|loose|cracked)\b/i,
      question: 'What if my pack arrives damaged?',
      answerTemplate: 'If your order arrives damaged in transit, contact our team within 7 days of delivery and we will replace it. Outer packaging is sized to protect the bottle in transit, and bottles use a tamper-evident seal. We mention this openly because a small number of reviews have flagged transit damage — our replacement policy is the answer when that happens.'
    },
    {
      keywords: /\b(no[- ]?effect|didn'?t notice|no difference|placebo|nothing happened)\b/i,
      question: 'What if I don’t notice a difference?',
      answerTemplate: 'Response to any supplement varies by individual, dose, and routine consistency. If you’ve taken this product daily through the full recommended window and notice no difference, contact our team — our 90-day money-back guarantee covers exactly that situation. We list it candidly because some reviews mention not feeling a change; consistency over weeks is the single biggest factor for most customers.'
    },
    {
      keywords: /\b(taste|flavou?r|aftertaste|chalky|smell)\b/i,
      question: 'What does it taste like?',
      answerTemplate: 'See the product description for the current flavour and format. Most customers describe the taste as clean and easy to take daily; if you prefer to mask it, mix powders into a smoothie or juice.'
    },
    {
      keywords: /\b(swallow|tablet size|capsule size|chok|hard to take)\b/i,
      question: 'Is it easy to swallow?',
      answerTemplate: 'Capsules are sized for daily use; tablets are scored where the format allows. If swallowing is a concern, our team can recommend an alternative format from our range.'
    },
    {
      keywords: /\b(format|effervescent|drink|powder|capsule|tablet form)\b/i,
      question: 'What format does this product come in?',
      answerTemplate: 'See the product header for the current format. Our range includes capsules, tablets, powders, and effervescent drinks across categories — each format suits a different routine.'
    },
    {
      keywords: /\b(dose|dosage|how many|per day|per serving)\b/i,
      question: 'How much should I take per day?',
      answerTemplate: 'Follow the daily intake on the pack — it reflects the dose used in the published research that informs this product. Do not exceed it. If you also take another supplement containing the same ingredient, add up the daily total and stay within the authorised upper limit for adults.'
    },
    {
      keywords: /\b(price|expensive|cost|value|cheap)\b/i,
      question: 'Why is the price what it is?',
      answerTemplate: 'The dose, form, and sourcing are set by the published research on this ingredient — not by hitting a price point. We publish the per-serving cost so you can compare directly with multivitamins or lower-dose alternatives.'
    },
    {
      keywords: /\b(timing|when to take|morning|night|with food|empty stomach)\b/i,
      question: 'When is the best time to take it?',
      answerTemplate: 'Take this product daily at a time that fits your routine. Fat-soluble vitamins (A, D, E, K) absorb better with a meal that contains some fat; water-soluble vitamins (B, C) can be taken with or without food. Consistency matters more than time of day.'
    },
    {
      keywords: /\b(delivery|shipping|arrive|postage|courier)\b/i,
      question: 'When will my order arrive?',
      answerTemplate: 'See the shipping page for current delivery windows. UK standard delivery is typically 2-4 working days from dispatch; tracking is sent by email once your order ships.'
    },
    {
      keywords: /\b(stock|out of stock|availability|sold out)\b/i,
      question: 'Is this product in stock?',
      answerTemplate: 'Stock status is shown on this page. If the item is out of stock, you can join the back-in-stock list and we will email you when it returns.'
    }
  ]
  // Filter blockers: must have content + must NOT be internal-only
  const internalOnlyPatterns = /\b(contamination|extraction|tagging|cross-product|workstream|metafield|tagging error|expectation management|minority)\b/i
  const blockers = (cb?.blockers || [])
    .filter(b => b.blocker && b.fix)
    .filter(b => !internalOnlyPatterns.test(b.blocker || '') && !internalOnlyPatterns.test(b.fix || ''))
    .filter(b => (b.confidence === 'high' || b.confidence === 'medium_public_snippet'))

  // Tier 1 (highest quality): hand-curated product FAQs from pilot-skus.json.
  // These are EFSA-cited, compliance-precise, product-truth-grounded.
  const productPairs: any[] = []
  const seenQuestions = new Set<string>()
  for (const f of (skuInfo.productSpecificFaqs || []).slice(0, 5)) {
    if (!f.question || !f.answer) continue
    if (seenQuestions.has(f.question)) continue
    seenQuestions.add(f.question)
    productPairs.push({
      handle: `${slug(skuInfo.sku)}-faq-product-${productPairs.length + 1}`,
      scope: 'product' as const,
      question: f.question,
      answer: f.answer,
      source_citation_id: `pilot-skus.json#${skuInfo.sku}.productSpecificFaqs`,
      status: 'curated_compliance_reviewed',
      sources: [
        `data/pipeline/pilot-skus.json#${skuInfo.sku}.productSpecificFaqs`,
        f.evidenceTier ? `evidence tier: ${f.evidenceTier}` : '',
        f.complianceNotes ? `compliance: ${f.complianceNotes}` : ''
      ].filter(Boolean),
      evidenceTier: f.evidenceTier,
      complianceNotes: f.complianceNotes,
      qa: scoreArtefact([...qaText('faq.question', f.question), ...qaText('faq.answer', f.answer)])
    })
  }
  // Tier 2 (fallback): keyword-map customer-voice from conversion-blockers.
  // Only fires if curated list didn't yield enough pairs.
  if (productPairs.length < 3) {
    for (const b of blockers) {
      const combined = `${b.blocker || ''} ${b.fix || ''} ${b.evidence || ''}`
      const match = customerVoiceMap.find(m => m.keywords.test(combined))
      if (!match) continue
      if (seenQuestions.has(match.question)) continue
      seenQuestions.add(match.question)
      productPairs.push({
        handle: `${slug(skuInfo.sku)}-faq-product-${productPairs.length + 1}`,
        scope: 'product' as const,
        question: match.question,
        answer: match.answerTemplate,
        source_citation_id: `${skuInfo.sku}/conversion-blockers#rank${b.rank}`,
        status: 'voice_template_keyword_matched',
        sources: [`data/intelligence/${skuInfo.sku}/conversion-blockers.json#rank${b.rank}`, `keyword-match: ${match.question}`],
        qa: scoreArtefact([...qaText('faq.question', match.question), ...qaText('faq.answer', match.answerTemplate)])
      })
      if (productPairs.length >= 3) break
    }
  }

  // Standard-scope: pull from elite-faq-templates.json by category
  const lib = eliteFaqs?.standardFaqs[skuInfo.category] || eliteFaqs?.standardFaqs.vitamin || []
  const standardPairs = lib.map((s, i) => ({
    handle: `${skuInfo.category}-${s.questionHandle}`,
    scope: 'standard' as const,
    question: s.question,
    answer: s.answer,
    source_citation_id: '',
    status: 'voice_reviewed_pending_compliance',
    sources: [`data/content/brand-voice/elite-faq-templates.json#${skuInfo.category}.${s.questionHandle}`, `voice: ${VOICE_GUIDE_PATH}`],
    evidenceTier: s.evidenceTier,
    complianceNotes: s.complianceNotes,
    qa: scoreArtefact([...qaText('faq.question', s.question), ...qaText('faq.answer', s.answer)])
  }))

  const allPairs = [...productPairs, ...standardPairs]
  const status = productPairs.length === 0 ? 'standard_only_voice_reviewed' : 'mixed_source_backed_and_voice_reviewed'
  return {
    handle: `${slug(skuInfo.sku)}-faq`,
    name_internal: `${skuInfo.name} — FAQ block`,
    section_title: 'Frequently asked questions',
    cta_label: 'Still unsure? Get in touch',
    cta_url: '/pages/contact',
    pairs: allPairs,
    status,
    sources: [`data/intelligence/${skuInfo.sku}/conversion-blockers.json`, `data/content/brand-voice/elite-faq-templates.json`],
    qa: { score: allPairs.length ? Math.round(allPairs.reduce((a, p) => a + p.qa.score, 0) / allPairs.length) : 0, failed: allPairs.flatMap(p => p.qa.failed) }
  }
}

// ============================================================================
// PER-SKU
// ============================================================================
async function generateForSku(skuInfo: Pilot, eliteFaqs: EliteFaqLib | null) {
  const intel = join(INTEL_DIR, skuInfo.sku)
  const claimsEv = readJson<ClaimsEvidence>(join(intel, 'claims-evidence.json'))
  const cc = readJson<CompetitorComparison>(join(intel, 'competitor-comparison.json'))
  const cb = readJson<ConversionBlockers>(join(intel, 'conversion-blockers.json'))
  const tm = readJson<ReviewTimeMentions>(join(intel, 'review-time-mentions.json'))
  const productTruth = readJson<ProductTruth>(join(PRODUCT_TRUTH_DIR, `${skuInfo.sku}.json`))
  const { ingredients, evidence } = (function () {
    const ings = INGREDIENT_MAP[skuInfo.sku] || []
    const ev: Record<string, Evidence | null> = {}
    for (const ing of ings) ev[ing] = readJson<Evidence>(join(EVIDENCE_DIR, `${ing}.json`))
    return { ingredients: ings, evidence: ev }
  })()

  const scientificStudy = generateScientificStudy(skuInfo, claimsEv, evidence, ingredients)
  const timelineBlock = generateTimelineBlock(skuInfo, claimsEv, tm, evidence, ingredients)
  const comparisonTable = generateComparisonTable(skuInfo, cc, productTruth)
  const faqBlock = generateFaqBlock(skuInfo, cb, eliteFaqs)

  const blockers: string[] = []
  if (scientificStudy.status === 'needs_evidence') blockers.push('Scientific Studies: no EFSA claim AND no PubMed evidence for primary ingredient')
  if (scientificStudy.status === 'needs_compliance_review') blockers.push('Scientific Studies: PubMed-grounded body but needs compliance sign-off (no EFSA wording)')
  if (timelineBlock.phases.some(p => p.status === 'category_default')) blockers.push('Timeline: at least one phase lacks both review mentions and PubMed evidence')
  if (comparisonTable.status === 'needs_input') blockers.push('Comparison: no competitor data')
  if (comparisonTable.status === 'mixed_evidence_draft') blockers.push('Comparison: some rows missing JV column (product truth not synced)')
  if (faqBlock.pairs.filter(p => p.scope === 'product').length === 0) blockers.push('FAQ: no product-specific pairs (only standard FAQs populated — consider editorial pass)')

  const allChecks = [
    ...(scientificStudy.qa.failed || []),
    ...(timelineBlock.qa.failed || []),
    ...timelineBlock.phases.flatMap((p: any) => p.qa.failed || []),
    ...(comparisonTable.qa.failed || []),
    ...(faqBlock.qa.failed || [])
  ]
  const scores = [scientificStudy.qa.score, timelineBlock.qa.score, comparisonTable.qa.score, faqBlock.qa.score]
  const overallScore = Math.round(scores.reduce((a, b) => a + b, 0) / scores.length)

  const out = {
    sku: skuInfo.sku,
    name: skuInfo.name,
    category: skuInfo.category,
    generatedAt: new Date().toISOString(),
    generatedBy: 'scripts/generate-pilot-content.ts',
    voiceGuide: VOICE_GUIDE_PATH,
    scope: '4 new PDP section types per 21 May + 1 June 2026 Loom briefs',
    primaryIngredients: ingredients,
    scientificStudy, timelineBlock, comparisonTable, faqBlock,
    qa: {
      sectionScores: {
        scientificStudy: scientificStudy.qa.score,
        timelineBlock: timelineBlock.qa.score,
        comparisonTable: comparisonTable.qa.score,
        faqBlock: faqBlock.qa.score
      },
      overallScore,
      failedChecks: allChecks,
      checksRun: ['empty', 'forbidden_phrase', 'medical_claim']
    },
    overallStatus: blockers.length === 0 ? 'draft_ready_for_review' : 'draft_with_gaps',
    blockers
  }

  mkdirSync(OUT_DIR, { recursive: true })
  const outFile = join(OUT_DIR, `${skuInfo.sku}.json`)
  writeFileSync(outFile, JSON.stringify(out, null, 2))
  console.log(`[${skuInfo.sku}]  qa=${overallScore}  status=${out.overallStatus}  blockers=${blockers.length}`)
  return out
}

async function main() {
  const pilot = readJson<Pilots>(PILOT_FILE)
  if (!pilot) { console.error(`Pilot lock missing: ${PILOT_FILE}`); process.exit(1) }
  const eliteFaqs = readJson<EliteFaqLib>(ELITE_FAQ_FILE)
  if (!eliteFaqs) console.warn(`Warning: elite FAQ templates not loaded (${ELITE_FAQ_FILE}). Falling back to empty templates.`)
  const requested = process.argv.slice(2)
  const targets = requested.length ? pilot.skus.filter(s => requested.includes(s.sku)) : pilot.skus
  console.log(`Generating elite-grade pilot content for ${targets.length} SKUs:`)
  const allOut = []
  for (const s of targets) allOut.push(await generateForSku(s, eliteFaqs))

  const summary = {
    generatedAt: new Date().toISOString(),
    voiceGuide: VOICE_GUIDE_PATH,
    skuCount: allOut.length,
    averageQa: Math.round(allOut.reduce((a, o) => a + o.qa.overallScore, 0) / allOut.length),
    perSku: allOut.map(o => ({
      sku: o.sku,
      name: o.name,
      overallScore: o.qa.overallScore,
      sectionScores: o.qa.sectionScores,
      overallStatus: o.overallStatus,
      blockers: o.blockers
    }))
  }
  writeFileSync(join(OUT_DIR, '_summary.json'), JSON.stringify(summary, null, 2))
  console.log(`\nAverage QA score: ${summary.averageQa}/100`)
  console.log(`Summary: ${join(OUT_DIR, '_summary.json')}`)
}

await main()
