/**
 * eventChangeDispatcher — push notification dispatch for calendar event changes.
 *
 * Responsibilities (NOTIF-03, D-03, D-04, D-13):
 *   - dispatchEventChange(change, actorUserId): build copy per UI-SPEC, fan out to
 *     all non-actor members' push subscriptions.
 *   - isMeaningfulChange(changedFields): true when the change involves a meaningful
 *     field (dtstartUtc/dtstartDate/allDay/title/location). Description-only edits
 *     are silent (D-04). Create and delete are always meaningful.
 *
 * D-03: The actor (member whose sync detected/wrote the change) is NEVER notified.
 * D-13: Reads only from MariaDB push_subscriptions — no tsdav / Fastmail I/O here.
 * D-11: Every push displays a visible notification (no silent pushes).
 *
 * Fire-and-forget: sync correctness does not depend on push success.
 */

import { eq, ne } from 'drizzle-orm';
import { db } from '../db/client.js';
import { users, pushSubscriptions } from '../db/schema.js';
import { dispatchPush } from './pushDispatcher.js';

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

export type EventChangeOperation = 'create' | 'update' | 'delete';

/**
 * Meaningful fields whose change triggers a push notification (D-04).
 * Description-only edits stay silent.
 */
export const MEANINGFUL_FIELDS = new Set([
  'dtstartUtc',
  'dtstartDate',
  'allDay',
  'title',
  'location',
]);

/**
 * Payload describing a detected calendar event change.
 * Produced by syncCalendar and consumed by poller + outboxWorker.
 */
export interface EventChange {
  uid: string;
  title: string | null;
  operation: EventChangeOperation;
  /** For 'update': which fields changed. Omit for 'create' and 'delete'. */
  changedFields?: string[];
  /** UTC timestamp of the event start (for notification copy). */
  dtstartUtc?: Date | null;
  allDay?: boolean;
}

// ── Core logic ───────────────────────────────────────────────────────────────

/**
 * Returns true when the change should trigger a push notification.
 *
 * - 'create' and 'delete': always meaningful.
 * - 'update': meaningful only when changedFields contains at least one field
 *   in MEANINGFUL_FIELDS (title, dtstartUtc, dtstartDate, allDay, location).
 *   Description-only edits are silent (D-04).
 */
export function isMeaningfulChange(change: EventChange): boolean {
  if (change.operation === 'create' || change.operation === 'delete') {
    return true;
  }
  // update: require at least one meaningful field
  const fields = change.changedFields ?? [];
  return fields.some((f) => MEANINGFUL_FIELDS.has(f));
}

/**
 * Builds the notification copy for a change (D-02/D-03).
 *
 * @param change    - The detected calendar event change.
 * @param actorName - Display name of the actor (D-03: named in every notification).
 *                    Falls back to 'A family member' when the users row is absent.
 */
function buildCopy(
  change: EventChange,
  actorName: string,
): { notifTitle: string; notifBody: string; navigate: string } {
  const eventTitle = change.title ?? change.uid;

  let notifTitle: string;
  let notifBody: string;

  // D-02: event notifications show specifics — actor + title.
  // D-03: name the actor in every change notification.
  if (change.operation === 'create') {
    notifTitle = `${actorName} added an event`;
    notifBody = eventTitle;
  } else if (change.operation === 'delete') {
    notifTitle = `${actorName} removed an event`;
    notifBody = eventTitle;
  } else {
    // update
    notifTitle = `${actorName} updated an event`;
    notifBody = eventTitle;
  }

  // Navigate: /calendar?event=uid for create/update; /calendar for delete
  const navigate =
    change.operation === 'delete'
      ? '/calendar'
      : `/calendar?event=${encodeURIComponent(change.uid)}`;

  return { notifTitle, notifBody, navigate };
}

/**
 * Dispatch a push notification for a calendar event change to all non-actor members.
 *
 * D-03: actorUserId is excluded from the fan-out.
 * D-04: description-only edits (isMeaningfulChange=false) are silent.
 * D-13: reads only push_subscriptions from MariaDB — no tsdav or Fastmail I/O.
 * D-11: every push has a visible title + body (no silent pushes).
 *
 * Fire-and-forget: resolves after dispatching without awaiting push ACKs.
 */
export async function dispatchEventChange(change: EventChange, actorUserId: number): Promise<void> {
  // D-04: skip description-only edits
  if (!isMeaningfulChange(change)) {
    return;
  }

  // IN-01: resolve actor display name for D-02/D-03 notification copy.
  // Runs in parallel with subscription query for minimal latency.
  const [actorRows, allSubs] = await Promise.all([
    db
      .select({ displayName: users.displayName })
      .from(users)
      .where(eq(users.id, actorUserId))
      .limit(1),
    // D-13: query push_subscriptions from MariaDB only
    // D-03: ne() filter excludes the actor at DB level; application-level filter
    // below provides defence-in-depth (also makes the mock-based tests deterministic).
    db.select().from(pushSubscriptions).where(ne(pushSubscriptions.userId, actorUserId)),
  ]);

  const actorName: string = actorRows[0]?.displayName ?? 'A family member';

  // D-03: additional application-level actor exclusion (defence-in-depth)
  const subs = allSubs.filter((s) => s.userId !== actorUserId);

  if (subs.length === 0) {
    return;
  }

  const { notifTitle, notifBody, navigate } = buildCopy(change, actorName);

  // Fan out to all non-actor subscriptions (D-03 already enforced by ne() filter)
  for (const sub of subs) {
    try {
      await dispatchPush(sub, {
        title: notifTitle,
        body: notifBody,
        tag: `event-change-${change.uid}`,
        navigate,
      });
    } catch (err) {
      console.error(
        `[eventChangeDispatcher] Error dispatching for uid=${change.uid} sub.id=${sub.id}:`,
        err instanceof Error ? err.message : String(err),
      );
    }
  }
}
