/**
 * POST /api/ai-strategist/chat
 * ─────────────────────────────────
 * Chat endpoint for the AI Strategist. Sends the user's message + full
 * conversation history to Claude along with the active analysis object as
 * grounded context. Claude may respond with plain text, or wrap a JSX
 * component in <artifact> tags which the frontend renders in a sandboxed
 * iframe via ArtifactRenderer.
 *
 * Request body:
 *   {
 *     brandId: string,
 *     message: string,
 *     history: Array<{ role: "user" | "assistant"; content: string }>,
 *     analysis?: Analysis | null,   // current loaded analysis for grounding
 *     mode?: "strategist" | "client",
 *   }
 *
 * Response:
 *   {
 *     text: string,
 *     artifacts: Array<{ title: string; type: string; code: string }>,
 *     provider: string,
 *     model: string,
 *     tokensUsed?: number,
 *   }
 */

import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getCurrentUser } from "@/lib/auth";
import { callLlm } from "@/lib/ai-avatar/llm-client";
import { logAiInteraction } from "@/lib/services/ai-interaction-log";
import { trackEvent } from "@/lib/services/platform-telemetry";
import { STRATEGIST_SYSTEM_PROMPT, parseArtifacts } from "@/lib/ai-strategist/artifacts";
import { getEnrichmentContext, formatEnrichmentForPrompt } from "@/lib/services/enrichment-context";
import type { Analysis } from "@/lib/services/analysis-schema";
import { log } from "@/lib/logger";
import { logMemory } from "@/lib/services/client-memory";
import { getSiteTagInstalledAt, siteTagAgeDays } from "@/lib/services/site-tag";
import { countedConversionWhere } from "@/lib/conversions/counted-where";
import { getSessionCount, getGSCMetrics } from "@/lib/services/platform-metrics";
import { buildPaidMediaContext } from "@/lib/services/paid-media-context";
import { buildLlmoContext } from "@/lib/intelligence/ai-strategist-context";

export const dynamic = "force-dynamic";
export const maxDuration = 120;

// Artifact generation can produce long JSX components, so we allow a much
// larger output budget than the default chat endpoint. 12000 tokens fits
// the richest dashboard the system prompt requests (~9 sections incl.
// Strategic Insights + Priority Actions) with headroom for verbose model
// output. Bumped from 8192 after observing mid-tag truncations on multi-
// section artifacts. claude-sonnet-4 supports up to 64k output natively
// so this doesn't need a beta header.
const ARTIFACT_MAX_TOKENS = 12000;

interface ChatRequest {
  brandId: string;
  message: string;
  sessionId?: string;
  history?: Array<{ role: "user" | "assistant"; content: string }>;
  analysis?: Analysis | null;
  analysisState?: "idle" | "running" | "complete" | "failed";
  analysisError?: string;
  mode?: "strategist" | "client";
}

export async function POST(req: Request) {
  try {
    const user = await getCurrentUser();
    if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

    const body = (await req.json()) as ChatRequest;
    const { brandId, message, sessionId, history = [], analysis = null, analysisState = "idle", analysisError, mode = "strategist" } = body;

    if (!brandId || !message?.trim()) {
      return NextResponse.json({ error: "brandId and message are required" }, { status: 400 });
    }

    // Verify access to the brand
    const brand = await prisma.brand.findUnique({
      where: { id: brandId },
      include: { organization: { include: { memberships: true } } },
    });
    if (!brand || !brand.organization.memberships.some((m) => m.userId === user.id)) {
      return NextResponse.json({ error: "Access denied" }, { status: 403 });
    }

    // Build the context block — analysis object + real-time data grounds the
    // response so Claude cites real numbers instead of fabricating them.
    // Wrapped in its own try/catch so a transient DB or timeout error returns
    // a clear 200 message to the client rather than a 500 that triggers the
    // page's local buildGroundedReply fallback with misleading KPI output.
    let ctx: Awaited<ReturnType<typeof buildContextBlock>>;
    try {
      ctx = await buildContextBlock(analysis, brand.domain, brand.name, brandId, mode, analysisState);
    } catch (ctxErr) {
      const ctxMsg = ctxErr instanceof Error ? ctxErr.message : "context build failed";
      log.error("api", `AI Strategist buildContextBlock error: ${ctxMsg}`);
      return NextResponse.json({
        text: "I'm having trouble fetching your site data right now. Please try again in a moment.",
        artifacts: [],
        provider: "error",
        model: "error",
        metadata: { dataSource: "unavailable", dateRange: "", analysisState, enrichmentDocs: 0, cachedResponse: false },
        sessionId,
      });
    }
    const contextBlock = ctx.text;
    log.info("api", "AI Strategist chat request", { metadata: { brandId, analysisState, analysisLoaded: !!analysis, dataSource: ctx.metadata.dataSource, cached: ctx.metadata.cachedResponse, enrichmentDocs: ctx.metadata.enrichmentDocs, messageCount: history.length + 1 } });

    // Compose the message array: system + prior history + current user message
    // with context appended. We only attach the heavy analysis context to the
    // FIRST user turn to keep token usage sane on longer conversations.
    // If the user's message contains dashboard/visualization keywords, prepend
    // a directive so the model knows to generate JSX, not prose.
    const artifactHint = /\b(build|create|dashboard|visualize|chart|show me|compare)\b/i.test(message)
      ? "[RESPOND WITH A JSX ARTIFACT — use <artifact> tags] "
      : "";

    // Cap history to last 6 messages to keep context size manageable.
    // Long histories inflate the input token count and slow response time.
    const recentHistory = history.slice(-4);

    const messages = [
      { role: "system" as const, content: STRATEGIST_SYSTEM_PROMPT },
      ...recentHistory.map((m) => ({ role: m.role, content: m.content })),
      { role: "user" as const, content: `${contextBlock}\n\n${artifactHint}User question: ${message.trim()}` },
    ];

    const result = await callLlm(
      messages,
      () =>
        `I need Anthropic API access to generate strategic analysis for ${brand.domain}. Add ANTHROPIC_API_KEY to your environment to enable live responses.`,
      { maxTokens: ARTIFACT_MAX_TOKENS },
    );

    const parsed = parseArtifacts(result.reply);

    // Admin observability — fire-and-forget writes feed
    // /admin/ai-monitor + engagement dashboards.
    logAiInteraction({
      userId: user.id,
      brandId,
      feature: "ai_strategist",
      userPrompt: message.trim(),
      systemPrompt: STRATEGIST_SYSTEM_PROMPT,
      aiResponse: result.reply,
      model: result.model,
      tokensUsed: result.tokensUsed ?? null,
      brandDomain: brand.domain,
    });
    trackEvent({
      userId: user.id,
      brandId,
      action: "ai_strategist_query",
      category: "ai",
      metadata: {
        promptLength: message.length,
        responseLength: parsed.text.length,
        artifactCount: parsed.artifacts.length,
        provider: result.provider,
        model: result.model,
        tokens: result.tokensUsed,
      },
      request: req,
    });

    log.info("api", `AI Strategist chat response: ${parsed.artifacts.length} artifact(s), ${result.tokensUsed ?? "?"} tokens`, {
      metadata: {
        brandId,
        provider: result.provider,
        model: result.model,
        tokens: result.tokensUsed,
        artifactCount: parsed.artifacts.length,
        textLength: parsed.text.length,
      },
    });

    logMemory({ brandId, userId: user.id, category: "chat", action: "generated", summary: `AI Strategist chat: "${message.slice(0, 80)}"`, aiContext: `AI chat: ${parsed.text.slice(0, 150)}` });

    // Persist messages to the database if a sessionId was provided
    if (sessionId) {
      try {
        await prisma.strategistMessage.createMany({
          data: [
            { sessionId, role: "user", content: message.trim() },
            { sessionId, role: "assistant", content: parsed.text, artifacts: parsed.artifacts.length > 0 ? (parsed.artifacts as unknown as object) : undefined },
          ],
        });
        // Auto-title from first user message if still default
        const session = await prisma.strategistSession.findUnique({ where: { id: sessionId }, select: { title: true } });
        if (session?.title === "New Chat") {
          await prisma.strategistSession.update({
            where: { id: sessionId },
            data: { title: generateSessionTitle(message) },
          });
        } else {
          // Touch updatedAt
          await prisma.strategistSession.update({ where: { id: sessionId }, data: {} });
        }
      } catch { /* message persistence is best-effort */ }
    }

    return NextResponse.json({
      text: parsed.text,
      artifacts: parsed.artifacts,
      provider: result.provider,
      model: result.model,
      tokensUsed: result.tokensUsed,
      metadata: ctx.metadata,
      sessionId,
    });
  } catch (err) {
    const msg = err instanceof Error ? err.message : "Chat request failed";
    log.error("api", `AI Strategist chat error: ${msg}`);
    // Return 200 to prevent page's buildGroundedReply fallback from firing with misleading KPI output
    return NextResponse.json({
      text: "I encountered an error processing your request. Please try again.",
      artifacts: [],
      provider: "error",
      model: "error",
      metadata: { dataSource: "unavailable", dateRange: "", analysisState: "idle", enrichmentDocs: 0, cachedResponse: false },
    });
  }
}

import { getGscCache, setGscCache } from "@/lib/stores/gsc-cache";
import { getTopQueries } from "@/lib/services/gsc-sync";

// ─── Session title generator ─────────────────────────────────────────────────

/**
 * Produce a short, ChatGPT-style session title from the user's first message.
 * Strips conversational filler prefixes ("Build me…", "What are the…",
 * "Can you show me…"), normalises casing, and caps length at ~50 chars
 * without cutting mid-word. Preserves acronyms like SEO, KPI, CTR, GA4.
 */
function generateSessionTitle(raw: string): string {
  let text = raw.trim().replace(/\s+/g, " ").replace(/[?.!,;:]+$/, "");
  if (!text) return "New Chat";

  // Strip common filler prefixes one at a time. Order matters — run the
  // longer multi-word patterns before the shorter single-word ones.
  const fillers: RegExp[] = [
    /^(please\s+)?can\s+you\s+(please\s+)?/i,
    /^(please|kindly)\s+/i,
    /^i\s+(want|need|would\s+like)\s+(to\s+(see\s+)?)?(a\s+|an\s+|the\s+)?/i,
    /^let('?s|\s+us)\s+/i,
    /^build\s+(me\s+)?(a\s+|an\s+|the\s+)?/i,
    /^show\s+(me\s+)?(a\s+|an\s+|the\s+)?/i,
    /^tell\s+me\s+(about\s+|more\s+about\s+)?/i,
    /^give\s+me\s+(a\s+|an\s+|the\s+)?/i,
    /^create\s+(me\s+)?(a\s+|an\s+|the\s+)?/i,
    /^generate\s+(me\s+)?(a\s+|an\s+|the\s+)?/i,
    /^analyz[es]+\s+(the\s+)?/i,
    /^visuali[sz]e\s+(the\s+)?/i,
    /^compare\s+(the\s+)?/i,
    /^explain\s+(the\s+|how\s+|why\s+)?/i,
    /^what('?s| is| are)(\s+the)?\s+/i,
    /^how\s+(do\s+i\s+|can\s+i\s+|should\s+i\s+|does\s+)?/i,
    /^why\s+(does\s+|is\s+|are\s+)?/i,
    /^which\s+(of\s+the\s+)?/i,
    /^do\s+i\s+/i,
    /^should\s+i\s+/i,
  ];
  for (const re of fillers) text = text.replace(re, "");
  text = text.trim();
  if (!text) return raw.trim().slice(0, 50);

  // Truncate at word boundary if too long
  const MAX = 50;
  if (text.length > MAX) {
    const clipped = text.slice(0, MAX);
    const lastSpace = clipped.lastIndexOf(" ");
    text = (lastSpace > 20 ? clipped.slice(0, lastSpace) : clipped) + "…";
  }

  // Title-case while preserving all-caps acronyms (KPI, SEO, CTR, GA4, GSC).
  const titled = text.split(" ").map((word) => {
    if (!word) return word;
    // Preserve already all-uppercase acronyms of 2+ chars (letters/digits)
    if (/^[A-Z0-9]{2,}$/.test(word)) return word;
    return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
  }).join(" ");

  return titled || "New Chat";
}

// ─── Context builder ─────────────────────────────────────────────────────────

interface ContextResult {
  text: string;
  metadata: { dataSource: string; dateRange: string; analysisState: string; enrichmentDocs: number; cachedResponse: boolean };
}

type PageAgg = { url: string; clicks: number; impressions: number; prevClicks: number; prevImpressions: number };

function pathOf(url: string): string {
  try { return new URL(url).pathname; } catch { return url; }
}

const MIN_DECLINING_CLICKS = 20; // filter noise from declining/growing lists

async function fetchLiveGscContext(brandId: string): Promise<{ text: string | null; cached: boolean }> {
  const cached = getGscCache(brandId);
  if (cached) return { text: cached, cached: true };

  try {
    const d30 = new Date(Date.now() - 30 * 86400000);
    const d60 = new Date(Date.now() - 60 * 86400000);

    const [curPages, prevPages, curAgg] = await Promise.all([
      prisma.gscPage.findMany({ where: { brandId, date: { gte: d30 } }, select: { pageUrl: true, clicks: true, impressions: true } }),
      prisma.gscPage.findMany({ where: { brandId, date: { gte: d60, lt: d30 } }, select: { pageUrl: true, clicks: true, impressions: true } }),
      prisma.gscPage.groupBy({ by: ["brandId"], where: { brandId, date: { gte: d30 } }, _sum: { clicks: true, impressions: true }, _avg: { position: true, ctr: true } }),
    ]);

    if (!curAgg[0]) return { text: null, cached: false };

    const pageMap = new Map<string, PageAgg>();
    for (const r of curPages) {
      const e = pageMap.get(r.pageUrl);
      if (e) { e.clicks += r.clicks; e.impressions += r.impressions; }
      else pageMap.set(r.pageUrl, { url: r.pageUrl, clicks: r.clicks, impressions: r.impressions, prevClicks: 0, prevImpressions: 0 });
    }
    for (const r of prevPages) {
      const e = pageMap.get(r.pageUrl);
      if (e) { e.prevClicks += r.clicks; e.prevImpressions += r.impressions; }
      else pageMap.set(r.pageUrl, { url: r.pageUrl, clicks: 0, impressions: 0, prevClicks: r.clicks, prevImpressions: r.impressions });
    }
    const all = [...pageMap.values()];
    const topByClicks = [...all].sort((a, b) => b.clicks - a.clicks).slice(0, 15);
    const declining = [...all].filter(p => p.prevClicks >= MIN_DECLINING_CLICKS).sort((a, b) => (a.clicks - a.prevClicks) - (b.clicks - b.prevClicks)).filter(p => p.clicks < p.prevClicks).slice(0, 10);
    const growing = [...all].filter(p => p.clicks >= MIN_DECLINING_CLICKS).sort((a, b) => (b.clicks - b.prevClicks) - (a.clicks - a.prevClicks)).filter(p => p.clicks > p.prevClicks).slice(0, 10);

    const fmtPage = (p: PageAgg, i: number) => {
      const delta = p.clicks - p.prevClicks;
      const sign = delta >= 0 ? "+" : "";
      return `${i + 1}. ${pathOf(p.url)} — ${p.clicks} clicks (prev: ${p.prevClicks}, ${sign}${delta}), ${p.impressions} imp`;
    };

    let revenueBlock: string;
    const revSnaps = await prisma.metricSnapshot.findMany({
      where: { brandId, source: "ga4", date: { gte: d30 }, revenue: { gt: 0 } },
      select: { revenue: true, conversions: true },
    }).catch(() => []);
    const prevRevSnaps = await prisma.metricSnapshot.findMany({
      where: { brandId, source: "ga4", date: { gte: d60, lt: d30 }, revenue: { gt: 0 } },
      select: { revenue: true, conversions: true },
    }).catch(() => []);

    if (revSnaps.length > 0) {
      const curRev = revSnaps.reduce((s, r) => s + r.revenue, 0);
      const curConv = revSnaps.reduce((s, r) => s + r.conversions, 0);
      const prevRev = prevRevSnaps.reduce((s, r) => s + r.revenue, 0);
      const prevConv = prevRevSnaps.reduce((s, r) => s + r.conversions, 0);
      const revChange = prevRev > 0 ? Math.round(((curRev - prevRev) / prevRev) * 100) : 0;
      revenueBlock = [
        `REVENUE DATA (Source: GA4 Ecommerce):`,
        `Current period: $${curRev.toLocaleString()} total revenue, ${curConv} conversions, $${curConv > 0 ? Math.round(curRev / curConv).toLocaleString() : "0"} avg order`,
        `Previous period: $${prevRev.toLocaleString()} total revenue, ${prevConv} conversions`,
        prevRev > 0 ? `Change: ${revChange > 0 ? "+" : ""}${revChange}% revenue` : "",
      ].filter(Boolean).join("\n");
    } else {
      revenueBlock = `Revenue data: NOT configured in GA4. If asked about revenue, tell user to set up GA4 ecommerce tracking.`;
    }

    const a = curAgg[0];
    const dateRange = `${d30.toISOString().split("T")[0]} to ${new Date().toISOString().split("T")[0]}`;
    const lines = [
      `DATA SOURCE: Google Search Console + GA4 (last 30 days, real-time query)`,
      `Period: ${dateRange}`,
      `Total Organic Clicks: ${(a._sum.clicks ?? 0).toLocaleString()}`,
      `Total Impressions: ${(a._sum.impressions ?? 0).toLocaleString()}`,
      `Avg Position: ${(a._avg.position ?? 0).toFixed(1)}`,
      `Avg CTR: ${((a._avg.ctr ?? 0) * 100).toFixed(2)}%`,
      ``, revenueBlock, ``,
      `TOP 15 PAGES BY CLICKS:`,
      ...topByClicks.map(fmtPage),
      ``,
      `DECLINING PAGES (min ${MIN_DECLINING_CLICKS} prev clicks, biggest drops):`,
      ...(declining.length > 0 ? declining.map(fmtPage) : ["No significant declines detected."]),
      ``,
      `GROWING PAGES (min ${MIN_DECLINING_CLICKS} current clicks, biggest gains):`,
      ...(growing.length > 0 ? growing.map(fmtPage) : ["No significant growth detected."]),
    ];

    const result = lines.join("\n");
    setGscCache(brandId, result);
    return { text: result, cached: false };
  } catch { return { text: null, cached: false }; }
}

async function fetchKeywordContext(brandId: string): Promise<string | null> {
  try {
    const tracked = await prisma.trackedKeyword.findMany({
      where: { brandId, isActive: true },
      include: { rankings: { orderBy: { checkedAt: "desc" }, take: 1 } },
      take: 30,
    });
    if (tracked.length === 0) return null;

    const trackedSet = new Set(tracked.map((k) => k.keyword.toLowerCase()));
    const lines: string[] = [
      "TRACKED KEYWORDS (Rank Tracker):",
      ...tracked.map((k) => {
        const r = k.rankings[0];
        const pos = r?.position != null ? `pos ${r.position}` : "not ranked";
        const prev = r?.previousPosition != null ? ` (prev: ${r.previousPosition})` : "";
        const vol = r?.searchVolume ? `, ~${r.searchVolume} vol` : "";
        return `  - "${k.keyword}" → ${pos}${prev}${vol}${k.tags.length > 0 ? ` [${k.tags.join(",")}]` : ""}`;
      }),
    ];

    const gscQueries = await getTopQueries(brandId, 50, 28);
    const opportunities = gscQueries
      .filter((q) => !trackedSet.has(q.query.toLowerCase()) && q.impressions >= 100)
      .slice(0, 10);
    if (opportunities.length > 0) {
      lines.push("", "KEYWORD OPPORTUNITIES (high-impression GSC queries NOT being tracked):");
      for (const q of opportunities) {
        lines.push(`  - "${q.query}" — pos ${q.position}, ${q.impressions.toLocaleString()} imp, ${q.ctr}% CTR`);
      }
      lines.push("When relevant, recommend the user add these to their Rank Tracker.");
    }

    return lines.join("\n");
  } catch {
    return null;
  }
}

async function buildSiteTagContext(brandId: string, days = 28): Promise<string | null> {
  try {
    const site = await prisma.trackedSite.findUnique({
      where: { brandId },
      select: { id: true },
    });
    if (!site) return null;

    const installedAt = await getSiteTagInstalledAt(brandId).catch(() => null);
    const start = new Date(Date.now() - days * 86_400_000);

    const EVENT_TYPES = [
      "page_view",
      "form_start", "form_submit",
      "click_to_call", "sms_click",
      "booking_widget_opened", "booking_submit", "booking_confirmed",
      "outbound_click",
    ] as const;

    const [eventRows, sessionCount, convTotal, convByType, convPageRows] = await Promise.all([
      prisma.trackedEvent.groupBy({
        by: ["eventType"],
        where: {
          trackedSiteId: site.id,
          eventType: { in: [...EVENT_TYPES] },
          timestamp: { gte: start },
        },
        _count: true,
      }).catch(() => [] as Array<{ eventType: string; _count: number }>),
      getSessionCount(brandId, start, { trackedSiteId: site.id }),
      prisma.siteConversion.count({
        where: countedConversionWhere(brandId, { since: start }),
      }).catch(() => 0),
      prisma.siteConversion.groupBy({
        by: ["conversionType"],
        where: countedConversionWhere(brandId, { since: start }),
        _count: true,
      }).catch(() => [] as Array<{ conversionType: string; _count: number }>),
      prisma.siteConversion.groupBy({
        by: ["pageUrl"],
        where: countedConversionWhere(brandId, { since: start }),
        _count: true,
      }).catch(() => [] as Array<{ pageUrl: string; _count: number }>),
    ]);

    const counts: Record<string, number> = {};
    for (const r of eventRows) {
      counts[r.eventType] = typeof r._count === "number" ? r._count : 0;
    }

    const sessions = sessionCount;
    const pageviews = counts["page_view"] ?? 0;
    const convRate = sessions > 0 ? Math.round((convTotal / sessions) * 1000) / 10 : 0;

    const topPages = convPageRows
      .map((r) => ({ pageUrl: r.pageUrl, count: typeof r._count === "number" ? r._count : 0 }))
      .filter((r) => r.count > 0)
      .sort((a, b) => b.count - a.count)
      .slice(0, 8);

    const lines: string[] = [];
    lines.push(`Source: meSEO Site Tag — cookieless, first-party server-side endpoint. ${days}-day window.`);
    if (installedAt) {
      const ageDays = siteTagAgeDays(installedAt);
      const maturityPct = Math.max(0, Math.min(100, Math.round((ageDays / 30) * 100)));
      lines.push(`Installed: ${installedAt.toISOString().split("T")[0]} — ${ageDays} day${ageDays === 1 ? "" : "s"} old (${maturityPct}% of 30-day maturity).`);
      if (ageDays < 7) lines.push("Maturity guardrail: do NOT make day-over-day, week-over-week, or month-over-month comparisons — there is not enough Site Tag data yet.");
      else if (ageDays < 14) lines.push("Maturity guardrail: day-over-day comparisons OK. Do NOT make week-over-week or month-over-month comparisons yet.");
      else if (ageDays < 30) lines.push("Maturity guardrail: day-over-day and week-over-week comparisons OK. Do NOT make month-over-month comparisons yet.");
    }

    lines.push("");
    lines.push("Traffic:");
    lines.push(`  Page views: ${pageviews.toLocaleString()}`);
    lines.push(`  Sessions:   ${sessions.toLocaleString()}`);

    lines.push("");
    lines.push("Engagement signals (raw TrackedEvent counts):");
    lines.push(`  form_start:             ${(counts["form_start"] ?? 0).toLocaleString()}`);
    lines.push(`  form_submit:            ${(counts["form_submit"] ?? 0).toLocaleString()}`);
    lines.push(`  click_to_call:          ${(counts["click_to_call"] ?? 0).toLocaleString()}`);
    lines.push(`  sms_click:              ${(counts["sms_click"] ?? 0).toLocaleString()}`);
    lines.push(`  booking_widget_opened:  ${(counts["booking_widget_opened"] ?? 0).toLocaleString()}`);
    lines.push(`  booking_submit:         ${(counts["booking_submit"] ?? 0).toLocaleString()}`);
    lines.push(`  booking_confirmed:      ${(counts["booking_confirmed"] ?? 0).toLocaleString()}`);
    lines.push(`  outbound_click:         ${(counts["outbound_click"] ?? 0).toLocaleString()}`);

    lines.push("");
    lines.push("Conversions (server-reconciled from raw events into the SiteConversion table):");
    lines.push(`  Total conversions: ${convTotal.toLocaleString()}`);
    lines.push(`  Conversion rate:   ${convRate}%  (conversions ÷ sessions)`);
    if (convByType.length > 0) {
      const breakdown = convByType
        .map((r) => `${r.conversionType}=${typeof r._count === "number" ? r._count : 0}`)
        .sort()
        .join(", ");
      lines.push(`  By type:           ${breakdown}`);
    }

    if (topPages.length > 0) {
      lines.push("");
      lines.push("Top converting pages:");
      topPages.forEach((r, i) => {
        const page = r.pageUrl.length > 70 ? r.pageUrl.slice(0, 67) + "..." : r.pageUrl;
        lines.push(`  ${i + 1}. ${page} — ${r.count} conversion${r.count === 1 ? "" : "s"}`);
      });
    }

    lines.push("");
    lines.push("Interpretation notes:");
    lines.push("- Site Tag is a first-party server-side endpoint and bypasses most ad-blockers / ITP / consent cookie rejections. It captures visitors GA4 loses.");
    lines.push("- Once Site Tag is 30+ days mature, the Site-Tag-sessions − GA4-sessions gap quantifies the ad-blocker / consent-opt-out impact on GA4 measurement. That delta is a strategic INSIGHT, not a tracking bug.");
    lines.push("- Use these numbers for funnel analysis (page_view → form_start → form_submit), micro-conversion attribution (outbound clicks, call / SMS clicks as soft conversions), and conversion-rate calculations the GA4 data alone cannot support.");
    lines.push("- Top converting pages are the pages the user's own visitors convert on — prioritise SEO / CRO investments there.");

    return lines.join("\n");
  } catch {
    return null;
  }
}

async function buildDailyGscContext(brandId: string, days = 28): Promise<string | null> {
  try {
    const start = new Date(Date.now() - days * 86_400_000);

    const pageRows = await prisma.gscPage.groupBy({
      by: ["date"],
      where: { brandId, date: { gte: start } },
      _sum: { clicks: true, impressions: true },
      _avg: { position: true },
      orderBy: { date: "asc" },
    });

    if (pageRows.length > 0) {
      const compact = pageRows.map((r) => {
        const clicks = r._sum.clicks ?? 0;
        const imps = r._sum.impressions ?? 0;
        const pos = r._avg.position ?? 0;
        const ctr = imps > 0 ? clicks / imps : 0;
        return {
          d: r.date.toISOString().split("T")[0],
          c: clicks,
          i: imps,
          p: Math.round(pos * 10) / 10,
          ctr: Math.round(ctr * 10000) / 10000,
        };
      });
      return JSON.stringify(compact);
    }

    const snapRows = await prisma.metricSnapshot.findMany({
      where: { brandId, source: "gsc", date: { gte: start } },
      orderBy: { date: "asc" },
      select: { date: true, organicClicks: true, impressions: true, avgPosition: true, ctr: true },
    });
    if (snapRows.length === 0) return null;
    const compact = snapRows.map((r) => ({
      d: r.date.toISOString().split("T")[0],
      c: r.organicClicks,
      i: r.impressions,
      p: Math.round(r.avgPosition * 10) / 10,
      ctr: Math.round(r.ctr * 10000) / 10000,
    }));
    return JSON.stringify(compact);
  } catch {
    return null;
  }
}

interface DailyConversionBlock {
  inner: string;
  attrs: string;
}

async function buildDailyConversionContext(brandId: string, days = 28): Promise<DailyConversionBlock | null> {
  try {
    const start = new Date(Date.now() - days * 86_400_000);

    const convRows = await prisma.siteConversion.findMany({
      where: { brandId, timestamp: { gte: start }, isDuplicateOf: null, invalidatedReason: null },
      orderBy: { timestamp: "asc" },
      select: { timestamp: true },
    });
    if (convRows.length === 0) return null;

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

    const [sessionRows, pageviewRows] = site
      ? await Promise.all([
          prisma.trackedSession.findMany({
            where: { trackedSiteId: site.id, startedAt: { gte: start } },
            select: { startedAt: true },
          }).catch(() => []),
          prisma.trackedEvent.findMany({
            where: { trackedSiteId: site.id, eventType: "page_view", timestamp: { gte: start } },
            select: { timestamp: true },
          }).catch(() => []),
        ])
      : [[], []];

    const convByDay = new Map<string, number>();
    for (const r of convRows) {
      const d = r.timestamp.toISOString().split("T")[0];
      convByDay.set(d, (convByDay.get(d) ?? 0) + 1);
    }
    const sessionsByDay = new Map<string, number>();
    for (const r of sessionRows) {
      const d = r.startedAt.toISOString().split("T")[0];
      sessionsByDay.set(d, (sessionsByDay.get(d) ?? 0) + 1);
    }
    const pageviewsByDay = new Map<string, number>();
    for (const r of pageviewRows) {
      const d = r.timestamp.toISOString().split("T")[0];
      pageviewsByDay.set(d, (pageviewsByDay.get(d) ?? 0) + 1);
    }

    const allDays = Array.from(new Set([
      ...convByDay.keys(),
      ...sessionsByDay.keys(),
      ...pageviewsByDay.keys(),
    ])).sort();
    const compact = allDays.map((d) => {
      const conv = convByDay.get(d) ?? 0;
      const sessions = sessionsByDay.get(d) ?? 0;
      const pageviews = pageviewsByDay.get(d) ?? 0;
      const rate = sessions > 0 ? Math.round((conv / sessions) * 1000) / 10 : 0;
      return { d, conv, sessions, rate, pageviews };
    });

    const installedAt = await getSiteTagInstalledAt(brandId);
    const daysTotal = 30;
    const attrParts: string[] = [];
    if (installedAt) {
      const daysCollected = siteTagAgeDays(installedAt);
      const maturityPct = Math.max(0, Math.min(100, Math.round((daysCollected / daysTotal) * 100)));
      attrParts.push(`installedAt="${installedAt.toISOString().split("T")[0]}"`);
      attrParts.push(`daysCollected="${daysCollected}"`);
      attrParts.push(`daysTotal="${daysTotal}"`);
      attrParts.push(`maturity="${maturityPct}%"`);
    }
    const attrs = attrParts.length > 0 ? ` ${attrParts.join(" ")}` : "";

    return { inner: JSON.stringify(compact), attrs };
  } catch {
    return null;
  }
}

async function buildContentLanguageSplit(brandId: string, days = 28): Promise<string | null> {
  try {
    const start = new Date(Date.now() - days * 86_400_000);
    const [spanish, english] = await Promise.all([
      prisma.gscPage.aggregate({
        where: { brandId, date: { gte: start }, pageUrl: { contains: "/es/" } },
        _sum: { clicks: true, impressions: true },
      }),
      prisma.gscPage.aggregate({
        where: { brandId, date: { gte: start }, NOT: { pageUrl: { contains: "/es/" } } },
        _sum: { clicks: true, impressions: true },
      }),
    ]);
    const esClicks = spanish._sum.clicks ?? 0;
    const enClicks = english._sum.clicks ?? 0;
    const esImps = spanish._sum.impressions ?? 0;
    const enImps = english._sum.impressions ?? 0;
    const total = esClicks + enClicks;
    if (total === 0) return null;
    return JSON.stringify({
      spanish: { clicks: esClicks, impressions: esImps },
      english: { clicks: enClicks, impressions: enImps },
      spanishPctOfClicks: Math.round((esClicks / total) * 1000) / 10,
      windowDays: days,
    });
  } catch {
    return null;
  }
}

async function buildTechHealthContext(brandId: string): Promise<string | null> {
  try {
    const [audit, perf, deadPageCount] = await Promise.all([
      prisma.technicalAudit.findFirst({
        where: { brandId, status: "completed" },
        orderBy: { createdAt: "desc" },
        include: { issues: { where: { status: "open" }, select: { severity: true } } },
      }),
      prisma.performanceAudit.findFirst({
        where: { brandId, status: "completed", device: "mobile" },
        orderBy: { createdAt: "desc" },
        include: { pages: { select: { performance: true, lcpMs: true, clsVal: true, inpMs: true } } },
      }),
      prisma.deadPage.count({ where: { brandId, status: "open" } }),
    ]);

    if (!audit && !perf && deadPageCount === 0) return null;

    const auditBlock = audit ? {
      crawlHealth: audit.crawlHealth,
      completedAt: audit.completedAt?.toISOString() ?? audit.createdAt.toISOString(),
      openIssues: audit.issues.length,
      criticalIssues: audit.issues.filter((i) => i.severity === "critical").length,
      highIssues: audit.issues.filter((i) => i.severity === "high").length,
      mediumIssues: audit.issues.filter((i) => i.severity === "medium").length,
    } : null;

    const perfBlock = perf && perf.pages.length > 0 ? {
      avgMobilePerformance: Math.round(perf.pages.reduce((s, p) => s + p.performance, 0) / perf.pages.length),
      pagesAudited: perf.pages.length,
      completedAt: perf.completedAt?.toISOString() ?? perf.createdAt.toISOString(),
      lcpPassing: perf.pages.filter((p) => p.lcpMs > 0 && p.lcpMs <= 2500).length,
      clsPassing: perf.pages.filter((p) => p.clsVal <= 0.1).length,
      inpPassing: perf.pages.filter((p) => p.inpMs > 0 && p.inpMs <= 200).length,
    } : null;

    return JSON.stringify({
      technicalAudit: auditBlock,
      performance: perfBlock,
      deadPagesOpen: deadPageCount,
    });
  } catch {
    return null;
  }
}

async function buildCompetitorContext(brandId: string): Promise<string | null> {
  try {
    const twoWeeksAgo = new Date(Date.now() - 14 * 86_400_000);
    const competitors = await prisma.competitor.findMany({
      where: { brandId, isActive: true },
      select: {
        name: true,
        domain: true,
        strongestPage: true,
        whyWinning: true,
        snapshots: {
          orderBy: { crawledAt: "desc" },
          take: 1,
          select: {
            movement: true, threat: true, rankChange: true, aiMentionChange: true,
            totalPages: true, newPages: true, removedPages: true,
          },
        },
        pages: {
          where: { firstSeenAt: { gte: twoWeeksAgo } },
          orderBy: { firstSeenAt: "desc" },
          take: 3,
          select: { url: true, title: true, firstSeenAt: true },
        },
      },
      take: 5,
    });
    if (competitors.length === 0) return null;

    const compact = competitors.map((c) => ({
      name: c.name,
      domain: c.domain,
      strongestPage: c.strongestPage,
      whyWinning: c.whyWinning,
      latestSnapshot: c.snapshots[0] ? {
        movement: c.snapshots[0].movement,
        threat: c.snapshots[0].threat,
        rankChange: c.snapshots[0].rankChange,
        totalPages: c.snapshots[0].totalPages,
        newPages: c.snapshots[0].newPages,
      } : null,
      recentlyPublished: c.pages.map((p) => ({
        url: p.url,
        title: p.title,
        firstSeenAt: p.firstSeenAt.toISOString(),
      })),
    }));
    return JSON.stringify(compact);
  } catch {
    return null;
  }
}

interface TrackingHealth {
  status:
    | "healthy"
    | "ga4_not_connected"
    | "critical_ga4_zero"
    | "site_tag_immature_no_comparison"
    | "warning_site_tag_low_capture";
  message: string;
}

async function buildTrackingHealth(
  brandId: string,
  analysis: Analysis | null,
): Promise<TrackingHealth> {
  const installedAt = await getSiteTagInstalledAt(brandId).catch(() => null);
  const siteTagAge = installedAt ? siteTagAgeDays(installedAt) : 0;
  const isImmature = !!installedAt && siteTagAge < 30;

  let ga4Sessions: number | null = null;
  let ga4Connected = false;
  if (analysis) {
    const sessionsKpi = analysis.kpis.find((k) => /^sessions$/i.test(k.label));
    if (sessionsKpi) {
      const parsed = sessionsKpi.rawValue ?? Number(String(sessionsKpi.value).replace(/[^\d.-]/g, ""));
      ga4Sessions = Number.isFinite(parsed) ? parsed : null;
      ga4Connected = analysis.sources.some((s) => s.id === "ga4" && s.status === "available");
    }
  }
  if (ga4Sessions == null) {
    const since = new Date(Date.now() - 28 * 86_400_000);
    const agg = await prisma.metricSnapshot.aggregate({
      where: { brandId, source: "ga4", date: { gte: since } },
      _sum: { sessions: true },
    }).catch(() => null);
    if (agg) {
      ga4Sessions = agg._sum.sessions ?? 0;
      ga4Connected = (agg._sum.sessions ?? 0) > 0;
    }
  }

  const since = new Date(Date.now() - 28 * 86_400_000);
  const gscData = await getGSCMetrics(brandId, since).catch(() => null);
  const gscClicks = gscData?.clicks ?? 0;

  if (!ga4Connected && ga4Sessions == null) {
    return {
      status: "ga4_not_connected",
      message: "GA4 is not connected for this brand. GSC is the source of truth for traffic metrics. Do not flag missing GA4 data as a tracking failure.",
    };
  }
  if (ga4Connected && ga4Sessions === 0 && gscClicks > 0) {
    return {
      status: "critical_ga4_zero",
      message: `GA4 is connected but returning 0 sessions while GSC reports ${gscClicks.toLocaleString()} clicks in the same period. This is a real tracking failure — recommend verifying GA4 property/stream configuration.`,
    };
  }
  if (isImmature) {
    return {
      status: "site_tag_immature_no_comparison",
      message: `Site Tag is ${siteTagAge} day${siteTagAge === 1 ? "" : "s"} old (installed ${installedAt!.toISOString().split("T")[0]}). Site Tag session counts are NOT comparable to GA4 session counts during the baseline-forming period. Do NOT generate any alert about session drops, tracking failure, or mismatched session counts. Do NOT compute percentage differences between Site Tag sessions and GA4 sessions. The lower Site Tag numbers are EXPECTED and NORMAL.`,
    };
  }

  if (installedAt && !isImmature && ga4Sessions != null && ga4Sessions > 0) {
    try {
      const stSessions = await getSessionCount(brandId, since);
      const ratio = stSessions / ga4Sessions;
      if (ratio < 0.1) {
        return {
          status: "warning_site_tag_low_capture",
          message: `Site Tag is mature (${siteTagAge} days) but only captured ${stSessions.toLocaleString()} sessions vs GA4's ${ga4Sessions.toLocaleString()} sessions (${(ratio * 100).toFixed(1)}% capture rate). Recommend an amber WARNING about Site Tag capture rate — verify the snippet is firing on every page.`,
        };
      }
    } catch { /* ignore — fall through to healthy */ }
  }

  return {
    status: "healthy",
    message: "All tracking systems are functioning normally. Do not generate alerts about tracking, session counts, or data integrity.",
  };
}

async function buildContentIntelContext(brandId: string, days = 28): Promise<string | null> {
  try {
    const start = new Date(Date.now() - days * 86_400_000);

    const [convPages, gscPages] = await Promise.all([
      prisma.siteConversion.groupBy({
        by: ["pageUrl"],
        where: { brandId, timestamp: { gte: start }, isDuplicateOf: null, invalidatedReason: null },
        _count: true,
        orderBy: { _count: { pageUrl: "desc" } },
        take: 10,
      }).catch(() => [] as Array<{ pageUrl: string; _count: number | { pageUrl: number } }>),
      prisma.gscPage.groupBy({
        by: ["pageUrl"],
        where: { brandId, date: { gte: start } },
        _sum: { clicks: true, impressions: true },
        orderBy: { _sum: { clicks: "desc" } },
        take: 10,
      }).catch(() => [] as Array<{ pageUrl: string; _sum: { clicks: number | null; impressions: number | null } }>),
    ]);

    if (convPages.length === 0 && gscPages.length === 0) return null;

    const lines: string[] = [`Content Intelligence — top pages over last ${days} days.`];

    if (convPages.length > 0) {
      lines.push("", "TOP CONVERTING PAGES (meSEO Site Tag — server-reconciled conversions):");
      convPages.forEach((r, i) => {
        const url = r.pageUrl.length > 70 ? r.pageUrl.slice(0, 67) + "..." : r.pageUrl;
        const count = typeof r._count === "number" ? r._count : (r._count as { pageUrl: number }).pageUrl ?? 0;
        lines.push(`  ${i + 1}. ${url} — ${count} conversion${count === 1 ? "" : "s"}`);
      });
      lines.push("Use these pages as priority targets for CRO and SEO investment — they are where conversions actually happen.");
    }

    if (gscPages.length > 0) {
      lines.push("", "TOP ORGANIC TRAFFIC PAGES (GSC clicks, last 28 days):");
      gscPages.forEach((r, i) => {
        const url = r.pageUrl.length > 70 ? r.pageUrl.slice(0, 67) + "..." : r.pageUrl;
        const clicks = r._sum?.clicks ?? 0;
        const imp = r._sum?.impressions ?? 0;
        lines.push(`  ${i + 1}. ${url} — ${clicks.toLocaleString()} clicks, ${imp.toLocaleString()} imp`);
      });
      lines.push("Cross-reference with converting pages above — pages with high traffic but no conversions are CRO opportunities.");
    }

    return lines.join("\n");
  } catch {
    return null;
  }
}

async function buildCompetitorIntelContext(brandId: string, days = 28): Promise<string | null> {
  try {
    const start = new Date(Date.now() - days * 86_400_000);
    const competitors = await prisma.competitor.findMany({
      where: { brandId, isActive: true },
      select: {
        name: true,
        domain: true,
        strongestPage: true,
        whyWinning: true,
        pages: {
          orderBy: { firstSeenAt: "desc" },
          take: 15,
          select: { url: true, title: true, firstSeenAt: true },
        },
      },
      take: 5,
    });
    if (competitors.length === 0) return null;

    const brandPages = await prisma.gscPage.groupBy({
      by: ["pageUrl"],
      where: { brandId, date: { gte: start } },
      _sum: { clicks: true },
      orderBy: { _sum: { clicks: "desc" } },
      take: 50,
    }).catch(() => [] as Array<{ pageUrl: string; _sum: { clicks: number | null } }>);

    const brandPaths = new Set(
      brandPages.map((p) => { try { return new URL(p.pageUrl).pathname.toLowerCase(); } catch { return p.pageUrl.toLowerCase(); } })
    );

    const lines: string[] = ["COMPETITOR CONTENT INTELLIGENCE:"];
    for (const c of competitors) {
      lines.push("", `${c.name} (${c.domain})`);
      if (c.strongestPage) lines.push(`  Strongest page: ${c.strongestPage}`);
      if (c.whyWinning) lines.push(`  Why winning: ${c.whyWinning}`);
      const gaps = c.pages.filter((p) => {
        try { return !brandPaths.has(new URL(p.url).pathname.toLowerCase()); } catch { return false; }
      }).slice(0, 5);
      if (gaps.length > 0) {
        lines.push("  Content gap pages (competitor has these; brand doesn't rank for them):");
        gaps.forEach((p) => lines.push(`    - ${p.url}${p.title ? ` — "${p.title}"` : ""}`))
      }
    }
    lines.push("", "Recommend creating content targeting these topics to close the gap with each competitor.");
    return lines.join("\n");
  } catch {
    return null;
  }
}

async function buildAiAttributionContext(brandId: string, days = 28): Promise<string | null> {
  try {
    const start = new Date(Date.now() - days * 86_400_000);
    const [platformGroups, topQueries, citationCount] = await Promise.all([
      prisma.aiAttribution.groupBy({
        by: ["aiPlatform"],
        where: { brandId, timestamp: { gte: start } },
        _count: true,
      }).catch(() => [] as Array<{ aiPlatform: string | null; _count: number }>),
      prisma.aiAttribution.findMany({
        where: { brandId, timestamp: { gte: start }, queryContext: { not: null } },
        orderBy: { timestamp: "desc" },
        take: 10,
        select: { aiPlatform: true, queryContext: true, converted: true, landingPage: true },
      }).catch(() => []),
      prisma.aiCitation.count({ where: { brandId, brandCited: true } }).catch(() => 0),
    ]);

    if (platformGroups.length === 0 && citationCount === 0) return null;

    type PlatformRow = { aiPlatform: string | null; _count: number };
    const typedPlatforms = platformGroups as PlatformRow[];
    const totalVisits = typedPlatforms.reduce((s, r) => s + r._count, 0);
    const lines: string[] = [`AI Traffic Attribution — last ${days} days (meSEO LLMO tracking):` ];
    lines.push(`Total AI-referred visits: ${totalVisits.toLocaleString()}`);
    lines.push(`Brand citations in AI responses: ${citationCount}`);

    if (typedPlatforms.length > 0) {
      lines.push("", "Visits by AI platform:");
      for (const r of [...typedPlatforms].sort((a, b) => b._count - a._count)) {
        lines.push(`  ${r.aiPlatform || "Unknown"}: ${r._count} visits`);
      }
    }

    const convertedQueries = topQueries.filter((q) => q.converted);
    if (convertedQueries.length > 0) {
      lines.push("", `Converting AI queries (${convertedQueries.length} queries led to conversions):`);
      convertedQueries.slice(0, 5).forEach((q) => {
        lines.push(`  [${q.aiPlatform || "?"}] "${q.queryContext}" → ${q.landingPage ?? "unknown page"}`);
      });
    } else if (topQueries.length > 0) {
      lines.push("", "Recent AI-referred queries:");
      topQueries.slice(0, 5).forEach((q) => {
        lines.push(`  [${q.aiPlatform || "?"}] "${q.queryContext}"`);
      });
    }

    lines.push("", "Use this to recommend FAQ-style content and schema markup that increases AI citation frequency.");
    return lines.join("\n");
  } catch {
    return null;
  }
}

async function buildSiteTagAnalyticsContext(brandId: string, days = 28): Promise<string | null> {
  try {
    const start = new Date(Date.now() - days * 86_400_000);
    const [rageClicks, deadClicks, heatmapAgg, topEvents] = await Promise.all([
      prisma.siteEvent.count({ where: { brandId, eventType: "rage_click", timestamp: { gte: start } } }).catch(() => 0),
      prisma.siteEvent.count({ where: { brandId, eventType: "dead_click", timestamp: { gte: start } } }).catch(() => 0),
      prisma.heatmapEvent.aggregate({
        where: { brandId, timestamp: { gte: start } },
        _avg: { maxScrollPercent: true },
        _count: true,
      }).catch(() => null),
      prisma.siteEvent.groupBy({
        by: ["eventType"],
        where: { brandId, timestamp: { gte: start } },
        _count: true,
        orderBy: { _count: { eventType: "desc" } },
        take: 8,
      }).catch(() => [] as Array<{ eventType: string; _count: number }>),
    ]);

    const heatmapCount = heatmapAgg ? (typeof heatmapAgg._count === "number" ? heatmapAgg._count : 0) : 0;
    if (rageClicks === 0 && deadClicks === 0 && heatmapCount === 0 && topEvents.length === 0) return null;

    const lines: string[] = [`Site Tag Behavioral Analytics — last ${days} days:`];
    lines.push("", "UX Friction signals:");
    lines.push(`  Rage clicks: ${rageClicks.toLocaleString()} (repeated clicks on same element — indicates user frustration)`);
    lines.push(`  Dead clicks: ${deadClicks.toLocaleString()} (clicks on non-interactive elements — indicates UI confusion)`);

    if (heatmapCount > 0) {
      const avg = heatmapAgg?._avg?.maxScrollPercent;
      lines.push("", "Scroll depth (HeatmapEvent):");
      lines.push(`  Average max scroll: ${avg != null ? Math.round(avg) + "%" : "N/A"}`);
      lines.push(`  Sessions with scroll data: ${heatmapCount.toLocaleString()}`);
      if (avg != null && avg < 50) {
        lines.push("  Note: average scroll depth < 50% — users aren't reaching content below the fold.");
      }
    }

    if (topEvents.length > 0) {
      lines.push("", "Top behavioral event counts:");
      topEvents.forEach((e) => {
        lines.push(`  ${e.eventType}: ${(typeof e._count === "number" ? e._count : 0).toLocaleString()}`);
      });
    }

    lines.push("", "Use rage_click and dead_click hotspots to prioritise UX fixes. High dead-click counts on a CTA usually mean the button isn't firing properly.");
    return lines.join("\n");
  } catch {
    return null;
  }
}

async function buildContextBlock(
  analysis: Analysis | null,
  domain: string,
  name: string,
  brandId: string,
  mode: "strategist" | "client",
  analysisState: "idle" | "running" | "complete" | "failed",
): Promise<ContextResult> {
  const modeHint = mode === "client"
    ? "Response tone: plain-language, client-friendly — avoid jargon, explain SEO concepts briefly."
    : "Response tone: senior strategist — cite specific numbers, be direct, use analytical vocabulary.";

  const dataRules = [
    "CRITICAL RULES:",
    "- ONLY cite numbers that appear in the data below. NEVER fabricate metrics.",
    "- If asked about data you don't have, say so clearly instead of guessing.",
    "- Every number you cite must come from the <analysis> or <live-data> block.",
    "- Do NOT reference Dashboard data or competitive scores unless they appear below.",
    "- If revenue data is marked as NOT configured, tell the user to set up GA4 ecommerce tracking.",
  ].join("\n");

  const d30 = new Date(Date.now() - 30 * 86400000);
  const dateRange = `${d30.toISOString().split("T")[0]} – ${new Date().toISOString().split("T")[0]}`;

  if (!analysis) {
    let stateNote = "";
    if (analysisState === "running") stateNote = "Note: Live Artifact analysis is currently generating. Showing raw GSC/GA4 data. Wait for generation to complete for full analysis.";
    else if (analysisState === "failed") stateNote = "Note: The Live Artifact analysis failed to generate. Showing raw GSC/GA4 data. The user can try clicking 'Refresh Analysis' to retry.";

    const [
      gsc, enrichment, kwContext, siteTagCtx, dailyGsc, dailyConv,
      langSplit, techHealth, competitorData, trackingHealth, paidMediaCtx,
      llmoCtx, contentIntelCtx, competitorIntelCtx, aiAttributionCtx, siteTagAnalyticsCtx,
    ] = await Promise.all([
      fetchLiveGscContext(brandId),
      getEnrichmentContext(brandId),
      fetchKeywordContext(brandId),
      buildSiteTagContext(brandId),
      buildDailyGscContext(brandId),
      buildDailyConversionContext(brandId),
      buildContentLanguageSplit(brandId),
      buildTechHealthContext(brandId),
      buildCompetitorContext(brandId),
      buildTrackingHealth(brandId, null),
      buildPaidMediaContext(brandId),
      buildLlmoContext(brandId).catch(() => ""),
      buildContentIntelContext(brandId),
      buildCompetitorIntelContext(brandId),
      buildAiAttributionContext(brandId),
      buildSiteTagAnalyticsContext(brandId),
    ]);
    const enrichmentText = formatEnrichmentForPrompt(enrichment);
    const dataSource = gsc.text ? `Live GSC + GA4 Data${enrichment.hasEnrichmentData ? ` + ${enrichment.fileCount} enrichment docs` : ""}` : "No data available";

    return {
      text: [
        "<brand>",
        `Domain: ${domain}`,
        `Name: ${name}`,
        "</brand>",
        `\n<tracking-health status="${trackingHealth.status}">\n${trackingHealth.message}\n</tracking-health>`,
        siteTagCtx ? `\n<site-tag>\n${siteTagCtx}\n</site-tag>` : "",
        "",
        stateNote,
        gsc.text ? `<live-data>\n${gsc.text}\n</live-data>` : "No GSC data available. Tell the user to generate a Live Analysis.",
        dailyGsc ? `\n<daily-gsc-data>\n${dailyGsc}\n</daily-gsc-data>` : "",
        dailyConv ? `\n<daily-conversions${dailyConv.attrs}>\n${dailyConv.inner}\n</daily-conversions>` : "",
        langSplit ? `\n<content-language-split>\n${langSplit}\n</content-language-split>` : "",
        techHealth ? `\n<technical-health>\n${techHealth}\n</technical-health>` : "",
        competitorData ? `\n<competitor-data>\n${competitorData}\n</competitor-data>` : "",
        kwContext ? `\n<keyword-tracking>\n${kwContext}\n</keyword-tracking>` : "",
        paidMediaCtx ? `\n<paid-media>\n${paidMediaCtx}\n</paid-media>` : "",
        llmoCtx ? `\n<llmo-attribution>\n${llmoCtx}\n</llmo-attribution>` : "",
        contentIntelCtx ? `\n<content-intel>\n${contentIntelCtx}\n</content-intel>` : "",
        competitorIntelCtx ? `\n<competitor-intel>\n${competitorIntelCtx}\n</competitor-intel>` : "",
        aiAttributionCtx ? `\n<ai-attribution>\n${aiAttributionCtx}\n</ai-attribution>` : "",
        siteTagAnalyticsCtx ? `\n<site-tag-analytics>\n${siteTagAnalyticsCtx}\n</site-tag-analytics>` : "",
        enrichmentText ? `\n<enrichment>\n${enrichmentText}\n</enrichment>\nReference enrichment documents for brand services/offerings. Cite which document.` : "",
        "",
        modeHint,
        "",
        dataRules,
      ].filter(Boolean).join("\n"),
      metadata: { dataSource, dateRange, analysisState, enrichmentDocs: enrichment.fileCount, cachedResponse: gsc.cached },
    };
  }

  const [
    enrichment, kwContext, siteTagCtx, dailyGsc, dailyConv,
    langSplit, techHealth, competitorData, trackingHealth, paidMediaCtx,
    llmoCtx, contentIntelCtx, competitorIntelCtx, aiAttributionCtx, siteTagAnalyticsCtx,
  ] = await Promise.all([
    getEnrichmentContext(brandId),
    fetchKeywordContext(brandId),
    buildSiteTagContext(brandId),
    buildDailyGscContext(brandId),
    buildDailyConversionContext(brandId),
    buildContentLanguageSplit(brandId),
    buildTechHealthContext(brandId),
    buildCompetitorContext(brandId),
    buildTrackingHealth(brandId, analysis),
    buildPaidMediaContext(brandId),
    buildLlmoContext(brandId).catch(() => ""),
    buildContentIntelContext(brandId),
    buildCompetitorIntelContext(brandId),
    buildAiAttributionContext(brandId),
    buildSiteTagAnalyticsContext(brandId),
  ]);
  const enrichmentText = formatEnrichmentForPrompt(enrichment);
  const srcLabels = analysis.sources.filter(s => s.status === "available").map(s => s.label).join(" + ") || "GSC + GA4";
  const dataSource = `Live Artifact Analysis (${srcLabels}${enrichment.hasEnrichmentData ? ` + ${enrichment.fileCount} docs` : ""})`;
  const analysisPeriod = `${analysis.period.start} – ${analysis.period.end}`;

  const isBounceRate = (label: string) => /bounce\s*rate/i.test(label);
  const compactAnalysis = {
    title: analysis.title, subtitle: analysis.subtitle, summary: analysis.summary,
    period: analysis.period, comparisonPeriod: analysis.comparisonPeriod,
    kpis: analysis.kpis.filter((k) => !isBounceRate(k.label)),
    findings: analysis.findings.filter((f) => !isBounceRate(f.title) && !isBounceRate(f.description)),
    predictions: analysis.predictions, recommendations: analysis.recommendations,
    caveats: analysis.caveats, sources: analysis.sources,
  };

  return {
    text: [
      "<analysis>",
      JSON.stringify(compactAnalysis, null, 2),
      "</analysis>",
      "",
      "<brand>",
      `Domain: ${domain}`,
      `Name: ${name}`,
      `Analysis period: ${analysisPeriod}`,
      "</brand>",
      `\n<tracking-health status="${trackingHealth.status}">\n${trackingHealth.message}\n</tracking-health>`,
      siteTagCtx ? `\n<site-tag>\n${siteTagCtx}\n</site-tag>` : "",
      dailyGsc ? `\n<daily-gsc-data>\n${dailyGsc}\n</daily-gsc-data>` : "",
      dailyConv ? `\n<daily-conversions${dailyConv.attrs}>\n${dailyConv.inner}\n</daily-conversions>` : "",
      langSplit ? `\n<content-language-split>\n${langSplit}\n</content-language-split>` : "",
      techHealth ? `\n<technical-health>\n${techHealth}\n</technical-health>` : "",
      competitorData ? `\n<competitor-data>\n${competitorData}\n</competitor-data>` : "",
      kwContext ? `\n<keyword-tracking>\n${kwContext}\n</keyword-tracking>` : "",
      paidMediaCtx ? `\n<paid-media>\n${paidMediaCtx}\n</paid-media>` : "",
      llmoCtx ? `\n<llmo-attribution>\n${llmoCtx}\n</llmo-attribution>` : "",
      contentIntelCtx ? `\n<content-intel>\n${contentIntelCtx}\n</content-intel>` : "",
      competitorIntelCtx ? `\n<competitor-intel>\n${competitorIntelCtx}\n</competitor-intel>` : "",
      aiAttributionCtx ? `\n<ai-attribution>\n${aiAttributionCtx}\n</ai-attribution>` : "",
      siteTagAnalyticsCtx ? `\n<site-tag-analytics>\n${siteTagAnalyticsCtx}\n</site-tag-analytics>` : "",
      enrichmentText ? `\n<enrichment>\n${enrichmentText}\n</enrichment>\nReference enrichment documents for brand services/offerings. Cite which document.` : "",
      "",
      modeHint,
      "",
      dataRules,
    ].filter(Boolean).join("\n"),
    metadata: { dataSource, dateRange: analysisPeriod, analysisState, enrichmentDocs: enrichment.fileCount, cachedResponse: false },
  };
}
