/**
 * Tour Builder Platform - Service Worker (Serwist)
 *
 * Provides offline caching for PWA functionality with:
 * - Range requests for video seeking
 * - IndexedDB fallback for large files
 * - Background sync for resumable downloads
 */

/// <reference lib="webworker" />

import { defaultCache } from '@serwist/next/worker';
import type { PrecacheEntry, SerwistGlobalConfig } from 'serwist';
import {
  CacheFirst,
  NetworkFirst,
  Serwist,
  StaleWhileRevalidate,
} from 'serwist';
import { OFFLINE_CONFIG } from './config/offline.config';

// Service Worker type declarations
declare const self: ServiceWorkerGlobalScope & typeof globalThis;

// Serwist global configuration
declare global {
  interface ServiceWorkerGlobalScope extends SerwistGlobalConfig {
    __SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
  }
}

// Cacheable asset extensions
const CACHEABLE_EXTENSIONS = [
  '.png',
  '.jpg',
  '.jpeg',
  '.gif',
  '.webp',
  '.svg',
  '.ico',
  '.mp4',
  '.webm',
  '.mov',
  '.mp3',
  '.wav',
  '.ogg',
  '.m4a',
  '.woff',
  '.woff2',
  '.ttf',
  '.eot',
  '.css',
  '.js',
];

// Check if request should be cached
const isCacheableRequest = (request: Request): boolean => {
  const url = new URL(request.url);

  // Don't cache API requests (except static assets from /file/download)
  if (
    url.pathname.startsWith('/api/') &&
    !url.pathname.includes('/file/download')
  ) {
    return false;
  }

  // Cache known asset extensions
  const hasExtension = CACHEABLE_EXTENSIONS.some((ext) =>
    url.pathname.toLowerCase().endsWith(ext),
  );
  if (hasExtension) {
    return true;
  }

  // Cache file downloads (S3 presigned URLs, CDN assets)
  if (
    url.pathname.includes('/file/download') ||
    url.hostname.includes('amazonaws.com') ||
    url.hostname.includes('cloudfront.net')
  ) {
    return true;
  }

  return false;
};

// Check if request is a video
const isVideoRequest = (request: Request): boolean => {
  const url = new URL(request.url);
  return ['.mp4', '.webm', '.mov'].some((ext) =>
    url.pathname.toLowerCase().endsWith(ext),
  );
};

// Initialize Serwist
const serwist = new Serwist({
  precacheEntries: self.__SW_MANIFEST,
  skipWaiting: true,
  clientsClaim: true,
  navigationPreload: true,
  runtimeCaching: [
    // Static assets (images, fonts, css, js) - Cache First
    // Uses 'assets' cache to match preloading (usePreloadOrchestrator stores here)
    {
      matcher: ({ request }) => {
        const url = new URL(request.url);
        return (
          request.destination === 'image' ||
          request.destination === 'font' ||
          ['.css', '.js', '.woff', '.woff2'].some((ext) =>
            url.pathname.endsWith(ext),
          )
        );
      },
      handler: new CacheFirst({
        cacheName: OFFLINE_CONFIG.cacheNames.assets,
        plugins: [
          {
            cacheWillUpdate: async ({ response }) => {
              if (response && response.status === 200) {
                return response;
              }
              return null;
            },
          },
        ],
      }),
    },
    // Videos - Cache First with Range Request support
    {
      matcher: ({ request }) => isVideoRequest(request),
      handler: new CacheFirst({
        cacheName: OFFLINE_CONFIG.cacheNames.assets,
        plugins: [
          {
            // Handle range requests for video seeking
            cachedResponseWillBeUsed: async ({ cachedResponse, request }) => {
              if (!cachedResponse) return null;

              const rangeHeader = request.headers.get('range');
              if (!rangeHeader) return cachedResponse;

              // Parse range header
              const match = rangeHeader.match(/bytes=(\d+)-(\d*)/);
              if (!match) return cachedResponse;

              const start = parseInt(match[1], 10);
              const end = match[2] ? parseInt(match[2], 10) : undefined;

              // Get the full response body
              const blob = await cachedResponse.blob();
              const slicedBlob =
                end !== undefined
                  ? blob.slice(start, end + 1)
                  : blob.slice(start);

              // Return partial content
              return new Response(slicedBlob, {
                status: 206,
                statusText: 'Partial Content',
                headers: {
                  'Content-Type':
                    cachedResponse.headers.get('Content-Type') || 'video/mp4',
                  'Content-Length': String(slicedBlob.size),
                  'Content-Range': `bytes ${start}-${end !== undefined ? end : blob.size - 1}/${blob.size}`,
                  'Accept-Ranges': 'bytes',
                },
              });
            },
            cacheWillUpdate: async ({ response }) => {
              if (response && response.status === 200) {
                return response;
              }
              return null;
            },
          },
        ],
      }),
    },
    // API requests - Network First
    {
      matcher: ({ url }) => url.pathname.startsWith('/api/'),
      handler: new NetworkFirst({
        cacheName: 'api-cache',
        networkTimeoutSeconds: 10,
      }),
    },
    // Dynamic assets (audio, other cacheable) - Cache First
    // Uses 'assets' cache to match preloading (usePreloadOrchestrator stores here)
    {
      matcher: ({ request }) =>
        isCacheableRequest(request) && !isVideoRequest(request),
      handler: new CacheFirst({
        cacheName: OFFLINE_CONFIG.cacheNames.assets,
        plugins: [
          {
            cacheWillUpdate: async ({ response }) => {
              if (response && response.status === 200) {
                return response;
              }
              return null;
            },
          },
        ],
      }),
    },
    // Default caching strategies
    ...defaultCache,
  ],
});

// Message event - handle commands from main thread
self.addEventListener('message', (event) => {
  const { type, payload } = event.data || {};

  switch (type) {
    case 'CACHE_ASSETS':
      // Cache specific assets for a project/page
      if (Array.isArray(payload?.urls)) {
        event.waitUntil(
          caches.open(OFFLINE_CONFIG.cacheNames.assets).then((cache) => {
            return Promise.all(
              payload.urls.map((url: string) =>
                fetch(url)
                  .then((response) => {
                    if (response.status === 200) {
                      return cache.put(url, response);
                    }
                  })
                  .catch((error) => {
                    console.warn('[SW] Failed to cache asset:', url, error);
                  }),
              ),
            );
          }),
        );
      }
      break;

    case 'CACHE_VIDEO_CHUNK':
      // Cache a video chunk with range support
      if (payload?.url && payload?.chunk) {
        event.waitUntil(
          caches.open(OFFLINE_CONFIG.cacheNames.assets).then((cache) => {
            const response = new Response(payload.chunk, {
              headers: {
                'Content-Type': payload.contentType || 'video/mp4',
                'Content-Length': String(payload.chunk.byteLength),
              },
            });
            return cache.put(payload.url, response);
          }),
        );
      }
      break;

    case 'CLEAR_CACHE':
      // Clear all dynamic caches
      event.waitUntil(
        Promise.all([
          caches.delete(OFFLINE_CONFIG.cacheNames.dynamic),
          caches.delete(OFFLINE_CONFIG.cacheNames.assets),
        ]).then(() => {
          console.log('[SW] Caches cleared');
        }),
      );
      break;

    case 'GET_CACHE_STATUS':
      // Return current cache status
      event.waitUntil(
        caches.open(OFFLINE_CONFIG.cacheNames.assets).then((cache) => {
          return cache.keys().then((keys) => {
            const source = event.source as Client | null;
            source?.postMessage({
              type: 'CACHE_STATUS',
              payload: {
                cachedCount: keys.length,
                urls: keys.map((request) => request.url),
              },
            });
          });
        }),
      );
      break;

    case 'SKIP_WAITING':
      self.skipWaiting();
      break;

    default:
      break;
  }
});

// Add event listeners for Serwist
serwist.addEventListeners();

console.log('[SW] Serwist service worker loaded');
