/**
 * usePreloadOrchestrator Hook
 *
 * Main coordinator for online mode asset preloading.
 * Manages the priority queue and orchestrates downloads based on navigation.
 */

import { useEffect, useRef, useCallback, useState } from 'react';
import { useNeighborGraph } from './useNeighborGraph';
import { useNetworkAware } from './useNetworkAware';
import { downloadEventBus } from '../lib/offline/DownloadEventBus';
import { StorageManager } from '../lib/offline/StorageManager';
import { PRELOAD_CONFIG } from '../config/preload.config';
import { OFFLINE_CONFIG } from '../config/offline.config';
import {
  resolveAssetPlaybackUrl,
  queuePresignedUrls,
  isRelativeStoragePath,
  markPresignedUrlFailed,
  markPresignedUrlsVerified,
  getPresignedUrl,
} from '../lib/assetUrl';
import { baseURLApi } from '../config';
import { logger } from '../lib/logger';

/**
 * Check if URL is a presigned S3 URL
 */
const isPresignedUrl = (url: string): boolean => {
  return url.includes('X-Amz-Signature=') || url.includes('x-amz-signature=');
};

/**
 * Build proxy URL from storage key
 */
const buildProxyUrl = (storageKey: string): string => {
  const normalizedPath = storageKey.replace(/^\/+/, '');
  return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(normalizedPath)}`;
};
import type {
  PreloadPage,
  PreloadPageLink,
  PreloadElement,
} from '../types/preload';

interface UsePreloadOrchestratorOptions {
  pages: PreloadPage[];
  pageLinks: PreloadPageLink[];
  elements: PreloadElement[];
  currentPageId: string | null;
  pageHistory?: string[];
  enabled?: boolean;
  maxNeighborDepth?: number;
}

interface PreloadQueueItem {
  id: string;
  url: string;
  storageKey?: string; // Original storage key for presigned URL cache invalidation
  priority: number;
  assetType: 'image' | 'video' | 'audio' | 'transition' | 'other';
  pageId: string;
}

interface UsePreloadOrchestratorResult {
  isPreloading: boolean;
  preloadedUrls: Set<string>;
  queueLength: number;
  preloadAsset: (url: string, priority?: number) => void;
  clearQueue: () => void;
  getCachedBlobUrl: (url: string) => Promise<string | null>;
  isUrlPreloaded: (url: string) => Promise<boolean>;
  /** Instant lookup - returns decoded blob URL or null */
  getReadyBlobUrl: (url: string) => string | null;
}

/**
 * Generate a unique ID for preload jobs
 */
const generateJobId = (): string => {
  return `preload-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
};

/**
 * Check if a URL is already cached (checks both IndexedDB and Cache API)
 */
const isUrlCached = async (url: string): Promise<boolean> => {
  try {
    // StorageManager.hasAsset checks both IndexedDB (large files) and Cache API (small files)
    return StorageManager.hasAsset(url);
  } catch {
    return false;
  }
};

/**
 * Decode an image so it's ready to paint (no white flash)
 */
const decodeImage = async (url: string): Promise<void> => {
  return new Promise((resolve) => {
    const img = new Image();
    img.src = url;

    if (typeof img.decode === 'function') {
      img
        .decode()
        .then(() => {
          logger.info('[PRELOAD] Image decoded', { url: url.slice(-50) });
          resolve();
        })
        .catch(() => resolve()); // Resolve even on error
    } else {
      // Fallback: wait for load event
      img.onload = () => resolve();
      img.onerror = () => resolve();
    }
  });
};

/**
 * Check if URL is an image based on extension or content type
 */
const isImageUrl = (url: string): boolean => {
  const imageExtensions = [
    '.jpg',
    '.jpeg',
    '.png',
    '.gif',
    '.webp',
    '.avif',
    '.svg',
  ];
  const lowerUrl = url.toLowerCase();
  return imageExtensions.some((ext) => lowerUrl.includes(ext));
};

/**
 * Preload a single asset with progress tracking and Cache API storage
 */
const preloadWithProgress = async (
  url: string,
  jobId: string,
  assetId: string,
): Promise<void> => {
  // Emit start event
  downloadEventBus.emitPreloadStart({ jobId, assetId, url });

  try {
    const response = await fetch(url);

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const contentLength = response.headers.get('content-length');
    const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;

    // Clone response for caching (original will be consumed for progress tracking)
    const responseToCache = response.clone();

    if (!response.body) {
      // No streaming, just wait for response and cache it
      await response.blob();
      downloadEventBus.emitPreloadProgress({
        jobId,
        progress: 100,
        bytesLoaded: totalBytes,
        totalBytes,
      });

      // Store in Cache API
      if (typeof caches !== 'undefined') {
        const cache = await caches.open(OFFLINE_CONFIG.cacheNames.assets);
        await cache.put(url, responseToCache);
      }

      downloadEventBus.emitPreloadComplete({ jobId, assetId });
      return;
    }

    // Stream with progress and collect chunks for caching
    const reader = response.body.getReader();
    const chunks: Uint8Array[] = [];
    let bytesLoaded = 0;

    while (true) {
      const { done, value } = await reader.read();

      if (done) break;

      chunks.push(value);
      bytesLoaded += value.length;
      const progress = totalBytes > 0 ? (bytesLoaded / totalBytes) * 100 : 0;

      downloadEventBus.emitPreloadProgress({
        jobId,
        progress: Math.round(progress),
        bytesLoaded,
        totalBytes,
      });
    }

    // Store in Cache API after successful download
    if (typeof caches !== 'undefined') {
      const cache = await caches.open(OFFLINE_CONFIG.cacheNames.assets);
      // Create a new response from collected chunks
      const blob = new Blob(chunks as BlobPart[], {
        type:
          responseToCache.headers.get('content-type') ||
          'application/octet-stream',
      });
      const cachedResponse = new Response(blob, {
        status: 200,
        statusText: 'OK',
        headers: {
          'Content-Type':
            responseToCache.headers.get('content-type') ||
            'application/octet-stream',
          'Content-Length': String(bytesLoaded),
        },
      });
      await cache.put(url, cachedResponse);
    }

    downloadEventBus.emitPreloadComplete({ jobId, assetId });
  } catch (error) {
    downloadEventBus.emitPreloadError({
      jobId,
      assetId,
      error: error instanceof Error ? error.message : 'Unknown error',
    });
    throw error;
  }
};

export function usePreloadOrchestrator(
  options: UsePreloadOrchestratorOptions,
): UsePreloadOrchestratorResult {
  const {
    pages,
    pageLinks,
    elements,
    currentPageId,
    enabled = true,
    maxNeighborDepth = 1, // Only preload immediate neighbors by default
  } = options;

  const [isPreloading, setIsPreloading] = useState(false);
  const [preloadedUrls] = useState(() => new Set<string>());
  const [queueLength, setQueueLength] = useState(0);

  const queueRef = useRef<PreloadQueueItem[]>([]);
  const activeDownloadsRef = useRef(0);
  const isProcessingRef = useRef(false);
  const lastPreloadedPageRef = useRef<string | null>(null);
  const lastPreloadedLinksCountRef = useRef<number>(0);

  // Map of original URL → decoded blob URL (ready to display instantly)
  const readyBlobUrlsRef = useRef<Map<string, string>>(new Map());
  // Set of URLs that failed cache lookup (prevents infinite retry loops)
  const failedCacheLookupRef = useRef<Set<string>>(new Set());

  // Use neighbor graph for determining what to preload
  const neighborGraph = useNeighborGraph({
    pages,
    pageLinks,
    elements,
    maxDepth: maxNeighborDepth,
  });

  // Use network info for adaptive preloading
  const { networkInfo, recommendedConcurrency, shouldPreloadAggressively } =
    useNetworkAware();

  /**
   * Create a blob URL from cache and decode if image.
   * Stores the ready-to-display blob URL in readyBlobUrlsRef.
   * If storageKey is provided, also maps the storage key to the blob URL for canonical lookup.
   */
  const createReadyBlobUrl = useCallback(
    async (url: string, storageKey?: string): Promise<void> => {
      try {
        // Skip if we already know this URL is not in cache (prevents infinite loops)
        if (failedCacheLookupRef.current.has(url)) {
          return;
        }
        if (storageKey && failedCacheLookupRef.current.has(storageKey)) {
          return;
        }

        // Try multiple URL formats to handle key mismatches
        // (presigned URL vs proxy URL vs storage key)
        let blob = await StorageManager.getAsset(url);

        // If not found and we have a storage key, try that too
        if (!blob && storageKey && storageKey !== url) {
          blob = await StorageManager.getAsset(storageKey);
        }

        // Also try with the proxy URL format as fallback
        if (!blob && storageKey) {
          const proxyUrl = `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(storageKey.replace(/^\/+/, ''))}`;
          if (proxyUrl !== url) {
            blob = await StorageManager.getAsset(proxyUrl);
          }
        }

        if (!blob) {
          // Mark as failed to prevent repeated lookups
          failedCacheLookupRef.current.add(url);
          if (storageKey) {
            failedCacheLookupRef.current.add(storageKey);
          }
          logger.info('[PRELOAD] No blob found in cache', {
            url: url.slice(-50),
            storageKey: storageKey?.slice(-50),
          });
          return;
        }

        // Create blob URL
        const blobUrl = URL.createObjectURL(blob);

        // Decode if image (blob URL decode is fast - local data)
        if (isImageUrl(url)) {
          await decodeImage(blobUrl);
        }

        // Store ready blob URL keyed by download URL
        readyBlobUrlsRef.current.set(url, blobUrl);
        preloadedUrls.add(url);

        // Also map storage key for canonical lookup (most reliable)
        if (storageKey) {
          readyBlobUrlsRef.current.set(storageKey, blobUrl);
          preloadedUrls.add(storageKey);
        }

        logger.info('[PRELOAD] Asset ready', {
          url: url.slice(-50),
          storageKey: storageKey?.slice(-50),
          blobUrl: blobUrl.slice(0, 30),
        });
      } catch (error) {
        logger.error('[PRELOAD] Failed to create ready blob URL', {
          url: url.slice(-50),
          error: error instanceof Error ? error.message : 'unknown',
        });
      }
    },
    [preloadedUrls],
  );

  // Process the queue
  const processQueue = useCallback(async () => {
    if (isProcessingRef.current) return;
    if (!networkInfo.isOnline) return;
    if (queueRef.current.length === 0) {
      setIsPreloading(false);
      return;
    }

    isProcessingRef.current = true;
    setIsPreloading(true);

    const maxConcurrent = recommendedConcurrency;

    while (
      queueRef.current.length > 0 &&
      activeDownloadsRef.current < maxConcurrent
    ) {
      const item = queueRef.current.shift();
      if (!item) break;

      setQueueLength(queueRef.current.length);

      // Skip if already preloaded
      if (preloadedUrls.has(item.url)) {
        continue;
      }

      // Skip download if already cached, create blob URL and decode
      const cached = await isUrlCached(item.url);
      if (cached) {
        logger.info('[PRELOAD] Already cached', { url: item.url.slice(-50) });
        // Create blob URL and decode - makes asset ready to display instantly
        await createReadyBlobUrl(item.url, item.storageKey);
        continue;
      }

      activeDownloadsRef.current++;

      const jobId = generateJobId();
      logger.info('[PRELOAD] Starting download', {
        url: item.url.slice(-50),
        assetType: item.assetType,
      });

      preloadWithProgress(item.url, jobId, item.id)
        .then(async () => {
          logger.info('[PRELOAD] Download complete', {
            url: item.url.slice(-50),
          });
          // Clear failed cache lookup status since we just downloaded fresh
          failedCacheLookupRef.current.delete(item.url);
          if (item.storageKey) {
            failedCacheLookupRef.current.delete(item.storageKey);
          }
          await createReadyBlobUrl(item.url, item.storageKey);
          if (isPresignedUrl(item.url)) {
            markPresignedUrlsVerified();
          }
          // Map proxy URL and storage key to the same blob URL for fallback lookup
          if (item.storageKey) {
            const blobUrl = readyBlobUrlsRef.current.get(item.url);
            if (blobUrl) {
              // Map proxy URL for fallback compatibility
              const proxyUrl = buildProxyUrl(item.storageKey);
              readyBlobUrlsRef.current.set(proxyUrl, blobUrl);
              preloadedUrls.add(proxyUrl);
            }

            // Store in Cache API under storage key for post-refresh lookups
            if (typeof caches !== 'undefined') {
              try {
                const cache = await caches.open(
                  OFFLINE_CONFIG.cacheNames.assets,
                );
                const existingResponse = await cache.match(item.url);
                if (existingResponse) {
                  await cache.put(item.storageKey, existingResponse.clone());
                }
              } catch (e) {
                logger.warn('[PRELOAD] Failed to store under storage key', {
                  storageKey: item.storageKey,
                });
              }
            }
          }
        })
        .catch(async (err) => {
          logger.error('[PRELOAD] Download failed', {
            url: item.url.slice(-50),
            error: err?.message,
          });

          // If presigned URL failed (e.g., CORS), retry with proxy URL
          if (item.storageKey && isPresignedUrl(item.url)) {
            markPresignedUrlFailed(item.storageKey);
            const proxyUrl = buildProxyUrl(item.storageKey);
            logger.info('[PRELOAD] Retrying with proxy URL', {
              storageKey: item.storageKey.slice(-50),
              proxyUrl: proxyUrl.slice(-60),
            });

            try {
              await preloadWithProgress(proxyUrl, generateJobId(), item.id);
              logger.info('[PRELOAD] Proxy download complete', {
                url: proxyUrl.slice(-60),
              });
              await createReadyBlobUrl(proxyUrl, item.storageKey);
            } catch (retryErr) {
              logger.error('[PRELOAD] Proxy download also failed', {
                url: proxyUrl.slice(-60),
                error: retryErr instanceof Error ? retryErr.message : 'unknown',
              });
            }
          }
        })
        .finally(() => {
          activeDownloadsRef.current--;
          // Process more items
          if (queueRef.current.length > 0) {
            processQueue();
          } else if (activeDownloadsRef.current === 0) {
            setIsPreloading(false);
            isProcessingRef.current = false;
          }
        });
    }

    if (activeDownloadsRef.current === 0) {
      setIsPreloading(false);
      isProcessingRef.current = false;
    }
  }, [
    networkInfo.isOnline,
    preloadedUrls,
    recommendedConcurrency,
    createReadyBlobUrl,
  ]);

  // Add item to queue with priority sorting
  const addToQueue = useCallback(
    (item: PreloadQueueItem) => {
      // Skip if already in queue or preloaded
      if (
        preloadedUrls.has(item.url) ||
        queueRef.current.some((q) => q.url === item.url)
      ) {
        logger.info('[PRELOAD] Skipping (already queued/preloaded)', {
          url: item.url.slice(-50),
          assetType: item.assetType,
        });
        return;
      }

      logger.info('[PRELOAD] Adding to queue', {
        url: item.url.slice(-60),
        assetType: item.assetType,
        priority: item.priority,
        queueLength: queueRef.current.length + 1,
      });

      // Insert in priority order (higher priority first)
      const insertIndex = queueRef.current.findIndex(
        (q) => q.priority < item.priority,
      );

      if (insertIndex === -1) {
        queueRef.current.push(item);
      } else {
        queueRef.current.splice(insertIndex, 0, item);
      }

      setQueueLength(queueRef.current.length);
      processQueue();
    },
    [preloadedUrls, processQueue],
  );

  // Manual preload function
  const preloadAsset = useCallback(
    (url: string, priority = 100) => {
      addToQueue({
        id: generateJobId(),
        url,
        priority,
        assetType: 'other',
        pageId: currentPageId || '',
      });
    },
    [addToQueue, currentPageId],
  );

  // Clear queue
  const clearQueue = useCallback(() => {
    queueRef.current = [];
    setQueueLength(0);
  }, []);

  // Get a cached asset as a blob URL (for video playback)
  // StorageManager.getAsset checks both IndexedDB (large files ≥ 5MB) and Cache API (small files)
  const getCachedBlobUrl = useCallback(
    async (url: string): Promise<string | null> => {
      try {
        const blob = await StorageManager.getAsset(url);
        if (blob) {
          return URL.createObjectURL(blob);
        }
        return null;
      } catch {
        return null;
      }
    },
    [],
  );

  // Check if URL is preloaded (in cache)
  const isUrlPreloaded = useCallback(
    async (url: string): Promise<boolean> => {
      // First check in-memory set
      if (preloadedUrls.has(url)) return true;

      // Then check Cache API
      return isUrlCached(url);
    },
    [preloadedUrls],
  );

  // Instant lookup - returns decoded blob URL or null (O(1) Map lookup)
  const getReadyBlobUrl = useCallback((url: string): string | null => {
    return readyBlobUrlsRef.current.get(url) || null;
  }, []);

  // Cleanup blob URLs to prevent memory leaks
  const clearReadyBlobUrls = useCallback(() => {
    readyBlobUrlsRef.current.forEach((blobUrl) => {
      URL.revokeObjectURL(blobUrl);
    });
    readyBlobUrlsRef.current.clear();
  }, []);

  // Cleanup on unmount
  useEffect(() => {
    return () => clearReadyBlobUrls();
  }, [clearReadyBlobUrls]);

  // Initialize ready blob URLs from Cache API for current page's assets
  // This ensures getReadyBlobUrl works on the first render for both backgrounds and element icons
  useEffect(() => {
    if (!currentPageId) return;

    const currentPage = pages.find((p) => p.id === currentPageId);
    if (!currentPage) return;

    const initializeFromCache = async () => {
      // Collect background URLs
      const bgUrls = [
        currentPage.background_image_url,
        currentPage.background_video_url,
        currentPage.background_audio_url,
      ].filter(Boolean) as string[];

      // Collect element asset URLs (icons, images, etc.) from current page
      const currentPageElements = elements.filter(
        (el) => el.pageId === currentPageId,
      );
      const elementAssetUrls: string[] = [];
      currentPageElements.forEach((element) => {
        if (!element.content_json) return;
        try {
          const content =
            typeof element.content_json === 'string'
              ? JSON.parse(element.content_json)
              : element.content_json;

          // Extract URLs from known asset fields
          const urlFields = PRELOAD_CONFIG.assetFields.all as readonly string[];
          const checkObject = (obj: Record<string, unknown>) => {
            if (!obj || typeof obj !== 'object') return;
            for (const [key, value] of Object.entries(obj)) {
              if (typeof value === 'string' && value && urlFields.includes(key)) {
                elementAssetUrls.push(value);
              } else if (typeof value === 'object' && value !== null) {
                checkObject(value as Record<string, unknown>);
              }
            }
          };
          checkObject(content);
        } catch {
          // Ignore parse errors
        }
      });

      // Initialize all URLs from cache
      const allUrls = [...bgUrls, ...elementAssetUrls];

      for (const storagePath of allUrls) {
        // Skip if already in memory
        if (readyBlobUrlsRef.current.has(storagePath)) continue;

        const fullUrl = resolveAssetPlaybackUrl(storagePath);
        if (readyBlobUrlsRef.current.has(fullUrl)) continue;

        // Try to load from Cache API
        await createReadyBlobUrl(fullUrl, storagePath);
      }
    };

    initializeFromCache();
  }, [currentPageId, pages, elements, createReadyBlobUrl]);

  // React to page changes - preload neighbors
  useEffect(() => {
    if (!enabled || !currentPageId || !networkInfo.isOnline) {
      return;
    }

    // Skip if we already preloaded for this page with the same data
    // Re-preload if pageLinks count changed (data just loaded)
    const currentLinksCount = pageLinks.length;
    const samePageAndData =
      lastPreloadedPageRef.current === currentPageId &&
      lastPreloadedLinksCountRef.current === currentLinksCount;

    if (samePageAndData) {
      return;
    }
    lastPreloadedPageRef.current = currentPageId;
    lastPreloadedLinksCountRef.current = currentLinksCount;

    logger.info('[PRELOAD] Starting preload for page', {
      currentPageId,
      maxNeighborDepth,
    });

    // Get prioritized assets based on current page
    const assets = neighborGraph.getPrioritizedAssets(
      currentPageId,
      maxNeighborDepth,
    );

    logger.info('[PRELOAD] Found assets from neighbor graph', {
      assetCount: assets.length,
      assets: assets.map((a) => ({ type: a.assetType, url: a.url.slice(-50) })),
    });

    // Collect all raw storage paths that need presigning
    const storagePaths: string[] = [];
    const currentPage = pages.find((p) => p.id === currentPageId);

    if (
      currentPage?.background_image_url &&
      isRelativeStoragePath(currentPage.background_image_url)
    ) {
      storagePaths.push(currentPage.background_image_url);
    }
    if (
      currentPage?.background_video_url &&
      isRelativeStoragePath(currentPage.background_video_url)
    ) {
      storagePaths.push(currentPage.background_video_url);
    }
    if (
      currentPage?.background_audio_url &&
      isRelativeStoragePath(currentPage.background_audio_url)
    ) {
      storagePaths.push(currentPage.background_audio_url);
    }

    assets.forEach((asset) => {
      if (isRelativeStoragePath(asset.url)) {
        storagePaths.push(asset.url);
      }
    });

    // Always collect neighbor background URLs for presigning
    // This ensures instant navigation to neighbor pages
    const neighbors = neighborGraph.getNeighbors(currentPageId, 1);
    neighbors.forEach(({ pageId }) => {
      const page = pages.find((p) => p.id === pageId);
      if (
        page?.background_image_url &&
        isRelativeStoragePath(page.background_image_url)
      ) {
        storagePaths.push(page.background_image_url);
      }
      // Always collect neighbor video URLs for smooth transitions
      if (
        page?.background_video_url &&
        isRelativeStoragePath(page.background_video_url)
      ) {
        storagePaths.push(page.background_video_url);
      }
      // Also collect neighbor audio URLs
      if (
        page?.background_audio_url &&
        isRelativeStoragePath(page.background_audio_url)
      ) {
        storagePaths.push(page.background_audio_url);
      }
    });

    // Batch fetch presigned URLs, then add to queue
    // Helper to resolve URL - prefer presigned if available, else fallback to proxy
    const resolveUrl = (
      storageKey: string,
      presignedUrls: Record<string, string>,
    ): string => {
      // Use presigned URL if available (will be tested on actual download)
      if (presignedUrls[storageKey]) {
        return presignedUrls[storageKey];
      }
      // Fallback to resolveAssetPlaybackUrl (will use proxy)
      return resolveAssetPlaybackUrl(storageKey);
    };

    const addAssetsToQueue = (presignedUrls: Record<string, string> = {}) => {
      // Add background assets from current page
      if (currentPage?.background_image_url) {
        const storageKey = currentPage.background_image_url;
        const resolvedUrl = resolveUrl(storageKey, presignedUrls);
        if (resolvedUrl) {
          addToQueue({
            id: `bg-img-${currentPageId}`,
            url: resolvedUrl,
            storageKey: isRelativeStoragePath(storageKey)
              ? storageKey
              : undefined,
            priority: PRELOAD_CONFIG.priority.currentPage + 200,
            assetType: 'image',
            pageId: currentPageId,
          });
        }
      }
      if (currentPage?.background_video_url) {
        const storageKey = currentPage.background_video_url;
        const resolvedUrl = resolveUrl(storageKey, presignedUrls);
        if (resolvedUrl) {
          addToQueue({
            id: `bg-vid-${currentPageId}`,
            url: resolvedUrl,
            storageKey: isRelativeStoragePath(storageKey)
              ? storageKey
              : undefined,
            priority: PRELOAD_CONFIG.priority.currentPage + 150,
            assetType: 'video',
            pageId: currentPageId,
          });
        }
      }
      if (currentPage?.background_audio_url) {
        const storageKey = currentPage.background_audio_url;
        const resolvedUrl = resolveUrl(storageKey, presignedUrls);
        if (resolvedUrl) {
          addToQueue({
            id: `bg-aud-${currentPageId}`,
            url: resolvedUrl,
            storageKey: isRelativeStoragePath(storageKey)
              ? storageKey
              : undefined,
            priority: PRELOAD_CONFIG.priority.currentPage + 100,
            assetType: 'audio',
            pageId: currentPageId,
          });
        }
      }

      // Add element assets
      assets.forEach((asset) => {
        const storageKey = asset.url;
        const resolvedUrl = resolveUrl(storageKey, presignedUrls);
        if (resolvedUrl) {
          addToQueue({
            id: generateJobId(),
            url: resolvedUrl,
            storageKey: isRelativeStoragePath(storageKey)
              ? storageKey
              : undefined,
            priority: asset.priority,
            assetType: asset.assetType,
            pageId: asset.pageId,
          });
        }
      });

      // Always preload immediate neighbor backgrounds for smooth navigation
      // This is critical for instant page switches without white flash
      const neighbors = neighborGraph.getNeighbors(currentPageId, 1);
      neighbors.forEach(({ pageId }) => {
        const page = pages.find((p) => p.id === pageId);
        if (page?.background_image_url) {
          const storageKey = page.background_image_url;
          const resolvedUrl = resolveUrl(storageKey, presignedUrls);
          if (resolvedUrl) {
            addToQueue({
              id: `bg-img-${pageId}`,
              url: resolvedUrl,
              storageKey: isRelativeStoragePath(storageKey)
                ? storageKey
                : undefined,
              // Neighbor backgrounds get high priority (just below current page)
              priority: PRELOAD_CONFIG.priority.neighborBase + 100,
              assetType: 'image',
              pageId,
            });
          }
        }
        // Always preload neighbor videos for smooth page transitions
        if (page?.background_video_url) {
          const storageKey = page.background_video_url;
          const resolvedUrl = resolveUrl(storageKey, presignedUrls);
          if (resolvedUrl) {
            addToQueue({
              id: `bg-vid-${pageId}`,
              url: resolvedUrl,
              storageKey: isRelativeStoragePath(storageKey)
                ? storageKey
                : undefined,
              priority: PRELOAD_CONFIG.priority.neighborBase + 50,
              assetType: 'video',
              pageId,
            });
          }
        }
        // Always preload neighbor audio for seamless playback
        if (page?.background_audio_url) {
          const storageKey = page.background_audio_url;
          const resolvedUrl = resolveUrl(storageKey, presignedUrls);
          if (resolvedUrl) {
            addToQueue({
              id: `bg-aud-${pageId}`,
              url: resolvedUrl,
              storageKey: isRelativeStoragePath(storageKey)
                ? storageKey
                : undefined,
              priority: PRELOAD_CONFIG.priority.neighborBase + 30,
              assetType: 'audio',
              pageId,
            });
          }
        }
      });
    };

    // If there are storage paths to presign, fetch them first
    if (storagePaths.length > 0) {
      logger.info('[PRELOAD] Fetching presigned URLs', {
        count: storagePaths.length,
      });
      queuePresignedUrls(storagePaths)
        .then(() => {
          logger.info('[PRELOAD] Presigned URLs fetched, adding to queue');
          addAssetsToQueue();
        })
        .catch((error) => {
          logger.error(
            '[PRELOAD] Failed to fetch presigned URLs, falling back to proxy',
            {
              error: error?.message,
            },
          );
          // Fallback: add to queue without presigned URLs (will use backend proxy)
          addAssetsToQueue();
        });
    } else {
      // No storage paths to presign, add directly to queue
      addAssetsToQueue();
    }
  }, [
    enabled,
    currentPageId,
    networkInfo.isOnline,
    neighborGraph,
    pages,
    pageLinks,
    addToQueue,
    maxNeighborDepth,
  ]);

  return {
    isPreloading,
    preloadedUrls,
    queueLength,
    preloadAsset,
    clearQueue,
    getCachedBlobUrl,
    isUrlPreloaded,
    getReadyBlobUrl,
  };
}
