import type {
  BaseItemDto,
  MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client/models";
import * as Application from "expo-application";
import { Directory, File, Paths } from "expo-file-system";
import * as Notifications from "expo-notifications";
import { router } from "expo-router";
import { atom, useAtom } from "jotai";
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
} from "react";
import { useTranslation } from "react-i18next";
import { DeviceEventEmitter, Platform } from "react-native";
import { toast } from "sonner-native";
import { useHaptic } from "@/hooks/useHaptic";
import useImageStorage from "@/hooks/useImageStorage";
import { useInterval } from "@/hooks/useInterval";
import { useSettings } from "@/utils/atoms/settings";
import { getOrSetDeviceId } from "@/utils/device";
import useDownloadHelper from "@/utils/download";
import { getItemImage } from "@/utils/getItemImage";
import { dumpDownloadDiagnostics, writeToLog } from "@/utils/log";
import { storage } from "@/utils/mmkv";
import { fetchAndParseSegments } from "@/utils/segments";
import { generateTrickplayUrl, getTrickplayInfo } from "@/utils/trickplay";
import { Bitrate } from "../components/BitrateSelector";
import {
  DownloadedItem,
  DownloadsDatabase,
  JobStatus,
  TrickPlayData,
} from "./Downloads/types";
import { apiAtom } from "./JellyfinProvider";

const BackGroundDownloader = !Platform.isTV
  ? require("@kesha-antonov/react-native-background-downloader")
  : null;

// Cap progress at 99% to avoid showing 100% before the download is actually complete
const MAX_PROGRESS_BEFORE_COMPLETION = 99;

// Estimate the total download size in bytes for a job. If the media source
// provides a Size, use that. Otherwise, if we have a bitrate and run time
// (RunTimeTicks), approximate size = (bitrate bits/sec * seconds) / 8.
const calculateEstimatedSize = (p: JobStatus): number => {
  const size = p.mediaSource?.Size || 0;
  const maxBitrate = p.maxBitrate?.value;
  const runTimeTicks = (p.item?.RunTimeTicks || 0) as number;

  if (!size && maxBitrate && runTimeTicks > 0) {
    // Jellyfin RunTimeTicks are in 10,000,000 ticks per second
    const seconds = runTimeTicks / 10000000;
    if (seconds > 0) {
      // maxBitrate is in bits per second; convert to bytes
      return Math.round((maxBitrate / 8) * seconds);
    }
  }

  return size || 0;
};

// Calculate download speed in bytes/sec based on a job's last update time
// and previously recorded bytesDownloaded.
const calculateSpeed = (
  p: JobStatus,
  currentBytesDownloaded?: number,
): number | undefined => {
  // Prefer session-only deltas when available: lastSessionBytes + lastSessionUpdateTime
  const now = Date.now();

  if (p.lastSessionUpdateTime && p.lastSessionBytes !== undefined) {
    const last = new Date(p.lastSessionUpdateTime).getTime();
    const deltaTime = (now - last) / 1000;
    if (deltaTime > 0) {
      const current =
        currentBytesDownloaded ?? p.bytesDownloaded ?? p.lastSessionBytes;
      const deltaBytes = current - p.lastSessionBytes;
      if (deltaBytes > 0) return deltaBytes / deltaTime;
    }
  }

  // Fallback to total-based deltas for compatibility
  if (!p.lastProgressUpdateTime || p.bytesDownloaded === undefined)
    return undefined;
  const last = new Date(p.lastProgressUpdateTime).getTime();
  const deltaTime = (now - last) / 1000;
  if (deltaTime <= 0) return undefined;
  const prev = p.bytesDownloaded || 0;
  const current = currentBytesDownloaded ?? prev;
  const deltaBytes = current - prev;
  if (deltaBytes <= 0) return undefined;
  return deltaBytes / deltaTime;
};

export const processesAtom = atom<JobStatus[]>([]);
const DOWNLOADS_DATABASE_KEY = "downloads.v2.json";

const DownloadContext = createContext<ReturnType<
  typeof useDownloadProvider
> | null>(null);

function useDownloadProvider() {
  const { t } = useTranslation();
  const [api] = useAtom(apiAtom);
  const { saveSeriesPrimaryImage } = useDownloadHelper();
  const { saveImage } = useImageStorage();
  const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
  const { settings } = useSettings();
  const successHapticFeedback = useHaptic("success");

  // Set up global download complete listener for debugging
  useEffect(() => {
    const listener = DeviceEventEmitter.addListener(
      "downloadComplete",
      (data) => {
        console.log("🔥 GLOBAL TEST LISTENER received downloadComplete:", data);
      },
    );

    return () => {
      listener.remove();
    };
  }, []);

  // Generate notification content based on item type
  const getNotificationContent = useCallback(
    (item: BaseItemDto, isSuccess: boolean) => {
      if (item.Type === "Episode") {
        const season = item.ParentIndexNumber
          ? String(item.ParentIndexNumber).padStart(2, "0")
          : "??";
        const episode = item.IndexNumber
          ? String(item.IndexNumber).padStart(2, "0")
          : "??";
        const subtitle = `${item.Name} - [S${season}E${episode}] (${item.SeriesName})`;

        return {
          title: isSuccess ? "Download complete" : "Download failed",
          body: subtitle,
        };
      } else if (item.Type === "Movie") {
        const year = item.ProductionYear ? ` (${item.ProductionYear})` : "";
        const subtitle = `${item.Name}${year}`;

        return {
          title: isSuccess ? "Download complete" : "Download failed",
          body: subtitle,
        };
      } else {
        // Fallback for other types
        return {
          title: isSuccess
            ? t("home.downloads.toasts.download_completed_for_item", {
                item: item.Name,
              })
            : t("home.downloads.toasts.download_failed_for_item", {
                item: item.Name,
              }),
          body: item.Name || "Unknown item",
        };
      }
    },
    [t],
  );

  // Send local notification for download events
  const sendDownloadNotification = useCallback(
    async (title: string, body: string, data?: Record<string, any>) => {
      if (Platform.isTV) return;

      try {
        await Notifications.scheduleNotificationAsync({
          content: {
            title,
            body,
            data,
            ...(Platform.OS === "android" && { channelId: "downloads" }),
          },
          trigger: null, // Show immediately
        });
      } catch (error) {
        console.error("Failed to send notification:", error);
      }
    },
    [],
  );

  /// Cant use the background downloader callback. As its not triggered if size is unknown.
  const updateProgress = async () => {
    const tasks = await BackGroundDownloader.checkForExistingDownloads();
    if (!tasks || tasks.length === 0) {
      return;
    }

    console.log(`[UPDATE_PROGRESS] Checking ${tasks.length} active tasks`);

    // check if processes are missing
    setProcesses((processes) => {
      const missingProcesses = tasks
        .filter((t: any) => t.metadata && !processes.some((p) => p.id === t.id))
        .map((t: any) => {
          return t.metadata as JobStatus;
        });

      const currentProcesses = [...processes, ...missingProcesses];
      const updatedProcesses = currentProcesses.map((p) => {
        // Enhanced filtering to prevent iOS zombie task interference
        // Only update progress for downloads that are actively downloading
        if (p.status !== "downloading") {
          return p;
        }

        // Find task for this process
        const task = tasks.find((s: any) => s.id === p.id);

        if (!task) {
          // ORPHANED DOWNLOAD CHECK: Task disappeared, but was it because it completed?
          // This handles the race condition where download finishes between polling intervals
          if (p.progress >= 90) {
            // Lower threshold to catch more cases
            console.log(
              `[UPDATE_PROGRESS] Orphaned download detected for ${p.item.Name} at ${p.progress.toFixed(1)}%, checking file...`,
            );
            const filename = generateFilename(p.item);
            const videoFile = new File(Paths.document, `${filename}.mp4`);

            if (videoFile.exists && videoFile.size > 0) {
              console.log(
                `[UPDATE_PROGRESS] Orphaned download complete! File size: ${videoFile.size}, marking as complete`,
              );
              return {
                ...p,
                progress: 100,
                speed: 0,
                bytesDownloaded: videoFile.size,
                lastProgressUpdateTime: new Date(),
                estimatedTotalSizeBytes: videoFile.size,
                lastSessionBytes: videoFile.size,
                lastSessionUpdateTime: new Date(),
                status: "completed" as const,
              };
            } else {
              console.warn(
                `[UPDATE_PROGRESS] Orphaned download at ${p.progress.toFixed(1)}% but file not found. Keeping current state.`,
              );
            }
          }
          return p; // No task found, keep current state
        }
        /* 
        // TODO: Uncomment this block to re-enable iOS zombie task detection
        // iOS: Extra validation to prevent zombie task interference
        if (Platform.OS === "ios") {
          // Check if we have multiple tasks for same ID (zombie detection)
          const tasksForId = tasks.filter((t: any) => t.id === p.id);
          if (tasksForId.length > 1) {
            console.warn(
              `[UPDATE] Detected ${tasksForId.length} zombie tasks for ${p.id}, ignoring progress update`,
            );
            return p; // Don't update progress from potentially conflicting tasks
          }

          // If task state looks suspicious (e.g., iOS task stuck in background), be conservative
          if (
            task.state &&
            ["SUSPENDED", "PAUSED"].includes(task.state) &&
            p.status === "downloading"
          ) {
            console.warn(
              `[UPDATE] Task ${p.id} has suspicious state ${task.state}, ignoring progress update`,
            );
            return p;
          }
        }
        */

        if (task && p.status === "downloading") {
          const estimatedSize = calculateEstimatedSize(p);
          let progress = p.progress;

          // If we have a pausedProgress snapshot then merge current session
          // progress into it. We accept pausedProgress === 0 as valid because
          // users can pause immediately after starting.
          if (p.pausedProgress !== undefined) {
            const totalBytesDownloaded =
              (p.pausedBytes ?? 0) + task.bytesDownloaded;

            // Calculate progress based on total bytes downloaded vs estimated size
            progress =
              estimatedSize > 0
                ? (totalBytesDownloaded / estimatedSize) * 100
                : 0;

            // Use the total accounted bytes when computing speed so the
            // displayed speed and progress remain consistent after resume.
            const speed = calculateSpeed(p, totalBytesDownloaded);

            return {
              ...p,
              progress: Math.min(progress, MAX_PROGRESS_BEFORE_COMPLETION),
              speed,
              bytesDownloaded: totalBytesDownloaded,
              lastProgressUpdateTime: new Date(),
              estimatedTotalSizeBytes: estimatedSize,
              // Set session bytes to total bytes downloaded
              lastSessionBytes: totalBytesDownloaded,
              lastSessionUpdateTime: new Date(),
            };
          } else {
            if (estimatedSize > 0) {
              progress = (100 / estimatedSize) * task.bytesDownloaded;
            }
            if (progress >= 100) {
              progress = MAX_PROGRESS_BEFORE_COMPLETION;
            }
            const speed = calculateSpeed(p, task.bytesDownloaded);

            console.log(
              `[UPDATE_PROGRESS] Task ${p.item.Name}: ${progress.toFixed(1)}% (${task.bytesDownloaded}/${estimatedSize} bytes), state: ${task.state}`,
            );

            // WORKAROUND: Check if download is actually complete by checking file existence
            // This handles cases where the .done() callback doesn't fire (unknown content length, simulator issues, etc.)
            if (progress >= 90 && task.state === "DONE") {
              console.log(
                `[UPDATE_PROGRESS] Task appears complete (state=DONE), checking file...`,
              );
              const filename = generateFilename(p.item);
              const videoFile = new File(Paths.document, `${filename}.mp4`);

              console.log(
                `[UPDATE_PROGRESS] Looking for file at: ${videoFile.uri}`,
              );
              console.log(
                `[UPDATE_PROGRESS] Paths.document.uri: ${Paths.document.uri}`,
              );
              console.log(`[UPDATE_PROGRESS] File exists: ${videoFile.exists}`);
              console.log(`[UPDATE_PROGRESS] File size: ${videoFile.size}`);

              if (videoFile.exists && videoFile.size > 0) {
                console.log(
                  `[UPDATE_PROGRESS] File exists with size ${videoFile.size}, marking as complete!`,
                );
                // Mark as complete by setting status - this will trigger removal from processes
                return {
                  ...p,
                  progress: 100,
                  speed: 0,
                  bytesDownloaded: videoFile.size,
                  lastProgressUpdateTime: new Date(),
                  estimatedTotalSizeBytes: videoFile.size,
                  lastSessionBytes: videoFile.size,
                  lastSessionUpdateTime: new Date(),
                  status: "completed" as const,
                };
              } else {
                console.warn(
                  `[UPDATE_PROGRESS] File not found or empty! Task state=${task.state}, progress=${progress}%`,
                );
              }
            }

            return {
              ...p,
              progress,
              speed,
              bytesDownloaded: task.bytesDownloaded,
              lastProgressUpdateTime: new Date(),
              estimatedTotalSizeBytes: estimatedSize,
              lastSessionBytes: task.bytesDownloaded,
              lastSessionUpdateTime: new Date(),
            };
          }
        }
        return p;
      });

      return updatedProcesses;
    });
  };

  useInterval(updateProgress, 1000);

  const getDownloadedItemById = (id: string): DownloadedItem | undefined => {
    const db = getDownloadsDatabase();

    // Check movies first
    if (db.movies[id]) {
      console.log(`[DB] Found movie with ID: ${id}`);
      return db.movies[id];
    }

    // Check episodes
    for (const series of Object.values(db.series)) {
      for (const season of Object.values(series.seasons)) {
        for (const episode of Object.values(season.episodes)) {
          if (episode.item.Id === id) {
            console.log(`[DB] Found episode with ID: ${id}`);
            return episode;
          }
        }
      }
    }

    console.log(`[DB] No item found with ID: ${id}`);
    // Check other media types
    if (db.other?.[id]) {
      return db.other[id];
    }

    return undefined;
  };

  const updateProcess = useCallback(
    (
      processId: string,
      updater:
        | Partial<JobStatus>
        | ((current: JobStatus) => Partial<JobStatus>),
    ) => {
      setProcesses((prev) =>
        prev.map((p) => {
          if (p.id !== processId) return p;
          const newStatus =
            typeof updater === "function" ? updater(p) : updater;
          return {
            ...p,
            ...newStatus,
          };
        }),
      );
    },
    [setProcesses],
  );

  const authHeader = useMemo(() => {
    return api?.accessToken;
  }, [api]);

  const APP_CACHE_DOWNLOAD_DIRECTORY = new Directory(
    Paths.cache,
    `${Application.applicationId}/Downloads/`,
  );

  const getDownloadsDatabase = (): DownloadsDatabase => {
    const file = storage.getString(DOWNLOADS_DATABASE_KEY);
    if (file) {
      const db = JSON.parse(file) as DownloadsDatabase;
      return db;
    }
    return { movies: {}, series: {}, other: {} }; // Initialize other media types storage
  };

  const getDownloadedItems = useCallback(() => {
    const db = getDownloadsDatabase();
    const movies = Object.values(db.movies);
    const episodes = Object.values(db.series).flatMap((series) =>
      Object.values(series.seasons).flatMap((season) =>
        Object.values(season.episodes),
      ),
    );
    const otherItems = Object.values(db.other || {});
    const allItems = [...movies, ...episodes, ...otherItems];
    return allItems;
  }, []);

  const saveDownloadsDatabase = (db: DownloadsDatabase) => {
    const movieCount = Object.keys(db.movies).length;
    const seriesCount = Object.keys(db.series).length;
    console.log(
      `[DB] Saving database: ${movieCount} movies, ${seriesCount} series`,
    );
    storage.set(DOWNLOADS_DATABASE_KEY, JSON.stringify(db));
    console.log(`[DB] Database saved successfully to MMKV`);
  };

  /** Generates a filename for a given item */
  const generateFilename = (item: BaseItemDto): string => {
    let rawFilename = "";
    if (item.Type === "Movie" && item.Name) {
      rawFilename = `${item.Name}`;
    } else if (
      item.Type === "Episode" &&
      item.SeriesName &&
      item.ParentIndexNumber !== undefined &&
      item.IndexNumber !== undefined
    ) {
      const season = String(item.ParentIndexNumber).padStart(2, "0");
      const episode = String(item.IndexNumber).padStart(2, "0");
      rawFilename = `${item.SeriesName} S${season}E${episode} ${item.Name}`;
    } else {
      // Fallback to a unique name if data is missing
      rawFilename = `${item.Name || "video"} ${item.Id}`;
    }
    // Sanitize the entire string to remove illegal characters
    return rawFilename.replace(/[\\/:*?"<>|\s]/g, "_");
  };

  /**
   * Downloads the trickplay images for a given item.
   * @param item - The item to download the trickplay images for.
   * @returns The path to the trickplay images.
   */
  const downloadTrickplayImages = async (
    item: BaseItemDto,
  ): Promise<TrickPlayData | undefined> => {
    const trickplayInfo = getTrickplayInfo(item);
    if (!api || !trickplayInfo || !item.Id) {
      return undefined;
    }

    const filename = generateFilename(item);
    const trickplayDir = new Directory(Paths.document, `${filename}_trickplay`);
    trickplayDir.create({ intermediates: true });
    let totalSize = 0;

    for (let index = 0; index < trickplayInfo.totalImageSheets; index++) {
      const url = generateTrickplayUrl(item, index);
      if (!url) continue;
      const destination = new File(trickplayDir, `${index}.jpg`);
      try {
        await File.downloadFileAsync(url, destination);
        totalSize += destination.size;
      } catch (e) {
        console.error(
          `Failed to download trickplay image ${index} for item ${item.Id}`,
          e,
        );
      }
    }

    return { path: trickplayDir.uri, size: totalSize };
  };

  /**
   * Downloads and links external subtitles to the media source.
   * @param mediaSource - The media source to download the subtitles for.
   */
  const downloadAndLinkSubtitles = async (
    mediaSource: MediaSourceInfo,
    item: BaseItemDto,
  ) => {
    const externalSubtitles = mediaSource.MediaStreams?.filter(
      (stream) =>
        stream.Type === "Subtitle" && stream.DeliveryMethod === "External",
    );
    if (externalSubtitles && api) {
      await Promise.all(
        externalSubtitles.map(async (subtitle) => {
          const url = api.basePath + subtitle.DeliveryUrl;
          const filename = generateFilename(item);
          const destination = new File(
            Paths.document,
            `${filename}_subtitle_${subtitle.Index}`,
          );
          await File.downloadFileAsync(url, destination);
          subtitle.DeliveryUrl = destination.uri;
        }),
      );
    }
  };

  /**
   * Starts a download for a given process.
   * @param process - The process to start the download for.
   */
  const startDownload = useCallback(
    async (process: JobStatus) => {
      if (!process?.item.Id || !authHeader) throw new Error("No item id");

      // Enhanced cleanup for existing tasks to prevent duplicates
      try {
        const allTasks = await BackGroundDownloader.checkForExistingDownloads();
        const existingTasks = allTasks?.filter((t: any) => t.id === process.id);

        if (existingTasks && existingTasks.length > 0) {
          console.log(
            `[START] Found ${existingTasks.length} existing task(s) for ${process.id}, cleaning up...`,
          );

          for (let i = 0; i < existingTasks.length; i++) {
            const existingTask = existingTasks[i];
            console.log(
              `[START] Cleaning up task ${i + 1}/${existingTasks.length} for ${process.id}`,
            );

            try {
              /* 
              // TODO: Uncomment this block to re-enable iOS-specific cleanup
              // iOS: More aggressive cleanup sequence
              if (Platform.OS === "ios") {
                try {
                  await existingTask.pause();
                  await new Promise((resolve) => setTimeout(resolve, 50));
                } catch (_pauseErr) {
                  // Ignore pause errors
                }

                await existingTask.stop();
                await new Promise((resolve) => setTimeout(resolve, 50));

                // Multiple complete handler calls to ensure cleanup
                BackGroundDownloader.completeHandler(process.id);
                await new Promise((resolve) => setTimeout(resolve, 25));
              } else {
              */

              // Simple cleanup for all platforms (currently Android only)
              await existingTask.stop();
              BackGroundDownloader.completeHandler(process.id);

              /* } // End of iOS block - uncomment when re-enabling iOS functionality */

              console.log(
                `[START] Successfully cleaned up task ${i + 1} for ${process.id}`,
              );
            } catch (taskError) {
              console.warn(
                `[START] Failed to cleanup task ${i + 1} for ${process.id}:`,
                taskError,
              );
            }
          }

          // Cleanup delay (simplified for Android)
          const cleanupDelay = 200; // Platform.OS === "ios" ? 500 : 200;
          await new Promise((resolve) => setTimeout(resolve, cleanupDelay));
          console.log(`[START] Cleanup completed for ${process.id}`);
        }
      } catch (error) {
        console.warn(
          `[START] Failed to check/cleanup existing tasks for ${process.id}:`,
          error,
        );
      }

      updateProcess(process.id, {
        speed: undefined,
        status: "downloading",
        progress: process.progress || 0, // Preserve existing progress for resume
      });

      if (!BackGroundDownloader) {
        throw new Error("Background downloader not available");
      }

      BackGroundDownloader.setConfig({
        isLogsEnabled: true, // Enable logs to debug
        progressInterval: 500,
        headers: {
          Authorization: authHeader,
        },
      });
      const filename = generateFilename(process.item);
      const videoFile = new File(Paths.document, `${filename}.mp4`);
      const videoFilePath = videoFile.uri;
      console.log(`[DOWNLOAD] Starting download for ${filename}`);
      console.log(`[DOWNLOAD] Destination path: ${videoFilePath}`);

      BackGroundDownloader.download({
        id: process.id,
        url: process.inputUrl,
        destination: videoFilePath,
        metadata: process,
      });
    },
    [authHeader, sendDownloadNotification, getNotificationContent],
  );

  const manageDownloadQueue = useCallback(() => {
    // Handle completed downloads (workaround for when .done() callback doesn't fire)
    const completedDownloads = processes.filter(
      (p) => p.status === "completed",
    );
    for (const completedProcess of completedDownloads) {
      console.log(
        `[QUEUE] Processing completed download: ${completedProcess.item.Name}`,
      );

      // Save to database
      (async () => {
        try {
          const filename = generateFilename(completedProcess.item);
          const videoFile = new File(Paths.document, `${filename}.mp4`);
          const videoFilePath = videoFile.uri;
          const videoFileSize = videoFile.size;

          console.log(`[QUEUE] Saving completed download to database`);
          console.log(`[QUEUE] Video file path: ${videoFilePath}`);
          console.log(`[QUEUE] Video file size: ${videoFileSize}`);
          console.log(`[QUEUE] Video file exists: ${videoFile.exists}`);

          if (!videoFile.exists) {
            console.error(
              `[QUEUE] Cannot save - video file does not exist at ${videoFilePath}`,
            );
            removeProcess(completedProcess.id);
            return;
          }

          const trickPlayData = await downloadTrickplayImages(
            completedProcess.item,
          );
          const db = getDownloadsDatabase();
          const { item, mediaSource } = completedProcess;

          // Only download external subtitles for non-transcoded streams.
          if (!mediaSource.TranscodingUrl) {
            await downloadAndLinkSubtitles(mediaSource, item);
          }

          const { introSegments, creditSegments } = await fetchAndParseSegments(
            item.Id!,
            api!,
          );

          const downloadedItem: DownloadedItem = {
            item,
            mediaSource,
            videoFilePath,
            videoFileSize,
            videoFileName: `${filename}.mp4`,
            trickPlayData,
            userData: {
              audioStreamIndex: 0,
              subtitleStreamIndex: 0,
            },
            introSegments,
            creditSegments,
          };

          if (item.Type === "Movie" && item.Id) {
            db.movies[item.Id] = downloadedItem;
          } else if (
            item.Type === "Episode" &&
            item.SeriesId &&
            item.ParentIndexNumber !== undefined &&
            item.ParentIndexNumber !== null &&
            item.IndexNumber !== undefined &&
            item.IndexNumber !== null
          ) {
            if (!db.series[item.SeriesId]) {
              const seriesInfo: Partial<BaseItemDto> = {
                Id: item.SeriesId,
                Name: item.SeriesName,
                Type: "Series",
              };
              db.series[item.SeriesId] = {
                seriesInfo: seriesInfo as BaseItemDto,
                seasons: {},
              };
            }

            const seasonNumber = item.ParentIndexNumber;
            if (!db.series[item.SeriesId].seasons[seasonNumber]) {
              db.series[item.SeriesId].seasons[seasonNumber] = {
                episodes: {},
              };
            }

            const episodeNumber = item.IndexNumber;
            db.series[item.SeriesId].seasons[seasonNumber].episodes[
              episodeNumber
            ] = downloadedItem;
          } else if (item.Id) {
            // Handle other media types
            if (!db.other) db.other = {};
            db.other[item.Id] = downloadedItem;
          }

          await saveDownloadsDatabase(db);

          toast.success(
            t("home.downloads.toasts.download_completed_for_item", {
              item: item.Name,
            }),
          );

          console.log(
            `[QUEUE] Removing completed process: ${completedProcess.id}`,
          );
          removeProcess(completedProcess.id);
        } catch (error) {
          console.error(`[QUEUE] Error processing completed download:`, error);
          removeProcess(completedProcess.id);
        }
      })();
    }

    const activeDownloads = processes.filter(
      (p) => p.status === "downloading",
    ).length;
    const concurrentLimit = settings?.remuxConcurrentLimit || 1;
    if (activeDownloads < concurrentLimit) {
      const queuedDownload = processes.find((p) => p.status === "queued");
      if (queuedDownload) {
        // Reserve the slot immediately to avoid race where startDownload's
        // asynchronous begin callback hasn't executed yet and multiple
        // downloads are started, bypassing the concurrent limit.
        updateProcess(queuedDownload.id, { status: "downloading" });
        startDownload(queuedDownload).catch((error) => {
          console.error("Failed to start download:", error);
          updateProcess(queuedDownload.id, { status: "error" });
          toast.error(t("home.downloads.toasts.failed_to_start_download"), {
            description: error.message || "Unknown error",
          });
        });
      }
    }
  }, [processes, settings?.remuxConcurrentLimit, startDownload, api, t]);

  const removeProcess = useCallback(
    async (id: string) => {
      const tasks = await BackGroundDownloader.checkForExistingDownloads();
      const task = tasks?.find((t: any) => t.id === id);
      if (task) {
        // On iOS, suspended tasks need to be cancelled properly
        if (Platform.OS === "ios") {
          const state = task.state || task.state?.();
          if (
            state === "PAUSED" ||
            state === "paused" ||
            state === "SUSPENDED" ||
            state === "suspended"
          ) {
            // For suspended tasks, we need to resume first, then stop
            try {
              await task.resume();
              // Small delay to allow resume to take effect
              await new Promise((resolve) => setTimeout(resolve, 100));
            } catch (_resumeError) {
              // Resume might fail, continue with stop
            }
          }
        }

        try {
          task.stop();
        } catch (_err) {
          // ignore stop errors
        }
        try {
          BackGroundDownloader.completeHandler(id);
        } catch (_err) {
          // ignore
        }
      }
      setProcesses((prev) => prev.filter((process) => process.id !== id));
      manageDownloadQueue();
    },
    [setProcesses, manageDownloadQueue],
  );

  useEffect(() => {
    manageDownloadQueue();
  }, [processes, manageDownloadQueue]);

  /**
   * Cleans the cache directory.
   */
  const cleanCacheDirectory = async (): Promise<void> => {
    try {
      if (APP_CACHE_DOWNLOAD_DIRECTORY.exists) {
        APP_CACHE_DOWNLOAD_DIRECTORY.delete();
      }
      APP_CACHE_DOWNLOAD_DIRECTORY.create({
        intermediates: true,
        idempotent: true,
      });
    } catch (_error) {
      toast.error(t("home.downloads.toasts.failed_to_clean_cache_directory"));
    }
  };

  const startBackgroundDownload = useCallback(
    async (
      url: string,
      item: BaseItemDto,
      mediaSource: MediaSourceInfo,
      maxBitrate: Bitrate,
    ) => {
      if (!api || !item.Id || !authHeader) {
        console.warn("startBackgroundDownload ~ Missing required params", {
          api,
          item,
          authHeader,
        });
        throw new Error("startBackgroundDownload ~ Missing required params");
      }
      try {
        const deviceId = getOrSetDeviceId();
        await saveSeriesPrimaryImage(item);
        const itemImage = getItemImage({
          item,
          api,
          variant: "Primary",
          quality: 90,
          width: 500,
        });
        await saveImage(item.Id, itemImage?.uri);
        const job: JobStatus = {
          id: item.Id!,
          deviceId: deviceId,
          maxBitrate,
          inputUrl: url,
          item: item,
          itemId: item.Id!,
          mediaSource,
          progress: 0,
          status: "queued",
          timestamp: new Date(),
        };
        setProcesses((prev) => {
          // Remove any existing processes for this item to prevent duplicates
          const filtered = prev.filter((p) => p.id !== item.Id);
          return [...filtered, job];
        });
        toast.success(
          t("home.downloads.toasts.download_started_for_item", {
            item: item.Name,
          }),
          {
            action: {
              label: t("home.downloads.toasts.go_to_downloads"),
              onClick: () => {
                router.push("/downloads");
                toast.dismiss();
              },
            },
          },
        );
      } catch (error) {
        writeToLog("ERROR", "Error in startBackgroundDownload", error);
      }
    },
    [authHeader, startDownload],
  );

  const deleteFile = async (id: string, type: BaseItemDto["Type"]) => {
    const db = getDownloadsDatabase();
    let downloadedItem: DownloadedItem | undefined;

    if (type === "Movie" && Object.entries(db.movies).length !== 0) {
      downloadedItem = db.movies[id];
      if (downloadedItem) {
        delete db.movies[id];
      }
    } else if (type === "Episode" && Object.entries(db.series).length !== 0) {
      const cleanUpEmptyParents = (
        series: any,
        seasonNumber: string,
        seriesId: string,
      ) => {
        if (!Object.keys(series.seasons[seasonNumber].episodes).length) {
          delete series.seasons[seasonNumber];
        }
        if (!Object.keys(series.seasons).length) {
          delete db.series[seriesId];
        }
      };

      for (const [seriesId, series] of Object.entries(db.series)) {
        for (const [seasonNumber, season] of Object.entries(series.seasons)) {
          for (const [episodeNumber, episode] of Object.entries(
            season.episodes,
          )) {
            if (episode.item.Id === id) {
              downloadedItem = episode;
              delete season.episodes[Number(episodeNumber)];
              cleanUpEmptyParents(series, seasonNumber, seriesId);
              break;
            }
          }
          if (downloadedItem) break;
        }
        if (downloadedItem) break;
      }
    } else {
      // Handle other media types
      if (db.other) {
        downloadedItem = db.other[id];
        if (downloadedItem) {
          delete db.other[id];
        }
      }
    }

    if (downloadedItem?.videoFilePath) {
      try {
        console.log(
          `[DELETE] Attempting to delete video file: ${downloadedItem.videoFilePath}`,
        );

        // Properly reconstruct File object using Paths.document and filename
        let videoFile: File;
        if (downloadedItem.videoFileName) {
          // New approach: use stored filename with Paths.document
          videoFile = new File(Paths.document, downloadedItem.videoFileName);
          console.log(
            `[DELETE] Reconstructed file from stored filename: ${downloadedItem.videoFileName}`,
          );
        } else {
          // Fallback for old downloads: extract filename from URI
          const filename = downloadedItem.videoFilePath.split("/").pop();
          if (!filename) {
            throw new Error("Could not extract filename from path");
          }
          videoFile = new File(Paths.document, filename);
          console.log(
            `[DELETE] Reconstructed file from URI (legacy): ${filename}`,
          );
        }

        console.log(`[DELETE] File URI: ${videoFile.uri}`);
        console.log(
          `[DELETE] File exists before deletion: ${videoFile.exists}`,
        );

        if (videoFile.exists) {
          videoFile.delete();
          console.log(`[DELETE] Video file deleted successfully`);
        } else {
          console.warn(`[DELETE] File does not exist, skipping deletion`);
        }
      } catch (err) {
        console.error(`[DELETE] Failed to delete video file:`, err);
        // File might not exist, continue anyway
      }
    }

    if (downloadedItem?.mediaSource?.MediaStreams) {
      for (const stream of downloadedItem.mediaSource.MediaStreams) {
        if (
          stream.Type === "Subtitle" &&
          stream.DeliveryMethod === "External" &&
          stream.DeliveryUrl
        ) {
          try {
            console.log(
              `[DELETE] Deleting subtitle file: ${stream.DeliveryUrl}`,
            );
            // Extract filename from the subtitle URI
            const subtitleFilename = stream.DeliveryUrl.split("/").pop();
            if (subtitleFilename) {
              const subtitleFile = new File(Paths.document, subtitleFilename);
              if (subtitleFile.exists) {
                subtitleFile.delete();
                console.log(
                  `[DELETE] Subtitle file deleted: ${subtitleFilename}`,
                );
              }
            }
          } catch (err) {
            console.error(`[DELETE] Failed to delete subtitle:`, err);
            // File might not exist, ignore
          }
        }
      }
    }

    if (downloadedItem?.trickPlayData?.path) {
      try {
        console.log(
          `[DELETE] Deleting trickplay directory: ${downloadedItem.trickPlayData.path}`,
        );
        // Extract directory name from URI
        const trickplayDirName = downloadedItem.trickPlayData.path
          .split("/")
          .pop();
        if (trickplayDirName) {
          const trickplayDir = new Directory(Paths.document, trickplayDirName);
          if (trickplayDir.exists) {
            trickplayDir.delete();
            console.log(
              `[DELETE] Trickplay directory deleted: ${trickplayDirName}`,
            );
          }
        }
      } catch (err) {
        console.error(`[DELETE] Failed to delete trickplay directory:`, err);
        // Directory might not exist, ignore
      }
    }

    await saveDownloadsDatabase(db);
    successHapticFeedback();
  };

  const deleteItems = async (items: BaseItemDto[]) => {
    for (const item of items) {
      if (item.Id) {
        await deleteFile(item.Id, item.Type);
      }
    }
  };

  /** Deletes all files */
  const deleteAllFiles = async (): Promise<void> => {
    await deleteFileByType("Movie");
    await deleteFileByType("Episode");
    toast.success(
      t(
        "home.downloads.toasts.all_files_folders_and_jobs_deleted_successfully",
      ),
    );
  };

  /** Deletes all files of a given type. */
  const deleteFileByType = async (type: BaseItemDto["Type"]) => {
    const downloadedItems = getDownloadedItems();
    const itemsToDelete = downloadedItems?.filter(
      (file) => file.item.Type === type,
    );
    if (itemsToDelete) await deleteItems(itemsToDelete.map((i) => i.item));
  };

  /** Returns the size of a downloaded item. */
  const getDownloadedItemSize = (itemId: string): number => {
    const downloadedItem = getDownloadedItemById(itemId);
    if (!downloadedItem) return 0;

    const trickplaySize = downloadedItem.trickPlayData?.size || 0;
    return downloadedItem.videoFileSize + trickplaySize;
  };

  /** Updates a downloaded item. */
  const updateDownloadedItem = (
    itemId: string,
    updatedItem: DownloadedItem,
  ) => {
    const db = getDownloadsDatabase();
    if (db.movies[itemId]) {
      db.movies[itemId] = updatedItem;
    } else if (db.other?.[itemId]) {
      db.other[itemId] = updatedItem;
    } else {
      for (const series of Object.values(db.series)) {
        for (const season of Object.values(series.seasons)) {
          for (const episode of Object.values(season.episodes)) {
            if (episode.item.Id === itemId) {
              season.episodes[episode.item.IndexNumber as number] = updatedItem;
            }
          }
        }
      }
    }
    saveDownloadsDatabase(db);
  };

  /**
   * Returns the size of the app and the remaining space on the device.
   * @returns The size of the app and the remaining space on the device.
   */
  const appSizeUsage = async () => {
    const total = Paths.totalDiskSpace;
    const remaining = Paths.availableDiskSpace;

    let appSize = 0;
    try {
      // Paths.document is a Directory object in the new API
      const documentDir = Paths.document;
      console.log(`[STORAGE] Listing contents of: ${documentDir.uri}`);
      console.log(`[STORAGE] Document dir exists: ${documentDir.exists}`);

      if (!documentDir.exists) {
        console.warn(`[STORAGE] Document directory does not exist`);
        return { total, remaining, appSize: 0 };
      }

      const contents = documentDir.list();
      console.log(
        `[STORAGE] Found ${contents.length} items in document directory`,
      );

      for (const item of contents) {
        if (item instanceof File) {
          console.log(`[STORAGE] File: ${item.name}, size: ${item.size} bytes`);
          appSize += item.size;
        } else if (item instanceof Directory) {
          const dirSize = item.size || 0;
          console.log(
            `[STORAGE] Directory: ${item.name}, size: ${dirSize} bytes`,
          );
          appSize += dirSize;
        }
      }
      console.log(`[STORAGE] Total app size: ${appSize} bytes`);
    } catch (error) {
      console.error(`[STORAGE] Error calculating app size:`, error);
    }
    return { total, remaining, appSize: appSize };
  };

  const pauseDownload = useCallback(
    async (id: string) => {
      const process = processes.find((p) => p.id === id);
      if (!process) throw new Error("No active download");

      // TODO: iOS pause functionality temporarily disabled due to background task issues
      // Remove this check to re-enable iOS pause functionality in the future
      if (Platform.OS === "ios") {
        console.warn(
          `[PAUSE] Pause functionality temporarily disabled on iOS for ${id}`,
        );
        throw new Error("Pause functionality is currently disabled on iOS");
      }

      const tasks = await BackGroundDownloader.checkForExistingDownloads();
      const task = tasks?.find((t: any) => t.id === id);
      if (!task) throw new Error("No task found");

      // Get current progress before stopping
      const currentProgress = process.progress;
      const currentBytes = process.bytesDownloaded || task.bytesDownloaded || 0;

      console.log(
        `[PAUSE] Starting pause for ${id}. Current bytes: ${currentBytes}, Progress: ${currentProgress}%`,
      );

      try {
        /* 
        // TODO: Uncomment this block to re-enable iOS pause functionality
        // iOS-specific aggressive cleanup approach based on GitHub issue #26
        if (Platform.OS === "ios") {
          // Get ALL tasks for this ID - there might be multiple zombie tasks
          const allTasks =
            await BackGroundDownloader.checkForExistingDownloads();
          const tasksForId = allTasks?.filter((t: any) => t.id === id) || [];

          console.log(`[PAUSE] Found ${tasksForId.length} task(s) for ${id}`);

          // Stop ALL tasks for this ID to prevent zombie processes
          for (let i = 0; i < tasksForId.length; i++) {
            const taskToStop = tasksForId[i];
            console.log(
              `[PAUSE] Stopping task ${i + 1}/${tasksForId.length} for ${id}`,
            );

            try {
              // iOS: pause → stop sequence with delays (based on issue research)
              await taskToStop.pause();
              await new Promise((resolve) => setTimeout(resolve, 100));

              await taskToStop.stop();
              await new Promise((resolve) => setTimeout(resolve, 100));

              console.log(
                `[PAUSE] Successfully stopped task ${i + 1} for ${id}`,
              );
            } catch (taskError) {
              console.warn(
                `[PAUSE] Failed to stop task ${i + 1} for ${id}:`,
                taskError,
              );
            }
          }

          // Extra cleanup delay for iOS NSURLSession to fully stop
          await new Promise((resolve) => setTimeout(resolve, 500));
        } else {
        */

        // Android: simpler approach (currently the only active platform)
        await task.stop();

        /* } // End of iOS block - uncomment when re-enabling iOS functionality */

        // Clean up the native task handler
        try {
          BackGroundDownloader.completeHandler(id);
        } catch (_err) {
          console.warn(`[PAUSE] Handler cleanup warning for ${id}:`, _err);
        }

        // Update process state to paused
        updateProcess(id, {
          status: "paused",
          progress: currentProgress,
          bytesDownloaded: currentBytes,
          pausedAt: new Date(),
          pausedProgress: currentProgress,
          pausedBytes: currentBytes,
          lastSessionBytes: process.lastSessionBytes ?? currentBytes,
          lastSessionUpdateTime: process.lastSessionUpdateTime ?? new Date(),
        });

        console.log(`Download paused successfully: ${id}`);
      } catch (error) {
        console.error("Error pausing task:", error);
        throw error;
      }
    },
    [processes, updateProcess],
  );

  const resumeDownload = useCallback(
    async (id: string) => {
      const process = processes.find((p) => p.id === id);
      if (!process) throw new Error("No active download");

      // TODO: iOS resume functionality temporarily disabled due to background task issues
      // Remove this check to re-enable iOS resume functionality in the future
      if (Platform.OS === "ios") {
        console.warn(
          `[RESUME] Resume functionality temporarily disabled on iOS for ${id}`,
        );
        throw new Error("Resume functionality is currently disabled on iOS");
      }

      console.log(
        `[RESUME] Attempting to resume ${id}. Paused bytes: ${process.pausedBytes}, Progress: ${process.pausedProgress}%`,
      );

      /* 
      // TODO: Uncomment this block to re-enable iOS resume functionality
      // Enhanced cleanup for iOS based on GitHub issue research
      if (Platform.OS === "ios") {
        try {
          // Clean up any lingering zombie tasks first (critical for iOS)
          const allTasks =
            await BackGroundDownloader.checkForExistingDownloads();
          const existingTasks = allTasks?.filter((t: any) => t.id === id) || [];

          if (existingTasks.length > 0) {
            console.log(
              `[RESUME] Found ${existingTasks.length} lingering task(s), cleaning up...`,
            );

            for (const task of existingTasks) {
              try {
                await task.stop();
                BackGroundDownloader.completeHandler(id);
              } catch (cleanupError) {
                console.warn(`[RESUME] Cleanup error:`, cleanupError);
              }
            }

            // Wait for iOS cleanup to complete
            await new Promise((resolve) => setTimeout(resolve, 500));
          }
        } catch (error) {
          console.warn(`[RESUME] Pre-resume cleanup failed:`, error);
        }
      }
      */

      // Simple approach: always restart the download from where we left off
      // This works consistently across all platforms (currently Android only)
      if (
        process.pausedProgress !== undefined &&
        process.pausedBytes !== undefined
      ) {
        // We have saved pause state - restore it and restart
        updateProcess(id, {
          progress: process.pausedProgress,
          bytesDownloaded: process.pausedBytes,
          status: "downloading",
          // Reset session counters for proper speed calculation
          lastSessionBytes: process.pausedBytes,
          lastSessionUpdateTime: new Date(),
        });

        // Small delay to ensure any cleanup in startDownload completes
        await new Promise((resolve) => setTimeout(resolve, 100));

        const updatedProcess = processes.find((p) => p.id === id);
        await startDownload(updatedProcess || process);

        console.log(`Download resumed successfully: ${id}`);
      } else {
        // No pause state - start from beginning
        await startDownload(process);
      }
    },
    [processes, updateProcess, startDownload],
  );

  return {
    processes,
    startBackgroundDownload,
    getDownloadedItems,
    getDownloadsDatabase,
    deleteAllFiles,
    deleteFile,
    deleteItems,
    removeProcess,
    startDownload,
    pauseDownload,
    resumeDownload,
    deleteFileByType,
    getDownloadedItemSize,
    getDownloadedItemById,
    APP_CACHE_DOWNLOAD_DIRECTORY: APP_CACHE_DOWNLOAD_DIRECTORY.uri,
    cleanCacheDirectory,
    updateDownloadedItem,
    appSizeUsage,
    dumpDownloadDiagnostics: async (id?: string) => {
      // Collect JS-side processes and native task info (best-effort)
      const tasks = BackGroundDownloader
        ? await BackGroundDownloader.checkForExistingDownloads()
        : [];
      const extra: any = {
        processes,
        nativeTasks: tasks || [],
      };
      if (id) {
        const p = processes.find((x) => x.id === id);
        extra.focusedProcess = p || null;
      }
      return dumpDownloadDiagnostics(extra);
    },
  };
}

export function useDownload() {
  const context = useContext(DownloadContext);

  if (Platform.isTV) {
    // Since tv doesn't do downloads, just return no-op functions for everything
    return {
      processes: [],
      startBackgroundDownload: async () => {},
      getDownloadedItems: () => [],
      getDownloadsDatabase: () => ({}),
      deleteAllFiles: async () => {},
      deleteFile: async () => {},
      deleteItems: async () => {},
      removeProcess: () => {},
      startDownload: async () => {},
      pauseDownload: async () => {},
      resumeDownload: async () => {},
      deleteFileByType: async () => {},
      getDownloadedItemSize: () => 0,
      getDownloadedItemById: () => undefined,
      APP_CACHE_DOWNLOAD_DIRECTORY: "",
      cleanCacheDirectory: async () => {},
      updateDownloadedItem: () => {},
      appSizeUsage: async () => ({ total: 0, remaining: 0, appSize: 0 }),
    };
  }

  if (context === null) {
    throw new Error("useDownload must be used within a DownloadProvider");
  }

  return context;
}

export function DownloadProvider({ children }: { children: React.ReactNode }) {
  const downloadUtils = useDownloadProvider();
  return (
    <DownloadContext.Provider value={downloadUtils}>
      {children}
    </DownloadContext.Provider>
  );
}
