import { getPushDevices, deletePushDevice, recordPushFailure, markPushDeviceSeen } from 'officerdb';
import type { Channel, DeliveryResult, Notification } from './types';
import { FcmAccessToken, parseServiceAccount } from './fcm-auth';
import { renderTitle, renderBody } from './text';

// The Google channel. FCM HTTP v1, called directly — no Expo, no firebase-admin.
//
// Ordinary HTTPS, unlike APNs, but two things differ from Apple in ways that matter:
//
// 1. One request per token. There is no multicast in v1 (`/batch` is deprecated), so N devices is N
//    requests — fine at this scale, and the same shape as APNs anyway.
// 2. FCM data values must all be STRINGS. A number or boolean in `message.data` is rejected outright
//    with an unhelpful INVALID_ARGUMENT, so everything is stringified on the way out and the app parses
//    it back. This is the single most common way a first FCM integration fails.

const ENDPOINT = (projectId: string) => `https://fcm.googleapis.com/v1/projects/${projectId}/messages:send`;

/** Google's ways of saying "this token is dead" — the row goes immediately. */
const DEAD_TOKEN_CODES = new Set(['UNREGISTERED', 'INVALID_ARGUMENT', 'NOT_FOUND']);

const raw = process.env.FCM_SERVICE_ACCOUNT;
const auth = raw ? new FcmAccessToken(parseServiceAccount(raw)) : null;

type FcmMessage = {
  message: {
    token: string;
    notification: { title: string; body?: string };
    data: Record<string, string>;
    android: {
      priority: 'high';
      notification: {
        /** Groups banners the way APNs' thread-id does. */
        tag: string;
        /** Replaces an existing banner with the same tag rather than stacking. */
        notification_priority: 'PRIORITY_HIGH';
      };
    };
  };
};

function buildMessage(token: string, n: Notification): FcmMessage {
  const body = renderBody(n);

  // Every value a string — see the note at the top. The app parses `id` and `ok` back.
  const data: Record<string, string> = { type: n.type };
  if (n.id !== undefined) data.id = String(n.id);
  if (n.ok !== undefined) data.ok = String(n.ok);
  if (n.count !== undefined) data.count = String(n.count);

  return {
    message: {
      token,
      notification: body ? { title: renderTitle(n), body } : { title: renderTitle(n) },
      data,
      android: {
        priority: 'high',
        notification: { tag: n.type, notification_priority: 'PRIORITY_HIGH' },
      },
    },
  };
}

type SendOutcome = { ok: boolean; dead: boolean; reason?: string };

async function sendOne(accessToken: string, projectId: string, token: string, n: Notification): Promise<SendOutcome> {
  try {
    const res = await fetch(ENDPOINT(projectId), {
      method: 'POST',
      headers: { authorization: `Bearer ${accessToken}`, 'content-type': 'application/json' },
      body: JSON.stringify(buildMessage(token, n)),
      signal: AbortSignal.timeout(10_000),
    });

    if (res.ok) return { ok: true, dead: false };

    // FCM errors look like { error: { status: 'UNREGISTERED', message: '…' } }. The message can echo the
    // token, so only the status is logged.
    let status = `status ${res.status}`;
    try {
      const body = (await res.json()) as { error?: { status?: string } };
      status = body.error?.status ?? status;
    } catch {
      /* keep the http status */
    }
    return { ok: false, dead: DEAD_TOKEN_CODES.has(status), reason: status };
  } catch (err) {
    return { ok: false, dead: false, reason: err instanceof Error ? err.message : 'send failed' };
  }
}

export const fcmChannel: Channel = {
  name: 'fcm',

  isConfigured: () => auth !== null,

  async send(n: Notification): Promise<DeliveryResult> {
    if (!auth) return { channel: 'fcm', sent: 0, failed: 0 };

    const devices = (await getPushDevices(n.userId, n.appSlug)).filter((d) => d.platform === 'android');
    if (devices.length === 0) return { channel: 'fcm', sent: 0, failed: 0 };

    let accessToken: string;
    try {
      accessToken = await auth.get();
    } catch (err) {
      // A failed token exchange fails every device equally; no point attempting the sends.
      console.error('[notify:fcm]', err instanceof Error ? err.message : err);
      return { channel: 'fcm', sent: 0, failed: devices.length };
    }

    const dead: string[] = [];
    let sent = 0;
    let failed = 0;

    await Promise.all(
      devices.map(async (d) => {
        const outcome = await sendOne(accessToken, auth.projectId, d.token, n);

        if (outcome.ok) {
          sent++;
          await markPushDeviceSeen(d.token);
          return;
        }

        failed++;
        console.error(`[notify:fcm] ${d.bundleId}: ${outcome.reason}`);

        if (outcome.dead) {
          dead.push(d.token);
          await deletePushDevice(d.token);
        } else {
          await recordPushFailure(d.token);
        }
      }),
    );

    return { channel: 'fcm', sent, failed, ...(dead.length ? { dead } : {}) };
  },
};

/** Exposed for tests — the wire shape is where FCM integrations usually go wrong. */
export const __testing = { buildMessage };
