// Deterministic analysis engine + sample data. Exposed on window.

const QUESTION_BANKS = {
  Generic: [
    { id: 'q-contact', q: 'How do I contact the company?', intent: 'Contact', match: /(contact|email|phone|support@|\+\d|\(\d{3}\))/i, missing: 'Contact email or phone' },
    { id: 'q-hours', q: 'What are the business hours?', intent: 'Availability', match: /(hours|24\/7|monday|mon-|am-|pm-|am\s*[–-]\s*\d|business hours)/i, missing: 'Business hours' },
    { id: 'q-where', q: 'Where is the company located?', intent: 'Location', match: /(headquarter|based in|located|address|street|suite\s|\bsf\b|san francisco|new york|london|austin)/i, missing: 'Physical or HQ address' },
    { id: 'q-trust', q: 'Why should I trust this company?', intent: 'Trust', match: /(founded|since\s\d{4}|years|customers|trusted by|case stud|review|rating|\d\.\d\s*\/\s*5)/i, missing: 'Social proof, founding date, or customer count' },
    { id: 'q-cta', q: 'What is the next action I should take?', intent: 'CTA', match: /(get started|start free|book a|talk to|sign up|buy|subscribe|contact us|request)/i, missing: 'A primary call-to-action' },
  ],
  SaaS: [
    { id: 'q-trial', q: 'Is there a free trial?', intent: 'Trial', match: /(free trial|free for \d+ days|\d+-day trial|try.*free|no credit card)/i, missing: 'Free trial availability and length' },
    { id: 'q-price', q: 'How much does the cheapest plan cost?', intent: 'Price', match: /\$\s?\d+(\.\d+)?\s*(\/|per)?\s*(mo|month|user|seat|year)?/i, missing: 'A specific dollar amount per plan' },
    { id: 'q-billing', q: 'Is billing monthly or annual?', intent: 'Billing', match: /(billed (monthly|annually|yearly)|per month|per year|annual plan|monthly plan)/i, missing: 'Billing cadence (monthly vs annual)' },
    { id: 'q-cancel', q: 'Can I cancel anytime?', intent: 'Cancellation', match: /(cancel anytime|cancel at any time|no long-term contract|month-to-month)/i, missing: 'Cancellation terms' },
    { id: 'q-seats', q: 'How many users or seats are included?', intent: 'Seats', match: /(\d+\s*(users|seats|members)|unlimited (users|seats)|per (user|seat))/i, missing: 'Seat or user limits per plan' },
    { id: 'q-refund', q: 'Is there a money-back guarantee?', intent: 'Refund', match: /(money-back|refund|guarantee|\d+-day refund)/i, missing: 'Refund or money-back policy' },
    { id: 'q-support', q: 'What support is included?', intent: 'Support', match: /(email support|24\/7 support|priority support|dedicated|chat support|slack)/i, missing: 'Support channel and SLA per plan' },
    { id: 'q-integrations', q: 'What integrations are supported?', intent: 'Integrations', match: /(integrat|api|webhook|slack|zapier|salesforce|hubspot|sso|saml)/i, missing: 'Named integrations or API availability' },
  ],
  Ecommerce: [
    { id: 'q-return-window', q: 'How long is the return window?', intent: 'Returns', match: /(\d+\s*(-|\s)?day(s)?\s*(return|window)|return within \d+)/i, missing: 'Specific return window in days' },
    { id: 'q-return-cost', q: 'Who pays for return shipping?', intent: 'Returns', match: /(free returns|return shipping (is )?(free|paid|covered)|prepaid label|customer pays)/i, missing: 'Who pays return shipping' },
    { id: 'q-refund-time', q: 'How long until I get my refund?', intent: 'Refund', match: /(refund.*\d+\s*(business\s*)?day|processed in \d+|\d+\s*days? to refund)/i, missing: 'Refund processing time' },
    { id: 'q-condition', q: 'What condition must items be in to return?', intent: 'Returns', match: /(unworn|unused|original packaging|tags attached|new condition|original tags)/i, missing: 'Required item condition for returns' },
    { id: 'q-exchange', q: 'Can I exchange for a different size or color?', intent: 'Exchange', match: /(exchange|swap|different size|different color)/i, missing: 'Exchange policy' },
    { id: 'q-sale', q: 'Are sale or final-sale items returnable?', intent: 'Returns', match: /(final sale|sale items|clearance|non-returnable|cannot be returned)/i, missing: 'Final-sale and clearance return rules' },
    { id: 'q-international', q: 'Do you accept international returns?', intent: 'Returns', match: /(international return|outside (the )?(us|united states)|worldwide return)/i, missing: 'International return policy' },
    { id: 'q-how', q: 'How do I start a return?', intent: 'Process', match: /(start a return|return portal|email us|return form|initiate.*return|contact.*to return)/i, missing: 'How to initiate a return' },
  ],
  Services: [
    { id: 'q-svc-price', q: 'How much does this service cost?', intent: 'Price', match: /\$\s?\d+|\bstarting at\b|\bfrom \$/i, missing: 'A starting price or rate' },
    { id: 'q-svc-time', q: 'How long does the engagement take?', intent: 'Timeline', match: /(\d+\s*(weeks|months|days)|timeline|turnaround|delivery in)/i, missing: 'Engagement length or turnaround' },
    { id: 'q-svc-scope', q: 'What is included in the engagement?', intent: 'Scope', match: /(includes|deliverables|scope|you get|we deliver)/i, missing: 'A list of deliverables' },
    { id: 'q-svc-team', q: 'Who will I work with?', intent: 'Team', match: /(team|consultant|specialist|dedicated|founder|principal)/i, missing: 'Team or point-of-contact details' },
    { id: 'q-svc-process', q: 'What does the process look like?', intent: 'Process', match: /(step\s?\d|kickoff|discovery|phase|process|workflow)/i, missing: 'Step-by-step process' },
    { id: 'q-svc-results', q: 'What results have past clients seen?', intent: 'Proof', match: /(case stud|\d+%|increased|reduced|saved|results)/i, missing: 'Client outcomes or case studies' },
    { id: 'q-svc-book', q: 'How do I get started?', intent: 'CTA', match: /(book a call|schedule|consultation|get started|contact us)/i, missing: 'A clear booking CTA' },
  ],
  // Product UX content: notifications, error states, success messages, empty states, transactional emails.
  // These get summarized by Apple Intelligence, Gmail's Gemini, and Android. Different rules apply.
  ProductUX: [
    { id: 'q-ux-what', q: 'What just happened, in one line?', intent: 'Outcome', match: /^(.{0,80}(your|you|payment|refund|order|account|file|message|invoice|password|subscription)\s+(is|was|has been|will|arrives|sent|received|paid|cancelled|updated|saved|deleted|expired|due|coming|ready)\b)/im, missing: 'A first sentence that names the outcome' },
    { id: 'q-ux-when', q: 'When does this happen, exactly?', intent: 'Timing', match: /(\b(today|tomorrow|this (morning|afternoon|evening))\b|\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\s+\d|\b\d{1,2}\/\d{1,2}\b|in \d+ (minute|hour|day|week|business day|business-day)|by \d|\d+\s*(am|pm)|\b\d+\s*business\s*days?\b)/i, missing: 'A specific time, date, or duration (no "soon")' },
    { id: 'q-ux-action', q: 'What should I do next?', intent: 'CTA', match: /(\b(tap|click|open|reply|confirm|review|update|verify|download|reorder|try again|use a different|contact)\b|\bgo to\b)/i, missing: 'A specific next action verb' },
    { id: 'q-ux-why', q: 'Why did this happen?', intent: 'Reason', match: /(because|due to|to (verify|confirm|protect)|since you|after you|reason:)/i, missing: 'A short reason or cause' },
    { id: 'q-ux-amount', q: 'How much money or how many items?', intent: 'Quantity', match: /(\$\s?\d+|\d+\s*(items?|seats?|messages?|files?|users?))/i, missing: 'An exact amount or count' },
    { id: 'q-ux-who', q: 'Who is this from or about?', intent: 'Entity', match: /(from\s+[A-Z][a-zA-Z]+|by\s+[A-Z][a-zA-Z]+|@\w+|\b[A-Z][a-zA-Z]+\s+(team|support|billing))/, missing: 'A named sender, team, or entity' },
    { id: 'q-ux-recover', q: 'If something went wrong, how do I fix it?', intent: 'Recovery', match: /(try (again|a different)|use a different|update your|check your|contact (support|us)|tap retry|wait and|review the)/i, missing: 'A recovery path when the action failed' },
    { id: 'q-ux-stakes', q: 'What happens if I ignore this?', intent: 'Consequence', match: /(or your|otherwise|after \d+|access (will be|ends)|will expire|will pause|will be (closed|cancelled|removed))/i, missing: 'The consequence of inaction' },
  ],
};

// Add Generic baseline to each industry (but not Product UX, which has its own logic)
for (const k of ['SaaS', 'Ecommerce', 'Services']) {
  QUESTION_BANKS[k] = [...QUESTION_BANKS.Generic.slice(0, 3), ...QUESTION_BANKS[k]];
}

const VAGUE_TERMS = [
  { term: 'world-class', why: '{Aud}s want the number, not the adjective. Replace with a real metric.' },
  { term: 'best-in-class', why: '{Aud}s want the number, not the adjective. Replace with a ranking or stat.' },
  { term: 'cutting-edge', why: '{Aud}s want to know what it actually does. Name the capability.' },
  { term: 'state-of-the-art', why: '{Aud}s want to know what it actually does. Name the capability.' },
  { term: 'industry-leading', why: '{Aud}s want proof, not posture. Cite the ranking or share.' },
  { term: 'seamless', why: '{Aud}s want to know what just got handled for them. Describe the actual step.' },
  { term: 'powerful', why: '{Aud}s want to know what it does. Name the feature.' },
  { term: 'robust', why: '{Aud}s want to know what it does. Name the feature.' },
  { term: 'innovative', why: '{Aud}s want to know what is new. Describe it.' },
  { term: 'simple', why: '{Aud}s want to know how. Show the steps.' },
  { term: 'easy', why: '{Aud}s want to know how. Show the steps.' },
  { term: 'fast', why: '{Aud}s want a number. Give one.' },
  { term: 'affordable', why: '{Aud}s want the price. Give it.' },
  { term: 'flexible', why: '{Aud}s want to know what their options are. List them.' },
  { term: 'scalable', why: '{Aud}s want the limit or capacity. State it.' },
];

// Idiom-risk: AI summarizers read keywords without context.
// "Nikki Glaser killed it" became "Nikki Glaser was killed" in Apple Intelligence.
// Flag these wherever they show up in product copy meant for AI-readable channels.
const IDIOM_RISK = [
  { term: 'killed it', why: 'A summarizer can read this as a death event. Say what actually happened.' },
  { term: 'crushed it', why: 'A summarizer strips the figurative meaning. Say what was achieved.' },
  { term: 'blew up', why: 'A summarizer can read this literally. Replace with the real outcome.' },
  { term: 'on fire', why: 'A summarizer can read this literally. Give the metric instead.' },
  { term: 'going to die', why: 'A summarizer turns this into bad news. Say what the {aud} will get.' },
  { term: 'to die for', why: 'A summarizer can flatten this into a concern. Use a concrete claim.' },
  { term: 'out of this world', why: 'A summarizer drops the figurative meaning. Say what makes it good.' },
  { term: 'over the moon', why: 'A summarizer drops the figurative meaning. State the outcome plainly.' },
  { term: 'kicking off', why: 'A summarizer can flag this as violence. Use "starts" or "begins".' },
  { term: 'shoot you a', why: 'Skip violence-coded idioms in transactional copy. Use "send you a".' },
];

// Vague time words. "Soon" is the single biggest offender Michelle calls out.
const VAGUE_TIME = [
  { term: 'soon', why: '{Aud}s need to know when to look for it. Give a date or a duration.' },
  { term: 'shortly', why: '{Aud}s need to know when. Replace with minutes or a date.' },
  { term: 'in a bit', why: '{Aud}s need to know when. Replace with minutes or a date.' },
  { term: 'any moment now', why: '{Aud}s need a real ETA. Give one.' },
  { term: 'any moment', why: '{Aud}s need a real ETA. Give one.' },
  { term: 'momentarily', why: '{Aud}s need to know when. Give seconds or minutes.' },
  { term: 'in no time', why: '{Aud}s need to know how long. Give a duration.' },
  { term: 'check back later', why: '{Aud}s need to know exactly when. Give a date.' },
  { term: 'check back soon', why: '{Aud}s need to know exactly when. Give a date.' },
  { term: 'coming soon', why: '{Aud}s need a date. Give a launch month at minimum.' },
];

const WEAK_CTAS = ['learn more', 'click here', 'read more', 'see more', 'find out more', 'get in touch'];

// Audience-term clusters. Alternating between these on the same page confuses AI.
const AUDIENCE_TERMS = ['users', 'customers', 'clients', 'members', 'shoppers', 'subscribers'];

function stripHtml(s) {
  return s
    .replace(/<script[\s\S]*?<\/script>/gi, ' ')
    .replace(/<style[\s\S]*?<\/style>/gi, ' ')
    .replace(/<[^>]+>/g, ' ')
    .replace(/&nbsp;/g, ' ')
    .replace(/&amp;/g, '&')
    .replace(/\s+/g, ' ')
    .trim();
}

// The plain-text the analysis ran on — used to verify the model never quotes
// or invents a fact that isn't actually on the page.
function analysisSource(raw, mode) {
  return (mode === 'html' ? stripHtml(raw) : String(raw || '')).toLowerCase();
}

// Guard against invented hard facts. Any $amount, percentage, or 3+ digit
// number in AI-written copy that does NOT appear in the source page gets
// wrapped in [brackets] so it reads as a placeholder, never a claim.
// Already-bracketed spans are left untouched.
function bracketInventedFacts(text, source) {
  if (!text) return text;
  const src = String(source || '').toLowerCase();
  return text.replace(/\[[^\]]*\]|(\$\s?\d[\d,.]*|\b\d[\d,.]*\s?%|\b\d[\d,.]{2,}\b)/g, (m, num) => {
    if (!num) return m; // a bracketed span — leave as-is
    const norm = num.toLowerCase().replace(/\s+/g, '');
    if (src.replace(/\s+/g, '').includes(norm) || src.includes(num.toLowerCase())) return num;
    return `[${num.trim()}]`;
  });
}

function findSnippet(text, regex) {
  const m = text.match(regex);
  if (!m) return null;
  const idx = m.index ?? text.indexOf(m[0]);
  const start = Math.max(0, idx - 50);
  const end = Math.min(text.length, idx + m[0].length + 80);
  let snip = text.slice(start, end).trim();
  if (start > 0) snip = '…' + snip;
  if (end < text.length) snip = snip + '…';
  return { snippet: snip, hit: m[0] };
}

function extractPlanNames(text) {
  const found = new Set();
  const candidates = ['Free', 'Starter', 'Basic', 'Pro', 'Plus', 'Premium', 'Business', 'Team', 'Growth', 'Enterprise', 'Scale', 'Standard'];
  for (const c of candidates) {
    const re = new RegExp(`\\b${c}\\b\\s*(plan|tier|–|-|\\$|/)`, 'i');
    if (re.test(text)) found.add(c);
  }
  // also pick out plan with prices nearby
  const planLines = text.match(/[A-Z][a-zA-Z]+\s*[–-]?\s*\$\s?\d+/g) || [];
  for (const l of planLines) {
    const name = l.split(/[–\-$]/)[0].trim();
    if (name && name.length < 20) found.add(name);
  }
  return [...found].slice(0, 5);
}

function extractFAQ(text) {
  const out = [];
  // Look for question patterns
  const qRe = /([A-Z][^.?!]{8,140}\?)\s+([^?]{20,300}?)(?=\s+[A-Z][^.?!]{8,140}\?|$)/g;
  let m;
  while ((m = qRe.exec(text)) && out.length < 6) {
    out.push({ q: m[1].trim(), a: m[2].trim().replace(/\s+/g, ' ').slice(0, 280) });
  }
  return out;
}

function extractOrgName(text) {
  const m = text.match(/(?:^|\.\s)([A-Z][A-Za-z]+(?:\s[A-Z][A-Za-z]+)?)\s+(?:helps|is|offers|provides|builds|makes)/);
  if (m) return m[1];
  return null;
}

function runAnalysis({ raw, mode, preset, ai }) {
  const text = mode === 'html' ? stripHtml(raw) : raw.replace(/\s+/g, ' ').trim();
  const bank = QUESTION_BANKS[preset] || QUESTION_BANKS.Generic;
  // Audience noun. When the AI has read the page, trust its read of who the
  // page is for. Otherwise fall back to the preset-based guess.
  const aud = (
    ai && ai.audience
      ? ai.audience
      : preset === 'SaaS' || preset === 'Services'
        ? 'customer'
        : preset === 'Ecommerce'
          ? 'customer'
          : preset === 'ProductUX'
            ? 'user'
            : 'reader'
  );
  const audPlural = aud + 's';
  const Aud = aud.charAt(0).toUpperCase() + aud.slice(1);
  const AudPlural = audPlural.charAt(0).toUpperCase() + audPlural.slice(1);
  const sub = (s) => (s || '').replace(/\{Aud\}/g, Aud).replace(/\{aud\}/g, aud);

  // Results. When the AI has generated page-specific questions, use those (with
  // its own answered/evidence read). Otherwise grade against the preset bank.
  const results = ai && ai.questions
    ? ai.questions.map((q, i) => ({
        id: q.id || `q-ai-${i}`,
        q: sub(q.q),
        intent: q.intent || 'detail',
        match: null,
        missing: q.missing || 'the specific fact a reader needs here',
        suggested: q.suggested || null,
        pass: !!q.pass,
        snippet: q.snippet || null,
        hit: q.hit || null,
      }))
    : bank.map((item) => {
        const found = findSnippet(text, item.match);
        return {
          ...item,
          pass: !!found,
          snippet: found?.snippet || null,
          hit: found?.hit || null,
        };
      });

  const passed = results.filter((r) => r.pass).length;
  const failed = results.length - passed;
  const failRate = Math.round((failed / results.length) * 100);

  // Vague terms + weak CTAs in original (preserve casing for highlight)
  const original = mode === 'html' ? stripHtml(raw) : raw;
  const flags = [];
  for (const v of VAGUE_TERMS) {
    const re = new RegExp(`\\b${v.term}\\b`, 'gi');
    let mm;
    while ((mm = re.exec(original))) {
      flags.push({ start: mm.index, end: mm.index + mm[0].length, severity: 'amber', kind: 'vague', text: mm[0], why: sub(v.why) });
      if (flags.length > 30) break;
    }
  }
  for (const c of WEAK_CTAS) {
    const re = new RegExp(`\\b${c}\\b`, 'gi');
    let mm;
    while ((mm = re.exec(original))) {
      flags.push({ start: mm.index, end: mm.index + mm[0].length, severity: 'rose', kind: 'cta', text: mm[0], why: 'Your buttons should tell people what they do. Use 4 to 8 specific words.' });
    }
  }
  // Idiom risk: Apple Intelligence has misread "killed it" as "was killed". Flag any idiom AI may strip context from.
  for (const i of IDIOM_RISK) {
    const re = new RegExp(`\\b${i.term.replace(/\s+/g, '\\s+')}\\b`, 'gi');
    let mm;
    while ((mm = re.exec(original))) {
      flags.push({ start: mm.index, end: mm.index + mm[0].length, severity: 'rose', kind: 'idiom', text: mm[0], why: sub(i.why) });
    }
  }
  // Vague time words: "soon", "shortly", "any moment now". The #1 Michelle Savage offender.
  for (const t of VAGUE_TIME) {
    const re = new RegExp(`\\b${t.term.replace(/\s+/g, '\\s+')}\\b`, 'gi');
    let mm;
    while ((mm = re.exec(original))) {
      flags.push({ start: mm.index, end: mm.index + mm[0].length, severity: 'amber', kind: 'time', text: mm[0], why: sub(t.why) });
    }
  }
  // unanchored pronouns at start of sentence (it/this/they without antecedent in same sentence)
  const sentRe = /(^|[.!?]\s+)(It|This|They|These|That)\b/g;
  let pm;
  while ((pm = sentRe.exec(original))) {
    flags.push({
      start: pm.index + pm[1].length,
      end: pm.index + pm[1].length + pm[2].length,
      severity: 'amber',
      kind: 'pronoun',
      text: pm[2],
      why: sub("{Aud}s can't tell what 'this' refers to. Name the actual product, plan, or policy."),
    });
  }
  flags.sort((a, b) => a.start - b.start);

  // Term-consistency check: alternating audience nouns on the same page confuses AI.
  // Only flag when 2+ different terms each appear 2+ times.
  const termCounts = {};
  for (const t of AUDIENCE_TERMS) {
    const matches = original.match(new RegExp(`\\b${t}\\b`, 'gi'));
    if (matches && matches.length >= 2) termCounts[t] = matches.length;
  }
  const audienceTermsUsed = Object.keys(termCounts);
  const termInconsistency = audienceTermsUsed.length >= 2 ? audienceTermsUsed : [];

  // Schema generation
  const planNames = extractPlanNames(text);
  const orgName = extractOrgName(text) || 'Your Company';
  const faqs = extractFAQ(text);

  // Scorecard
  const passRate = passed / results.length;
  const actionability = Math.max(1, Math.round(passRate * 5));
  const vagueCount = flags.filter((f) => f.kind === 'vague').length;
  const ctaCount = flags.filter((f) => f.kind === 'cta').length;
  const clarity = Math.max(1, Math.min(5, 5 - Math.min(4, Math.floor(vagueCount / 2)) - (ctaCount > 0 ? 1 : 0)));
  const wordCount = text.split(/\s+/).length;
  const structure = wordCount < 80 ? 2 : /(^|\n)\s*[-•\d+]\s/.test(raw) || /<h[1-6]/i.test(raw) ? 4 : 3;
  const entityExplicit = (planNames.length > 0 ? 2 : 0) + (orgName !== 'Your Company' ? 2 : 0) + (/(®|©|trademark)/.test(text) ? 1 : 0) + 1;
  const schemaReady = (planNames.length > 0 ? 2 : 0) + (faqs.length > 0 ? 2 : 0) + 1;

  // ---------- Agent-readiness signals (unique to this tool) ----------
  // Citation-readiness: declarative, self-contained sentences agents will quote
  const sentences = original
    .split(/(?<=[.!?])\s+/)
    .map((s) => s.trim())
    .filter((s) => s.length > 12 && s.length < 320);
  const quotableSentences = sentences.filter((s) => {
    if (/^(it|this|they|that|these|those|here|there)\b/i.test(s)) return false;
    if (VAGUE_TERMS.some((v) => new RegExp(`\\b${v.term}\\b`, 'i').test(s))) return false;
    if (s.split(/\s+/).length < 6) return false;
    return /(\$|\d|\b[A-Z][a-z]+ [A-Z]|\b[A-Z]{2,}\b)/.test(s);
  });
  const quotableRatio = sentences.length > 0 ? quotableSentences.length / sentences.length : 0;
  const citationScore = Math.max(1, Math.min(5, Math.round(quotableRatio * 5)));

  // Recency: agents prefer recent, dated content
  const hasRecentDate = /\b20(2[3-9]|3\d)\b/.test(text) || /\b(updated|last updated|as of)\b/i.test(text);
  const recencyScore = hasRecentDate ? 5 : 2;

  // Entity coherence
  const productAliases = ['platform', 'tool', 'product', 'service', 'solution', 'app', 'software', 'system'];
  const aliasUsed = productAliases.filter((a) => new RegExp(`\\bour ${a}\\b`, 'i').test(original));
  const entityIssues = [];
  if (aliasUsed.length >= 2) {
    entityIssues.push(`refers to itself as "our ${aliasUsed.slice(0, 2).join('", "our ')}"`);
  }
  if (planNames.length === 0 && (preset === 'SaaS' || preset === 'Services')) {
    entityIssues.push('no named tiers or plans');
  }

  // ---------- Front-load + summary-safe (Michelle Savage rules) ----------
  // Front-loaded outcome: the first sentence/header of an important block should
  // put the action or outcome in the first 5 to 7 words. Apple Intelligence and
  // Gmail summarizers extract the first clause they see.
  const firstSentence = (sentences[0] || '').trim();
  const firstWords = firstSentence.split(/\s+/).slice(0, 7).join(' ');
  const FRONTLOAD_VERBS = /\b(arrives|paid|sent|saved|cancelled|updated|expired|due|ready|received|added|removed|deleted|approved|denied|delivered|charged|refunded|locked|paused|enabled|disabled|scheduled|started|ended|completed|failed|published|shipped|tracked|booked|confirmed)\b/i;
  const frontLoaded = !!firstSentence && (
    FRONTLOAD_VERBS.test(firstWords) ||
    /\$\s?\d/.test(firstWords) ||
    /^(your|you|the (refund|payment|order|invoice|file|message|account|trial|subscription))/i.test(firstSentence)
  );
  const weakOpener = /^(here|welcome|thanks|hi|hey|hello|something|introducing|we['']ve|we are|let)/i.test(firstSentence);
  const frontLoadScore = !firstSentence ? 1 : frontLoaded ? 5 : weakOpener ? 1 : 3;

  // Summary-safe: how likely an AI summarizer is to mangle this page.
  const idiomCount = flags.filter((f) => f.kind === 'idiom').length;
  const vagueTimeCount = flags.filter((f) => f.kind === 'time').length;
  const summarySafeScore = Math.max(1, 5 - idiomCount * 2 - vagueTimeCount);

  // Term consistency: alternating audience nouns confuses AI.
  const termConsistencyScore = termInconsistency.length === 0 ? 5 : termInconsistency.length === 2 ? 3 : 2;

  const scorecard = [
    {
      name: 'Agent actionability',
      score: actionability,
      why: `${passed} of ${results.length} ${aud} questions answered in your content.`,
    },
    {
      name: 'Citation-readiness',
      score: citationScore,
      why:
        quotableSentences.length > 0
          ? `${quotableSentences.length} of ${sentences.length} sentences stand on their own.`
          : 'Sentences need surrounding context to make sense. Make each one quotable on its own.',
    },
    {
      name: 'Plain-language clarity',
      score: clarity,
      why:
        vagueCount + ctaCount > 0
          ? `${vagueCount} hype phrase${vagueCount === 1 ? '' : 's'}, ${ctaCount} weak button${ctaCount === 1 ? '' : 's'} in the way of the facts.`
          : 'Specific language throughout.',
    },
    {
      name: 'Structure',
      score: structure,
      why: structure >= 4 ? 'Headings and lists give clear places to land.' : `Mostly prose. Add headings and lists ${audPlural} can scan.`,
    },
    {
      name: 'Entity coherence',
      score: Math.min(5, entityExplicit - entityIssues.length),
      why:
        entityIssues.length === 0
          ? planNames.length
            ? `Plans named outright: ${planNames.join(', ')}.`
            : 'Things are named, not gestured at.'
          : `${AudPlural} can't tell what's being referred to: ${entityIssues.join('; ')}.`,
    },
    {
      name: 'Recency signal',
      score: recencyScore,
      why: hasRecentDate ? 'Current dates in your content.' : `No 'updated' date or current year. ${AudPlural} wonder how recent this is.`,
    },
    {
      name: 'Structured-data readiness',
      score: Math.min(5, schemaReady),
      why: faqs.length ? `${faqs.length} Q&A pair${faqs.length === 1 ? '' : 's'}, scannable and parseable.` : `No question-and-answer pattern. Add 4 to 6 Q&A pairs ${audPlural} actually ask.`,
    },
    {
      name: 'Front-loaded outcome',
      score: frontLoadScore,
      why: !firstSentence
        ? 'Nothing to score yet.'
        : frontLoaded
          ? `First sentence leads with the fact: "${firstWords}…"`
          : weakOpener
            ? `Opens with "${firstWords}…". The first clause is what summaries grab. Put the fact there.`
            : `Outcome isn't in the first few words. Move "${firstWords.split(' ').slice(-3).join(' ')}" to the front.`,
    },
    {
      name: 'Summary-safe',
      score: summarySafeScore,
      why:
        idiomCount + vagueTimeCount === 0
          ? 'Nothing here a summarizer is likely to mangle.'
          : `${idiomCount} idiom${idiomCount === 1 ? '' : 's'} and ${vagueTimeCount} vague time word${vagueTimeCount === 1 ? '' : 's'} a summary will mangle.`,
    },
    {
      name: 'Term consistency',
      score: termConsistencyScore,
      why:
        termInconsistency.length === 0
          ? `One word for the ${aud}, used consistently.`
          : `Page switches between "${termInconsistency.join('", "')}". Pick one.`,
    },
  ];

  const agentSignals = {
    quotableCount: quotableSentences.length,
    sentenceCount: sentences.length,
    hasRecentDate,
    entityIssues,
    sampleQuotable: quotableSentences.slice(0, 3),
  };

  // Additions for failed questions
  const additionTemplates = {
    'q-trial': `**Free trial.** Yes. Try [PRODUCT] free for [N] days, no credit card required. After the trial, pick a plan or your account pauses.`,
    'q-price': `**Pricing.** [PLAN_A] is $[PRICE_A]/month per user. [PLAN_B] is $[PRICE_B]/month per user. [PLAN_C] is custom (contact us).`,
    'q-billing': `**Billing.** All plans are billed [monthly OR annually]. Annual plans save [N]%. You can switch cadence from your billing page anytime.`,
    'q-cancel': `**Cancellation.** You can cancel anytime from Settings → Billing. Your account stays active until the end of the paid period. We don't email you to win you back.`,
    'q-seats': `**Seats.** [PLAN_A] includes [N] seats. [PLAN_B] includes [N] seats. Additional seats are $[PRICE]/month each.`,
    'q-refund': `**Refunds.** If you cancel within [N] days of your first payment, email [billing@example.com] for a full refund. No forms.`,
    'q-support': `**Support.** [PLAN_A]: email support, replies within 24 hours. [PLAN_B]: priority chat, replies within 2 hours. [PLAN_C]: dedicated Slack channel and a named CSM.`,
    'q-integrations': `**Integrations.** Native: [Slack, Salesforce, HubSpot, Google Workspace]. API: REST + webhooks. SSO: SAML on [PLAN_C].`,
    'q-return-window': `**Return window.** You have [30] days from the delivery date to start a return. After that, items are final sale.`,
    'q-return-cost': `**Return shipping.** [Returns are free. We email a prepaid label.] OR [Customers pay return shipping, typically $[X]].`,
    'q-refund-time': `**Refund timing.** Once we receive your return, refunds are processed within [3 to 5] business days to your original payment method.`,
    'q-condition': `**Item condition.** Items must be unworn, unwashed, with original tags attached and in original packaging.`,
    'q-exchange': `**Exchanges.** Yes. Start a return and reorder the size or color you want. We'll waive shipping on the new order.`,
    'q-sale': `**Sale items.** Items marked Final Sale (50% off or more) are not eligible for return or exchange.`,
    'q-international': `**International returns.** We accept returns from [COUNTRIES]. Customer pays return shipping. Original duties are non-refundable.`,
    'q-how': `**How to return.** 1. Go to [/returns]. 2. Enter your order number and email. 3. Print the prepaid label. 4. Drop off at any [USPS/FedEx] location.`,
    'q-contact': `**Contact.** Email [hello@example.com]. Phone [+1 (555) 123-4567], Mon–Fri 9am–6pm ET.`,
    'q-hours': `**Hours.** Mon–Fri 9am–6pm ET. Closed weekends and US federal holidays.`,
    'q-where': `**Location.** Headquartered in [San Francisco, CA]. Remote team across [US, EU].`,
    'q-trust': `**About us.** Founded in [YEAR]. Trusted by [N,000+] customers including [LOGO, LOGO, LOGO]. [4.8/5] across [N] reviews.`,
    'q-cta': `**Next step.** [Start your free trial.] Takes 60 seconds, no card required.`,
    'q-svc-price': `**Pricing.** Engagements start at $[PRICE]. Final scope is quoted after a 30-minute discovery call.`,
    'q-svc-time': `**Timeline.** A typical engagement runs [4 to 6] weeks from kickoff to delivery.`,
    'q-svc-scope': `**What you get.** [Deliverable 1], [Deliverable 2], [Deliverable 3]. Two rounds of revisions included.`,
    'q-svc-team': `**Who you'll work with.** A [Senior Strategist] leads the engagement, supported by [Designer, Engineer]. Same team start to finish.`,
    'q-svc-process': `**Process.** 1) Discovery call. 2) Audit and proposal. 3) Build phase. 4) Review and handoff.`,
    'q-svc-results': `**Results.** [Client A] saw [+N%] in [METRIC]. [Client B] reduced [METRIC] by [N%]. Full case studies on request.`,
    'q-svc-book': `**Get started.** [Book a 30-minute discovery call.] No prep needed.`,
    // Product UX content templates (Michelle Savage rules: front-load outcome, exact times, named actions)
    'q-ux-what': `**Lead with the outcome.** Rewrite the first sentence so the action is in the first 5 words.\n\nBefore: "Something exciting is here."\nAfter: "Track all your payments in one place."`,
    'q-ux-when': `**Give an exact time.** Replace "soon" or "shortly" with a date, duration, or business-day count.\n\nBefore: "Your refund will arrive shortly."\nAfter: "Your refund arrives in 3 to 5 business days."`,
    'q-ux-action': `**Name the next action.** Use a verb the user can act on, with what happens next.\n\nBefore: "Check it out."\nAfter: "Tap Confirm to send the payment."`,
    'q-ux-why': `**Say why.** One short clause is enough.\n\nBefore: "We need you to verify."\nAfter: "Verify your email so we can send your receipts there."`,
    'q-ux-amount': `**State the number.** Exact dollars or count, no rounding into "a few".\n\nBefore: "We charged your card."\nAfter: "We charged $42.18 to your Visa ending 4032."`,
    'q-ux-who': `**Name the sender.** Agents and users both want to know who.\n\nBefore: "You have a new message."\nAfter: "Priya from PayPal Support replied to your case."`,
    'q-ux-recover': `**Give the recovery path.** What to do if it failed.\n\nBefore: "Payment didn't go through."\nAfter: "Your payment didn't go through. Try again or use a different card."`,
    'q-ux-stakes': `**State the consequence.** What happens if they ignore it, with timing.\n\nBefore: "Please verify your email."\nAfter: "Verify your email by Nov 20 or your account pauses."`,
  };

  const additions = results
    .filter((r) => !r.pass)
    .map((r) => ({
      id: r.id,
      q: r.q,
      template: r.suggested
        ? r.suggested
        : additionTemplates[r.id] || `**${r.intent}.** [Add a one-sentence answer to: ${r.q}]`,
    }));

  // Rewrites: pick up to 4 vague/CTA flags, propose stronger version
  const rewriteSuggestions = {
    'world-class': 'used by [N,000+] teams across [N] countries',
    'best-in-class': 'ranked #1 in [G2 / Forrester / specific list]',
    'cutting-edge': 'powered by [specific technology, e.g. retrieval-augmented LLMs]',
    'state-of-the-art': 'powered by [specific technology]',
    'industry-leading': '#[N] in [specific market], according to [source]',
    seamless: 'syncs in under [N] seconds, no setup required',
    powerful: 'handles [N M] events per day per workspace',
    robust: 'tested at [N] req/sec with [99.99]% uptime',
    innovative: 'first to ship [specific capability]',
    simple: 'set up in [under 2 minutes]',
    easy: 'set up in [under 2 minutes]',
    fast: 'returns results in under [200ms]',
    affordable: 'starts at $[19]/month',
    flexible: 'monthly, annual, or per-seat. Switch anytime',
    scalable: 'tested up to [10M] records per workspace',
    'learn more': 'See pricing →',
    'click here': 'Start your free trial →',
    'read more': 'Read the case study →',
    'see more': 'See full pricing →',
    'find out more': 'Book a 15-min call →',
    'get in touch': 'Email hello@example.com →',
    // Idiom risk replacements (Michelle Savage: AI strips figurative meaning)
    'killed it': 'hosted a sold-out show',
    'crushed it': 'hit [N]% above target',
    'blew up': 'reached [N] views in [N] hours',
    'on fire': 'hit [N] sign-ups this week',
    'going to die': 'will love this',
    'to die for': 'voted #1 by [N] customers',
    'out of this world': 'rated [4.9]/5 by [N] customers',
    'over the moon': 'thrilled. [Add specific outcome]',
    'kicking off': 'starting',
    'shoot you a': 'send you a',
    // Vague time replacements (Michelle Savage: front-load exact times)
    soon: '[on Nov 20] OR [in 3 business days]',
    shortly: '[in under 2 minutes] OR [by 5pm ET]',
    'in a bit': '[in 5 minutes] OR [by 3pm ET]',
    'any moment now': '[in the next minute] OR [by 9am Nov 20]',
    'any moment': '[in the next minute]',
    momentarily: '[in 30 seconds]',
    'in no time': '[in under 60 seconds]',
    'check back later': '[Check back on Nov 20]',
    'check back soon': '[Available Nov 20]',
    'coming soon': '[Available Nov 20]',
  };
  const rewriteFlags = flags.filter((f) => f.kind === 'vague' || f.kind === 'cta' || f.kind === 'idiom' || f.kind === 'time').slice(0, 6);
  const rewrites = rewriteFlags.map((f) => {
    const sentenceStart = original.lastIndexOf('.', f.start) + 1;
    const sentenceEnd = (() => {
      const idx = original.indexOf('.', f.end);
      return idx === -1 ? Math.min(original.length, f.end + 80) : idx + 1;
    })();
    const before = original.slice(sentenceStart, sentenceEnd).trim();
    const replacement = rewriteSuggestions[f.text.toLowerCase()] || '[specific, measurable claim]';
    const after = before.replace(new RegExp(`\\b${f.text}\\b`, 'i'), replacement);
    return {
      before,
      after,
      why: f.why,
      kind: f.kind,
    };
  });

  // JSON-LD blocks
  const faqJson =
    faqs.length > 0
      ? {
          '@context': 'https://schema.org',
          '@type': 'FAQPage',
          mainEntity: faqs.map((f) => ({
            '@type': 'Question',
            name: f.q,
            acceptedAnswer: { '@type': 'Answer', text: f.a },
          })),
        }
      : {
          '@context': 'https://schema.org',
          '@type': 'FAQPage',
          mainEntity: results
            .filter((r) => r.pass)
            .slice(0, 4)
            .map((r) => ({
              '@type': 'Question',
              name: r.q,
              acceptedAnswer: { '@type': 'Answer', text: r.snippet?.replace(/^…|…$/g, '').trim() || '' },
            })),
        };

  const productJson = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: orgName,
    offers:
      planNames.length > 0
        ? planNames.map((p) => ({
            '@type': 'Offer',
            name: p,
            priceCurrency: 'USD',
            price: '[PRICE]',
            url: '[URL]',
          }))
        : [{ '@type': 'Offer', name: '[PLAN]', priceCurrency: 'USD', price: '[PRICE]' }],
  };

  const orgJson = {
    '@context': 'https://schema.org',
    '@type': 'Organization',
    name: orgName,
    url: '[https://example.com]',
    contactPoint: {
      '@type': 'ContactPoint',
      contactType: 'customer support',
      email: '[support@example.com]',
    },
  };

  // ---------- Action checklist, fixes ranked by severity then effort ----------
  const checklist = [];
  for (const r of results.filter((x) => !x.pass)) {
    checklist.push({
      title: `Answer: “${r.q}”`,
      detail: `Add a sentence with ${r.missing.toLowerCase()}.`,
      severity: 'high',
      effort: '2 min',
      fixId: `addition:${r.id}`,
    });
  }
  for (const f of flags.filter((x) => x.kind === 'cta').slice(0, 3)) {
    checklist.push({
      title: `Weak button: “${f.text}”`,
      detail: 'Say what happens on click. 4 to 8 specific words.',
      severity: 'high',
      effort: '1 min',
      flagText: f.text,
    });
  }
  for (const f of flags.filter((x) => x.kind === 'vague').slice(0, 4)) {
    checklist.push({
      title: `Hype in a fact moment: “${f.text}”`,
      detail: f.why,
      severity: 'med',
      effort: '2 min',
      flagText: f.text,
    });
  }
  for (const f of flags.filter((x) => x.kind === 'idiom').slice(0, 3)) {
    checklist.push({
      title: `A summarizer will mangle “${f.text}”`,
      detail: f.why,
      severity: 'high',
      effort: '2 min',
      flagText: f.text,
    });
  }
  for (const f of flags.filter((x) => x.kind === 'time').slice(0, 3)) {
    checklist.push({
      title: `${AudPlural} can't act on “${f.text}”`,
      detail: f.why,
      severity: 'high',
      effort: '1 min',
      flagText: f.text,
    });
  }
  if (!frontLoaded && firstSentence) {
    checklist.push({
      title: 'Lead with the fact, not the greeting',
      detail: `Opens with “${firstWords}…”. The first clause is what summaries grab. Put the fact there.`,
      severity: 'high',
      effort: '2 min',
    });
  }
  if (termInconsistency.length >= 2) {
    checklist.push({
      title: `Pick one word for your ${aud}`,
      detail: `Page switches between “${termInconsistency.join('”, “')}”. Use one everywhere.`,
      severity: 'med',
      effort: '5 min',
    });
  }
  if (faqs.length === 0) {
    checklist.push({ title: `Add the questions ${audPlural} actually ask`, detail: 'Four to six Q&A pairs. Use the real words people use.', severity: 'med', effort: '15 min' });
  }
  if (planNames.length === 0 && (preset === 'SaaS' || preset === 'Services')) {
    checklist.push({ title: 'Name your plans outright', detail: `Use proper-noun plan names (e.g. “Starter”, “Pro”) next to each price. ${AudPlural} can't pick what they can't name.`, severity: 'med', effort: '5 min' });
  }

  // ---------- Headline insight: the one-liner the user shows their team ----------
  // Find the dominant gap so the user knows what to fix first without reading the report.
  const failedByIntent = {};
  for (const r of results.filter((x) => !x.pass)) {
    failedByIntent[r.intent] = (failedByIntent[r.intent] || 0) + 1;
  }
  const topIntent = Object.entries(failedByIntent).sort((a, b) => b[1] - a[1])[0];
  const flagsByKind = {
    vague: flags.filter((f) => f.kind === 'vague').length,
    cta: flags.filter((f) => f.kind === 'cta').length,
    idiom: flags.filter((f) => f.kind === 'idiom').length,
    time: flags.filter((f) => f.kind === 'time').length,
    pronoun: flags.filter((f) => f.kind === 'pronoun').length,
  };
  let headline;
  if (failed === 0 && vagueCount + ctaCount + idiomCount + vagueTimeCount === 0) {
    headline = `Your content answers the questions ${audPlural} are asking. Ship it.`;
  } else if (topIntent && topIntent[1] >= 2) {
    const intentLabel = { pricing: 'pricing', billing: 'billing', cancellation: 'cancellation', trial: 'the trial', support: 'support', shipping: 'shipping', returns: 'returns', timing: 'timing', integrations: 'integrations', condition: 'item condition', payment: 'payment', error: 'error states', delivery: 'delivery', confirmation: 'confirmation' }[topIntent[0]] || topIntent[0];
    headline = `Your content goes silent on ${intentLabel}. ${AudPlural} are asking, and there's nothing here to answer them.`;
  } else if (flagsByKind.cta >= 2) {
    headline = `Your buttons are vague. ${AudPlural} can't tell what happens when they click.`;
  } else if (flagsByKind.idiom + flagsByKind.time >= 3) {
    headline = `A summarizer will mangle this content. Idioms and vague time words are doing fact work.`;
  } else if (flagsByKind.vague >= 4) {
    headline = `This content is heavy on adjectives, light on facts. ${AudPlural} need numbers, not feelings.`;
  } else if (failed > 0) {
    headline = `${failed} of ${results.length} ${aud} questions go unanswered in your content.`;
  } else {
    headline = `${vagueCount + ctaCount + idiomCount} weak phrase${vagueCount + ctaCount + idiomCount === 1 ? '' : 's'} sit between the ${aud} and the facts they came for.`;
  }

  // ---------- Group checklist by severity ----------
  const groupedChecklist = {
    high: checklist.filter((c) => c.severity === 'high'),
    med: checklist.filter((c) => c.severity === 'med' || c.severity === 'low'),
  };

  return {
    text,
    original,
    audience: aud,
    audiencePlural: audPlural,
    headline,
    results,
    passed,
    failed,
    failRate,
    flags,
    additions,
    rewrites,
    schema: {
      faq: faqJson,
      product: productJson,
      org: orgJson,
    },
    scorecard,
    checklist,
    groupedChecklist,
    agentSignals,
    translation: buildTranslation(original, flags, results, headline),
    meta: { planNames, orgName, faqs },
  };
}

// ---------- Translation: what you wrote vs. what an agent keeps ----------
// The "aha" moment. Take the page's most voice-heavy sentence, strip the
// phrases doing fact-work, and show what's actually left for an agent to
// quote. When nothing quotable survives, that IS the finding.
function buildTranslation(original, flags, results, headline) {
  const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const cap = (s, n) => (s && s.length > n ? s.slice(0, n - 1).trim() + '…' : s);

  // Split into sentences with positions so we can map a flag to its sentence.
  const sentences = [];
  const re = /[^.!?\n]+[.!?]*/g;
  let m;
  while ((m = re.exec(original))) {
    const t = m[0].trim();
    if (t.length > 8) sentences.push({ text: t, start: m.index, end: m.index + m[0].length });
  }
  const sentenceFor = (pos) => sentences.find((s) => pos >= s.start && pos < s.end) || null;

  // Rank flags by how much they distort meaning: idiom/cta worst, then vague, then time.
  const rank = { idiom: 0, cta: 1, vague: 2, time: 3, pronoun: 4 };
  const ranked = [...flags].sort((a, b) => (rank[a.kind] ?? 9) - (rank[b.kind] ?? 9));

  let original_excerpt = null;
  let what_got_lost = null;
  let ai_quote = '';

  for (const fl of ranked) {
    const sent = sentenceFor(fl.start);
    if (!sent) continue;
    original_excerpt = sent.text;
    what_got_lost = fl.why;
    // Strip every flagged phrase that lands inside this sentence.
    let stripped = sent.text;
    const inSent = flags.filter((f) => f.start >= sent.start && f.start < sent.end);
    for (const f of inSent) {
      stripped = stripped.replace(new RegExp(`\\b${escapeRe(f.text)}\\b`, 'gi'), '');
    }
    stripped = stripped.replace(/\s{2,}/g, ' ').replace(/\s+([.,!?])/g, '$1').replace(/^[\s,;:–-]+/, '').trim();
    // Does any hard fact survive? A number, money, percent, or a proper noun mid-sentence.
    const hasFact = /\d|\$|%/.test(stripped) || /\s[A-Z][a-z]{2,}/.test(stripped);
    ai_quote = hasFact && stripped.split(/\s+/).length >= 4 ? cap(stripped, 200) : '';
    break;
  }

  // Fallback: clean-ish page with no flags. Frame around the first gap.
  if (!original_excerpt) {
    const firstFail = results.find((r) => !r.pass);
    const firstPass = results.find((r) => r.pass && r.snippet);
    original_excerpt = sentences[0] ? sentences[0].text : original.slice(0, 180);
    ai_quote = firstPass ? firstPass.snippet.replace(/^…|…$/g, '').trim() : '';
    what_got_lost = firstFail ? firstFail.missing : headline;
  }

  return {
    original_excerpt: cap(original_excerpt, 240),
    ai_quote,
    what_got_lost: what_got_lost || headline,
  };
}

const SAMPLES = {
  saas: {
    label: 'SaaS pricing page',
    preset: 'SaaS',
    mode: 'paste',
    text: `Helio is a powerful, world-class platform that helps teams move faster.

Pricing
Our plans are simple and affordable. Choose the one that fits.

Starter, for small teams getting started.
Includes core features and email support.

Pro, for growing teams that need more power.
Includes advanced features, integrations, and priority support.

Enterprise, for organizations with custom needs.
Includes everything in Pro plus dedicated support.

We offer flexible billing. Cancel anytime.

Thousands of innovative companies use Helio to streamline their operations. It's the easiest way to get started.

Click here to get started.
Learn more about our features.`,
  },
  ecom: {
    label: 'Ecommerce returns page',
    preset: 'Ecommerce',
    mode: 'paste',
    text: `Returns at Northwind Apparel

We want you to love what you ordered. If something isn't quite right, we make returns simple.

How returns work
Reach out to our team and we'll take care of you. Items should be in good condition. We'll process your refund quickly once we receive your package.

Final sale items cannot be returned.

If you have questions about your order, get in touch and a member of our world-class support team will help you out.

For international orders, please contact us for assistance.

Read more about our policies in our help center.`,
  },
  ux: {
    label: 'Product UX strings',
    preset: 'ProductUX',
    mode: 'paste',
    text: `Welcome back!

Something exciting is on the way. Your refund will be processed shortly and should arrive any moment now.

We just shot you an email to verify your account. Please check it out when you get a chance — we need this so things don't blow up later.

Payment status: Your card was charged. We'll send the receipt soon.

Hey there, member! As one of our most loyal users, you're going to die when you see what's next. Customers love it. It's to die for.

If your payment didn't go through, no worries — try again whenever.

Coming soon: faster checkout for all our shoppers. Stay tuned!

Click here to learn more.`,
  },
};

// ---------- cost controls ----------
// A small FNV-ish hash of the input so we can (a) cache identical analyses and
// (b) recognize the built-in samples and serve canned results with no API call.
function inputHash(s) {
  let h = 2166136261;
  const str = String(s || '');
  for (let i = 0; i < str.length; i++) {
    h ^= str.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return (h >>> 0).toString(36);
}

const ANALYSIS_CACHE_KEY = 'agentspeak_analysis_cache_v1';
const ANALYSIS_CACHE_MAX = 25;

function readAnalysisCache(key) {
  try {
    const c = JSON.parse(localStorage.getItem(ANALYSIS_CACHE_KEY) || '{}');
    return c[key] ? c[key].a : null;
  } catch (e) { return null; }
}
function writeAnalysisCache(key, analysis) {
  try {
    const c = JSON.parse(localStorage.getItem(ANALYSIS_CACHE_KEY) || '{}');
    c[key] = { a: analysis, ts: Date.now() };
    // evict oldest beyond the cap
    const keys = Object.keys(c);
    if (keys.length > ANALYSIS_CACHE_MAX) {
      keys.sort((x, y) => c[x].ts - c[y].ts).slice(0, keys.length - ANALYSIS_CACHE_MAX).forEach((k) => delete c[k]);
    }
    localStorage.setItem(ANALYSIS_CACHE_KEY, JSON.stringify(c));
  } catch (e) {}
}

// Cap how much text we ever send to the model. Most signal is up top, and an
// uncapped paste is an uncapped bill. ~6k chars ≈ enough for any real page.
const MAX_INPUT_CHARS = 6000;

// ---------- Page-aware analysis (AI-driven) ----------
// The category checklist is gone. Instead we read what THIS page is for, then
// ask only the questions a real visitor to this page would ask. Falls back to
// the local rules engine when no model is available (offline / no API).
async function runSmartAnalysis({ raw, mode, hint }) {
  const pageText = (mode === 'html' ? stripHtml(raw) : raw).replace(/\s+/g, '\n').replace(/\n{3,}/g, '\n\n').trim();

  // Too little real copy to say anything useful. Bail before spending a call.
  const compact = pageText.replace(/\s+/g, ' ').trim();
  if (compact.length < 120 || compact.split(' ').length < 18) {
    return { tooThin: true, pageAware: false, wordCount: compact ? compact.split(' ').length : 0 };
  }

  // Cache: identical input (incl. any correction hint) → no API call.
  const cacheKey = inputHash(compact.slice(0, MAX_INPUT_CHARS) + '|' + (hint || ''));
  if (!window.__forceFreshAnalysis) {
    const cached = readAnalysisCache(cacheKey);
    if (cached) return { ...cached, fromCache: true };
  }

  if (!window.claude || typeof window.claude.complete !== 'function') {
    // No model available — degrade to the local rules engine.
    return { ...runAnalysis({ raw, mode, preset: 'Generic' }), pageAware: false };
  }

  const prompt = `You are auditing a web page for "agent readiness" — whether an AI assistant could answer the questions a real visitor would ask about THIS specific page.

First, read the page and work out what it is actually FOR. A rewards signup page is about joining and earning — NOT refunds. A pricing page is about cost and plans. A returns page is about sending things back. Only ask questions that genuinely belong to THIS page's job.
${hint ? `\nIMPORTANT: The person who owns this page tells you it is: "${hint}". Treat that as authoritative — set pageType accordingly and ask the questions that belong to THAT kind of page.\n` : ''}
PAGE CONTENT:
"""
${pageText.slice(0, MAX_INPUT_CHARS)}
"""

Return ONLY valid JSON (no markdown, no commentary) in this exact shape:
{
  "pageType": "short plain-language label, e.g. 'rewards program signup'",
  "audience": "the single noun for who this page is for, lowercase singular, e.g. 'member', 'customer', 'reader'",
  "questions": [
    {
      "q": "a real question THIS page's visitor would ask, in their words",
      "intent": "one or two word label, lowercase, e.g. 'cost', 'how to join', 'rewards'",
      "answered": true or false,
      "evidence": "if answered: the verbatim sentence/phrase from the page that answers it. If not answered: empty string",
      "missing": "if NOT answered: the specific fact the page should state. If answered: empty string",
      "suggested": "if NOT answered: a ready-to-paste 1-2 sentence answer using [BRACKETED] placeholders for facts you don't know. If answered: empty string"
    }
  ],
  "rewrites": [
    { "current": "verbatim weak/vague line from the page", "rewrite": "specific, quotable version with [BRACKETED] placeholders where needed", "why": "one short reason an agent struggles with the current line" }
  ]
}

Rules:
- 5 to 8 questions, the ones that matter most for THIS page. Quality over coverage.
- "answered": true ONLY if the page gives a clear, specific answer. Vague brand copy does not count.
- Every "evidence" must be text that actually appears in the page above.
- 0 to 3 rewrites, only for lines that genuinely appear on the page. Skip if the copy is already specific.
- No generic checklist items. If the page doesn't need a question, don't ask it.
- NEVER invent specific facts. Do not write a price, percentage, count, date, or proper name that does not appear in the page text above. For any fact you don't have, write a [BRACKETED] placeholder the owner fills in. This is critical — a made-up number destroys trust.`;

  let parsed;
  try {
    const out = await window.claude.complete({ messages: [{ role: 'user', content: [{ type: 'text', text: prompt }] }] });
    let s = String(out || '').trim();
    // strip code fences if the model added them
    s = s.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
    const first = s.indexOf('{');
    const last = s.lastIndexOf('}');
    if (first > 0 || last < s.length - 1) s = s.slice(first, last + 1);
    parsed = JSON.parse(s);
  } catch (e) {
    return { ...runAnalysis({ raw, mode, preset: 'Generic' }), pageAware: false, aiError: true };
  }

  if (!parsed || !Array.isArray(parsed.questions) || parsed.questions.length === 0) {
    return { ...runAnalysis({ raw, mode, preset: 'Generic' }), pageAware: false, aiError: true };
  }

  // Map AI questions into the injected-question shape runAnalysis expects.
  const src = analysisSource(raw, mode);
  const aiQuestions = parsed.questions.slice(0, 8).map((q, i) => ({
    id: `q-ai-${i}`,
    q: String(q.q || '').trim(),
    intent: String(q.intent || 'detail').trim(),
    pass: !!q.answered,
    snippet: q.answered && q.evidence ? `…${String(q.evidence).trim()}…` : null,
    missing: !q.answered ? String(q.missing || 'a specific answer to this question').trim() : '',
    suggested: !q.answered && q.suggested ? bracketInventedFacts(String(q.suggested).trim(), src) : null,
  }));

  const ai = {
    audience: String(parsed.audience || 'reader').trim().toLowerCase().split(/\s+/)[0] || 'reader',
    questions: aiQuestions,
  };

  const analysis = runAnalysis({ raw, mode, preset: 'Generic', ai });
  analysis.pageAware = true;
  analysis.pageType = String(parsed.pageType || '').trim();
  // The single highest-impact gap = first unanswered question (AI orders by importance).
  const firstGap = analysis.results.find((r) => !r.pass);
  analysis.topGapId = firstGap ? firstGap.id : null;

  // Prefer AI-found rewrites when they're real (verbatim lines on the page).
  if (Array.isArray(parsed.rewrites) && parsed.rewrites.length) {
    const lower = analysis.original.toLowerCase();
    const aiRewrites = parsed.rewrites
      .filter((r) => r && r.current && r.rewrite && lower.includes(String(r.current).trim().toLowerCase().slice(0, 24)))
      .slice(0, 4)
      .map((r) => ({
        before: String(r.current).trim(),
        after: bracketInventedFacts(String(r.rewrite).trim(), analysis.original),
        why: String(r.why || 'An agent can’t turn this into a quotable fact.').trim(),
        kind: 'vague',
      }));
    if (aiRewrites.length) analysis.rewrites = aiRewrites;
  }

  writeAnalysisCache(cacheKey, analysis);
  return analysis;
}

window.runAnalysis = runAnalysis;
window.runSmartAnalysis = runSmartAnalysis;
// Lets the UI know an input would be served from cache (free) before running,
// so it can skip the quota gate for repeat/identical analyses.
window.isCachedInput = function ({ raw, mode, hint }) {
  try {
    const pageText = (mode === 'html' ? stripHtml(raw) : raw).replace(/\s+/g, ' ').trim();
    if (pageText.length < 120) return true; // tooThin → no API call
    const key = inputHash(pageText.slice(0, MAX_INPUT_CHARS) + '|' + (hint || ''));
    return !!readAnalysisCache(key);
  } catch (e) { return false; }
};
window.SAMPLES = SAMPLES;
window.QUESTION_BANKS = QUESTION_BANKS;
