// StripStream Service Worker - Version 2
// Architecture: SWR (Stale-While-Revalidate) for all resources

const VERSION = "v2.5";
const STATIC_CACHE = `stripstream-static-${VERSION}`;
const PAGES_CACHE = `stripstream-pages-${VERSION}`; // Navigation + RSC (client-side navigation)
const API_CACHE = `stripstream-api-${VERSION}`;
const IMAGES_CACHE = `stripstream-images-${VERSION}`;
const BOOKS_CACHE = "stripstream-books"; // Never version this - managed by DownloadManager

const OFFLINE_PAGE = "/offline.html";
const PRECACHE_ASSETS = [OFFLINE_PAGE, "/manifest.json"];

// Cache size limits
const IMAGES_CACHE_MAX_SIZE = 100 * 1024 * 1024; // 100MB
const IMAGES_CACHE_MAX_ENTRIES = 500;

// ============================================================================
// Utility Functions - Request Detection
// ============================================================================

function isNextStaticResource(url) {
  return url.includes("/_next/static/");
}

function isNextRSCRequest(request) {
  const url = new URL(request.url);
  return url.searchParams.has("_rsc") || request.headers.get("RSC") === "1";
}

function isApiRequest(url) {
  return url.includes("/api/komga/") && !url.includes("/api/komga/images/");
}

function isImageRequest(url) {
  return url.includes("/api/komga/images/");
}

function isBookPageRequest(url) {
  // Book pages: /api/komga/images/books/{id}/pages/{num} or /api/komga/books/{id}/pages/{num}
  // These are handled by manual download (DownloadManager) - don't cache via SWR
  return (
    (url.includes("/api/komga/images/books/") || url.includes("/api/komga/books/")) &&
    url.includes("/pages/")
  );
}

function isBooksManualCache(url) {
  // Check if this is a request that should be handled by the books manual cache
  return url.includes("/api/komga/images/books/") && url.includes("/pages");
}

// ============================================================================
// Client Communication
// ============================================================================

async function notifyClients(message) {
  const clients = await self.clients.matchAll({ type: "window" });
  clients.forEach((client) => {
    client.postMessage(message);
  });
}

// ============================================================================
// Cache Management
// ============================================================================

async function getCacheSize(cacheName) {
  const cache = await caches.open(cacheName);
  const keys = await cache.keys();
  let totalSize = 0;

  for (const request of keys) {
    const response = await cache.match(request);
    if (response) {
      const blob = await response.clone().blob();
      totalSize += blob.size;
    }
  }

  return totalSize;
}

async function trimCache(cacheName, maxEntries) {
  const cache = await caches.open(cacheName);
  const keys = await cache.keys();

  if (keys.length > maxEntries) {
    // Remove oldest entries (FIFO)
    const toDelete = keys.slice(0, keys.length - maxEntries);
    await Promise.all(toDelete.map((key) => cache.delete(key)));
    // eslint-disable-next-line no-console
    console.log(`[SW] Trimmed ${toDelete.length} entries from ${cacheName}`);
  }
}

// ============================================================================
// Cache Strategies
// ============================================================================

/**
 * Cache-First: Serve from cache, fallback to network
 * Used for: Next.js static resources (immutable)
 */
async function cacheFirstStrategy(request, cacheName, options = {}) {
  const cache = await caches.open(cacheName);
  const cached = await cache.match(request, options);

  if (cached) {
    return cached;
  }

  try {
    const response = await fetch(request);
    if (response.ok) {
      cache.put(request, response.clone());
    }
    return response;
  } catch (error) {
    // Network failed - try cache without ignoreSearch as fallback
    if (options.ignoreSearch) {
      const fallback = await cache.match(request, { ignoreSearch: false });
      if (fallback) return fallback;
    }
    throw error;
  }
}

/**
 * Stale-While-Revalidate: Serve from cache immediately, update in background
 * Used for: API calls, images
 * Respects Cache-Control: no-cache to force network-first (for refresh buttons)
 */
async function staleWhileRevalidateStrategy(request, cacheName, options = {}) {
  const cache = await caches.open(cacheName);

  // Check if client requested no-cache (refresh button, router.refresh(), etc.)
  // 1. Check Cache-Control header
  const cacheControl = request.headers.get("Cache-Control");
  const noCacheHeader =
    cacheControl && (cacheControl.includes("no-cache") || cacheControl.includes("no-store"));
  // 2. Check request.cache mode (used by Next.js router.refresh())
  const noCacheMode =
    request.cache === "no-cache" || request.cache === "no-store" || request.cache === "reload";
  const noCache = noCacheHeader || noCacheMode;

  // If no-cache, skip cached response and go network-first
  const cached = noCache ? null : await cache.match(request);

  // Start network request (don't await)
  const fetchPromise = fetch(request)
    .then(async (response) => {
      if (response.ok) {
        // Clone response for cache
        const responseToCache = response.clone();

        // Check if content changed (for notification)
        if (cached && options.notifyOnChange) {
          try {
            const cachedResponse = await cache.match(request);
            if (cachedResponse) {
              // For JSON APIs, compare content
              if (options.isJson) {
                const oldText = await cachedResponse.text();
                const newText = await response.clone().text();
                if (oldText !== newText) {
                  notifyClients({
                    type: "CACHE_UPDATED",
                    url: request.url,
                    timestamp: Date.now(),
                  });
                }
              }
            }
          } catch {
            // Ignore comparison errors
          }
        }

        // Update cache
        await cache.put(request, responseToCache);

        // Trim cache if needed (for images)
        if (options.maxEntries) {
          trimCache(cacheName, options.maxEntries);
        }
      }
      return response;
    })
    .catch((error) => {
      // eslint-disable-next-line no-console
      console.log("[SW] Network request failed:", request.url, error.message);
      return null;
    });

  // Return cached version immediately if available
  if (cached) {
    return cached;
  }

  // Otherwise wait for network
  const response = await fetchPromise;
  if (response) {
    return response;
  }

  throw new Error("Network failed and no cache available");
}

/**
 * Navigation SWR: Serve from cache immediately, update in background
 * Falls back to offline page if nothing cached
 * Used for: Page navigations
 */
async function navigationSWRStrategy(request, cacheName) {
  const cache = await caches.open(cacheName);
  const cached = await cache.match(request);

  // Start network request in background
  const fetchPromise = fetch(request)
    .then(async (response) => {
      if (response.ok) {
        await cache.put(request, response.clone());
      }
      return response;
    })
    .catch(() => null);

  // Return cached version immediately if available
  if (cached) {
    return cached;
  }

  // No cache - wait for network
  const response = await fetchPromise;
  if (response) {
    return response;
  }

  // Network failed and no cache - try fallbacks
  // Try to serve root page for SPA client-side routing
  const rootPage = await cache.match("/");
  if (rootPage) {
    return rootPage;
  }

  // Last resort: offline page (in static cache)
  const staticCache = await caches.open(STATIC_CACHE);
  const offlinePage = await staticCache.match(OFFLINE_PAGE);
  if (offlinePage) {
    return offlinePage;
  }

  throw new Error("Offline and no cached page available");
}

// ============================================================================
// Service Worker Lifecycle
// ============================================================================

self.addEventListener("install", (event) => {
  // eslint-disable-next-line no-console
  console.log("[SW] Installing version", VERSION);

  event.waitUntil(
    (async () => {
      const cache = await caches.open(STATIC_CACHE);
      try {
        await cache.addAll(PRECACHE_ASSETS);
        // eslint-disable-next-line no-console
        console.log("[SW] Precached assets");
      } catch (error) {
        // eslint-disable-next-line no-console
        console.error("[SW] Precache failed:", error);
      }
      await self.skipWaiting();
    })()
  );
});

self.addEventListener("activate", (event) => {
  // eslint-disable-next-line no-console
  console.log("[SW] Activating version", VERSION);

  event.waitUntil(
    (async () => {
      // Clean up old caches, but preserve BOOKS_CACHE
      const cacheNames = await caches.keys();
      const currentCaches = [STATIC_CACHE, PAGES_CACHE, API_CACHE, IMAGES_CACHE, BOOKS_CACHE];

      const cachesToDelete = cacheNames.filter(
        (name) => name.startsWith("stripstream-") && !currentCaches.includes(name)
      );

      await Promise.all(cachesToDelete.map((name) => caches.delete(name)));

      if (cachesToDelete.length > 0) {
        // eslint-disable-next-line no-console
        console.log("[SW] Deleted old caches:", cachesToDelete);
      }

      await self.clients.claim();
      // eslint-disable-next-line no-console
      console.log("[SW] Activated and claimed clients");

      // Notify clients that SW is ready
      notifyClients({ type: "SW_ACTIVATED", version: VERSION });
    })()
  );
});

// ============================================================================
// Message Handler - Client Communication
// ============================================================================

self.addEventListener("message", async (event) => {
  const { type, payload } = event.data || {};

  switch (type) {
    case "GET_CACHE_STATS": {
      try {
        const [staticSize, pagesSize, apiSize, imagesSize, booksSize] = await Promise.all([
          getCacheSize(STATIC_CACHE),
          getCacheSize(PAGES_CACHE),
          getCacheSize(API_CACHE),
          getCacheSize(IMAGES_CACHE),
          getCacheSize(BOOKS_CACHE),
        ]);

        const staticCache = await caches.open(STATIC_CACHE);
        const pagesCache = await caches.open(PAGES_CACHE);
        const apiCache = await caches.open(API_CACHE);
        const imagesCache = await caches.open(IMAGES_CACHE);
        const booksCache = await caches.open(BOOKS_CACHE);

        const [staticKeys, pagesKeys, apiKeys, imagesKeys, booksKeys] = await Promise.all([
          staticCache.keys(),
          pagesCache.keys(),
          apiCache.keys(),
          imagesCache.keys(),
          booksCache.keys(),
        ]);

        event.source.postMessage({
          type: "CACHE_STATS",
          payload: {
            static: { size: staticSize, entries: staticKeys.length },
            pages: { size: pagesSize, entries: pagesKeys.length },
            api: { size: apiSize, entries: apiKeys.length },
            images: { size: imagesSize, entries: imagesKeys.length },
            books: { size: booksSize, entries: booksKeys.length },
            total: staticSize + pagesSize + apiSize + imagesSize + booksSize,
          },
        });
      } catch (error) {
        event.source.postMessage({
          type: "CACHE_STATS_ERROR",
          payload: { error: error.message },
        });
      }
      break;
    }

    case "CLEAR_CACHE": {
      try {
        const cacheType = payload?.cacheType || "all";
        const cachesToClear = [];

        if (cacheType === "all" || cacheType === "static") {
          cachesToClear.push(STATIC_CACHE);
        }
        if (cacheType === "all" || cacheType === "pages") {
          cachesToClear.push(PAGES_CACHE);
        }
        if (cacheType === "all" || cacheType === "api") {
          cachesToClear.push(API_CACHE);
        }
        if (cacheType === "all" || cacheType === "images") {
          cachesToClear.push(IMAGES_CACHE);
        }
        // Note: BOOKS_CACHE is not cleared by default, only explicitly

        await Promise.all(
          cachesToClear.map(async (cacheName) => {
            const cache = await caches.open(cacheName);
            const keys = await cache.keys();
            await Promise.all(keys.map((key) => cache.delete(key)));
          })
        );

        event.source.postMessage({
          type: "CACHE_CLEARED",
          payload: { caches: cachesToClear },
        });
      } catch (error) {
        event.source.postMessage({
          type: "CACHE_CLEAR_ERROR",
          payload: { error: error.message },
        });
      }
      break;
    }

    case "SKIP_WAITING": {
      self.skipWaiting();
      break;
    }

    case "GET_VERSION": {
      event.source.postMessage({
        type: "SW_VERSION",
        payload: { version: VERSION },
      });
      break;
    }

    case "GET_CACHE_ENTRIES": {
      try {
        const cacheType = payload?.cacheType;
        let cacheName;

        switch (cacheType) {
          case "static":
            cacheName = STATIC_CACHE;
            break;
          case "pages":
            cacheName = PAGES_CACHE;
            break;
          case "api":
            cacheName = API_CACHE;
            break;
          case "images":
            cacheName = IMAGES_CACHE;
            break;
          case "books":
            cacheName = BOOKS_CACHE;
            break;
          default:
            throw new Error("Invalid cache type");
        }

        const cache = await caches.open(cacheName);
        const keys = await cache.keys();

        const entries = await Promise.all(
          keys.map(async (request) => {
            const response = await cache.match(request);
            let size = 0;
            if (response) {
              const blob = await response.clone().blob();
              size = blob.size;
            }
            return {
              url: request.url,
              size,
            };
          })
        );

        // Sort by size descending
        entries.sort((a, b) => b.size - a.size);

        event.source.postMessage({
          type: "CACHE_ENTRIES",
          payload: { cacheType, entries },
        });
      } catch (error) {
        event.source.postMessage({
          type: "CACHE_ENTRIES_ERROR",
          payload: { error: error.message },
        });
      }
      break;
    }
  }
});

// ============================================================================
// Fetch Handler - Request Routing
// ============================================================================

self.addEventListener("fetch", (event) => {
  const { request } = event;
  const { method } = request;
  const url = new URL(request.url);

  // Only handle GET requests
  if (method !== "GET") {
    return;
  }

  // Route 1: Book pages (handled by DownloadManager) - Check manual cache only, no SWR
  if (isBookPageRequest(url.href)) {
    event.respondWith(
      (async () => {
        // Check the manual books cache
        const booksCache = await caches.open(BOOKS_CACHE);
        const cached = await booksCache.match(request);
        if (cached) {
          return cached;
        }

        // Not in cache - fetch from network without caching
        // Book pages are large and should only be cached via manual download
        return fetch(request);
      })()
    );
    return;
  }

  // Route 2: Next.js RSC payloads (client-side navigation) → SWR in PAGES_CACHE
  if (isNextRSCRequest(request)) {
    event.respondWith(
      staleWhileRevalidateStrategy(request, PAGES_CACHE, {
        notifyOnChange: false,
      })
    );
    return;
  }

  // Route 3: Next.js static resources → Cache-First with ignoreSearch
  if (isNextStaticResource(url.href)) {
    event.respondWith(cacheFirstStrategy(request, STATIC_CACHE, { ignoreSearch: true }));
    return;
  }

  // Route 4: API requests (JSON) → SWR with notification
  if (isApiRequest(url.href)) {
    event.respondWith(
      staleWhileRevalidateStrategy(request, API_CACHE, {
        notifyOnChange: true,
        isJson: true,
      })
    );
    return;
  }

  // Route 5: Image requests (thumbnails, covers) → SWR with cache size management
  // Note: Book pages are excluded (Route 1) and only use manual download cache
  if (isImageRequest(url.href)) {
    event.respondWith(
      staleWhileRevalidateStrategy(request, IMAGES_CACHE, {
        maxEntries: IMAGES_CACHE_MAX_ENTRIES,
      })
    );
    return;
  }

  // Route 6: Navigation → SWR (cache first, revalidate in background)
  if (request.mode === "navigate") {
    event.respondWith(navigationSWRStrategy(request, PAGES_CACHE));
    return;
  }

  // Route 7: Everything else → Network only (no caching)
});
