/**
 * Server-side socket event emitter helpers.
 * Rooms:
 *   owner:<ownerId>        — owner-only updates (their org)
 *   approver:<approverId>  — assigned approver
 *   checkpoint:<id>        — reception operator
 */
import { io } from '../server';
import { prisma } from '../config/database';

export type EventPayload = Record<string, any>;
export type RecipientType = 'OWNER' | 'APPROVER' | 'CHECKPOINT';

const ownerRoom = (ownerId: string) => `owner:${ownerId}`;
const approverRoom = (approverId: string) => `approver:${approverId}`;
const checkpointRoom = (checkpointId: string) => `checkpoint:${checkpointId}`;

export function emitToOwner(ownerId: string, event: string, payload: EventPayload) {
  try { io.to(ownerRoom(ownerId)).emit(event, payload); } catch (e) { console.error('emitToOwner', e); }
}
export function emitToApprover(approverId: string, event: string, payload: EventPayload) {
  try { io.to(approverRoom(approverId)).emit(event, payload); } catch (e) { console.error('emitToApprover', e); }
}
export function emitToCheckpoint(checkpointId: string, event: string, payload: EventPayload) {
  try { io.to(checkpointRoom(checkpointId)).emit(event, payload); } catch (e) { console.error('emitToCheckpoint', e); }
}

interface NotifyOpts {
  recipientType: RecipientType;
  recipientId: string;
  type: string;
  title: string;
  body?: string;
  link?: string;
}

/**
 * Marks any open `visitor.awaiting` notifications that reference a given
 * short-id as read across all recipients, then emits a `notification.read`
 * event so connected bells can drop the unread badge in real-time.
 * Called whenever a visitor's awaiting state is resolved (approve / reject
 * / cancel / expire) so the same "Approve {name}" entry doesn't keep
 * tempting the user to act on a settled decision.
 */
export async function resolveAwaitingNotifications(shortId: string) {
  try {
    const tag = `#${shortId}`;
    const rows = await prisma.notification.findMany({
      where: { type: 'visitor.awaiting', readAt: null, body: { contains: tag } },
      select: { id: true, recipientType: true, recipientId: true },
    });
    if (rows.length === 0) return;
    const now = new Date();
    await prisma.notification.updateMany({
      where: { id: { in: rows.map((r) => r.id) } },
      data: { readAt: now },
    });
    for (const r of rows) {
      const room = r.recipientType === 'OWNER'
        ? ownerRoom(r.recipientId)
        : r.recipientType === 'APPROVER'
          ? approverRoom(r.recipientId)
          : checkpointRoom(r.recipientId);
      io.to(room).emit('notification.read', { id: r.id, readAt: now.toISOString() });
    }
  } catch (e) {
    console.error('resolveAwaitingNotifications', e);
  }
}

/**
 * Deletes every visitor.* notification whose body references one of the given
 * shortIds. Call this from any code path that deletes Visitor rows so the
 * bell doesn't keep referencing rows that no longer exist. Also emits
 * `notification.deleted` per recipient room so live bells drop the entry
 * without a refresh. Best-effort — never throws.
 */
export async function deleteVisitorNotifications(shortIds: string[]) {
  if (shortIds.length === 0) return { count: 0 };
  try {
    // Body always contains `#${shortId}` (see recordNotification call sites).
    const where = {
      type: { startsWith: 'visitor.' },
      OR: shortIds.map((s) => ({ body: { contains: `#${s}` } })),
    };
    const rows = await prisma.notification.findMany({
      where,
      select: { id: true, recipientType: true, recipientId: true },
    });
    if (rows.length === 0) return { count: 0 };
    const result = await prisma.notification.deleteMany({ where: { id: { in: rows.map((r) => r.id) } } });
    for (const r of rows) {
      const room = r.recipientType === 'OWNER'
        ? ownerRoom(r.recipientId)
        : r.recipientType === 'APPROVER'
          ? approverRoom(r.recipientId)
          : checkpointRoom(r.recipientId);
      io.to(room).emit('notification.deleted', { id: r.id });
    }
    return { count: result.count };
  } catch (e) {
    console.error('deleteVisitorNotifications', e);
    return { count: 0 };
  }
}

/**
 * Persists a Notification row + emits `notification.new` to the recipient's room.
 * Use alongside emitToX when the event should also surface in the bell/inbox.
 *
 * Push notifications: when `opts.push` is provided (and the recipient owns any
 * registered DeviceTokens), fire the same content to the mobile app via FCM.
 * `push.data` (e.g. `{ visitorId: '...' }`) lets the mobile app deep-link.
 */
export async function recordNotification(opts: NotifyOpts & { push?: { data?: Record<string, string> } }) {
  try {
    const n = await prisma.notification.create({
      data: {
        recipientType: opts.recipientType,
        recipientId: opts.recipientId,
        type: opts.type,
        title: opts.title,
        body: opts.body || null,
        link: opts.link || null,
      },
    });
    const room = opts.recipientType === 'OWNER'
      ? ownerRoom(opts.recipientId)
      : opts.recipientType === 'APPROVER'
        ? approverRoom(opts.recipientId)
        : checkpointRoom(opts.recipientId);
    io.to(room).emit('notification.new', { notification: n });
    // Fire-and-forget push if the caller opted in. Imported lazily so this
    // module doesn't pull firebase-admin into every request path. Mobile
    // (FCM/APNs) and browser (Web Push) dispatchers run side-by-side — each
    // user might have only one of the two, or both, registered.
    if (opts.push) {
      try {
        const { sendPushToRecipient } = await import('./push');
        sendPushToRecipient(
          { type: opts.recipientType, id: opts.recipientId },
          { title: opts.title, body: opts.body || '', data: opts.push.data },
        ).catch((e) => console.error('[push] dispatch failed', e));
      } catch (e) {
        console.error('[push] import failed', e);
      }
      try {
        const { sendWebPushToRecipient } = await import('./webPush');
        sendWebPushToRecipient(
          { type: opts.recipientType, id: opts.recipientId },
          { title: opts.title, body: opts.body || '', data: opts.push.data, link: opts.link },
        ).catch((e) => console.error('[webpush] dispatch failed', e));
      } catch (e) {
        console.error('[webpush] import failed', e);
      }
    }
    return n;
  } catch (e) {
    console.error('recordNotification', e);
    return null;
  }
}
