/**
 * Brand Strength Radar
 * ────────────────────
 * Computes the 8-dimension brand health score the admin radar
 * dashboard renders. Each dimension scores 0-100; the overall
 * brand health = simple equal-weighted average.
 *
 * Dimensions:
 *   technical    Technical Audit crawlHealth.
 *   content      Average content health (best-effort proxy).
 *   search       GSC visibility (impressions × position composite).
 *   traffic      Sessions normalized + bounce rate inverted.
 *   conversion   Conversion rate × volume.
 *   competitive  Keyword-battle win rate (neutral 50 when no data).
 *   ai           AI-citation coverage from AI Visibility checks.
 *   maturity     Setup completeness — integrations + age + events.
 *
 * The platform-aggregate radar averages each dimension across all
 * brands. Every score is computed against a single brand by
 * scoreBrand(brandId), and the platform average is the mean of
 * scoreBrand() across the brand list.
 */

import { prisma } from "@/lib/prisma";
import { getSessionCount } from "./platform-metrics";
import { countedConversionWhere } from "@/lib/conversions/counted-where";

export type RadarDimension =
  | "technical"
  | "content"
  | "search"
  | "traffic"
  | "conversion"
  | "competitive"
  | "ai"
  | "maturity";

export const RADAR_DIMENSIONS: Array<{ id: RadarDimension; label: string }> = [
  { id: "technical",   label: "Technical Health" },
  { id: "content",     label: "Content Quality" },
  { id: "search",      label: "Search Visibility" },
  { id: "traffic",     label: "Traffic & Engagement" },
  { id: "conversion",  label: "Conversion" },
  { id: "competitive", label: "Competitive" },
  { id: "ai",          label: "AI Visibility" },
  { id: "maturity",    label: "Data Maturity" },
];

export interface RadarScore {
  brandId: string;
  brandName: string;
  domain: string;
  scores: Record<RadarDimension, number>;
  health: number;
  components: {
    technical: { auditScore: number | null };
    content: { avgHealth: number | null; pageCount: number };
    search: { impressions: number; avgPosition: number | null };
    traffic: { sessions30d: number; bounceRate: number | null };
    conversion: { conversions30d: number; rate: number | null };
    competitive: { battlesAnalyzed: number; winRate: number | null };
    ai: { totalChecks: number; mentionRate: number | null };
    maturity: { siteTagInstalled: boolean; ga4Connected: boolean; gscConnected: boolean; daysOfData: number; eventCount: number; featureAdoption: number };
  };
}

const clamp01 = (n: number): number => Math.max(0, Math.min(100, Math.round(n)));

/** Normalise a value against a platform-median proxy. We avoid
 *  recomputing the median for every call — just cap+log-scale. */
function logScale(value: number, target: number): number {
  if (value <= 0) return 0;
  // Log scale so a brand with 10k events doesn't dominate one with
  // 100k. Reaches ~80 at the target value.
  return clamp01((Math.log10(value + 1) / Math.log10(target + 1)) * 100);
}

export async function scoreBrand(brandId: string): Promise<RadarScore | null> {
  const brand = await prisma.brand.findUnique({
    where: { id: brandId },
    include: {
      trackedSite: { select: { id: true, createdAt: true } },
      integrations: { select: { integrationId: true, connected: true } },
    },
  });
  if (!brand) return null;

  const now = Date.now();
  const d30 = new Date(now - 30 * 86_400_000);
  const trackedSiteId = brand.trackedSite?.id ?? null;

  const [
    audit, contentPages, gscAgg, sessions30, bounceData, conversions30,
    aiVisibility, distinctActions,
  ] = await Promise.all([
    prisma.technicalAudit.findFirst({
      where: { brandId },
      orderBy: { createdAt: "desc" },
      select: { crawlHealth: true },
    }),
    // Content quality proxy: pageRecord count + average wordCount as a
    // crude indicator. A real "content health" would need a stored
    // per-page health score; this stands in until that lands.
    prisma.pageRecord.aggregate({
      where: { brandId, currentStatus: 200 },
      _avg: { wordCount: true },
      _count: true,
    }),
    prisma.gscPage.aggregate({
      where: { brandId, date: { gte: d30 } },
      _sum: { impressions: true },
      _avg: { position: true },
    }),
    getSessionCount(brandId, d30, trackedSiteId ? { trackedSiteId } : {}),
    trackedSiteId
      ? prisma.trackedSession.aggregate({
          where: { trackedSiteId, startedAt: { gte: d30 } },
          _avg: { pageCount: true },
          _count: true,
        })
      : Promise.resolve({ _avg: { pageCount: null }, _count: 0 } as { _avg: { pageCount: number | null }; _count: number }),
    prisma.siteConversion.count({ where: countedConversionWhere(brandId, { since: d30 }) }),
    // AiVisibilitySnapshot stores per-platform mentions + citations
    // (no explicit total-queries field). Use mentions vs citations
    // as a coverage proxy: mentions / max(citations, 1) capped.
    prisma.aiVisibilitySnapshot.findFirst({
      where: { brandId },
      orderBy: { createdAt: "desc" },
      select: { mentions: true, citations: true, visibilityScore: true },
    }),
    prisma.platformEvent.groupBy({
      by: ["action"],
      where: { brandId },
      _count: true,
    }),
  ]);

  // ── Technical ─────────────────────────────────────────────────
  const auditScore = audit?.crawlHealth ?? null;
  const technical = clamp01(auditScore ?? 0);

  // ── Content ───────────────────────────────────────────────────
  // Score = clamp(avgWordCount / 10) capped at 100. So 1000 words
  // averages to a 100; 300 words to 30. Crude but consistent.
  const avgWordCount = contentPages._avg.wordCount ?? null;
  const content = avgWordCount != null
    ? clamp01(avgWordCount / 10)
    : 0;

  // ── Search ────────────────────────────────────────────────────
  // 60% impressions (log-scaled to 100k as the "great" anchor) +
  // 40% position (inverted: position 1 = 100, position 10 = 0).
  const impressions = gscAgg._sum.impressions ?? 0;
  const avgPosition = gscAgg._avg.position ?? null;
  const impScore = logScale(impressions, 100_000);
  const posScore = avgPosition == null ? 0 : clamp01(100 - avgPosition * 10);
  const search = clamp01(impScore * 0.6 + posScore * 0.4);

  // ── Traffic & Engagement ──────────────────────────────────────
  // 70% session count (log-scaled to 50k/mo) + 30% bounce inverted.
  // pageCount > 1 = engaged; we use avg pages-per-session as the
  // bounce proxy because TrackedSession doesn't carry bounceRate.
  const sessionScore = logScale(sessions30, 50_000);
  const avgPages = bounceData._avg.pageCount ?? null;
  const bounceRate = avgPages != null && bounceData._count > 0
    // Approximate bounce: sessions with pageCount === 1 / total.
    // We don't have that exact ratio without another query; use
    // 1 - min(1, (avgPages-1)) as a stand-in (more pages → less bounce).
    ? Math.max(0, Math.min(1, 1 / Math.max(1, avgPages)))
    : null;
  const bounceScore = bounceRate != null ? clamp01((1 - bounceRate) * 100) : 0;
  const traffic = clamp01(sessionScore * 0.7 + bounceScore * 0.3);

  // ── Conversion ────────────────────────────────────────────────
  // Rate × 1000, capped at 100 (so 10% CR = 100). Volume contributes
  // 40% via log scale to 100/mo.
  const rate = sessions30 > 0 ? conversions30 / sessions30 : null;
  const rateScore = rate != null ? clamp01(rate * 1000) : 0;
  const volScore = logScale(conversions30, 100);
  const conversion = clamp01(rateScore * 0.6 + volScore * 0.4);

  // ── Competitive ───────────────────────────────────────────────
  // Neutral 50 for now — keyword-battle data isn't trivially
  // queryable without parsing per-competitor SiteEvent caches.
  // Improvement opportunity: add a denormalised CompetitorBattle
  // model so this becomes a cheap aggregate.
  const competitive = 50;

  // ── AI Visibility ─────────────────────────────────────────────
  // Prefer the stored visibilityScore (already 0-100) when it's
  // populated; fall back to mentions / max(citations, mentions).
  const totalChecks = (aiVisibility?.mentions ?? 0) + (aiVisibility?.citations ?? 0);
  const mentionRate = aiVisibility && (aiVisibility.mentions + aiVisibility.citations) > 0
    ? aiVisibility.mentions / (aiVisibility.mentions + aiVisibility.citations)
    : null;
  const ai = aiVisibility?.visibilityScore != null
    ? clamp01(aiVisibility.visibilityScore)
    : (mentionRate != null ? clamp01(mentionRate * 100) : 0);

  // ── Data Maturity ─────────────────────────────────────────────
  // Composite of integration completeness + age + activity.
  const ga4 = brand.integrations.some((i) => i.integrationId === "ga4" && i.connected);
  const gsc = brand.integrations.some((i) => i.integrationId === "gsc" && i.connected);
  const siteTag = !!brand.trackedSite;
  const daysOfData = brand.trackedSite
    ? Math.floor((now - brand.trackedSite.createdAt.getTime()) / 86_400_000)
    : 0;
  // Use lifetime event count via a cached lookup. To keep this cheap
  // we reuse the count we already have from sessions30 weight as a
  // proxy for "is there data flowing"; 30d sessions > 0 → 10pts.
  const sessions30Indicator = sessions30 > 0 ? 10 : 0;
  const featureAdoption = Math.min(20, distinctActions.length);
  const maturity = clamp01(
    (siteTag ? 25 : 0)
    + (ga4 ? 20 : 0)
    + (gsc ? 20 : 0)
    + (daysOfData > 30 ? 15 : daysOfData / 2)  // ramp up to 15 over 30 days
    + sessions30Indicator
    + featureAdoption,
  );

  const scores: Record<RadarDimension, number> = {
    technical, content, search, traffic, conversion, competitive, ai, maturity,
  };
  const health = Math.round(
    Object.values(scores).reduce((s, v) => s + v, 0) / RADAR_DIMENSIONS.length,
  );

  return {
    brandId,
    brandName: brand.name,
    domain: brand.domain,
    scores,
    health,
    components: {
      technical: { auditScore },
      content: { avgHealth: avgWordCount, pageCount: contentPages._count },
      search: { impressions, avgPosition },
      traffic: { sessions30d: sessions30, bounceRate },
      conversion: { conversions30d: conversions30, rate },
      competitive: { battlesAnalyzed: 0, winRate: null },
      ai: { totalChecks, mentionRate },
      maturity: {
        siteTagInstalled: siteTag,
        ga4Connected: ga4,
        gscConnected: gsc,
        daysOfData,
        eventCount: 0,
        featureAdoption,
      },
    },
  };
}

// ─── Per-dimension deep-dive ────────────────────────────────────
//
// scoreBrandDetailed() returns the same RadarScore shape + a
// `perDimension` block carrying the specific facts the UI surfaces
// in the deep-dive cards:
//   technical   — latest audit + top-3 open issues
//   content     — page count + avg word count + best/worst page
//   search      — impressions + clicks + top-3 gainers/losers
//   traffic     — sessions + bounce proxy + top pages
//   conversion  — deduped conversions + breakdown + best pages
//   competitive — tracked competitors + battles (placeholder until
//                 CompetitorBattle model lands)
//   ai          — total checks + mention rate + top cited content
//   maturity    — integration status + days of data + feature list
//
// Each block gives the UI everything it needs to render the card
// without another round trip. Optional `trend` per dimension is
// also computed from the 3 most-recent audits / the 7-day-ago
// baseline. The service stays read-only — no DB writes.

export interface PerDimensionDetails {
  technical: {
    latestScore: number | null;
    trend: Array<{ date: string; score: number }>;
    topIssues: Array<{ category: string; severity: string; title: string; affected: number }>;
    pagesWithCritical: number;
  };
  content: {
    pageCount: number;
    avgWordCount: number | null;
    bestPage: { pageUrl: string; wordCount: number } | null;
    worstPage: { pageUrl: string; wordCount: number } | null;
  };
  search: {
    impressions: number;
    clicks: number;
    avgPosition: number | null;
    avgCtr: number | null;
    topQueries: Array<{ query: string; clicks: number; position: number }>;
    biggestMovers: Array<{ query: string; deltaPosition: number }>;
  };
  traffic: {
    sessions30d: number;
    sessions7d: number;
    avgPagesPerSession: number | null;
    bounceProxy: number | null;
    topPages: Array<{ pageUrl: string; views: number }>;
  };
  conversion: {
    conversions30d: number;
    interactions30d: number;
    breakdown: Record<string, number>;
    topPages: Array<{ pageUrl: string; count: number }>;
    rate: number | null;
  };
  competitive: {
    trackedCompetitors: number;
    keywordsMonitored: number;
  };
  ai: {
    totalChecks: number;
    mentionRate: number | null;
    latestScore: number | null;
    topPlatforms: Array<{ platform: string; mentions: number }>;
  };
  maturity: {
    siteTagInstalled: boolean;
    ga4Connected: boolean;
    gscConnected: boolean;
    gbpConnected: boolean;
    competitorsTracked: boolean;
    daysOfData: number;
    eventCount30d: number;
    featuresUsed: string[];
  };
}

export interface DetailedRadarScore extends RadarScore {
  perDimension: PerDimensionDetails;
}

export async function scoreBrandDetailed(brandId: string): Promise<DetailedRadarScore | null> {
  const base = await scoreBrand(brandId);
  if (!base) return null;

  const now = Date.now();
  const d30 = new Date(now - 30 * 86_400_000);
  const d7 = new Date(now - 7 * 86_400_000);

  const trackedSite = await prisma.trackedSite.findUnique({
    where: { brandId }, select: { id: true },
  });
  const trackedSiteId = trackedSite?.id ?? null;

  const [
    lastAudits, topIssues, bestPage, worstPage,
    gscTotals, gscTopQueries, sessions7d, topPagesEvents,
    conversionRows, topConversionPages, competitorCount, keywordCount,
    aiByPlatform, aiLatest, integrations, featureEvents, eventCount30d,
  ] = await Promise.all([
    // Last 3 audits for the trend.
    prisma.technicalAudit.findMany({
      where: { brandId, status: "completed" },
      orderBy: { createdAt: "desc" },
      take: 3,
      select: { createdAt: true, crawlHealth: true, errorCount: true },
    }),
    // Top 3 OPEN issues from the most recent audit.
    prisma.technicalIssue.findMany({
      where: { audit: { brandId }, status: "open" },
      orderBy: [{ severity: "asc" }, { affected: "desc" }],
      take: 3,
      select: { category: true, severity: true, title: true, affected: true },
    }),
    // Best/worst content pages by word count (proxy for depth).
    prisma.pageRecord.findFirst({
      where: { brandId, currentStatus: 200, wordCount: { gt: 0 } },
      orderBy: { wordCount: "desc" },
      select: { url: true, wordCount: true },
    }),
    prisma.pageRecord.findFirst({
      where: { brandId, currentStatus: 200, wordCount: { gt: 0 } },
      orderBy: { wordCount: "asc" },
      select: { url: true, wordCount: true },
    }),
    // GSC totals — impressions + clicks + avg position + avg ctr.
    prisma.gscPage.aggregate({
      where: { brandId, date: { gte: d30 } },
      _sum: { clicks: true, impressions: true },
      _avg: { position: true, ctr: true },
    }),
    prisma.gscQuery.groupBy({
      by: ["query"],
      where: { brandId, date: { gte: d30 } },
      _sum: { clicks: true },
      _avg: { position: true },
      orderBy: { _sum: { clicks: "desc" } },
      take: 5,
    }),
    // 7d session baseline via shared helper.
    getSessionCount(brandId, d7, trackedSiteId ? { trackedSiteId } : {}),
    // Top pages by page_view count.
    trackedSiteId
      ? prisma.trackedEvent.groupBy({
          by: ["pageUrl"],
          where: { trackedSiteId, eventType: "page_view", timestamp: { gte: d30 } },
          _count: true,
          orderBy: { _count: { pageUrl: "desc" } },
          take: 5,
        })
      : Promise.resolve([] as Array<{ pageUrl: string; _count: number }>),
    // Conversion rows — for deduped count, breakdown, and
    // interactions total.
    prisma.siteConversion.findMany({
      where: countedConversionWhere(brandId, { since: d30 }),
      select: { conversionType: true, pageUrl: true, sessionId: true },
      take: 20_000,
    }),
    prisma.siteConversion.groupBy({
      by: ["pageUrl"],
      where: countedConversionWhere(brandId, { since: d30 }),
      _count: true,
      orderBy: { _count: { pageUrl: "desc" } },
      take: 5,
    }),
    prisma.competitor.count({ where: { brandId, isActive: true } }),
    prisma.trackedKeyword.count({ where: { brandId, isActive: true } }),
    prisma.aiVisibilityCheck.groupBy({
      by: ["platform"],
      where: { brandId, mentioned: true },
      _count: true,
      orderBy: { _count: { platform: "desc" } },
      take: 5,
    }),
    prisma.aiVisibilitySnapshot.findFirst({
      where: { brandId },
      orderBy: { createdAt: "desc" },
      select: { visibilityScore: true, mentions: true, citations: true },
    }),
    prisma.brandIntegration.findMany({
      where: { brandId },
      select: { integrationId: true, connected: true },
    }),
    prisma.platformEvent.groupBy({
      by: ["action"],
      where: { brandId },
      _count: true,
      orderBy: { _count: { action: "desc" } },
      take: 20,
    }),
    trackedSiteId
      ? prisma.trackedEvent.count({ where: { trackedSiteId, timestamp: { gte: d30 } } })
      : Promise.resolve(0),
  ]);

  // ── Conversions: dedupe per (session|page|conversionType). Legacy
  // rows carry the old type names; canonicalise via the phone-merge
  // rules already established in the taxonomy module.
  const seen = new Set<string>();
  const breakdown: Record<string, number> = {};
  for (const r of conversionRows) {
    const canon = canonicalise(r.conversionType);
    const key = `${r.sessionId}|${r.pageUrl}|${canon}`;
    if (seen.has(key)) continue;
    seen.add(key);
    breakdown[canon] = (breakdown[canon] ?? 0) + 1;
  }
  const conversions30d = seen.size;
  const interactions30d = conversionRows.length;
  const rate = base.components.traffic.sessions30d > 0
    ? conversions30d / base.components.traffic.sessions30d
    : null;

  const perDimension: PerDimensionDetails = {
    technical: {
      latestScore: base.components.technical.auditScore,
      trend: lastAudits.reverse().map((a) => ({
        date: a.createdAt.toISOString().slice(0, 10),
        score: a.crawlHealth,
      })),
      topIssues: topIssues.map((i) => ({
        category: i.category, severity: i.severity, title: i.title, affected: i.affected,
      })),
      pagesWithCritical: lastAudits[lastAudits.length - 1]?.errorCount ?? 0,
    },
    content: {
      pageCount: base.components.content.pageCount,
      avgWordCount: base.components.content.avgHealth,
      bestPage: bestPage ? { pageUrl: bestPage.url, wordCount: bestPage.wordCount ?? 0 } : null,
      worstPage: worstPage ? { pageUrl: worstPage.url, wordCount: worstPage.wordCount ?? 0 } : null,
    },
    search: {
      impressions: gscTotals._sum.impressions ?? 0,
      clicks: gscTotals._sum.clicks ?? 0,
      avgPosition: gscTotals._avg.position ?? null,
      avgCtr: gscTotals._avg.ctr ?? null,
      topQueries: gscTopQueries.map((q) => ({
        query: q.query,
        clicks: q._sum.clicks ?? 0,
        position: q._avg.position ?? 0,
      })),
      biggestMovers: [], // reserved for when position deltas are stored
    },
    traffic: {
      sessions30d: base.components.traffic.sessions30d,
      sessions7d,
      avgPagesPerSession: null, // surfaced via bounceProxy below
      bounceProxy: base.components.traffic.bounceRate,
      topPages: topPagesEvents.map((p) => ({ pageUrl: p.pageUrl, views: p._count })),
    },
    conversion: {
      conversions30d,
      interactions30d,
      breakdown,
      topPages: topConversionPages.map((p) => ({ pageUrl: p.pageUrl, count: p._count })),
      rate,
    },
    competitive: {
      trackedCompetitors: competitorCount,
      keywordsMonitored: keywordCount,
    },
    ai: {
      totalChecks: base.components.ai.totalChecks,
      mentionRate: base.components.ai.mentionRate,
      latestScore: aiLatest?.visibilityScore ?? null,
      topPlatforms: aiByPlatform.map((p) => ({ platform: p.platform, mentions: p._count })),
    },
    maturity: {
      siteTagInstalled: base.components.maturity.siteTagInstalled,
      ga4Connected: base.components.maturity.ga4Connected,
      gscConnected: base.components.maturity.gscConnected,
      gbpConnected: integrations.some((i) => i.integrationId === "gbp" && i.connected),
      competitorsTracked: competitorCount > 0,
      daysOfData: base.components.maturity.daysOfData,
      eventCount30d,
      featuresUsed: featureEvents.map((f) => f.action),
    },
  };

  return { ...base, perDimension };
}

// Canonicalise — local copy so this module doesn't pull in the
// conversion-taxonomy module at import time (they're in the same
// service directory; avoiding a circular-import risk).
function canonicalise(raw: string): string {
  const r = raw.toLowerCase();
  if (r === "phone_number_click" || r === "click_to_call" || r === "phone_call") return "phone_call";
  if (r === "form_submit" || r === "form_submission" || r === "form_submitted") return "form_submitted";
  if (r === "booking_attempt" || r === "booking_submit" || r === "appointment_attempted") return "appointment_attempted";
  if (r === "booking_confirmed" || r === "appointment_booked") return "appointment_booked";
  if (r === "purchase") return "purchase";
  return r;
}

// ─── Recommendations ────────────────────────────────────────────
//
// Generate actionable guidance based on the lowest-scoring
// dimensions. Returns up to 3 recommendations, each with an
// estimated impact + a CTA URL linking to the relevant feature.

export interface RadarRecommendation {
  dimension: RadarDimension;
  message: string;
  impact: "low" | "medium" | "high";
  ctaUrl: string;
  ctaLabel: string;
}

const RECOMMENDATIONS: Record<RadarDimension, (score: DetailedRadarScore) => RadarRecommendation | null> = {
  technical: (s) => {
    const score = s.scores.technical;
    if (score >= 70) return null;
    const issues = s.perDimension.technical.pagesWithCritical;
    return {
      dimension: "technical",
      message: issues > 0
        ? `Technical score ${score}/100 · ${issues} pages have critical issues. Run a Technical Audit and triage the top blockers.`
        : `Technical score ${score}/100. Run a Technical Audit to surface crawl + indexation problems.`,
      impact: score < 30 ? "high" : score < 60 ? "medium" : "low",
      ctaUrl: "/technical-audit",
      ctaLabel: "Open Technical Audit",
    };
  },
  content: (s) => {
    const score = s.scores.content;
    if (score >= 70) return null;
    return {
      dimension: "content",
      message: `Content Quality ${score}/100. Run a Content Audit to identify thin and outdated pages — avg ${s.perDimension.content.avgWordCount ?? 0} words per page currently.`,
      impact: score < 30 ? "high" : score < 60 ? "medium" : "low",
      ctaUrl: "/content-audit",
      ctaLabel: "Open Content Audit",
    };
  },
  search: (s) => {
    const score = s.scores.search;
    if (score >= 70) return null;
    const { impressions, avgPosition } = s.perDimension.search;
    return {
      dimension: "search",
      message: impressions === 0
        ? "Search score 0 — no GSC data. Connect Google Search Console in Settings → Integrations."
        : `Search Visibility ${score}/100 · ${impressions.toLocaleString()} impressions, avg pos ${avgPosition?.toFixed(1) ?? "—"}. Review Rank Tracker for improving targets.`,
      impact: score < 30 ? "high" : score < 60 ? "medium" : "low",
      ctaUrl: "/rank-tracker",
      ctaLabel: impressions === 0 ? "Connect GSC" : "Open Rank Tracker",
    };
  },
  traffic: (s) => {
    const score = s.scores.traffic;
    if (score >= 70) return null;
    return {
      dimension: "traffic",
      message: `Traffic ${score}/100 · ${s.perDimension.traffic.sessions30d.toLocaleString()} sessions over 30 days. Review Site Tag Analytics for traffic sources + engagement.`,
      impact: score < 30 ? "high" : score < 60 ? "medium" : "low",
      ctaUrl: "/site-tag-analytics",
      ctaLabel: "Open Site Tag Analytics",
    };
  },
  conversion: (s) => {
    const score = s.scores.conversion;
    if (score >= 70) return null;
    const rate = s.perDimension.conversion.rate != null
      ? (s.perDimension.conversion.rate * 100).toFixed(1)
      : null;
    return {
      dimension: "conversion",
      message: rate != null
        ? `Conversion ${score}/100 · only ${rate}% of sessions convert. Add clearer CTAs and test form placement.`
        : `Conversion ${score}/100 · no conversions in the last 30 days. Verify the Site Tag is firing on every page.`,
      impact: score < 30 ? "high" : score < 60 ? "medium" : "low",
      ctaUrl: "/site-tag-analytics",
      ctaLabel: "Open Analytics",
    };
  },
  competitive: (s) => {
    const score = s.scores.competitive;
    const tracked = s.perDimension.competitive.trackedCompetitors;
    if (tracked >= 3 && score >= 60) return null;
    return {
      dimension: "competitive",
      message: tracked === 0
        ? "No competitors tracked. Add 3-5 competitors to see keyword battles + strength comparisons."
        : `Only ${tracked} competitor${tracked === 1 ? "" : "s"} tracked. Add more for a richer competitive view.`,
      impact: tracked === 0 ? "high" : "medium",
      ctaUrl: "/competitors",
      ctaLabel: "Manage Competitors",
    };
  },
  ai: (s) => {
    const score = s.scores.ai;
    if (score >= 70) return null;
    return {
      dimension: "ai",
      message: s.perDimension.ai.totalChecks === 0
        ? "AI Visibility 0 — no queries checked. Run an AI Visibility check to see if LLMs cite your brand."
        : `AI Visibility ${score}/100 · mention rate ${Math.round((s.perDimension.ai.mentionRate ?? 0) * 100)}%. Expand the query set + focus on factual content.`,
      impact: score < 30 ? "high" : score < 60 ? "medium" : "low",
      ctaUrl: "/ai-visibility",
      ctaLabel: "Run AI Visibility",
    };
  },
  maturity: (s) => {
    const score = s.scores.maturity;
    if (score >= 70) return null;
    const missing: string[] = [];
    if (!s.perDimension.maturity.siteTagInstalled) missing.push("Site Tag");
    if (!s.perDimension.maturity.ga4Connected) missing.push("GA4");
    if (!s.perDimension.maturity.gscConnected) missing.push("GSC");
    if (!s.perDimension.maturity.gbpConnected) missing.push("Google Business Profile");
    return {
      dimension: "maturity",
      message: missing.length > 0
        ? `Data Maturity ${score}/100. Connect: ${missing.join(", ")} to complete the tracking stack.`
        : `Data Maturity ${score}/100. Only ${s.perDimension.maturity.daysOfData} days of data collected.`,
      impact: missing.length >= 2 ? "high" : "medium",
      ctaUrl: "/settings/integrations",
      ctaLabel: "Open Integrations",
    };
  },
};

export function recommendationsForBrand(score: DetailedRadarScore): RadarRecommendation[] {
  // Sort dimensions by score ascending (weakest first) and pick the
  // top 3 whose recommender returns a non-null result. Skipping null
  // returns means a fully-healthy brand (every dimension ≥ 70) gets
  // an empty list rather than three noise items.
  const sorted = [...RADAR_DIMENSIONS].sort(
    (a, b) => (score.scores[a.id] ?? 0) - (score.scores[b.id] ?? 0),
  );
  const out: RadarRecommendation[] = [];
  for (const dim of sorted) {
    if (out.length >= 3) break;
    const rec = RECOMMENDATIONS[dim.id](score);
    if (rec) out.push(rec);
  }
  return out;
}

// ─── Platform-wide insights ─────────────────────────────────────
//
// Computes the "weakest / strongest / most improved / needs
// attention" summary surfaced above the top/bottom leaderboards
// when scope is "all brands". Data comes entirely from the scored
// brands array — no extra queries.

export interface PlatformInsights {
  weakestDimension: { id: RadarDimension; label: string; avgScore: number };
  strongestDimension: { id: RadarDimension; label: string; avgScore: number };
  mostImproved: { brandId: string; brandName: string; health: number } | null;
  needsAttention: { brandId: string; brandName: string; health: number } | null;
  distribution: { "0-20": number; "20-40": number; "40-60": number; "60-80": number; "80-100": number };
}

export function computePlatformInsights(
  brands: RadarScore[],
  averages: Record<RadarDimension, number>,
): PlatformInsights {
  const dimsSorted = [...RADAR_DIMENSIONS].sort(
    (a, b) => (averages[a.id] ?? 0) - (averages[b.id] ?? 0),
  );
  const weakest = dimsSorted[0];
  const strongest = dimsSorted[dimsSorted.length - 1];

  // "Most improved" + "needs attention" — without a snapshot history
  // we can't compute a delta, so:
  //   mostImproved  → the HIGHEST-scoring brand (proxy for "doing best")
  //   needsAttention → the LOWEST-scoring brand
  // When PlatformSnapshot starts carrying radar averages the proper
  // month-over-month delta replaces these proxies without an API
  // contract change.
  let bestBrand: RadarScore | null = null;
  let worstBrand: RadarScore | null = null;
  for (const b of brands) {
    if (!bestBrand || b.health > bestBrand.health) bestBrand = b;
    if (!worstBrand || b.health < worstBrand.health) worstBrand = b;
  }

  const distribution = { "0-20": 0, "20-40": 0, "40-60": 0, "60-80": 0, "80-100": 0 };
  for (const b of brands) {
    const h = b.health;
    if (h < 20) distribution["0-20"]++;
    else if (h < 40) distribution["20-40"]++;
    else if (h < 60) distribution["40-60"]++;
    else if (h < 80) distribution["60-80"]++;
    else distribution["80-100"]++;
  }

  return {
    weakestDimension: { id: weakest.id, label: weakest.label, avgScore: averages[weakest.id] ?? 0 },
    strongestDimension: { id: strongest.id, label: strongest.label, avgScore: averages[strongest.id] ?? 0 },
    mostImproved: bestBrand ? { brandId: bestBrand.brandId, brandName: bestBrand.brandName, health: bestBrand.health } : null,
    needsAttention: worstBrand ? { brandId: worstBrand.brandId, brandName: worstBrand.brandName, health: worstBrand.health } : null,
    distribution,
  };
}

/** Score every brand in parallel. Returns the per-brand list +
 *  per-dimension averages. */
export async function scoreAllBrands(): Promise<{
  brands: RadarScore[];
  averages: Record<RadarDimension, number>;
  averageHealth: number;
}> {
  const brands = await prisma.brand.findMany({ where: { isDemo: false }, select: { id: true } });
  // Score in parallel but cap concurrency loosely via Promise.all —
  // 25 brands × 8 small queries each = 200 round-trips, fine for
  // current platform sizes. Larger platforms should chunk.
  const scored = (await Promise.all(brands.map((b) => scoreBrand(b.id))))
    .filter((s): s is RadarScore => s != null);

  if (scored.length === 0) {
    const empty = Object.fromEntries(RADAR_DIMENSIONS.map((d) => [d.id, 0])) as Record<RadarDimension, number>;
    return { brands: [], averages: empty, averageHealth: 0 };
  }

  const averages = Object.fromEntries(
    RADAR_DIMENSIONS.map((d) => [
      d.id,
      Math.round(scored.reduce((s, b) => s + b.scores[d.id], 0) / scored.length),
    ]),
  ) as Record<RadarDimension, number>;
  const averageHealth = Math.round(
    scored.reduce((s, b) => s + b.health, 0) / scored.length,
  );

  return { brands: scored, averages, averageHealth };
}
