/**
 * Rank Tracker — GSC backfill
 * ─────────────────────────────
 * When a keyword is added to the Rank Tracker, we can seed its position
 * history from Google Search Console. That way the chart shows a real
 * 90-day trend on day one instead of a single dot at "day added."
 *
 * IMPORTANT — why we don't just read the GscQuery table:
 * The gsc-sync service stores GscQuery rows aggregated across the entire
 * sync window (one row per query, dated to the period's end date — see
 * src/lib/services/gsc-sync.ts:95-119). That gives one data point per
 * keyword regardless of how long the brand has been connected, so reading
 * GscQuery alone produces a single dot.
 *
 * To get real per-day history we hit the Google Search Console API
 * directly with `dimensions: ["date"]` + a `query` filter. That returns
 * one row per day for the specific keyword. We then write a
 * KeywordRanking row per day, deduplicating by date so re-runs are
 * idempotent and existing live DataForSEO checks aren't overwritten.
 *
 * Two public entry points:
 *
 *   • backfillKeyword(brandId, trackedKeywordId) — called fire-and-forget
 *     from POST /api/rank-tracking/keywords right after the TrackedKeyword
 *     row is created.
 *
 *   • backfillBrand(brandId) — called by POST /api/rank-tracking/backfill
 *     to seed every active TrackedKeyword for the brand at once.
 */

import { prisma } from "@/lib/prisma";
import { getCredentials } from "@/lib/services/integration-credentials";
import { apiCallAuthed } from "@/lib/integrations/types";

const BACKFILL_DAYS = 90;
const GSC_API_BASE = "https://www.googleapis.com/webmasters/v3";

export interface BackfillSummary {
  /** Rows inserted across all keywords in this run. */
  backfilled: number;
  /** Rows skipped because the date was already covered by a prior row. */
  skipped: number;
  /** Keywords that had at least one row inserted. */
  keywordsTouched: number;
  /** Keywords scanned but for which GSC returned no data. */
  keywordsWithNoData: number;
  /** True if GSC isn't connected / no siteUrl configured. */
  noGscConnection?: boolean;
}

interface GscDayRow {
  date: string; // YYYY-MM-DD
  position: number;
  impressions: number;
  clicks: number;
}

/**
 * Fetch per-day position history for a single keyword from the GSC API.
 * Uses `dimensions: ["date"]` + `query` filter so we get one row per day
 * for that exact keyword (case-insensitive match on Google's side).
 *
 * Returns null when GSC isn't connected or the API call fails — the
 * caller falls back to the GscQuery table.
 */
async function fetchGscDailyHistory(
  brandId: string,
  keyword: string,
): Promise<GscDayRow[] | null> {
  const creds = await getCredentials(brandId, "gsc");
  if (!creds?.accessToken) return null;
  const siteUrl = (creds.siteUrl as string) || (creds.accountId as string);
  if (!siteUrl) return null;

  const endDate = new Date().toISOString().split("T")[0];
  const startDate = new Date(Date.now() - BACKFILL_DAYS * 86_400_000)
    .toISOString().split("T")[0];

  const res = await apiCallAuthed(
    `${GSC_API_BASE}/sites/${encodeURIComponent(siteUrl)}/searchAnalytics/query`,
    {
      method: "POST",
      body: {
        startDate,
        endDate,
        dimensions: ["date"],
        // GSC accepts "contains" / "equals" — equals is exact match
        // (case-insensitive on their side, matching how Google clusters
        // queries internally).
        dimensionFilterGroups: [{
          filters: [{ dimension: "query", operator: "equals", expression: keyword }],
        }],
        rowLimit: 25_000,
      },
    },
    { brandId, integrationId: "gsc" },
  );

  if (!res.ok) {
    console.info("[rank-tracker-backfill] GSC API failed", {
      brandId, keyword, status: res.status, error: res.error,
    });
    return null;
  }

  const data = res.data as { rows?: Array<{ keys: string[]; clicks: number; impressions: number; position: number }> };
  const rows = data.rows ?? [];
  return rows
    .map((r) => ({
      date: r.keys[0],
      position: r.position,
      impressions: r.impressions,
      clicks: r.clicks,
    }))
    .filter((r) => r.date && !Number.isNaN(Date.parse(r.date)));
}

/**
 * Aggregate fallback: read GscQuery rows for the keyword (case-insensitive)
 * and group by day. In practice this only ever returns 1 row per query
 * because of how gsc-sync stores aggregated query data, but it lets us
 * still show *something* if the GSC API call itself fails.
 */
async function fetchGscQueryFallback(brandId: string, keyword: string): Promise<GscDayRow[]> {
  const since = new Date(Date.now() - BACKFILL_DAYS * 86_400_000);
  const rows = await prisma.gscQuery.findMany({
    where: {
      brandId,
      query: { equals: keyword, mode: "insensitive" },
      date: { gte: since },
    },
    select: { date: true, position: true, impressions: true, clicks: true },
    orderBy: { date: "asc" },
  });
  return rows.map((r) => ({
    date: r.date.toISOString().split("T")[0],
    position: r.position,
    impressions: r.impressions,
    clicks: r.clicks,
  }));
}

/**
 * Backfill a single tracked keyword from GSC history. Returns a summary
 * scoped to that keyword — callers typically don't inspect it (the POST
 * handler fires-and-forgets) but the batch path sums them.
 */
export async function backfillKeyword(
  brandId: string,
  trackedKeywordId: string,
): Promise<BackfillSummary> {
  const kw = await prisma.trackedKeyword.findUnique({
    where: { id: trackedKeywordId },
    select: { id: true, keyword: true, brandId: true },
  });
  if (!kw || kw.brandId !== brandId) {
    return { backfilled: 0, skipped: 0, keywordsTouched: 0, keywordsWithNoData: 0 };
  }

  // Try the GSC API first for true per-day history.
  let dayRows = await fetchGscDailyHistory(brandId, kw.keyword);
  let usedApi = dayRows !== null;
  if (!dayRows) {
    // GSC not connected or API failed — fall back to whatever's in the
    // GscQuery table (typically a single aggregated row).
    dayRows = await fetchGscQueryFallback(brandId, kw.keyword);
  }

  console.info("[rank-tracker-backfill] keyword scan", {
    brandId, keyword: kw.keyword, rowsFound: dayRows.length, source: usedApi ? "gsc-api" : "gsc-query-table",
  });

  if (dayRows.length === 0) {
    return { backfilled: 0, skipped: 0, keywordsTouched: 0, keywordsWithNoData: 1 };
  }

  // Aggregate by YYYY-MM-DD so we emit exactly one ranking row per day.
  // Impression-weighted position handles the rare case of multiple rows
  // on the same day (only happens with the GscQuery fallback path; the
  // GSC API path returns one row per date by construction).
  type DayAgg = { impressions: number; weightedPos: number; rawPos: number };
  const byDay = new Map<string, DayAgg>();
  for (const r of dayRows) {
    const agg = byDay.get(r.date) ?? { impressions: 0, weightedPos: 0, rawPos: 0 };
    const imps = r.impressions || 0;
    agg.weightedPos += (r.position || 0) * imps;
    agg.impressions += imps;
    agg.rawPos = r.position || agg.rawPos; // fallback when 0 impressions
    byDay.set(r.date, agg);
  }

  // Day-level dedup: skip only the specific dates that already have a
  // KeywordRanking row, NOT the entire keyword. The previous bug used
  // findFirst here, which short-circuited on a single existing row.
  const since = new Date(Date.now() - BACKFILL_DAYS * 86_400_000);
  const existingRows = await prisma.keywordRanking.findMany({
    where: { trackedKeywordId, checkedAt: { gte: since } },
    select: { checkedAt: true },
  });
  const covered = new Set(
    existingRows.map((r) => r.checkedAt.toISOString().split("T")[0]),
  );

  const toCreate: Array<{ trackedKeywordId: string; position: number; checkedAt: Date; source: string }> = [];
  let skipped = 0;
  for (const [day, agg] of byDay) {
    if (covered.has(day)) { skipped++; continue; }
    // GSC position is a float (impression-weighted avg). Round to match
    // the Int? column — fractional ranks aren't meaningful in the UI.
    const posAvg = agg.impressions > 0 ? agg.weightedPos / agg.impressions : agg.rawPos;
    if (posAvg <= 0) continue;
    const position = Math.max(1, Math.round(posAvg));
    toCreate.push({
      trackedKeywordId,
      position,
      // Anchored to UTC midday so the day bucketing matches the history
      // route's date grouping regardless of viewer timezone.
      checkedAt: new Date(`${day}T12:00:00.000Z`),
      source: "gsc_backfill",
    });
  }

  if (toCreate.length > 0) {
    await prisma.keywordRanking.createMany({ data: toCreate });
  }

  return {
    backfilled: toCreate.length,
    skipped,
    keywordsTouched: toCreate.length > 0 ? 1 : 0,
    keywordsWithNoData: 0,
  };
}

/**
 * Batch backfill: walk every active TrackedKeyword for a brand and
 * backfill each. Used by POST /api/rank-tracking/backfill.
 */
export async function backfillBrand(brandId: string): Promise<BackfillSummary> {
  // Detect "no GSC connection" once up front so we can surface a clearer
  // message in the UI instead of just reporting 0/0/0.
  const creds = await getCredentials(brandId, "gsc");
  const noGscConnection = !creds?.accessToken
    || !((creds.siteUrl as string) || (creds.accountId as string));

  const keywords = await prisma.trackedKeyword.findMany({
    where: { brandId, isActive: true },
    select: { id: true },
  });
  const totals: BackfillSummary = {
    backfilled: 0, skipped: 0, keywordsTouched: 0, keywordsWithNoData: 0,
  };
  for (const kw of keywords) {
    try {
      const r = await backfillKeyword(brandId, kw.id);
      totals.backfilled += r.backfilled;
      totals.skipped += r.skipped;
      totals.keywordsTouched += r.keywordsTouched;
      totals.keywordsWithNoData += r.keywordsWithNoData;
    } catch (err) {
      // Surface per-keyword failures in logs but don't abort the whole
      // brand — one malformed row shouldn't nuke the run.
      console.error(`[rank-tracker] backfill failed for keyword ${kw.id}:`, err instanceof Error ? err.message : err);
    }
  }
  if (noGscConnection) totals.noGscConnection = true;
  return totals;
}
