import { prisma } from "@/lib/prisma";
import { getAiVisibility } from "./ai-visibility";
import { getCompetitors } from "./competitors";

/**
 * A unified recommendation format that Recommendations and
 * Execution Hub can consume from any audit source.
 */
export interface AuditRecommendation {
  id: string;
  source: "technical_audit" | "dead_pages" | "performance_audit" | "ai_visibility" | "competitors" | "site_tag";
  severity: "critical" | "high" | "medium" | "low";
  title: string;
  detail: string;
  suggestedFix: string;
  affectedPages: string[];
  affectedCount: number;
  effort: "low" | "medium" | "high";
  impact: "low" | "medium" | "high";
  category?: "technical" | "errors" | "ux" | "conversion" | "performance" | "content" | "competitive";
  /** Specific metric values powering this recommendation (e.g., "23 visits", "8 errors"). */
  metric?: string;
  platform?: string;
  competitor?: string;
}

/**
 * Get all recommendation-ready items for a brand across all audit sources.
 * Returns a single sorted list, highest priority first.
 */
export async function getAuditRecommendations(brandId: string): Promise<AuditRecommendation[]> {
  const [techRecs, deadRecs, perfRecs, aiRecs, compRecs, siteTagRecs] = await Promise.all([
    getTechnicalAuditRecs(brandId),
    getDeadPageRecs(brandId),
    getPerformanceRecs(brandId),
    getAiVisibilityRecs(brandId),
    getCompetitorRecs(brandId),
    getSiteTagRecs(brandId),
  ]);

  // ── Dead 404 cross-reference ────────────────────────────────────
  // If a Site Tag 404 rec matches an existing DeadPage record, boost
  // its priority and add a "confirmed by real users" note. If a Site
  // Tag 404 has no matching DeadPage, it was caught by real users but
  // missed by the crawler — the detail already mentions this.
  try {
    const deadPages = await prisma.deadPage.findMany({
      where: { brandId, status: "open" },
      select: { deadUrl: true },
    });
    const deadUrlSet = new Set(deadPages.map((d) => d.deadUrl));

    for (const rec of siteTagRecs) {
      if (!rec.id.startsWith("st-404-")) continue;
      const url = rec.affectedPages[0];
      if (url && deadUrlSet.has(url)) {
        // Confirmed by both crawler AND real users — max priority
        if (rec.severity !== "critical") rec.severity = "critical";
        rec.impact = "high";
        rec.detail = `[Confirmed by crawler + real users] ${rec.detail}`;
      }
    }
  } catch { /* cross-reference is best-effort */ }

  // Combine and sort by severity, then by impact
  const all = [...techRecs, ...deadRecs, ...perfRecs, ...aiRecs, ...compRecs, ...siteTagRecs];
  const order: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
  const impactOrder: Record<string, number> = { high: 0, medium: 1, low: 2 };
  all.sort((a, b) =>
    (order[a.severity] ?? 4) - (order[b.severity] ?? 4) ||
    (impactOrder[a.impact] ?? 3) - (impactOrder[b.impact] ?? 3),
  );

  return all;
}

// ─── Technical Audit → Recommendations ───────────────────────────────────────

async function getTechnicalAuditRecs(brandId: string): Promise<AuditRecommendation[]> {
  const audit = await prisma.technicalAudit.findFirst({
    where: { brandId, status: "completed" },
    orderBy: { createdAt: "desc" },
    include: {
      issues: {
        where: { status: "open" },
        orderBy: [{ severity: "asc" }, { affected: "desc" }],
      },
    },
  });

  if (!audit) return [];

  return audit.issues.map((issue) => ({
    id: `tech_${issue.id}`,
    source: "technical_audit" as const,
    severity: issue.severity as AuditRecommendation["severity"],
    title: issue.title,
    detail: issue.detail || `${issue.affected} page(s) affected`,
    suggestedFix: getSuggestedFix(issue.category, issue.title),
    affectedPages: issue.pageUrl ? [issue.pageUrl] : [],
    affectedCount: issue.affected,
    effort: getEffort(issue.category),
    impact: getImpact(issue.severity),
  }));
}

// ─── Dead Pages → Recommendations ────────────────────────────────────────────

async function getDeadPageRecs(brandId: string): Promise<AuditRecommendation[]> {
  const deadPages = await prisma.deadPage.findMany({
    where: { brandId, status: "open" },
    include: { sources: true },
    orderBy: { priority: "asc" },
  });

  return deadPages.map((dp) => ({
    id: `dead_${dp.id}`,
    source: "dead_pages" as const,
    severity: dp.priority === "high" ? "high" as const : dp.priority === "medium" ? "medium" as const : "low" as const,
    title: `Fix broken page: ${dp.deadUrl}`,
    detail: `${dp.sources.length} page(s) link to this dead URL`,
    suggestedFix: dp.suggestedFix || `Redirect ${dp.deadUrl} to a relevant live page`,
    affectedPages: dp.sources.map((s) => s.sourceUrl),
    affectedCount: dp.sources.length,
    effort: "low" as const,
    impact: dp.sources.length >= 3 ? "high" as const : dp.sources.length >= 2 ? "medium" as const : "low" as const,
  }));
}

// ─── Performance Audit → Recommendations ─────────────────────────────────────

async function getPerformanceRecs(brandId: string): Promise<AuditRecommendation[]> {
  const audit = await prisma.performanceAudit.findFirst({
    where: { brandId, device: "mobile", status: "completed" },
    orderBy: { createdAt: "desc" },
    include: { pages: { orderBy: { performance: "asc" } } },
  });

  if (!audit) return [];

  const recs: AuditRecommendation[] = [];

  // Find pages failing Core Web Vitals
  const slowLcp = audit.pages.filter((p) => p.lcpMs > 2500);
  const highCls = audit.pages.filter((p) => p.clsVal > 0.1);
  const slowInp = audit.pages.filter((p) => p.inpMs > 200);
  const lowPerf = audit.pages.filter((p) => p.performance < 50);

  if (slowLcp.length > 0) {
    recs.push({
      id: `perf_lcp_${audit.id}`,
      source: "performance_audit",
      severity: slowLcp.some((p) => p.lcpMs > 4000) ? "critical" : "high",
      title: `Fix slow LCP on ${slowLcp.length} page(s)`,
      detail: `Largest Contentful Paint exceeds 2.5s threshold. Worst: ${Math.max(...slowLcp.map((p) => p.lcpMs))}ms`,
      suggestedFix: "Compress hero images, lazy-load below-fold content, and defer non-critical JavaScript",
      affectedPages: slowLcp.map((p) => p.pageUrl),
      affectedCount: slowLcp.length,
      effort: "medium",
      impact: "high",
    });
  }

  if (highCls.length > 0) {
    recs.push({
      id: `perf_cls_${audit.id}`,
      source: "performance_audit",
      severity: "medium",
      title: `Fix layout shift on ${highCls.length} page(s)`,
      detail: `CLS exceeds 0.1 threshold. Worst: ${Math.max(...highCls.map((p) => p.clsVal))}`,
      suggestedFix: "Add explicit width/height to images and ad containers, avoid inserting content above existing content",
      affectedPages: highCls.map((p) => p.pageUrl),
      affectedCount: highCls.length,
      effort: "low",
      impact: "medium",
    });
  }

  if (slowInp.length > 0) {
    recs.push({
      id: `perf_inp_${audit.id}`,
      source: "performance_audit",
      severity: "high",
      title: `Improve interaction speed on ${slowInp.length} page(s)`,
      detail: `INP exceeds 200ms threshold. Worst: ${Math.max(...slowInp.map((p) => p.inpMs))}ms`,
      suggestedFix: "Reduce JavaScript execution time, break up long tasks, and defer non-essential scripts",
      affectedPages: slowInp.map((p) => p.pageUrl),
      affectedCount: slowInp.length,
      effort: "high",
      impact: "high",
    });
  }

  if (lowPerf.length > 0) {
    recs.push({
      id: `perf_score_${audit.id}`,
      source: "performance_audit",
      severity: "high",
      title: `${lowPerf.length} page(s) have performance score below 50`,
      detail: `These pages are significantly below Google's performance threshold`,
      suggestedFix: "Run a full performance audit and address render-blocking resources, image optimization, and caching",
      affectedPages: lowPerf.map((p) => p.pageUrl),
      affectedCount: lowPerf.length,
      effort: "high",
      impact: "high",
    });
  }

  return recs;
}

// ─── AI Visibility → Recommendations ─────────────────────────────────────────

async function getAiVisibilityRecs(brandId: string): Promise<AuditRecommendation[]> {
  const recs: AuditRecommendation[] = [];

  const data = await getAiVisibility(brandId, "30d", "previous");
  if (!data || data.totals.mentions === 0 && data.totals.mentionChange === 0) return recs;

    // Overall visibility drop
    if (data.totals.mentionChange < 0) {
      recs.push({
        id: `ai_drop_overall`,
        source: "ai_visibility",
        severity: Math.abs(data.totals.mentionChange) >= 5 ? "high" : "medium",
        title: `AI mentions dropped by ${Math.abs(data.totals.mentionChange)} across all platforms`,
        detail: `Total mentions went from ${data.totals.mentions - data.totals.mentionChange} to ${data.totals.mentions}. Review content freshness and structured data.`,
        suggestedFix: "Update FAQ schema on key pages, refresh outdated content, and add authoritative sources to improve AI citation likelihood",
        affectedPages: data.byPage.slice(0, 3).map((p) => p.page),
        affectedCount: data.byPage.length,
        effort: "medium",
        impact: "high",
      });
    }

    // Per-platform drops
    for (const p of data.byPlatform) {
      if (p.mentionChange < 0) {
        recs.push({
          id: `ai_drop_${p.platform}`,
          source: "ai_visibility",
          severity: Math.abs(p.mentionChange) >= 3 ? "high" : "medium",
          title: `${p.platform} mentions dropped by ${Math.abs(p.mentionChange)}`,
          detail: `Your visibility on ${p.platform} decreased from ${p.mentions - p.mentionChange} to ${p.mentions} mentions.`,
          suggestedFix: `Review ${p.platform}'s citation patterns and update content to match. Add conversational Q&A sections that ${p.platform} commonly cites.`,
          affectedPages: data.byPage.filter((pg) => pg.platforms.includes(p.platform)).slice(0, 3).map((pg) => pg.page),
          affectedCount: 1,
          effort: "medium",
          impact: Math.abs(p.mentionChange) >= 3 ? "high" : "medium",
          platform: p.platform,
        });
      }
    }

    // Missing platforms
    const activePlatforms = data.byPlatform.filter((p) => p.mentions > 0).map((p) => p.platform);
    const allPlatforms = ["chatgpt", "claude", "perplexity", "gemini"];
    const missing = allPlatforms.filter((p) => !activePlatforms.includes(p));

    for (const platform of missing.slice(0, 2)) {
      recs.push({
        id: `ai_missing_${platform}`,
        source: "ai_visibility",
        severity: "medium",
        title: `Not visible on ${platform}`,
        detail: `Your brand has no mentions on ${platform}. This platform is actively used for queries in your industry.`,
        suggestedFix: `Create authoritative, well-structured content targeting queries ${platform} commonly answers. Include FAQ schema and clear entity definitions.`,
        affectedPages: [],
        affectedCount: 0,
        effort: "high",
        impact: "medium",
        platform,
      });
    }

    // No mentions at all
    if (data.totals.mentions === 0) {
      recs.push({
        id: `ai_no_mentions`,
        source: "ai_visibility",
        severity: "critical",
        title: "No AI mentions detected across any platform",
        detail: "Your brand is not being cited by any tracked AI platform. This is a significant visibility gap.",
        suggestedFix: "Add FAQ schema to top pages, create authoritative long-form content, ensure your brand entity is well-defined with structured data",
        affectedPages: [],
        affectedCount: 0,
        effort: "high",
        impact: "high",
      });
    }

  return recs;
}

// ─── Competitors → Recommendations ───────────────────────────────────────────

async function getCompetitorRecs(brandId: string): Promise<AuditRecommendation[]> {
  const recs: AuditRecommendation[] = [];

  const competitors = await getCompetitors(brandId, "30d");

    for (const c of competitors) {
      if (!c.latest) continue;

      // High threat competitor
      if (c.latest.threat === "high") {
        recs.push({
          id: `comp_threat_${c.id}`,
          source: "competitors",
          severity: "high",
          title: `${c.name} is a high threat competitor`,
          detail: c.whyWinning || `${c.name} (${c.domain}) is outperforming you in key areas.`,
          suggestedFix: `Analyze ${c.name}'s strongest page (${c.strongestPage || c.domain}) and create competing content. Review their internal linking and content structure.`,
          affectedPages: c.strongestPage ? [c.strongestPage] : [],
          affectedCount: 1,
          effort: "high",
          impact: "high",
          competitor: c.name,
        });
      }

      // Competitor gaining AI mentions
      if (c.latest.aiMentionChange > 0) {
        recs.push({
          id: `comp_ai_gain_${c.id}`,
          source: "competitors",
          severity: c.latest.aiMentionChange >= 3 ? "high" : "medium",
          title: `${c.name} gained ${c.latest.aiMentionChange} AI mentions`,
          detail: `${c.name} now has ${c.latest.aiMentions} AI mentions, up ${c.latest.aiMentionChange} from the previous period.`,
          suggestedFix: `Review ${c.name}'s content strategy for AI visibility. Check their FAQ schema, structured data, and content depth on "${c.latest.topKeyword || "core topics"}".`,
          affectedPages: [],
          affectedCount: 0,
          effort: "medium",
          impact: c.latest.aiMentionChange >= 3 ? "high" : "medium",
          competitor: c.name,
        });
      }

      // Competitor rank improvement
      if (c.latest.rankChange > 0) {
        recs.push({
          id: `comp_rank_${c.id}`,
          source: "competitors",
          severity: c.latest.rankChange >= 3 ? "high" : "medium",
          title: `${c.name} moved up ${c.latest.rankChange} positions for "${c.latest.topKeyword || "core keywords"}"`,
          detail: `${c.name} improved their ranking${c.latest.movement ? ` (${c.latest.movement})` : ""}. Their strongest page is ${c.strongestPage || c.domain}.`,
          suggestedFix: `Update your competing page for "${c.latest.topKeyword || "this keyword"}". Improve content depth, add internal links, and update structured data.`,
          affectedPages: c.strongestPage ? [c.strongestPage] : [],
          affectedCount: 1,
          effort: "medium",
          impact: c.latest.rankChange >= 3 ? "high" : "medium",
          competitor: c.name,
        });
      }
    }

  return recs;
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

function getSuggestedFix(category: string, title: string): string {
  const fixes: Record<string, string> = {
    metadata: "Add or update meta tags (title, description) for affected pages",
    headings: "Fix heading structure — ensure one H1 per page and proper hierarchy",
    crawl: "Resolve crawl issues — fix mixed content, reduce page size, improve load time",
    links: "Fix or remove broken internal links pointing to 404 pages",
    redirects: "Shorten redirect chains to a single 301 redirect",
    indexability: "Review noindex/canonical tags and ensure important pages are indexable",
  };
  return fixes[category] || `Fix: ${title}`;
}

function getEffort(category: string): "low" | "medium" | "high" {
  const efforts: Record<string, "low" | "medium" | "high"> = {
    metadata: "low",
    headings: "low",
    crawl: "medium",
    links: "medium",
    redirects: "low",
    indexability: "low",
  };
  return efforts[category] || "medium";
}

function getImpact(severity: string): "low" | "medium" | "high" {
  if (severity === "critical" || severity === "high") return "high";
  if (severity === "medium") return "medium";
  return "low";
}

// ─── Site Tag → Recommendations ─────────────────────────────────────────────
// Generate recommendations from real-user signals: 404s, 500s, redirects,
// broken links, broken resources, soft 404s, rage clicks, dead clicks, exit
// intent, outbound clicks, scroll depth, form abandonment, JS errors,
// page timing, and traffic/conversion gaps.

// Thresholds — only generate recommendations when signals cross these
// minimums to avoid noise.
const T = {
  HTTP_404_HITS: 3,              // 3+ real user hits on the same 404
  HTTP_500_HITS: 1,              // any server error is worth flagging
  REDIRECT_HITS: 5,              // 5+ traversals of the same redirect
  CHAIN_REDIRECT_HITS: 1,        // any chain redirect (>1 hop)
  BROKEN_LINK_CLICKS: 3,         // 3+ clicks on the same broken link
  RESOURCE_ERROR_PAGES: 2,       // 2+ pages with the same missing resource
  RESOURCE_SCRIPT_HITS: 1,       // any broken script on any page
  SOFT_404_VIEWS: 10,            // 10+ views of a soft 404 page
  RAGE_CLICKS: 5,                // 5+ rage click events on a page
  DEAD_CLICKS: 10,               // 10+ dead click events on a page
  EXIT_INTENT_PCT: 30,           // 30%+ of sessions trigger exit intent
  OUTBOUND_CLICKS: 20,           // 20+ outbound clicks on a page
  SCROLL_DEPTH_PCT: 30,          // avg below 30% scroll
  SCROLL_DEPTH_VIEWS: 50,        // need 50+ views for meaningful data
  FORM_ABANDON_PCT: 40,          // 40%+ form abandonment
  FORM_ABANDON_STARTS: 20,       // need 20+ starts for meaningful data
  JS_ERRORS: 3,                  // 3+ unique errors on a page
  PAGE_TIMING_MS: 3000,          // real user P75 above 3 seconds
  TRAFFIC_NO_CONVERSION_VIEWS: 100, // 100+ views with 0 conversions
} as const;

// 1-hour in-memory cache per brand — Site Tag recommendations change slowly
const siteTagCache = new Map<string, { recs: AuditRecommendation[]; at: number }>();
const CACHE_TTL = 60 * 60 * 1000; // 1 hour

// Types that only need (eventType, pageUrl, count) — no eventData needed.
// These use a lightweight groupBy query instead of pulling full rows.
const COUNT_ONLY_TYPES = ["soft_404", "rage_click", "dead_click", "exit_intent", "outbound_click"] as const;

// Types that need eventData fields (statusCode, referrer, linkText, etc.)
const DETAIL_TYPES = ["http_error", "broken_link", "redirect_detected", "resource_error", "resource_errors", "js_error"] as const;

async function getSiteTagRecs(brandId: string): Promise<AuditRecommendation[]> {
  // Return cached results if fresh enough
  const cached = siteTagCache.get(brandId);
  if (cached && Date.now() - cached.at < CACHE_TTL) return cached.recs;

  const recs: AuditRecommendation[] = [];
  const since = new Date(Date.now() - 30 * 86400000);

  try {
    // Query 1: Lightweight groupBy for types that only need counts
    // Returns ~100-500 rows (one per eventType+pageUrl combo) instead
    // of ~5,000-10,000 individual event rows.
    const countRows = await prisma.siteEvent.groupBy({
      by: ["eventType", "pageUrl"],
      where: {
        brandId,
        eventType: { in: [...COUNT_ONLY_TYPES] },
        timestamp: { gte: since },
      },
      _count: true,
    });

    // Query 2: Full rows only for types that need eventData (smaller set)
    const detailEvents = await prisma.siteEvent.findMany({
      where: {
        brandId,
        eventType: { in: [...DETAIL_TYPES] },
        timestamp: { gte: since },
      },
      select: { eventType: true, pageUrl: true, eventData: true },
      take: 5000,
    });

    // Build a unified page-count map from the groupBy results
    const pageCounts = new Map<string, Map<string, number>>();
    for (const row of countRows) {
      let typeMap = pageCounts.get(row.eventType);
      if (!typeMap) { typeMap = new Map(); pageCounts.set(row.eventType, typeMap); }
      typeMap.set(row.pageUrl, row._count);
    }

    // Alias for the detail events (used by sections 1-5, 10)
    const events = detailEvents;

    // ── 1. 404 errors ───────────────────────────────────────────────
    const http404s = new Map<string, { count: number; referrers: Set<string> }>();
    const http5xxs = new Map<string, { count: number; code: number; referrers: Set<string> }>();
    for (const e of events) {
      if (e.eventType !== "http_error") continue;
      const d = (e.eventData ?? {}) as Record<string, unknown>;
      const code = typeof d.statusCode === "number" ? d.statusCode : 404;
      const referrer = typeof d.referrer === "string" && d.referrer ? d.referrer : null;
      if (code >= 500) {
        const ex = http5xxs.get(e.pageUrl);
        if (ex) { ex.count++; if (referrer) ex.referrers.add(referrer); }
        else http5xxs.set(e.pageUrl, { count: 1, code, referrers: new Set(referrer ? [referrer] : []) });
      } else {
        const ex = http404s.get(e.pageUrl);
        if (ex) { ex.count++; if (referrer) ex.referrers.add(referrer); }
        else http404s.set(e.pageUrl, { count: 1, referrers: new Set(referrer ? [referrer] : []) });
      }
    }

    for (const [url, info] of http404s) {
      if (info.count < T.HTTP_404_HITS) continue;
      const refs = [...info.referrers].slice(0, 3);
      const refText = refs.length > 0 ? ` Users arrive from ${refs.join(", ")}.` : "";
      recs.push({
        id: `st-404-${url}`, source: "site_tag", severity: info.count >= 20 ? "critical" : "high",
        category: "errors", effort: "low", impact: info.count >= 10 ? "high" : "medium",
        title: `Page returns 404 — ${info.count} real users affected`,
        detail: `'${url}' returns 404 and was visited ${info.count} times this month.${refText}`,
        suggestedFix: refs.length > 0
          ? `Fix the broken links on ${refs[0]} or add a 301 redirect from '${url}' to the correct page.`
          : `Add a 301 redirect from '${url}' to the correct page, or restore the content.`,
        affectedPages: [url, ...refs], affectedCount: info.count, metric: `${info.count} user hits`,
      });
    }

    // ── 2. 500 errors ───────────────────────────────────────────────
    for (const [url, info] of http5xxs) {
      if (info.count < T.HTTP_500_HITS) continue;
      const refs = [...info.referrers].slice(0, 3);
      recs.push({
        id: `st-5xx-${url}`, source: "site_tag", severity: "critical",
        category: "errors", effort: "high", impact: "high",
        title: `Server error ${info.code} — ${info.count} real users affected`,
        detail: `'${url}' returned a ${info.code} server error ${info.count} times.${refs.length > 0 ? ` Users arriving from ${refs.join(", ")} hit a broken page.` : ""}`,
        suggestedFix: "This indicates a backend issue. Check server logs, application error tracking, and hosting status for this URL.",
        affectedPages: [url, ...refs], affectedCount: info.count, metric: `${info.count} server errors`,
      });
    }

    // ── 3. Redirects ────────────────────────────────────────────────
    const redirects = new Map<string, { orig: string; final: string; count: number; totalMs: number; isChain: boolean }>();
    for (const e of events) {
      if (e.eventType !== "redirect_detected") continue;
      const d = (e.eventData ?? {}) as Record<string, unknown>;
      const orig = typeof d.originalUrl === "string" ? d.originalUrl : "unknown";
      const final = typeof d.finalUrl === "string" ? d.finalUrl : e.pageUrl;
      const key = `${orig}::${final}`;
      const ms = typeof d.redirectTimeMs === "number" ? d.redirectTimeMs : 0;
      const chain = d.isChain === true;
      const ex = redirects.get(key);
      if (ex) { ex.count++; ex.totalMs += ms; if (chain) ex.isChain = true; }
      else redirects.set(key, { orig, final, count: 1, totalMs: ms, isChain: chain });
    }

    for (const [, info] of redirects) {
      const threshold = info.isChain ? T.CHAIN_REDIRECT_HITS : T.REDIRECT_HITS;
      if (info.count < threshold) continue;
      const avgMs = info.count > 0 ? Math.round(info.totalMs / info.count) : 0;
      const sev: AuditRecommendation["severity"] = avgMs > 1000 ? "critical" : info.isChain ? "high" : "medium";
      recs.push({
        id: `st-redir-${info.orig}`, source: "site_tag", severity: sev,
        category: "errors", effort: "low", impact: info.isChain ? "high" : "medium",
        title: `301 redirect${info.isChain ? " chain" : ""} — ${info.count} user traversals`,
        detail: `'${info.orig}' redirects to '${info.final}'${info.isChain ? " via multiple hops" : ""} (avg ${avgMs}ms). ${info.count} users followed this path.`,
        suggestedFix: `Update links pointing to '${info.orig}' to point directly to '${info.final}'. This eliminates the redirect hop and improves page speed.`,
        affectedPages: [info.orig, info.final], affectedCount: info.count, metric: `${avgMs}ms avg redirect`,
      });
    }

    // ── 4. Broken internal links ────────────────────────────────────
    const brokenLinks = new Map<string, { src: string; tgt: string; text: string | null; count: number }>();
    for (const e of events) {
      if (e.eventType !== "broken_link") continue;
      const d = (e.eventData ?? {}) as Record<string, unknown>;
      const src = typeof d.sourceUrl === "string" ? d.sourceUrl : "";
      const tgt = typeof d.targetUrl === "string" ? d.targetUrl : e.pageUrl;
      const key = `${src}::${tgt}`;
      const ex = brokenLinks.get(key);
      if (ex) ex.count++;
      else brokenLinks.set(key, { src, tgt, text: typeof d.linkText === "string" ? d.linkText : null, count: 1 });
    }

    for (const [, info] of brokenLinks) {
      if (info.count < T.BROKEN_LINK_CLICKS) continue;
      const linkDesc = info.text ? ` The link text is "${info.text}".` : "";
      recs.push({
        id: `st-bl-${info.src}-${info.tgt}`, source: "site_tag", severity: "high",
        category: "errors", effort: "low", impact: "high",
        title: `Broken link — ${info.count} users clicked a dead link`,
        detail: `'${info.src}' contains a broken link to '${info.tgt}' that ${info.count} users clicked.${linkDesc}`,
        suggestedFix: `Update the href on '${info.src}' to point to the correct page, or remove the link.`,
        affectedPages: [info.src, info.tgt], affectedCount: info.count, metric: `${info.count} clicks`,
      });
    }

    // ── 5. Broken resources ─────────────────────────────────────────
    const brokenRes = new Map<string, { type: string; pages: Set<string>; count: number }>();
    const addBrokenRes = (pageUrl: string, resUrl: string, resType: string) => {
      if (!resUrl) return;
      const ex = brokenRes.get(resUrl);
      if (ex) { ex.pages.add(pageUrl); ex.count++; }
      else brokenRes.set(resUrl, { type: resType, pages: new Set([pageUrl]), count: 1 });
    };
    for (const e of events) {
      if (e.eventType === "resource_error") {
        const d = (e.eventData ?? {}) as Record<string, unknown>;
        addBrokenRes(e.pageUrl, typeof d.resourceUrl === "string" ? d.resourceUrl : "", typeof d.resourceType === "string" ? d.resourceType : "unknown");
      } else if (e.eventType === "resource_errors") {
        const d = (e.eventData ?? {}) as Record<string, unknown>;
        const resources = Array.isArray(d.resources) ? d.resources : [];
        for (const r of resources) {
          const res = r as Record<string, unknown>;
          addBrokenRes(e.pageUrl, typeof res.url === "string" ? res.url : "", typeof res.type === "string" ? res.type : "unknown");
        }
      }
    }

    for (const [resUrl, info] of brokenRes) {
      const threshold = info.type === "script" ? T.RESOURCE_SCRIPT_HITS : T.RESOURCE_ERROR_PAGES;
      if (info.pages.size < threshold && info.count < threshold) continue;
      const sev: AuditRecommendation["severity"] = info.type === "script" ? "critical" : "medium";
      const pages = [...info.pages].slice(0, 5);
      recs.push({
        id: `st-res-${resUrl}`, source: "site_tag", severity: sev,
        category: "errors", effort: info.type === "script" ? "high" : "low",
        impact: info.type === "script" ? "high" : "medium",
        title: `Broken ${info.type} — missing on ${info.pages.size} page${info.pages.size > 1 ? "s" : ""}`,
        detail: `Resource '${resUrl}' (${info.type}) fails to load on ${pages.join(", ")}${info.pages.size > 5 ? ` and ${info.pages.size - 5} more` : ""}.`,
        suggestedFix: info.type === "script"
          ? "Broken scripts can break page functionality (forms, navigation). Fix the resource URL or remove the reference."
          : "Fix the resource URL, re-upload the file, or remove the broken reference.",
        affectedPages: pages, affectedCount: info.pages.size, metric: `${info.count} load failures`,
      });
    }

    // ── 6. Soft 404 (from groupBy) ─────────────────────────────────
    for (const [url, count] of pageCounts.get("soft_404") ?? []) {
      if (count < T.SOFT_404_VIEWS) continue;
      recs.push({
        id: `st-s404-${url}`, source: "site_tag", severity: "medium",
        category: "errors", effort: "low", impact: "medium",
        title: `Soft 404 page — ${count} visits to thin content`,
        detail: `'${url}' returns 200 but has almost no content (likely an error page). Google may index this as a soft 404.`,
        suggestedFix: "Either return a proper 404 status, add a noindex meta tag, or add meaningful content to the page.",
        affectedPages: [url], affectedCount: count, metric: `${count} views`,
      });
    }

    // ── 7. Rage clicks (from groupBy) ──────────────────────────────
    for (const [url, count] of pageCounts.get("rage_click") ?? []) {
      if (count < T.RAGE_CLICKS) continue;
      recs.push({
        id: `st-rage-${url}`, source: "site_tag", severity: "high",
        category: "ux", effort: "medium", impact: "high",
        title: `Rage click hotspot — ${count} frustrated clicks`,
        detail: `'${url}' has ${count} rage click events. Users are rapidly clicking on non-responsive elements.`,
        suggestedFix: "Check for broken buttons, unresponsive links, or misleading UI elements that users expect to be clickable.",
        affectedPages: [url], affectedCount: count, metric: `${count} rage clicks`,
      });
    }

    // ── 8. Dead clicks (from groupBy) ──────────────────────────────
    for (const [url, count] of pageCounts.get("dead_click") ?? []) {
      if (count < T.DEAD_CLICKS) continue;
      recs.push({
        id: `st-dead-${url}`, source: "site_tag", severity: "medium",
        category: "ux", effort: "low", impact: "medium",
        title: `Dead click area — ${count} clicks on non-interactive elements`,
        detail: `Users are clicking on elements that aren't interactive on '${url}'.`,
        suggestedFix: "Consider adding links or making visual elements clickable. Common causes: images that look like buttons, text styled as links but without hrefs.",
        affectedPages: [url], affectedCount: count, metric: `${count} dead clicks`,
      });
    }

    // ── 9. Exit intent (from groupBy) ──────────────────────────────
    for (const [url, count] of pageCounts.get("exit_intent") ?? []) {
      if (count < 10) continue; // need meaningful volume
      recs.push({
        id: `st-exit-${url}`, source: "site_tag", severity: "medium",
        category: "conversion", effort: "medium", impact: "medium",
        title: `High exit intent — ${count} users tried to leave`,
        detail: `'${url}' triggers exit intent ${count} times. Users are leaving this page before engaging.`,
        suggestedFix: "Consider adding an exit-intent popup with a special offer or appointment CTA. Review page content for engagement hooks.",
        affectedPages: [url], affectedCount: count, metric: `${count} exit intents`,
      });
    }

    // ── 10. JS errors ───────────────────────────────────────────────
    const jsErrors = new Map<string, { count: number; messages: Set<string> }>();
    for (const e of events) {
      if (e.eventType !== "js_error") continue;
      const d = (e.eventData ?? {}) as Record<string, unknown>;
      const msg = typeof d.message === "string" ? d.message : "";
      const ex = jsErrors.get(e.pageUrl);
      if (ex) { ex.count++; if (msg) ex.messages.add(msg); }
      else jsErrors.set(e.pageUrl, { count: 1, messages: new Set(msg ? [msg] : []) });
    }

    for (const [url, info] of jsErrors) {
      if (info.messages.size < T.JS_ERRORS) continue;
      recs.push({
        id: `st-js-${url}`, source: "site_tag", severity: "critical",
        category: "technical", effort: "high", impact: "high",
        title: `JavaScript errors — ${info.messages.size} unique errors on page`,
        detail: `'${url}' has ${info.count} JS errors (${info.messages.size} unique). Errors: ${[...info.messages].slice(0, 2).join("; ")}`,
        suggestedFix: "Broken scripts may prevent form submissions, navigation, and tracking. Debug the errors in the browser console.",
        affectedPages: [url], affectedCount: info.count, metric: `${info.messages.size} unique errors`,
      });
    }

    // ── 11. High outbound clicks (from groupBy) ────────────────────
    for (const [url, count] of pageCounts.get("outbound_click") ?? []) {
      if (count < T.OUTBOUND_CLICKS) continue;
      recs.push({
        id: `st-out-${url}`, source: "site_tag", severity: "medium",
        category: "conversion", effort: "low", impact: "medium",
        title: `High outbound traffic — ${count} users leaving from this page`,
        detail: `'${url}' has ${count} outbound clicks. Users may be leaving before converting.`,
        suggestedFix: "Review outbound links and consider adding internal CTAs before external links. Move key conversion elements above outbound links.",
        affectedPages: [url], affectedCount: count, metric: `${count} outbound clicks`,
      });
    }
  } catch (err) {
    console.error("[audit-recommendations] getSiteTagRecs failed", err);
  }

  siteTagCache.set(brandId, { recs, at: Date.now() });
  return recs;
}
