/**
 * GET /api/admin/cost-center?range=thisMonth|lastMonth|last3m|30d|90d
 *
 * Platform cost rollup. Five parallel `count()`s + two tiny AI
 * aggregates (range-scoped + all-time) + a live DataForSEO user
 * balance lookup. Historical groupBys / pg_stat raw queries /
 * per-brand joins were removed when they started 504'ing on the
 * Neon pooler; richer breakdowns will come back as cheap
 * equivalents exist.
 *
 * Sources:
 *   • AI cost:  sum(AiInteraction.costEstimate) for the window —
 *     this is the authoritative per-call cost the AI router writes
 *     at insert time (same source /admin/ai-monitor displays).
 *     If the range is empty but we have historical data, we fall
 *     back to the all-time total and flag that via ai.note.
 *   • SERP cost: live DataForSEO user_data API (total_money_spent
 *     + money_balance). Falls back to note text if creds aren't
 *     present or the call errors.
 *
 * The response shape still matches the CostPayload interface the
 * /admin/ai-usage page consumes. Added optional note / balance /
 * spent fields that the UI renders conditionally.
 *
 * Superadmin-only.
 */

import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { requireSuperadmin, withAdminTiming } from "@/lib/admin-auth";
import { SERVICE_LABELS } from "@/lib/cost-config";

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

type Range = "thisMonth" | "lastMonth" | "last3m" | "30d" | "90d";

function rangeToWindow(
  r: Range,
): { from: Date; to: Date; days: number; prevFrom: Date; prevTo: Date } {
  const now = new Date();
  if (r === "thisMonth") {
    const from = new Date(now.getFullYear(), now.getMonth(), 1);
    const to = new Date(now.getFullYear(), now.getMonth() + 1, 1);
    const prevFrom = new Date(now.getFullYear(), now.getMonth() - 1, 1);
    return { from, to, days: Math.max(1, Math.ceil((to.getTime() - from.getTime()) / 86_400_000)), prevFrom, prevTo: from };
  }
  if (r === "lastMonth") {
    const from = new Date(now.getFullYear(), now.getMonth() - 1, 1);
    const to = new Date(now.getFullYear(), now.getMonth(), 1);
    const prevFrom = new Date(now.getFullYear(), now.getMonth() - 2, 1);
    return { from, to, days: Math.max(1, Math.ceil((to.getTime() - from.getTime()) / 86_400_000)), prevFrom, prevTo: from };
  }
  if (r === "last3m") {
    const from = new Date(now.getFullYear(), now.getMonth() - 3, 1);
    const to = now;
    const prevFrom = new Date(now.getFullYear(), now.getMonth() - 6, 1);
    return { from, to, days: Math.max(1, Math.ceil((to.getTime() - from.getTime()) / 86_400_000)), prevFrom, prevTo: from };
  }
  const days = r === "90d" ? 90 : 30;
  const to = now;
  const from = new Date(now.getTime() - days * 86_400_000);
  const prevFrom = new Date(from.getTime() - days * 86_400_000);
  return { from, to, days, prevFrom, prevTo: from };
}

/** Queries DataForSEO's user_data endpoint for the live balance +
 *  lifetime spend. Returns null when creds are missing, the call
 *  errors, or the response shape is off — every failure path is
 *  silent so a vendor outage never 500s the cost-center route. */
async function fetchDataForSeoUsage(): Promise<
  { balance: number; spent: number; totalPaid: number } | null
> {
  const login = process.env.DATAFORSEO_LOGIN;
  const password = process.env.DATAFORSEO_PASSWORD;
  if (!login || !password) return null;

  try {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 4_000);
    const auth = "Basic " + Buffer.from(`${login}:${password}`).toString("base64");
    const r = await fetch("https://api.dataforseo.com/v3/appendix/user_data", {
      method: "GET",
      headers: { Authorization: auth, "Content-Type": "application/json" },
      signal: controller.signal,
    }).finally(() => clearTimeout(timeout));
    if (!r.ok) return null;
    const body = (await r.json()) as {
      tasks?: Array<{ result?: Array<{ money?: { balance?: number; total?: number } }> }>;
    };
    const money = body?.tasks?.[0]?.result?.[0]?.money;
    if (!money) return null;
    const balance = typeof money.balance === "number" ? money.balance : 0;
    // `money.total` in DataForSEO's response is the lifetime total
    // PAID INTO the account (deposits), NOT lifetime spend. Before
    // this fix we surfaced `total` directly as "spent", which
    // produced a $101 figure on an account that had only burned
    // $0.27 — wildly inflating the Cost Center total and masking
    // real variable costs. Lifetime spend is deposits minus current
    // balance. Clamped at 0 in case of timing skew where the API
    // snapshot shows balance > total for a moment right after a
    // refund/credit.
    const totalPaid = typeof money.total === "number" ? money.total : 0;
    const spent = Math.max(0, totalPaid - balance);
    return { balance, spent, totalPaid };
  } catch {
    return null;
  }
}

export async function GET(req: Request) {
  const denied = await requireSuperadmin();
  if (denied) return denied;

  const { searchParams } = new URL(req.url);
  const range = (searchParams.get("range") ?? "thisMonth") as Range;
  const { from, to, days, prevFrom, prevTo } = rangeToWindow(range);

  return NextResponse.json(await withAdminTiming("admin.cost-center", async () => {
    // All cheap DB queries + the DataForSEO probe kicked off in
    // one parallel batch. Each catch()/null-fallback keeps a single
    // failure local to its own metric.
    const [
      eventCount,
      sessionCount,
      conversionCount,
      userCount,
      aiWindow,
      aiAll,
      dfsUsage,
    ] = await Promise.all([
      prisma.trackedEvent.count().catch(() => 0),
      prisma.trackedSession.count().catch(() => 0),
      // intentionally unfiltered: admin-only endpoint; needs raw or all-time counts
      prisma.siteConversion.count().catch(() => 0),
      prisma.user.count().catch(() => 0),
      // Range-scoped AI — authoritative cost from costEstimate.
      prisma.aiInteraction
        .aggregate({
          where: { timestamp: { gte: from, lt: to } },
          _count: true,
          _sum: { costEstimate: true, tokensUsed: true },
        })
        .catch(() => ({ _count: 0, _sum: { costEstimate: 0 as number | null, tokensUsed: 0 as number | null } })),
      // All-time AI — used for the fallback when the selected
      // window is empty but we have historical data.
      prisma.aiInteraction
        .aggregate({ _count: true, _sum: { costEstimate: true, tokensUsed: true } })
        .catch(() => ({ _count: 0, _sum: { costEstimate: 0 as number | null, tokensUsed: 0 as number | null } })),
      fetchDataForSeoUsage(),
    ]);

    // Prefer the stored costEstimate; fall back to a $15/M blended
    // token rate when costEstimate is null or 0 for every row
    // (historical rows predating the cost logger, or a provider
    // outage). This mirrors how /admin/ai-monitor surfaces the
    // same figure.
    function costFromAgg(
      agg: { _count: number; _sum: { costEstimate: number | null; tokensUsed: number | null } },
    ): { calls: number; tokens: number; cost: number } {
      const calls = typeof agg._count === "number" ? agg._count : 0;
      const tokens = agg._sum?.tokensUsed ?? 0;
      const stored = agg._sum?.costEstimate ?? 0;
      const cost = stored > 0 ? stored : (tokens / 1_000_000) * 15;
      return { calls, tokens, cost };
    }
    const win = costFromAgg(aiWindow);
    const all = costFromAgg(aiAll);

    // Range-empty fallback — if the selected window has zero
    // interactions but we have all-time history, surface that so
    // the card doesn't render as a flat zero.
    const useFallback = win.calls === 0 && all.calls > 0;
    const aiCalls = useFallback ? all.calls : win.calls;
    const aiCost = useFallback ? all.cost : win.cost;
    const aiTokens = useFallback ? all.tokens : win.tokens;
    const aiNote = useFallback
      ? `No AI calls in the selected window — showing all-time total (${all.calls.toLocaleString()} calls)`
      : undefined;

    // Storage estimate: rows × ~500B average JSON payload, at
    // $0.75/GB-month (Neon Launch tier).
    const dbRows = eventCount + sessionCount + conversionCount;
    const dbStorageMB = Math.round((dbRows * 500) / 1024 / 1024);
    const dbCost = ((dbRows * 500) / 1024 / 1024 / 1024) * 0.75;

    // Fixed / live service estimates.
    const vercelCost = 20;
    const clerkCost = userCount < 10_000 ? 0 : (userCount - 10_000) * 0.02;
    const serpCost = dfsUsage ? dfsUsage.spent : 0;
    const serpBalance = dfsUsage ? dfsUsage.balance : null;
    const serpNote = dfsUsage
      ? `Balance: $${dfsUsage.balance.toFixed(2)} · Spent: $${dfsUsage.spent.toFixed(2)} (lifetime spend since account creation — verify against DataForSEO billing)`
      : "DataForSEO credentials not configured — set DATAFORSEO_LOGIN / DATAFORSEO_PASSWORD to pull live balance";
    const emailCost = 0;

    const totalCost = aiCost + dbCost + vercelCost + clerkCost + serpCost + emailCost;

    return {
      scope: {
        range,
        fromISO: from.toISOString(),
        toISO: to.toISOString(),
        days,
        previousFromISO: prevFrom.toISOString(),
        previousToISO: prevTo.toISOString(),
      },
      totalCost: Math.round(totalCost * 100) / 100,
      previousTotalCost: Math.round(totalCost * 100) / 100,
      trendPct: 0,
      alerts: [],
      services: {
        ai: {
          calls: aiCalls,
          inputTokens: 0,
          outputTokens: 0,
          cost: Math.round(aiCost * 10000) / 10000,
          avgCostPerCall: aiCalls > 0 ? Math.round((aiCost / aiCalls) * 10000) / 10000 : 0,
          byModel: [],
          byFeature: [],
          dailyTrend: [],
          tokens: aiTokens,
          totalCalls: all.calls,
          totalCost: Math.round(all.cost * 10000) / 10000,
          note: aiNote,
        },
        database: {
          sizeMb: dbStorageMB,
          rowCounts: [
            { table: "TrackedEvent", rows: eventCount },
            { table: "TrackedSession", rows: sessionCount },
            { table: "SiteConversion", rows: conversionCount },
          ],
          costEstimate: Math.round(dbCost * 100) / 100,
          growthRateMbPerDay: 0,
          tier: "launch",
        },
        auth: {
          mau: userCount,
          authentications: 0,
          costEstimate: Math.round(clerkCost * 100) / 100,
        },
        serp: {
          callsTotal: 0,
          byEndpoint: { serp: 0, keywords: 0, backlinks: 0, rankTracker: 0 },
          cost: Math.round(serpCost * 100) / 100,
          balance: serpBalance !== null ? Math.round(serpBalance * 100) / 100 : null,
          spent: Math.round(serpCost * 100) / 100,
          note: serpNote,
        },
        hosting: {
          invocations: 0,
          bandwidthGbEstimate: 0,
          costEstimate: vercelCost,
        },
        email: {
          sent: 0,
          cost: emailCost,
        },
        siteTag: {
          serves: 0,
          collects: eventCount,
          bandwidthMb: 0,
        },
      },
      byBrand: await buildPerBrandCosts(from, to),
      projection: {
        nextMonthEstimate: Math.round(totalCost * 100) / 100,
        breakdown: {
          ai: Math.round(aiCost * 100) / 100,
          database: Math.round(dbCost * 100) / 100,
          auth: Math.round(clerkCost * 100) / 100,
          serp: Math.round(serpCost * 100) / 100,
          hosting: vercelCost,
          email: emailCost,
        },
      },
      labels: SERVICE_LABELS,
      generatedAt: new Date().toISOString(),
    };
  }));
}

async function buildPerBrandCosts(from: Date, to: Date) {
  try {
    const [aiByBrand, brands, sites] = await Promise.all([
      prisma.aiInteraction.groupBy({
        by: ["brandId"],
        where: { timestamp: { gte: from, lt: to } },
        _count: true,
        _sum: { costEstimate: true, tokensUsed: true },
      }).catch(() => [] as Array<{ brandId: string; _count: number; _sum: { costEstimate: number | null; tokensUsed: number | null } }>),
      prisma.brand.findMany({ select: { id: true, name: true } }).catch(() => []),
      prisma.trackedSite.findMany({ select: { id: true, brandId: true } }).catch(() => []),
    ]);

    const nameMap = new Map(brands.map((b) => [b.id, b.name]));
    const siteMap = new Map(sites.map((s) => [s.brandId, s.id]));
    const allBrandIds = [...new Set([
      ...aiByBrand.map((r) => r.brandId).filter((id): id is string => !!id),
      ...sites.map((s) => s.brandId).filter((id): id is string => !!id),
    ])];

    const [eventsByBrand, convsByBrand] = await Promise.all([
      sites.length > 0
        ? prisma.trackedEvent.groupBy({
            by: ["trackedSiteId"],
            where: { trackedSiteId: { in: sites.map((s) => s.id) }, timestamp: { gte: from, lt: to } },
            _count: true,
          }).catch(() => [] as Array<{ trackedSiteId: string; _count: number }>)
        : [],
      allBrandIds.length > 0
        // intentionally unfiltered: admin-only endpoint; needs raw or all-time counts
        ? prisma.siteConversion.groupBy({
            by: ["brandId"],
            where: { brandId: { in: allBrandIds as string[] }, timestamp: { gte: from, lt: to } },
            _count: true,
          }).catch(() => [] as Array<{ brandId: string; _count: number }>)
        : [],
    ]);

    const siteIdToBrand = new Map(sites.map((s) => [s.id, s.brandId]).filter((e): e is [string, string] => !!e[1]));
    const eventsByBrandMap = new Map<string, number>();
    for (const e of eventsByBrand) {
      const bid = siteIdToBrand.get(e.trackedSiteId);
      if (bid) eventsByBrandMap.set(bid, (eventsByBrandMap.get(bid) ?? 0) + (typeof e._count === "number" ? e._count : 0));
    }
    const convsMap = new Map(convsByBrand.map((c) => [c.brandId, typeof c._count === "number" ? c._count : 0]));
    const aiMap = new Map<string, (typeof aiByBrand)[number]>();
    for (const a of aiByBrand) { if (a.brandId) aiMap.set(a.brandId, a); }

    const rows = allBrandIds.map((bid) => {
      const ai = aiMap.get(bid);
      const aiCost = ai?._sum.costEstimate ?? 0;
      const aiCalls = typeof ai?._count === "number" ? ai._count : 0;
      const events = eventsByBrandMap.get(bid) ?? 0;
      const dbCostEstimate = Math.round(events * 500 / 1024 / 1024 * 0.75 * 100) / 100;
      return {
        brandId: bid,
        brandName: nameMap.get(bid) ?? "Unknown",
        aiCost: Math.round(aiCost * 10000) / 10000,
        aiCalls,
        eventCount: events,
        dbCostEstimate,
        totalCost: Math.round((aiCost + dbCostEstimate) * 10000) / 10000,
      };
    }).filter((r) => r.totalCost > 0 || r.aiCalls > 0 || r.eventCount > 0)
      .sort((a, b) => b.totalCost - a.totalCost)
      .slice(0, 25);

    return rows;
  } catch (err) {
    console.error("[cost-center] byBrand failed:", err instanceof Error ? err.message : String(err));
    return [];
  }
}
