/**
 * Google Search Console — Sync & Storage Service
 * -------------------------------------------------
 * Takes raw GSC API response data and stores it in the database.
 * Also provides query functions for other modules to read GSC data.
 *
 * Data flow:
 *   1. GSC integration service calls the API → gets raw rows
 *   2. This service stores rows in GscQuery + GscPage tables
 *   3. Also updates MetricSnapshot with daily totals
 *   4. Dashboard, Content Briefs, and Launch Tracker read from these tables
 */

import { prisma } from "@/lib/prisma";
import { log } from "@/lib/logger";

// ─── Types ───────────────────────────────────────────────────────────────────

/** A single row from the GSC searchAnalytics/query response */
interface GscRawRow {
  keys: string[];       // [query, page] or [query] or [page] depending on dimensions
  clicks: number;
  impressions: number;
  ctr: number;          // 0.0 to 1.0
  position: number;
}

export interface GscSummary {
  totalClicks: number;
  totalImpressions: number;
  avgCtr: number;
  avgPosition: number;
  topQueries: Array<{ query: string; clicks: number; impressions: number; ctr: number; position: number }>;
  topPages: Array<{ page: string; clicks: number; impressions: number; ctr: number; position: number }>;
  period: { startDate: string; endDate: string };
}

// ─── Store GSC sync data ─────────────────────────────────────────────────────

/**
 * Store raw GSC rows in the database.
 * Called by the GSC integration service after a successful sync.
 *
 * @param brandId   - The brand these rows belong to
 * @param rows      - Raw rows from GSC API (query + page dimensions, aggregated)
 * @param startDate - Period start (YYYY-MM-DD)
 * @param endDate   - Period end (YYYY-MM-DD)
 * @param dailyRows - Per-day rows from GSC API (date dimension) — one row per day
 *                   for writing historical MetricSnapshot entries. When provided,
 *                   one snapshot is upserted per day. When omitted, a single
 *                   aggregated snapshot is stored at endDate (legacy behavior).
 */
export async function storeGscData(
  brandId: string,
  rows: GscRawRow[],
  startDate: string,
  endDate: string,
  dailyRows?: GscRawRow[],
  dailyPageRows?: GscRawRow[],
): Promise<{ queriesStored: number; pagesStored: number; snapshotsStored: number }> {
  const date = new Date(endDate);

  // Separate query-level and page-level data
  const queryMap = new Map<string, { clicks: number; impressions: number; ctr: number; position: number; count: number }>();
  const pageMap = new Map<string, { clicks: number; impressions: number; ctr: number; position: number; count: number }>();

  for (const row of rows) {
    const query = row.keys[0] ?? "";
    const page = row.keys[1] ?? "";

    // Aggregate by query
    if (query) {
      const existing = queryMap.get(query) ?? { clicks: 0, impressions: 0, ctr: 0, position: 0, count: 0 };
      existing.clicks += row.clicks;
      existing.impressions += row.impressions;
      existing.ctr += row.ctr;
      existing.position += row.position;
      existing.count++;
      queryMap.set(query, existing);
    }

    // Aggregate by page
    if (page) {
      const existing = pageMap.get(page) ?? { clicks: 0, impressions: 0, ctr: 0, position: 0, count: 0 };
      existing.clicks += row.clicks;
      existing.impressions += row.impressions;
      existing.ctr += row.ctr;
      existing.position += row.position;
      existing.count++;
      pageMap.set(page, existing);
    }
  }

  // Delete old data to avoid duplicates
  await prisma.gscQuery.deleteMany({ where: { brandId, date } });
  // For pages: if we have daily data, delete the full range; otherwise just the end date
  if (dailyPageRows && dailyPageRows.length > 0) {
    await prisma.gscPage.deleteMany({ where: { brandId, date: { gte: new Date(startDate), lte: new Date(endDate) } } });
  } else {
    await prisma.gscPage.deleteMany({ where: { brandId, date } });
  }

  // Store queries (top 500)
  const queryEntries = Array.from(queryMap.entries())
    .sort(([, a], [, b]) => b.clicks - a.clicks)
    .slice(0, 500);

  if (queryEntries.length > 0) {
    await prisma.gscQuery.createMany({
      data: queryEntries.map(([query, data]) => ({
        brandId,
        query,
        clicks: data.clicks,
        impressions: data.impressions,
        ctr: data.count > 0 ? data.ctr / data.count : 0,
        position: data.count > 0 ? data.position / data.count : 0,
        date,
      })),
    });
  }

  // Store pages — use daily per-page rows when available (for chart granularity)
  let pagesStored = 0;
  if (dailyPageRows && dailyPageRows.length > 0) {
    // Daily rows: keys = [date, page]
    const dailyBatch = dailyPageRows.map((row) => ({
      brandId,
      pageUrl: row.keys[1] ?? "",
      clicks: row.clicks,
      impressions: row.impressions,
      ctr: row.ctr,
      position: row.position,
      date: new Date(row.keys[0]),
    })).filter((r) => r.pageUrl);

    // Insert in batches of 1000
    for (let i = 0; i < dailyBatch.length; i += 1000) {
      await prisma.gscPage.createMany({ data: dailyBatch.slice(i, i + 1000) });
    }
    pagesStored = dailyBatch.length;
  } else {
    // Fallback: aggregated pages with single date
    const pageEntries = Array.from(pageMap.entries())
      .sort(([, a], [, b]) => b.clicks - a.clicks)
      .slice(0, 200);

    if (pageEntries.length > 0) {
      await prisma.gscPage.createMany({
        data: pageEntries.map(([pageUrl, data]) => ({
          brandId,
          pageUrl,
          clicks: data.clicks,
          impressions: data.impressions,
          ctr: data.count > 0 ? data.ctr / data.count : 0,
          position: data.count > 0 ? data.position / data.count : 0,
          date,
        })),
      });
      pagesStored = pageEntries.length;
    }
  }

  // Use aggregated page entries for cascade (always need this)
  const pageEntries = Array.from(pageMap.entries())
    .sort(([, a], [, b]) => b.clicks - a.clicks)
    .slice(0, 200);

  if (pageEntries.length > 0) {

    // ── Cascade GSC page data into PageRecord / PageDailyMetric ──
    // This is what makes the Launch Tracker show real search performance
    // for every page from the day it was first detected. For each GscPage
    // row, upsert a matching PageDailyMetric (if a PageRecord exists for
    // that URL) and update the PageRecord's aggregate totals.
    await cascadeGscToPageLifecycle(brandId, pageEntries, date);
  }

  // ── MetricSnapshot: write one row per day when per-day data is available ──
  let snapshotsStored = 0;
  if (dailyRows && dailyRows.length > 0) {
    // Each row has keys[0] = "YYYY-MM-DD" from the "date" dimension
    for (const dr of dailyRows) {
      const dayStr = dr.keys?.[0];
      if (!dayStr) continue;
      const dayDate = new Date(dayStr);
      if (isNaN(dayDate.getTime())) continue;

      await prisma.metricSnapshot.upsert({
        where: { brandId_date_source: { brandId, date: dayDate, source: "gsc" } },
        create: {
          brandId,
          date: dayDate,
          source: "gsc",
          organicClicks: dr.clicks,
          impressions: dr.impressions,
          avgPosition: Math.round(dr.position * 10) / 10,
          ctr: Math.round(dr.ctr * 1000) / 1000,
        },
        update: {
          organicClicks: dr.clicks,
          impressions: dr.impressions,
          avgPosition: Math.round(dr.position * 10) / 10,
          ctr: Math.round(dr.ctr * 1000) / 1000,
        },
      });
      snapshotsStored++;
    }
  } else {
    // Legacy fallback: store a single aggregated snapshot at endDate
    const totalClicks = rows.reduce((s, r) => s + r.clicks, 0);
    const totalImpressions = rows.reduce((s, r) => s + r.impressions, 0);
    const avgPosition = rows.length > 0 ? rows.reduce((s, r) => s + r.position, 0) / rows.length : 0;
    const avgCtr = totalImpressions > 0 ? totalClicks / totalImpressions : 0;

    await prisma.metricSnapshot.upsert({
      where: { brandId_date_source: { brandId, date, source: "gsc" } },
      create: {
        brandId,
        date,
        source: "gsc",
        organicClicks: totalClicks,
        impressions: totalImpressions,
        avgPosition: Math.round(avgPosition * 10) / 10,
        ctr: Math.round(avgCtr * 1000) / 1000,
      },
      update: {
        organicClicks: totalClicks,
        impressions: totalImpressions,
        avgPosition: Math.round(avgPosition * 10) / 10,
        ctr: Math.round(avgCtr * 1000) / 1000,
      },
    });
    snapshotsStored = 1;
  }

  log.info("integration", `Stored GSC data: ${snapshotsStored} snapshots, ${queryEntries.length} queries, ${pageEntries.length} pages`, {
    metadata: { brandId, startDate, endDate, snapshotsStored },
  });

  return { queriesStored: queryEntries.length, pagesStored, snapshotsStored };
}

// ─── Read functions (used by other modules) ──────────────────────────────────

/**
 * Get a summary of GSC data for a brand.
 * Used by Dashboard and AI Avatar.
 */
export async function getGscSummary(
  brandId: string,
  days: number = 28
): Promise<GscSummary | null> {
  const since = new Date(Date.now() - days * 86400000);

  const queries = await prisma.gscQuery.findMany({
    where: { brandId, date: { gte: since } },
    orderBy: { clicks: "desc" },
    take: 20,
  });

  const pages = await prisma.gscPage.findMany({
    where: { brandId, date: { gte: since } },
    orderBy: { clicks: "desc" },
    take: 20,
  });

  if (queries.length === 0 && pages.length === 0) return null;

  const totalClicks = queries.reduce((s, q) => s + q.clicks, 0);
  const totalImpressions = queries.reduce((s, q) => s + q.impressions, 0);

  return {
    totalClicks,
    totalImpressions,
    avgCtr: totalImpressions > 0 ? Math.round((totalClicks / totalImpressions) * 1000) / 10 : 0,
    avgPosition: queries.length > 0 ? Math.round((queries.reduce((s, q) => s + q.position, 0) / queries.length) * 10) / 10 : 0,
    topQueries: queries.slice(0, 10).map(q => ({
      query: q.query, clicks: q.clicks, impressions: q.impressions,
      ctr: Math.round(q.ctr * 1000) / 10, position: Math.round(q.position * 10) / 10,
    })),
    topPages: pages.slice(0, 10).map(p => ({
      page: p.pageUrl, clicks: p.clicks, impressions: p.impressions,
      ctr: Math.round(p.ctr * 1000) / 10, position: Math.round(p.position * 10) / 10,
    })),
    period: {
      startDate: since.toISOString().split("T")[0],
      endDate: new Date().toISOString().split("T")[0],
    },
  };
}

/**
 * Get GSC performance for specific pages.
 * Used by Page Launch Tracker to show post-launch search performance.
 */
export async function getGscPagePerformance(
  brandId: string,
  pageUrls: string[],
  days: number = 28
): Promise<Array<{ pageUrl: string; clicks: number; impressions: number; ctr: number; position: number }>> {
  if (pageUrls.length === 0) return [];

  const since = new Date(Date.now() - days * 86400000);
  const results: Array<{ pageUrl: string; clicks: number; impressions: number; ctr: number; position: number }> = [];

  for (const url of pageUrls) {
    const rows = await prisma.gscPage.findMany({
      where: { brandId, pageUrl: { contains: url }, date: { gte: since } },
    });

    if (rows.length > 0) {
      results.push({
        pageUrl: url,
        clicks: rows.reduce((s, r) => s + r.clicks, 0),
        impressions: rows.reduce((s, r) => s + r.impressions, 0),
        ctr: rows.length > 0 ? Math.round((rows.reduce((s, r) => s + r.ctr, 0) / rows.length) * 1000) / 10 : 0,
        position: rows.length > 0 ? Math.round((rows.reduce((s, r) => s + r.position, 0) / rows.length) * 10) / 10 : 0,
      });
    }
  }

  return results;
}

/**
 * Get top queries for a brand — used by SEO Content Briefs
 * to suggest keywords and show ranking opportunities.
 */
export async function getTopQueries(
  brandId: string,
  limit: number = 50,
  days: number = 28
): Promise<Array<{ query: string; clicks: number; impressions: number; ctr: number; position: number }>> {
  const since = new Date(Date.now() - days * 86400000);

  const queries = await prisma.gscQuery.findMany({
    where: { brandId, date: { gte: since } },
    orderBy: { clicks: "desc" },
    take: limit,
  });

  return queries.map(q => ({
    query: q.query,
    clicks: q.clicks,
    impressions: q.impressions,
    ctr: Math.round(q.ctr * 1000) / 10,
    position: Math.round(q.position * 10) / 10,
  }));
}

// ─── GSC → Page Lifecycle cascade ───────────────────────────────────────────

/**
 * Mirror GSC page-level data into PageDailyMetric + PageRecord aggregates.
 * Only pages that already exist in PageRecord (i.e. have been detected by
 * the Site Tag) get metrics written — GSC often reports URLs that the
 * visitor-driven Site Tag hasn't seen yet (e.g. deep pages). For those,
 * we create a stub PageRecord so the performance history isn't lost.
 */
async function cascadeGscToPageLifecycle(
  brandId: string,
  pageEntries: Array<[string, { clicks: number; impressions: number; ctr: number; position: number; count: number }]>,
  date: Date,
): Promise<void> {
  // Pull existing PageRecord URLs once to minimise DB round-trips
  const existingPages = await prisma.pageRecord.findMany({
    where: { brandId },
    select: { id: true, url: true, fullUrl: true },
  });
  const byUrl = new Map<string, { id: string; url: string }>();
  for (const p of existingPages) {
    byUrl.set(normalisePath(p.url), { id: p.id, url: p.url });
    byUrl.set(normalisePath(p.fullUrl), { id: p.id, url: p.url });
  }

  for (const [pageUrl, data] of pageEntries) {
    const path = normalisePath(pageUrl);
    let record = byUrl.get(path);

    if (!record) {
      // GSC knows about a page that the Site Tag hasn't seen yet. Create
      // a stub so we don't lose the historical performance data.
      const created = await prisma.pageRecord.create({
        data: {
          brandId,
          url: path,
          fullUrl: pageUrl,
          firstSeenAt: date,
          lastSeenAt: date,
          currentStatus: 200,
          totalClicks: data.clicks,
          totalImpressions: data.impressions,
          currentCtr: data.count > 0 ? data.ctr / data.count : 0,
          currentPosition: data.count > 0 ? data.position / data.count : 0,
        },
        select: { id: true, url: true },
      });
      record = created;
      byUrl.set(path, created);
    }

    const clicks = data.clicks;
    const impressions = data.impressions;
    const ctr = data.count > 0 ? data.ctr / data.count : 0;
    const avgPosition = data.count > 0 ? data.position / data.count : 0;

    await prisma.pageDailyMetric.upsert({
      where: { pageId_date: { pageId: record.id, date } },
      create: { pageId: record.id, date, clicks, impressions, ctr, avgPosition, statusCode: 200 },
      update: { clicks, impressions, ctr, avgPosition },
    });
  }

  // Recompute aggregate totals on PageRecord from the daily rows. This
  // runs once after all upserts so the numbers match what the UI shows.
  await recomputePageAggregates(brandId);
}

function normalisePath(input: string): string {
  try {
    const u = new URL(input);
    return (u.pathname || "/").replace(/\/+$/, "") || "/";
  } catch {
    return (input || "/").split("?")[0].split("#")[0].replace(/\/+$/, "") || "/";
  }
}

async function recomputePageAggregates(brandId: string): Promise<void> {
  // Aggregate all-time totals + most recent CTR/position per page. We do
  // this with a single groupBy and a separate "latest row" query to keep
  // it efficient even with many pages.
  const totals = await prisma.pageDailyMetric.groupBy({
    by: ["pageId"],
    where: { page: { brandId } },
    _sum: { clicks: true, impressions: true },
  });

  // Latest metrics per page (for currentCtr / currentPosition)
  const latestRows = await prisma.pageDailyMetric.findMany({
    where: { page: { brandId } },
    orderBy: { date: "desc" },
    select: { pageId: true, ctr: true, avgPosition: true, date: true },
  });
  const latestByPage = new Map<string, { ctr: number; avgPosition: number }>();
  for (const r of latestRows) {
    if (!latestByPage.has(r.pageId)) {
      latestByPage.set(r.pageId, { ctr: r.ctr, avgPosition: r.avgPosition });
    }
  }

  for (const t of totals) {
    const latest = latestByPage.get(t.pageId) ?? { ctr: 0, avgPosition: 0 };
    await prisma.pageRecord.update({
      where: { id: t.pageId },
      data: {
        totalClicks: t._sum.clicks ?? 0,
        totalImpressions: t._sum.impressions ?? 0,
        currentCtr: latest.ctr,
        currentPosition: latest.avgPosition,
      },
    });
  }
}
