/**
 * GET|POST /api/cron/signals-competitor-regional — 3-hourly AI competitive +
 * regional scan. For each brand with competitors OR GBP locations, assembles
 * a snapshot of competitor movement (new pages, content gaps, ranking shifts
 * via CompetitorSnapshot) and regional signals (recent GBP reviews, rating
 * shifts, local market context) and sends it to Claude for analysis.
 *
 * Signal types this cron can produce:
 *   • competitor_new_content   — competitor published pages on a topic
 *   • competitor_ranking_gain  — competitor overtook us on a keyword
 *   • content_gap_detected     — competitor ranks for topic we don't cover
 *   • local_review_trend       — review volume or rating shift
 *   • regional_opportunity     — local event / seasonal trend
 *   • competitor_threat        — aggressive competitor moves
 *
 * Same cost + auth shape as signals-site-tag-ai (sequential iteration,
 * 115s runtime cap, CRON_SECRET guard, one AI call per brand).
 */

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

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

const CRON_SECRET = process.env.CRON_SECRET ?? "";
const RUNTIME_CAP_MS = 115_000;

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

  // Only consider brands that have at least one competitor OR at least
  // one GBP location — otherwise the snapshot would be empty and the
  // AI call would just cost tokens for "[]".
  const brands = await prisma.brand.findMany({
    where: {
      isDemo: false,
      OR: [
        { competitors: { some: { isActive: true } } },
        { gbpLocations: { some: { isActive: true } } },
      ],
    },
    select: { id: true, name: true, domain: true, industry: true, location: true, locations: true },
  });

  interface Row {
    brandId: string;
    domain: string;
    created?: number;
    deduped?: number;
    tokens?: number;
    skipped?: "no_data" | "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 buildCompetitorRegionalSnapshot(brand.id);
      if (!snapshot || (snapshot.competitors.length === 0 && snapshot.gbpLocations.length === 0)) {
        results.push({ brandId: brand.id, domain: brand.domain, skipped: "no_data" });
        continue;
      }

      const ai = await callAiForSignals({
        brandIdForLog: brand.id,
        maxTokens: 2000,
        systemPrompt: buildSystemPrompt(brand),
        userPayload: snapshot,
      });
      const persist = await writeAiSignals(brand.id, "competitor-regional-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} source: competitor-regional 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} (competitor-regional) failed:`, msg);
    }
  }

  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-competitor-regional] ${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, results });
}

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

function buildSystemPrompt(brand: { name: string; domain: string; industry: string | null; location: string | null; locations: string[] }): string {
  const industry = brand.industry ?? "business";
  const locs = getBrandLocations(brand);
  const location = locs.length > 0 ? locs.join(", ") : "its regional market";
  return `You are a competitive intelligence analyst for ${brand.name} (${brand.domain}), a ${industry} in ${location}.
Analyze the following competitor and regional data. Identify actionable signals about:
- Competitor content moves (new pages, topics they're covering that we're not)
- Competitive position shifts (keywords where competitors gained/lost)
- Content gaps (topics competitors cover that we don't)
- Local market signals (review trends, sentiment shifts, new competitors)
- Regional opportunities (seasonal trends, local events, community topics)
- Competitor threats (aggressive moves in our space)

For each signal, return JSON with:
- title (max 200 chars)
- description (1-2 sentences, max 600 chars)
- category: "issue" | "opportunity" | "win" | "revenue"
- priority: "high" | "medium" | "low"
- signalType: one of "competitor_new_content", "competitor_ranking_gain", "content_gap_detected", "local_review_trend", "regional_opportunity", "competitor_threat"
- suggestedAction (max 600 chars)
- competitorName (when applicable)
- affectedUrl (when applicable)

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

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

interface CompetitorSummary {
  name: string;
  domain: string;
  totalPages: number;
  newPagesLast3d: Array<{ url: string; title: string | null; firstSeenAt: string }>;
  updatedPagesLast3d: Array<{ url: string; title: string | null; lastCheckedAt: string }>;
  latestSnapshot: {
    movement: string | null;
    threat: string | null;
    rankChange: number | null;
    aiMentionChange: number | null;
  } | null;
}

interface GbpLocationSummary {
  city: string | null;
  state: string | null;
  category: string | null;
  avgRating: number | null;
  totalReviews: number;
  recentReviewsLast7d: Array<{ rating: number; sentiment: string | null; topics: string[]; publishedAt: string }>;
  ratingShiftLast30d: number | null; // delta vs older reviews
}

interface Snapshot {
  competitors: CompetitorSummary[];
  gbpLocations: GbpLocationSummary[];
}

async function buildCompetitorRegionalSnapshot(brandId: string): Promise<Snapshot | null> {
  const now = Date.now();
  const threeDaysAgo = new Date(now - 3 * 86_400_000);
  const sevenDaysAgo = new Date(now - 7 * 86_400_000);
  const thirtyDaysAgo = new Date(now - 30 * 86_400_000);
  const sixtyDaysAgo = new Date(now - 60 * 86_400_000);

  const [competitors, gbpLocations] = await Promise.all([
    prisma.competitor.findMany({
      where: { brandId, isActive: true },
      select: {
        id: true, name: true, domain: true,
        snapshots: {
          orderBy: { crawledAt: "desc" },
          take: 1,
          select: { movement: true, threat: true, rankChange: true, aiMentionChange: true, totalPages: true },
        },
        pages: {
          where: {
            OR: [
              { firstSeenAt: { gte: threeDaysAgo } },
              { lastCheckedAt: { gte: threeDaysAgo }, status: "updated" },
            ],
          },
          select: { url: true, title: true, status: true, firstSeenAt: true, lastCheckedAt: true },
          take: 20,
        },
      },
      take: 10,
    }),
    prisma.gbpLocation.findMany({
      where: { brandId, isActive: true },
      select: {
        id: true, city: true, state: true, category: true, avgRating: true, totalReviews: true,
        reviews: {
          where: { publishedAt: { gte: thirtyDaysAgo } },
          orderBy: { publishedAt: "desc" },
          select: { rating: true, sentiment: true, topics: true, publishedAt: true },
          take: 50,
        },
      },
      take: 10,
    }),
  ]);

  const competitorSummaries: CompetitorSummary[] = competitors.map((c) => {
    const newPages = c.pages.filter((p) => p.firstSeenAt >= threeDaysAgo);
    const updatedPages = c.pages.filter((p) => p.status === "updated" && p.firstSeenAt < threeDaysAgo);
    return {
      name: c.name,
      domain: c.domain,
      totalPages: c.snapshots[0]?.totalPages ?? 0,
      newPagesLast3d: newPages.slice(0, 10).map((p) => ({
        url: p.url,
        title: p.title,
        firstSeenAt: p.firstSeenAt.toISOString(),
      })),
      updatedPagesLast3d: updatedPages.slice(0, 10).map((p) => ({
        url: p.url,
        title: p.title,
        lastCheckedAt: p.lastCheckedAt.toISOString(),
      })),
      latestSnapshot: c.snapshots[0]
        ? {
            movement: c.snapshots[0].movement ?? null,
            threat: c.snapshots[0].threat ?? null,
            rankChange: c.snapshots[0].rankChange ?? null,
            aiMentionChange: c.snapshots[0].aiMentionChange ?? null,
          }
        : null,
    };
  });

  const gbpSummaries: GbpLocationSummary[] = gbpLocations.map((loc) => {
    const recent = loc.reviews.filter((r) => r.publishedAt >= sevenDaysAgo);
    // Rating shift: compare avg of last-30d reviews against prior 30d if we have both windows.
    const recent30dAvg = loc.reviews.length > 0
      ? loc.reviews.reduce((s, r) => s + r.rating, 0) / loc.reviews.length
      : null;
    // We don't have reviews further than 30d in this selection — keep the
    // shift nullable; the AI doesn't need a deep baseline to spot a
    // surge. Caller can cross-reference avgRating with current avg.
    void sixtyDaysAgo;
    const ratingShift = recent30dAvg !== null && loc.avgRating !== null
      ? Math.round((recent30dAvg - loc.avgRating) * 100) / 100
      : null;
    return {
      city: loc.city,
      state: loc.state,
      category: loc.category,
      avgRating: loc.avgRating,
      totalReviews: loc.totalReviews,
      recentReviewsLast7d: recent.slice(0, 15).map((r) => ({
        rating: r.rating,
        sentiment: r.sentiment,
        topics: r.topics,
        publishedAt: r.publishedAt.toISOString(),
      })),
      ratingShiftLast30d: ratingShift,
    };
  });

  return { competitors: competitorSummaries, gbpLocations: gbpSummaries };
}
