/**
 * POST /api/schema/bulk-generate
 *
 * Generate schemas for a batch of pages and persist each successful
 * result to the SchemaMarkup table. Two input modes:
 *
 *   1. Explicit list — body.pageUrls: string[]
 *   2. Auto-sourced  — body.source: "trackedSite" | "gsc"
 *      Server derives top URLs for the brand from the named source
 *      so the client can kick off a full-site bulk run without
 *      pre-loading every URL into the request body.
 *
 * Runs with MAX_CONCURRENCY=3 to stay under PageSpeed / Claude rate
 * limits while moving measurably faster than the previous sequential
 * for-loop (10 URLs ≈ 10 × ~6s = 60s sequential → ~20s at fan-out=3).
 * Wall budget 110s leaves 10s headroom for the DB writes + response
 * under the maxDuration=120 Vercel cap.
 *
 * Per-URL skip when the brand already has a SchemaMarkup row for
 * that pageUrl — avoids duplicating rows when a user bulk-generates
 * twice in a row. Users who want to regenerate can delete the
 * existing row first.
 */

import { NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { generateSchema, type SchemaResult } from "@/lib/services/schema-generator";

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

const MAX_CONCURRENCY = 3;
const MAX_URLS = 20;
const WALL_BUDGET_MS = 110_000;

/** Resolve a URL list from the brand's data when the client requested
 *  auto-sourcing instead of passing an explicit pageUrls array. Falls
 *  back to the homepage when neither source yields rows — better than
 *  returning an empty-list error for a brand whose TrackedSite hasn't
 *  captured events yet. */
async function sourceUrls(brandId: string, brandDomain: string, source: "trackedSite" | "gsc"): Promise<string[]> {
  const origin = brandDomain.startsWith("http") ? brandDomain.replace(/\/+$/, "") : `https://${brandDomain}`;
  const home = origin;
  const thirtyDaysAgo = new Date(Date.now() - 30 * 86_400_000);

  if (source === "trackedSite") {
    const site = await prisma.trackedSite.findUnique({
      where: { brandId },
      select: { id: true },
    }).catch(() => null);
    if (!site) return [home];
    const rows = await prisma.trackedEvent.groupBy({
      by: ["pageUrl"],
      where: { trackedSiteId: site.id, eventType: "page_view", timestamp: { gte: thirtyDaysAgo } },
      _count: true,
      orderBy: { _count: { pageUrl: "desc" } },
      take: MAX_URLS,
    }).catch(() => []);
    // TrackedEvent pageUrl can be relative ("/about") or absolute;
    // normalise both so downstream /api/schema/generate gets a
    // consistent input. Dedupe at the end just in case the same
    // path appears twice under different casings.
    const urls = new Set<string>([home]);
    for (const r of rows) {
      if (!r.pageUrl) continue;
      const abs = r.pageUrl.startsWith("http") ? r.pageUrl : `${origin}${r.pageUrl.startsWith("/") ? r.pageUrl : "/" + r.pageUrl}`;
      urls.add(abs.replace(/\/+$/, ""));
      if (urls.size >= MAX_URLS) break;
    }
    return Array.from(urls);
  }

  // source === "gsc"
  const rows = await prisma.gscPage.groupBy({
    by: ["pageUrl"],
    where: { brandId, date: { gte: new Date(Date.now() - 90 * 86_400_000) }, clicks: { gt: 0 } },
    _sum: { clicks: true },
    orderBy: { _sum: { clicks: "desc" } },
    take: MAX_URLS,
  }).catch(() => []);
  const urls = new Set<string>([home]);
  for (const r of rows) {
    if (!r.pageUrl) continue;
    urls.add(r.pageUrl.replace(/\/+$/, ""));
    if (urls.size >= MAX_URLS) break;
  }
  return Array.from(urls);
}

interface BulkResult {
  pageUrl: string;
  savedSchemaIds: string[];
  result?: SchemaResult;
  error?: string;
}

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) as {
    brandId?: string;
    pageUrls?: string[];
    source?: "trackedSite" | "gsc";
  } | null;
  const brandIdMaybe = body?.brandId;
  if (!brandIdMaybe) return NextResponse.json({ error: "brandId required" }, { status: 400 });
  // Narrowing doesn't propagate into the async worker closure below
  // without a stable non-optional const. Using a separate identifier
  // keeps the "required" branch at the top of the handler without
  // fighting TS on every subsequent usage.
  const brandId: string = brandIdMaybe;

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

  // Resolve the URL list. Explicit pageUrls wins; falls back to
  // auto-sourcing when the client requested it.
  let urls: string[];
  if (Array.isArray(body?.pageUrls) && body!.pageUrls.length > 0) {
    urls = body!.pageUrls.slice(0, MAX_URLS);
  } else if (body?.source === "trackedSite" || body?.source === "gsc") {
    urls = await sourceUrls(brandId, brand.domain, body.source);
  } else {
    return NextResponse.json({ error: "pageUrls[] or source=trackedSite|gsc required" }, { status: 400 });
  }

  if (urls.length === 0) {
    return NextResponse.json({ error: "No URLs available to generate schemas for" }, { status: 400 });
  }

  // Pre-fetch existing schemas for these URLs so we can skip dupes
  // instead of generating (expensive) and then silently overwriting
  // an earlier run.
  const existing = await prisma.schemaMarkup.findMany({
    where: { brandId, pageUrl: { in: urls } },
    select: { pageUrl: true },
  }).catch(() => []);
  const existingSet = new Set(existing.map((e) => e.pageUrl));

  const queue = urls.filter((u) => !existingSet.has(u));
  const skipped = urls.filter((u) => existingSet.has(u));

  const runnerStart = Date.now();
  const results: BulkResult[] = skipped.map((url) => ({
    pageUrl: url,
    savedSchemaIds: [],
    error: "Already has a saved schema — delete it first to regenerate.",
  }));
  let budgetHit = false;

  console.info("[schema:bulk] started", { brandId, totalUrls: urls.length, skipped: skipped.length, toGenerate: queue.length });

  // Worker-pool pattern mirrored from performance-audit-runner so
  // the throughput + budget-aware skip behaviour is consistent
  // across every bulk job in the codebase.
  async function worker() {
    while (queue.length > 0) {
      const elapsed = Date.now() - runnerStart;
      if (WALL_BUDGET_MS - elapsed < 8_000) {
        // generate+save typically runs 3-6s per page; skip new
        // pulls when the remaining budget can't fit a worst case.
        budgetHit = true;
        return;
      }
      const pageUrl = queue.shift();
      if (!pageUrl) return;
      try {
        const result = await generateSchema(brandId, pageUrl);
        // Persist EACH recommended schema as its own SchemaMarkup row.
        // A single page can legitimately need multiple schemas (e.g.
        // a FAQ page with BreadcrumbList + FAQPage + Organization);
        // the previous bulk route just returned them without saving.
        const savedIds: string[] = [];
        for (const s of result.schemas) {
          try {
            const saved = await prisma.schemaMarkup.create({
              data: {
                brandId,
                pageUrl,
                schemaType: s.type,
                // Prisma's generated `Json` column types expect
                // InputJsonValue; Record<string, unknown> from the
                // AI response is structurally compatible but TS
                // doesn't infer that on its own. JSON-stringify +
                // parse normalises to a plain JSON-safe tree.
                jsonLd: JSON.parse(JSON.stringify(s.jsonLd)),
                status: "draft",
                autoInject: false,
              },
              select: { id: true },
            });
            savedIds.push(saved.id);
          } catch (err) {
            console.warn("[schema:bulk] save failed", { brandId, pageUrl, type: s.type, err: err instanceof Error ? err.message : "unknown" });
          }
        }
        results.push({ pageUrl, savedSchemaIds: savedIds, result });
      } catch (err) {
        results.push({ pageUrl, savedSchemaIds: [], error: err instanceof Error ? err.message : "Generation failed" });
      }
    }
  }

  const workerCount = Math.min(MAX_CONCURRENCY, queue.length);
  await Promise.all(Array.from({ length: workerCount }, () => worker()));

  const succeeded = results.filter((r) => r.savedSchemaIds.length > 0).length;
  const failed = results.filter((r) => r.error).length;

  console.info("[schema:bulk] done", {
    brandId,
    totalProcessed: results.length,
    succeeded,
    failed,
    skipped: skipped.length,
    budgetHit,
    elapsedMs: Date.now() - runnerStart,
  });

  return NextResponse.json({
    results,
    total: results.length,
    succeeded,
    failed,
    skipped: skipped.length,
    partial: budgetHit || queue.length > 0,
    message: budgetHit
      ? `Generated ${succeeded} of ${urls.length} pages — ${Math.round(WALL_BUDGET_MS / 1000)}s budget reached before finishing the queue.`
      : `Generated ${succeeded} of ${urls.length} pages (${skipped.length} skipped, ${failed} failed).`,
  });
}
