import { get, writable } from 'svelte/store';
import { mediaUrl } from './api';
import {
  arrivalIn,
  type Arrival,
  badgeTotal,
  isRoomWatched,
  notifyingCount,
  planNotification,
  roomTag,
  sourceFromSummary,
  sourceFromTimeline,
  type NotificationPlan
} from './notification-model';
import { effectiveRooms } from './mute';
import { PUSH_CHANGED_MESSAGE } from './push-model';
import { disablePush, enablePush, pushActive, syncPush } from './push';
import { selectedRoomId, sessionUser, timelines } from './store';
import type { RoomSummary } from './types';

const SETTLE_MS = 300;

export const OPEN_ROOM_MESSAGE = 'malice-open-room';

export type NotificationPermissionState = NotificationPermission | 'unsupported';

const supported = typeof Notification !== 'undefined';
const preferenceKey = (account: string) => `malice.notifications.${account}`;

export const notificationsEnabled = writable(false);
export const notificationPermission = writable<NotificationPermissionState>(supported ? Notification.permission : 'unsupported');

function readPreference(account: string | null | undefined): boolean {
  if (!account) return false;
  try {
    return localStorage.getItem(preferenceKey(account)) === '1';
  } catch {
    return false;
  }
}

function writePreference(account: string, enabled: boolean): void {
  try {
    localStorage.setItem(preferenceKey(account), enabled ? '1' : '0');
  } catch {}
}

/** Must run inside a user gesture, since browsers refuse the permission prompt otherwise. */
export async function enableNotifications(): Promise<void> {
  const account = get(sessionUser);
  if (!supported || !account) return;
  const result = Notification.permission === 'default' ? await Notification.requestPermission() : Notification.permission;
  notificationPermission.set(result);
  const on = result === 'granted';
  writePreference(account, on);
  notificationsEnabled.set(on);
  if (on) await enablePush(account);
}

export function disableNotifications(): void {
  const account = get(sessionUser);
  if (account) {
    writePreference(account, false);
    void disablePush(account);
  }
  notificationsEnabled.set(false);
}

function active(): boolean {
  return supported && get(notificationsEnabled) && Notification.permission === 'granted';
}

async function registration(): Promise<ServiceWorkerRegistration | null> {
  if (!('serviceWorker' in navigator) || !navigator.serviceWorker.controller) return null;
  try {
    return await navigator.serviceWorker.ready;
  } catch {
    return null;
  }
}

async function show(plan: NotificationPlan, openRoom: (roomId: string) => void): Promise<void> {
  const icon = mediaUrl(plan.iconMxc, 128) ?? undefined;
  const options: NotificationOptions & { renotify?: boolean } = {
    body: plan.body,
    tag: plan.tag,
    icon,
    renotify: plan.renotify,
    data: { roomId: plan.roomId }
  };
  const reg = await registration();
  if (reg) {
    await reg.showNotification(plan.title, options);
    return;
  }
  const note = new Notification(plan.title, options);
  note.onclick = () => {
    window.focus();
    openRoom(plan.roomId);
    note.close();
  };
}

async function closeRoom(roomId: string): Promise<void> {
  const reg = await registration();
  if (!reg) return;
  for (const note of await reg.getNotifications({ tag: roomTag(roomId) })) note.close();
}

function setBadge(total: number): void {
  const nav = navigator as Navigator & { setAppBadge?: (n: number) => Promise<void>; clearAppBadge?: () => Promise<void> };
  if (total > 0) void nav.setAppBadge?.(total).catch(() => undefined);
  else void nav.clearAppBadge?.().catch(() => undefined);
}

/** Watches room summaries for notifying unread increases, shows and clears notifications, and keeps the app badge current. */
export function startNotifications(openRoom: (roomId: string) => void): () => void {
  let previous = new Map<string, RoomSummary>();
  let notified = new Set<string>();
  let badge = -1;

  const unsubscribeSession = sessionUser.subscribe((account) => {
    const enabled = readPreference(account);
    notificationsEnabled.set(enabled);
    if (account) void syncPush(account, enabled);
  });

  void registration().then(async (reg) => {
    for (const note of (await reg?.getNotifications()) ?? []) {
      const roomId = (note.data as { roomId?: unknown } | null)?.roomId;
      if (typeof roomId === 'string') notified.add(roomId);
    }
  });

  const watched = (roomId: string) =>
    isRoomWatched(roomId, get(selectedRoomId), document.visibilityState === 'visible', document.hasFocus());

  const clearRead = (list: RoomSummary[]) => {
    for (const room of list) {
      if (notified.has(room.room_id) && (notifyingCount(room) === 0 || watched(room.room_id))) {
        notified.delete(room.room_id);
        void closeRoom(room.room_id);
      }
    }
  };

  const waiting = new Map<string, Arrival & { timer: ReturnType<typeof setTimeout> }>();

  const queue = (roomId: string, mention: boolean) => {
    clearTimeout(waiting.get(roomId)?.timer);
    const timer = setTimeout(() => {
      waiting.delete(roomId);
      const room = get(effectiveRooms).find((r) => r.room_id === roomId);
      if (!room || !active() || watched(roomId) || notifyingCount(room) === 0) return;
      if (get(pushActive)) return;
      const source = sourceFromTimeline(get(timelines)[roomId]) ?? sourceFromSummary(room);
      notified.add(roomId);
      void show(planNotification(room, source, { mention }), openRoom);
    }, SETTLE_MS);
    waiting.set(roomId, { mention, timer });
  };

  const onRooms = (list: RoomSummary[]) => {
    const next = new Map(list.map((room) => [room.room_id, room]));
    const prior = previous;
    previous = next;
    if (!active()) {
      if (badge !== 0) setBadge((badge = 0));
      return;
    }
    const total = badgeTotal(list);
    if (total !== badge) setBadge((badge = total));
    const now = Date.now();
    for (const room of list) {
      const arrival = arrivalIn(prior.get(room.room_id), room, now);
      if (!arrival || watched(room.room_id)) continue;
      queue(room.room_id, arrival.mention || (waiting.get(room.room_id)?.mention ?? false));
    }
    clearRead(list);
  };

  const unsubscribeRooms = effectiveRooms.subscribe(onRooms);
  const unsubscribeEnabled = notificationsEnabled.subscribe(() => onRooms(get(effectiveRooms)));
  const onAttention = () => {
    notificationPermission.set(supported ? Notification.permission : 'unsupported');
    if (supported && Notification.permission !== 'granted') pushActive.set(false);
    clearRead(get(effectiveRooms));
    const open = get(selectedRoomId);
    if (open && get(pushActive) && watched(open)) void closeRoom(open);
  };
  const onMessage = (ev: MessageEvent) => {
    const data = ev.data as { type?: unknown; roomId?: unknown } | null;
    const account = get(sessionUser);
    if (data?.type === PUSH_CHANGED_MESSAGE && account) void syncPush(account, get(notificationsEnabled));
    if (data?.type === OPEN_ROOM_MESSAGE && typeof data.roomId === 'string') openRoom(data.roomId);
  };
  window.addEventListener('focus', onAttention);
  document.addEventListener('visibilitychange', onAttention);
  const unsubscribeSelected = selectedRoomId.subscribe(onAttention);
  if ('serviceWorker' in navigator) navigator.serviceWorker.addEventListener('message', onMessage);

  return () => {
    unsubscribeSession();
    unsubscribeRooms();
    unsubscribeEnabled();
    unsubscribeSelected();
    for (const { timer } of waiting.values()) clearTimeout(timer);
    window.removeEventListener('focus', onAttention);
    document.removeEventListener('visibilitychange', onAttention);
    if ('serviceWorker' in navigator) navigator.serviceWorker.removeEventListener('message', onMessage);
  };
}
