/**
 * Notification source aggregator
 * ────────────────────────────────
 * The bell + notifications page pull from two data sources that the
 * /api/notifications routes need to union:
 *
 *   1. prisma.Notification — the "real" notification table, populated
 *      by alert-rule triggers, user-facing system events, etc. Has
 *      proper isRead / readAt columns.
 *
 *   2. prisma.SiteEvent where eventType LIKE 'signal_%' or 'rank_%' —
 *      the detection engines (src/lib/services/signal-detection.ts +
 *      src/app/api/cron/rank-check) persist alerts here rather than
 *      writing separate rows into Notification. That predates the
 *      bell work and the detectors already carry title / description /
 *      severity in their eventData JSON.
 *
 * Rather than shadow-inserting a Notification row every time a signal
 * fires (duplicates + a second cleanup path to get wrong), we merge at
 * read time. This file owns the merge + the normalised shape that the
 * three notification routes return.
 *
 * Models the spec referenced (DetectedSignal, AiVisibilityChange,
 * IndustryAlert) don't exist as Prisma tables in this repo. SiteEvent
 * with eventType='signal_*' is the closest analog for DetectedSignal.
 * When those tables land, add another branch to listSignalEvents().
 */

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

/** Unified shape the bell + notifications page consume. */
export interface UnifiedNotification {
  id: string;
  /** "notification" (Notification row) | "signal" (SiteEvent signal) | "rank" (SiteEvent rank_*). */
  source: "notification" | "signal" | "rank";
  title: string;
  description: string;
  priority: "critical" | "high" | "medium" | "low";
  /** true when the user has seen it (Notification.isRead, or signal
   *  eventData.status !== "new"). */
  read: boolean;
  createdAt: string;
  /** Page to open when the item is clicked. */
  sourceUrl: string;
}

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 RANK_EVENT_TYPES = [
  "rank_entered_top3",
  "rank_dropped_off_page1",
  "rank_improved",
  "rank_declined",
];

function severityToPriority(sev: string | undefined | null): UnifiedNotification["priority"] {
  switch (sev) {
    case "critical": return "critical";
    case "high": return "high";
    case "medium": return "medium";
    case "low":
    case "info":
    default: return "low";
  }
}

interface SignalRow {
  id: string;
  eventType: string;
  pageUrl: string;
  eventData: unknown;
  timestamp: Date;
}

function normalizeSignal(row: SignalRow): UnifiedNotification {
  const data = (row.eventData ?? {}) as Record<string, unknown>;
  const title = typeof data.title === "string"
    ? data.title
    : typeof data.keyword === "string"
      ? String(data.title ?? `${row.eventType.replace(/^rank_/, "").replace(/_/g, " ")}: ${data.keyword}`)
      : row.eventType.replace(/^signal_/, "").replace(/_/g, " ");
  const description = typeof data.description === "string"
    ? data.description
    : row.pageUrl || "";
  const sevRaw = typeof data.severity === "string" ? data.severity : (typeof data.priority === "string" ? data.priority : "medium");
  const status = typeof data.status === "string" ? data.status : undefined;
  const readAt = typeof data.readAt === "string" ? data.readAt : undefined;

  // Unread = signal status is still "new" (detection engine), OR (for
  // rank signals which don't use status) the eventData has no readAt.
  const isSignalRow = row.eventType.startsWith("signal_");
  const read = isSignalRow ? status !== "new" : !!readAt;

  return {
    id: row.id,
    source: isSignalRow ? "signal" : "rank",
    title,
    description,
    priority: severityToPriority(sevRaw),
    read,
    createdAt: row.timestamp.toISOString(),
    sourceUrl: isSignalRow ? "/signals" : "/rank-tracker",
  };
}

/**
 * Pull recent signal-type SiteEvent rows for the brand, normalised to
 * the UnifiedNotification shape. Only looks back 7 days — older signals
 * stay in the database for audit but aren't relevant to a bell badge.
 */
export async function listSignalEvents(brandId: string, limit: number): Promise<UnifiedNotification[]> {
  const since = new Date(Date.now() - 7 * 86_400_000);
  const rows = await prisma.siteEvent.findMany({
    where: {
      brandId,
      eventType: { in: [...SIGNAL_EVENT_TYPES, ...RANK_EVENT_TYPES] },
      timestamp: { gte: since },
    },
    orderBy: { timestamp: "desc" },
    take: limit,
    select: { id: true, eventType: true, pageUrl: true, eventData: true, timestamp: true },
  });
  return rows.map(normalizeSignal);
}

/** Count of UNREAD signal + rank SiteEvent rows for the brand badge. */
export async function countUnreadSignalEvents(brandId: string): Promise<number> {
  const since = new Date(Date.now() - 7 * 86_400_000);
  // Can't filter on nested JSON path reliably across providers, so we
  // fetch the minimal fields and count in JS. 7-day + type filter keeps
  // the payload tiny (typically <50 rows even on busy brands).
  const rows = await prisma.siteEvent.findMany({
    where: {
      brandId,
      eventType: { in: [...SIGNAL_EVENT_TYPES, ...RANK_EVENT_TYPES] },
      timestamp: { gte: since },
    },
    select: { eventType: true, eventData: true },
  });
  let count = 0;
  for (const r of rows) {
    const data = (r.eventData ?? {}) as Record<string, unknown>;
    if (r.eventType.startsWith("signal_")) {
      if (data.status === "new") count++;
    } else if (!data.readAt) {
      count++;
    }
  }
  return count;
}

/**
 * Mark a batch of signal SiteEvent IDs as "read" for the bell. Rewrites
 * each row's eventData JSON — signal_* rows flip status="new" → "acknowledged"
 * (matches the Signals Center semantics), rank_* rows get a readAt stamp.
 * Already-resolved or already-acknowledged rows pass through untouched.
 */
export async function markSignalsRead(brandId: string, ids: string[]): Promise<number> {
  if (ids.length === 0) return 0;
  const rows = await prisma.siteEvent.findMany({
    where: { id: { in: ids }, brandId },
    select: { id: true, eventType: true, eventData: true },
  });
  let updated = 0;
  const now = new Date().toISOString();
  for (const r of rows) {
    const data = (r.eventData ?? {}) as Record<string, unknown>;
    const isSignal = r.eventType.startsWith("signal_");
    const isRank = r.eventType.startsWith("rank_");
    if (!isSignal && !isRank) continue;
    if (isSignal && data.status !== "new") continue;
    if (isRank && data.readAt) continue;
    const nextData = isSignal
      ? { ...data, status: "acknowledged", acknowledgedAt: now }
      : { ...data, readAt: now };
    try {
      await prisma.siteEvent.update({ where: { id: r.id }, data: { eventData: nextData as object } });
      updated++;
    } catch { /* best-effort */ }
  }
  return updated;
}

/** Mark every unread signal + rank SiteEvent for a brand as read. */
export async function markAllSignalsRead(brandId: string): Promise<number> {
  const since = new Date(Date.now() - 7 * 86_400_000);
  const rows = await prisma.siteEvent.findMany({
    where: {
      brandId,
      eventType: { in: [...SIGNAL_EVENT_TYPES, ...RANK_EVENT_TYPES] },
      timestamp: { gte: since },
    },
    select: { id: true },
  });
  if (rows.length === 0) return 0;
  return markSignalsRead(brandId, rows.map((r) => r.id));
}
