/**
 * Audit Runner — Orchestrator
 * ────────────────────────────
 * Drives an audit from queued → running → completed (or failed).
 * Advances tiers explicitly so the polling UI can show live progress.
 *
 * Designed to run as a fire-and-forget async function — the API
 * route returns the auditId immediately; this function runs in the
 * background and updates the Audit record as it progresses.
 */

import { prisma } from "@/lib/prisma";
import { advanceTier, markStarted, markCompleted, markFailed, updateAuditProgress } from "./progress";
import { scanSite, fetchBacklinksSummary } from "./scrapers/dataforseo-onpage";
import { fetchKeyPageContent } from "./scrapers/firecrawl-content";
import { analyzeSite } from "./analyzers/seo-analyzer";
import { synthesizeSeoSection, SynthesizerTimeoutError, SynthesizerError } from "./synthesizers/seo-synthesizer";
import type { AuditEnrichment } from "./synthesizers/seo-synthesizer";
import { classifyIndustry } from "./synthesizers/industry-classifier";
import { fetchSerpAnalysis } from "./data-sources/serp-analysis";
import { getGscSummary } from "@/lib/services/gsc-sync";
import { aggregateSignalCenter } from "./data-sources/signal-center";
import { aggregateBilingualAudit } from "./data-sources/bilingual-audit";
import { aggregateCompetitorAnalysis } from "./data-sources/competitor-analysis";
import { aggregateSchemaOpportunityForecast } from "./data-sources/schema-opportunity-forecast";
import { aggregateIndustryBenchmarkComparison } from "./data-sources/industry-benchmark-comparison";
import { aggregatePageStrategicPass } from "./data-sources/page-strategic-pass";
import { aggregateAiEnginePersonalityAnalysis } from "./data-sources/ai-engine-personality-analysis";
import { aggregateAiSourceAttributionComparison } from "./data-sources/ai-source-attribution-comparison";
import { aggregateBehavioralActionBridge } from "./data-sources/behavioral-action-bridge";
import { aggregateEeatAudit } from "./data-sources/eeat-audit";
import { runVoiceSearchEligibility } from "./data-sources/voice-search-eligibility";
import { aggregateAiSnippetEligibility } from "./data-sources/ai-snippet-eligibility";
import { aggregateMultiTouchAiPath } from "./data-sources/multi-touch-ai-path";
import { aggregateRevenueAttribution } from "./data-sources/revenue-attribution";
import { aggregatePredictiveConversionScoring } from "./data-sources/predictive-conversion-scoring";
import { aggregateConversionHeatMap } from "./data-sources/conversion-heat-map";
import { aggregateAiSearchVerification } from "./data-sources/ai-search-verification";
import { aggregateLostOpportunity } from "./data-sources/lost-opportunity-calculator";
import { aggregateLiveVisualAnnotations } from "./data-sources/live-visual-annotations";
import { aggregateCompetitorVisualComparison } from "./data-sources/competitor-visual-comparison";
import { aggregateBrandAuthorityScore } from "./data-sources/brand-authority";
import { aggregateSnippetCaptureStrategy } from "./data-sources/snippet-capture-strategy";
import { aggregateMobileFirstAudit } from "./data-sources/mobile-first-audit";
import { aggregatePageSpeedInsights } from "./data-sources/page-speed-deep-dive";
import { runRealUserTechnicalSynthesizer } from "./synthesizers/real-user-technical";
import { runMultilingualSynthesizer } from "./synthesizers/multilingual";
import { runComplianceSynthesizer } from "./synthesizers/compliance";
import { runEcommerceSynthesizer } from "./synthesizers/ecommerce";
import { runB2bSynthesizer } from "./synthesizers/b2b";
import { getErrorPatterns } from "@/lib/services/site-tag-analytics";
import { deleteCached, deleteCachedPrefix } from "@/lib/cache/redis-cache";
import type { StrategicRoadmap, SeoAction } from "./types";
import {
  computeDimensionScores,
  computeThirdPartyDimensionScores,
  computeHealthScore,
  type ThirdPartyDimensionInput,
} from "./scoring";
import type {
  AuditInput,
  BrandContext,
  BehavioralContext,
  BehavioralPageContext,
  AiAttributionContext,
  AiAttributionPageContext,
  PageData,
  SeoFindings,
  DimensionScores,
  SiteTagContext,
  WebVitalEntry,
  CrawlStatsContext,
} from "./types";
import { normUrl } from "./url-utils";

// ─── Cost tracking ────────────────────────────────────────────────────────────
// Estimated unit costs — all values in USD. These are conservative proxies;
// actual API charges may vary with plan. Used only for logging + ceiling guard.

const COST_PER_CRAWL_PAGE = 0.0001;      // DFS on-page crawl
const COST_PER_FIRECRAWL_PAGE = 0.0003;  // Firecrawl content extract
const COST_PER_DFS_BACKLINK = 0.003;     // DataForSEO backlinks summary
const COST_PER_SERP_QUERY = 0.002;       // DataForSEO SERP query
const COST_PER_HAIKU_CALL = 0.0005;      // Haiku: ~3-4K tokens per call
const COST_PER_SONNET_CALL = 0.05;       // Sonnet: ~8K tokens per call

interface AuditCostRecord {
  crawl: number;
  firecrawl: number;
  dfsBacklinks: number;
  dfsSerp: number;
  haiku: number;
  synth: number;
  total: number;
}

function logAuditCost(
  auditId: string,
  auditType: "1st" | "3rd",
  brandId: string | null,
  record: AuditCostRecord,
  ceiling: number,
): void {
  const { crawl, firecrawl, dfsBacklinks, dfsSerp, haiku, synth, total } = record;
  console.log(
    `[audit-cost] type=${auditType} auditId=${auditId} brandId=${brandId ?? "n/a"}` +
    ` crawl=$${crawl.toFixed(4)} firecrawl=$${firecrawl.toFixed(4)}` +
    ` dfs=$${(dfsBacklinks + dfsSerp).toFixed(4)} haiku=$${haiku.toFixed(4)}` +
    ` synth=$${synth.toFixed(4)} total=$${total.toFixed(4)} ceiling=$${ceiling.toFixed(2)}`,
  );
  if (total > ceiling) {
    console.warn(`[audit-cost] WARN: estimated cost $${total.toFixed(4)} exceeds ceiling $${ceiling.toFixed(2)} for ${auditId}`);
  }
}

// ─── Crawl error helpers ──────────────────────────────────────────────────────

function getCrawlErrorMessage(reason: string): string {
  switch (reason) {
    case "crawl_blocked":
      return "This site is blocking automated access. Likely bot protection is active. To audit fully, install the meSEO Site Tag which uses first-party access.";
    case "crawl_timeout":
      return "Site took too long to respond. Please try again in a few minutes, or check if the site is experiencing performance issues.";
    case "site_unreachable":
      return "Could not reach the site. Verify the URL is correct and the site is online.";
    case "js_required":
      return "This site requires JavaScript to render content. To audit fully, install the meSEO Site Tag for first-party access.";
    case "redirect_loop":
      return "The site has a redirect chain that prevented crawling. Check for SSL or www redirect conflicts.";
    case "firecrawl_service_error":
      return "Our crawler service is temporarily unavailable. Please retry in a few minutes.";
    default:
      return "We were unable to crawl this site. It may be blocking automated access, redirecting unexpectedly, or requires JavaScript to load. Verify the URL and try again.";
  }
}

function buildEmptyFindings(): SeoFindings {
  return {
    siteSize: { totalPages: 0, crawlableDepth: 0, hasSitemap: false, hasRobotsTxt: false },
    metaTitles: { total: 0, missing: 0, tooLong: 0, tooShort: 0, duplicates: [], averageLength: 0 },
    metaDescriptions: { total: 0, missing: 0, tooLong: 0, tooShort: 0, duplicates: [], averageLength: 0 },
    headings: { pagesWithH1: 0, missingH1: 0, multipleH1: 0, deepStructureIssues: [] },
    brokenLinks: { total: 0, internal404s: [] },
    images: { missingAlt: 0, totalImages: 0, oversizedCount: 0, brokenCount: 0 },
    schema: { percentCoverage: 0, typesDetected: [], pagesWithSchema: 0, missingOpportunities: [] },
    content: { thinContentPages: [], totalContentPages: 0, avgWordCount: 0 },
    technical: {
      hasSitemap: false, hasRobotsTxt: false, mobileFriendly: null,
      nonIndexablePages: 0, canonicalIssues: [], redirectChains: [],
    },
    topPages: [],
  };
}

// ─── Raw first-party data (fetched in parallel with DFS crawl) ────────────────

interface RawFirstPartyData {
  brand: {
    id: string;
    name: string;
    primaryServices: string[];
    locations: string[];
    siteTagInstalled: boolean;
    trackedSiteId: string | null;
  };
  aiAttrs: Array<{
    landingPage: string;
    aiPlatform: string | null;
    converted: boolean;
    dealValue: number | null;
    timestamp: Date;
  }>;
  conversions: Array<{ pageUrl: string; conversionValue: number | null }>;
  sessions: Array<{ entryPage: string; pageCount: number; startedAt: Date; lastSeenAt: Date }>;
  rawMetrics: Array<{ pageUrl: string; lcp: number | null; cls: number | null; inp: number | null; ttfb: number | null }>;
  siteTagFirstEventAt: Date | null;
  siteTagLastEventAt: Date | null;
}

async function fetchFirstPartyData(brandId: string): Promise<RawFirstPartyData | null> {
  try {
    const since90d = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
    const since30d = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);

    const [brand, aiAttrs, conversions, rawMetrics] = await Promise.all([
      prisma.brand.findUnique({
        where: { id: brandId },
        select: {
          id: true,
          name: true,
          primaryServices: true,
          locations: true,
          trackedSite: { select: { id: true, firstEventAt: true, lastEventAt: true } },
        },
      }),
      prisma.aiAttribution.findMany({
        where: { brandId, timestamp: { gte: since90d } },
        select: { landingPage: true, aiPlatform: true, converted: true, dealValue: true, timestamp: true },
        orderBy: { timestamp: "desc" },
        take: 2000,
      }),
      // intentionally unfiltered: audit report; needs broader conversion history
      prisma.siteConversion.findMany({
        where: {
          brandId,
          timestamp: { gte: since30d },
          isDuplicateOf: null,
          invalidatedReason: null,
        },
        select: { pageUrl: true, conversionValue: true },
        take: 2000,
      }),
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      (prisma as any).realUserMetric.findMany({
        where: { brandId, timestamp: { gte: since30d } },
        select: { pageUrl: true, lcp: true, cls: true, inp: true, ttfb: true },
        take: 10000,
      }).catch(() => [] as RawFirstPartyData["rawMetrics"]),
    ]);

    if (!brand) {
      console.warn(`[audit-firstparty] Brand ${brandId} not found`);
      return null;
    }

    const siteTagInstalled = !!(brand.trackedSite?.firstEventAt);
    let sessions: RawFirstPartyData["sessions"] = [];

    if (brand.trackedSite) {
      sessions = await prisma.trackedSession.findMany({
        where: { trackedSiteId: brand.trackedSite.id, startedAt: { gte: since30d } },
        select: { entryPage: true, pageCount: true, startedAt: true, lastSeenAt: true },
        take: 5000,
      });
    }

    console.log(
      `[audit-firstparty] brandId=${brandId} aiAttrs=${aiAttrs.length} conversions=${conversions.length} sessions=${sessions.length} siteTagInstalled=${siteTagInstalled}`,
    );

    return {
      brand: {
        id: brand.id,
        name: brand.name,
        primaryServices: brand.primaryServices,
        locations: brand.locations,
        siteTagInstalled,
        trackedSiteId: brand.trackedSite?.id ?? null,
      },
      aiAttrs,
      conversions,
      sessions,
      rawMetrics: rawMetrics as RawFirstPartyData["rawMetrics"],
      siteTagFirstEventAt: brand.trackedSite?.firstEventAt ?? null,
      siteTagLastEventAt: brand.trackedSite?.lastEventAt ?? null,
    };
  } catch (err) {
    console.error(
      `[audit-firstparty] Failed to fetch first-party data for brandId=${brandId}:`,
      err instanceof Error ? err.message : err,
    );
    return null;
  }
}

// ─── Aggregation: raw data → typed context objects ────────────────────────────

function buildFirstPartyContexts(
  raw: RawFirstPartyData,
  crawledPages: PageData[],
): {
  brandContext: BrandContext;
  behavioralContext: BehavioralContext;
  aiAttributionContext: AiAttributionContext;
} {
  const since30d = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);

  // ── BrandContext ──────────────────────────────────────────────────────────
  const siteTagDaysActive = raw.siteTagFirstEventAt
    ? Math.floor((Date.now() - raw.siteTagFirstEventAt.getTime()) / (24 * 60 * 60 * 1000))
    : null;

  const brandContext: BrandContext = {
    brandId: raw.brand.id,
    brandName: raw.brand.name,
    services: raw.brand.primaryServices,
    locations: raw.brand.locations,
    siteTagInstalled: raw.brand.siteTagInstalled,
    siteTagDaysActive,
  };

  // ── BehavioralContext — aggregate session/conversion data by page URL ────
  const behavioralPages: BehavioralPageContext[] = [];

  function aggregateBehavioralPage(
    url: string,
    pageSessions: typeof raw.sessions,
    pageConversions: typeof raw.conversions,
  ) {
    const bounces = pageSessions.filter((s) => s.pageCount === 1).length;
    const bounceRate = pageSessions.length > 0 ? bounces / pageSessions.length : null;
    const avgSessionDuration =
      pageSessions.length > 0
        ? pageSessions.reduce(
            (sum, s) => sum + (s.lastSeenAt.getTime() - s.startedAt.getTime()),
            0,
          ) /
          pageSessions.length /
          1000
        : null;
    const conversionCount = pageConversions.length;
    const conversionValue = pageConversions.reduce((sum, c) => sum + (c.conversionValue ?? 0), 0);
    behavioralPages.push({ url, bounceRate, avgSessionDuration, conversionCount, conversionValue });
  }

  if (crawledPages.length > 0) {
    // Normal mode: filter to crawled page URLs
    for (const page of crawledPages) {
      const key = normUrl(page.url);
      const pageSessions = raw.sessions.filter((s) => normUrl(s.entryPage) === key);
      const pageConversions = raw.conversions.filter((c) => normUrl(c.pageUrl) === key);
      if (pageSessions.length === 0 && pageConversions.length === 0) continue;
      aggregateBehavioralPage(page.url, pageSessions, pageConversions);
    }
  } else {
    // Degraded mode: no crawled pages, so build from all observed URLs in telemetry
    const pageMap = new Map<string, { url: string; sessions: typeof raw.sessions; conversions: typeof raw.conversions }>();
    for (const s of raw.sessions) {
      const key = normUrl(s.entryPage);
      const entry = pageMap.get(key) ?? { url: s.entryPage, sessions: [], conversions: [] };
      entry.sessions.push(s);
      pageMap.set(key, entry);
    }
    for (const c of raw.conversions) {
      const key = normUrl(c.pageUrl);
      const entry = pageMap.get(key) ?? { url: c.pageUrl, sessions: [], conversions: [] };
      entry.conversions.push(c);
      pageMap.set(key, entry);
    }
    // Take top 30 pages by total activity
    const topEntries = [...pageMap.values()]
      .sort((a, b) => (b.sessions.length + b.conversions.length * 3) - (a.sessions.length + a.conversions.length * 3))
      .slice(0, 30);
    for (const { url, sessions: ps, conversions: pc } of topEntries) {
      aggregateBehavioralPage(url, ps, pc);
    }
  }

  const behavioralContext: BehavioralContext = { windowDays: 30, pages: behavioralPages };

  // ── AiAttributionContext ──────────────────────────────────────────────────
  const recentAttrs = raw.aiAttrs.filter((a) => a.timestamp >= since30d);
  const olderAttrs = raw.aiAttrs.filter((a) => a.timestamp < since30d);

  const totalAiReferrals = recentAttrs.length;
  const totalConversions30d = recentAttrs.filter((a) => a.converted).length;

  // Top pages by AI visits in the last 30 days
  const pageMap = new Map<string, { aiVisits: number; conversions: number; attributedRevenue: number }>();
  for (const a of recentAttrs) {
    const entry = pageMap.get(a.landingPage) ?? { aiVisits: 0, conversions: 0, attributedRevenue: 0 };
    entry.aiVisits++;
    if (a.converted) entry.conversions++;
    entry.attributedRevenue += a.dealValue ?? 0;
    pageMap.set(a.landingPage, entry);
  }
  const topPages: AiAttributionPageContext[] = [...pageMap.entries()]
    .sort((a, b) => b[1].aiVisits - a[1].aiVisits)
    .slice(0, 20)
    .map(([url, stats]) => ({ url, ...stats }));

  // Trend: recent-30d daily rate vs prior comparable period.
  // Guard: if fewer than 14 days of prior data exist, trend is meaningless.
  const siteTagAgeDays = raw.siteTagFirstEventAt
    ? Math.floor((Date.now() - raw.siteTagFirstEventAt.getTime()) / (24 * 60 * 60 * 1000))
    : null;
  const recentDailyRate = recentAttrs.length / 30;
  let trendRatio: number | null = null;
  if (siteTagAgeDays !== null && siteTagAgeDays - 30 >= 14) {
    const olderDailyRate = olderAttrs.length / 60;
    trendRatio = olderDailyRate > 0 ? recentDailyRate / olderDailyRate : null;
  }

  // Top engines by session count over the 30-day window (matches headline window).
  // No slice cap — surface all tracked engines (Copilot, etc.) regardless of position.
  const engineMap = new Map<string, number>();
  for (const a of recentAttrs) {
    const engine = a.aiPlatform ?? "unknown";
    engineMap.set(engine, (engineMap.get(engine) ?? 0) + 1);
  }
  const topEngines = [...engineMap.entries()]
    .sort((a, b) => b[1] - a[1])
    .slice(0, 10)
    .map(([engine, count]) => ({ engine, count }));

  const aiAttributionContext: AiAttributionContext = {
    windowDays: 30,
    totalAiReferrals,
    totalConversions30d,
    topPages,
    trendRatio,
    topEngines,
    referralsByDay: buildReferralsByDay(recentAttrs),
  };

  return { brandContext, behavioralContext, aiAttributionContext };
}

// ─── Daily referral rollup (sparkline data) ───────────────────────────────────

function buildReferralsByDay(
  aiAttrs: Array<{ timestamp: Date }>,
  days = 30,
): Array<{ date: string; count: number }> {
  const now = Date.now();
  const map = new Map<string, number>();
  for (let i = days - 1; i >= 0; i--) {
    const d = new Date(now - i * 24 * 60 * 60 * 1000);
    map.set(d.toISOString().slice(0, 10), 0);
  }
  const cutoff = new Date(now - days * 24 * 60 * 60 * 1000);
  for (const a of aiAttrs) {
    if (a.timestamp >= cutoff) {
      const key = a.timestamp.toISOString().slice(0, 10);
      if (map.has(key)) map.set(key, map.get(key)! + 1);
    }
  }
  return [...map.entries()].map(([date, count]) => ({ date, count }));
}

// ─── Web vitals p75 context ───────────────────────────────────────────────────

function buildWebVitalsContext(rawMetrics: RawFirstPartyData["rawMetrics"]): WebVitalEntry[] {
  const byUrl = new Map<string, { lcp: number[]; cls: number[]; inp: number[]; ttfb: number[] }>();
  for (const m of rawMetrics) {
    const entry = byUrl.get(m.pageUrl) ?? { lcp: [], cls: [], inp: [], ttfb: [] };
    if (m.lcp != null) entry.lcp.push(m.lcp);
    if (m.cls != null) entry.cls.push(m.cls);
    if (m.inp != null) entry.inp.push(m.inp);
    if (m.ttfb != null) entry.ttfb.push(m.ttfb);
    byUrl.set(m.pageUrl, entry);
  }

  const p75 = (arr: number[]): number | undefined => {
    if (arr.length === 0) return undefined;
    const sorted = [...arr].sort((a, b) => a - b);
    return sorted[Math.floor(sorted.length * 0.75)];
  };

  const entries: WebVitalEntry[] = [];
  for (const [url, data] of byUrl.entries()) {
    const samples = Math.max(data.lcp.length, data.cls.length, data.inp.length, data.ttfb.length);
    if (samples < 10) continue;
    entries.push({ url, samples, lcp75: p75(data.lcp), cls75: p75(data.cls), inp75: p75(data.inp), ttfb75: p75(data.ttfb) });
  }

  return entries.sort((a, b) => b.samples - a.samples).slice(0, 20);
}

// ─── Crawl stats URL context ──────────────────────────────────────────────────

function buildCrawlStatsContext(pages: PageData[]): CrawlStatsContext {
  const indexable = pages.filter((p) => p.isIndexable);
  return {
    missingH1Urls: indexable.filter((p) => !p.h1).map((p) => p.url).slice(0, 15),
    missingMetaDescUrls: indexable.filter((p) => !p.metaDescription).map((p) => p.url).slice(0, 15),
    thinContentUrls: indexable.filter((p) => (p.wordCount ?? 0) < 300).map((p) => p.url).slice(0, 15),
    missingSchemaUrls: indexable.filter((p) => !p.hasSchema).map((p) => p.url).slice(0, 15),
  };
}

// ─── Dimension scores ─────────────────────────────────────────────────────────

// computeDimensionScores and computeHealthScore are imported from ./scoring

// ─── Site Tag context ─────────────────────────────────────────────────────────

function computeSiteTagContext(
  firstEventAt: Date | null,
  lastEventAt: Date | null,
): SiteTagContext {
  if (!firstEventAt) return { installedAt: null, daysActive: null, state: "silent" };

  const now = Date.now();
  const daysActive = Math.floor((now - firstEventAt.getTime()) / (24 * 60 * 60 * 1000));
  const daysSinceLast = lastEventAt
    ? Math.floor((now - lastEventAt.getTime()) / (24 * 60 * 60 * 1000))
    : 999;

  const state: SiteTagContext["state"] =
    daysSinceLast > 7 ? "silent" : daysActive < 14 ? "active-fresh" : "active-mature";

  return { installedAt: firstEventAt.toISOString(), daysActive, state };
}

// ─── Branded SERP queries ─────────────────────────────────────────────────────

function buildBrandedQueries(input: AuditInput): string[] {
  const queries: string[] = [input.brandName];
  if (input.services.length > 0) {
    queries.push(`${input.brandName} ${input.services[0]}`);
  }
  if (input.locations.length > 0) {
    queries.push(`${input.brandName} ${input.locations[0]}`);
  }
  if (input.services.length > 1) {
    queries.push(`${input.brandName} ${input.services[1]}`);
  }
  return queries.slice(0, 4);
}

// ─── Crawl samples (trust signal for the public result page) ─────────────────

function extractCrawlSamples(pages: PageData[]): Array<{ url: string; issues: string[] }> {
  return pages.slice(0, 5).map((page) => {
    const issues: string[] = [];
    if (!page.h1) issues.push("No H1 heading found");
    else if (page.h1.length < 20) issues.push("H1 too short");
    if (!page.metaDescription) issues.push("No meta description");
    else if (page.metaDescription.length > 160) issues.push(`Meta description too long (${page.metaDescription.length} chars)`);
    else if (page.metaDescription.length < 70) issues.push("Meta description too short");
    if (!page.title) issues.push("No page title");
    else if (page.title.length > 60) issues.push(`Title too long (${page.title.length} chars)`);
    const missingAlt = page.images.filter((img) => !img.alt).length;
    if (missingAlt > 0) issues.push(`${missingAlt} image${missingAlt > 1 ? "s" : ""} missing alt text`);
    if (!page.isIndexable) issues.push("Page is non-indexable");
    return { url: page.url, issues: issues.slice(0, 3) };
  }).filter((s) => s.issues.length > 0);
}

// ─── Strategic Roadmap assembly ───────────────────────────────────────────────

const PHASE_META: Record<string, { phaseName: string; timeframe: string; theme: string; successMetrics: string[] }> = {
  "week-1": {
    phaseName: "Week 1",
    timeframe: "Days 1-7",
    theme: "Foundation & Quick Wins",
    successMetrics: [
      "All targeted schema types deployed and validated in Search Console",
      "Meta titles and H1s updated on priority pages",
      "Core CTA copy updated on homepage and top landing pages",
      "Zero console errors on priority pages",
    ],
  },
  "weeks-2-4": {
    phaseName: "Weeks 2-4",
    timeframe: "Days 8-28",
    theme: "Content Depth Expansion",
    successMetrics: [
      "Service pages expanded to 600+ words with FAQ sections added",
      "Internal linking structure updated to support new content hubs",
      "Redirect chains resolved across top-traffic URLs",
      "AI Overview presence monitored for targeted queries",
    ],
  },
  "month-2": {
    phaseName: "Month 2",
    timeframe: "Days 29-60",
    theme: "Authority Building",
    successMetrics: [
      "10+ new referring domains from directory and citation submissions",
      "Thought leadership content published and indexed",
      "Backlink profile growth trend established",
      "Brand mentions appearing in AI Overviews for 2+ target queries",
    ],
  },
  "month-3": {
    phaseName: "Month 3",
    timeframe: "Days 61-90",
    theme: "Optimization & Monitoring",
    successMetrics: [
      "A/B test results reviewed and winning variants locked",
      "Automated monitoring alerts configured for schema and ranking",
      "Score projection validated against actual performance data",
      "90-day review completed with next quarter roadmap drafted",
    ],
  },
};

const PHASE_ORDER = ["week-1", "weeks-2-4", "month-2", "month-3"] as const;

function buildStrategicRoadmap(
  priorityActions: SeoAction[],
  currentScore: number,
  isThirdParty: boolean,
): StrategicRoadmap | null {
  const actionsWithPhase = priorityActions.filter((a) => a.phaseAssignment);
  if (actionsWithPhase.length === 0) return null;

  const maxScore = isThirdParty ? 85 : 90;
  let runningScore = currentScore;
  let totalEffort = 0;
  let totalLift = 0;

  const phases = PHASE_ORDER.map((phaseKey) => {
    const meta = PHASE_META[phaseKey]!;
    const phaseActions = actionsWithPhase.filter((a) => a.phaseAssignment === phaseKey);
    const phaseEffort = phaseActions.reduce((s, a) => s + (a.effortHours ?? 1), 0);
    const phaseLift = phaseActions.reduce((s, a) => s + (a.expectedLiftPoints ?? 1), 0);

    totalEffort += phaseEffort;
    totalLift += phaseLift;
    runningScore = Math.min(maxScore, runningScore + phaseLift);

    return {
      phaseName: meta.phaseName,
      timeframe: meta.timeframe,
      theme: meta.theme,
      actions: phaseActions.map((a) => ({
        title: a.title,
        type: a.implementation?.type ?? "Content",
        effortHours: a.effortHours ?? 1,
        expectedLiftPoints: a.expectedLiftPoints ?? 1,
      })),
      cumulativeEffortHours: Math.round(totalEffort * 10) / 10,
      cumulativeLiftPoints: totalLift,
      projectedScoreAtEndOfPhase: runningScore,
      successMetrics: meta.successMetrics,
    };
  }).filter((p) => p.actions.length > 0);

  if (phases.length === 0) return null;

  return {
    phases,
    totalEffortHours: Math.round(totalEffort * 10) / 10,
    totalLiftPoints: totalLift,
    finalProjectedScore: Math.min(maxScore, currentScore + totalLift),
  };
}

// ─── Main runner ─────────────────────────────────────────────────────────────

export async function runAudit(auditId: string): Promise<void> {
  console.log(`[audit-runner] Starting audit ${auditId}`);

  try {
    await markStarted(auditId);
    await advanceTier(auditId, 1);
    await updateAuditProgress(auditId, 1, "Initializing audit");

    // Load audit record
    const audit = await prisma.audit.findUnique({ where: { id: auditId } });
    if (!audit) throw new Error(`Audit ${auditId} not found`);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const auditSource = ((audit as any).source as string | undefined) ?? "admin";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const auditMode = ((audit as any).auditMode as string | undefined) ?? "auto";

    const input: AuditInput = {
      brandName: audit.requestedBrandName ?? "Unknown Brand",
      website: audit.requestedWebsite ?? "",
      competitors: audit.requestedCompetitors ?? [],
      locations: audit.requestedLocations ?? [],
      services: audit.requestedServices ?? [],
      isLocal: audit.isLocal,
    };

    if (!input.website) throw new Error("Audit has no website URL");

    // ── Step 2: Site Tag check ──────────────────────────────────────────────
    let siteTagDetected: boolean | null = null;

    if (audit.brandId) {
      await advanceTier(auditId, 2);
      await updateAuditProgress(auditId, 2, "Checking Site Tag");

      if (auditMode === "first-party") {
        siteTagDetected = true;
      } else if (auditMode === "third-party") {
        siteTagDetected = false;
      } else {
        // "auto" — check whether the brand has an active site tag
        const brand = await prisma.brand.findUnique({
          where: { id: audit.brandId },
          select: { trackedSite: { select: { firstEventAt: true } } },
        });
        siteTagDetected = !!(brand?.trackedSite?.firstEventAt);
      }

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      await (prisma.audit.update as any)({
        where: { id: auditId },
        data: { siteTagDetected },
      });

      console.log(`[audit-runner] ${auditId} siteTagDetected=${siteTagDetected} auditMode=${auditMode}`);
    }

    const shouldFetchFirstParty =
      !!audit.brandId &&
      auditMode !== "third-party" &&
      (auditMode === "first-party" || siteTagDetected === true);

    // ── Tier 3 / Step 3: Crawl + parallel enrichment ──────────────────────
    await advanceTier(auditId, audit.brandId ? 3 : 2);
    await updateAuditProgress(auditId, 3, "Crawling site", input.website);
    console.log(`[audit-runner] ${auditId} Scanning site: ${input.website} source=${auditSource} shouldFetchFirstParty=${shouldFetchFirstParty}`);

    // Public audits get a reduced page budget to control crawler cost
    const scanOptions = auditSource === "public" ? { maxPages: 3 } : undefined;

    // For 3rd-party audits: extract domain and pre-generate branded SERP queries
    const domain = input.website.replace(/^https?:\/\//, "").replace(/\/$/, "").split("/")[0] ?? "";
    const brandedQueries = buildBrandedQueries(input);
    const isThirdParty = !shouldFetchFirstParty;

    // Phase 1: DFS crawl + first-party fetch + (for 3rd-party) backlinks — all in parallel
    // Note: SERP analysis runs after industry classification (Phase 3) to use suggestedQueries
    const [crawlResult, rawFirstParty, rawAuthority] = await Promise.all([
      scanSite(input.website, scanOptions),
      shouldFetchFirstParty ? fetchFirstPartyData(audit.brandId!) : Promise.resolve(null),
      isThirdParty
        ? fetchBacklinksSummary(domain).catch((e) => {
            console.warn(`[audit-runner] ${auditId} backlinks failed (non-fatal):`, e instanceof Error ? e.message : e);
            return null;
          })
        : Promise.resolve(null),
    ]);

    const { pages, summary, crawlError } = crawlResult;
    console.log(`[audit-runner] ${auditId} Scan complete: ${pages.length} pages, summary=${JSON.stringify(summary)}`);
    if (rawAuthority) console.log(`[audit-runner] ${auditId} Authority: rank=${rawAuthority.domainRank} backlinks=${rawAuthority.totalBacklinks} referringDomains=${rawAuthority.referringDomains}`);

    if (pages.length === 0) {
      const canDegrade = shouldFetchFirstParty && (rawFirstParty?.brand.siteTagInstalled === true);
      const errorReason = crawlError?.reason ?? "unknown";
      const errorDetail = crawlError?.detail ?? `Crawl returned 0 pages for ${input.website}`;

      console.log(`[audit-runner] ${auditId} crawl returned 0 pages reason=${errorReason} canDegrade=${canDegrade}`);

      if (!canDegrade) {
        // Mark failed with classified reason
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        await (prisma.audit.update as any)({ where: { id: auditId }, data: { errorReason, errorDetail: errorDetail.slice(0, 500) } });
        const userFacingMessage = getCrawlErrorMessage(errorReason);
        throw Object.assign(new Error(errorDetail), { userFacingMessage });
      }

      // ── DEGRADED MODE: 1st-party with Site Tag, crawl failed ────────────────
      console.log(`[audit-runner] ${auditId} entering degraded mode`);

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      await (prisma.audit.update as any)({
        where: { id: auditId },
        data: { degradedMode: true, errorReason, errorDetail: errorDetail.slice(0, 500) },
      });

      // Build first-party contexts (no crawled pages)
      if (rawFirstParty) {
        try {
          const contexts = buildFirstPartyContexts(rawFirstParty, []);
          input.brandContext = contexts.brandContext;
          input.behavioralContext = contexts.behavioralContext;
          input.aiAttributionContext = contexts.aiAttributionContext;
        } catch (ctxErr) {
          console.error(`[audit-runner] ${auditId} degraded context build failed:`, ctxErr instanceof Error ? ctxErr.message : ctxErr);
        }
        // Web vitals: available from first-party data regardless of crawl outcome
        if (rawFirstParty.rawMetrics.length > 0) {
          const webVitals = buildWebVitalsContext(rawFirstParty.rawMetrics);
          if (webVitals.length > 0) {
            input.webVitalsContext = webVitals;
            console.log(`[audit-runner] ${auditId} degraded webVitals=${webVitals.length} URLs`);
          }
        }
      }

      // Persist minimal raw data
      await prisma.audit.update({
        where: { id: auditId },
        data: {
          rawData: JSON.parse(JSON.stringify({ pagesScanned: 0, summary, scanCompletedAt: new Date().toISOString(), degraded: true })),
        },
      });

      await advanceTier(auditId, shouldFetchFirstParty ? 5 : 4);
      await updateAuditProgress(auditId, 5, "Extracting content");

      // Fetch Firecrawl content using a synthetic homepage seed — WAF often allows Firecrawl
      // even when the DFS crawler is blocked, giving the synthesizer real page content.
      const degradedSyntheticPage: PageData = {
        url: input.website, statusCode: 200,
        internalLinks: [], externalLinks: [], images: [],
        hasSchema: false, schemaTypes: [], isIndexable: true,
      };
      const degradedContentSamples = await fetchKeyPageContent([degradedSyntheticPage]).catch((e) => {
        console.warn(`[audit-runner] ${auditId} degraded content fetch failed (non-fatal):`, e instanceof Error ? e.message : e);
        return [];
      });

      // Industry classification: classifyIndustry returns suggestedQueries needed for SERP + AI Search Verification
      const degradedIndustryContext = await classifyIndustry(input.brandName, input.website, degradedContentSamples).catch(() => null);
      const degradedIndustry = degradedIndustryContext?.industry ?? "other";
      input.inferredIndustry = degradedIndustry;
      if (degradedIndustryContext) input.industryContext = degradedIndustryContext;
      console.log(`[audit-runner] ${auditId} degraded industry=${degradedIndustry} contentSamples=${degradedContentSamples.length} suggestedQueries=${degradedIndustryContext?.suggestedQueries.length ?? 0}`);

      // GSC Summary + Signal Center + Error Patterns: crawl-independent, read from GSC DB and Site Tag DB
      let degRawGscSummary: Awaited<ReturnType<typeof getGscSummary>> = null;
      let degRawSignalCenter: Awaited<ReturnType<typeof aggregateSignalCenter>> = null;
      if (shouldFetchFirstParty && audit.brandId && rawFirstParty) {
        [degRawGscSummary, degRawSignalCenter] = await Promise.all([
          getGscSummary(audit.brandId, 30).catch(() => null),
          aggregateSignalCenter(audit.brandId, 30).catch((e) => {
            console.warn(`[audit-runner] ${auditId} degraded Signal Center failed (non-fatal):`, e instanceof Error ? e.message : e);
            return null;
          }),
        ]);
        if (degRawSignalCenter) input.signalCenterContext = degRawSignalCenter;

        const rawErrorPats = await getErrorPatterns(audit.brandId, { limit: 2000 }).catch(() => []);
        if (rawErrorPats.length > 0) {
          const top = rawErrorPats[0];
          input.realUserTechContext = {
            patternCount: rawErrorPats.filter(
              (p) => p.occurrenceCount >= 100 || p.affectedSessionCount >= 20,
            ).length,
            topFinding: `${top.message.slice(0, 80)} (${top.occurrenceCount} occurrences on ${top.affectedPageCount} pages)`,
          };
        }
        console.log(`[audit-runner] ${auditId} degraded gscClicks=${degRawGscSummary?.totalClicks ?? "n/a"} signalCenter=${degRawSignalCenter ? `${degRawSignalCenter.totalSessions} sessions` : "null"}`);
      }

      await advanceTier(auditId, shouldFetchFirstParty ? 6 : 5);
      await advanceTier(auditId, shouldFetchFirstParty ? 7 : 6);
      await updateAuditProgress(auditId, 6, "Generating report");

      // SERP analysis: crawl-independent; uses suggestedQueries from classifyIndustry, falls back to branded queries
      const degradedSerpQueries = degradedIndustryContext?.suggestedQueries?.length
        ? degradedIndustryContext.suggestedQueries
        : brandedQueries;
      const degradedSerpPromise = degradedSerpQueries.length > 0
        ? fetchSerpAnalysis(degradedSerpQueries).catch((e) => {
            console.warn(`[audit-runner] ${auditId} degraded serpAnalysis failed (non-fatal):`, e instanceof Error ? e.message : e);
            return null;
          })
        : Promise.resolve(null);
      const degRawSerpAnalysis = await Promise.race([
        degradedSerpPromise,
        new Promise<null>((resolve) => setTimeout(() => {
          console.warn(`[audit-runner] ${auditId} degraded serpAnalysis timed out after 90s, continuing without SERP`);
          resolve(null);
        }, 90_000)),
      ]);
      if (degRawSerpAnalysis) {
        console.log(`[audit-runner] ${auditId} degraded SERP: positions=${degRawSerpAnalysis.positions.length} aiOverviews=${degRawSerpAnalysis.aiOverviewPresence.filter((a) => a.hasAiOverview).length} competitorDomains=${degRawSerpAnalysis.competitorDomains.join(",")}`);
      }

      // Industry Benchmark Comparison: crawl-independent; needs industryContext + SERP
      const degRawIndustryBenchmark = degradedIndustryContext
        ? aggregateIndustryBenchmarkComparison(
            degradedIndustryContext,
            undefined,
            degRawSerpAnalysis ?? undefined,
            0,
            0,
          )
        : null;
      if (degRawIndustryBenchmark) {
        input.industryBenchmarkComparisonContext = degRawIndustryBenchmark;
        console.log(`[audit-runner] ${auditId} degraded IndustryBenchmark: industry=${degRawIndustryBenchmark.industry} overallPercentile=${degRawIndustryBenchmark.overallPercentile}`);
      }

      // AI Engine Personality Analysis: crawl-independent; static profiles + optional Site Tag overlay
      const degRawAiEnginePersonality = aggregateAiEnginePersonalityAnalysis(
        input.aiAttributionContext ?? null,
      );
      input.aiEnginePersonalityAnalysisContext = degRawAiEnginePersonality;
      console.log(`[audit-runner] ${auditId} degraded AiEnginePersonality: engines=${degRawAiEnginePersonality.engines.length} topEngine=${degRawAiEnginePersonality.topEngineId ?? "n/a"}`);

      // Site Tag DB: Multi-Touch AI Path, Revenue Attribution, Predictive Conversion Scoring, Conversion Heat Map
      let degRawMultiTouchAiPath: Awaited<ReturnType<typeof aggregateMultiTouchAiPath>> = null;
      let degRawRevenueAttribution: Awaited<ReturnType<typeof aggregateRevenueAttribution>> = null;
      let degRawPredictiveConversionScoring: Awaited<ReturnType<typeof aggregatePredictiveConversionScoring>> = null;
      let degRawConversionHeatMap: Awaited<ReturnType<typeof aggregateConversionHeatMap>> = null;
      if (shouldFetchFirstParty && audit.brandId) {
        [
          degRawMultiTouchAiPath,
          degRawRevenueAttribution,
          degRawPredictiveConversionScoring,
          degRawConversionHeatMap,
        ] = await Promise.all([
          aggregateMultiTouchAiPath(audit.brandId, input.aiAttributionContext ?? null, degRawSignalCenter ?? null).catch((e) => {
            console.warn(`[audit-runner] ${auditId} degraded MultiTouchAiPath failed (non-fatal):`, e instanceof Error ? e.message : e);
            return null;
          }),
          aggregateRevenueAttribution(audit.brandId, 30).catch((e) => {
            console.warn(`[audit-runner] ${auditId} degraded RevenueAttribution failed (non-fatal):`, e instanceof Error ? e.message : e);
            return null;
          }),
          aggregatePredictiveConversionScoring(audit.brandId, 30).catch((e) => {
            console.warn(`[audit-runner] ${auditId} degraded PredictiveConversionScoring failed (non-fatal):`, e instanceof Error ? e.message : e);
            return null;
          }),
          aggregateConversionHeatMap(audit.brandId, 30).catch((e) => {
            console.warn(`[audit-runner] ${auditId} degraded ConversionHeatMap failed (non-fatal):`, e instanceof Error ? e.message : e);
            return null;
          }),
        ]);
        if (degRawMultiTouchAiPath) {
          input.multiTouchAiPathContext = degRawMultiTouchAiPath;
          console.log(`[audit-runner] ${auditId} degraded MultiTouchAiPath: paths=${degRawMultiTouchAiPath.topConversionPaths.length}`);
        }
        if (degRawRevenueAttribution) {
          input.revenueAttributionContext = degRawRevenueAttribution;
          console.log(`[audit-runner] ${auditId} degraded RevenueAttribution: hasRevenue=${degRawRevenueAttribution.hasRevenueTracking} confidence=${degRawRevenueAttribution.dataConfidence}`);
        }
        if (degRawPredictiveConversionScoring) {
          input.predictiveConversionScoringContext = degRawPredictiveConversionScoring;
          console.log(`[audit-runner] ${auditId} degraded PredictiveConversionScoring: sessions=${degRawPredictiveConversionScoring.totalScoredSessions}`);
        }
        if (degRawConversionHeatMap) {
          input.conversionHeatMapContext = degRawConversionHeatMap;
          console.log(`[audit-runner] ${auditId} degraded ConversionHeatMap: hasClickData=${degRawConversionHeatMap.hasClickData}`);
        }
      }

      // AI Search Verification: crawl-independent; guards against empty suggestedQueries
      const degRawAiSearchVerification = degradedIndustryContext?.suggestedQueries?.length
        ? await aggregateAiSearchVerification({
            brandName: input.brandName,
            brandUrl: input.website,
            brandLocation: input.locations?.[0],
            industryContext: degradedIndustryContext,
            competitors: input.competitors,
          }).catch((e) => {
            console.warn(`[audit-runner] ${auditId} degraded AiSearchVerification failed (non-fatal):`, e instanceof Error ? e.message : e);
            return null;
          })
        : null;
      if (degRawAiSearchVerification) {
        input.aiSearchVerificationContext = degRawAiSearchVerification;
        console.log(`[audit-runner] ${auditId} degraded AiSearchVerification: queries=${degRawAiSearchVerification.queriesChecked} mentionRate=${Math.round(degRawAiSearchVerification.brandVisibility.overallMentionRate * 100)}%`);
      }

      console.log(`[audit-runner] ${auditId} degraded mode: invoking Phase 12 (homepage-only), 13, 15 in parallel`);
      const [
        degBrandAuthoritySettled,
        degMobileFirstSettled,
        degPageSpeedSettled,
        degVisualAnnotationsSettled,
      ] = await Promise.allSettled([
        aggregateBrandAuthorityScore({
          brandName: input.brandName,
          brandDomain: input.website,
          vertical: degradedIndustry,
          existingAuditContext: { eeatAudit: null, liveVisualAnnotations: null, schemaOpportunityForecast: null },
        }).catch((e) => {
          console.warn(`[audit-runner] ${auditId} degraded BrandAuthority failed (non-fatal):`, e instanceof Error ? e.message : e);
          return null;
        }),
        aggregateMobileFirstAudit({
          brandDomain: input.website,
          vertical: degradedIndustry,
          liveVisualAnnotations: null,
        }).catch((e) => {
          console.warn(`[audit-runner] ${auditId} degraded MobileFirstAudit failed (non-fatal):`, e instanceof Error ? e.message : e);
          return null;
        }),
        aggregatePageSpeedInsights({
          brandDomain: input.website,
          vertical: degradedIndustry,
          liveVisualAnnotations: null,
        }).catch((e) => {
          console.warn(`[audit-runner] ${auditId} degraded PageSpeedInsights failed (non-fatal):`, e instanceof Error ? e.message : e);
          return null;
        }),
        aggregateLiveVisualAnnotations(
          input.website,
          { brandName: input.brandName, hasSchemaDetected: false, hasLocalSchema: input.isLocal },
          degradedIndustry,
          [],
        ).catch((e) => {
          console.warn(`[audit-runner] ${auditId} degraded LiveVisualAnnotations failed (non-fatal):`, e instanceof Error ? e.message : e);
          return null;
        }),
      ]);
      const degradedBrandAuthority = degBrandAuthoritySettled.status === "fulfilled" ? degBrandAuthoritySettled.value : null;
      const degradedMobileFirst = degMobileFirstSettled.status === "fulfilled" ? degMobileFirstSettled.value : null;
      const degradedPageSpeed = degPageSpeedSettled.status === "fulfilled" ? degPageSpeedSettled.value : null;
      const degradedVisualAnnotations = degVisualAnnotationsSettled.status === "fulfilled" ? degVisualAnnotationsSettled.value : null;
      console.log(`[audit-runner] ${auditId} degraded phases: authority=${degradedBrandAuthority ? "ok" : "skip"} mobile=${degradedMobileFirst ? "ok" : "skip"} pagespeed=${degradedPageSpeed ? "ok" : "skip"} visualAnnotations=${degradedVisualAnnotations ? "ok" : "skip"}`);

      // Full synthesizer call: degradedMode flag adjusts prompts to lead with telemetry/content
      const degradedFindings = buildEmptyFindings();
      const degScores = computeDimensionScores(
        degradedFindings, input.aiAttributionContext, input.behavioralContext, input.webVitalsContext, true,
      );
      const degradedHealthScore = computeHealthScore(degScores);
      const seoSection = await synthesizeSeoSection(
        {
          ...input,
          degradedMode: true,
          brandAuthorityContext: degradedBrandAuthority,
          mobileFirstContext: degradedMobileFirst,
          pageSpeedContext: degradedPageSpeed,
          liveVisualAnnotationsContext: degradedVisualAnnotations,
        },
        degradedFindings,
        degradedHealthScore,
        { critical: 0, high: 0, medium: 0, low: 0 },
        degradedContentSamples,
        undefined,
      );

      // Real-User Technical Findings: post-synthesis; returns [] when no qualifying error patterns
      if (shouldFetchFirstParty && audit.brandId) {
        const techFindings = await runRealUserTechnicalSynthesizer(audit.brandId).catch((e) => {
          console.warn(`[audit-runner] ${auditId} degraded real-user tech synthesizer failed (non-fatal):`, e instanceof Error ? e.message : e);
          return [];
        });
        if (techFindings.length > 0) {
          seoSection.prioritizedFindings = [...(seoSection.prioritizedFindings ?? []), ...techFindings];
          console.log(`[audit-runner] ${auditId} degraded real-user tech findings merged: ${techFindings.length} patterns`);
        }
      }

      // Strategic Roadmap: post-synthesis; same assembly as non-degraded
      const degRawStrategicRoadmap = buildStrategicRoadmap(seoSection.priorityActions, degradedHealthScore, false);

      // AI Source Attribution Comparison: crawl-independent; compares public predictions to Site Tag actuals
      const degRawAiSourceAttribution = aggregateAiSourceAttributionComparison(
        degRawSerpAnalysis ?? null,
        degradedIndustryContext ?? null,
        degRawSignalCenter ?? null,
        input.aiAttributionContext ?? null,
        input.behavioralContext ?? null,
      );
      if (degRawAiSourceAttribution) {
        console.log(`[audit-runner] ${auditId} degraded AiSourceAttribution: dimensions=${degRawAiSourceAttribution.predictionVsActual.length} gapMagnitude=${degRawAiSourceAttribution.overallGapMagnitude}`);
      }

      // Behavioral Action Bridge: post-synthesis; crawl-independent; needs signalCenter with sufficient confidence
      const degRawBehavioralActionBridge = degRawSignalCenter && degRawSignalCenter.dataConfidence !== "low"
        ? aggregateBehavioralActionBridge(
            seoSection.priorityActions,
            seoSection.prioritizedFindings,
            degRawSignalCenter,
            null,
            input.aiAttributionContext ?? null,
          )
        : null;
      if (degRawBehavioralActionBridge) {
        console.log(`[audit-runner] ${auditId} degraded BehavioralActionBridge: enhanced=${degRawBehavioralActionBridge.enhancedActions.length} newFindings=${degRawBehavioralActionBridge.newBehavioralFindings.length}`);
      }

      // Lost Opportunity Calculator: post-synthesis; crawl-independent; guards against missing industryContext
      const degRawLostOpportunity = degradedIndustryContext
        ? await aggregateLostOpportunity(
            degradedIndustryContext,
            seoSection.priorityActions,
            seoSection.prioritizedFindings,
            degRawSerpAnalysis ?? null,
            null,
            null,
          ).catch((e) => {
            console.warn(`[audit-runner] ${auditId} degraded LostOpportunity failed (non-fatal):`, e instanceof Error ? e.message : e);
            return null;
          })
        : null;
      if (degRawLostOpportunity) {
        console.log(`[audit-runner] ${auditId} degraded LostOpportunity: total=$${degRawLostOpportunity.aggregateOpportunity.totalMonthlyOpportunityUsd}/mo confidence=${degRawLostOpportunity.confidence}`);
      }

      const siteTagContext = rawFirstParty
        ? computeSiteTagContext(rawFirstParty.siteTagFirstEventAt, rawFirstParty.siteTagLastEventAt)
        : undefined;

      // Competitive landscape and AI visibility assembled from SERP (same shape as non-degraded)
      const degCompetitiveLandscape = degRawSerpAnalysis
        ? (() => {
            const total = degRawSerpAnalysis.aiOverviewPresence.length;
            const withAi = degRawSerpAnalysis.aiOverviewPresence.filter((a) => a.hasAiOverview).length;
            const withBrand = degRawSerpAnalysis.aiOverviewPresence.filter((a) => a.brandMentioned).length;
            return {
              topCompetitors: degRawSerpAnalysis.competitorDomains,
              aiOverviewCoveragePercent: total > 0 ? Math.round((withAi / total) * 100) : 0,
              aiOverviewBrandCitationRate: withAi > 0 ? Math.round((withBrand / withAi) * 100) : 0,
            };
          })()
        : undefined;

      const degPublicAiVisibility = degRawSerpAnalysis
        ? {
            queriesWithAiOverview: degRawSerpAnalysis.aiOverviewPresence.filter((a) => a.hasAiOverview).length,
            totalQueriesChecked: degRawSerpAnalysis.aiOverviewPresence.length,
            brandMentionedInAiOverview: degRawSerpAnalysis.aiOverviewPresence.some((a) => a.brandMentioned),
            aiOverviewDetails: degRawSerpAnalysis.aiOverviewPresence.map((a) => ({
              query: a.keyword,
              brandMentioned: a.brandMentioned,
              snippet: a.snippet,
            })),
          }
        : undefined;

      const degOrganicSearchSummary = degRawGscSummary
        ? {
            totalClicks30d: degRawGscSummary.totalClicks,
            totalImpressions30d: degRawGscSummary.totalImpressions,
            avgCtr: degRawGscSummary.avgCtr,
            avgPosition: degRawGscSummary.avgPosition,
            topQueries: degRawGscSummary.topQueries.slice(0, 10).map((q) => ({
              query: q.query,
              clicks: q.clicks,
              impressions: q.impressions,
              position: q.position,
            })),
            topPages: degRawGscSummary.topPages.slice(0, 10).map((p) => ({
              page: p.page,
              clicks: p.clicks,
              impressions: p.impressions,
              position: p.position,
            })),
          }
        : undefined;

      const degAiConversionAttribution = input.aiAttributionContext
        ? (() => {
            const ctx = input.aiAttributionContext!;
            const rate = ctx.totalAiReferrals > 0
              ? Math.round((ctx.totalConversions30d / ctx.totalAiReferrals) * 1000) / 10
              : 0;
            const topPages = ctx.topPages
              .filter((p) => p.conversions > 0)
              .sort((a, b) => b.conversions - a.conversions)
              .slice(0, 5)
              .map((p) => ({ url: p.url, aiVisits: p.aiVisits, conversions: p.conversions }));
            return {
              totalAiReferrals30d: ctx.totalAiReferrals,
              totalAiConversions30d: ctx.totalConversions30d,
              aiConversionRate: rate,
              topConvertingPages: topPages,
              topConvertingEngines: ctx.topEngines.slice(0, 5),
            };
          })()
        : undefined;

      const enrichedSeoSection = {
        ...seoSection,
        healthScore: degradedHealthScore,
        dimensionScores: degScores,
        ...(siteTagContext && { siteTagContext }),
        ...(degradedIndustryContext && { industryContext: degradedIndustryContext }),
        degradedMode: true,
        crawlErrorReason: errorReason,
        ...(degradedBrandAuthority && { brandAuthorityScore: degradedBrandAuthority }),
        ...(degradedMobileFirst?.hasAudit && { mobileFirstAudit: degradedMobileFirst }),
        ...(degradedPageSpeed?.hasInsights && { pageSpeedInsights: degradedPageSpeed }),
        ...(degradedVisualAnnotations && { liveVisualAnnotations: degradedVisualAnnotations }),
        ...(degCompetitiveLandscape && { competitiveLandscape: degCompetitiveLandscape }),
        ...(degPublicAiVisibility && { publicAiVisibility: degPublicAiVisibility }),
        ...(degOrganicSearchSummary && { organicSearchSummary: degOrganicSearchSummary }),
        ...(degAiConversionAttribution && { aiConversionAttribution: degAiConversionAttribution }),
        ...(shouldFetchFirstParty && { signalCenter: degRawSignalCenter ?? null }),
        ...(degRawIndustryBenchmark && { industryBenchmarkComparison: degRawIndustryBenchmark }),
        aiEnginePersonalityAnalysis: degRawAiEnginePersonality,
        ...(degRawAiSourceAttribution && { aiSourceAttributionComparison: degRawAiSourceAttribution }),
        ...(degRawBehavioralActionBridge && { behavioralActionBridge: degRawBehavioralActionBridge }),
        ...(degRawMultiTouchAiPath && { multiTouchAiPath: degRawMultiTouchAiPath }),
        ...(degRawRevenueAttribution && { revenueAttribution: degRawRevenueAttribution }),
        ...(degRawPredictiveConversionScoring && { predictiveConversionScoring: degRawPredictiveConversionScoring }),
        ...(degRawConversionHeatMap && { conversionHeatMap: degRawConversionHeatMap }),
        ...(degRawAiSearchVerification && { aiSearchVerification: degRawAiSearchVerification }),
        ...(degRawLostOpportunity && { lostOpportunityCalculation: degRawLostOpportunity }),
        ...(degRawStrategicRoadmap && { strategicRoadmap: degRawStrategicRoadmap }),
      };

      await prisma.audit.update({
        where: { id: auditId },
        data: { seoSection: enrichedSeoSection as object },
      });

      await updateAuditProgress(auditId, 7, "Complete");
      if (audit.brandId) {
        await Promise.all([
          deleteCachedPrefix(`traffic-summary:${audit.brandId}:`),
          deleteCachedPrefix(`conversion-summary:${audit.brandId}:`),
          deleteCached(`audit-latest:${audit.brandId}`),
        ]).catch(() => {});
      }
      await markCompleted(auditId);
      console.log(`[audit-runner] ${auditId} Completed in degraded mode`);
      return;
    }

    // ── Step 4: First-party data (if applicable) ───────────────────────────
    if (shouldFetchFirstParty) {
      await updateAuditProgress(auditId, 4, "Loading first-party data");
    }

    // Attach first-party contexts to input (non-fatal if unavailable)
    let rawGscSummary: Awaited<ReturnType<typeof getGscSummary>> = null;
    let rawSignalCenter: Awaited<ReturnType<typeof aggregateSignalCenter>> = null;
    if (audit.brandId && rawFirstParty) {
      try {
        const contexts = buildFirstPartyContexts(rawFirstParty, pages);
        input.brandContext = contexts.brandContext;
        input.behavioralContext = contexts.behavioralContext;
        input.aiAttributionContext = contexts.aiAttributionContext;
        const webVitals = buildWebVitalsContext(rawFirstParty.rawMetrics);
        if (webVitals.length > 0) input.webVitalsContext = webVitals;
        // Fetch GSC + Signal Center in parallel (both non-fatal)
        [rawGscSummary, rawSignalCenter] = await Promise.all([
          getGscSummary(audit.brandId, 30).catch(() => null),
          aggregateSignalCenter(audit.brandId, 30).catch((e) => {
            console.warn(`[audit-runner] ${auditId} Signal Center aggregation failed (non-fatal):`, e instanceof Error ? e.message : e);
            return null;
          }),
        ]);
        if (rawSignalCenter) input.signalCenterContext = rawSignalCenter;

        // Real-user tech context: sample top JS error patterns to inject into
        // the executive summary prompt and later create tech findings from.
        const rawErrorPats = await getErrorPatterns(audit.brandId, { limit: 2000 }).catch(() => []);
        if (rawErrorPats.length > 0) {
          const top = rawErrorPats[0];
          input.realUserTechContext = {
            patternCount: rawErrorPats.filter(
              (p) => p.occurrenceCount >= 100 || p.affectedSessionCount >= 20,
            ).length,
            topFinding: `${top.message.slice(0, 80)} (${top.occurrenceCount} occurrences on ${top.affectedPageCount} pages)`,
          };
        }
        console.log(
          `[audit-firstparty] ${auditId} Loaded: ${contexts.behavioralContext.pages.length} behavioral pages, ${contexts.aiAttributionContext.totalAiReferrals} AI referrals (30d), webVitalUrls=${webVitals.length}, gscClicks=${rawGscSummary?.totalClicks ?? "n/a"}, signalCenter=${rawSignalCenter ? `${rawSignalCenter.totalSessions} sessions` : "null"}`,
        );
      } catch (ctxErr) {
        console.error(
          `[audit-firstparty] ${auditId} Failed to build first-party contexts (non-fatal):`,
          ctxErr instanceof Error ? ctxErr.message : ctxErr,
        );
      }
    }

    // Crawl stats URL lists for synthesizer
    input.crawlStatsContext = buildCrawlStatsContext(pages);

    // ── Phase 2: Content + analysis in parallel ────────────────────────────
    await advanceTier(auditId, shouldFetchFirstParty ? 4 : 3);
    await updateAuditProgress(auditId, 5, "Extracting content");
    const [contentSamples, analysisResult] = await Promise.all([
      fetchKeyPageContent(pages),
      Promise.resolve(analyzeSite(pages, summary)),
    ]);

    const { findings, scoringBreakdown: rawBreakdown } = analysisResult;
    const scoringBreakdown = rawBreakdown as { critical: number; high: number; medium: number; low: number };
    console.log(`[audit-runner] ${auditId} Analysis complete: findings ready, content samples: ${contentSamples.length} pages`);

    // Enrich rawData with technical metrics for PDF rendering
    await prisma.audit.update({
      where: { id: auditId },
      data: {
        rawData: JSON.parse(JSON.stringify({
          pagesScanned: pages.length,
          summary,
          scanCompletedAt: new Date().toISOString(),
          crawlSamples: extractCrawlSamples(pages),
          technicalMetrics: {
            brokenLinksTotal: findings.brokenLinks.total,
            schemaPercent: findings.schema.percentCoverage,
            nonIndexablePages: findings.technical.nonIndexablePages,
            redirectChainsCount: findings.technical.redirectChains.length,
            missingTitles: findings.metaTitles.missing,
            missingDescriptions: findings.metaDescriptions.missing,
            missingH1: findings.headings.missingH1,
          },
          ...(input.webVitalsContext && input.webVitalsContext.length > 0 && { webVitalsSummary: input.webVitalsContext }),
          ...(rawAuthority && { authority: rawAuthority }),
        })),
      },
    });

    // ── Phase 3: Industry classification (Haiku, ~5s) ─────────────────────
    await advanceTier(auditId, shouldFetchFirstParty ? 5 : 4);
    await updateAuditProgress(auditId, 5, "Classifying industry");
    const industryContext = await classifyIndustry(input.brandName, input.website, contentSamples).catch(() => null);
    const inferredIndustry = industryContext?.industry ?? "other";
    input.inferredIndustry = inferredIndustry;
    if (industryContext) input.industryContext = industryContext;
    console.log(`[audit-runner] ${auditId} Industry classified: ${inferredIndustry} subVertical=${industryContext?.subVertical ?? "n/a"} suggestedQueries=${industryContext?.suggestedQueries.length ?? 0}`);

    // Schema Opportunity Forecast — pure computation from crawl + firecrawl + industry data
    const rawSchemaOpportunityForecast = industryContext
      ? aggregateSchemaOpportunityForecast(industryContext, pages, contentSamples)
      : null;
    if (rawSchemaOpportunityForecast) {
      input.schemaOpportunityForecastContext = rawSchemaOpportunityForecast;
      console.log(`[audit-runner] ${auditId} SchemaOpportunityForecast: coverage=${rawSchemaOpportunityForecast.currentCoveragePercent}% liftAvailable=${rawSchemaOpportunityForecast.cumulativeLiftEstimate}%`);
    }

    // ── Phase 4 (parallel for 3rd-party): SERP analysis + bilingual audit ────
    await advanceTier(auditId, shouldFetchFirstParty ? 6 : 5);

    // Bilingual audit runs in parallel with SERP — reuses already-loaded data, no external calls
    const bilingualAuditPromise = aggregateBilingualAudit(
      pages,
      contentSamples,
      input.aiAttributionContext ?? null,
      audit.brandId ?? null,
    ).catch((e) => {
      console.warn(`[audit-runner] ${auditId} bilingualAudit failed (non-fatal):`, e instanceof Error ? e.message : e);
      return null;
    });

    // Run SERP analysis for both audit types. For first-party audits a 90s
    // timeout caps exposure (the trigger route maxDuration is 800s but first-
    // party audits typically run ~490s, leaving ~310s of headroom; the 90s cap
    // ensures a slow DataForSEO call cannot eat into that budget unexpectedly).
    const serpQueries = industryContext?.suggestedQueries.length
      ? industryContext.suggestedQueries
      : brandedQueries;
    const serpFetchPromise = serpQueries.length > 0
      ? fetchSerpAnalysis(serpQueries).catch((e) => {
          console.warn(`[audit-runner] ${auditId} serpAnalysis failed (non-fatal):`, e instanceof Error ? e.message : e);
          return null;
        })
      : Promise.resolve(null);
    const rawSerpAnalysis = await (isThirdParty
      ? serpFetchPromise
      : Promise.race([
          serpFetchPromise,
          new Promise<null>((resolve) => setTimeout(() => {
            console.warn(`[audit-runner] ${auditId} serpAnalysis timed out after 90s (first-party safeguard), continuing without SERP`);
            resolve(null);
          }, 90_000)),
        ]));

    if (rawSerpAnalysis) {
      console.log(`[audit-runner] ${auditId} SERP analysis: positions=${rawSerpAnalysis.positions.length} aiOverviews=${rawSerpAnalysis.aiOverviewPresence.filter((a) => a.hasAiOverview).length} competitorDomains=${rawSerpAnalysis.competitorDomains.join(",")}`);
    }

    // Industry Benchmark Comparison — computed after SERP so AI visibility dimension is available
    const rawIndustryBenchmark = industryContext
      ? aggregateIndustryBenchmarkComparison(
          industryContext,
          rawAuthority ?? undefined,
          rawSerpAnalysis ?? undefined,
          pages.length,
          findings.schema.percentCoverage,
        )
      : null;
    if (rawIndustryBenchmark) {
      input.industryBenchmarkComparisonContext = rawIndustryBenchmark;
      console.log(`[audit-runner] ${auditId} IndustryBenchmark: industry=${rawIndustryBenchmark.industry} overallPercentile=${rawIndustryBenchmark.overallPercentile}`);
    }

    // Competitor analysis starts after SERP (depends on competitorDetails); runs in parallel with synthesis setup
    const competitorAnalysisPromise = rawSerpAnalysis && rawSerpAnalysis.competitorDomains.length > 0
      ? aggregateCompetitorAnalysis(rawSerpAnalysis, pages, contentSamples, rawAuthority ?? null).catch((e) => {
          console.warn(`[audit-runner] ${auditId} competitorAnalysis failed (non-fatal):`, e instanceof Error ? e.message : e);
          return null;
        })
      : Promise.resolve(null);

    // ── Phase 5: Synthesis ─────────────────────────────────────────────────
    await advanceTier(auditId, shouldFetchFirstParty ? 7 : 6);
    await updateAuditProgress(auditId, 6, "Generating report");

    const [rawBilingualAudit, rawCompetitorAnalysis] = await Promise.all([
      bilingualAuditPromise,
      competitorAnalysisPromise,
    ]);
    if (rawBilingualAudit) {
      input.bilingualAuditContext = rawBilingualAudit;
      console.log(`[audit-runner] ${auditId} Bilingual audit: languages=${rawBilingualAudit.detectedLanguages.map((l) => l.code).join(",")} confidence=${rawBilingualAudit.dataConfidence}`);
    }
    if (rawCompetitorAnalysis) {
      input.competitorAnalysisContext = rawCompetitorAnalysis;
      console.log(`[audit-runner] ${auditId} Competitor analysis: competitors=${rawCompetitorAnalysis.detectedCompetitors.map((c) => c.domain).join(",")} quality=${rawCompetitorAnalysis.dataQuality}`);
    }

    // Page Strategic Pass — deterministic from already-loaded data, no external calls
    const rawPageStrategicPass = aggregatePageStrategicPass(
      pages,
      contentSamples,
      input.behavioralContext ?? null,
      input.aiAttributionContext ?? null,
    );
    if (rawPageStrategicPass) {
      input.pageStrategicPassContext = rawPageStrategicPass;
      console.log(`[audit-runner] ${auditId} PageStrategicPass: pages=${rawPageStrategicPass.pages.length}`);
    }

    // Pre-synthesis context for specialized synthesizers (injected into executive summary)
    {
      // Multilingual context from already-computed bilingualAuditContext
      const bc = input.bilingualAuditContext;
      if (bc && bc.detectedLanguages.length >= 2) {
        const hrefIssue = bc.hreflangImplementation;
        const missingEquivalents = bc.contentParity.perNonPrimaryLanguage.reduce(
          (sum, l) => sum + l.pagesMissingEquivalent, 0,
        );
        const flags = [
          hrefIssue.status !== "complete",
          missingEquivalents > 0,
          hrefIssue.commonBugs.length > 0,
        ].filter(Boolean);
        if (flags.length > 0) {
          const topFinding =
            hrefIssue.status === "missing"
              ? "Hreflang not implemented on multilingual site"
              : missingEquivalents > 0
              ? `${missingEquivalents} pages missing translated equivalents`
              : hrefIssue.commonBugs[0]?.description ?? "Multilingual coverage gaps detected";
          input.multilingualContext = { findingCount: flags.length, topFinding };
        }
      }

      // Compliance context from structural findings and industry classification
      const complianceIssues: string[] = [];
      if ((findings.images.missingAlt ?? 0) > 0) {
        complianceIssues.push(`${findings.images.missingAlt} images missing alt text`);
      }
      const industryList = industryContext?.industries ?? [];
      const isMedical = industryList.some((i) => i.startsWith("healthcare"));
      if (complianceIssues.length > 0 || isMedical) {
        input.complianceContext = {
          findingCount: complianceIssues.length + (isMedical ? 1 : 0),
          topFinding: complianceIssues[0] ?? "HIPAA-relevant medical site with compliance surfaces",
          isHipaaRelevant: isMedical,
        };
      }

      // E-commerce context from schema types and URL patterns
      const allPageSchemas = (input.pageStrategicPassContext?.pages ?? [])
        .flatMap((p) => p.structuralSignals.schemaTypes);
      const isEcom =
        industryList.some((i) => i.startsWith("ecommerce")) ||
        allPageSchemas.includes("Product") ||
        findings.schema.typesDetected.includes("Product");
      if (isEcom) {
        input.ecommerceContext = {
          findingCount: 1,
          topFinding: "E-commerce product schema completeness and structured data coverage",
        };
      }

      // B2B context from industry classification and URL patterns
      const pageUrls = (input.pageStrategicPassContext?.pages ?? []).map((p) => p.url).join(" ");
      const isB2b =
        industryList.some((i) => i.startsWith("b2b")) ||
        /pricing|\/plans|\/case-stud|\/resources|\/solutions/i.test(pageUrls);
      if (isB2b) {
        input.b2bContext = {
          findingCount: 1,
          topFinding: "B2B funnel-stage content coverage and sales-cycle discoverability",
        };
      }
    }

    // AI Engine Personality Analysis — always runs (static profiles + optional 1st-party overlay)
    const rawAiEnginePersonalityAnalysis = aggregateAiEnginePersonalityAnalysis(
      input.aiAttributionContext ?? null,
    );
    input.aiEnginePersonalityAnalysisContext = rawAiEnginePersonalityAnalysis;
    console.log(`[audit-runner] ${auditId} AiEnginePersonality: engines=${rawAiEnginePersonalityAnalysis.engines.length} topEngine=${rawAiEnginePersonalityAnalysis.topEngineId ?? "n/a"}`);

    // E-E-A-T Audit — always runs for both audit types
    const rawEeatAudit = aggregateEeatAudit(
      industryContext ?? null,
      pages,
      contentSamples,
      rawAuthority ?? null,
    );
    input.eeatAuditContext = rawEeatAudit;
    console.log(`[audit-runner] ${auditId} EeatAudit: compositeScore=${rawEeatAudit.compositeScore} weight=${rawEeatAudit.industryWeight} gaps=${rawEeatAudit.topGaps.length}`);

    // Voice Search Eligibility -- deterministic, no external calls, skipped when no pages/samples
    if (pages.length > 0 && contentSamples.length > 0) {
      const rawVoiceSearchEligibility = runVoiceSearchEligibility(pages, contentSamples, industryContext ?? null);
      input.voiceSearchEligibilityContext = rawVoiceSearchEligibility;
      console.log(`[audit-runner] ${auditId} VoiceSearchEligibility: compositeScore=${rawVoiceSearchEligibility.compositeScore} industryRelevance=${rawVoiceSearchEligibility.industryRelevance} gaps=${rawVoiceSearchEligibility.topGaps.length}`);
    }

    // Brand Authority Score -- both audit types; async; depends on eeat + schema + visual data already set on input
    const rawBrandAuthority = await aggregateBrandAuthorityScore({
      brandName: input.brandName,
      brandDomain: input.website,
      vertical: industryContext?.industry ?? "general",
      existingAuditContext: {
        eeatAudit: null,
        liveVisualAnnotations: input.liveVisualAnnotationsContext ?? null,
        schemaOpportunityForecast: input.schemaOpportunityForecastContext ?? null,
      },
    }).catch((e) => {
      console.warn(`[audit-runner] ${auditId} BrandAuthority failed (non-fatal):`, e instanceof Error ? e.message : e);
      return null;
    });
    if (rawBrandAuthority) {
      input.brandAuthorityContext = rawBrandAuthority;
      console.log(`[audit-runner] ${auditId} BrandAuthority: overall=${rawBrandAuthority.overall} tier=${rawBrandAuthority.tier} signals=${rawBrandAuthority.signals.length}`);
    }

    // Mobile-First Audit + Page Speed Insights -- both audit types; parallel; 60s/120s timeouts
    const [rawMobileFirstSettled, rawPageSpeedSettled] = await Promise.allSettled([
      aggregateMobileFirstAudit({
        brandDomain: input.website,
        vertical: industryContext?.industry ?? "general",
        liveVisualAnnotations: input.liveVisualAnnotationsContext ?? null,
      }).catch((e) => {
        console.warn(`[audit-runner] ${auditId} MobileFirstAudit failed (non-fatal):`, e instanceof Error ? e.message : e);
        return null;
      }),
      aggregatePageSpeedInsights({
        brandDomain: input.website,
        vertical: industryContext?.industry ?? "general",
        liveVisualAnnotations: input.liveVisualAnnotationsContext ?? null,
      }).catch((e) => {
        console.warn(`[audit-runner] ${auditId} PageSpeedInsights failed (non-fatal):`, e instanceof Error ? e.message : e);
        return null;
      }),
    ]);

    const rawMobileFirstAudit = rawMobileFirstSettled.status === "fulfilled" ? rawMobileFirstSettled.value : null;
    const rawPageSpeedInsights = rawPageSpeedSettled.status === "fulfilled" ? rawPageSpeedSettled.value : null;

    if (rawMobileFirstAudit?.hasAudit) {
      input.mobileFirstContext = rawMobileFirstAudit;
      console.log(`[audit-runner] ${auditId} MobileFirstAudit: pages=${rawMobileFirstAudit.pagesEvaluated} score=${rawMobileFirstAudit.overallMobileScore} tier=${rawMobileFirstAudit.scoreTier}`);
    }
    if (rawPageSpeedInsights?.hasInsights) {
      input.pageSpeedContext = rawPageSpeedInsights;
      console.log(`[audit-runner] ${auditId} PageSpeedInsights: pages=${rawPageSpeedInsights.pagesAnalyzed} tier=${rawPageSpeedInsights.overallScoreTier} trafficLoss=${rawPageSpeedInsights.estimatedMonthlyTrafficLoss}`);
    }

    // AI Snippet Eligibility — both audit types; needs SERP data
    const rawAiSnippetEligibility = aggregateAiSnippetEligibility(
      rawSerpAnalysis ?? null,
      pages,
      contentSamples,
      input.isLocal,
    );
    if (rawAiSnippetEligibility) {
      input.aiSnippetEligibilityContext = rawAiSnippetEligibility;
      console.log(`[audit-runner] ${auditId} AiSnippetEligibility: queries=${rawAiSnippetEligibility.aggregateStats.totalQueriesAnalyzed} eligibilityRate=${rawAiSnippetEligibility.aggregateStats.eligibilityRatePercent}%`);
    }

    // Snippet Capture Strategy -- both audit types; requires snippet eligibility data to already be set
    const rawSnippetCaptureStrategy = await aggregateSnippetCaptureStrategy({
      aiSnippetEligibility: rawAiSnippetEligibility ?? null,
      brandName: input.brandName,
      brandDomain: input.website,
      vertical: industryContext?.industry ?? "general",
    }).catch((e) => {
      console.warn(`[audit-runner] ${auditId} SnippetCaptureStrategy failed (non-fatal):`, e instanceof Error ? e.message : e);
      return null;
    });
    if (rawSnippetCaptureStrategy?.hasStrategy) {
      input.snippetCaptureContext = rawSnippetCaptureStrategy;
      console.log(`[audit-runner] ${auditId} SnippetCaptureStrategy: reformulations=${rawSnippetCaptureStrategy.totalReformulationsGenerated} trafficGain=${rawSnippetCaptureStrategy.totalEstimatedMonthlyTrafficGain} revenueGain=${rawSnippetCaptureStrategy.totalEstimatedMonthlyRevenueGain}`);
    }

    // AI Source Attribution Comparison — 1st-party only; compares public signal predictions to Site Tag actuals
    const rawAiSourceAttribution = shouldFetchFirstParty
      ? aggregateAiSourceAttributionComparison(
          rawSerpAnalysis ?? null,
          industryContext ?? null,
          rawSignalCenter ?? null,
          input.aiAttributionContext ?? null,
          input.behavioralContext ?? null,
        )
      : null;
    if (rawAiSourceAttribution) {
      input.aiSourceAttributionComparisonContext = rawAiSourceAttribution;
      console.log(`[audit-runner] ${auditId} AiSourceAttribution: dimensions=${rawAiSourceAttribution.predictionVsActual.length} gapMagnitude=${rawAiSourceAttribution.overallGapMagnitude}`);
    }

    // Multi-Touch AI Attribution Path — 1st-party only; reconstructs visitor journeys from Site Tag conversion data
    const rawMultiTouchAiPath = shouldFetchFirstParty
      ? await aggregateMultiTouchAiPath(audit.brandId!, input.aiAttributionContext ?? null, rawSignalCenter ?? null)
      : null;
    if (rawMultiTouchAiPath) {
      input.multiTouchAiPathContext = rawMultiTouchAiPath;
      console.log(`[audit-runner] ${auditId} MultiTouchAiPath: paths=${rawMultiTouchAiPath.topConversionPaths.length} crossDevice=${rawMultiTouchAiPath.crossDeviceJourneys.detected}`);
    }

    // Revenue Attribution — 1st-party only; requires Site Tag conversion data with revenue values
    const rawRevenueAttribution = shouldFetchFirstParty && audit.brandId
      ? await aggregateRevenueAttribution(audit.brandId, 30).catch((e) => {
          console.warn(`[audit-runner] ${auditId} RevenueAttribution failed (non-fatal):`, e instanceof Error ? e.message : e);
          return null;
        })
      : null;
    if (rawRevenueAttribution) {
      input.revenueAttributionContext = rawRevenueAttribution;
      console.log(`[audit-runner] ${auditId} RevenueAttribution: hasRevenue=${rawRevenueAttribution.hasRevenueTracking} channels=${rawRevenueAttribution.revenueByChannel.length} confidence=${rawRevenueAttribution.dataConfidence}`);
    }

    // Predictive Conversion Scoring — 1st-party only; requires scored sessions
    const rawPredictiveConversionScoring = shouldFetchFirstParty && audit.brandId
      ? await aggregatePredictiveConversionScoring(audit.brandId, 30).catch((e) => {
          console.warn(`[audit-runner] ${auditId} PredictiveConversionScoring failed (non-fatal):`, e instanceof Error ? e.message : e);
          return null;
        })
      : null;
    if (rawPredictiveConversionScoring) {
      input.predictiveConversionScoringContext = rawPredictiveConversionScoring;
      console.log(`[audit-runner] ${auditId} PredictiveConversionScoring: sessions=${rawPredictiveConversionScoring.totalScoredSessions} avgScore=${rawPredictiveConversionScoring.avgScore} live=${rawPredictiveConversionScoring.liveSessionsSnapshot.activeLast30Min}`);
    }

    // Conversion Heat Map — 1st-party only; requires HeatmapEvent click data
    const rawConversionHeatMap = shouldFetchFirstParty && audit.brandId
      ? await aggregateConversionHeatMap(audit.brandId, 30).catch((e) => {
          console.warn(`[audit-runner] ${auditId} ConversionHeatMap failed (non-fatal):`, e instanceof Error ? e.message : e);
          return null;
        })
      : null;
    if (rawConversionHeatMap) {
      input.conversionHeatMapContext = rawConversionHeatMap;
      console.log(`[audit-runner] ${auditId} ConversionHeatMap: hasClickData=${rawConversionHeatMap.hasClickData} pages=${rawConversionHeatMap.perPage.length} confidence=${rawConversionHeatMap.dataConfidence}`);
    }

    // AI Search Verification — both 1st-party and 3rd-party; requires industryContext.suggestedQueries
    const rawAiSearchVerification = industryContext?.suggestedQueries?.length
      ? await aggregateAiSearchVerification({
          brandName: input.brandName,
          brandUrl: input.website,
          brandLocation: input.locations?.[0],
          industryContext,
          competitors: input.competitors,
        }).catch((e) => {
          console.warn(`[audit-runner] ${auditId} AiSearchVerification failed (non-fatal):`, e instanceof Error ? e.message : e);
          return null;
        })
      : null;
    if (rawAiSearchVerification) {
      input.aiSearchVerificationContext = rawAiSearchVerification;
      console.log(`[audit-runner] ${auditId} AiSearchVerification: queries=${rawAiSearchVerification.queriesChecked} mentionRate=${Math.round(rawAiSearchVerification.brandVisibility.overallMentionRate * 100)}% degraded=${rawAiSearchVerification.degraded}`);
    }

    console.log(`[audit-runner] ${auditId} Running synthesis: industry=${inferredIndustry} contentPages=${contentSamples.length} enrichment=${JSON.stringify({ hasAuthority: !!rawAuthority, hasSerpAnalysis: !!rawSerpAnalysis })}`);

    const enrichment: AuditEnrichment | undefined = isThirdParty
      ? {
          ...(rawAuthority && { authority: rawAuthority }),
          ...(rawSerpAnalysis && rawSerpAnalysis.positions.length > 0 && { serp: rawSerpAnalysis.positions }),
          ...(rawSerpAnalysis && { serpAnalysis: rawSerpAnalysis }),
          ...(industryContext && { industryContext }),
        }
      : undefined;

    // Compute dimension scores deterministically before synthesis so the
    // headline (healthScore) is the weighted sum of those exact dimensions.
    let dimensionScores: DimensionScores;
    if (isThirdParty) {
      const serpTotal = rawSerpAnalysis?.aiOverviewPresence.length ?? 0;
      const serpWithAi = rawSerpAnalysis?.aiOverviewPresence.filter((a) => a.hasAiOverview).length ?? 0;
      const serpWithBrand = rawSerpAnalysis?.aiOverviewPresence.filter((a) => a.brandMentioned).length ?? 0;

      const thirdPartyInput: ThirdPartyDimensionInput = {
        aiVisibility: {
          aiOverviewCoveragePercent: serpTotal > 0 ? Math.round((serpWithAi / serpTotal) * 100) : 0,
          aiOverviewBrandCitationRate: serpWithAi > 0 ? Math.round((serpWithBrand / serpWithAi) * 100) : 0,
          queriesWithAiOverview: serpWithAi,
          totalQueriesChecked: serpTotal,
          schemaTypesDetected: findings.schema.typesDetected,
          schemaPercentCoverage: findings.schema.percentCoverage,
          schemaPriorities: industryContext?.schemaPriorities ?? [],
          referringDomains: rawAuthority?.referringDomains ?? null,
          totalBacklinks: rawAuthority?.totalBacklinks ?? null,
        },
        conversion: {
          ctaPageCount: contentSamples.filter((s) => s.hasCTA).length,
          phonePageCount: contentSamples.filter((s) => s.hasPhoneClick).length,
          totalSamples: contentSamples.length,
          allCtaExamples: contentSamples.flatMap((s) => s.ctaExamples),
          conversionPathExpectation: industryContext?.conversionPathExpectation ?? [],
        },
        engagement: {
          avgWordCount: findings.content.avgWordCount,
          totalPages: findings.siteSize.totalPages,
          avgInternalLinks: pages.length > 0
            ? Math.round(pages.reduce((sum, p) => sum + p.internalLinks.length, 0) / pages.length)
            : 0,
        },
      };
      dimensionScores = computeThirdPartyDimensionScores(findings, thirdPartyInput);
    } else {
      dimensionScores = computeDimensionScores(
        findings,
        input.aiAttributionContext,
        input.behavioralContext,
        input.webVitalsContext,
      );
    }
    const computedHealthScore = computeHealthScore(dimensionScores);
    console.log(`[audit-runner] ${auditId} Dimension scores: tech=${dimensionScores.technicalSeo} content=${dimensionScores.contentMetadata} aiVis=${dimensionScores.aiVisibility} conv=${dimensionScores.conversionOptimization} eng=${dimensionScores.userEngagement} → healthScore=${computedHealthScore}`);

    const seoSection = await synthesizeSeoSection(
      input,
      findings,
      computedHealthScore,
      scoringBreakdown,
      contentSamples,
      enrichment,
    );
    console.log(`[audit-runner] ${auditId} Synthesis complete: findings=${seoSection.prioritizedFindings?.length ?? 0} themes=${seoSection.summaryThemes?.length ?? 0}`);

    // Real-User Technical Findings — post-synthesis merge so it never blocks the LLM call.
    // Returns [] when Site Tag is not installed or has no qualifying error patterns.
    if (shouldFetchFirstParty && audit.brandId) {
      const techFindings = await runRealUserTechnicalSynthesizer(audit.brandId).catch((e) => {
        console.warn(`[audit-runner] ${auditId} Real-user tech synthesizer failed (non-fatal):`, e instanceof Error ? e.message : e);
        return [];
      });
      if (techFindings.length > 0) {
        seoSection.prioritizedFindings = [...(seoSection.prioritizedFindings ?? []), ...techFindings];
        console.log(`[audit-runner] ${auditId} Real-user tech findings merged: ${techFindings.length} patterns`);
      }
    }

    // Specialized post-synthesis synthesizers — all run in parallel, all fail open.
    // Each returns [] for sites where the category does not apply.
    const [multilingualFindings, complianceFindings, ecommerceFindings, b2bFindings] =
      await Promise.all([
        runMultilingualSynthesizer(input).catch((e) => {
          console.warn(`[audit-runner] ${auditId} Multilingual synthesizer failed (non-fatal):`, e instanceof Error ? e.message : e);
          return [];
        }),
        runComplianceSynthesizer(input, findings).catch((e) => {
          console.warn(`[audit-runner] ${auditId} Compliance synthesizer failed (non-fatal):`, e instanceof Error ? e.message : e);
          return [];
        }),
        runEcommerceSynthesizer(input, findings).catch((e) => {
          console.warn(`[audit-runner] ${auditId} Ecommerce synthesizer failed (non-fatal):`, e instanceof Error ? e.message : e);
          return [];
        }),
        runB2bSynthesizer(input).catch((e) => {
          console.warn(`[audit-runner] ${auditId} B2B synthesizer failed (non-fatal):`, e instanceof Error ? e.message : e);
          return [];
        }),
      ]);

    const specializedFindings = [
      ...multilingualFindings,
      ...complianceFindings,
      ...ecommerceFindings,
      ...b2bFindings,
    ];
    if (specializedFindings.length > 0) {
      seoSection.prioritizedFindings = [...(seoSection.prioritizedFindings ?? []), ...specializedFindings];
      console.log(
        `[audit-runner] ${auditId} Specialized findings merged: ` +
        `multilingual=${multilingualFindings.length} compliance=${complianceFindings.length} ` +
        `ecommerce=${ecommerceFindings.length} b2b=${b2bFindings.length}`,
      );
    }

    // Assemble Strategic Roadmap from synthesizer-assigned phase/effort/lift per action
    const rawStrategicRoadmap = buildStrategicRoadmap(seoSection.priorityActions, computedHealthScore, !shouldFetchFirstParty);

    // Behavioral Action Bridge — post-synthesis; needs synthesizer's priorityActions to match and enhance
    const rawBehavioralActionBridge = shouldFetchFirstParty && rawSignalCenter && rawSignalCenter.dataConfidence !== "low"
      ? aggregateBehavioralActionBridge(
          seoSection.priorityActions,
          seoSection.prioritizedFindings,
          rawSignalCenter,
          rawPageStrategicPass ?? null,
          input.aiAttributionContext ?? null,
        )
      : null;
    if (rawBehavioralActionBridge) {
      console.log(`[audit-runner] ${auditId} BehavioralActionBridge: enhanced=${rawBehavioralActionBridge.enhancedActions.length} newFindings=${rawBehavioralActionBridge.newBehavioralFindings.length} patterns=${rawBehavioralActionBridge.topBehavioralPatterns.length}`);
    }

    // Lost Opportunity Calculator — post-synthesis; both audit types; needs priorityActions + industryContext
    const rawLostOpportunity = industryContext
      ? await aggregateLostOpportunity(
          industryContext,
          seoSection.priorityActions,
          seoSection.prioritizedFindings,
          rawSerpAnalysis ?? null,
          rawSchemaOpportunityForecast ?? null,
          rawAiSnippetEligibility ?? null,
        ).catch((e) => {
          console.warn(`[audit-runner] ${auditId} LostOpportunity failed (non-fatal):`, e instanceof Error ? e.message : e);
          return null;
        })
      : null;
    if (rawLostOpportunity) {
      input.lostOpportunityCalculationContext = rawLostOpportunity;
      console.log(`[audit-runner] ${auditId} LostOpportunity: total=$${rawLostOpportunity.aggregateOpportunity.totalMonthlyOpportunityUsd}/mo findings=${rawLostOpportunity.perFindingEstimates.length} confidence=${rawLostOpportunity.confidence}`);
    }

    // Phase 12: Visual Annotation — both audit types; run in parallel; 180s total timeout
    const visualAnnotationTimeout = 180_000;
    const visualAnnotationsPromise = aggregateLiveVisualAnnotations(
      input.website,
      { brandName: input.brandName, hasSchemaDetected: !!rawSchemaOpportunityForecast, hasLocalSchema: input.isLocal },
      industryContext?.industry ?? "other",
      pages.filter((p) => p.isIndexable && p.url !== input.website && (
        /\/(services?|treatments?|solutions?|products?)/i.test(p.url)
      )).slice(0, 2).map((p) => p.url),
    ).catch((e) => {
      console.warn(`[audit-runner] ${auditId} LiveVisualAnnotations failed (non-fatal):`, e instanceof Error ? e.message : e);
      return null;
    });

    const competitorVisualPromise = rawCompetitorAnalysis && rawCompetitorAnalysis.detectedCompetitors.length > 0
      ? aggregateCompetitorVisualComparison(
          input.website,
          input.brandName,
          rawCompetitorAnalysis.detectedCompetitors.map((c) => ({
            domain: c.domain,
            avgRankPosition: c.avgRankPosition,
          })),
        ).catch((e) => {
          console.warn(`[audit-runner] ${auditId} CompetitorVisualComparison failed (non-fatal):`, e instanceof Error ? e.message : e);
          return null;
        })
      : Promise.resolve(null);

    const [rawLiveVisualAnnotations, rawCompetitorVisualComparison] = await Promise.race([
      Promise.all([visualAnnotationsPromise, competitorVisualPromise]),
      new Promise<[null, null]>((resolve) =>
        setTimeout(() => resolve([null, null]), visualAnnotationTimeout),
      ),
    ]);

    if (rawLiveVisualAnnotations) {
      input.liveVisualAnnotationsContext = rawLiveVisualAnnotations;
      console.log(`[audit-runner] ${auditId} LiveVisualAnnotations: pages=${rawLiveVisualAnnotations.perPageAnnotations.length} annotations=${rawLiveVisualAnnotations.aggregateInsights.totalAnnotations}`);
    }
    if (rawCompetitorVisualComparison) {
      input.competitorVisualComparisonContext = rawCompetitorVisualComparison;
      console.log(`[audit-runner] ${auditId} CompetitorVisualComparison: comparisons=${rawCompetitorVisualComparison.comparisons.length}`);
    }

    // ── Step 6: Assembly ───────────────────────────────────────────────────
    await updateAuditProgress(auditId, 6, "Finalizing recommendations");
    if (!shouldFetchFirstParty) await advanceTier(auditId, 7);

    const siteTagContext = rawFirstParty
      ? computeSiteTagContext(rawFirstParty.siteTagFirstEventAt, rawFirstParty.siteTagLastEventAt)
      : undefined;

    // ── Competitive + backlink structured data ─────────────────────────────
    const backlinkProfile = rawAuthority
      ? {
          referringDomains: rawAuthority.referringDomains,
          totalBacklinks: rawAuthority.totalBacklinks,
          domainRank: rawAuthority.domainRank,
          spamScore: rawAuthority.spamScore ?? null,
        }
      : undefined;

    const competitiveLandscape = rawSerpAnalysis
      ? (() => {
          const total = rawSerpAnalysis.aiOverviewPresence.length;
          const withAi = rawSerpAnalysis.aiOverviewPresence.filter((a) => a.hasAiOverview).length;
          const withBrand = rawSerpAnalysis.aiOverviewPresence.filter((a) => a.brandMentioned).length;
          return {
            topCompetitors: rawSerpAnalysis.competitorDomains,
            aiOverviewCoveragePercent: total > 0 ? Math.round((withAi / total) * 100) : 0,
            aiOverviewBrandCitationRate: withAi > 0 ? Math.round((withBrand / withAi) * 100) : 0,
          };
        })()
      : undefined;

    const publicAiVisibility = rawSerpAnalysis
      ? {
          queriesWithAiOverview: rawSerpAnalysis.aiOverviewPresence.filter((a) => a.hasAiOverview).length,
          totalQueriesChecked: rawSerpAnalysis.aiOverviewPresence.length,
          brandMentionedInAiOverview: rawSerpAnalysis.aiOverviewPresence.some((a) => a.brandMentioned),
          aiOverviewDetails: rawSerpAnalysis.aiOverviewPresence.map((a) => ({
            query: a.keyword,
            brandMentioned: a.brandMentioned,
            snippet: a.snippet,
          })),
        }
      : undefined;

    // ── Organic search summary (GSC — 1st-party only) ─────────────────────
    const organicSearchSummary = rawGscSummary
      ? {
          totalClicks30d: rawGscSummary.totalClicks,
          totalImpressions30d: rawGscSummary.totalImpressions,
          avgCtr: rawGscSummary.avgCtr,
          avgPosition: rawGscSummary.avgPosition,
          topQueries: rawGscSummary.topQueries.slice(0, 10).map((q) => ({
            query: q.query,
            clicks: q.clicks,
            impressions: q.impressions,
            position: q.position,
          })),
          topPages: rawGscSummary.topPages.slice(0, 10).map((p) => ({
            page: p.page,
            clicks: p.clicks,
            impressions: p.impressions,
            position: p.position,
          })),
        }
      : undefined;

    // ── AI conversion funnel (1st-party only) ─────────────────────────────
    const aiConversionAttribution = input.aiAttributionContext
      ? (() => {
          const ctx = input.aiAttributionContext!;
          const rate = ctx.totalAiReferrals > 0
            ? Math.round((ctx.totalConversions30d / ctx.totalAiReferrals) * 1000) / 10
            : 0;
          const topPages = ctx.topPages
            .filter((p) => p.conversions > 0)
            .sort((a, b) => b.conversions - a.conversions)
            .slice(0, 5)
            .map((p) => ({ url: p.url, aiVisits: p.aiVisits, conversions: p.conversions }));
          return {
            totalAiReferrals30d: ctx.totalAiReferrals,
            totalAiConversions30d: ctx.totalConversions30d,
            aiConversionRate: rate,
            topConvertingPages: topPages,
            topConvertingEngines: ctx.topEngines.slice(0, 5),
          };
        })()
      : undefined;

    // ── Cost logging (estimates — for monitoring, not billing) ────────────
    const costCeiling = shouldFetchFirstParty ? 4.00 : 2.00;
    const costRecord: AuditCostRecord = {
      crawl: pages.length * COST_PER_CRAWL_PAGE,
      firecrawl: contentSamples.length * COST_PER_FIRECRAWL_PAGE,
      dfsBacklinks: rawAuthority ? COST_PER_DFS_BACKLINK : 0,
      dfsSerp: (rawSerpAnalysis?.positions.length ?? 0) * COST_PER_SERP_QUERY,
      haiku: (industryContext ? 1 : 0) * COST_PER_HAIKU_CALL + COST_PER_HAIKU_CALL, // classifier + structured
      synth: COST_PER_SONNET_CALL,
      total: 0,
    };
    costRecord.total = Object.values(costRecord).reduce((a, b) => a + b, 0) - costRecord.total;
    logAuditCost(auditId, shouldFetchFirstParty ? "1st" : "3rd", audit.brandId ?? null, costRecord, costCeiling);

    const enrichedSeoSection = {
      ...seoSection,
      healthScore: computedHealthScore,
      dimensionScores,
      ...(siteTagContext && { siteTagContext }),
      ...(industryContext && { industryContext }),
      ...(backlinkProfile && { backlinkProfile }),
      ...(competitiveLandscape && { competitiveLandscape }),
      ...(publicAiVisibility && { publicAiVisibility }),
      ...(organicSearchSummary && { organicSearchSummary }),
      ...(aiConversionAttribution && { aiConversionAttribution }),
      ...(shouldFetchFirstParty && { signalCenter: rawSignalCenter ?? null }),
      ...(rawBilingualAudit && { bilingualAudit: rawBilingualAudit }),
      ...(rawCompetitorAnalysis && { competitorAnalysis: rawCompetitorAnalysis }),
      ...(rawSchemaOpportunityForecast && { schemaOpportunityForecast: rawSchemaOpportunityForecast }),
      ...(rawIndustryBenchmark && { industryBenchmarkComparison: rawIndustryBenchmark }),
      ...(rawStrategicRoadmap && { strategicRoadmap: rawStrategicRoadmap }),
      ...(rawPageStrategicPass && { pageStrategicPass: rawPageStrategicPass }),
      aiEnginePersonalityAnalysis: rawAiEnginePersonalityAnalysis,
      ...(rawAiSourceAttribution && { aiSourceAttributionComparison: rawAiSourceAttribution }),
      ...(rawBehavioralActionBridge && { behavioralActionBridge: rawBehavioralActionBridge }),
      eeatAudit: rawEeatAudit,
      ...(input.voiceSearchEligibilityContext && { voiceSearchEligibility: input.voiceSearchEligibilityContext }),
      ...(rawAiSnippetEligibility && { aiSnippetEligibility: rawAiSnippetEligibility }),
      ...(rawSnippetCaptureStrategy?.hasStrategy && { snippetCaptureStrategy: rawSnippetCaptureStrategy }),
      ...(rawMultiTouchAiPath && { multiTouchAiPath: rawMultiTouchAiPath }),
      ...(rawRevenueAttribution && { revenueAttribution: rawRevenueAttribution }),
      ...(rawPredictiveConversionScoring && { predictiveConversionScoring: rawPredictiveConversionScoring }),
      ...(rawConversionHeatMap && { conversionHeatMap: rawConversionHeatMap }),
      ...(rawAiSearchVerification && { aiSearchVerification: rawAiSearchVerification }),
      ...(rawLostOpportunity && { lostOpportunityCalculation: rawLostOpportunity }),
      ...(rawLiveVisualAnnotations && { liveVisualAnnotations: rawLiveVisualAnnotations }),
      ...(rawCompetitorVisualComparison && { competitorVisualComparison: rawCompetitorVisualComparison }),
      ...(rawBrandAuthority && { brandAuthorityScore: rawBrandAuthority }),
      ...(rawMobileFirstAudit?.hasAudit && { mobileFirstAudit: rawMobileFirstAudit }),
      ...(rawPageSpeedInsights?.hasInsights && { pageSpeedInsights: rawPageSpeedInsights }),
      // Engineering-only metadata — vendor names permitted here; NEVER render in UI/PDF
      internal: {
        auditType: shouldFetchFirstParty ? ("1st" as const) : ("3rd" as const),
        dataSourcesUsed: {
          crawl: true,
          content: contentSamples.length > 0,
          authority: !!rawAuthority,
          serp: !!rawSerpAnalysis,
          framework: !!industryContext,
        },
        costLog: {
          crawl: costRecord.crawl,
          firecrawl: costRecord.firecrawl,
          dfsBacklinks: costRecord.dfsBacklinks,
          dfsSerp: costRecord.dfsSerp,
          haiku: costRecord.haiku,
          synth: costRecord.synth,
          total: costRecord.total,
        },
      },
    };

    await prisma.audit.update({
      where: { id: auditId },
      data: { seoSection: enrichedSeoSection as object },
    });

    await updateAuditProgress(auditId, 7, "Complete");
    // Invalidate analytics cache so the next poll reflects fresh audit data.
    if (audit.brandId) {
      await Promise.all([
        deleteCachedPrefix(`traffic-summary:${audit.brandId}:`),
        deleteCachedPrefix(`conversion-summary:${audit.brandId}:`),
        deleteCached(`audit-latest:${audit.brandId}`),
      ]).catch(() => {});
    }
    await markCompleted(auditId);
    console.log(`[audit-runner] ${auditId} Completed successfully`);
  } catch (err) {
    let errorReason: string | undefined;
    let userFacingMessage: string | undefined;
    let errorDetail: string | undefined;

    if (err instanceof SynthesizerTimeoutError) {
      errorReason = "synthesizer_timeout";
      userFacingMessage = "Our analysis engine took too long to respond. This is usually transient — retry the audit.";
      errorDetail = err.underlyingError instanceof Error ? err.underlyingError.message : String(err.underlyingError);
      console.error(`[audit-runner] synthesis failed audit=${auditId} reason=synthesizer_timeout attempts=${err.attempts} detail=${errorDetail}`);
    } else if (err instanceof SynthesizerError) {
      errorReason = "synthesizer_error";
      userFacingMessage = "Our analysis engine encountered an error. Click Retry to try again.";
      errorDetail = err.underlyingError instanceof Error ? err.underlyingError.message : String(err.underlyingError);
      console.error(`[audit-runner] synthesis failed audit=${auditId} reason=synthesizer_error detail=${errorDetail}`);
    } else {
      errorReason = (err as { errorReason?: string }).errorReason;
      userFacingMessage = (err as { userFacingMessage?: string }).userFacingMessage;
      errorDetail = err instanceof Error ? err.message.slice(0, 500) : String(err);
      // Log full stack for TypeError so downstream .replace()-on-undefined bugs are diagnosable
      if (err instanceof TypeError) {
        console.error(`[audit-runner] ${auditId} TypeError stack:`, (err as Error).stack);
        errorReason = errorReason ?? "code_error";
      }
      console.error(`[audit-runner] ${auditId} Failed:`, errorDetail);
    }

    await markFailed(auditId, err, userFacingMessage, errorReason, errorDetail?.slice(0, 500)).catch((e) =>
      console.error(`[audit-runner] Failed to write failure state for ${auditId}:`, e),
    );
  }
}
