/**
 * useOfflineMode Hook
 *
 * Manages offline mode state and project download functionality.
 * Uses frontend asset discovery (same as online preloading) for consistent behavior.
 * No backend manifest dependency - single source of truth in frontend.
 */

import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { downloadManager } from '../lib/offline/DownloadManager';
import { StorageManager } from '../lib/offline/StorageManager';
import { OfflineDbManager } from '../lib/offlineDb/OfflineDbManager';
import { downloadEventBus } from '../lib/offline/DownloadEventBus';
import { OFFLINE_CONFIG } from '../config/offline.config';
import {
  queuePresignedUrls,
  resolveAssetPlaybackUrl,
  isRelativeStoragePath,
} from '../lib/assetUrl';
import { extractPageLinksAndElements } from '../lib/extractPageLinks';
import { discoverProjectAssets, type AssetToCache } from '../lib/assetCache';
import { logger } from '../lib/logger';
import type {
  CachedAssetInfo,
  OfflineProject,
  ProjectOfflineStatus,
  ProjectDownloadProgressEvent,
  PreloadCompleteEvent,
  PreloadErrorEvent,
} from '../types/offline';
import type { PreloadPage } from '../types/preload';

interface UseOfflineModeOptions {
  projectId: string | null;
  projectSlug?: string;
  projectName?: string;
  /** Pages data for frontend asset discovery (required for offline download) */
  pages?: PreloadPage[];
  enabled?: boolean;
}

interface UseOfflineModeResult {
  // Status
  isOfflineCapable: boolean;
  isDownloaded: boolean;
  isDownloading: boolean;
  status: ProjectOfflineStatus;
  progress: number;
  downloadedAssets: number;
  totalAssets: number;
  downloadedBytes: number;
  totalBytes: number;
  error: string | null;

  // Actions
  startDownload: () => Promise<void>;
  pauseDownload: () => void;
  resumeDownload: () => void;
  cancelDownload: () => void;
  deleteOfflineData: () => Promise<void>;
  checkForUpdates: () => Promise<boolean>;

  // Info
  projectInfo: OfflineProject | null;
  estimatedSize: number;
  formatSize: (bytes: number) => string;
}

/**
 * Format bytes to human-readable string
 */
const formatBytes = (bytes: number): string => {
  if (bytes === 0) return '0 B';

  const k = 1024;
  const sizes = ['B', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));

  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
};

export function useOfflineMode(
  options: UseOfflineModeOptions,
): UseOfflineModeResult {
  const { projectId, projectSlug, projectName, pages, enabled = true } = options;

  const [projectInfo, setProjectInfo] = useState<OfflineProject | null>(null);
  const [discoveredAssets, setDiscoveredAssets] = useState<AssetToCache[]>([]);
  const [status, setStatus] = useState<ProjectOfflineStatus>('not_downloaded');
  const [progress, setProgress] = useState(0);
  const [downloadedAssets, setDownloadedAssets] = useState(0);
  const [totalAssets, setTotalAssets] = useState(0);
  const [downloadedBytes, setDownloadedBytes] = useState(0);
  const [totalBytes, setTotalBytes] = useState(0);
  const [error, setError] = useState<string | null>(null);
  const [isPaused, setIsPaused] = useState(false);

  // Track assets for event-driven progress
  const assetsRef = useRef<AssetToCache[]>([]);
  const downloadedCountRef = useRef(0);
  const downloadedBytesRef = useRef(0);

  // Check if offline mode is supported
  const isOfflineCapable = useMemo(() => {
    if (typeof window === 'undefined') return false;
    return 'serviceWorker' in navigator && 'caches' in window;
  }, []);

  // Load project offline status from IndexedDB
  useEffect(() => {
    if (!projectId || !enabled) return;

    const loadProjectInfo = async () => {
      const info = await OfflineDbManager.getProject(projectId);
      if (info) {
        setProjectInfo(info);
        setStatus(info.status);
        setDownloadedAssets(info.downloadedAssets);
        setTotalAssets(info.totalAssets);
        setDownloadedBytes(info.downloadedSizeBytes);
        setTotalBytes(info.totalSizeBytes);

        if (info.totalAssets > 0) {
          setProgress(
            Math.round((info.downloadedAssets / info.totalAssets) * 100),
          );
        }
      }
    };

    loadProjectInfo();
  }, [projectId, enabled]);

  // Listen for individual asset complete events for progress tracking
  useEffect(() => {
    if (!projectId) return;

    const handleComplete = (data: PreloadCompleteEvent) => {
      // Only track if we have discovered assets
      if (!assetsRef.current.length) return;

      // Find the asset to get its size
      const asset = assetsRef.current.find(
        (a) => a.storageKey === data.storageKey || `offline-${a.storageKey}` === data.assetId,
      );
      const assetSize = asset?.sizeBytes || 0;

      // Update counters
      downloadedCountRef.current += 1;
      downloadedBytesRef.current += assetSize;

      const downloaded = downloadedCountRef.current;
      const dlBytes = downloadedBytesRef.current;
      const total = assetsRef.current.length;
      const totalSize = assetsRef.current.reduce((sum, a) => sum + (a.sizeBytes || 0), 0);

      // Update state
      setDownloadedAssets(downloaded);
      setDownloadedBytes(dlBytes);

      const prog = Math.round((downloaded / total) * 100);
      setProgress(prog);

      // Emit project progress event
      downloadEventBus.emitProjectProgress({
        projectId,
        progress: prog,
        downloadedAssets: downloaded,
        totalAssets: total,
        downloadedBytes: dlBytes,
        totalBytes: totalSize,
      });

      // Update IndexedDB
      OfflineDbManager.updateProjectProgress(projectId, downloaded, dlBytes);

      // Check if complete
      if (downloaded >= total) {
        setStatus('downloaded');
        OfflineDbManager.updateProjectStatus(projectId, 'downloaded');
        downloadEventBus.emitProjectComplete({ projectId });
        logger.info('[useOfflineMode] Download complete', { projectId });
      }
    };

    const handleError = (data: PreloadErrorEvent) => {
      logger.error('[useOfflineMode] Asset download error', {
        assetId: data.assetId,
        error: data.error,
      });
    };

    const unsubComplete = downloadEventBus.on(
      OFFLINE_CONFIG.events.preloadComplete as Parameters<
        typeof downloadEventBus.on
      >[0],
      handleComplete as Parameters<typeof downloadEventBus.on>[1],
    );
    const unsubError = downloadEventBus.on(
      OFFLINE_CONFIG.events.preloadError as Parameters<
        typeof downloadEventBus.on
      >[0],
      handleError as Parameters<typeof downloadEventBus.on>[1],
    );

    return () => {
      unsubComplete();
      unsubError();
    };
  }, [projectId]);

  // Listen for project progress events (allows external components to sync state)
  useEffect(() => {
    if (!projectId) return;

    const handleProgress = (data: ProjectDownloadProgressEvent) => {
      if (data.projectId !== projectId) return;

      setProgress(data.progress);
      setDownloadedAssets(data.downloadedAssets);
      setTotalAssets(data.totalAssets);
      setDownloadedBytes(data.downloadedBytes);
      setTotalBytes(data.totalBytes);
    };

    return downloadEventBus.on(
      OFFLINE_CONFIG.events.projectDownloadProgress as Parameters<
        typeof downloadEventBus.on
      >[0],
      handleProgress as Parameters<typeof downloadEventBus.on>[1],
    );
  }, [projectId]);

  // Discover assets using frontend logic (same as online preloading)
  const discoverAssets = useCallback((): AssetToCache[] => {
    if (!pages || pages.length === 0) {
      logger.warn('[useOfflineMode] No pages provided for asset discovery');
      return [];
    }

    // Extract pageLinks and elements from all pages
    const { pageLinks, preloadElements } = extractPageLinksAndElements(pages);

    // Use shared asset discovery (same as online preload)
    const assets = discoverProjectAssets(pages, pageLinks, preloadElements);

    logger.info('[useOfflineMode] Discovered assets from pages', {
      pageCount: pages.length,
      linkCount: pageLinks.length,
      elementCount: preloadElements.length,
      assetCount: assets.length,
    });

    return assets;
  }, [pages]);

  // Start download
  const startDownload = useCallback(async (): Promise<void> => {
    if (!projectId || !enabled) return;

    setError(null);
    setStatus('downloading');
    setIsPaused(false);

    try {
      // Discover assets from pages (frontend-only, no backend call)
      const assets = discoverAssets();
      if (assets.length === 0) {
        throw new Error('No assets discovered. Make sure pages data is provided.');
      }

      // Store assets for event-driven progress tracking
      assetsRef.current = assets;
      setDiscoveredAssets(assets);
      setTotalAssets(assets.length);

      // Estimate total size (may not have exact sizes for all assets)
      const estimatedTotalSize = assets.reduce((sum, a) => sum + (a.sizeBytes || 0), 0);
      setTotalBytes(estimatedTotalSize);

      // Create or update project record
      const projectRecord: OfflineProject = {
        id: projectId,
        slug: projectSlug || '',
        name: projectName || '',
        status: 'downloading',
        totalAssets: assets.length,
        downloadedAssets: 0,
        totalSizeBytes: estimatedTotalSize,
        downloadedSizeBytes: 0,
        version: `v${Date.now()}`,
      };
      await OfflineDbManager.upsertProject(projectRecord);
      setProjectInfo(projectRecord);

      // Check storage quota
      const quota = await StorageManager.getStorageQuota();
      if (estimatedTotalSize > 0 && !quota.canStore(estimatedTotalSize)) {
        throw new Error('Insufficient storage space');
      }

      // Reset progress counters
      let downloadedCount = 0;
      let downloadedSize = 0;

      // First, check which assets are already fully cached (not partial)
      // Partial downloads from online preload need to be re-downloaded fully for offline
      const assetsToDownload: AssetToCache[] = [];
      for (const asset of assets) {
        const assetInfo: CachedAssetInfo | null = await StorageManager.getAssetInfo(asset.storageKey);

        if (assetInfo?.exists && !assetInfo.isPartial) {
          // Fully cached, skip
          downloadedCount++;
          downloadedSize += asset.sizeBytes || 0;
          logger.info('[useOfflineMode] Asset already fully cached', {
            storageKey: asset.storageKey.slice(-50),
          });
        } else if (assetInfo?.exists && assetInfo.isPartial) {
          // Partial download - need full download for offline
          logger.info('[useOfflineMode] Upgrading partial download for offline', {
            storageKey: asset.storageKey.slice(-50),
            partialSize: assetInfo.sizeBytes,
          });
          assetsToDownload.push(asset);
        } else {
          // Not cached at all
          assetsToDownload.push(asset);
        }
      }

      // Initialize counters for event-driven progress
      downloadedCountRef.current = downloadedCount;
      downloadedBytesRef.current = downloadedSize;

      logger.info('[useOfflineMode] Assets to download:', {
        total: assets.length,
        alreadyCached: downloadedCount,
        toDownload: assetsToDownload.length,
      });

      // Update initial progress
      setDownloadedAssets(downloadedCount);
      setDownloadedBytes(downloadedSize);

      if (downloadedCount === assets.length) {
        // All already downloaded
        setStatus('downloaded');
        setProgress(100);
        await OfflineDbManager.updateProjectStatus(projectId, 'downloaded');
        logger.info('[useOfflineMode] All assets already cached', {
          projectId,
        });
        return;
      }

      // Fetch presigned URLs for assets that need them
      const storagePaths = assetsToDownload
        .filter((a) => isRelativeStoragePath(a.originalUrl))
        .map((a) => a.storageKey);

      let presignedUrls: Record<string, string> = {};
      if (storagePaths.length > 0) {
        try {
          presignedUrls = await queuePresignedUrls(storagePaths);
          logger.info('[useOfflineMode] Fetched presigned URLs', {
            count: Object.keys(presignedUrls).length,
          });
        } catch (err) {
          logger.warn('[useOfflineMode] Failed to fetch presigned URLs, using proxy', {
            error: err instanceof Error ? err.message : 'unknown',
          });
        }
      }

      // Queue all remaining assets for parallel download
      // DownloadManager handles concurrency internally
      // Progress is tracked via event subscriptions (see useEffect above)
      for (const asset of assetsToDownload) {
        // Resolve download URL - prefer presigned, fallback to proxy
        const downloadUrl = presignedUrls[asset.storageKey] || resolveAssetPlaybackUrl(asset.storageKey);

        downloadManager
          .addJob({
            assetId: `offline-${asset.storageKey}`,
            projectId,
            url: downloadUrl,
            filename: asset.storageKey.split('/').pop() || 'asset',
            variantType: 'original',
            assetType: asset.assetType,
            priority:
              asset.assetType === 'image'
                ? 100
                : asset.assetType === 'video'
                  ? 50
                  : 75,
            storageKey: asset.storageKey,
            createBlobUrl: true, // Create blob URL for instant display
            persist: true, // Persist for resume after page refresh
          })
          .catch((err) => {
            // Errors handled by DownloadManager retry logic and events
            logger.error('[useOfflineMode] Asset download failed', {
              storageKey: asset.storageKey.slice(-50),
              error: err?.message,
            });
          });
      }

      logger.info('[useOfflineMode] Downloads queued, progress via events', {
        projectId,
        queued: assetsToDownload.length,
      });
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Download failed';
      setError(message);
      setStatus('error');
      await OfflineDbManager.updateProjectStatus(projectId, 'error');
    }
  }, [projectId, projectSlug, projectName, enabled, discoverAssets]);

  // Pause download
  const pauseDownload = useCallback(() => {
    downloadManager.pauseAll();
    setIsPaused(true);
  }, []);

  // Resume download
  const resumeDownload = useCallback(() => {
    downloadManager.resumeAll();
    setIsPaused(false);
  }, []);

  // Cancel download
  const cancelDownload = useCallback(() => {
    if (!projectId) return;

    downloadManager.cancelProjectDownloads(projectId);
    setStatus('not_downloaded');
    setProgress(0);
    setDownloadedAssets(0);
    setDownloadedBytes(0);
    setIsPaused(false);
    setError(null);

    // Reset refs
    assetsRef.current = [];
    downloadedCountRef.current = 0;
    downloadedBytesRef.current = 0;

    OfflineDbManager.deleteProject(projectId);
    setProjectInfo(null);
  }, [projectId]);

  // Delete offline data
  const deleteOfflineData = useCallback(async () => {
    if (!projectId) return;

    await StorageManager.deleteProjectAssets(projectId);
    await OfflineDbManager.deleteProject(projectId);

    setProjectInfo(null);
    setStatus('not_downloaded');
    setProgress(0);
    setDownloadedAssets(0);
    setTotalAssets(0);
    setDownloadedBytes(0);
    setTotalBytes(0);
  }, [projectId]);

  // Check for updates by comparing discovered assets with stored project
  const checkForUpdates = useCallback(async (): Promise<boolean> => {
    if (!projectId || !projectInfo || !pages) return false;

    try {
      const currentAssets = discoverAssets();

      // Simple check: if asset count changed, there are updates
      if (currentAssets.length !== projectInfo.totalAssets) {
        setStatus('outdated');
        await OfflineDbManager.updateProjectStatus(projectId, 'outdated');
        return true;
      }

      return false;
    } catch {
      return false;
    }
  }, [projectId, projectInfo, pages, discoverAssets]);

  // Computed values
  const isDownloaded = status === 'downloaded';
  const isDownloading = status === 'downloading' && !isPaused;
  const estimatedSize = discoveredAssets.reduce((sum, a) => sum + (a.sizeBytes || 0), 0) || totalBytes;

  return {
    isOfflineCapable,
    isDownloaded,
    isDownloading,
    status,
    progress,
    downloadedAssets,
    totalAssets,
    downloadedBytes,
    totalBytes,
    error,
    startDownload,
    pauseDownload,
    resumeDownload,
    cancelDownload,
    deleteOfflineData,
    checkForUpdates,
    projectInfo,
    estimatedSize,
    formatSize: formatBytes,
  };
}
