/**
 * Service Worker for justin.deal
 * Provides caching and offline support with advanced strategies
 */

// Cache versioning for easier updates
const CACHE_VERSION = '2';
const STATIC_CACHE_NAME = `justin-deal-static-v${CACHE_VERSION}`;
const DYNAMIC_CACHE_NAME = `justin-deal-dynamic-v${CACHE_VERSION}`;
const API_CACHE_NAME = `justin-deal-api-v${CACHE_VERSION}`;
const IMAGE_CACHE_NAME = `justin-deal-images-v${CACHE_VERSION}`;

// Cache expiration times (in milliseconds)
const CACHE_EXPIRATION = {
  API: 5 * 60 * 1000, // 5 minutes
  DYNAMIC: 24 * 60 * 60 * 1000, // 1 day
  IMAGES: 7 * 24 * 60 * 60 * 1000 // 7 days
};

// Cache size limits
const CACHE_SIZE_LIMITS = {
  DYNAMIC: 50, // items
  IMAGES: 100 // items
};

// Assets to cache immediately on service worker install
const PRECACHE_ASSETS = [
  '/',
  '/index.html',
  '/favicon.svg',
  '/site.webmanifest',
  '/offline.html',
  '/favicons/favicon.png',
  '/favicons/apple-touch-icon.png',
  '/favicons/favicon-16x16.png',
  '/favicons/favicon-32x32.png'
];

// Cache strategies
const CACHE_STRATEGIES = {
  // Cache first, falling back to network
  CACHE_FIRST: 'cache-first',
  // Network first, falling back to cache
  NETWORK_FIRST: 'network-first',
  // Cache only (no network request)
  CACHE_ONLY: 'cache-only',
  // Network only (no cache lookup)
  NETWORK_ONLY: 'network-only',
  // Stale while revalidate (serve from cache, update cache in background)
  STALE_WHILE_REVALIDATE: 'stale-while-revalidate'
};

// Route patterns and their corresponding cache strategies
const ROUTE_STRATEGIES = [
  // HTML pages - network first
  {
    pattern: /\.html$|\/$/,
    strategy: CACHE_STRATEGIES.NETWORK_FIRST,
    cacheName: STATIC_CACHE_NAME
  },
  // CSS and JS - stale while revalidate
  {
    pattern: /\.(css|js)$/,
    strategy: CACHE_STRATEGIES.STALE_WHILE_REVALIDATE,
    cacheName: STATIC_CACHE_NAME
  },
  // Images - cache first
  {
    pattern: /\.(jpe?g|png|gif|svg|webp|avif)$/,
    strategy: CACHE_STRATEGIES.CACHE_FIRST,
    cacheName: IMAGE_CACHE_NAME
  },
  // Fonts - cache first
  {
    pattern: /\.(woff2?|ttf|otf|eot)$/,
    strategy: CACHE_STRATEGIES.CACHE_FIRST,
    cacheName: STATIC_CACHE_NAME
  },
  // API requests - stale while revalidate
  {
    pattern: /\/api\//,
    strategy: CACHE_STRATEGIES.STALE_WHILE_REVALIDATE,
    cacheName: API_CACHE_NAME
  }
];

// Determine cache strategy and cache name for a given URL
function getStrategyForUrl(url) {
  const matchedRoute = ROUTE_STRATEGIES.find(route => route.pattern.test(url));
  return {
    strategy: matchedRoute ? matchedRoute.strategy : CACHE_STRATEGIES.NETWORK_FIRST,
    cacheName: matchedRoute ? matchedRoute.cacheName : DYNAMIC_CACHE_NAME
  };
}

// Helper function to store cache metadata
function storeCacheMetadata(cacheName, url, metadata) {
  return caches.open(`${cacheName}-metadata`)
    .then(metaCache => {
      return metaCache.put(
        new Request(`metadata:${url}`),
        new Response(JSON.stringify(metadata))
      );
    });
}

// Helper function to get cache metadata
async function getCacheMetadata(cacheName, url) {
  const metaCache = await caches.open(`${cacheName}-metadata`);
  const metadataResponse = await metaCache.match(new Request(`metadata:${url}`));
  
  if (metadataResponse) {
    return JSON.parse(await metadataResponse.text());
  }
  
  return null;
}

// Helper function to check if a cached response is expired
async function isCacheExpired(cacheName, url) {
  const metadata = await getCacheMetadata(cacheName, url);
  
  if (!metadata || !metadata.timestamp) {
    return true;
  }
  
  const age = Date.now() - metadata.timestamp;
  
  switch (cacheName) {
    case API_CACHE_NAME:
      return age > CACHE_EXPIRATION.API;
    case DYNAMIC_CACHE_NAME:
      return age > CACHE_EXPIRATION.DYNAMIC;
    case IMAGE_CACHE_NAME:
      return age > CACHE_EXPIRATION.IMAGES;
    default:
      return false; // Static cache doesn't expire
  }
}

// Helper function to limit cache size
async function trimCache(cacheName, maxItems) {
  const cache = await caches.open(cacheName);
  const keys = await cache.keys();
  
  if (keys.length > maxItems) {
    // Delete oldest items (first in the list)
    for (let i = 0; i < keys.length - maxItems; i++) {
      await cache.delete(keys[i]);
      
      // Also delete metadata
      const metaCache = await caches.open(`${cacheName}-metadata`);
      await metaCache.delete(new Request(`metadata:${keys[i].url}`));
    }
  }
}

// Install event - precache critical assets
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(STATIC_CACHE_NAME)
      .then(cache => cache.addAll(PRECACHE_ASSETS))
      .then(() => self.skipWaiting())
  );
});

// Activate event - clean up old caches
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames
          .filter(cacheName => {
            // Keep current version caches
            return !cacheName.includes(`-v${CACHE_VERSION}`) && 
                   !cacheName.includes(`-metadata`);
          })
          .map(cacheName => caches.delete(cacheName))
      );
    }).then(() => self.clients.claim())
  );
});

// Helper function to handle network-first strategy
async function networkFirstStrategy(request, cacheName) {
  try {
    // Try network first
    const networkResponse = await fetch(request);
    
    // If successful, clone and cache the response
    if (networkResponse.ok) {
      const cache = await caches.open(cacheName);
      cache.put(request, networkResponse.clone());
      
      // Store metadata with timestamp
      storeCacheMetadata(cacheName, request.url, {
        timestamp: Date.now(),
        url: request.url
      });
    }
    
    return networkResponse;
  } catch (error) {
    // If network fails, try cache
    const cachedResponse = await caches.match(request);
    if (cachedResponse) {
      return cachedResponse;
    }
    
    // If no cache, throw error
    throw error;
  }
}

// Helper function to handle cache-first strategy
async function cacheFirstStrategy(request, cacheName) {
  // Try cache first
  const cachedResponse = await caches.match(request);
  
  // Check if we have a cached response and if it's expired
  const isExpired = cachedResponse ? await isCacheExpired(cacheName, request.url) : true;
  
  // If we have a valid cached response, use it
  if (cachedResponse && !isExpired) {
    return cachedResponse;
  }
  
  // If not in cache or expired, get from network
  try {
    const networkResponse = await fetch(request);
    
    // Cache the response for future
    if (networkResponse.ok) {
      const cache = await caches.open(cacheName);
      cache.put(request, networkResponse.clone());
      
      // Store metadata with timestamp
      storeCacheMetadata(cacheName, request.url, {
        timestamp: Date.now(),
        url: request.url
      });
      
      // Trim cache if needed
      if (cacheName === IMAGE_CACHE_NAME) {
        trimCache(IMAGE_CACHE_NAME, CACHE_SIZE_LIMITS.IMAGES);
      }
    }
    
    return networkResponse;
  } catch (error) {
    // If network fails and we have an expired cached response, use it as fallback
    if (cachedResponse) {
      console.log('Using expired cached response as fallback');
      return cachedResponse;
    }
    
    // If no cache, throw error
    throw error;
  }
}

// Enhanced stale-while-revalidate strategy with cache expiration and metadata
async function staleWhileRevalidateStrategy(request, cacheName) {
  const url = request.url;
  const cache = await caches.open(cacheName);
  
  // Try to get from cache
  const cachedResponse = await cache.match(request);
  
  // Check if we have a cached response and if it's expired
  const isExpired = cachedResponse ? await isCacheExpired(cacheName, url) : true;
  
  // If we have a cached response, use it immediately (even if expired)
  // This is the "stale" part
  
  // Fetch from network to update cache (don't await)
  const fetchPromise = fetch(request)
    .then(networkResponse => {
      if (networkResponse.ok) {
        // Clone the response before using it
        const responseToCache = networkResponse.clone();
        
        // Store in cache
        cache.put(request, responseToCache);
        
        // Store metadata with timestamp
        storeCacheMetadata(cacheName, url, {
          timestamp: Date.now(),
          url: url
        });
        
        // Trim cache if needed
        if (cacheName === DYNAMIC_CACHE_NAME) {
          trimCache(DYNAMIC_CACHE_NAME, CACHE_SIZE_LIMITS.DYNAMIC);
        } else if (cacheName === IMAGE_CACHE_NAME) {
          trimCache(IMAGE_CACHE_NAME, CACHE_SIZE_LIMITS.IMAGES);
        }
      }
      return networkResponse;
    })
    .catch(error => {
      console.error('Failed to fetch and update cache:', error);
      // If we have a cached response but it's expired, still return it
      if (cachedResponse) {
        console.log('Returning expired cached response as fallback');
        return cachedResponse;
      }
      throw error;
    });
  
  // Return cached response immediately if available and not expired
  // Otherwise wait for the network response
  return cachedResponse && !isExpired ? cachedResponse : fetchPromise;
}

// Fetch event - handle all fetch requests
self.addEventListener('fetch', event => {
  // Skip non-GET requests and browser extensions
  if (event.request.method !== 'GET' || 
      event.request.url.startsWith('chrome-extension://') ||
      event.request.url.includes('extension://')) {
    return;
  }
  
  // Get the appropriate strategy and cache name for this URL
  const { strategy, cacheName } = getStrategyForUrl(event.request.url);
  
  // Apply the selected strategy
  switch (strategy) {
    case CACHE_STRATEGIES.NETWORK_FIRST:
      event.respondWith(networkFirstStrategy(event.request, cacheName));
      break;
      
    case CACHE_STRATEGIES.CACHE_FIRST:
      event.respondWith(cacheFirstStrategy(event.request, cacheName));
      break;
      
    case CACHE_STRATEGIES.STALE_WHILE_REVALIDATE:
      event.respondWith(staleWhileRevalidateStrategy(event.request, cacheName));
      break;
      
    case CACHE_STRATEGIES.CACHE_ONLY:
      event.respondWith(caches.match(event.request));
      break;
      
    case CACHE_STRATEGIES.NETWORK_ONLY:
      event.respondWith(fetch(event.request));
      break;
      
    default:
      // Default to network first
      event.respondWith(networkFirstStrategy(event.request, DYNAMIC_CACHE_NAME));
  }
});

// Handle offline fallback
self.addEventListener('fetch', event => {
  // Only handle HTML navigation requests that fail
  if (event.request.mode === 'navigate') {
    event.respondWith(
      fetch(event.request)
        .catch(() => {
          // If fetch fails, return the offline page
          return caches.match('/offline.html')
            .then(response => {
              return response || new Response(
                '<html><body><h1>Offline</h1><p>You are currently offline.</p></body></html>',
                {
                  headers: { 'Content-Type': 'text/html' }
                }
              );
            });
        })
    );
  }
});

// Listen for messages from the client
self.addEventListener('message', event => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
  
  // Handle cache cleanup request
  if (event.data && event.data.type === 'CLEAR_CACHES') {
    event.waitUntil(
      caches.keys().then(cacheNames => {
        return Promise.all(
          cacheNames.map(cacheName => caches.delete(cacheName))
        );
      }).then(() => {
        // Notify client that caches were cleared, but only if port is available
        if (event.ports && event.ports.length > 0) {
          try {
            event.ports[0].postMessage({ 
              status: 'success',
              message: 'All caches cleared successfully'
            });
          } catch (err) {
            console.log('Could not post message to client: port may be closed');
          }
        }
      })
    );
  }
});
