/**
 * Brand Sitemap Crawler
 * ─────────────────────
 * Thin wrapper over `discoverAndFetch` (in competitor-sitemap.ts) that
 * ingests the brand's *own* sitemap and caches the result in SiteEvent.
 *
 * Why the cache lives in SiteEvent rather than a dedicated Prisma model:
 * it's an ad-hoc per-brand blob (sitemap entries + crawledAt), and the
 * same JSON cache pattern is already used for page-recommendations,
 * seo-monitor, topic-map, etc. Adding a new table for this would be
 * schema churn without a clear win.
 *
 * Shape of the cached row:
 *   eventType: "brand_sitemap_cache"
 *   pageUrl:   "__brand_sitemap__"          ← sentinel, always this
 *   eventData: { entries, crawledAt, sitemapUrls, detectedCms, count }
 *   timestamp: the moment the crawl finished (acts as TTL)
 *
 * Readers always pull the most-recent row via
 *   findFirst({ eventType, pageUrl, brandId }, orderBy: timestamp desc).
 */

import { prisma } from "@/lib/prisma";
import { discoverAndFetch, type SitemapEntry } from "./competitor-sitemap";
import { normalizePath } from "./url-normalize";

export const BRAND_SITEMAP_EVENT_TYPE = "brand_sitemap_cache";
export const BRAND_SITEMAP_SENTINEL_URL = "__brand_sitemap__";

/** Max cache age before a tracker request will trigger a fresh crawl. */
export const DEFAULT_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24h

export interface BrandSitemapCache {
  entries: SitemapEntry[];
  crawledAt: string; // ISO
  sitemapUrls: string[];
  detectedCms: string | null;
  count: number;
}

/**
 * Fetch the brand's sitemap fresh from the origin, store it in SiteEvent,
 * and return the parsed result. Throws on domain missing or zero entries
 * (callers should catch and fall back to the last cached crawl).
 */
export async function crawlBrandSitemap(brandId: string, domain: string): Promise<BrandSitemapCache> {
  const result = await discoverAndFetch(domain);
  const cache: BrandSitemapCache = {
    entries: result.entries,
    crawledAt: new Date().toISOString(),
    sitemapUrls: result.sitemapUrls,
    detectedCms: result.detectedCms,
    count: result.entries.length,
  };
  // Persist the crawl. JSON roundtrip is the cleanest way to coerce the
  // typed object into the Prisma Json input shape.
  await prisma.siteEvent.create({
    data: {
      brandId,
      pageUrl: BRAND_SITEMAP_SENTINEL_URL,
      eventType: BRAND_SITEMAP_EVENT_TYPE,
      eventData: JSON.parse(JSON.stringify(cache)),
    },
  });
  return cache;
}

/** Pull the most-recent cached sitemap crawl for a brand, or null. */
export async function getCachedBrandSitemap(brandId: string): Promise<BrandSitemapCache | null> {
  const row = await prisma.siteEvent.findFirst({
    where: { brandId, pageUrl: BRAND_SITEMAP_SENTINEL_URL, eventType: BRAND_SITEMAP_EVENT_TYPE },
    orderBy: { timestamp: "desc" },
    select: { eventData: true, timestamp: true },
  });
  if (!row) return null;
  const data = row.eventData as unknown as BrandSitemapCache | null;
  if (!data || !Array.isArray(data.entries)) return null;
  // Prefer the row's timestamp as the source of truth for crawledAt,
  // even if the serialised ISO drifted from it.
  return { ...data, crawledAt: row.timestamp.toISOString() };
}

/**
 * Get the cache if fresh, otherwise crawl and replace. A failed live
 * crawl falls back to the last cache so the tracker never goes without
 * sitemap data once it's been populated at least once.
 */
export async function getOrRefreshBrandSitemap(
  brandId: string,
  domain: string,
  opts: { maxAgeMs?: number; force?: boolean } = {},
): Promise<BrandSitemapCache | null> {
  const maxAge = opts.maxAgeMs ?? DEFAULT_CACHE_MAX_AGE_MS;
  const cached = await getCachedBrandSitemap(brandId);
  if (!opts.force && cached) {
    const ageMs = Date.now() - new Date(cached.crawledAt).getTime();
    if (ageMs < maxAge) return cached;
  }
  try {
    const fresh = await crawlBrandSitemap(brandId, domain);
    if (fresh.count === 0 && cached) return cached;
    return fresh;
  } catch {
    return cached;
  }
}

/**
 * Build a pathname → lastmod Date map from sitemap entries. Keys use
 * normalizePath so lookups are compatible with PageRecord.url (which
 * stores paths like "/about"). Entries without a parseable lastmod are
 * still stored with a null date so callers can detect "in sitemap but
 * no lastmod" vs "not in sitemap".
 */
export function buildSitemapLookup(entries: SitemapEntry[]): Map<string, Date | null> {
  const map = new Map<string, Date | null>();
  for (const e of entries) {
    const path = normalizePath(e.loc);
    if (!path) continue;
    let lastmod: Date | null = null;
    if (e.lastmod) {
      const parsed = new Date(e.lastmod);
      if (!Number.isNaN(parsed.getTime())) lastmod = parsed;
    }
    // Keep the earliest lastmod when the same path appears in multiple
    // sitemaps (rare but happens with wp-sitemap + page-sitemap overlap).
    const existing = map.get(path);
    if (existing === undefined) {
      map.set(path, lastmod);
    } else if (lastmod && (!existing || lastmod < existing)) {
      map.set(path, lastmod);
    }
  }
  return map;
}
