/**
 * Signal Detection Engine
 * ─────────────────────────
 * Scans real-user behavioural data (SiteEvent / TrackedEvent) and emits
 * structured alerts when thresholds are crossed. Each detector is pure —
 * takes a brandId, returns `DetectedSignal[]`. The top-level
 * `runSignalDetection()` aggregates them into one array.
 *
 * Persistence, deduplication, and auto-resolve land in a later commit;
 * this file focuses on detection logic so individual detectors can be
 * unit-tested and swapped without touching the orchestration layer.
 *
 * Signals persist through the existing `SiteEvent` table rather than a
 * new model — eventType `signal_<type>` with a structured eventData
 * payload is the shared convention, matching how the rank-check cron
 * already stores rank signals.
 */

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

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

export type SignalType =
  | "error_spike"
  | "rage_click_spike"
  | "traffic_anomaly"
  | "conversion_drop"
  | "exit_intent_spike"
  | "broken_resource";

export type SignalSeverity = "critical" | "high" | "medium" | "low" | "info";

export interface DetectedSignal {
  type: SignalType;
  severity: SignalSeverity;
  title: string;
  description: string;
  /** Affected URL — empty string for brand-wide signals (rare). */
  pageUrl: string;
  /** Detector-specific counts, percentages, referrers, comparison data. */
  metadata: Record<string, unknown>;
}

/** Prefix used when storing a signal as a SiteEvent row. */
export function signalEventType(type: SignalType): string {
  return `signal_${type}`;
}

/** Parse a SiteEvent back into a DetectedSignal shape. Returns null when
 *  the row isn't a signal (different eventType, malformed eventData). */
export function parseSignalEvent(row: { eventType: string; pageUrl: string; eventData: unknown }): DetectedSignal | null {
  if (!row.eventType.startsWith("signal_")) return null;
  const type = row.eventType.slice("signal_".length) as SignalType;
  const d = (row.eventData ?? {}) as Record<string, unknown>;
  if (typeof d.title !== "string" || typeof d.description !== "string") return null;
  return {
    type,
    severity: (typeof d.severity === "string" ? d.severity : "medium") as SignalSeverity,
    title: d.title,
    description: d.description,
    pageUrl: row.pageUrl,
    metadata: (typeof d.metadata === "object" && d.metadata) ? d.metadata as Record<string, unknown> : {},
  };
}

// ─── A) 404 / 500 error spikes ──────────────────────────────────────────────

/**
 * Scans `http_error` events in the last 24 hours and emits:
 *   • CRITICAL immediately for any 5xx — backend issue.
 *   • CRITICAL for 404s hit by 10+ unique visitors in 24h.
 *   • HIGH (demotes the CRITICAL) when a URL now-404ing had real
 *     page_view traffic in the 7-14 day window — i.e. a recently-
 *     working page that broke.
 *
 * `http_error` is already being emitted by the existing Site Tag
 * analytics pipeline (see site-tag-analytics.ts around "HTTP errors
 * detected by real users"), so this detector is live against real data.
 */
export async function detectErrorSpikes(brandId: string): Promise<DetectedSignal[]> {
  const out: DetectedSignal[] = [];
  const now = Date.now();
  const dayAgo = new Date(now - 86_400_000);

  const errors = await prisma.siteEvent.findMany({
    where: { brandId, eventType: "http_error", timestamp: { gte: dayAgo } },
    select: { pageUrl: true, visitorId: true, eventData: true, timestamp: true },
  });
  if (errors.length === 0) return out;

  type Agg = {
    visitors: Set<string>;
    count: number;
    statusCodes: Set<number>;
    referrers: Set<string>;
  };
  const byUrl = new Map<string, Agg>();
  for (const e of errors) {
    const agg = byUrl.get(e.pageUrl) ?? { visitors: new Set(), count: 0, statusCodes: new Set(), referrers: new Set() };
    if (e.visitorId) agg.visitors.add(e.visitorId);
    agg.count++;
    const d = (e.eventData ?? {}) as Record<string, unknown>;
    if (typeof d.statusCode === "number") agg.statusCodes.add(d.statusCode);
    if (typeof d.referrer === "string" && d.referrer) agg.referrers.add(d.referrer);
    byUrl.set(e.pageUrl, agg);
  }

  // First pass: classify every URL
  for (const [pageUrl, agg] of byUrl) {
    const has500 = [...agg.statusCodes].some((c) => c >= 500);
    const has404 = [...agg.statusCodes].some((c) => c === 404);
    const uniqueVisitors = agg.visitors.size;
    const topRefs = [...agg.referrers].slice(0, 3);

    if (has500) {
      out.push({
        type: "error_spike",
        severity: "critical",
        title: `Server error detected on ${pageUrl}`,
        description: `${agg.count} 5xx occurrence${agg.count === 1 ? "" : "s"} in the last 24 hours. This may indicate a backend issue requiring immediate attention.`,
        pageUrl,
        metadata: { count: agg.count, uniqueVisitors, statusCodes: [...agg.statusCodes], referrers: topRefs, kind: "server_error" },
      });
    } else if (has404 && uniqueVisitors >= 10) {
      out.push({
        type: "error_spike",
        severity: "critical",
        title: `Page ${pageUrl} is returning 404`,
        description: `${uniqueVisitors} unique visitor${uniqueVisitors === 1 ? "" : "s"} hit this 404 in the last 24 hours.${topRefs.length > 0 ? ` Top source pages: ${topRefs.join(", ")}.` : ""}`,
        pageUrl,
        metadata: { count: agg.count, uniqueVisitors, statusCodes: [...agg.statusCodes], referrers: topRefs, kind: "404_spike" },
      });
    }
  }

  // Second pass: check which 404ing URLs were recently working and demote
  // the severity accordingly (this is a softer alert than "mass 404 NOW").
  const recent404Urls = out
    .filter((s) => s.metadata.kind === "404_spike")
    .map((s) => s.pageUrl);
  if (recent404Urls.length > 0) {
    const twoWeeksAgo = new Date(now - 14 * 86_400_000);
    const weekAgo = new Date(now - 7 * 86_400_000);
    const site = await prisma.trackedSite.findUnique({ where: { brandId }, select: { id: true } });
    if (site) {
      const wasWorking = await prisma.trackedEvent.groupBy({
        by: ["pageUrl"],
        where: {
          trackedSiteId: site.id,
          eventType: "page_view",
          pageUrl: { in: recent404Urls },
          timestamp: { gte: twoWeeksAgo, lt: weekAgo },
        },
        _count: true,
      }).catch(() => []);
      const workingUrls = new Set(wasWorking.filter((r) => r._count > 0).map((r) => r.pageUrl));
      for (let i = 0; i < out.length; i++) {
        const s = out[i];
        if (s.metadata.kind !== "404_spike" || !workingUrls.has(s.pageUrl)) continue;
        out[i] = {
          type: "error_spike",
          severity: "high",
          title: `Page ${s.pageUrl} was recently active but is now returning 404`,
          description: `${s.metadata.uniqueVisitors ?? 0} visitor${s.metadata.uniqueVisitors === 1 ? "" : "s"} affected in the last 24 hours. This page had active traffic within the past 7-14 days.`,
          pageUrl: s.pageUrl,
          metadata: { ...s.metadata, kind: "was_working_now_404" },
        };
      }
    }
  }

  return out;
}

// ─── B) Rage click spikes ──────────────────────────────────────────────────

/**
 * Compares per-page rage_click volume this week vs. last week and emits:
 *   • HIGH when this week's count increased by 200%+ (and ≥10 events).
 *   • CRITICAL when any page crosses 20 rage clicks TODAY — bypasses the
 *     weekly comparison so fresh UX breakage surfaces immediately.
 */
export async function detectRageClickSpikes(brandId: string): Promise<DetectedSignal[]> {
  const out: DetectedSignal[] = [];
  const now = Date.now();
  const weekAgo = new Date(now - 7 * 86_400_000);
  const twoWeeksAgo = new Date(now - 14 * 86_400_000);
  const dayAgo = new Date(now - 86_400_000);

  const [thisWeek, prevWeek, today] = await Promise.all([
    prisma.siteEvent.groupBy({
      by: ["pageUrl"],
      where: { brandId, eventType: "rage_click", timestamp: { gte: weekAgo } },
      _count: true,
    }),
    prisma.siteEvent.groupBy({
      by: ["pageUrl"],
      where: { brandId, eventType: "rage_click", timestamp: { gte: twoWeeksAgo, lt: weekAgo } },
      _count: true,
    }),
    prisma.siteEvent.groupBy({
      by: ["pageUrl"],
      where: { brandId, eventType: "rage_click", timestamp: { gte: dayAgo } },
      _count: true,
    }),
  ]);

  const prevMap = new Map(prevWeek.map((r) => [r.pageUrl, r._count]));
  const emittedUrls = new Set<string>();

  // Urgent "today" path first — takes precedence over the weekly spike so
  // we don't emit two signals for the same page.
  for (const row of today) {
    if (row._count >= 20) {
      out.push({
        type: "rage_click_spike",
        severity: "critical",
        title: `Urgent UX issue on ${row.pageUrl}`,
        description: `${row._count} rage clicks today. Check for broken buttons or misleading UI elements on this page.`,
        pageUrl: row.pageUrl,
        metadata: { today: row._count, kind: "urgent_today" },
      });
      emittedUrls.add(row.pageUrl);
    }
  }

  for (const row of thisWeek) {
    if (emittedUrls.has(row.pageUrl)) continue;
    const thisCount = row._count;
    const prevCount = prevMap.get(row.pageUrl) ?? 0;
    const pctIncrease = prevCount > 0 ? ((thisCount - prevCount) / prevCount) * 100 : null;
    if (thisCount >= 10 && pctIncrease !== null && pctIncrease >= 200) {
      out.push({
        type: "rage_click_spike",
        severity: "high",
        title: `Rage clicks spiked ${Math.round(pctIncrease)}% on ${row.pageUrl}`,
        description: `${thisCount} rage click${thisCount === 1 ? "" : "s"} this week vs ${prevCount} last week. Users are clicking on non-interactive elements — check for broken buttons or misleading UI.`,
        pageUrl: row.pageUrl,
        metadata: { thisWeek: thisCount, prevWeek: prevCount, pctIncrease: Math.round(pctIncrease), kind: "weekly_spike" },
      });
    }
  }

  return out;
}

// ─── C) Traffic anomaly detection ──────────────────────────────────────────

/**
 * Day-over-day traffic anomaly detector. Uses z-score against the 7-day
 * rolling average + standard deviation to flag drops / surges, and ALSO
 * compares today vs the same weekday last week to absorb weekly-pattern
 * noise (Monday vs last Monday) so Sunday-low-traffic doesn't trigger.
 *
 * Both conditions must agree before we emit:
 *   • z ≤ -2  AND  weekday-over-weekday < 70%  → HIGH drop signal
 *   • z ≥  2  AND  weekday-over-weekday > 130% → OPPORTUNITY surge signal
 *
 * "Today" is bucketed in UTC to match how MetricSnapshot / page_view
 * timestamps are normalised elsewhere in the codebase.
 */
export async function detectTrafficAnomalies(brandId: string): Promise<DetectedSignal[]> {
  const out: DetectedSignal[] = [];

  // Pull 15 days of page_view events (14 for history + today). We resolve
  // the TrackedSite once and count by local-day via a grouped query.
  const site = await prisma.trackedSite.findUnique({
    where: { brandId },
    select: { id: true },
  });
  if (!site) return out;

  const fifteenDaysAgo = new Date(Date.now() - 15 * 86_400_000);
  const views = await prisma.trackedEvent.findMany({
    where: {
      trackedSiteId: site.id,
      eventType: "page_view",
      timestamp: { gte: fifteenDaysAgo },
    },
    select: { timestamp: true },
  });
  if (views.length === 0) return out;

  // Bucket by UTC calendar day.
  const byDay = new Map<string, number>();
  for (const v of views) {
    const d = v.timestamp.toISOString().split("T")[0];
    byDay.set(d, (byDay.get(d) ?? 0) + 1);
  }

  const todayStr = new Date().toISOString().split("T")[0];
  const todayCount = byDay.get(todayStr) ?? 0;

  // Previous 14 days (excluding today) — this forms the baseline.
  const sortedDays = [...byDay.keys()].filter((d) => d !== todayStr).sort();
  if (sortedDays.length < 7) return out; // not enough history to compare

  const history = sortedDays.slice(-14).map((d) => byDay.get(d) ?? 0);
  const mean = history.reduce((s, x) => s + x, 0) / history.length;
  const variance = history.reduce((s, x) => s + (x - mean) ** 2, 0) / history.length;
  const stdDev = Math.sqrt(variance);
  const z = stdDev > 0 ? (todayCount - mean) / stdDev : 0;

  // Same-weekday comparison (Monday vs last Monday). We look exactly 7
  // days back so weekly-pattern noise is absorbed.
  const weekAgoStr = new Date(Date.now() - 7 * 86_400_000).toISOString().split("T")[0];
  const weekAgoCount = byDay.get(weekAgoStr) ?? 0;
  const wowPct = weekAgoCount > 0 ? Math.round((todayCount / weekAgoCount) * 100) : null;

  if (z <= -2 && wowPct !== null && wowPct < 70) {
    const dropPct = Math.round(((mean - todayCount) / mean) * 100);
    out.push({
      type: "traffic_anomaly",
      severity: "high",
      title: `Traffic dropped ${dropPct}% today`,
      description: `${todayCount.toLocaleString()} pageviews today vs a ${Math.round(mean).toLocaleString()}-day average and ${weekAgoCount.toLocaleString()} on the same weekday last week. Possible causes: server issues, a Google algorithm update, or blocked crawling.`,
      pageUrl: "",
      metadata: {
        kind: "drop",
        today: todayCount,
        average: Math.round(mean),
        stdDev: Math.round(stdDev),
        zScore: Math.round(z * 100) / 100,
        sameWeekdayPct: wowPct,
      },
    });
  } else if (z >= 2 && wowPct !== null && wowPct > 130) {
    const surgePct = mean > 0 ? Math.round(((todayCount - mean) / mean) * 100) : 100;
    out.push({
      type: "traffic_anomaly",
      severity: "low", // "opportunity" maps to a low-urgency UI; wins don't need to interrupt
      title: `Traffic surged ${surgePct}% today`,
      description: `${todayCount.toLocaleString()} pageviews today vs a ${Math.round(mean).toLocaleString()}-day average and ${weekAgoCount.toLocaleString()} on the same weekday last week. Check if a page went viral, earned a featured snippet, or received press coverage.`,
      pageUrl: "",
      metadata: {
        kind: "surge",
        today: todayCount,
        average: Math.round(mean),
        stdDev: Math.round(stdDev),
        zScore: Math.round(z * 100) / 100,
        sameWeekdayPct: wowPct,
      },
    });
  }

  return out;
}

// ─── D) Conversion rate drop detection ─────────────────────────────────────

/**
 * Conversion health check. Two signals emitted independently:
 *   • CRITICAL when the conversion rate (conversions / sessions) dropped
 *     by 30%+ week-over-week (and both windows had real traffic).
 *   • CRITICAL when zero conversions have landed in 48h despite ≥50
 *     sessions — strongly suggests form tracking is broken.
 *
 * Sessions come from TrackedEvent session_start events; conversions
 * from SiteConversion. Both tables are what the Site Tag writes live,
 * matching how the AI Strategist context builder already queries them.
 */
export async function detectConversionDrop(brandId: string): Promise<DetectedSignal[]> {
  const out: DetectedSignal[] = [];
  const now = Date.now();
  const weekAgo = new Date(now - 7 * 86_400_000);
  const twoWeeksAgo = new Date(now - 14 * 86_400_000);
  const twoDaysAgo = new Date(now - 2 * 86_400_000);

  const site = await prisma.trackedSite.findUnique({
    where: { brandId },
    select: { id: true },
  });
  if (!site) return out;

  // Sessions read from TrackedSession (the deduped unique-visitor
  // count) to stay in lockstep with every other session-showing
  // surface. Previously counted TrackedEvent.eventType="session_start",
  // which overcounts by ~10% because not every session emits the event
  // cleanly and re-starts after idle timeout are counted twice.
  const [convThis, convPrev, sessThis, sessPrev, convRecent, sessRecent] = await Promise.all([
    // intentionally unfiltered: trending signal; raw count is intentional for spike detection
    prisma.siteConversion.count({ where: { brandId, timestamp: { gte: weekAgo } } }),
    // intentionally unfiltered: trending signal; raw count is intentional for spike detection
    prisma.siteConversion.count({ where: { brandId, timestamp: { gte: twoWeeksAgo, lt: weekAgo } } }),
    prisma.trackedSession.count({
      where: { trackedSiteId: site.id, startedAt: { gte: weekAgo } },
    }),
    prisma.trackedSession.count({
      where: { trackedSiteId: site.id, startedAt: { gte: twoWeeksAgo, lt: weekAgo } },
    }),
    // intentionally unfiltered: trending signal; raw count is intentional for spike detection
    prisma.siteConversion.count({ where: { brandId, timestamp: { gte: twoDaysAgo } } }),
    prisma.trackedSession.count({
      where: { trackedSiteId: site.id, startedAt: { gte: twoDaysAgo } },
    }),
  ]);

  // Zero-conversions-with-traffic signal (independent of rate comparison)
  if (convRecent === 0 && sessRecent >= 50) {
    out.push({
      type: "conversion_drop",
      severity: "critical",
      title: "No conversions detected in the last 48 hours",
      description: `${sessRecent.toLocaleString()} sessions in the last 48 hours but zero conversions recorded. Form tracking may be broken, or CTAs/forms may have been removed.`,
      pageUrl: "",
      metadata: { kind: "zero_with_traffic", sessionsLast48h: sessRecent, conversionsLast48h: convRecent },
    });
  }

  // Rate-drop signal — only when both windows have non-trivial traffic
  if (sessThis > 0 && sessPrev > 0) {
    const rateThis = convThis / sessThis;
    const ratePrev = convPrev / sessPrev;
    if (ratePrev > 0) {
      const dropPct = Math.round(((ratePrev - rateThis) / ratePrev) * 100);
      if (dropPct >= 30 && convPrev >= 3) {
        out.push({
          type: "conversion_drop",
          severity: "critical",
          title: `Conversion rate dropped ${dropPct}%`,
          description: `Rate fell from ${(ratePrev * 100).toFixed(2)}% (${convPrev} / ${sessPrev}) last week to ${(rateThis * 100).toFixed(2)}% (${convThis} / ${sessThis}) this week. Check if forms are broken, CTAs were removed, or the page layout changed.`,
          pageUrl: "",
          metadata: {
            kind: "rate_drop",
            thisWeekConversions: convThis,
            thisWeekSessions: sessThis,
            thisWeekRate: Math.round(rateThis * 10000) / 100,
            prevWeekConversions: convPrev,
            prevWeekSessions: sessPrev,
            prevWeekRate: Math.round(ratePrev * 10000) / 100,
            dropPct,
          },
        });
      }
    }
  }

  return out;
}

// ─── E) Exit intent spike detection ────────────────────────────────────────

/**
 * Emits a MEDIUM signal for any page where the share of sessions that
 * fire an exit_intent event exceeds 50% over the last 7 days. Skips
 * low-traffic pages (<20 sessions) so a single visitor with an itchy
 * mouse doesn't spook a quiet URL into a fake alert.
 */
export async function detectExitIntentSpikes(brandId: string): Promise<DetectedSignal[]> {
  const out: DetectedSignal[] = [];
  const site = await prisma.trackedSite.findUnique({
    where: { brandId },
    select: { id: true },
  });
  if (!site) return out;

  const weekAgo = new Date(Date.now() - 7 * 86_400_000);

  // Sessions per URL (page_view events are the broadest proxy for "sessions
  // that saw this page"; session_start alone doesn't carry pageUrl context
  // reliably enough for per-URL attribution).
  const [exitEvents, pageViews] = await Promise.all([
    prisma.siteEvent.groupBy({
      by: ["pageUrl"],
      where: { brandId, eventType: "exit_intent", timestamp: { gte: weekAgo } },
      _count: true,
    }),
    prisma.trackedEvent.groupBy({
      by: ["pageUrl"],
      where: { trackedSiteId: site.id, eventType: "page_view", timestamp: { gte: weekAgo } },
      _count: true,
    }),
  ]);

  if (exitEvents.length === 0) return out;

  const viewsByUrl = new Map(pageViews.map((r) => [r.pageUrl, r._count]));
  for (const row of exitEvents) {
    const views = viewsByUrl.get(row.pageUrl) ?? 0;
    if (views < 20) continue; // insufficient traffic for a stable rate
    const rate = row._count / views;
    if (rate >= 0.5) {
      const pct = Math.round(rate * 100);
      out.push({
        type: "exit_intent_spike",
        severity: "medium",
        title: `High exit intent on ${row.pageUrl}`,
        description: `${pct}% of visitors (${row._count} of ${views} pageviews) triggered exit intent in the last 7 days. Consider adding retention elements — exit-intent modal, trust badges, or a clearer CTA.`,
        pageUrl: row.pageUrl,
        metadata: {
          kind: "high_exit_intent",
          exitEvents: row._count,
          pageViews: views,
          exitRatePct: pct,
        },
      });
    }
  }

  return out;
}

// ─── F) Broken resource alert ──────────────────────────────────────────────

/**
 * Flags broken on-page resources (404'd images, failed scripts, bad CSS)
 * that hit 5+ distinct visitors. Events carry the resource URL and type
 * in eventData so we can group by "page + resource" rather than just
 * page — the same page might have two separate broken assets and we
 * want one signal per asset.
 */
export async function detectBrokenResources(brandId: string): Promise<DetectedSignal[]> {
  const out: DetectedSignal[] = [];
  const weekAgo = new Date(Date.now() - 7 * 86_400_000);

  // Both legacy spellings show up in the field notes — accept either.
  const events = await prisma.siteEvent.findMany({
    where: {
      brandId,
      eventType: { in: ["resource_error", "resource_errors"] },
      timestamp: { gte: weekAgo },
    },
    select: { pageUrl: true, visitorId: true, eventData: true },
  });
  if (events.length === 0) return out;

  type Agg = { visitors: Set<string>; count: number; resourceType: string };
  const byKey = new Map<string, Agg>();
  for (const e of events) {
    const d = (e.eventData ?? {}) as Record<string, unknown>;
    const resourceUrl = typeof d.resourceUrl === "string"
      ? d.resourceUrl
      : typeof d.url === "string" ? d.url : null;
    if (!resourceUrl) continue;
    const resourceType = typeof d.resourceType === "string"
      ? d.resourceType
      : typeof d.type === "string" ? d.type : "resource";
    const key = `${e.pageUrl}|${resourceUrl}`;
    const agg = byKey.get(key) ?? { visitors: new Set(), count: 0, resourceType };
    if (e.visitorId) agg.visitors.add(e.visitorId);
    agg.count++;
    byKey.set(key, agg);
  }

  for (const [key, agg] of byKey) {
    if (agg.visitors.size < 5) continue;
    const [pageUrl, resourceUrl] = key.split("|");
    out.push({
      type: "broken_resource",
      severity: "medium",
      title: `Broken ${agg.resourceType} on ${pageUrl}`,
      description: `${resourceUrl} failed to load for ${agg.visitors.size} visitor${agg.visitors.size === 1 ? "" : "s"} in the last 7 days (${agg.count} total occurrences). Fix the asset or remove the reference.`,
      pageUrl,
      metadata: {
        kind: "broken_resource",
        resourceUrl,
        resourceType: agg.resourceType,
        uniqueVisitors: agg.visitors.size,
        occurrences: agg.count,
      },
    });
  }

  return out;
}

// ─── Orchestration ──────────────────────────────────────────────────────────

/** Every detector registered with the engine. New detectors go here. */
const DETECTORS: Array<(brandId: string) => Promise<DetectedSignal[]>> = [
  detectErrorSpikes,
  detectRageClickSpikes,
  detectTrafficAnomalies,
  detectConversionDrop,
  detectExitIntentSpikes,
  detectBrokenResources,
];

/**
 * Run every registered detector for a brand. Detectors run in parallel so
 * a slow one doesn't block the others; individual failures are logged and
 * swallowed so a single broken detector can't take out the whole run.
 */
export async function runSignalDetection(brandId: string): Promise<DetectedSignal[]> {
  const results = await Promise.all(
    DETECTORS.map(async (detector) => {
      try {
        return await detector(brandId);
      } catch (err) {
        const msg = err instanceof Error ? err.message : "unknown";
        log.error("system", `[signal-detection] ${detector.name} failed for brand ${brandId}: ${msg}`, {
          metadata: { brandId, detector: detector.name, err: msg },
        });
        return [] as DetectedSignal[];
      }
    }),
  );
  return results.flat();
}

// ─── Persistence (dedup + auto-resolve) ─────────────────────────────────────

export interface PersistSummary {
  brandId: string;
  /** Newly-inserted signals. */
  created: number;
  /** Already-open signals that the detector surfaced again — skipped. */
  deduped: number;
  /** Previously-open signals whose condition cleared — marked resolved. */
  resolved: number;
  durationMs: number;
}

/**
 * Unique identity of an open signal — `type + pageUrl + metadata.kind`.
 * The `kind` tie-breaker lets a single detector emit multiple flavours
 * of the same type per URL (e.g. `server_error`, `404_spike`,
 * `was_working_now_404` all share `error_spike` but are distinct).
 */
function signalKey(type: SignalType, pageUrl: string, metadata: Record<string, unknown>): string {
  const kind = typeof metadata.kind === "string" ? metadata.kind : "";
  return `${type}|${pageUrl}|${kind}`;
}

/**
 * Run the engine and write results through with dedup + auto-resolve:
 *   • Existing unresolved signal with the same key in the last 24h?
 *     Skip the new one (prevents duplicate inserts when a user sits on
 *     the on-demand endpoint or cron runs overlap).
 *   • Existing unresolved signal whose key is NOT in the new detection
 *     set? Mark it resolved with `resolvedAt` — the condition has cleared
 *     since the last run (404 got fixed, rage clicks stopped, etc.).
 */
export async function runAndPersistSignals(brandId: string): Promise<PersistSummary> {
  const start = Date.now();
  const detected = await runSignalDetection(brandId);
  const now = new Date();
  const dayAgo = new Date(now.getTime() - 86_400_000);

  // Pull currently-open signals for this brand in the 24h window.
  // Enumerate every signal_* eventType explicitly — Prisma rejects
  // `startsWith` inside the `in` filter, and this list is short.
  const SIGNAL_EVENT_TYPES = [
    "signal_error_spike",
    "signal_rage_click_spike",
    "signal_traffic_anomaly",
    "signal_conversion_drop",
    "signal_exit_intent_spike",
    "signal_broken_resource",
  ];
  const recentSignals = await prisma.siteEvent.findMany({
    where: {
      brandId,
      timestamp: { gte: dayAgo },
      eventType: { in: SIGNAL_EVENT_TYPES },
    },
    select: { id: true, eventType: true, pageUrl: true, eventData: true },
  });

  const openByKey = new Map<string, { id: string; data: Record<string, unknown> }>();
  for (const row of recentSignals) {
    const data = (row.eventData ?? {}) as Record<string, unknown>;
    if (data.status === "resolved") continue;
    const type = row.eventType.slice("signal_".length) as SignalType;
    const meta = (data.metadata ?? {}) as Record<string, unknown>;
    openByKey.set(signalKey(type, row.pageUrl, meta), { id: row.id, data });
  }

  const detectedKeys = new Set<string>();
  let created = 0;
  let deduped = 0;
  let resolved = 0;

  for (const s of detected) {
    const key = signalKey(s.type, s.pageUrl, s.metadata);
    detectedKeys.add(key);
    if (openByKey.has(key)) { deduped++; continue; }
    try {
      await prisma.siteEvent.create({
        data: {
          brandId,
          pageUrl: s.pageUrl,
          eventType: signalEventType(s.type),
          eventData: {
            signalType: s.type,
            severity: s.severity,
            title: s.title,
            description: s.description,
            metadata: s.metadata,
            status: "new",
          } as object,
        },
      });
      created++;
    } catch (err) {
      log.error("system", `[signal-detection] persist failed for brand ${brandId}: ${err instanceof Error ? err.message : "unknown"}`);
    }
  }

  // Auto-resolve: anything still open whose key didn't re-appear.
  for (const [key, row] of openByKey) {
    if (detectedKeys.has(key)) continue;
    try {
      await prisma.siteEvent.update({
        where: { id: row.id },
        data: {
          eventData: {
            ...row.data,
            status: "resolved",
            resolvedAt: now.toISOString(),
          } as object,
        },
      });
      resolved++;
    } catch { /* best-effort */ }
  }

  return { brandId, created, deduped, resolved, durationMs: Date.now() - start };
}
