/**
 * Live-notifications query layer — typed async functions for the
 * `live_notifications` table (symbol: liveNotifications).
 *
 * NOTE: Named `live-notifications` / `liveNotifications` to avoid
 * collision with the existing vendor `notifications` table.
 * Downstream tasks T5/T6/T15/T19/T26 must import from this file.
 */

import { and, desc, eq, gt, inArray, isNull, lt, or, sql } from 'drizzle-orm';
import type { DrizzleClient } from '../client.js';
import { liveNotifications, type NotificationInsert, type NotificationRow } from '../schema.js';

export type { NotificationInsert, NotificationRow };

export interface NotificationCursor {
  createdAt: Date;
  id: string;
}

export interface NotificationPage {
  items: NotificationRow[];
  nextCursor: string | null;
}

/** Cursor ordering is createdAt DESC, then id DESC for stable page boundaries. */
function encodeCursor(row: NotificationRow): string {
  return `${row.createdAt.toISOString()}:${row.id}`;
}

export async function insertNotification(
  db: DrizzleClient,
  row: NotificationInsert,
): Promise<NotificationRow> {
  const [out] = await db.insert(liveNotifications).values(row).returning();
  if (!out) throw new Error('insertNotification: returning() yielded no row');
  return out;
}

export async function listInboxPage(
  db: DrizzleClient,
  userId: string,
  opts: {
    unreadOnly?: boolean;
    limit?: number;
    afterTs?: Date | null;
    afterCursor?: NotificationCursor | null;
  } = {},
): Promise<NotificationPage> {
  const limit = Math.max(1, Math.min(opts.limit ?? 50, 100));
  const cursorWhere = opts.afterCursor
    ? or(
        lt(liveNotifications.createdAt, opts.afterCursor.createdAt),
        and(
          eq(liveNotifications.createdAt, opts.afterCursor.createdAt),
          lt(liveNotifications.id, opts.afterCursor.id),
        ),
      )
    : opts.afterTs
      ? gt(liveNotifications.createdAt, opts.afterTs)
      : sql`true`;
  const where = and(
    eq(liveNotifications.userId, userId),
    opts.unreadOnly ? isNull(liveNotifications.readAt) : sql`true`,
    cursorWhere,
  );
  const rows = await db
    .select()
    .from(liveNotifications)
    .where(where)
    .orderBy(desc(liveNotifications.createdAt), desc(liveNotifications.id))
    .limit(limit + 1);
  const hasMore = rows.length > limit;
  const items = hasMore ? rows.slice(0, limit) : rows;
  const last = items.at(-1);
  return {
    items,
    nextCursor: hasMore && last ? encodeCursor(last) : null,
  };
}

export async function markRead(db: DrizzleClient, userId: string, ids: string[]): Promise<number> {
  if (ids.length === 0) return 0;
  const res = await db
    .update(liveNotifications)
    .set({ readAt: new Date() })
    .where(
      and(
        eq(liveNotifications.userId, userId),
        inArray(liveNotifications.id, ids),
        isNull(liveNotifications.readAt),
      ),
    )
    .returning({ id: liveNotifications.id });
  return res.length;
}

export async function markAllRead(db: DrizzleClient, userId: string): Promise<number> {
  const res = await db
    .update(liveNotifications)
    .set({ readAt: new Date() })
    .where(and(eq(liveNotifications.userId, userId), isNull(liveNotifications.readAt)))
    .returning({ id: liveNotifications.id });
  return res.length;
}

export async function unreadCount(db: DrizzleClient, userId: string): Promise<number> {
  const [row] = await db
    .select({ n: sql<number>`count(*)::int` })
    .from(liveNotifications)
    .where(and(eq(liveNotifications.userId, userId), isNull(liveNotifications.readAt)));
  return row?.n ?? 0;
}

export async function replaySince(
  db: DrizzleClient,
  userId: string,
  sinceTs: Date,
  cap = 100,
): Promise<NotificationRow[]> {
  const page = await listInboxPage(db, userId, { afterTs: sinceTs, limit: cap });
  return page.items;
}

export async function getNotificationById(
  db: DrizzleClient,
  id: string,
): Promise<NotificationRow | null> {
  const [row] = await db
    .select()
    .from(liveNotifications)
    .where(eq(liveNotifications.id, id))
    .limit(1);
  return row ?? null;
}
