import { prisma } from '../config/database';
import type { RecipientType } from './events';

// Firebase Admin SDK is lazy-loaded so the API still boots when no creds are
// set (dev / fresh checkouts). Set FIREBASE_SERVICE_ACCOUNT_JSON to the raw
// JSON string of a service-account key, OR set FIREBASE_SERVICE_ACCOUNT_PATH
// to a file path. Without either we log + skip, so push calls are no-ops
// instead of throwing.
let fcm: any = null;
let fcmAttempted = false;

async function getFcm(): Promise<any> {
  if (fcmAttempted) return fcm;
  fcmAttempted = true;
  const raw = process.env.FIREBASE_SERVICE_ACCOUNT_JSON;
  const path = process.env.FIREBASE_SERVICE_ACCOUNT_PATH;
  if (!raw && !path) {
    console.warn('[push] FIREBASE_SERVICE_ACCOUNT_JSON / _PATH not set — push notifications disabled.');
    return null;
  }
  try {
    const adminSdk = await import('firebase-admin');
    const serviceAccount = raw ? JSON.parse(raw) : await import(path!);
    if (!adminSdk.apps.length) {
      adminSdk.initializeApp({ credential: adminSdk.credential.cert(serviceAccount as any) });
    }
    fcm = adminSdk.messaging();
    return fcm;
  } catch (e) {
    console.error('[push] failed to init firebase-admin', e);
    return null;
  }
}

interface PushPayload {
  title: string;
  body: string;
  // Arbitrary key/value data the mobile app reads to deep-link (e.g. visitor id).
  data?: Record<string, string>;
}

// Sends a push notification to every device the recipient has registered.
// Best-effort: stale tokens are cleaned up; errors are logged, not thrown.
export async function sendPushToRecipient(
  recipient: { type: RecipientType; id: string },
  payload: PushPayload,
): Promise<void> {
  try {
    // CHECKPOINT tokens are stored with approverId = checkpointId (no extra
    // column needed — see devices.controller.ts pickPrincipal).
    // APPROVER id can appear under approverId (legacy table) or adminId
    // (Admin-as-approver after the merge). We also accept OWNER tokens
    // registered directly under adminId === id (legacy fallback rows).
    const orClauses: any[] =
      recipient.type === 'OWNER'
        ? [{ ownerId: recipient.id, adminId: null, approverId: null }, { adminId: recipient.id }]
      : recipient.type === 'APPROVER'
        ? [{ approverId: recipient.id }, { adminId: recipient.id }]
      : recipient.type === 'CHECKPOINT'
        ? [{ approverId: recipient.id }]
      : [];
    if (orClauses.length === 0) return;

    const tokens = await prisma.deviceToken.findMany({
      where: { OR: orClauses },
      select: { id: true, token: true },
    });
    if (tokens.length === 0) return;

    const messaging = await getFcm();
    if (!messaging) {
      console.log('[push] skipped (no creds):', payload.title, '→', tokens.length, 'token(s)');
      return;
    }

    const res = await messaging.sendEachForMulticast({
      tokens: tokens.map((t: { token: string }) => t.token),
      notification: { title: payload.title, body: payload.body },
      data: payload.data || {},
      apns: { payload: { aps: { sound: 'default', 'mutable-content': 1 } } },
    });
    // Prune tokens FCM tells us are bad so we don't keep firing at them.
    const stale: string[] = [];
    res.responses.forEach((r: any, i: number) => {
      if (!r.success) {
        const code = r.error?.code || '';
        if (code === 'messaging/registration-token-not-registered' || code === 'messaging/invalid-registration-token') {
          stale.push(tokens[i].id);
        }
      }
    });
    if (stale.length > 0) {
      await prisma.deviceToken.deleteMany({ where: { id: { in: stale } } });
    }
  } catch (e) {
    console.error('[push] sendPushToRecipient failed', e);
  }
}
