export const DEVICE_PUSH_APP_ID = 'dev.malice.device';
export const PUSH_CHANNEL = 'malice-push';
export const PUSH_CHANGED_MESSAGE = 'malice-push-changed';
export const PUSH_NOTIFY_PATH = '/_matrix/push/v1/notify';

export interface PushDisplay {
  title: string;
  body: string;
  tag: string;
  navigate: string | null;
  silent: boolean;
  badge: number | null;
  roomId: string | null;
  eventId: string | null;
  mention: boolean;
  encrypted: boolean;
}

export interface ResolvedPush {
  title: string;
  body: string;
}

export interface PushSubscriptionKeys {
  endpoint: string;
  p256dh: string;
  auth: string;
}

const FALLBACK_TITLE = 'New message';
const FALLBACK_TAG = 'malice';

function record(value: unknown): Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
}

function text(value: unknown): string | null {
  return typeof value === 'string' && value.length > 0 ? value : null;
}

function count(value: unknown): number | null {
  const n = typeof value === 'string' ? Number(value) : value;
  return typeof n === 'number' && Number.isInteger(n) && n >= 0 ? n : null;
}

function sameOrigin(url: string | null, origin: string): string | null {
  if (!url) return null;
  try {
    const parsed = new URL(url, origin);
    return parsed.origin === origin ? parsed.href : null;
  } catch {
    return null;
  }
}

export function roomFromTag(tag: string | null | undefined): string | null {
  return tag?.startsWith('room:') && tag.length > 5 ? tag.slice(5) : null;
}

/** Never throws, so the push handler can always show something. */
export function parsePush(raw: unknown, origin: string): PushDisplay {
  let value = raw;
  if (typeof raw === 'string') {
    try {
      value = JSON.parse(raw);
    } catch {
      value = null;
    }
  }
  const payload = record(value);
  const note = record(payload['notification']);
  const malice = record(payload['malice']);
  const roomId = text(malice['room_id']);
  const tag = text(note['tag']) ?? (roomId ? `room:${roomId}` : FALLBACK_TAG);
  const badge = count(note['app_badge']) ?? count(malice['unread']);
  return {
    title: text(note['title']) ?? FALLBACK_TITLE,
    body: text(note['body']) ?? '',
    tag,
    navigate: sameOrigin(text(note['navigate']), origin),
    silent: note['silent'] === true,
    badge,
    roomId: roomId ?? roomFromTag(tag),
    eventId: text(malice['event_id']),
    mention: malice['mention'] === true,
    encrypted: malice['encrypted'] === true
  };
}

export function roomTarget(roomId: string | null, navigate: string | null): string {
  if (navigate) return navigate;
  return roomId ? `/?room=${encodeURIComponent(roomId)}` : '/';
}

/** A visible focused window is already showing the app, so the banner stays but without sound or vibration. */
export function shouldSilence(display: PushDisplay, windowFocused: boolean): boolean {
  return display.silent || windowFocused;
}

/** The decrypted reply replaces the tier 1 notification only when it adds something. */
export function parseResolved(value: unknown, current: PushDisplay): ResolvedPush | null {
  const reply = record(value);
  const title = text(reply['title']);
  const body = text(reply['body']);
  if (!title && !body) return null;
  const next = { title: title ?? current.title, body: body ?? current.body };
  return next.title === current.title && next.body === current.body ? null : next;
}

export function urlBase64ToBytes(value: string): Uint8Array<ArrayBuffer> {
  const padded = value.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(value.length / 4) * 4, '=');
  const raw = atob(padded);
  const bytes = new Uint8Array(raw.length);
  for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
  return bytes;
}

export function bytesToUrlBase64(bytes: Uint8Array): string {
  let raw = '';
  for (const byte of bytes) raw += String.fromCharCode(byte);
  return btoa(raw).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

export function subscriptionKeys(sub: { endpoint: string; getKey(name: 'p256dh' | 'auth'): ArrayBuffer | null }): PushSubscriptionKeys | null {
  const p256dh = sub.getKey('p256dh');
  const auth = sub.getKey('auth');
  if (!p256dh || !auth) return null;
  return { endpoint: sub.endpoint, p256dh: bytesToUrlBase64(new Uint8Array(p256dh)), auth: bytesToUrlBase64(new Uint8Array(auth)) };
}

export interface StoredPush {
  endpoint: string;
  pushkey: string | null;
}

export type SyncAction = 'none' | 'subscribe' | 'register';

/** What a launch check must repair, given whether the user wants push, the live subscription and what was last registered. */
export function syncAction(wanted: boolean, live: string | null, stored: StoredPush | null): SyncAction {
  if (!wanted) return 'none';
  if (!live) return 'subscribe';
  return stored?.endpoint === live ? 'none' : 'register';
}

export function parseStoredPush(raw: string | null): StoredPush | null {
  if (!raw) return null;
  try {
    const value = record(JSON.parse(raw));
    const endpoint = text(value['endpoint']);
    return endpoint ? { endpoint, pushkey: text(value['pushkey']) } : null;
  } catch {
    return null;
  }
}

export function devicePusherRequest(keys: PushSubscriptionKeys, origin: string, lang: string, browser: string) {
  return {
    op: 'set_pusher' as const,
    app_id: DEVICE_PUSH_APP_ID,
    pushkey: keys.p256dh,
    app_display_name: 'Malice',
    device_display_name: 'Malice web',
    lang,
    url: `${origin}${PUSH_NOTIFY_PATH}`,
    format: 'Full' as const,
    data: { endpoint: keys.endpoint, auth: keys.auth, browser }
  };
}

/** Pushers this browser registered under an endpoint it no longer holds. Other browsers on the account share the app id, so only its own id matches. */
export function stalePushers(reply: unknown, browser: string, endpoint: string): string[] {
  const list = record(reply)['pushers'];
  if (!Array.isArray(list)) return [];
  const stale: string[] = [];
  for (const entry of list) {
    const pusher = record(entry);
    const data = record(pusher['data']);
    const pushkey = text(pusher['pushkey']);
    if (pusher['app_id'] === DEVICE_PUSH_APP_ID && data['browser'] === browser && data['endpoint'] !== endpoint && pushkey) stale.push(pushkey);
  }
  return stale;
}
