/**
 * Performance Audit Runner
 * -------------------------
 * Runs Core Web Vitals audits across a small set of representative
 * pages (homepage + top traffic pages). Stores results per-URL
 * per-device in PerformanceAudit + PerformancePageScore.
 *
 * Timing constraints (hard):
 *   - MAX_PAGES=2         homepage + top-traffic page (see comments)
 *   - MAX_CONCURRENCY=1   serialised — one URL at a time to avoid
 *                         saturating the anonymous PageSpeed quota
 *                         with parallel calls that all 429.
 *   - PAGESPEED_TIMEOUT_MS=45000  per-call timeout; up to 3 attempts
 *                                 with 2s/4s exponential backoff.
 *   - WALL_BUDGET_MS=55000        total wall-clock the runner will
 *                                 spend before declaring done with
 *                                 whatever results are in the DB.
 *                                 Sits 5s under Vercel's 60s
 *                                 function deadline so the route
 *                                 can still return a clean response.
 *
 * Progress is persisted incrementally — every completed URL's
 * PerformancePageScore rows are flushed to the DB as that URL
 * finishes, so the client can poll
 * GET /api/performance-audit?brandId=... and watch
 * `progress.completedPages` count up in real time. If the runner
 * hits WALL_BUDGET_MS mid-run, everything completed up to that
 * point is kept and the audit is marked "completed" with whatever
 * was gathered (as opposed to "failed" — partial data is useful).
 */

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

export interface PerfAuditResult {
  auditId: string;
  status: "completed" | "failed" | "partial";
  message: string;
  pagesTested?: number;
}

interface PageScore {
  pageUrl: string;
  device: string;
  performance: number;
  accessibility: number;
  bestPractices: number;
  seo: number;
  lcpMs: number;
  inpMs: number;
  clsVal: number;
  fcpMs: number;
  ttfbMs: number;
  status: string;
}

const MAX_PAGES = 2;
// Serialised — one URL at a time. Running 2 concurrent URLs against
// the anonymous PageSpeed quota doubles the rate-limit exposure: each
// URL fires mobile + desktop, so 2 URLs = 4 in-flight calls, which
// reliably triggers 429s on the keyless tier. Sequential avoids the
// collision; the wall-budget check still bails before Vercel's ceiling.
const MAX_CONCURRENCY = 1;
// Per-call hard timeout. 45s covers heavy pages (healthcare/e-commerce
// with many third-party scripts) in practice. Must stay below
// WALL_BUDGET_MS so the budget-aware worker dispatch check
// (remaining < PAGESPEED_TIMEOUT_MS) can ever be false at t=0.
const PAGESPEED_TIMEOUT_MS = 45_000;
// Exponential backoff delays between retry attempts. 3 total attempts:
// attempt 1 → wait 2s → attempt 2 → wait 4s → attempt 3 → give up.
// Only retries on recoverable failures (timeout, 5xx, 429).
const RETRY_BACKOFFS_MS = [2_000, 4_000] as const;
const WALL_BUDGET_MS = 55_000;

/** Pick a representative set of URLs to test:
 *   1. Homepage (always — even if GSC + Site Tag are empty).
 *   2. Top Site Tag pages by visits (last 30 days) — real-visitor
 *      traffic including pages not yet in GSC.
 *   3. Top GSC pages by clicks (last 90 days).
 *  Returns absolute URLs (PageSpeed requires them), capped at MAX_PAGES.
 *
 *  Site Tag ranks above GSC because the tracker captures today's
 *  traffic reality (including fresh pages still building GSC history).
 *  Dedupe is by stripped trailing slash. */
async function pickPagesToTest(brandId: string, domain: string): Promise<string[]> {
  const homeUrl = domain.startsWith("http") ? domain : `https://${domain}`;
  const cleanHome = homeUrl.replace(/\/$/, "");
  const origin = homeUrl.replace(/\/$/, "");

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

  const thirtyDaysAgo = new Date(Date.now() - 30 * 86_400_000);
  const ninetyDaysAgo = new Date(Date.now() - 90 * 86_400_000);

  const [siteTagPages, gscPages] = await Promise.all([
    site?.id
      ? prisma.trackedEvent.groupBy({
          by: ["pageUrl"],
          where: { trackedSiteId: site.id, eventType: "page_view", timestamp: { gte: thirtyDaysAgo } },
          _count: true,
          orderBy: { _count: { pageUrl: "desc" } },
          take: MAX_PAGES * 2,
        }).catch(() => [])
      : Promise.resolve([]),
    prisma.gscPage.groupBy({
      by: ["pageUrl"],
      where: { brandId, date: { gte: ninetyDaysAgo }, clicks: { gt: 0 } },
      _sum: { clicks: true },
      orderBy: { _sum: { clicks: "desc" } },
      take: MAX_PAGES * 2,
    }).catch(() => []),
  ]);

  function absolutize(raw: string): string | null {
    if (!raw) return null;
    try {
      if (raw.startsWith("http")) return raw.replace(/\/$/, "");
      return `${origin}${raw.startsWith("/") ? raw : "/" + raw}`.replace(/\/$/, "");
    } catch { return null; }
  }

  const LOCALE_RE = /\/(es|fr|de|it|pt|ja|zh|ko|ru|ar|nl|sv|pl|tr|hi|id|th|vi)(\/|$)/i;
  const isLocalised = (raw: string): boolean => {
    try {
      const u = absolutize(raw);
      if (!u) return false;
      const path = new URL(u).pathname;
      return LOCALE_RE.test(path);
    } catch { return false; }
  };

  const partition = <T extends { pageUrl: string }>(rows: T[]): { en: T[]; localised: T[] } => {
    const en: T[] = [];
    const localised: T[] = [];
    for (const r of rows) {
      if (isLocalised(r.pageUrl)) localised.push(r);
      else en.push(r);
    }
    return { en, localised };
  };
  const siteTagPart = partition(siteTagPages);
  const gscPart = partition(gscPages);

  const urls = new Set<string>();
  urls.add(cleanHome);

  const addFrom = (rows: Array<{ pageUrl: string }>) => {
    for (const p of rows) {
      if (urls.size >= MAX_PAGES) break;
      const u = absolutize(p.pageUrl);
      if (u) urls.add(u);
    }
  };
  addFrom(siteTagPart.en);
  addFrom(gscPart.en);
  addFrom(siteTagPart.localised);
  addFrom(gscPart.localised);
  return Array.from(urls).slice(0, MAX_PAGES);
}

/** Map a raw PageSpeed API payload into a PageScore row. */
function payloadToScore(data: Record<string, unknown>, url: string, device: "mobile" | "desktop"): PageScore | null {
  const cats = (data.categories ?? {}) as Record<string, { score: number }>;
  if (!cats.performance) return null;
  const perf = Math.round((cats.performance.score ?? 0) * 100);
  const a11y = Math.round((cats.accessibility?.score ?? 0) * 100);
  const bp = Math.round((cats["best-practices"]?.score ?? 0) * 100);
  const seoScore = Math.round((cats.seo?.score ?? 0) * 100);

  const audits = (data.audits ?? {}) as Record<string, { numericValue?: number }>;
  const lcpMs = Math.round(audits["largest-contentful-paint"]?.numericValue ?? 0);
  const fcpMs = Math.round(audits["first-contentful-paint"]?.numericValue ?? 0);
  const clsVal = audits["cumulative-layout-shift"]?.numericValue ?? 0;
  const ttfbMs = Math.round(audits["server-response-time"]?.numericValue ?? 0);
  const inpMs = Math.round(audits["interaction-to-next-paint"]?.numericValue ?? 0);

  const status = perf >= 90 ? "pass" : perf >= 50 ? "average" : "fail";
  return {
    pageUrl: url, device, performance: perf, accessibility: a11y,
    bestPractices: bp, seo: seoScore, lcpMs, inpMs, clsVal, fcpMs, ttfbMs, status,
  };
}

async function scoreOneUrl(
  url: string,
  auditId: string,
  apiKey: string,
  index: number,
  total: number,
  deadlineAt?: number,
): Promise<{ url: string; scored: number; failureDetail: string | null }> {
  const t0 = Date.now();
  const batch: PageScore[] = [];

  async function fetchWithRetry(strategy: "mobile" | "desktop"): Promise<{ data: Record<string, unknown> | null; failedWith: string | null }> {
    let lastReason: string | null = null;
    for (let attempt = 0; attempt < 3; attempt++) {
      if (attempt > 0) {
        const delay = RETRY_BACKOFFS_MS[attempt - 1];
        console.info("[cwi:pagespeed] retrying after backoff", { url, strategy, attempt, delayMs: delay });
        await new Promise((r) => setTimeout(r, delay));
      }
      const result = await fetchPSI(url, strategy, apiKey);
      if (result.ok) return { data: result.data, failedWith: null };
      lastReason = result.reason;
      if (!result.retryable) {
        console.warn("[cwi:pagespeed] non-retryable failure, stopping", { url, strategy, reason: result.reason });
        break;
      }
      console.warn("[cwi:pagespeed] attempt failed, will retry", { url, strategy, attempt: attempt + 1, reason: result.reason });
    }
    return { data: null, failedWith: lastReason };
  }

  const { data: mobileData, failedWith: mobileFailedWith } = await fetchWithRetry("mobile");
  if (mobileData) {
    const score = payloadToScore(mobileData, url, "mobile");
    if (score) batch.push(score);
  }

  if (batch.length > 0 && batch[0].device === "mobile") {
    const canFitDesktop = !deadlineAt || (deadlineAt - Date.now()) >= PAGESPEED_TIMEOUT_MS;
    if (!canFitDesktop) {
      console.info("[cwi:audit] skipping desktop to stay within wall budget", { url, remainingMs: deadlineAt ? deadlineAt - Date.now() : null });
    } else {
      const { data: desktopData } = await fetchWithRetry("desktop");
      if (desktopData) {
        const score = payloadToScore(desktopData, url, "desktop");
        if (score) batch.push(score);
      }
    }
  }

  if (batch.length > 0) {
    try {
      await prisma.performancePageScore.createMany({
        data: batch.map((p) => ({ auditId, ...p })),
      });
    } catch (err) {
      log.warn("api", `Performance audit score write failed for ${url}: ${err instanceof Error ? err.message : "unknown"}`, { metadata: { auditId, url } });
      return { url, scored: 0, failureDetail: `DB write failed for ${url}` };
    }
  }

  const elapsed = Date.now() - t0;
  console.info("[cwi:audit] page complete", { url, elapsed, index, total, scored: batch.length, devices: batch.map((b) => b.device) });

  const failureDetail = batch.length === 0
    ? `${url} — ${mobileFailedWith === "timeout" ? `timed out after ${PAGESPEED_TIMEOUT_MS / 1000}s on attempt 3` : (mobileFailedWith === "rate_limited" ? "rate-limited (429)" : (mobileFailedWith ?? "no data returned"))}`
    : null;

  return { url, scored: batch.length, failureDetail };
}

export async function runPerformanceAudit(brandId: string, domain: string): Promise<PerfAuditResult> {
  const runnerStart = Date.now();
  const audit = await prisma.performanceAudit.create({
    data: { brandId, status: "running", device: "mobile" },
  });

  log.info("api", `Performance audit started for ${domain}`, { metadata: { brandId, auditId: audit.id } });

  try {
    const apiKey = process.env.GOOGLE_PAGESPEED_API_KEY ?? "";
    const urls = await pickPagesToTest(brandId, domain);
    console.info("[cwi:audit] started", { brandId, auditId: audit.id, pageCount: urls.length, hasApiKey: !!apiKey });

    if (urls.length === 0) {
      await prisma.performanceAudit.update({
        where: { id: audit.id },
        data: { status: "failed", errorMessage: "No URLs to audit", completedAt: new Date() },
      });
      return { auditId: audit.id, status: "failed", message: "No URLs to audit" };
    }

    const queue = [...urls];
    const total = urls.length;
    let nextIndex = 0;
    let succeeded = 0;
    let failed = 0;
    let budgetHit = false;
    const failureDetails: string[] = [];

    async function worker() {
      while (queue.length > 0) {
        const elapsed = Date.now() - runnerStart;
        const remaining = WALL_BUDGET_MS - elapsed;
        if (remaining < PAGESPEED_TIMEOUT_MS) {
          budgetHit = true;
          return;
        }
        const url = queue.shift();
        if (!url) return;
        const idx = ++nextIndex;
        try {
          const deadlineAt = runnerStart + WALL_BUDGET_MS;
          const { scored, failureDetail } = await scoreOneUrl(url, audit.id, apiKey, idx, total, deadlineAt);
          if (scored > 0) {
            succeeded++;
          } else {
            failed++;
            if (failureDetail) failureDetails.push(`URL ${idx} of ${total}: ${failureDetail}`);
          }
        } catch (err) {
          failed++;
          const url2 = url;
          failureDetails.push(`URL ${idx} of ${total}: ${url2} — unexpected error`);
          log.warn("api", `PageSpeed worker error for ${url}: ${err instanceof Error ? err.message : "unknown"}`, { metadata: { auditId: audit.id, url } });
        }
      }
    }

    const workerCount = Math.min(MAX_CONCURRENCY, urls.length);
    await Promise.all(Array.from({ length: workerCount }, () => worker()));

    const totalElapsed = Date.now() - runnerStart;

    const closingStatus: "completed" | "partial" | "failed" =
      succeeded === 0 ? "failed"
      : (budgetHit && queue.length > 0) || failed > 0 ? "partial"
      : "completed";

    const dbStatus = closingStatus === "failed" ? "failed" : "completed";
    const hasApiKey = !!apiKey;
    const allRateLimited = failureDetails.length > 0 && failureDetails.every((d) => d.includes("rate-limited"));
    const apiKeyTip = !hasApiKey
      ? " Add GOOGLE_PAGESPEED_API_KEY to Vercel env vars to lift anonymous rate limits."
      : "";

    const failedErrorMessage = failureDetails.length > 0
      ? `${failureDetails.join("; ")}.${apiKeyTip} Try again in a few minutes.`
      : `All ${total} URLs failed to score — PageSpeed API did not respond.${apiKeyTip} Try again in a few minutes.`;

    await prisma.performanceAudit.update({
      where: { id: audit.id },
      data: {
        status: dbStatus,
        completedAt: new Date(),
        errorMessage: closingStatus === "partial"
          ? `Scored ${succeeded} of ${total} pages — ${queue.length > 0 ? "wall-clock budget reached" : "some pages were unreachable"}. Results may be limited.`
          : closingStatus === "failed"
            ? failedErrorMessage
            : null,
      },
    });

    log.info("api", `Performance audit ${closingStatus} for ${domain}`, {
      metadata: { auditId: audit.id, urlsTested: succeeded, urlsFailed: failed, urlsSkipped: queue.length, elapsedMs: totalElapsed, budgetHit, hasApiKey, allRateLimited },
    });
    console.info("[cwi:audit] done", { auditId: audit.id, closingStatus, succeeded, failed, skipped: queue.length, elapsedMs: totalElapsed });

    return {
      auditId: audit.id,
      status: closingStatus,
      message: closingStatus === "partial"
        ? `Scored ${succeeded} of ${total} pages — some pages were unreachable. Results may be limited.`
        : closingStatus === "failed"
          ? failedErrorMessage
          : `Audit completed: ${succeeded} of ${total} pages scored.`,
      pagesTested: succeeded,
    };
  } catch (err) {
    const msg = err instanceof Error ? err.message : "Performance audit failed";

    await prisma.performanceAudit.update({
      where: { id: audit.id },
      data: { status: "failed", errorMessage: msg.slice(0, 500), completedAt: new Date() },
    });

    log.error("api", `Performance audit failed for ${domain}: ${msg}`, { metadata: { auditId: audit.id } });
    return { auditId: audit.id, status: "failed", message: msg };
  }
}

type PSIResult =
  | { ok: true; data: Record<string, unknown> }
  | { ok: false; retryable: boolean; reason: string; httpStatus?: number };

/** Internal: single PageSpeed fetch with discriminated result for smart retry. */
async function fetchPSI(url: string, strategy: "mobile" | "desktop", apiKey: string): Promise<PSIResult> {
  const params = new URLSearchParams({ url, strategy });
  params.append("category", "performance");
  params.append("category", "accessibility");
  params.append("category", "best-practices");
  params.append("category", "seo");
  if (apiKey) params.set("key", apiKey);
  params.set("_t", String(Date.now()));

  const endpoint = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?${params}`;
  const t0 = Date.now();
  try {
    const res = await fetch(endpoint, {
      signal: AbortSignal.timeout(PAGESPEED_TIMEOUT_MS),
      cache: "no-store",
    });

    const elapsed = Date.now() - t0;
    console.info("[cwi:pagespeed] API response", { status: res.status, ok: res.ok, elapsedMs: elapsed, url, strategy, hasApiKey: !!apiKey });

    if (!res.ok) {
      const retryable = res.status >= 500 || res.status === 429;
      const reason = res.status === 429 ? "rate_limited" : res.status === 403 ? "auth_error" : `http_${res.status}`;
      if (res.status === 429) {
        log.warn("api", `PageSpeed API rate-limited (429) for ${url} (${strategy}) — set GOOGLE_PAGESPEED_API_KEY to lift the anonymous quota`, { metadata: { status: res.status, url, strategy } });
      } else {
        log.warn("api", `PageSpeed API ${res.status} for ${url} (${strategy})`, { metadata: { status: res.status, url, strategy } });
      }
      return { ok: false, retryable, reason, httpStatus: res.status };
    }

    const json = await res.json();
    const data = (json.lighthouseResult ?? null) as Record<string, unknown> | null;
    if (!data) return { ok: false, retryable: false, reason: "empty_response" };
    return { ok: true, data };
  } catch (err) {
    const elapsed = Date.now() - t0;
    const isTimeout = err instanceof DOMException && err.name === "TimeoutError";
    console.warn("[cwi:pagespeed] fetch failed", { url, strategy, elapsedMs: elapsed, isTimeout, err: err instanceof Error ? err.message : "unknown" });
    return { ok: false, retryable: true, reason: isTimeout ? "timeout" : "network_error" };
  }
}

/** Exported: single-shot PageSpeed fetch. Competitor comparison reuses this;
 *  the audit runner calls fetchWithRetry (inside scoreOneUrl) for smarter retry. */
export async function fetchPageSpeed(url: string, strategy: "mobile" | "desktop", apiKey: string) {
  const result = await fetchPSI(url, strategy, apiKey);
  return result.ok ? result.data : null;
}
