/**
 * Competitor CWV Comparison Runner
 * ─────────────────────────────────
 * Scores the brand's homepage alongside up to 3 active competitor
 * homepages on Google PageSpeed Insights, mobile strategy only, and
 * returns a side-by-side comparison for the UI to render.
 *
 * Intentionally NOT persisted to the DB — this is a point-in-time
 * diff the user requests on demand. Saving would require a new
 * schema for competitor performance snapshots; current scope keeps
 * the results ephemeral.
 *
 * Timing constraints match the main CWI audit runner so the
 * competitor flow can't exhaust the function budget independently:
 *   - MAX_COMPETITORS=3
 *   - PAGESPEED_TIMEOUT_MS=15000  (per call)
 *   - WALL_BUDGET_MS=45000        (4 URLs × 15s ÷ 3 concurrent ≈
 *                                  20s expected, 45s gives us head-
 *                                  room for Google cold-start latency
 *                                  without blowing Vercel's 60s.)
 *   - MAX_CONCURRENCY=3           (3 pages in flight at a time;
 *                                  mobile-only so peak PageSpeed
 *                                  calls = 3)
 *
 * Returns partial results when the budget runs out — a user gets
 * "your site + 1 competitor" back rather than a hard error when
 * Google is slow for the third.
 */

import { prisma } from "@/lib/prisma";
import { fetchPageSpeed } from "@/lib/services/performance-audit-runner";

const MAX_COMPETITORS = 3;
const MAX_CONCURRENCY = 3;
const WALL_BUDGET_MS = 45_000;

export interface CompetitorComparisonResult {
  brand:       CompetitorPerfRow | null;
  competitors: CompetitorPerfRow[];
  partial:     boolean;
  message:     string;
}

export interface CompetitorPerfRow {
  label: string;   // "Your Site" or competitor name
  url:   string;
  scored: boolean; // false when PageSpeed timed out / rejected
  performance:   number;
  accessibility: number;
  bestPractices: number;
  seo:           number;
  lcpMs:         number;
  inpMs:         number;
  clsVal:        number;
  fcpMs:         number;
  ttfbMs:        number;
}

/** Normalise a domain into an absolute URL the PageSpeed API accepts.
 *  Handles bare hosts ("acme.com"), hosts with path ("acme.com/shop"),
 *  and fully-qualified URLs ("https://acme.com"). Always returns the
 *  homepage — paths are stripped so a competitor stored as
 *  "/blog/seo-tips" still audits the root of their site. */
function normaliseHomepageUrl(domain: string): string {
  let url = domain.trim().replace(/\/+$/, "");
  if (!url.startsWith("http")) url = `https://${url}`;
  try {
    const u = new URL(url);
    return `${u.protocol}//${u.host}`;
  } catch {
    return url;
  }
}

/** Score a single URL on PageSpeed (mobile strategy). Shaped for
 *  consumption by the comparison table — if the fetch fails, returns
 *  a row with `scored: false` and zeros across the metric fields so
 *  the UI can render a "No data" state without branching on null. */
async function scoreUrlForComparison(label: string, url: string, apiKey: string): Promise<CompetitorPerfRow> {
  const fallback: CompetitorPerfRow = {
    label, url, scored: false,
    performance: 0, accessibility: 0, bestPractices: 0, seo: 0,
    lcpMs: 0, inpMs: 0, clsVal: 0, fcpMs: 0, ttfbMs: 0,
  };
  const data = await fetchPageSpeed(url, "mobile", apiKey);
  if (!data) return fallback;
  const cats = (data.categories ?? {}) as Record<string, { score: number }>;
  const audits = (data.audits ?? {}) as Record<string, { numericValue?: number }>;
  return {
    label, url, scored: true,
    performance:   Math.round((cats.performance?.score       ?? 0) * 100),
    accessibility: Math.round((cats.accessibility?.score     ?? 0) * 100),
    bestPractices: Math.round((cats["best-practices"]?.score ?? 0) * 100),
    seo:           Math.round((cats.seo?.score               ?? 0) * 100),
    lcpMs:  Math.round(audits["largest-contentful-paint"]?.numericValue    ?? 0),
    fcpMs:  Math.round(audits["first-contentful-paint"]?.numericValue      ?? 0),
    clsVal: audits["cumulative-layout-shift"]?.numericValue                 ?? 0,
    ttfbMs: Math.round(audits["server-response-time"]?.numericValue        ?? 0),
    inpMs:  Math.round(audits["interaction-to-next-paint"]?.numericValue   ?? 0),
  };
}

/** Parallel worker-pool pattern — identical shape to the main runner
 *  so the wall-clock behaviour and budget-aware skip logic carry
 *  over unchanged. Scores targets[] at MAX_CONCURRENCY in parallel,
 *  aborts pulling new targets when remaining budget < per-call
 *  timeout, returns whatever completed. */
async function scoreAll(targets: Array<{ label: string; url: string }>, apiKey: string): Promise<{ rows: CompetitorPerfRow[]; partial: boolean }> {
  const start = Date.now();
  const queue = [...targets];
  const results: CompetitorPerfRow[] = [];
  let budgetHit = false;

  async function worker() {
    while (queue.length > 0) {
      const elapsed = Date.now() - start;
      if (WALL_BUDGET_MS - elapsed < 15_000) {
        budgetHit = true;
        return;
      }
      const next = queue.shift();
      if (!next) return;
      try {
        const row = await scoreUrlForComparison(next.label, next.url, apiKey);
        results.push(row);
      } catch {
        // scoreUrlForComparison swallows internally; an outer throw
        // would have to come from a bug. Record a non-scored row so
        // the UI can still render the label.
        results.push({
          label: next.label, url: next.url, scored: false,
          performance: 0, accessibility: 0, bestPractices: 0, seo: 0,
          lcpMs: 0, inpMs: 0, clsVal: 0, fcpMs: 0, ttfbMs: 0,
        });
      }
    }
  }

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

  // Preserve the original target order — workers finish out of
  // sequence but the UI wants "Your Site" first, then competitors
  // in whatever order they were passed.
  const ordered = targets
    .map((t) => results.find((r) => r.url === t.url))
    .filter((r): r is CompetitorPerfRow => !!r);

  return { rows: ordered, partial: budgetHit || queue.length > 0 };
}

export async function runCompetitorComparison(brandId: string, brandDomain: string): Promise<CompetitorComparisonResult> {
  const apiKey = process.env.GOOGLE_PAGESPEED_API_KEY ?? "";

  const competitors = await prisma.competitor.findMany({
    where: { brandId, isActive: true },
    select: { id: true, name: true, domain: true },
    orderBy: { createdAt: "asc" },
    take: MAX_COMPETITORS,
  }).catch(() => []);

  const brandHome = normaliseHomepageUrl(brandDomain);
  const targets: Array<{ label: string; url: string }> = [
    { label: "Your Site", url: brandHome },
    ...competitors.map((c) => ({
      label: c.name || c.domain,
      url: normaliseHomepageUrl(c.domain),
    })),
  ];

  console.info("[cwi:compare] started", { brandId, targetCount: targets.length, competitorCount: competitors.length });

  if (competitors.length === 0) {
    // Honest empty-state path — nothing to compare against.
    return {
      brand:       null,
      competitors: [],
      partial:     false,
      message:     "No active competitors tracked for this brand. Add competitors on the Competitor Intel page, then try again.",
    };
  }

  const { rows, partial } = await scoreAll(targets, apiKey);

  const brand = rows.find((r) => r.label === "Your Site") ?? null;
  const competitorRows = rows.filter((r) => r.label !== "Your Site");

  console.info("[cwi:compare] done", {
    brandId,
    brandScored: !!brand?.scored,
    competitorsScored: competitorRows.filter((r) => r.scored).length,
    competitorsFailed: competitorRows.filter((r) => !r.scored).length,
    partial,
  });

  return {
    brand,
    competitors: competitorRows,
    partial,
    message: partial
      ? `Scored ${rows.filter((r) => r.scored).length} of ${targets.length} sites — some PageSpeed calls timed out or hit the ${Math.round(WALL_BUDGET_MS / 1000)}s budget.`
      : `Compared your site against ${competitorRows.length} competitor${competitorRows.length === 1 ? "" : "s"}.`,
  };
}
