/**
 * Page Lifecycle Intelligence — shared helpers
 * ────────────────────────────────────────────
 * Auth + upsert helpers used by the three Site Tag ingest endpoints
 * (page-seen, page-links, nav-structure) and the read endpoints that
 * back the Launch Tracker UI.
 *
 * Auth model: siteId acts as a shared secret (same pattern as
 * /api/collect and /api/site-tag/audit-overlay). The referring URL is
 * validated against the TrackedSite's allowedDomains so an attacker
 * can't spoof lifecycle data for a site they don't own.
 */

import { prisma } from "@/lib/prisma";
import type { Prisma } from "@prisma/client";

/**
 * CORS headers for Site Tag endpoints.
 *
 * Static export (legacy): wildcard origin, no credentials. Works for tags
 * that explicitly send `credentials: "omit"`.
 *
 * Dynamic helper (preferred): reflects the request's Origin header and
 * sets `Access-Control-Allow-Credentials: true`. This is a safety net for
 * edge cases where a service worker or browser extension injects
 * `credentials: "include"` into the tag's fetch calls — without dynamic
 * origin reflection, the browser blocks the response because wildcard is
 * incompatible with credentialed requests.
 */
export const CORS_HEADERS = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type",
};

/** Build CORS headers that reflect the request's Origin header. */
export function getCorsHeaders(request?: Request): Record<string, string> {
  const origin = request?.headers?.get("origin") || "*";
  return {
    "Access-Control-Allow-Origin": origin,
    "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type",
    // Only set credentials: true when we have a concrete origin. Credentials
    // are incompatible with wildcard origin, so omit the header in that case.
    ...(origin !== "*" ? { "Access-Control-Allow-Credentials": "true" } : {}),
    "Vary": "Origin",
  };
}

export interface ResolvedSite {
  id: string;
  brandId: string;
  allowedDomains: string[];
}

export type SiteTagDropReason =
  | "missing_site_id"
  | "malformed_site_id"
  | "bad_page_url"
  | "site_not_found"
  | "site_not_active"
  | "domain_not_allowed"
  | "url_parse_fail";

/**
 * Look up a TrackedSite by siteId and verify the requesting URL belongs
 * to one of its allowed domains. Returns the resolved site + brandId on
 * success, or an error string on failure.
 */
export async function resolveSiteFromTag(
  siteId: string | null | undefined,
  anyUrl: string | null | undefined,
): Promise<
  | { ok: true; site: ResolvedSite }
  | { ok: false; error: string; status: number; reason: SiteTagDropReason; failedHostname?: string }
> {
  // All failures return status 200 so the client-side tag doesn't
  // flood the console with red 4xx errors. The event is simply
  // dropped -- same outcome, zero console noise.
  if (!siteId || typeof siteId !== "string") {
    return { ok: false, error: "Missing site ID", status: 200, reason: "missing_site_id" };
  }
  if (!siteId.startsWith("ms_")) {
    return { ok: false, error: "Invalid site ID format", status: 200, reason: "malformed_site_id" };
  }
  if (!anyUrl || typeof anyUrl !== "string" || anyUrl.length > 2048) {
    return { ok: false, error: "Invalid URL", status: 200, reason: "bad_page_url" };
  }

  const site = await prisma.trackedSite.findUnique({
    where: { siteId },
    select: { id: true, brandId: true, status: true, allowedDomains: true },
  });
  if (!site) {
    return { ok: false, error: "Site not found", status: 200, reason: "site_not_found" };
  }
  if (site.status !== "active") {
    return { ok: false, error: `Site is ${site.status}`, status: 200, reason: "site_not_active" };
  }

  try {
    const url = new URL(anyUrl);
    const rawHost = url.hostname.toLowerCase();
    const host = rawHost.replace(/^www\./, "").replace(/\/+$/, "");
    const allowed = site.allowedDomains.map((d) =>
      d.toLowerCase().replace(/^www\./, "").replace(/\/+$/, "")
    );
    const matches = allowed.some((d) => host === d || host.endsWith(`.${d}`));
    if (!matches) {
      return { ok: false, error: `Domain ${rawHost} not allowed`, status: 200, reason: "domain_not_allowed", failedHostname: rawHost };
    }
  } catch {
    return { ok: false, error: "Invalid URL", status: 200, reason: "url_parse_fail" };
  }

  return {
    ok: true,
    site: { id: site.id, brandId: site.brandId, allowedDomains: site.allowedDomains },
  };
}

// ─── URL normalisation ──────────────────────────────────────────────────────

/**
 * Convert a URL (absolute or relative) into a canonical `/path` form so
 * we can match pages across protocol/host variants and avoid creating
 * duplicate PageRecord rows for "/" vs "" vs "https://example.com/".
 */
export function toPathKey(input: string): string {
  if (!input) return "/";
  try {
    const u = new URL(input);
    return stripTrailingSlash(u.pathname) || "/";
  } catch {
    // Already a relative path — strip query + hash
    const cleaned = input.split("?")[0].split("#")[0].trim();
    if (!cleaned || cleaned === "/") return "/";
    return stripTrailingSlash(cleaned.startsWith("/") ? cleaned : "/" + cleaned) || "/";
  }
}

function stripTrailingSlash(s: string): string {
  return s.length > 1 ? s.replace(/\/+$/, "") : s;
}

/** Build an absolute URL from a base and a path, for storage in fullUrl. */
export function buildFullUrl(brandDomain: string | null, path: string, fallbackFullUrl?: string): string {
  if (fallbackFullUrl && /^https?:\/\//i.test(fallbackFullUrl)) return fallbackFullUrl;
  if (!brandDomain) return path;
  const base = brandDomain.startsWith("http") ? brandDomain : `https://${brandDomain}`;
  return base.replace(/\/$/, "") + (path.startsWith("/") ? path : "/" + path);
}

// ─── PageRecord upsert ──────────────────────────────────────────────────────

export interface PageSeenInput {
  brandId: string;
  url: string;             // already normalised path
  fullUrl: string;
  title?: string | null;
  statusCode?: number | null;
  metaDescription?: string | null;
  h1?: string | null;
  canonicalUrl?: string | null;
  wordCount?: number | null;
}

export interface PageSeenResult {
  pageId: string;
  isNew: boolean;
  statusChanged: boolean;
  fromStatus?: number;
  toStatus?: number;
}

/**
 * Upsert a PageRecord for a newly-seen (or re-seen) page. Also records
 * a PageStatusChange row if the HTTP status differs from the last known
 * status for this URL. Increments totalVisits on every call.
 */
export async function upsertPageSeen(input: PageSeenInput): Promise<PageSeenResult> {
  const now = new Date();
  const toStatus = input.statusCode ?? 200;

  const existing = await prisma.pageRecord.findUnique({
    where: { brandId_url: { brandId: input.brandId, url: input.url } },
    select: { id: true, currentStatus: true, title: true },
  });

  if (!existing) {
    const created = await prisma.pageRecord.upsert({
      where: { brandId_url: { brandId: input.brandId, url: input.url } },
      create: {
        brandId: input.brandId,
        url: input.url,
        fullUrl: input.fullUrl,
        title: input.title ?? null,
        firstSeenAt: now,
        lastSeenAt: now,
        currentStatus: toStatus,
        totalVisits: 1,
        metaDescription: input.metaDescription ?? null,
        h1: input.h1 ?? null,
        canonicalUrl: input.canonicalUrl ?? null,
        wordCount: input.wordCount ?? null,
      },
      update: {},
      select: { id: true },
    });
    return { pageId: created.id, isNew: true, statusChanged: false };
  }

  const statusChanged = existing.currentStatus !== toStatus;

  await prisma.pageRecord.update({
    where: { id: existing.id },
    data: {
      fullUrl: input.fullUrl,
      title: input.title ?? existing.title ?? null,
      lastSeenAt: now,
      currentStatus: toStatus,
      totalVisits: { increment: 1 },
      ...(input.metaDescription != null ? { metaDescription: input.metaDescription } : {}),
      ...(input.h1 != null ? { h1: input.h1 } : {}),
      ...(input.canonicalUrl != null ? { canonicalUrl: input.canonicalUrl } : {}),
      ...(input.wordCount != null ? { wordCount: input.wordCount } : {}),
    },
  });

  if (statusChanged) {
    await prisma.pageStatusChange.create({
      data: {
        pageId: existing.id,
        fromStatus: existing.currentStatus,
        toStatus,
      },
    });
  }

  return {
    pageId: existing.id,
    isNew: false,
    statusChanged,
    fromStatus: existing.currentStatus,
    toStatus,
  };
}

// ─── Redirect ingest ────────────────────────────────────────────────────────

export async function recordRedirect(
  brandId: string,
  sourceUrl: string,
  targetUrl: string,
  statusCode: number,
): Promise<void> {
  const sourcePath = toPathKey(sourceUrl);
  const targetPath = toPathKey(targetUrl);
  if (sourcePath === targetPath) return;

  const [sourcePage, targetPage] = await Promise.all([
    prisma.pageRecord.findUnique({
      where: { brandId_url: { brandId, url: sourcePath } },
      select: { id: true },
    }),
    prisma.pageRecord.findUnique({
      where: { brandId_url: { brandId, url: targetPath } },
      select: { id: true },
    }),
  ]);

  await prisma.redirectRecord.upsert({
    where: {
      brandId_sourceUrl_targetUrl: { brandId, sourceUrl: sourcePath, targetUrl: targetPath },
    },
    create: {
      brandId,
      sourceUrl: sourcePath,
      targetUrl: targetPath,
      statusCode,
      sourcePageId: sourcePage?.id ?? null,
      targetPageId: targetPage?.id ?? null,
      firstSeenAt: new Date(),
      lastSeenAt: new Date(),
      isActive: true,
    },
    update: {
      statusCode,
      lastSeenAt: new Date(),
      isActive: true,
      ...(sourcePage ? { sourcePageId: sourcePage.id } : {}),
      ...(targetPage ? { targetPageId: targetPage.id } : {}),
    },
  });
}

// ─── Nav diff helpers ───────────────────────────────────────────────────────

export interface NavLinkInput {
  href: string;
  anchorText?: string | null;
  section?: string | null;
}

export interface NavDiffResult {
  added: NavLinkInput[];
  removed: NavLinkInput[];
}

/**
 * Compare two nav snapshots and return the added/removed link sets.
 * Matches by normalised path so "/about/" and "/about" are the same link.
 */
export function diffNavSnapshots(
  previous: NavLinkInput[] | null,
  current: NavLinkInput[],
): NavDiffResult {
  const prev = new Map<string, NavLinkInput>();
  (previous ?? []).forEach((l) => prev.set(toPathKey(l.href), l));
  const curr = new Map<string, NavLinkInput>();
  current.forEach((l) => curr.set(toPathKey(l.href), l));

  const added: NavLinkInput[] = [];
  const removed: NavLinkInput[] = [];
  curr.forEach((v, k) => { if (!prev.has(k)) added.push(v); });
  prev.forEach((v, k) => { if (!curr.has(k)) removed.push(v); });
  return { added, removed };
}

// ─── Internal link helpers ──────────────────────────────────────────────────

export type JsonInternalLinks = Prisma.InputJsonValue;
