/**
 * 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 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;
  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;

  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;
  }): Promise<void> {
    // Check if already downloaded
    const hasAsset = await StorageManager.hasAsset(params.url);
    if (hasAsset) {
      return;
    }

    // Check if already in queue
    if (
      this.queue.some((j) => j.url === params.url) ||
      this.activeDownloads.has(params.url)
    ) {
      return;
    }

    return new Promise((resolve, reject) => {
      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(),
        resolve,
        reject,
      };

      // Persist to IndexedDB for resume capability
      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 {
      const response = await fetch(job.url, {
        signal: job.abortController.signal,
      });

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

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

      let blob: Blob;

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

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

          chunks.push(value);
          job.bytesLoaded += value.length;
          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,
          });

          await OfflineDbManager.updateQueueProgress(
            job.id,
            job.bytesLoaded,
            job.totalBytes,
          );
        }

        blob = new Blob(chunks, {
          type:
            response.headers.get('content-type') || 'application/octet-stream',
        });
      } else {
        // No streaming, get blob directly
        blob = await response.blob();
        job.totalBytes = blob.size;
        job.bytesLoaded = blob.size;
        job.progress = 100;
      }

      // Store the asset
      await StorageManager.storeAsset(job.url, blob, {
        id: job.assetId,
        projectId: job.projectId,
        filename: job.filename,
        variantType: job.variantType,
        assetType: job.assetType,
      });

      // Mark as completed
      job.status = 'completed';
      await OfflineDbManager.removeFromQueue(job.id);

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

      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) {
      // Skip if already downloaded
      const hasAsset = await StorageManager.hasAsset(item.url);
      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,
      };

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

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