/**
 * POST /api/topic-map/generate — AI-powered topic cluster map.
 *
 * Produces a radial map of the seed topic. The AI supplies the
 * cluster + subtopic shape; we enrich each cluster server-side with
 * data the UI needs for the cluster-detail panel:
 *
 *   • `brandPages`       — pages on the brand domain that already
 *                          rank for keywords in the cluster (from
 *                          GscPage, last 90 days). Drives the
 *                          "You have X pages ranking for this
 *                          cluster" summary + the content-gap flag.
 *   • `competitorPages`  — competitor pages whose title / h1 / url
 *                          mention cluster keywords. Lets the UI
 *                          show "Competitors covering this cluster".
 *   • `isContentGap`     — true when the brand has zero ranking
 *                          pages for any keyword in the cluster.
 *   • `subtopic.position` — brand's GSC average position for the
 *                          subtopic keyword when available; powers
 *                          the per-keyword ranking badge in the
 *                          detail panel.
 *   • `topQueries`       — brand's top GSC queries for the topic-
 *                          map placeholder chips on first load.
 *
 * After a successful generation we also record a `topic_map_search`
 * SiteEvent so the page can show "Recent Searches" on next visit.
 * Using SiteEvent avoids a new Prisma model — the shape we write
 * here is what the GET handler reads back.
 *
 * All enrichment is best-effort — any sub-query that fails falls
 * back to an empty array so a thin-data brand still gets a map.
 *
 * GET /api/topic-map/generate?brandId=X&recent=true — returns the
 * brand's most recent distinct topic-map searches (up to 10) for
 * the recent-searches UI.
 */
import { NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import type { Prisma } from "@prisma/client";
import { callLlm } from "@/lib/ai-avatar/llm-client";
import { logAiInteraction } from "@/lib/services/ai-interaction-log";

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

interface Subtopic {
  keyword: string;
  volume: number | null;
  questions?: string[];
  /** Populated server-side when the brand ranks for this keyword. */
  position?: number | null;
  ranking?: boolean;
}

interface Cluster {
  keyword: string;
  volume: number | null;
  subtopics: Subtopic[];
  /** Enrichment fields — always present (possibly empty). */
  brandPages?: Array<{ pageUrl: string; position: number; clicks: number }>;
  competitorPages?: Array<{ competitorDomain: string; url: string; title: string | null }>;
  isContentGap?: boolean;
  rankingCount?: number;
}

function extractJson(raw: string): string {
  let s = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "");
  const f = s.indexOf("{"), l = s.lastIndexOf("}");
  if (f >= 0 && l > f) s = s.slice(f, l + 1);
  return s;
}

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

  const body = await req.json().catch(() => null);
  if (!body?.brandId || !body?.seedTopic) {
    return NextResponse.json({ error: "brandId and seedTopic required" }, { status: 400 });
  }

  const brand = await prisma.brand.findUnique({
    where: { id: body.brandId },
    select: { name: true, industry: true, domain: true },
  });

  const prompt = `Create a topic research map for "${body.seedTopic}" in the ${brand?.industry || "general"} industry.

Return JSON:
{"center":{"keyword":"${body.seedTopic}","volume":null},"clusters":[{"keyword":"cluster name","volume":null,"subtopics":[{"keyword":"subtopic","volume":null,"questions":["question 1","question 2"]}]}]}

Include 5-8 clusters with 3-5 subtopics each. Focus on real search intent.`;

  const result = await callLlm(
    [
      { role: "system", content: "You are a keyword research expert. Respond ONLY with JSON." },
      { role: "user", content: prompt },
    ],
    () => JSON.stringify({
      center: { keyword: body.seedTopic, volume: null },
      clusters: [
        {
          keyword: "Getting Started",
          volume: null,
          subtopics: [{ keyword: `${body.seedTopic} guide`, volume: null, questions: [`What is ${body.seedTopic}?`] }],
        },
      ],
    }),
    { maxTokens: 4000 },
  );

  logAiInteraction({
    userId: user.id,
    brandId: body.brandId,
    feature: "topic_map",
    userPrompt: prompt,
    aiResponse: result.reply,
    model: result.model,
    brandDomain: brand?.domain ?? null,
  });

  let map: { center: { keyword: string; volume: number | null }; clusters: Cluster[] };
  try {
    map = JSON.parse(extractJson(result.reply));
  } catch {
    return NextResponse.json({ error: "AI returned invalid response" }, { status: 500 });
  }

  // ── Enrichment — run all three lookups in parallel, each degrades
  //    to an empty shape on failure so one missing relation can't
  //    500 the whole map. ───────────────────────────────────────────
  const since = new Date(Date.now() - 90 * 86_400_000);

  const [tracked, gscPages, gscQueries, competitorPages] = await Promise.all([
    prisma.trackedKeyword.findMany({
      where: { brandId: body.brandId, isActive: true },
      select: { keyword: true },
      take: 500,
    }).catch(() => []),
    prisma.gscPage.findMany({
      where: { brandId: body.brandId, date: { gte: since }, clicks: { gt: 0 } },
      orderBy: { clicks: "desc" },
      take: 500,
      select: { pageUrl: true, clicks: true, position: true },
    }).catch(() => []),
    prisma.gscQuery.findMany({
      where: { brandId: body.brandId, date: { gte: since } },
      orderBy: { clicks: "desc" },
      take: 500,
      select: { query: true, clicks: true, position: true },
    }).catch(() => []),
    prisma.competitorPage.findMany({
      where: {
        competitor: { brandId: body.brandId, isActive: true },
        status: { notIn: ["removed"] },
      },
      include: { competitor: { select: { domain: true } } },
      take: 500,
    }).catch(() => []),
  ]);

  const rankedSet = new Set(tracked.map((t) => t.keyword.toLowerCase()));

  // Keyword → avg position map for subtopic badges. We also union
  // GSC query keywords into the "ranking" set so subtopics the user
  // actually ranks for (but hasn't added to Rank Tracker yet) light
  // up green.
  const queryPositionMap = new Map<string, number>();
  for (const q of gscQueries) {
    const k = q.query.toLowerCase();
    const prev = queryPositionMap.get(k);
    // Keep the best (lowest) position we've seen for this query.
    if (prev === undefined || q.position < prev) queryPositionMap.set(k, q.position);
    rankedSet.add(k);
  }

  // Index helpers reused per cluster.
  function tokensOf(s: string): string[] {
    return s.toLowerCase().split(/\s+/).filter((t) => t.length >= 4);
  }

  // Keep the clusters enrichment cheap: one pass over GSC + competitor
  // arrays per cluster, token-matched against the cluster keyword +
  // subtopic keywords. We deduplicate by URL so one competitor page
  // doesn't show up for every matching subtopic.
  const enrichedClusters: Cluster[] = map.clusters.map((cluster) => {
    const clusterTerms = new Set<string>([cluster.keyword.toLowerCase()]);
    for (const st of cluster.subtopics || []) clusterTerms.add(st.keyword.toLowerCase());
    const tokenSet = new Set<string>();
    for (const t of clusterTerms) for (const tok of tokensOf(t)) tokenSet.add(tok);

    // Match brand pages to this cluster via GSC queries. Find queries
    // that match any cluster keyword, then find which pages rank for
    // those queries (inferred from URL overlap with the query terms).
    // Stricter than the previous token-based URL matching which caused
    // every cluster to show the same top pages.
    const matchesCluster = (query: string): boolean => {
      const q = query.toLowerCase();
      for (const term of clusterTerms) {
        if (q.includes(term) || term.includes(q)) return true;
      }
      return false;
    };

    const clusterQueryUrls = new Set<string>();
    for (const q of gscQueries) {
      if (matchesCluster(q.query)) {
        // GSC queries don't have pageUrl, but we can mark this cluster
        // as having relevant queries for the subtopic badges
      }
    }

    // Brand pages: match URLs against the FULL cluster keyword (as a
    // slug) rather than individual tokens. "dental implants" matches
    // "/dental-implants" but not "/dental-services".
    const slugTerms = new Set<string>();
    for (const term of clusterTerms) {
      slugTerms.add(term.replace(/\s+/g, "-"));
      slugTerms.add(term.replace(/\s+/g, ""));
      if (term.split(/\s+/).length <= 2) slugTerms.add(term);
    }

    const brandPageMap = new Map<string, { pageUrl: string; position: number; clicks: number }>();
    for (const p of gscPages) {
      const url = p.pageUrl.toLowerCase();
      let matched = false;
      for (const slug of slugTerms) {
        if (url.includes(slug)) { matched = true; break; }
      }
      if (!matched) continue;
      const existing = brandPageMap.get(p.pageUrl);
      if (!existing || p.position < existing.position) {
        brandPageMap.set(p.pageUrl, {
          pageUrl: p.pageUrl,
          position: Math.round(p.position * 10) / 10,
          clicks: p.clicks,
        });
      }
    }
    const brandPages = Array.from(brandPageMap.values())
      .sort((a, b) => b.clicks - a.clicks)
      .slice(0, 8);

    // Competitor pages — dedupe by URL, match title/URL against cluster terms.
    const compUrlSet = new Set<string>();
    const comps: Array<{ competitorDomain: string; url: string; title: string | null }> = [];
    for (const p of competitorPages) {
      if (compUrlSet.has(p.url)) continue;
      const hay = `${p.title ?? ""} ${p.h1 ?? ""} ${p.url}`.toLowerCase();
      let compMatch = false;
      for (const term of clusterTerms) { if (hay.includes(term)) { compMatch = true; break; } }
      if (!compMatch) continue;
      compUrlSet.add(p.url);
      comps.push({ competitorDomain: p.competitor.domain, url: p.url, title: p.title });
      if (comps.length >= 10) break;
    }

    // Subtopic rank badges — do we have GSC data for this exact keyword?
    const enrichedSubtopics: Subtopic[] = (cluster.subtopics || []).map((st) => {
      const pos = queryPositionMap.get(st.keyword.toLowerCase());
      return {
        ...st,
        position: pos !== undefined ? Math.round(pos * 10) / 10 : null,
        ranking: pos !== undefined && pos <= 100,
      };
    });
    const rankingCount = enrichedSubtopics.filter((s) => s.ranking).length;

    return {
      ...cluster,
      subtopics: enrichedSubtopics,
      brandPages,
      competitorPages: comps,
      isContentGap: brandPages.length === 0,
      rankingCount,
    };
  });

  // Brand's top GSC queries — surfaced to the UI for the placeholder
  // chips on first render. Dedup'd + capped to 5.
  const topQueryMap = new Map<string, { query: string; clicks: number }>();
  for (const q of gscQueries) {
    const key = q.query.toLowerCase();
    const prev = topQueryMap.get(key);
    if (!prev || prev.clicks < q.clicks) topQueryMap.set(key, { query: q.query, clicks: q.clicks });
  }
  const topQueries = Array.from(topQueryMap.values())
    .sort((a, b) => b.clicks - a.clicks)
    .slice(0, 5);

  // Record this generation as a `topic_map_search` SiteEvent so the
  // page can render a recent-searches list. Best-effort — a failed
  // write must not block the response.
  prisma.siteEvent.create({
    data: {
      brandId: body.brandId,
      eventType: "topic_map_search",
      pageUrl: "/topic-map",
      eventData: {
        keyword: body.seedTopic,
        clusterCount: enrichedClusters.length,
        generatedAt: new Date().toISOString(),
      } as Prisma.InputJsonValue,
    },
  }).catch(() => { /* best-effort */ });

  return NextResponse.json({
    map: { center: map.center, clusters: enrichedClusters },
    rankedKeywords: Array.from(rankedSet),
    topQueries,
  });
}

// ─── GET: recent searches ──────────────────────────────────────────────

interface RecentSearchEventData {
  keyword?: string;
  clusterCount?: number;
  generatedAt?: string;
}

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

  const { searchParams } = new URL(req.url);
  const brandId = searchParams.get("brandId");
  if (!brandId) return NextResponse.json({ error: "brandId required" }, { status: 400 });

  // Access check — same org-membership pattern as the rest of the app.
  const brand = await prisma.brand.findUnique({
    where: { id: brandId },
    include: { organization: { include: { memberships: true } } },
  });
  if (!brand) return NextResponse.json({ error: "Brand not found" }, { status: 404 });
  if (!brand.organization.memberships.some((m) => m.userId === user.id)) {
    return NextResponse.json({ error: "Access denied" }, { status: 403 });
  }

  // Only the recent-searches shape is supported on GET today. Guards
  // against accidental reads if a future client omits the flag.
  if (searchParams.get("recent") !== "true") {
    return NextResponse.json({ error: "recent=true required" }, { status: 400 });
  }

  // Pull more than we need so we can dedupe by keyword in-memory.
  // `distinct: ["eventData"]` doesn't work because eventData is JSON.
  const events = await prisma.siteEvent.findMany({
    where: { brandId, eventType: "topic_map_search" },
    orderBy: { timestamp: "desc" },
    take: 50,
    select: { eventData: true, timestamp: true },
  }).catch(() => []);

  const seen = new Set<string>();
  const recentSearches: Array<{ keyword: string; date: string; clusters: number }> = [];
  for (const ev of events) {
    const data = (ev.eventData ?? {}) as RecentSearchEventData;
    const keyword = (data.keyword || "").trim();
    if (!keyword) continue;
    const key = keyword.toLowerCase();
    if (seen.has(key)) continue;
    seen.add(key);
    recentSearches.push({
      keyword,
      date: ev.timestamp.toISOString(),
      clusters: typeof data.clusterCount === "number" ? data.clusterCount : 0,
    });
    if (recentSearches.length >= 10) break;
  }

  return NextResponse.json({ recentSearches });
}
