/* global self */
import { precacheAndRoute } from 'workbox-precaching'
import { offlineFallback } from 'workbox-recipes'
import { setDefaultHandler } from 'workbox-routing'
import { NetworkOnly } from 'workbox-strategies'
import { enable } from 'workbox-navigation-preload'
import manifest from './precache-manifest.json'
import ServiceWorkerStorage from 'serviceworker-storage'
import { numWithUnits } from '../lib/format'

// comment out to enable workbox console logs
self.__WB_DISABLE_DEV_LOGS = true

const storage = new ServiceWorkerStorage('sw:storage', 1)
let messageChannelPort

// preloading improves startup performance
// https://developer.chrome.com/docs/workbox/modules/workbox-navigation-preload/
enable()

// ignore precache manifest generated by InjectManifest
// they statically check for the presence of this variable
console.log(self.__WB_MANIFEST)

precacheAndRoute(manifest)

self.addEventListener('install', () => {
  self.skipWaiting()
})

// Using network-only as the default strategy ensures that we fallback
// to the browser as if the service worker wouldn't exist.
// The browser may use own caching (HTTP cache).
// Also, the offline fallback only works if request matched a route
setDefaultHandler(new NetworkOnly({
  plugins: [{
    fetchDidFail: async (args) => {
      //  tell us why a request failed in dev
      process.env.NODE_ENV !== 'production' && console.log('fetch did fail', ...args)
    },
    fetchDidSucceed: async ({ request, response, event, state }) => {
      if (
        response.ok &&
        request.headers.get('x-nextjs-data') &&
        response.headers.get('x-nextjs-matched-path') &&
        response.headers.get('content-type') === 'application/json' &&
        response.headers.get('content-length') === '2' &&
        response.status === 200) {
        console.log('service worker detected a successful yet empty nextjs SSR data response')
        console.log('nextjs has a bug where it returns a 200 with empty data when it should return a 404')
        console.log('see https://github.com/vercel/next.js/issues/56852')
        console.log('HACK ... intercepting response and returning 404')

        const headers = new Headers(response.headers)
        headers.delete('x-nextjs-matched-path')
        headers.delete('content-type')
        headers.delete('content-length')
        return new Response(null, {
          status: 404,
          statusText: 'Not Found',
          headers,
          ok: false
        })
      }
      return response
    }
  }]
}))

// This won't work in dev because pages are never cached.
// See https://github.com/vercel/next.js/blob/337fb6a9aadb61c916f0121c899e463819cd3f33/server/render.js#L181-L185
offlineFallback({ pageFallback: '/offline' })

self.addEventListener('push', async function (event) {
  const payload = event.data?.json()
  if (!payload) return
  const { tag } = payload.options
  event.waitUntil((async () => {
    // TIP and EARN notifications simply replace the previous notifications
    if (!tag || ['TIP', 'EARN'].includes(tag.split('-')[0])) {
      return self.registration.showNotification(payload.title, payload.options)
    }

    const notifications = await self.registration.getNotifications({ tag })
    // since we used a tag filter, there should only be zero or one notification
    if (notifications.length > 1) {
      const message = `[sw:push] more than one notification with tag ${tag} found`
      messageChannelPort?.postMessage({ level: 'error', message })
      console.error(message)
      return null
    }
    if (notifications.length === 0) {
      return self.registration.showNotification(payload.title, payload.options)
    }
    const currentNotification = notifications[0]
    const amount = currentNotification.data?.amount ? currentNotification.data.amount + 1 : 2
    let newTitle = ''
    const data = {}
    if (tag === 'REPLY') {
      newTitle = `You have ${amount} new replies`
    } else if (tag === 'MENTION') {
      newTitle = `You were mentioned ${amount} times`
    } else if (tag === 'REFERRAL') {
      newTitle = `${amount} stackers joined via your referral links`
    } else if (tag === 'INVITE') {
      newTitle = `your invite has been redeemed by ${amount} stackers`
    } else if (tag === 'DEPOSIT') {
      const currentSats = currentNotification.data.sats
      const incomingSats = payload.options.data.sats
      const newSats = currentSats + incomingSats
      data.sats = newSats
      newTitle = `${numWithUnits(newSats, { abbreviate: false })} were deposited in your account`
    }
    currentNotification.close()
    const { icon } = currentNotification
    return self.registration.showNotification(newTitle, { icon, tag, data: { url: '/notifications', amount, ...data } })
  })())
})

self.addEventListener('notificationclick', (event) => {
  const url = event.notification.data?.url
  if (url) {
    event.waitUntil(self.clients.openWindow(url))
  }
  event.notification.close()
})

// https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f
self.addEventListener('message', (event) => {
  if (event.data.action === 'MESSAGE_PORT') {
    messageChannelPort = event.ports[0]
  }
  messageChannelPort?.postMessage({ message: '[sw:message] received message', context: { action: event.data.action } })
  if (event.data.action === 'STORE_SUBSCRIPTION') {
    messageChannelPort?.postMessage({ message: '[sw:message] storing subscription in IndexedDB', context: { endpoint: event.data.subscription.endpoint } })
    return event.waitUntil(storage.setItem('subscription', event.data.subscription))
  }
  if (event.data.action === 'SYNC_SUBSCRIPTION') {
    return event.waitUntil(handlePushSubscriptionChange())
  }
})

async function handlePushSubscriptionChange (oldSubscription, newSubscription) {
  // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/pushsubscriptionchange_event
  // fallbacks since browser may not set oldSubscription and newSubscription
  messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] invoked' })
  oldSubscription ??= await storage.getItem('subscription')
  newSubscription ??= await self.registration.pushManager.getSubscription()
  if (!newSubscription) {
    // no subscription exists at the moment
    messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] no existing subscription found' })
    return
  }
  if (oldSubscription?.endpoint === newSubscription.endpoint) {
    // subscription did not change. no need to sync with server
    messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] old subscription matches existing subscription' })
    return
  }
  // convert keys from ArrayBuffer to string
  newSubscription = JSON.parse(JSON.stringify(newSubscription))
  const variables = {
    endpoint: newSubscription.endpoint,
    p256dh: newSubscription.keys.p256dh,
    auth: newSubscription.keys.auth,
    oldEndpoint: oldSubscription?.endpoint
  }
  const query = `
  mutation savePushSubscription($endpoint: String!, $p256dh: String!, $auth: String!, $oldEndpoint: String!) {
    savePushSubscription(endpoint: $endpoint, p256dh: $p256dh, auth: $auth, oldEndpoint: $oldEndpoint) {
      id
    }
  }`
  const body = JSON.stringify({ query, variables })
  await fetch('/api/graphql', {
    method: 'POST',
    headers: {
      'Content-type': 'application/json'
    },
    body
  })
  messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] synced push subscription with server', context: { endpoint: variables.endpoint, oldEndpoint: variables.oldEndpoint } })
  await storage.setItem('subscription', JSON.parse(JSON.stringify(newSubscription)))
}

self.addEventListener('pushsubscriptionchange', (event) => {
  messageChannelPort?.postMessage({ message: '[sw:pushsubscriptionchange] received event' })
  event.waitUntil(handlePushSubscriptionChange(event.oldSubscription, event.newSubscription))
}, false)