/**
 * 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 { PRELOAD_CONFIG } from '../config/preload.config';
import { OFFLINE_CONFIG } from '../config/offline.config';
import { resolveAssetPlaybackUrl } from '../lib/assetUrl';
import { logger } from '../lib/logger';

interface Page {
  id: string;
  background_image_url?: string;
  background_video_url?: string;
}

interface Element {
  id: string;
  pageId?: string;
  element_type?: string;
  content_json?: string;
}

interface PageLink {
  id: string;
  from_pageId?: string;
  to_pageId?: string;
  transitionId?: string;
  is_active?: boolean;
}

interface UsePreloadOrchestratorOptions {
  pages: Page[];
  pageLinks: PageLink[];
  elements: Element[];
  currentPageId: string | null;
  pageHistory?: string[];
  enabled?: boolean;
  maxNeighborDepth?: number;
}

interface PreloadQueueItem {
  id: string;
  url: string;
  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>;
}

/**
 * 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 (simplified check)
 */
const isUrlCached = async (url: string): Promise<boolean> => {
  if (typeof caches === 'undefined') return false;

  try {
    const cacheNames = await caches.keys();
    for (const cacheName of cacheNames) {
      const cache = await caches.open(cacheName);
      const response = await cache.match(url);
      if (response) return true;
    }
    return false;
  } 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);
      }

      // Decode image so it's ready to paint (eliminates white flash)
      if (isImageUrl(url)) {
        await decodeImage(url);
      }

      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);
    }

    // Decode image so it's ready to paint (eliminates white flash)
    if (isImageUrl(url)) {
      await decodeImage(url);
    }

    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 = PRELOAD_CONFIG.neighborGraph.maxDepth,
  } = 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);

  // 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();

  // 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, but still decode images
      const cached = await isUrlCached(item.url);
      if (cached) {
        logger.info('[PRELOAD] Already cached', { url: item.url.slice(-50) });
        // Decode image even if cached (so it's ready to paint)
        if (isImageUrl(item.url)) {
          await decodeImage(item.url);
        }
        preloadedUrls.add(item.url);
        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(() => {
          logger.info('[PRELOAD] Download complete', { url: item.url.slice(-50) });
          preloadedUrls.add(item.url);
        })
        .catch((err) => {
          logger.error('[PRELOAD] Download failed', { url: item.url.slice(-50), error: err?.message });
        })
        .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]);

  // 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)
  const getCachedBlobUrl = useCallback(async (url: string): Promise<string | null> => {
    if (typeof caches === 'undefined') return null;

    try {
      const cache = await caches.open(OFFLINE_CONFIG.cacheNames.assets);
      const response = await cache.match(url);
      if (!response) return null;

      const blob = await response.blob();
      return URL.createObjectURL(blob);
    } 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]);

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

    // Add background assets from pages
    const currentPage = pages.find((p) => p.id === currentPageId);
    if (currentPage?.background_image_url) {
      const resolvedUrl = resolveAssetPlaybackUrl(
        currentPage.background_image_url,
      );
      if (resolvedUrl) {
        addToQueue({
          id: `bg-img-${currentPageId}`,
          url: resolvedUrl,
          priority: PRELOAD_CONFIG.priority.currentPage + 200,
          assetType: 'image',
          pageId: currentPageId,
        });
      }
    }
    if (currentPage?.background_video_url) {
      const resolvedUrl = resolveAssetPlaybackUrl(
        currentPage.background_video_url,
      );
      if (resolvedUrl) {
        addToQueue({
          id: `bg-vid-${currentPageId}`,
          url: resolvedUrl,
          priority: PRELOAD_CONFIG.priority.currentPage + 150,
          assetType: 'video',
          pageId: currentPageId,
        });
      }
    }

    // Add element assets
    assets.forEach((asset) => {
      const resolvedUrl = resolveAssetPlaybackUrl(asset.url);
      if (resolvedUrl) {
        addToQueue({
          id: generateJobId(),
          url: resolvedUrl,
          priority: asset.priority,
          assetType: asset.assetType,
          pageId: asset.pageId,
        });
      }
    });

    // If aggressive preloading, also preload neighbor backgrounds
    if (shouldPreloadAggressively) {
      const neighbors = neighborGraph.getNeighbors(currentPageId, 1);
      neighbors.forEach(({ pageId }) => {
        const page = pages.find((p) => p.id === pageId);
        if (page?.background_image_url) {
          const resolvedUrl = resolveAssetPlaybackUrl(page.background_image_url);
          if (resolvedUrl) {
            addToQueue({
              id: `bg-img-${pageId}`,
              url: resolvedUrl,
              priority: PRELOAD_CONFIG.priority.neighborBase,
              assetType: 'image',
              pageId,
            });
          }
        }
      });
    }
  }, [
    enabled,
    currentPageId,
    networkInfo.isOnline,
    neighborGraph,
    pages,
    pageLinks,
    addToQueue,
    shouldPreloadAggressively,
    maxNeighborDepth,
  ]);

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