// Системные уведомления (ТЗ 3.21): подписка устройства на Web Push.
//
// Здесь только «браузерная» половина: разрешение, PushManager, регистрация
// подписки на сервере. Тексты уведомлений собирает сервер (у воркера нет
// локалей), а показ и подавление при видимой вкладке — src/sw.ts.
//
// Ключ VAPID приходит с сервера: без него PushManager.subscribe бросает
// исключение, и узнать об этом можно только здесь.
import { api, type PushConfig } from './api'

/** Состояние push на этом устройстве. */
export type PushState = 'unsupported' | 'off' | 'on' | 'denied' | 'unconfigured'

export function pushSupported(): boolean {
  // serviceWorker — только в secure context: по http (кроме localhost) его нет
  return 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window
}

function base64ToBytes(base64: string): Uint8Array {
  // applicationServerKey — сырая точка P-256, а не текст: base64url → байты
  const padded = base64.replace(/-/g, '+').replace(/_/g, '/')
  const raw = atob(padded.padEnd(padded.length + ((4 - (padded.length % 4)) % 4), '='))
  return Uint8Array.from(raw, (char) => char.charCodeAt(0))
}

async function registration(): Promise<ServiceWorkerRegistration> {
  // ready ждёт активации воркера: регистрация в dev может ещё идти
  return navigator.serviceWorker.ready
}

/** Есть ли уже подписка этого устройства на сервере — по endpoint из PushManager. */
async function hasLocalSubscription(): Promise<boolean> {
  const sub = await (await registration()).pushManager.getSubscription()
  return sub !== null
}

export async function pushState(): Promise<PushState> {
  if (!pushSupported()) return 'unsupported'
  const config = await api.getPushConfig()
  if (!config.enabled) return 'unconfigured'
  if (Notification.permission === 'denied') return 'denied'
  return (await hasLocalSubscription()) ? 'on' : 'off'
}

/**
 * Разрешение → подписка в браузере → регистрация на сервере (общая часть обоих входов).
 *
 * Если разрешение уже выдано, `requestPermission` возвращается сразу и ничего не
 * спрашивает; существующую подписку переиспользуем, поэтому повторный вызов —
 * не ошибка, а лечение: строка на сервере могла пропасть, а браузерная — нет.
 */
async function subscribe(config: PushConfig): Promise<PushState> {
  const permission = await Notification.requestPermission()
  if (permission !== 'granted') return permission === 'denied' ? 'denied' : 'off'

  const reg = await registration()
  const existing = await reg.pushManager.getSubscription()
  const sub =
    existing ??
    (await reg.pushManager.subscribe({
      // userVisibleOnly обязателен: Chrome не принимает «тихие» подписки
      userVisibleOnly: true,
      applicationServerKey: base64ToBytes(config.public_key || ''),
    }))

  const json = sub.toJSON()
  if (!json.endpoint || !json.keys?.p256dh || !json.keys.auth) return 'off'
  await api.subscribePush(json.endpoint, { p256dh: json.keys.p256dh, auth: json.keys.auth })
  return 'on'
}

/**
 * Включить уведомления (переключатель в Настройках).
 *
 * Возвращает итоговое состояние, а не бросает: отказ в разрешении — обычный
 * ответ пользователя, и интерфейс должен про него рассказать, а не упасть.
 */
export async function enablePush(): Promise<PushState> {
  if (!pushSupported()) return 'unsupported'
  const config = await api.getPushConfig()
  if (!config.enabled || !config.public_key) return 'unconfigured'
  return subscribe(config)
}

/** Выключить: отписка в браузере и снятие подписки на сервере. */
export async function disablePush(): Promise<PushState> {
  if (!pushSupported()) return 'unsupported'
  const reg = await registration()
  const sub = await reg.pushManager.getSubscription()
  if (sub) {
    const endpoint = sub.endpoint
    await sub.unsubscribe()
    // Сначала браузер, потом сервер: если сервер не ответит, мёртвая подписка
    // всё равно будет вычищена лениво при первой отправке (404/410)
    await api.unsubscribePush(endpoint).catch(() => undefined)
  }
  return 'off'
}

/**
 * Запрос разрешения при входе (ТЗ 3.21, п. 4).
 *
 * Браузеры требуют жеста пользователя, а возврат из SSO — это загрузка страницы
 * без клика: Safari и Firefox молча проигнорируют запрос, Chrome может ответить
 * «тихим» отказом. Поэтому состояние останется `default`, и надёжный путь —
 * переключатель в Настройках, где клик есть всегда.
 */
export async function ensurePushOnLogin(): Promise<void> {
  try {
    if (!pushSupported() || Notification.permission === 'denied') return
    const config = await api.getPushConfig()
    if (!config.enabled || !config.public_key) return
    await subscribe(config)
  } catch {
    // Push — не условие входа: молчание лучше, чем сломанная загрузка приложения
  }
}
