import { writable } from 'svelte/store';
import { runCommand } from './api';
import {
  DEVICE_PUSH_APP_ID,
  devicePusherRequest,
  parseStoredPush,
  stalePushers,
  subscriptionKeys,
  syncAction,
  urlBase64ToBytes,
  type PushSubscriptionKeys,
  type StoredPush
} from './push-model';

/** True while this browser holds a subscription the server knows about, so in-app notifications stand down. */
export const pushActive = writable(false);

const supported = typeof window !== 'undefined' && 'serviceWorker' in navigator && 'PushManager' in window;
const storageKey = (account: string) => `malice.push.${account}`;
const browserKey = 'malice.push.browser';
const vapidPath = __MALICE_DEVICE__ ? '/push/vapid' : '/api/push/vapid';

let queue: Promise<unknown> = Promise.resolve();

/** Enable, disable and launch checks all touch one subscription, so they run one at a time. */
function serial<T>(task: () => Promise<T>): Promise<T> {
  const run = queue.then(task, task);
  queue = run.catch(() => undefined);
  return run;
}

function browserId(): string {
  try {
    const existing = localStorage.getItem(browserKey);
    if (existing) return existing;
    const id = crypto.randomUUID();
    localStorage.setItem(browserKey, id);
    return id;
  } catch {
    return 'unknown';
  }
}

function readStored(account: string): StoredPush | null {
  try {
    return parseStoredPush(localStorage.getItem(storageKey(account)));
  } catch {
    return null;
  }
}

function writeStored(account: string, stored: StoredPush | null): void {
  try {
    if (stored) localStorage.setItem(storageKey(account), JSON.stringify(stored));
    else localStorage.removeItem(storageKey(account));
  } catch {}
}

async function registration(): Promise<ServiceWorkerRegistration | null> {
  if (!supported) return null;
  return (await navigator.serviceWorker.getRegistration()) ?? null;
}

async function applicationServerKey(): Promise<Uint8Array<ArrayBuffer>> {
  const res = await fetch(vapidPath, { credentials: 'same-origin' });
  if (!res.ok) throw new Error('push key unavailable');
  const body: unknown = await res.json().catch(() => null);
  const key = typeof body === 'string' ? body : (body as { public_key?: unknown } | null)?.public_key;
  if (typeof key !== 'string') throw new Error('push key malformed');
  return urlBase64ToBytes(key);
}

async function post(path: string, body: unknown): Promise<void> {
  const res = await fetch(path, {
    method: 'POST',
    credentials: 'same-origin',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body)
  });
  if (!res.ok) throw new Error(`push request failed (${res.status})`);
}

async function removeRemote(stored: StoredPush): Promise<void> {
  if (__MALICE_DEVICE__) {
    if (stored.pushkey) await runCommand({ op: 'remove_pusher', app_id: DEVICE_PUSH_APP_ID, pushkey: stored.pushkey });
  } else {
    await post('/api/push/unsubscribe', { endpoint: stored.endpoint });
  }
}

async function register(account: string, keys: PushSubscriptionKeys): Promise<void> {
  const previous = readStored(account);
  if (__MALICE_DEVICE__) {
    await runCommand(devicePusherRequest(keys, window.location.origin, navigator.language, browserId()));
    if (previous?.pushkey && previous.pushkey !== keys.p256dh) {
      await runCommand({ op: 'remove_pusher', app_id: DEVICE_PUSH_APP_ID, pushkey: previous.pushkey }).catch(() => undefined);
    }
  } else {
    await post('/api/push/subscribe', { endpoint: keys.endpoint, keys: { p256dh: keys.p256dh, auth: keys.auth } });
  }
  writeStored(account, { endpoint: keys.endpoint, pushkey: keys.p256dh });
}

async function dropStale(endpoint: string): Promise<void> {
  const reply = await runCommand({ op: 'pushers' });
  for (const pushkey of stalePushers(reply, browserId(), endpoint)) {
    await runCommand({ op: 'remove_pusher', app_id: DEVICE_PUSH_APP_ID, pushkey });
  }
}

async function subscribe(reg: ServiceWorkerRegistration): Promise<PushSubscription> {
  return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: await applicationServerKey() });
}

async function activate(account: string, sub: PushSubscription): Promise<void> {
  const keys = subscriptionKeys(sub);
  if (!keys) throw new Error('subscription has no keys');
  await register(account, keys);
}

/** Runs inside the settings toggle after permission is granted. */
export function enablePush(account: string): Promise<void> {
  return serial(async () => {
    try {
      const reg = await registration();
      if (!reg) return;
      await activate(account, (await reg.pushManager.getSubscription()) ?? (await subscribe(reg)));
      pushActive.set(true);
    } catch {
      pushActive.set(false);
    }
  });
}

/** Removes the server side registration and the browser subscription. Used by the toggle and before logout. */
export function disablePush(account: string): Promise<void> {
  return serial(async () => {
    pushActive.set(false);
    const stored = readStored(account);
    if (stored) await removeRemote(stored).catch(() => undefined);
    writeStored(account, null);
    const sub = await (await registration())?.pushManager.getSubscription();
    await sub?.unsubscribe().catch(() => false);
  });
}

/** Launch check. iOS revokes subscriptions after missed notifications and push services rotate endpoints. */
export function syncPush(account: string, wanted: boolean): Promise<void> {
  return serial(async () => {
    try {
      const reg = supported && Notification.permission === 'granted' ? await registration() : null;
      if (!reg) {
        pushActive.set(false);
        return;
      }
      let sub = await reg.pushManager.getSubscription();
      const action = syncAction(wanted, sub?.endpoint ?? null, readStored(account));
      if (action === 'subscribe') sub = await subscribe(reg);
      if (action !== 'none' && sub) await activate(account, sub);
      pushActive.set(wanted && sub !== null);
      if (__MALICE_DEVICE__ && wanted && sub) await dropStale(sub.endpoint).catch(() => undefined);
    } catch {
      pushActive.set(false);
    }
  });
}
