/**
 * DownloadManager
 *
 * Manages asset downloads with queue, retry, and progress tracking.
 * Adapted from hoboken's videoProcessingQueue pattern for frontend use.
 */

import { PRELOAD_CONFIG } from '../../config/preload.config';
import { OFFLINE_CONFIG } from '../../config/offline.config';
import { downloadEventBus } from './DownloadEventBus';
import { StorageManager } from './StorageManager';
import { OfflineDbManager } from '../offlineDb/OfflineDbManager';
import { extractStoragePath } from '../assetUrl';
import { logger } from '../logger';
import type {
  PreloadJobStatus,
  AssetVariantType,
  AssetType,
  DownloadQueueItem,
} from '../../types/offline';

interface DownloadJob {
  id: string;
  assetId: string;
  projectId: string;
  url: string;
  filename: string;
  variantType: AssetVariantType;
  assetType: AssetType;
  priority: number;
  status: PreloadJobStatus;
  progress: number;
  bytesLoaded: number;
  totalBytes: number;
  retryCount: number;
  addedAt: number;
  storageKey: string; // Canonical storage key for consistent caching
  createBlobUrl?: boolean; // Create decoded blob URL after download
  persist?: boolean; // Persist to IndexedDB for resume (default: true)
  maxBytes?: number; // Partial download limit (undefined = full download)
  isPartial?: boolean; // Whether this was a partial download (for tracking)
  abortController?: AbortController;
  resolve?: () => void;
  reject?: (error: Error) => void;
}

class DownloadManagerClass {
  private queue: DownloadJob[] = [];
  private activeDownloads: Map<string, DownloadJob> = new Map();
  private isPaused = false;
  private isProcessing = false;

  // Blob URL cache for instant lookup (storageKey → blobUrl)
  private readyBlobUrls: Map<string, string> = new Map();

  // Track partial downloads completed in this session (not persisted)
  // Prevents re-downloading same partial content on repeated page visits
  private partialDownloadsReady: Set<string> = new Set();

  private config = {
    maxConcurrent: PRELOAD_CONFIG.maxConcurrentDownloads,
    chunkSize: PRELOAD_CONFIG.videoChunkSize,
    maxRetries: PRELOAD_CONFIG.maxRetries,
    retryDelayMs: PRELOAD_CONFIG.retryDelayMs,
    largeFileThreshold: PRELOAD_CONFIG.largeFileThreshold,
  };

  /**
   * Add a download job to the queue
   */
  async addJob(params: {
    assetId: string;
    projectId: string;
    url: string;
    filename: string;
    variantType: AssetVariantType;
    assetType: AssetType;
    priority?: number;
    storageKey?: string; // Optional, will extract if not provided
    createBlobUrl?: boolean; // Create blob URL after download
    persist?: boolean; // Persist to IndexedDB for resume (default: true)
    maxBytes?: number; // Download limit in bytes (for partial preload)
  }): Promise<void> {
    const storageKey = params.storageKey || extractStoragePath(params.url);
    const isPartialDownload = params.maxBytes !== undefined;

    // For partial downloads, check session cache first (fast path)
    if (isPartialDownload && this.partialDownloadsReady.has(storageKey)) {
      logger.info('[DownloadManager] Partial download already ready (session)', {
        storageKey: storageKey.slice(-50),
      });
      return;
    }

    // Check cache status using getAssetInfo for smart handling
    const assetInfo = await StorageManager.getAssetInfo(storageKey);

    if (assetInfo?.exists) {
      if (isPartialDownload) {
        // For partial downloads, any cached version is sufficient
        this.partialDownloadsReady.add(storageKey);
        return;
      }

      if (!assetInfo.isPartial) {
        // Fully cached - create blob URL if requested
        if (params.createBlobUrl && !this.readyBlobUrls.has(storageKey)) {
          await this.createBlobUrlFromCache(storageKey);
        }
        return;
      }

      // Asset exists but is partial - full download requested
      // Continue to download the full asset
      logger.info('[DownloadManager] Upgrading partial to full download', {
        storageKey: storageKey.slice(-50),
      });
    }

    // Check if already in queue (use storageKey for deduplication)
    if (
      this.queue.some((j) => j.storageKey === storageKey) ||
      Array.from(this.activeDownloads.values()).some(
        (j) => j.storageKey === storageKey,
      )
    ) {
      return;
    }

    return new Promise((resolve, reject) => {
      // For partial downloads, don't persist and don't create blob URL
      // (video will play from presigned URL, browser handles buffering)
      const isPartialDownload = params.maxBytes !== undefined;
      const shouldPersist = isPartialDownload
        ? false
        : (params.persist ?? true);
      const shouldCreateBlobUrl = isPartialDownload
        ? false
        : (params.createBlobUrl ?? false);

      const job: DownloadJob = {
        id: `dl-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
        assetId: params.assetId,
        projectId: params.projectId,
        url: params.url,
        filename: params.filename,
        variantType: params.variantType,
        assetType: params.assetType,
        priority:
          params.priority ??
          this.calculatePriority(params.assetType, params.variantType),
        status: 'queued',
        progress: 0,
        bytesLoaded: 0,
        totalBytes: 0,
        retryCount: 0,
        addedAt: Date.now(),
        storageKey,
        createBlobUrl: shouldCreateBlobUrl,
        persist: shouldPersist,
        maxBytes: params.maxBytes,
        isPartial: isPartialDownload,
        resolve,
        reject,
      };

      // Persist to IndexedDB for resume capability (default true)
      if (job.persist !== false) {
        this.persistQueueItem(job);
      }

      // Insert in priority order (higher priority first)
      const insertIndex = this.queue.findIndex(
        (q) => q.priority < job.priority,
      );
      if (insertIndex === -1) {
        this.queue.push(job);
      } else {
        this.queue.splice(insertIndex, 0, job);
      }

      downloadEventBus.emitQueueUpdate();
      this.processQueue();
    });
  }

  /**
   * Calculate priority based on asset and variant type
   */
  private calculatePriority(
    assetType: AssetType,
    variantType: AssetVariantType,
  ): number {
    const typePriority = PRELOAD_CONFIG.priority.assetType[assetType] || 0;
    const variantPriority = PRELOAD_CONFIG.priority.variant[variantType] || 0;
    return typePriority + variantPriority;
  }

  /**
   * Persist queue item to IndexedDB
   */
  private async persistQueueItem(job: DownloadJob): Promise<void> {
    const queueItem: DownloadQueueItem = {
      id: job.id,
      projectId: job.projectId,
      assetId: job.assetId,
      url: job.url,
      filename: job.filename,
      status: job.status,
      priority: job.priority,
      retryCount: job.retryCount,
      bytesLoaded: job.bytesLoaded,
      totalBytes: job.totalBytes,
      addedAt: job.addedAt,
    };
    await OfflineDbManager.addToQueue(queueItem);
  }

  /**
   * Process the download queue
   */
  private async processQueue(): Promise<void> {
    if (this.isProcessing || this.isPaused) return;
    if (this.queue.length === 0 && this.activeDownloads.size === 0) return;

    this.isProcessing = true;

    while (
      !this.isPaused &&
      this.activeDownloads.size < this.config.maxConcurrent &&
      this.queue.length > 0
    ) {
      const job = this.queue.shift();
      if (!job) break;

      this.activeDownloads.set(job.url, job);
      this.downloadAsset(job);
    }

    this.isProcessing = false;
  }

  /**
   * Download a single asset with progress tracking
   */
  private async downloadAsset(job: DownloadJob): Promise<void> {
    job.status = 'downloading';
    job.abortController = new AbortController();

    await OfflineDbManager.updateQueueStatus(job.id, 'downloading');

    downloadEventBus.emitPreloadStart({
      jobId: job.id,
      assetId: job.assetId,
      url: job.url,
    });

    try {
      // Build request headers - use Range header for partial downloads
      const headers: HeadersInit = {};
      if (job.maxBytes) {
        headers['Range'] = `bytes=0-${job.maxBytes - 1}`;
        logger.info('[DownloadManager] Partial download requested', {
          url: job.url.slice(-50),
          maxBytes: job.maxBytes,
        });
      }

      const response = await fetch(job.url, {
        signal: job.abortController.signal,
        headers,
      });

      // Accept both 200 OK and 206 Partial Content
      if (!response.ok && response.status !== 206) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

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

      // For partial downloads, track if we reached the limit
      const isPartialResponse = response.status === 206 || job.maxBytes;

      let blob: Blob;

      if (response.body) {
        // Stream with progress tracking
        const reader = response.body.getReader();
        const chunks: BlobPart[] = [];
        let reachedLimit = false;

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

          chunks.push(value);
          job.bytesLoaded += value.length;

          // Check if we've reached the maxBytes limit
          if (job.maxBytes && job.bytesLoaded >= job.maxBytes) {
            reachedLimit = true;
            logger.info('[DownloadManager] Reached partial download limit', {
              bytesLoaded: job.bytesLoaded,
              maxBytes: job.maxBytes,
            });
            // Cancel the remaining download gracefully
            try {
              await reader.cancel();
            } catch {
              // Ignore cancel errors - stream may already be closed
            }
            break;
          }

          job.progress =
            job.totalBytes > 0
              ? Math.round((job.bytesLoaded / job.totalBytes) * 100)
              : 0;

          downloadEventBus.emitPreloadProgress({
            jobId: job.id,
            progress: job.progress,
            bytesLoaded: job.bytesLoaded,
            totalBytes: job.totalBytes,
          });

          // Only update queue progress if persisting
          if (job.persist !== false) {
            await OfflineDbManager.updateQueueProgress(
              job.id,
              job.bytesLoaded,
              job.totalBytes,
            );
          }
        }

        blob = new Blob(chunks, {
          type:
            response.headers.get('content-type') || 'application/octet-stream',
        });

        // For partial downloads, mark as complete even if we didn't get everything
        if (reachedLimit || isPartialResponse) {
          job.progress = 100; // Consider partial download as "complete"
        }
      } else {
        // No streaming, get blob directly
        blob = await response.blob();
        job.totalBytes = blob.size;
        job.bytesLoaded = blob.size;
        job.progress = 100;
      }

      // Store the asset using canonical storage key
      // Partial downloads are now stored with isPartial: true for offline mode awareness
      await StorageManager.storeAsset(job.storageKey, blob, {
        id: job.assetId,
        projectId: job.projectId,
        filename: job.filename,
        variantType: job.variantType,
        assetType: job.assetType,
        isPartial: job.isPartial || false,
      });

      if (job.isPartial) {
        // Mark partial download as ready in session cache
        this.partialDownloadsReady.add(job.storageKey);

        // Register with Service Worker for full-file caching during playback
        // When the browser fetches the full media, SW will cache it using the storage key
        this.registerUrlForCaching(job.url, job.storageKey);

        logger.info('[DownloadManager] Partial download complete (stored)', {
          storageKey: job.storageKey.slice(-50),
          bytesLoaded: job.bytesLoaded,
        });
      } else {
        // Create blob URL if requested (full downloads only)
        if (job.createBlobUrl) {
          await this.createBlobUrlFromCache(job.storageKey);
        }
      }

      // Mark as completed
      job.status = 'completed';
      if (job.persist !== false) {
        await OfflineDbManager.removeFromQueue(job.id);
      }

      downloadEventBus.emitPreloadComplete({
        jobId: job.id,
        assetId: job.assetId,
        storageKey: job.storageKey,
      });

      job.resolve?.();
    } catch (error) {
      if (job.abortController.signal.aborted) {
        // Download was cancelled
        job.status = 'paused';
        return;
      }

      job.retryCount++;
      const errorMessage =
        error instanceof Error ? error.message : 'Unknown error';

      if (job.retryCount < this.config.maxRetries) {
        // Retry with backoff
        job.status = 'queued';
        await OfflineDbManager.updateQueueStatus(job.id, 'queued');

        setTimeout(() => {
          this.queue.unshift(job);
          this.processQueue();
        }, this.config.retryDelayMs * job.retryCount);
      } else {
        // Max retries exceeded
        job.status = 'error';
        await OfflineDbManager.updateQueueStatus(job.id, 'error', errorMessage);

        downloadEventBus.emitPreloadError({
          jobId: job.id,
          assetId: job.assetId,
          error: errorMessage,
        });

        job.reject?.(error instanceof Error ? error : new Error(errorMessage));
      }
    } finally {
      this.activeDownloads.delete(job.url);
      downloadEventBus.emitQueueUpdate();
      this.processQueue();
    }
  }

  /**
   * Pause all downloads
   */
  pauseAll(): void {
    this.isPaused = true;
    this.activeDownloads.forEach((job) => {
      job.abortController?.abort();
      job.status = 'paused';
    });
    downloadEventBus.emitQueueUpdate();
  }

  /**
   * Resume all downloads
   */
  resumeAll(): void {
    this.isPaused = false;

    // Move paused jobs back to queue
    this.activeDownloads.forEach((job) => {
      if (job.status === 'paused') {
        job.status = 'queued';
        this.queue.unshift(job);
      }
    });
    this.activeDownloads.clear();

    downloadEventBus.emitQueueUpdate();
    this.processQueue();
  }

  /**
   * Cancel a specific download
   */
  cancelJob(jobId: string): void {
    // Check active downloads
    const entries = Array.from(this.activeDownloads.entries());
    for (const [url, job] of entries) {
      if (job.id === jobId) {
        job.abortController?.abort();
        this.activeDownloads.delete(url);
        OfflineDbManager.removeFromQueue(jobId);
        downloadEventBus.emitQueueUpdate();
        return;
      }
    }

    // Check queue
    const index = this.queue.findIndex((j) => j.id === jobId);
    if (index !== -1) {
      this.queue.splice(index, 1);
      OfflineDbManager.removeFromQueue(jobId);
      downloadEventBus.emitQueueUpdate();
    }
  }

  /**
   * Cancel all downloads for a project
   */
  cancelProjectDownloads(projectId: string): void {
    // Cancel active downloads
    const entries = Array.from(this.activeDownloads.entries());
    for (const [url, job] of entries) {
      if (job.projectId === projectId) {
        job.abortController?.abort();
        this.activeDownloads.delete(url);
      }
    }

    // Remove from queue
    this.queue = this.queue.filter((j) => j.projectId !== projectId);

    // Clear from IndexedDB
    OfflineDbManager.clearProjectQueue(projectId);
    downloadEventBus.emitQueueUpdate();
  }

  /**
   * Clear entire queue
   */
  clearQueue(): void {
    // Abort all active downloads
    this.activeDownloads.forEach((job) => {
      job.abortController?.abort();
    });
    this.activeDownloads.clear();

    // Clear queue
    this.queue = [];

    // Clear IndexedDB
    OfflineDbManager.clearQueue();
    downloadEventBus.emitQueueUpdate();
  }

  /**
   * Get current queue status
   */
  getStatus(): {
    queueLength: number;
    activeCount: number;
    isPaused: boolean;
  } {
    return {
      queueLength: this.queue.length,
      activeCount: this.activeDownloads.size,
      isPaused: this.isPaused,
    };
  }

  /**
   * Restore queue from IndexedDB (for resume after page reload)
   */
  async restoreQueue(): Promise<void> {
    const pendingItems = await OfflineDbManager.getPendingQueue();

    for (const item of pendingItems) {
      // Get canonical storage key
      const storageKey = extractStoragePath(item.url);

      // Skip if already downloaded
      const hasAsset = await StorageManager.hasAsset(storageKey);
      if (hasAsset) {
        await OfflineDbManager.removeFromQueue(item.id);
        continue;
      }

      // Re-add to queue
      const job: DownloadJob = {
        id: item.id,
        assetId: item.assetId,
        projectId: item.projectId,
        url: item.url,
        filename: item.filename,
        variantType: 'original',
        assetType: 'other',
        priority: item.priority,
        status: 'queued',
        progress: 0,
        bytesLoaded: item.bytesLoaded,
        totalBytes: item.totalBytes,
        retryCount: item.retryCount,
        addedAt: item.addedAt,
        storageKey,
        createBlobUrl: true, // Create blob URL for resumed downloads
        persist: true,
      };

      this.queue.push(job);
    }

    // Sort by priority
    this.queue.sort((a, b) => b.priority - a.priority);

    if (this.queue.length > 0) {
      downloadEventBus.emitQueueUpdate();
      this.processQueue();
    }
  }

  /**
   * Get a ready blob URL for instant display (O(1) lookup)
   */
  getReadyBlobUrl(url: string): string | null {
    const storageKey = extractStoragePath(url);
    return this.readyBlobUrls.get(storageKey) || null;
  }

  /**
   * Cache an externally fetched blob and register blob URL for instant lookup.
   * Use this when fetching via XHR (e.g., transition playback) to enable caching.
   */
  async cacheBlob(
    storageKey: string,
    blob: Blob,
    metadata: {
      assetType: AssetType;
      projectId?: string;
    },
  ): Promise<string> {
    // Store in Cache API / IndexedDB via existing StorageManager
    await StorageManager.storeAsset(storageKey, blob, {
      id: `cached-${storageKey}`,
      projectId: metadata.projectId || '',
      filename: storageKey.split('/').pop() || 'asset',
      variantType: 'original',
      assetType: metadata.assetType,
    });

    // Create blob URL and register for instant O(1) lookup
    const blobUrl = URL.createObjectURL(blob);
    this.readyBlobUrls.set(storageKey, blobUrl);

    // Emit event for consumers (existing pattern)
    downloadEventBus.emitBlobUrlReady({
      storageKey,
      blobUrl,
    });

    logger.info('[DownloadManager] Cached external blob', {
      storageKey: storageKey.slice(-50),
      size: blob.size,
    });

    return blobUrl;
  }

  /**
   * Create blob URL from cached asset and store in readyBlobUrls map
   */
  private async createBlobUrlFromCache(storageKey: string): Promise<void> {
    try {
      const blob = await StorageManager.getAsset(storageKey);
      if (!blob) {
        logger.info('[DownloadManager] No blob found for', {
          storageKey: storageKey.slice(-50),
        });
        return;
      }

      const blobUrl = URL.createObjectURL(blob);

      // Decode images to prevent white flash
      if (this.isImageUrl(storageKey)) {
        await this.decodeImage(blobUrl);
      }

      this.readyBlobUrls.set(storageKey, blobUrl);

      // Emit event for consumers
      downloadEventBus.emitBlobUrlReady({
        storageKey,
        blobUrl,
      });

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

  /**
   * Register a presigned URL → storage key mapping with the Service Worker.
   * This enables the SW to cache the full response when the browser fetches the media
   * during playback, using the canonical storage key instead of the expiring presigned URL.
   */
  private registerUrlForCaching(presignedUrl: string, storageKey: string): void {
    if (navigator.serviceWorker?.controller) {
      navigator.serviceWorker.controller.postMessage({
        type: 'REGISTER_CACHE_URL',
        payload: { presignedUrl, storageKey },
      });
      logger.info('[DownloadManager] Registered URL for SW caching', {
        storageKey: storageKey.slice(-40),
      });
    }
  }

  /**
   * Clear blob URLs and partial downloads cache (call on unmount to prevent memory leaks)
   */
  clearBlobUrls(): void {
    this.readyBlobUrls.forEach((blobUrl) => URL.revokeObjectURL(blobUrl));
    this.readyBlobUrls.clear();
    this.partialDownloadsReady.clear();

    // Clear SW URL mappings (optional, SW has its own cleanup interval)
    if (navigator.serviceWorker?.controller) {
      navigator.serviceWorker.controller.postMessage({
        type: 'CLEAR_URL_MAPPINGS',
      });
    }
  }

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

  /**
   * Decode image for instant display (prevents white flash)
   */
  private decodeImage(blobUrl: string): Promise<void> {
    return new Promise((resolve) => {
      const img = new Image();
      img.src = blobUrl;
      if (typeof img.decode === 'function') {
        img
          .decode()
          .then(() => resolve())
          .catch(() => resolve());
      } else {
        img.onload = () => resolve();
        img.onerror = () => resolve();
      }
    });
  }
}

// Singleton instance
export const downloadManager = new DownloadManagerClass();
