/* eslint-env serviceworker */
/// <reference lib="webworker" />
/// <reference types="vite/client" />
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'
import { NavigationRoute, registerRoute } from 'workbox-routing'
import { NetworkFirst, CacheFirst, StaleWhileRevalidate } from 'workbox-strategies'
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
import { ExpirationPlugin } from 'workbox-expiration'
import { BackgroundSyncPlugin } from 'workbox-background-sync'

declare const self: ServiceWorkerGlobalScope
// Only apply service worker logic if on the app subdomain
if (!self.location.hostname.startsWith('app.')) {
  console.log('Skipping SW install: not on app domain');
  self.skipWaiting();
  self.registration?.unregister();
}

// Define cache names with domain awareness
const CACHE_NAMES = {
  static: `static-assets-v1-${self.location.hostname}`,
  dynamic: `dynamic-content-v1-${self.location.hostname}`,
  pages: `pages-v1-${self.location.hostname}`,
  api: `api-v1-${self.location.hostname}`,
  offline: `offline-v1-${self.location.hostname}`
}

// Clean up old caches
cleanupOutdatedCaches()

// Precache production assets only
// Development resources are excluded by Vite's build configuration
const manifest = self.__WB_MANIFEST;

// Find the index.html entry from the manifest
const manifestIndexEntry = manifest.find(entry => {
  const url = typeof entry === 'string' ? entry : entry.url;
  return url === 'index.html' || url === '/index.html';
});

// Create index entries using the revision from manifest if available
const indexEntries = [
  {
    url: '/index.html',
    revision: manifestIndexEntry && typeof manifestIndexEntry !== 'string' ? manifestIndexEntry.revision : null
  },
  {
    url: '/',
    revision: manifestIndexEntry && typeof manifestIndexEntry !== 'string' ? manifestIndexEntry.revision : null
  }
];

// Filter out index.html from manifest and add absolute paths
const manifestWithoutIndex = manifest.filter(entry => {
  const url = typeof entry === 'string' ? entry : entry.url;
  return url !== 'index.html' && url !== '/index.html';
});

// Combine entries and ensure absolute paths
const manifestWithIndex = [
  ...indexEntries,
  ...manifestWithoutIndex.map(entry => {
    if (typeof entry === 'string') {
      return { url: entry.startsWith('/') ? entry : `/${entry}`, revision: null };
    }
    return {
      ...entry,
      url: entry.url.startsWith('/') ? entry.url : `/${entry.url}`
    };
  })
];

// Precache all assets
precacheAndRoute(manifestWithIndex);

// Create a background sync queue for failed requests
const bgSyncPlugin = new BackgroundSyncPlugin('failedRequests', {
  maxRetentionTime: 24 * 60 // Retry for up to 24 hours (specified in minutes)
})

// Navigation handler for offline usage
const navigationHandler = async ({ request }: { request: Request }): Promise<Response> => {
  try {
    // Try network first
    const response = await fetch(request);
    if (response.ok) {
      return response;
    }
  } catch (error) {
    console.log('Navigation fetch failed:', error);
  }

  // If network fails, try cache
  const cache = await caches.open(CACHE_NAMES.pages);

  // Try both root and /index.html paths
  const cachedResponse = await cache.match(request) ||
    await cache.match('/') ||
    await cache.match('/index.html');

  if (cachedResponse) {
    return cachedResponse;
  }

  // If cache fails, try to fetch index.html directly
  try {
    const indexResponse = await fetch('/index.html');
    if (indexResponse.ok) {
      await cache.put('/index.html', indexResponse.clone());
      return indexResponse;
    }
  } catch (error) {
    console.error('Failed to fetch index.html:', error);
  }

  // If all else fails, return a basic offline page
  return new Response(
    '<html><body><h1>Offline</h1><p>Please check your internet connection.</p></body></html>',
    {
      headers: { 'Content-Type': 'text/html' }
    }
  );
};

// Register navigation route
const navigationRoute = new NavigationRoute(navigationHandler, {
  denylist: [
    /^\/(auth|rest|api|whisperlive|tldraw|searxng-api)/,
    /^\/@.*/,  // Block all /@vite/, /@react-refresh/, etc.
    /^\/src\/.*/  // Block all /src/ paths
  ]
});
registerRoute(navigationRoute);

// Cache page navigations (html) with a Network First strategy
registerRoute(
  // Check to see if the request is a navigation to a new page
  ({ request }) => request.mode === 'navigate',
  new NetworkFirst({
    // Put all cached files in a cache named 'pages'
    cacheName: CACHE_NAMES.pages,
    plugins: [
      // Ensure that only requests that result in a 200 status are cached
      new CacheableResponsePlugin({
        statuses: [200]
      })
    ],
    networkTimeoutSeconds: 3
  })
)

// Cache manifest and icons with a Stale While Revalidate strategy
registerRoute(
  ({ request }) =>
    request.destination === 'manifest' ||
    request.url.includes('/icons/'),
  new StaleWhileRevalidate({
    cacheName: 'manifest-and-icons',
    plugins: [
      new CacheableResponsePlugin({
        statuses: [200]
      })
    ]
  })
)

// Cache other static assets with a Cache First strategy
registerRoute(
  ({ request }) => {
    const destination = request.destination;
    return destination === 'style' ||
      destination === 'script' ||
      destination === 'image' ||
      destination === 'font'
  },
  new CacheFirst({
    cacheName: CACHE_NAMES.static,
    plugins: [
      new CacheableResponsePlugin({
        statuses: [200]
      }),
      new ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
        purgeOnQuotaError: true
      })
    ]
  })
)

// Cache API responses with a Network First strategy
// TODO: This is a temporary solution to cache API responses. We should move to a more efficient strategy in the future using our new domain names search.classroomcopilot.ai and api.classroomcopilot.ai
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: CACHE_NAMES.api,
    plugins: [
      new CacheableResponsePlugin({
        statuses: [200]
      }),
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 5 * 60 // 5 minutes
      }),
      bgSyncPlugin
    ]
  })
)

// Cache SearXNG API with Network First strategy
registerRoute(
  ({ url }) => url.pathname.startsWith('/searxng-api/'),
  new NetworkFirst({
    cacheName: CACHE_NAMES.api,
    plugins: [
      new CacheableResponsePlugin({
        statuses: [200]
      }),
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 60 * 60 // 1 hour
      }),
      bgSyncPlugin
    ]
  })
)

// Cache SearXNG static assets
registerRoute(
  ({ url }) => url.pathname.startsWith('/searxng-api/static/'),
  new CacheFirst({
    cacheName: CACHE_NAMES.static,
    plugins: [
      new CacheableResponsePlugin({
        statuses: [200]
      }),
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 30 * 24 * 60 * 60 // 30 days
      })
    ]
  })
)

// This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting()
  }
})

// Handle offline fallback
self.addEventListener('install', () => {
  self.skipWaiting()
})

self.addEventListener('activate', (event) => {
  event.waitUntil(
    Promise.all([
      // Enable navigation preload if available
      self.registration.navigationPreload?.enable(),
      // Delete old caches
      caches.keys().then((cacheNames) => {
        return Promise.all(
          cacheNames
            .filter((cacheName) => !Object.values(CACHE_NAMES).includes(cacheName))
            .map((cacheName) => caches.delete(cacheName))
        )
      }),
      // Tell the active service worker to take control of the page immediately
      self.clients.claim()
    ])
  )
})

// Handle errors
self.addEventListener('error', (event) => {
  console.error('[Service Worker] Error:', event.error)
  // You could send this to your error tracking service
})

self.addEventListener('unhandledrejection', (event) => {
  console.error('[Service Worker] Unhandled rejection:', event.reason)
  // You could send this to your error tracking service
}) 