/* real-scan.jsx
 *
 * Real prompt scan across configured AI providers + real homepage fixes engine.
 * Drives the difference between "sample" report data and verified-live data.
 */

const PROMPT_LIBRARY = {
  default: ({ brand, industry, location }) => [
    industry && location ? `What are the best ${industry} in ${location}?` : null,
    location ? `Recommend a great ${industry || 'business'} in ${location}.` : null,
    industry ? `Compare top ${industry} providers — which would you pick?` : null,
    brand ? `What is ${brand}? Have you heard of them?` : null,
    industry ? `Where should I go for ${industry} that's high-quality?` : null,
    industry && location ? `Who are the top 5 ${industry} ${location ? 'in ' + location : ''}?` : null,
  ].filter(Boolean),
};

function brandFromDomainStr(d) {
  return String(d || '')
    .replace(/^https?:\/\//, '')
    .replace(/^www\./, '')
    .split('/')[0]
    .split('.')[0];
}

/* Pull real competitor brands out of the scan responses via the model (managed mode),
   replacing the old regex that grabbed any capitalized phrase. Returns [{name,count}]. */
async function extractCompetitorsFromResponses({ brandName, industry, responses, keys }) {
  if (!responses || !responses.length || typeof window.callModel !== 'function') return [];
  const corpus = responses.slice(0, 30).map((t, i) => `[${i + 1}] ${String(t || '').slice(0, 800)}`).join('\n\n');
  const prompt = `Below are ${responses.length} AI-assistant answers to questions about ${industry || 'this market'}.
List the specific competing brands / companies / products named across them — exclude "${brandName}" and any generic words, categories, or place names.
Count how many of the answers mention each one.
Return ONLY a JSON array, max 8 items, sorted by count descending:
[{"name":"Brand Name","count":N}]

ANSWERS:
${corpus}`;
  try {
    const r = await window.callModel('anthropic', (keys && keys.anthropic) || '', prompt, { maxTokens: 600 });
    const m = (r.text || '').match(/\[[\s\S]*\]/);
    if (!m) return [];
    const arr = JSON.parse(m[0]);
    if (!Array.isArray(arr)) return [];
    const bn = String(brandName || '').toLowerCase();
    return arr
      .filter((c) => c && c.name && String(c.name).trim().length >= 2 && String(c.name).toLowerCase() !== bn)
      .map((c) => ({ name: String(c.name).trim(), count: Number(c.count) || 1, domain: '' }))
      .slice(0, 8);
  } catch {
    return [];
  }
}

/* Rank-weighted visibility: being recommended #1 counts ~1.0, a mid-list mention
   less, a buried/in-passing mention least. Replaces binary mentioned-or-not, which
   scored every brand that got named at all ~100% regardless of WHERE it ranked. */
function mentionWeight(parsed, textLen) {
  if (!parsed || !parsed.mentioned) return 0;
  const rank = parsed.rank || 1;
  // #1=1.0, #2=.82, #3=.64, #4=.46, #5=.28, floor .2
  let w = rank <= 1 ? 1 : Math.max(0.2, 1 - (rank - 1) * 0.18);
  // discount a mention buried in the last third of the answer (low prominence)
  if (parsed.position != null && textLen > 0 && parsed.position / textLen > 0.66) w *= 0.6;
  return w;
}

/* run a real scan across every provider that has a key */
async function runRealScan({ profile, brand, onProgress = () => {}, signal }) {
  // Accept brand directly (preferred) or fall back to the legacy single-brand profile shape.
  const b = brand || profile.brand || (window.getActiveSite ? window.getActiveSite(profile) : null) || {};
  const brandName = b.name || '';
  const domain = b.domain || '';
  const industry = b.industry || '';
  const location = b.location || '';
  const keys = profile.keys || {};
  const managed = (window.getManagedProviders && window.getManagedProviders()) || new Set();
  const configured = (window.PROVIDERS || []).filter((p) => keys[p.id] || managed.has(p.id));
  if (configured.length === 0) {
    throw new Error(window.aisoEnabled && window.aisoEnabled()
      ? 'Server has no provider keys configured yet. Wire ANTHROPIC_API_KEY in Vercel.'
      : 'No keys wired — go to Settings → API keys and paste at least one.');
  }

  // Prefer Claude-suggested prompts from discovery (vertical-tuned), else generic library.
  const discoveredPrompts = (b.discovered?.scanPrompts || b.scanPrompts || []).filter((p) => typeof p === 'string' && p.length > 8);
  const prompts = discoveredPrompts.length >= 3
    ? discoveredPrompts.slice(0, 8)
    : PROMPT_LIBRARY.default({ brand: brandName, industry, location });

  if (prompts.length === 0) {
    throw new Error('Need a brand name + industry + location. Run discovery first.');
  }

  const results = {};
  configured.forEach((p) => {
    results[p.id] = { mentioned: 0, total: 0, responses: [], errors: [] };
  });

  onProgress({ phase: 'start', total: configured.length * prompts.length });

  let done = 0;

  await Promise.all(
    configured.map(async (provider) => {
      onProgress({ phase: 'model-start', model: provider.id });
      for (let i = 0; i < prompts.length; i++) {
        if (signal?.aborted) return;
        const prompt = prompts[i];
        try {
          const out = await window.callModel(provider.id, keys[provider.id], prompt, { maxTokens: 400 });
          const parsed = window.parseMention(out.text, brandName, domain);
          results[provider.id].total += 1;
          if (parsed.mentioned) results[provider.id].mentioned += 1;
          results[provider.id].responses.push({ prompt, text: out.text, parsed });
          done += 1;
          onProgress({ phase: 'tick', done, model: provider.id });
        } catch (e) {
          results[provider.id].errors.push({ prompt, error: String(e.message || e) });
          done += 1;
          onProgress({ phase: 'tick', done, model: provider.id, error: String(e.message || e) });
        }
      }
      onProgress({ phase: 'model-done', model: provider.id });
    })
  );

  // visibility score — RANK-WEIGHTED (see mentionWeight). totalMentions stays the raw
  // mentioned/total count for the "X/Y prompts" display; the score itself is weighted
  // so a brand named #1 scores ~100% but one buried at #6 scores ~20%, not 100%.
  let totalCalls = 0, totalMentions = 0, weightedSum = 0;
  Object.values(results).forEach((r) => {
    totalCalls += r.total;
    totalMentions += r.mentioned;
    r.responses.forEach(({ text, parsed }) => { weightedSum += mentionWeight(parsed, (text || '').length); });
  });
  const score = totalCalls > 0 ? Math.round((weightedSum / totalCalls) * 100) : 0;

  // per-model rate — same rank-weighting, per model
  const perModel = {};
  Object.entries(results).forEach(([k, r]) => {
    if (r.total === 0) { perModel[k] = null; return; }
    let w = 0;
    r.responses.forEach(({ text, parsed }) => { w += mentionWeight(parsed, (text || '').length); });
    perModel[k] = Math.round((w / r.total) * 100);
  });

  // competitor extraction — ask the model to read the REAL responses (replaces the
  // old capitalized-word regex that surfaced sentence-starters and place names).
  const allResponseTexts = [];
  Object.values(results).forEach((r) => r.responses.forEach(({ text }) => allResponseTexts.push(text)));
  const competitors = (await extractCompetitorsFromResponses({ brandName, industry, responses: allResponseTexts, keys }))
    .map((c, i) => ({ ...c, rank: i + 1 }));

  // real rank: how many competitors are named MORE often than the brand itself.
  // totalMentions = responses where the brand was mentioned; competitor.count is the
  // same measure, so they're comparable. null when there's nothing to rank against.
  const rank = competitors.length
    ? 1 + competitors.filter((c) => (c.count || 0) > totalMentions).length
    : (totalMentions > 0 ? 1 : null);

  // errored-model accounting: separate models that actually answered from all-errored
  // ones, so the report doesn't count a dead model as "live".
  const answered = Object.entries(results).filter(([, r]) => r.total > 0).map(([k]) => k);
  const errored = Object.entries(results).filter(([, r]) => r.total === 0 && r.errors.length > 0).map(([k]) => k);

  return {
    score,
    perModel,
    competitors,
    rank,
    answered,
    errored,
    results,
    prompts,
    totalCalls,
    totalMentions,
    runAt: Date.now(),
    configured: configured.map((p) => p.id),
  };
}

/* real fixes engine — derives a ranked, REAL fix list from the homepage HTML */
async function analyzeFixesFromHomepage(domain) {
  if (!domain) return [];
  window.__aisoHpCache = window.__aisoHpCache || {};

  if (!window.__aisoHpCache[domain]) {
    window.__aisoHpCache[domain] = (async () => {
      const direct = await fetch(`https://${domain}/`).catch(() => null);
      if (direct && direct.ok) return { html: await direct.text(), via: 'direct' };
      const proxied = await fetch('https://corsproxy.io/?' + encodeURIComponent(`https://${domain}/`)).catch(() => null);
      if (proxied && proxied.ok) return { html: await proxied.text(), via: 'proxy' };
      return { err: 'unreachable' };
    })();
  }
  const cached = await window.__aisoHpCache[domain];
  if (cached.err || !cached.html) return [];
  const html = cached.html;

  const ld = (html.match(/<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi) || []).join('\n');
  const has = (re) => re.test(ld);
  const present = {
    FAQ:       has(/"@type"\s*:\s*"FAQPage"/i),
    Org:       has(/"@type"\s*:\s*"Organization"/i),
    LocalBiz:  has(/"@type"\s*:\s*"LocalBusiness"/i),
    Review:    has(/"@type"\s*:\s*"(Review|AggregateRating)"/i),
    Breadcrumb:has(/"@type"\s*:\s*"BreadcrumbList"/i),
    Product:   has(/"@type"\s*:\s*"Product"/i),
  };
  const hasOg = /<meta[^>]+property=["']og:title["']/i.test(html);
  const hasOgImg = /<meta[^>]+property=["']og:image["']/i.test(html);
  const hasOgDesc = /<meta[^>]+property=["']og:description["']/i.test(html);
  const hasMetaDesc = /<meta[^>]+name=["']description["']/i.test(html);
  const hasTwitter = /<meta[^>]+name=["']twitter:card["']/i.test(html);
  const title = (html.match(/<title[^>]*>([\s\S]*?)<\/title>/i) || [])[1] || '';
  const imgs = html.match(/<img\b[^>]*>/gi) || [];
  const imgsNoAlt = imgs.filter((i) => !/\balt\s*=/i.test(i));
  const wordCount = (html.replace(/<[^>]+>/g, ' ').match(/\b[\w']+\b/g) || []).length;
  const internalLinks = (html.match(/<a\b[^>]+href=["']\/[^"']/gi) || []).length;
  const externalLinks = (html.match(/<a\b[^>]+href=["']https?:\/\//gi) || []).length;

  const fixes = [];
  function add(f) { fixes.push({ ...f, id: fixes.length + 1, verifiedLive: true }); }

  // schema gaps — high impact
  if (!present.FAQ) add({
    title: 'No FAQPage JSON-LD on homepage',
    desc: 'AI assistants extract structured Q&A far more reliably than prose. Generating 8–12 entity-bound questions and shipping them as FAQPage schema is the single highest-leverage change.',
    impact: 'high', gain: 9, shipping: true,
    tags: ['Schema', 'Auto-deployable'],
  });
  if (!present.Org) add({
    title: 'No Organization schema',
    desc: 'Without Organization JSON-LD, AI models can\'t anchor a confident entity record for your brand.',
    impact: 'high', gain: 7, tags: ['Schema', 'Auto-deployable'],
  });
  if (!present.LocalBiz) add({
    title: 'No LocalBusiness markup',
    desc: '“Best X near me” / “in {city}” queries can\'t resolve your business without LocalBusiness JSON-LD (address, geo, openingHours).',
    impact: 'medium', gain: 5, tags: ['Schema', 'Auto-deployable'],
  });
  if (!present.Review) add({
    title: 'No Review / AggregateRating schema',
    desc: 'Adding Review schema surfaces ratings in AI answers and acts as a trust signal in citations.',
    impact: 'medium', gain: 4, tags: ['Schema', 'Auto-deployable'],
  });
  if (!present.Breadcrumb) add({
    title: 'No BreadcrumbList schema',
    desc: 'Lets AI understand your site hierarchy and produce better deep-link citations.',
    impact: 'low', gain: 2, tags: ['Schema', 'Auto-deployable'],
  });

  // meta gaps
  if (!hasMetaDesc) add({
    title: 'No <meta name="description">',
    desc: 'AI models pull short brand summaries from here. Without it they default to scraping body prose, which is noisier.',
    impact: 'medium', gain: 4, tags: ['Markup'],
  });
  if (!hasOg) add({
    title: 'No Open Graph metadata',
    desc: 'og:title / og:description are first-class crawler citizens. Their absence reduces summary fidelity.',
    impact: 'medium', gain: 3, tags: ['Markup'],
  });
  if (hasOg && !hasOgImg) add({
    title: 'Missing og:image',
    desc: 'Adds a canonical visual for AI assistants and link-unfurl previews.',
    impact: 'low', gain: 2, tags: ['Markup'],
  });
  if (!hasTwitter) add({
    title: 'No twitter:card meta',
    desc: 'A small but free signal — many crawlers read twitter:* tags as fallback.',
    impact: 'low', gain: 1, tags: ['Markup'],
  });

  // title issues
  if (!title) add({
    title: 'No <title> tag on homepage',
    desc: 'Critical — title is the single highest-weight signal for every AI crawler.',
    impact: 'high', gain: 6, tags: ['Markup'],
  });
  else if (title.length < 10) add({
    title: 'Title tag is too short',
    desc: `Your homepage title is just ${title.length} characters. Aim for 30–60 with the brand + value prop.`,
    impact: 'low', gain: 2, tags: ['Markup'],
  });
  else if (title.length > 80) add({
    title: 'Title tag is too long',
    desc: `Your title is ${title.length} chars; assistants and search engines truncate around 60–70.`,
    impact: 'low', gain: 1, tags: ['Markup'],
  });

  // alt text
  if (imgsNoAlt.length >= 6) add({
    title: `${imgsNoAlt.length} of ${imgs.length} images missing alt text`,
    desc: 'AI crawlers can\'t describe imagery without alt attributes. Affects accessibility too.',
    impact: 'low', gain: 2, tags: ['Markup', 'Auto-deployable'],
  });

  // thin content
  if (wordCount < 250) add({
    title: 'Thin homepage content',
    desc: `Only ~${wordCount} words on the homepage. AI models down-weight thin pages; aim for 400–800 with entity-rich copy.`,
    impact: 'medium', gain: 4, tags: ['Content'],
  });

  // linking
  if (internalLinks < 6) add({
    title: 'Weak internal linking from homepage',
    desc: `Only ${internalLinks} internal links found. AI models use link graphs to confirm a brand owns multiple related topics.`,
    impact: 'low', gain: 2, tags: ['IA'],
  });

  return fixes.slice(0, 10);
}

/* ---------- DataBadge — elegant "this is real" / "this is sample" tag ---------- */
function DataBadge({ kind = 'sample', detail = null, onClick }) {
  const label = kind === 'live' ? 'live' : kind === 'mixed' ? 'mixed' : 'sample';
  const tip = kind === 'live'
    ? 'Real-time data from your wired API keys.'
    : kind === 'mixed'
      ? 'A mix of live (verified) and modelled data.'
      : 'Sample / modelled data — wire keys in Settings to verify against real model responses.';
  const Tag = onClick ? 'button' : 'span';
  return (
    <Tag className={`data-badge db-${kind}`} title={tip} onClick={onClick}>
      <span className="db-dot" />
      <span className="db-label">{label}</span>
      {detail && <span className="db-detail">· {detail}</span>}
    </Tag>
  );
}

/* ---------- RealScanBanner — top-of-report CTA to run real scan ---------- */
function RealScanBanner({ profile, scanState, onRun, onCancel, onOpenSettings }) {
  const wired = (window.useWiredProviders ? window.useWiredProviders(profile) : (window.PROVIDERS || []).filter((p) => profile.keys[p.id]));
  const wiredCount = wired.length;
  const hasResult = !!scanState?.result;
  const running = scanState?.running;
  const managedMode = window.aisoEnabled && window.aisoEnabled();

  if (wiredCount === 0 && !hasResult) {
    return (
      <div className="rsb rsb-empty">
        <div className="rsb-left">
          <div className="eyebrow"><span className="db-dot warn-pulse" /> Server provisioning · waiting for managed providers</div>
          <h4>AISO is starting up.</h4>
          <p>The model-by-model scan will run as soon as a provider is reachable. The technical audit below already runs against your homepage.</p>
        </div>
      </div>
    );
  }

  if (running) {
    const pct = scanState.total > 0 ? Math.min(100, Math.round((scanState.done / scanState.total) * 100)) : 0;
    return (
      <div className="rsb rsb-running">
        <div className="rsb-left">
          <div className="eyebrow"><span className="pulse" /> Scanning live · {wired.length} models · {scanState.done}/{scanState.total} prompts</div>
          <h4>Asking {wired.map((p) => p.label).join(', ')} about {scanState.brandName || 'your brand'}…</h4>
          <div className="rsb-progress"><i style={{ width: `${pct}%` }} /></div>
        </div>
        <div className="rsb-right">
          <button className="btn" onClick={onCancel}>Stop</button>
        </div>
      </div>
    );
  }

  if (hasResult) {
    const r = scanState.result;
    const ago = Math.max(1, Math.round((Date.now() - r.runAt) / 1000));
    return (
      <div className="rsb rsb-live">
        <div className="rsb-left">
          <div className="eyebrow"><span className="db-dot live-pulse" /> Live · ran {ago}s ago · {r.configured.length} models · {r.totalCalls} prompts</div>
          <h4>Verified visibility: <span className="grad">{r.score}%</span> across {r.configured.length} model{r.configured.length === 1 ? '' : 's'}{managedMode ? ' on the server' : ' you wired'}.</h4>
          <p>Everything below now reflects real responses. {!managedMode && wiredCount < 6 && (<>Add more keys in <button className="link-btn" onClick={onOpenSettings}>Settings</button> to widen coverage.</>)}</p>
        </div>
        <div className="rsb-right">
          <button className="btn" onClick={onRun}>Re-run scan</button>
        </div>
      </div>
    );
  }

  // wired but not yet run
  const promptCount = (window.PROMPT_LIBRARY?.default({ brand: scanState.brandName || '', industry: scanState.industry || '', location: scanState.location || '' }) || []).length || 6;
  return (
    <div className="rsb rsb-ready">
      <div className="rsb-left">
        <div className="eyebrow"><span className="db-dot live-pulse" /> {wiredCount} {managedMode ? 'model' : 'key'}{wiredCount === 1 ? '' : 's'} {managedMode ? 'live' : 'wired'} · ready to verify</div>
        <h4>Run real prompts against {wired.map((p) => p.label).join(' · ')}.</h4>
        <p>We'll ask each model {promptCount} vertical-aware questions and update every chart below with measured numbers.</p>
      </div>
      <div className="rsb-right">
        <button className="btn btn-primary" onClick={onRun}>Run real scan <Arrow /></button>
      </div>
    </div>
  );
}

function Arrow() { return <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 7h10M8 3l4 4-4 4" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg>; }

Object.assign(window, { runRealScan, analyzeFixesFromHomepage, DataBadge, RealScanBanner, PROMPT_LIBRARY });
