import { PUSH_CHANGED_MESSAGE, PUSH_CHANNEL, parsePush, parseResolved, shouldSilence, subscriptionKeys, type PushDisplay } from './lib/push-model';

const RESOLVE_TIMEOUT_MS = 3000;

declare const __MALICE_DEVICE__: boolean;

interface BadgeNavigator {
  setAppBadge?: (n: number) => Promise<void>;
  clearAppBadge?: () => Promise<void>;
}

let channel: BroadcastChannel | undefined;
let nextRequestId = 0;
const pending = new Map<string, (reply: { json?: unknown; error?: unknown }) => void>();

function resolveChannel(): BroadcastChannel {
  if (!channel) {
    channel = new BroadcastChannel(PUSH_CHANNEL);
    channel.onmessage = (ev: MessageEvent) => {
      const msg = ev.data as { id?: unknown; json?: unknown; error?: unknown } | null;
      if (typeof msg?.id !== 'string') return;
      const done = pending.get(msg.id);
      if (!done) return;
      pending.delete(msg.id);
      done(msg);
    };
  }
  return channel;
}

function requestResolve(roomId: string, eventId: string): Promise<unknown | null> {
  const id = `${Date.now()}-${nextRequestId++}`;
  return new Promise((resolve) => {
    const timer = setTimeout(() => {
      pending.delete(id);
      resolve(null);
    }, RESOLVE_TIMEOUT_MS);
    pending.set(id, (reply) => {
      clearTimeout(timer);
      resolve(reply.error ? null : (reply.json ?? null));
    });
    resolveChannel().postMessage({ id, kind: 'resolve', room_id: roomId, event_id: eventId });
  });
}

function show(sw: ServiceWorkerGlobalScope, display: PushDisplay, title: string, body: string, silent: boolean): Promise<void> {
  const options: NotificationOptions & { renotify?: boolean } = {
    body,
    tag: display.tag,
    silent,
    renotify: display.mention && !silent,
    data: { roomId: display.roomId, navigate: display.navigate, eventId: display.eventId }
  };
  return sw.registration.showNotification(title, options);
}

function setBadge(sw: ServiceWorkerGlobalScope, count: number | null): void {
  if (count === null) return;
  const nav = sw.navigator as unknown as BadgeNavigator;
  const call = count > 0 ? nav.setAppBadge?.(count) : nav.clearAppBadge?.();
  void call?.catch(() => undefined);
}

async function handle(sw: ServiceWorkerGlobalScope, event: PushEvent): Promise<void> {
  const display = parsePush(event.data?.text() ?? null, sw.location.origin);
  const windows = await sw.clients.matchAll({ type: 'window', includeUncontrolled: true });
  const focused = windows.some((client) => client.focused && client.visibilityState === 'visible');
  await show(sw, display, display.title, display.body, shouldSilence(display, focused));
  setBadge(sw, display.badge);
  if (!__MALICE_DEVICE__ || windows.length === 0 || !display.roomId || !display.eventId) return;
  const resolved = parseResolved(await requestResolve(display.roomId, display.eventId), display);
  if (resolved) await show(sw, display, resolved.title, resolved.body, true);
}

/** Browsers revoke the subscription when a push shows nothing, so every failure still shows a generic notification. */
export function onPush(sw: ServiceWorkerGlobalScope, event: PushEvent): void {
  event.waitUntil(
    handle(sw, event).catch(() =>
      sw.registration.showNotification('New message', { tag: 'malice', data: {} }).catch(() => undefined)
    )
  );
}

export function onSubscriptionChange(sw: ServiceWorkerGlobalScope, event: Event): void {
  const change = event as Event & { oldSubscription?: PushSubscription | null; newSubscription?: PushSubscription | null };
  (event as ExtendableEvent).waitUntil(
    (async () => {
      let sub = change.newSubscription ?? (await sw.registration.pushManager.getSubscription());
      const key = change.oldSubscription?.options.applicationServerKey;
      if (!sub && key) sub = await sw.registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key });
      const keys = sub ? subscriptionKeys(sub) : null;
      if (keys && !__MALICE_DEVICE__) {
        await fetch('/api/push/subscribe', {
          method: 'POST',
          credentials: 'same-origin',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify({ endpoint: keys.endpoint, keys: { p256dh: keys.p256dh, auth: keys.auth } })
        }).catch(() => undefined);
      }
      for (const client of await sw.clients.matchAll({ type: 'window', includeUncontrolled: true })) {
        client.postMessage({ type: PUSH_CHANGED_MESSAGE });
      }
    })().catch(() => undefined)
  );
}
