/**
 * GET|POST /api/cron/signals-site-tag-ai — hourly AI scan of Site Tag data
 * ────────────────────────────────────────────────────────────────────────
 * For each brand with an active TrackedSite, builds a last-hour snapshot
 * (new pages, form abandonment, 404s, JS errors, bouncing pages, etc.),
 * sends it to Claude for SEO/UX analysis, and persists the returned
 * signals via writeAiSignals().
 *
 * Cost-conscious defaults:
 *   • Brands iterate sequentially — no fan-out — so we don't spike
 *     Anthropic rate limits with 50 concurrent calls.
 *   • One AI call per brand per run (batched snapshot, max 8 signals).
 *   • 55s runtime cap — Vercel kills the function at 60s.
 *   • Brands with no Site Tag activity in the last hour are skipped
 *     BEFORE the AI call to save tokens.
 */

import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { callAiForSignals, writeAiSignals } from "@/lib/services/ai-signals";
import { log } from "@/lib/logger";

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

const CRON_SECRET = process.env.CRON_SECRET ?? "";
const RUNTIME_CAP_MS = 55_000;
const CRON_NAME = "signals-site-tag-ai";
const PAGE_SIZE = 200;

function authOk(req: Request): boolean {
  if (!CRON_SECRET) return false;
  const header = req.headers.get("authorization") ?? "";
  if (header === `Bearer ${CRON_SECRET}`) return true;
  return new URL(req.url).searchParams.get("secret") === CRON_SECRET;
}

export async function GET(req: Request)  { return authOk(req) ? handle() : NextResponse.json({ error: "Unauthorized" }, { status: 401 }); }
export async function POST(req: Request) { return authOk(req) ? handle() : NextResponse.json({ error: "Unauthorized" }, { status: 401 }); }

async function handle() {
  const startedAt = Date.now();

  const cursor = await prisma.cronCursor.findUnique({ where: { name: CRON_NAME } });
  const lastProcessedId = cursor?.brandId ?? null;

  const brands = await prisma.brand.findMany({
    where: { trackedSite: { isNot: null }, isDemo: false },
    orderBy: { id: "asc" },
    take: PAGE_SIZE,
    ...(lastProcessedId ? { cursor: { id: lastProcessedId }, skip: 1 } : {}),
    select: { id: true, name: true, domain: true, industry: true },
  });

  interface Row {
    brandId: string;
    domain: string;
    created?: number;
    deduped?: number;
    tokens?: number;
    skipped?: "no_activity" | "runtime_cap";
    error?: string;
  }
  const results: Row[] = [];

  for (const brand of brands) {
    if (Date.now() - startedAt > RUNTIME_CAP_MS) {
      results.push({ brandId: brand.id, domain: brand.domain, skipped: "runtime_cap" });
      continue;
    }
    try {
      const snapshot = await buildSiteTagSnapshot(brand.id);
      // Skip the AI call entirely when there was no activity to analyse.
      // Saves tokens on brands that haven't installed the snippet or
      // haven't had traffic in the last hour.
      if (!snapshot || snapshot.pageViewsCount === 0) {
        results.push({ brandId: brand.id, domain: brand.domain, skipped: "no_activity" });
        continue;
      }

      const ai = await callAiForSignals({
        brandIdForLog: brand.id,
        maxTokens: 2000,
        systemPrompt: buildSystemPrompt(brand),
        userPayload: snapshot,
      });
      const persist = await writeAiSignals(brand.id, "site-tag-ai", ai.signals);
      results.push({
        brandId: brand.id,
        domain: brand.domain,
        created: persist.created,
        deduped: persist.deduped,
        tokens: ai.tokensUsed,
      });
      console.info(`[signals-ai] brand: ${brand.id} tokens: ${ai.tokensUsed ?? "?"} created: ${persist.created} deduped: ${persist.deduped}`);
    } catch (err) {
      const msg = err instanceof Error ? err.message : "unknown";
      results.push({ brandId: brand.id, domain: brand.domain, error: msg });
      console.error(`[signals-ai] brand ${brand.id} failed:`, msg);
    }
  }

  // Persist the last successfully-started brand as the cursor for the next run.
  // If the budget was exhausted mid-page the next run resumes where we stopped.
  // When we reached the end of the page (brands.length < PAGE_SIZE) reset so
  // the next invocation starts from the beginning.
  const lastBrandId = brands.length > 0 ? brands[brands.length - 1].id : null;
  const nextCursorId = brands.length >= PAGE_SIZE ? lastBrandId : null;
  await prisma.cronCursor.upsert({
    where: { name: CRON_NAME },
    create: { name: CRON_NAME, brandId: nextCursorId },
    update: { brandId: nextCursorId },
  });

  const totals = results.reduce(
    (acc, r) => ({
      created: acc.created + (r.created ?? 0),
      deduped: acc.deduped + (r.deduped ?? 0),
      tokens: acc.tokens + (r.tokens ?? 0),
      skipped: acc.skipped + (r.skipped ? 1 : 0),
    }),
    { created: 0, deduped: 0, tokens: 0, skipped: 0 },
  );
  log.info("system", `[cron/signals-site-tag-ai] ${brands.length} brands · ${totals.created} new · ${totals.deduped} deduped · ${totals.tokens} tokens · ${totals.skipped} skipped`, {
    metadata: { brands: brands.length, ...totals, durationMs: Date.now() - startedAt },
  });

  return NextResponse.json({ brands: brands.length, ...totals, cursorReset: nextCursorId === null, results });
}

// ─── System prompt ─────────────────────────────────────────────────────────

function buildSystemPrompt(brand: { name: string; domain: string; industry: string | null }): string {
  const industry = brand.industry ? `, ${brand.industry}` : "";
  return `You are an SEO and UX analyst for ${brand.name} (${brand.domain}${industry}).
Analyze the following website activity data from the last hour and identify actionable signals.
For each signal, return JSON with:
- title (concise, specific, max 200 chars)
- description (1-2 sentences explaining the issue and impact, max 600 chars)
- category: "issue" | "opportunity" | "win" | "revenue"
- priority: "high" | "medium" | "low"
- signalType (short snake_case label, e.g. "missing_meta", "new_page_no_schema", "form_abandonment", "high_exit_page", "js_error_spike", "conversion_opportunity")
- suggestedAction (what the user should do, max 600 chars)
- affectedUrl (specific page URL if applicable)

Look for: form abandonment patterns, new pages without conversion tracking, 404 spikes, JS errors impacting UX, pages getting traffic but zero conversions, outbound-click leaks to competitors, content/metadata gaps on newly-seen pages.

Return ONLY a JSON array. No preamble. Maximum 8 signals. If nothing actionable, return [].`;
}

// ─── Snapshot builder ──────────────────────────────────────────────────────

interface Snapshot {
  pageViewsCount: number;
  sessionsCount: number;
  newPages: string[];
  topPages: Array<{ url: string; views: number }>;
  formAbandonment: Array<{ page: string; starts: number; submits: number }>;
  http404Count: number;
  top404s: string[];
  jsErrorCount: number;
  topOutboundDestinations: Array<{ page: string; count: number }>;
  highTrafficZeroConversion: string[];
}

/** Builds the per-brand data snapshot sent to the LLM. Keeps the payload
 *  tight — large Prisma result sets would blow the prompt budget and
 *  inflate token cost. */
async function buildSiteTagSnapshot(brandId: string): Promise<Snapshot | null> {
  const hourAgo = new Date(Date.now() - 60 * 60 * 1000);
  const dayAgo = new Date(Date.now() - 86_400_000);

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

  const [
    pageViews,
    sessionsCount,
    formStarts,
    formSubmits,
    errors404,
    jsErrors,
    outboundClicks,
    conversionsByPage,
  ] = await Promise.all([
    prisma.trackedEvent.findMany({
      where: { trackedSiteId: site.id, eventType: "page_view", timestamp: { gte: hourAgo } },
      select: { pageUrl: true },
      take: 500,
    }),
    prisma.trackedSession.count({
      where: { trackedSiteId: site.id, startedAt: { gte: hourAgo } },
    }),
    prisma.trackedEvent.groupBy({
      by: ["pageUrl"],
      where: { trackedSiteId: site.id, eventType: "form_start", timestamp: { gte: hourAgo } },
      _count: true,
    }).catch(() => []),
    prisma.trackedEvent.groupBy({
      by: ["pageUrl"],
      where: { trackedSiteId: site.id, eventType: "form_submit", timestamp: { gte: hourAgo } },
      _count: true,
    }).catch(() => []),
    prisma.siteEvent.findMany({
      where: { brandId, eventType: "http_error", timestamp: { gte: hourAgo } },
      select: { pageUrl: true },
      take: 50,
    }).catch(() => []),
    prisma.siteEvent.findMany({
      where: { brandId, eventType: "js_error", timestamp: { gte: hourAgo } },
      select: { pageUrl: true },
      take: 50,
    }).catch(() => []),
    prisma.siteEvent.groupBy({
      by: ["pageUrl"],
      where: { brandId, eventType: "outbound_click", timestamp: { gte: hourAgo } },
      _count: true,
    }).catch(() => []),
    // intentionally unfiltered: cron job; counts all rows for system monitoring
    prisma.siteConversion.groupBy({
      by: ["pageUrl"],
      where: { brandId, timestamp: { gte: dayAgo } },
      _count: true,
    }).catch(() => []),
  ]);

  // Top pages by view count in the last hour
  const viewsByPage = new Map<string, number>();
  for (const pv of pageViews) {
    viewsByPage.set(pv.pageUrl, (viewsByPage.get(pv.pageUrl) ?? 0) + 1);
  }
  const topPages = [...viewsByPage.entries()]
    .sort((a, b) => b[1] - a[1])
    .slice(0, 5)
    .map(([url, views]) => ({ url, views }));

  // New pages: seen in the last hour but not in the preceding 23 hours
  const recentPages = Array.from(new Set(pageViews.map((p) => p.pageUrl)));
  let newPages: string[] = [];
  if (recentPages.length > 0) {
    const historicalSet = new Set(
      (
        await prisma.trackedEvent.findMany({
          where: {
            trackedSiteId: site.id,
            eventType: "page_view",
            timestamp: { gte: dayAgo, lt: hourAgo },
            pageUrl: { in: recentPages },
          },
          select: { pageUrl: true },
          distinct: ["pageUrl"],
        }).catch(() => [])
      ).map((p) => p.pageUrl),
    );
    newPages = recentPages.filter((p) => !historicalSet.has(p)).slice(0, 10);
  }

  // Form abandonment: starts but 0 submits on the same page (min 3 starts)
  const submitsByPage = new Map(formSubmits.map((r) => [r.pageUrl, r._count]));
  const formAbandonment = formStarts
    .filter((r) => r._count >= 3 && (submitsByPage.get(r.pageUrl) ?? 0) === 0)
    .map((r) => ({ page: r.pageUrl, starts: r._count, submits: 0 }))
    .slice(0, 10);

  // High-traffic zero-conversion pages (24h window for conversions)
  const convByPage = new Set(conversionsByPage.map((r) => r.pageUrl));
  const highTrafficZeroConversion = topPages
    .filter((p) => p.views >= 10 && !convByPage.has(p.url))
    .map((p) => p.url);

  return {
    pageViewsCount: pageViews.length,
    sessionsCount,
    newPages,
    topPages,
    formAbandonment,
    http404Count: errors404.length,
    top404s: Array.from(new Set(errors404.map((e) => e.pageUrl))).slice(0, 5),
    jsErrorCount: jsErrors.length,
    topOutboundDestinations: outboundClicks.slice(0, 5).map((r) => ({ page: r.pageUrl, count: r._count })),
    highTrafficZeroConversion,
  };
}
