/**
 * 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),
  );
};

// Check if request is audio
const isAudioRequest = (request: Request): boolean => {
  const url = new URL(request.url);
  return ['.mp3', '.wav', '.ogg', '.m4a', '.aac'].some((ext) =>
    url.pathname.toLowerCase().endsWith(ext),
  );
};

/**
 * Extract storage path from various URL formats.
 * Handles:
 * - Presigned S3 URLs: https://s3.../bucket/assets/project/file.mp4?X-Amz-Signature=...
 * - Backend proxy URLs: http://localhost:8080/api/file/download?privateUrl=assets%2F...
 * - Relative paths: assets/project/file.mp4
 */
const extractStoragePathFromUrl = (url: string): string | null => {
  try {
    // Backend proxy URL format
    if (url.includes('/file/download?privateUrl=')) {
      const match = url.match(/privateUrl=([^&]+)/);
      if (match) {
        return decodeURIComponent(match[1]).replace(/^\/+/, '');
      }
    }

    // Presigned S3 URL
    if (url.includes('X-Amz-Signature=') || url.includes('x-amz-signature=')) {
      const urlObj = new URL(url);
      const pathParts = urlObj.pathname.split('/').filter(Boolean);
      const assetsIndex = pathParts.findIndex((part) => part === 'assets');
      if (assetsIndex !== -1) {
        return pathParts.slice(assetsIndex).join('/');
      }
      if (pathParts.length > 1) {
        return pathParts.slice(1).join('/');
      }
    }

    // Already a relative storage path (starts with 'assets/')
    if (url.startsWith('assets/')) {
      return url;
    }

    // Full S3 URL (non-presigned)
    const s3Match = url.match(
      /^https?:\/\/[^/]+\.s3\.[^/]+\.amazonaws\.com\/[^/]+\/(assets\/.+)$/,
    );
    if (s3Match) {
      return s3Match[1].split('?')[0]; // Remove query params
    }

    return null;
  } catch {
    return null;
  }
};

// Storage path → storage key mapping (for caching browser video/audio requests)
// When main thread does partial preload, it registers {storagePath → storageKey}
// SW extracts storage path from any URL format and uses it for cache lookups
const storagePathToKeyMap = new Map<string, string>();

// Clean up old mappings every hour (session cleanup)
setInterval(
  () => {
    storagePathToKeyMap.clear();
    console.log('[SW] Cleared storage path mappings');
  },
  60 * 60 * 1000,
);

// 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: [
          {
            // Transform URL to storage key for consistent caching
            // Matches preload behavior (DownloadManager uses storage keys)
            cacheKeyWillBeUsed: async ({ request, mode }) => {
              const storagePath = extractStoragePathFromUrl(request.url);
              if (storagePath) {
                console.log(
                  `[SW] Using storagePath for static asset ${mode}:`,
                  storagePath.slice(-40),
                );
                return new Request(storagePath);
              }
              return request;
            },
            cacheWillUpdate: async ({ response }) => {
              if (response && response.status === 200) {
                return response;
              }
              return null;
            },
          },
        ],
      }),
    },
    // Videos - Cache First with Range Request support and storage key mapping
    {
      matcher: ({ request }) => isVideoRequest(request),
      handler: new CacheFirst({
        cacheName: OFFLINE_CONFIG.cacheNames.assets,
        plugins: [
          {
            // Transform URL to storage key for BOTH cache reads and writes
            // Per Serwist: mode='read' for lookups, mode='write' for storing
            cacheKeyWillBeUsed: async ({ request, mode }) => {
              // Extract storage path from any URL format
              const storagePath = extractStoragePathFromUrl(request.url);
              if (storagePath) {
                // Check if we have a mapping for this storage path
                const storageKey = storagePathToKeyMap.get(storagePath);
                if (storageKey) {
                  console.log(
                    `[SW] Using storageKey for video ${mode}:`,
                    storageKey.slice(-40),
                  );
                  return new Request(storageKey);
                }
                // No explicit mapping, use extracted storage path as cache key
                console.log(
                  `[SW] Using storagePath for video ${mode}:`,
                  storagePath.slice(-40),
                );
                return new Request(storagePath);
              }
              // No storage path extracted - use original URL (fallback)
              return request;
            },

            // 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;
            },
          },
        ],
      }),
    },
    // Audio - Cache First with Range Request support and storage key mapping
    {
      matcher: ({ request }) => isAudioRequest(request),
      handler: new CacheFirst({
        cacheName: OFFLINE_CONFIG.cacheNames.assets,
        plugins: [
          {
            // Transform URL to storage key for BOTH cache reads and writes
            cacheKeyWillBeUsed: async ({ request, mode }) => {
              // Extract storage path from any URL format
              const storagePath = extractStoragePathFromUrl(request.url);
              if (storagePath) {
                const storageKey = storagePathToKeyMap.get(storagePath);
                if (storageKey) {
                  console.log(
                    `[SW] Using storageKey for audio ${mode}:`,
                    storageKey.slice(-40),
                  );
                  return new Request(storageKey);
                }
                // No explicit mapping, use extracted storage path as cache key
                console.log(
                  `[SW] Using storagePath for audio ${mode}:`,
                  storagePath.slice(-40),
                );
                return new Request(storagePath);
              }
              return request;
            },

            // Handle range requests for audio seeking
            cachedResponseWillBeUsed: async ({ cachedResponse, request }) => {
              if (!cachedResponse) return null;

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

              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;

              const blob = await cachedResponse.blob();
              const slicedBlob =
                end !== undefined
                  ? blob.slice(start, end + 1)
                  : blob.slice(start);

              return new Response(slicedBlob, {
                status: 206,
                statusText: 'Partial Content',
                headers: {
                  'Content-Type':
                    cachedResponse.headers.get('Content-Type') || 'audio/mpeg',
                  '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 (other cacheable, excluding video and audio) - Cache First
    // Uses 'assets' cache to match preloading (usePreloadOrchestrator stores here)
    {
      matcher: ({ request }) =>
        isCacheableRequest(request) &&
        !isVideoRequest(request) &&
        !isAudioRequest(request),
      handler: new CacheFirst({
        cacheName: OFFLINE_CONFIG.cacheNames.assets,
        plugins: [
          {
            // Transform URL to storage key for consistent caching
            // Matches preload behavior (DownloadManager uses storage keys)
            cacheKeyWillBeUsed: async ({ request, mode }) => {
              const storagePath = extractStoragePathFromUrl(request.url);
              if (storagePath) {
                console.log(
                  `[SW] Using storagePath for dynamic asset ${mode}:`,
                  storagePath.slice(-40),
                );
                return new Request(storagePath);
              }
              return request;
            },
            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 'REGISTER_CACHE_URL':
      // Register storage path → storage key mapping for media caching
      // Main thread sends this after partial preload; when browser fetches
      // the full media during playback, we cache using the storage key
      if (payload?.storageKey) {
        // Extract storage path from presigned URL (or use storageKey directly)
        const storagePath = payload.presignedUrl
          ? extractStoragePathFromUrl(payload.presignedUrl) || payload.storageKey
          : payload.storageKey;
        storagePathToKeyMap.set(storagePath, payload.storageKey);
        console.log('[SW] Registered storage path for caching', {
          storagePath: storagePath.slice(-50),
          storageKey: payload.storageKey.slice(-50),
        });
      }
      break;

    case 'CLEAR_URL_MAPPINGS':
      // Clear storage path mappings (called on cleanup/unmount)
      storagePathToKeyMap.clear();
      console.log('[SW] Storage path mappings cleared');
      break;

    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 and storage path mappings
      storagePathToKeyMap.clear();
      event.waitUntil(
        Promise.all([
          caches.delete(OFFLINE_CONFIG.cacheNames.dynamic),
          caches.delete(OFFLINE_CONFIG.cacheNames.assets),
        ]).then(() => {
          console.log('[SW] Caches and storage path mappings 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');
