import type { ChecklistItem, UserSummary } from "@/lib/notes-api";

/**
 * Common fields shared by every floating-card popup entry, regardless
 * of whether it represents a fresh incoming note or a reply on an
 * existing thread. The popup component switches on `kind` to render the
 * matching variant; the queue uses `noteId` for primary dedupe and
 * (for replies) `replyId` as a secondary dedupe key so a reconnect
 * storm can't replay the same reply card twice.
 */
interface BasePayload {
  noteId: number;
  recipientRowId?: number;
  title: string;
  content: string;
  color: string;
  sentAt?: string;
  senderUserId: number;
  sender: UserSummary | null;
}

export interface IncomingNotePayload extends BasePayload {
  kind?: "note";
  /**
   * Mirrors the underlying note's `kind` ("text" | "checklist") so the
   * popup body can render the same surface as the NoteCard. Named
   * `noteKind` to avoid clashing with the popup-variant discriminator
   * (`kind: "note" | "reply"`) above.
   */
  noteKind?: "text" | "checklist" | null;
  items?: ChecklistItem[] | null;
}

export interface IncomingReplyPayload extends BasePayload {
  kind: "reply";
  replyId: number;
  replyContent: string;
  replier: UserSummary | null;
  noteTitle: string;
}

export type PopupPayload = IncomingNotePayload | IncomingReplyPayload;

function isReply(p: PopupPayload): p is IncomingReplyPayload {
  return p.kind === "reply";
}

function payloadKey(p: PopupPayload): string {
  return isReply(p) ? `reply:${p.replyId}` : `note:${p.noteId}`;
}

/**
 * Pure FIFO + dedupe + sender-suppression for the incoming-note queue.
 * Exported so unit tests can exercise the rules without React.
 *
 * Reply entries are deduped by `replyId`; note entries by `noteId`.
 * A reply on the *same* underlying note as a queued note is allowed —
 * the user wants to see both events.
 */
export function applyEnqueue(
  queue: PopupPayload[],
  payload: PopupPayload,
  currentUserId: number | null,
): PopupPayload[] {
  if (!payload || typeof payload.noteId !== "number") return queue;
  if (currentUserId != null && payload.senderUserId === currentUserId) {
    return queue;
  }
  if (isReply(payload) && typeof payload.replyId !== "number") return queue;
  const key = payloadKey(payload);
  if (queue.some((p) => payloadKey(p) === key)) return queue;
  return [...queue, payload];
}

/**
 * Predicate: would this payload actually land in the queue, or is it
 * silently dropped (sender's own note, duplicate, malformed)? Used by
 * the socket layer to decide whether to play the chime. Mirrors the
 * suppression rules in {@link applyEnqueue} so the two stay in sync.
 */
export function shouldEnqueueNote(
  queue: PopupPayload[],
  payload: PopupPayload,
  currentUserId: number | null,
): boolean {
  if (!payload || typeof payload.noteId !== "number") return false;
  if (currentUserId != null && payload.senderUserId === currentUserId) {
    return false;
  }
  if (isReply(payload) && typeof payload.replyId !== "number") return false;
  const key = payloadKey(payload);
  if (queue.some((p) => payloadKey(p) === key)) return false;
  return true;
}

/**
 * Pop the head of the queue if its identity matches; otherwise filter
 * the matching entry out from anywhere it appears. Pure for testability.
 *
 * Accepts either a noteId (for note entries) or `{ replyId }` (for
 * reply entries) so callers can dismiss either variant unambiguously.
 */
export function applyDismiss(
  queue: PopupPayload[],
  target: number | { replyId: number },
): PopupPayload[] {
  if (queue.length === 0) return queue;
  const key =
    typeof target === "number" ? `note:${target}` : `reply:${target.replyId}`;
  if (payloadKey(queue[0]) === key) return queue.slice(1);
  const next = queue.filter((p) => payloadKey(p) !== key);
  return next.length === queue.length ? queue : next;
}
