/**
 * GET /api/dashboard/search-performance?brandId=X&range=28d
 *
 * Returns the GSC "top pages" + "top queries" panels for the
 * Dashboard's Search Performance card. Split out from the main
 * /api/dashboard route because these two GSC API calls were the
 * slowest per-request work — historically responsible for
 * `[PERF] dashboard tail-queries 51279ms` style hangs that blocked
 * every KPI card from rendering. The dashboard now loads its KPIs
 * first, then the frontend lazy-fetches this endpoint to fill in
 * the Search Performance card.
 *
 * Resolution order, mirrored from the main dashboard:
 *   1. Live GSC API (6s timeout per call, parallel for queries +
 *      pages).
 *   2. DB fallback from gscQuery / gscPage if live returns empty.
 *   3. Empty arrays — UI renders "No query data yet".
 *
 * Caching: per-user, 60s fresh + stale-while-revalidate so back/
 * forward navigation feels instant and doesn't re-call Google.
 */

import { NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { getCredentials } from "@/lib/services/integration-credentials";
import { apiCall } from "@/lib/integrations/types";

export const dynamic = "force-dynamic";

const GSC_TIMEOUT_MS = 6_000;

interface DateRange {
  startDate: string;
  endDate: string;
}

function getDateRange(range: string, customStart?: string, customEnd?: string): DateRange {
  const today = new Date();
  const endDate = today.toISOString().split("T")[0];

  if (range === "custom" && customStart && customEnd) {
    return { startDate: customStart, endDate: customEnd };
  }
  if (range === "month") {
    const firstOfMonthMs = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), 1);
    return { startDate: new Date(firstOfMonthMs).toISOString().split("T")[0], endDate };
  }
  const days = range === "7d" ? 7 : range === "90d" ? 90 : range === "1d" ? 1 : 28;
  return {
    startDate: new Date(Date.now() - (days - 1) * 86400000).toISOString().split("T")[0],
    endDate,
  };
}

async function withTimeout<T>(promise: Promise<T>, ms: number, fallback: T): Promise<T> {
  return Promise.race([
    promise,
    new Promise<T>((resolve) => setTimeout(() => resolve(fallback), ms)),
  ]);
}

interface TopQuery { query: string; clicks: number; position: number }
interface TopPage { pageUrl: string; clicks: number; impressions: number }

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");
  const range = searchParams.get("range") ?? "28d";
  if (!brandId) return NextResponse.json({ error: "brandId is required" }, { status: 400 });

  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 });
  }

  const { startDate, endDate } = getDateRange(
    range,
    searchParams.get("start") ?? undefined,
    searchParams.get("end") ?? undefined,
  );

  // Try live GSC first. The GSC creds are read directly — no
  // refresh is attempted here. If the access_token is stale the
  // API will 401 and we fall through to the DB. The main dashboard
  // handles the refresh path for us; this endpoint is purely
  // presentational and should never block on OAuth.
  const gscCreds = await getCredentials(brandId, "gsc").catch(() => null);
  const siteUrl = (gscCreds?.siteUrl as string | undefined) || (gscCreds?.accountId as string | undefined);

  let topQueries: TopQuery[] = [];
  let topPages: TopPage[] = [];
  let source: "live" | "cached" | "stale" | "empty" = "empty";
  const t0 = Date.now();

  if (gscCreds?.accessToken && siteUrl) {
    const accessToken = gscCreds.accessToken as string;
    const queryFallback = { ok: false as const, status: 0, data: null, error: "client timeout" };
    const tLive = Date.now();
    const [queryRes, pageRes] = await Promise.all([
      withTimeout(
        apiCall(
          `https://www.googleapis.com/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/searchAnalytics/query`,
          { method: "POST", bearerToken: accessToken, body: { startDate, endDate, dimensions: ["query"], rowLimit: 10 } },
        ),
        GSC_TIMEOUT_MS,
        queryFallback,
      ),
      withTimeout(
        apiCall(
          `https://www.googleapis.com/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/searchAnalytics/query`,
          { method: "POST", bearerToken: accessToken, body: { startDate, endDate, dimensions: ["page"], rowLimit: 5 } },
        ),
        GSC_TIMEOUT_MS,
        queryFallback,
      ),
    ]);
    console.info(`[search-perf:perf] live GSC ${Date.now() - tLive}ms`, { brandId, queryOk: queryRes.ok, pageOk: pageRes.ok, queryStatus: queryRes.status, pageStatus: pageRes.status });

    if (queryRes.ok) {
      const data = queryRes.data as { rows?: Array<{ keys: string[]; clicks: number; position: number }> };
      topQueries = (data.rows ?? []).slice(0, 5).map((r) => ({
        query: r.keys[0],
        clicks: r.clicks,
        position: Math.round(r.position * 10) / 10,
      }));
      if (topQueries.length > 0) source = "live";
    }
    if (pageRes.ok) {
      const data = pageRes.data as { rows?: Array<{ keys: string[]; clicks: number; impressions: number }> };
      topPages = (data.rows ?? []).slice(0, 5).map((r) => ({
        pageUrl: r.keys[0],
        clicks: r.clicks,
        impressions: r.impressions,
      }));
      if (topPages.length > 0) source = "live";
    }
  } else {
    console.info("[search-perf:perf] live GSC skipped — no creds or siteUrl", { brandId, hasAccessToken: !!gscCreds?.accessToken, hasSiteUrl: !!siteUrl });
  }

  // ── In-range DB fallback ──
  if (topQueries.length === 0) {
    const tDb = Date.now();
    const dbQueries = await prisma.gscQuery.groupBy({
      by: ["query"],
      where: { brandId, date: { gte: new Date(startDate), lte: new Date(endDate) }, clicks: { gt: 0 } },
      _sum: { clicks: true },
      _avg: { position: true },
      orderBy: { _sum: { clicks: "desc" } },
      take: 10,
    }).catch(() => []);
    console.info(`[search-perf:perf] in-range DB queries ${Date.now() - tDb}ms`, { brandId, count: dbQueries.length, startDate, endDate });
    if (dbQueries.length > 0) {
      topQueries = dbQueries.slice(0, 5).map((q) => ({
        query: q.query,
        clicks: q._sum.clicks ?? 0,
        position: Math.round((q._avg.position ?? 0) * 10) / 10,
      }));
      if (source === "empty") source = "cached";
    }
  }

  if (topPages.length === 0) {
    const tDb = Date.now();
    const dbPages = await prisma.gscPage.groupBy({
      by: ["pageUrl"],
      where: { brandId, date: { gte: new Date(startDate), lte: new Date(endDate) }, clicks: { gt: 0 } },
      _sum: { clicks: true, impressions: true },
      orderBy: { _sum: { clicks: "desc" } },
      take: 10,
    }).catch(() => []);
    console.info(`[search-perf:perf] in-range DB pages ${Date.now() - tDb}ms`, { brandId, count: dbPages.length });
    if (dbPages.length > 0) {
      topPages = dbPages.slice(0, 5).map((p) => ({
        pageUrl: p.pageUrl,
        clicks: p._sum.clicks ?? 0,
        impressions: p._sum.impressions ?? 0,
      }));
      if (source === "empty") source = "cached";
    }
  }

  // ── Stale last-known-good fallback ──
  // GSC has a 2-3 day reporting lag — a Full Re-sync writes 90 days
  // of data dated up through ~3 days ago. If the user's range
  // (default 28d) doesn't include rows, we fell to "Awaiting Data"
  // even with thousands of recent queries in the DB. Now we widen
  // the lookup to the most recent ~90 days regardless of the
  // requested range. Surfaces source:"stale" so the UI can flag
  // that the numbers don't match the date selector.
  if (topQueries.length === 0) {
    const tStale = Date.now();
    const dbQueries = await prisma.gscQuery.groupBy({
      by: ["query"],
      where: { brandId, clicks: { gt: 0 } },
      _sum: { clicks: true },
      _avg: { position: true },
      orderBy: { _sum: { clicks: "desc" } },
      take: 10,
    }).catch(() => []);
    console.info(`[search-perf:perf] stale-fallback queries ${Date.now() - tStale}ms`, { brandId, count: dbQueries.length });
    if (dbQueries.length > 0) {
      topQueries = dbQueries.slice(0, 5).map((q) => ({
        query: q.query,
        clicks: q._sum.clicks ?? 0,
        position: Math.round((q._avg.position ?? 0) * 10) / 10,
      }));
      source = "stale";
    }
  }

  if (topPages.length === 0) {
    const tStale = Date.now();
    const dbPages = await prisma.gscPage.groupBy({
      by: ["pageUrl"],
      where: { brandId, clicks: { gt: 0 } },
      _sum: { clicks: true, impressions: true },
      orderBy: { _sum: { clicks: "desc" } },
      take: 10,
    }).catch(() => []);
    console.info(`[search-perf:perf] stale-fallback pages ${Date.now() - tStale}ms`, { brandId, count: dbPages.length });
    if (dbPages.length > 0) {
      topPages = dbPages.slice(0, 5).map((p) => ({
        pageUrl: p.pageUrl,
        clicks: p._sum.clicks ?? 0,
        impressions: p._sum.impressions ?? 0,
      }));
      source = "stale";
    }
  }

  console.info(`[PERF] /api/dashboard/search-performance ${Date.now() - t0}ms`, { brandId, range, source, topQueriesCount: topQueries.length, topPagesCount: topPages.length });

  // Cache only when we actually have data. Caching an empty result
  // for 60s would persist the "Awaiting Data" state across browser
  // refreshes for the full minute even after a successful sync.
  const headers = (topQueries.length > 0 || topPages.length > 0)
    ? { "Cache-Control": "private, max-age=60, stale-while-revalidate=300" }
    : { "Cache-Control": "private, no-cache, no-store, must-revalidate" };

  return NextResponse.json(
    { topQueries, topPages, source, range, startDate, endDate },
    { headers },
  );
}
