/**
 * Push subscription manager.
 * Handles requesting notification permission, subscribing to push,
 * and syncing the subscription with the backend.
 */

export type PushSubscribeResult =
  | { ok: true }
  | {
      ok: false
      reason:
        | 'unsupported'
        | 'permission_denied'
        | 'no_vapid_key'
        | 'sw_error'
        | 'subscribe_error'
        | 'backend_error'
      detail?: string
    }

async function fetchVapidPublicKey(): Promise<string | null> {
  try {
    const res = await fetch('/api/push-vapid-key')
    if (!res.ok) return null
    const data = await res.json()
    return data.public_key || null
  } catch {
    return null
  }
}

function urlBase64ToUint8Array(base64String: string): Uint8Array {
  const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
  const rawData = window.atob(base64)
  return Uint8Array.from([...rawData].map((c) => c.charCodeAt(0)))
}

function uint8ArraysEqual(a: Uint8Array, b: Uint8Array): boolean {
  if (a.length !== b.length) return false
  for (let i = 0; i < a.length; i += 1) {
    if (a[i] !== b[i]) return false
  }
  return true
}

async function postSubscriptionToBackend(subscription: PushSubscription): Promise<boolean> {
  const subJson = subscription.toJSON()
  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone

  const res = await fetch('/api/push-subscription', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      endpoint: subJson.endpoint,
      keys: subJson.keys,
      timezone,
    }),
  })

  return res.ok
}

/**
 * Request notification permission and subscribe to push notifications.
 * Posts the subscription to the backend, including the user's timezone.
 * Returns a result object describing success or the specific failure reason.
 */
export async function subscribeToPushWithResult(): Promise<PushSubscribeResult> {
  if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
    return { ok: false, reason: 'unsupported' }
  }

  const permission = await Notification.requestPermission()
  if (permission !== 'granted') {
    return { ok: false, reason: 'permission_denied' }
  }

  const vapidKey = await fetchVapidPublicKey()
  if (!vapidKey) {
    console.warn('[Push] VAPID public key not available')
    return { ok: false, reason: 'no_vapid_key' }
  }

  let registration: ServiceWorkerRegistration
  try {
    // Explicitly register the SW so we don't race against index.html's 'load' handler.
    // navigator.serviceWorker.register() is idempotent — it returns the existing
    // registration if one already exists and does not re-install the worker.
    await navigator.serviceWorker.register('/sw.js')

    // Now wait for it to become active. Use a generous timeout for slow mobile networks.
    registration = await Promise.race([
      navigator.serviceWorker.ready,
      new Promise<never>((_, reject) =>
        setTimeout(() => reject(new Error('SW ready timeout')), 10_000),
      ),
    ])
  } catch (err) {
    console.warn('[Push] Service worker not ready:', err)
    return { ok: false, reason: 'sw_error', detail: String(err) }
  }

  let subscription: PushSubscription
  try {
    subscription = await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(vapidKey),
    })
  } catch (firstErr) {
    // VAPID key mismatch: the browser has a stale subscription tied to a different key.
    // Clear it and retry once.
    console.warn(
      '[Push] pushManager.subscribe() failed — attempting to clear stale subscription:',
      firstErr,
    )
    try {
      const existing = await registration.pushManager.getSubscription()
      if (existing) {
        await existing.unsubscribe()
        console.info('[Push] Cleared stale subscription, retrying…')
      }
      subscription = await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(vapidKey),
      })
    } catch (retryErr) {
      console.warn('[Push] Retry failed:', retryErr)
      return { ok: false, reason: 'subscribe_error', detail: String(retryErr) }
    }
  }

  try {
    const ok = await postSubscriptionToBackend(subscription)
    if (!ok) {
      return { ok: false, reason: 'backend_error', detail: 'HTTP error posting subscription' }
    }
  } catch (fetchErr) {
    console.warn('[Push] Network error posting subscription:', fetchErr)
    return { ok: false, reason: 'backend_error', detail: String(fetchErr) }
  }

  return { ok: true }
}

/**
 * Silent self-heal path used on app startup/login.
 * - Never prompts for permission.
 * - If already subscribed with a stale VAPID key, rotates the browser subscription.
 * - Ensures backend has the current endpoint/keys.
 */
export async function ensurePushSubscriptionSynced(): Promise<boolean> {
  if (!('serviceWorker' in navigator) || !('PushManager' in window)) return false
  if (Notification.permission !== 'granted') return false

  const vapidKey = await fetchVapidPublicKey()
  if (!vapidKey) return false

  let registration: ServiceWorkerRegistration
  try {
    await navigator.serviceWorker.register('/sw.js')
    registration = await Promise.race([
      navigator.serviceWorker.ready,
      new Promise<never>((_, reject) =>
        setTimeout(() => reject(new Error('SW ready timeout')), 10_000),
      ),
    ])
  } catch (err) {
    console.warn('[Push] Service worker not ready for self-heal:', err)
    return false
  }

  const desiredServerKey = urlBase64ToUint8Array(vapidKey)
  let subscription = await registration.pushManager.getSubscription()

  if (subscription) {
    const existingServerKeyBuffer = subscription.options?.applicationServerKey
    if (existingServerKeyBuffer) {
      const existingServerKey = new Uint8Array(existingServerKeyBuffer)
      const keyMatches = uint8ArraysEqual(existingServerKey, desiredServerKey)
      if (!keyMatches) {
        await subscription.unsubscribe()
        subscription = null
      }
    }
  }

  if (!subscription) {
    try {
      subscription = await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: desiredServerKey,
      })
    } catch (err) {
      console.warn('[Push] Failed to create subscription in self-heal:', err)
      return false
    }
  }

  try {
    return await postSubscriptionToBackend(subscription)
  } catch (err) {
    console.warn('[Push] Failed to sync subscription in self-heal:', err)
    return false
  }
}

/**
 * Convenience wrapper — returns boolean for callers that don't need the reason.
 */
export async function subscribeToPush(): Promise<boolean> {
  const result = await subscribeToPushWithResult()
  return result.ok
}

/**
 * Unsubscribe from push notifications and remove the subscription from the backend.
 */
export async function unsubscribeFromPush(): Promise<void> {
  if (!('serviceWorker' in navigator) || !('PushManager' in window)) return

  try {
    const registration = await navigator.serviceWorker.ready
    const subscription = await registration.pushManager.getSubscription()
    if (!subscription) return

    const endpoint = subscription.endpoint
    await subscription.unsubscribe()

    await fetch('/api/push-subscription', {
      method: 'DELETE',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ endpoint }),
    })
  } catch (err) {
    console.warn('[Push] Failed to unsubscribe:', err)
  }
}

/**
 * Check the current push permission state without prompting.
 */
export function getPushPermissionState(): NotificationPermission | 'unsupported' {
  if (!('Notification' in window)) return 'unsupported'
  return Notification.permission
}

/**
 * Returns true if the browser currently has an active push subscription.
 */
export async function isSubscribedToPush(): Promise<boolean> {
  if (!('serviceWorker' in navigator) || !('PushManager' in window)) return false
  try {
    const registration = await Promise.race([
      navigator.serviceWorker.ready,
      new Promise<never>((_, reject) =>
        setTimeout(() => reject(new Error('SW ready timeout')), 3_000),
      ),
    ])
    const subscription = await registration.pushManager.getSubscription()
    return subscription !== null
  } catch {
    return false
  }
}

/** localStorage key used to persist the user's explicit push opt-out across logins. */
const PUSH_OPT_OUT_KEY = 'push_opt_out'

/**
 * Persist or clear the user's push opt-out preference in localStorage.
 * Call with `true` when the user disables push, `false` when they enable it.
 */
export function setPushOptOut(optOut: boolean): void {
  if (optOut) {
    localStorage.setItem(PUSH_OPT_OUT_KEY, '1')
  } else {
    localStorage.removeItem(PUSH_OPT_OUT_KEY)
  }
}

/**
 * Returns true if the user has explicitly opted out of push notifications.
 */
export function isPushOptedOut(): boolean {
  return localStorage.getItem(PUSH_OPT_OUT_KEY) === '1'
}
