import PropTypes from "prop-types";
import {
  createContext,
  useContext,
  useEffect,
  useState,
  useCallback,
} from "react";

// Simple video cache that stores object URLs (from Blobs)
class VideoCache {
  constructor() {
    this.cache = new Map(); // url -> objectUrl
    this.loadingPromises = new Map(); // url -> Promise<string>
  }

  isCached(url) {
    return this.cache.has(url);
  }

  get(url) {
    return this.cache.get(url) || null;
  }

  set(url, objectUrl) {
    // Revoke old object URL if overwriting
    if (this.cache.has(url)) {
      const oldUrl = this.cache.get(url);
      if (oldUrl && oldUrl !== objectUrl) {
        try {
          URL.revokeObjectURL(oldUrl);
        } catch {
          // ignore
        }
      }
    }
    this.cache.set(url, objectUrl);
  }

  remove(url) {
    if (this.cache.has(url)) {
      const objectUrl = this.cache.get(url);
      try {
        URL.revokeObjectURL(objectUrl);
      } catch {
        // ignore
      }
      this.cache.delete(url);
    }
    this.loadingPromises.delete(url);
  }

  clear() {
    for (const [, objectUrl] of this.cache.entries()) {
      try {
        URL.revokeObjectURL(objectUrl);
      } catch {
        // ignore
      }
    }
    this.cache.clear();
    this.loadingPromises.clear();
  }

  async _fetchToObjectUrl(url, onProgress) {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    // Stream to track progress when possible
    const contentLengthHeader = response.headers.get("content-length");
    const totalBytes = contentLengthHeader
      ? parseInt(contentLengthHeader, 10)
      : null;
    if (response.body && typeof response.body.getReader === "function") {
      const reader = response.body.getReader();
      const chunks = [];
      let loadedBytes = 0;
      // Notify start
      if (onProgress) {
        try {
          onProgress(totalBytes ? 0 : null, { loaded: 0, total: totalBytes });
        } catch {
          // ignore
        }
      }
      let doneReading = false;
      while (!doneReading) {
        const { done, value } = await reader.read();
        if (done) {
          doneReading = true;
          break;
        }
        chunks.push(value);
        loadedBytes += value.length;
        if (onProgress) {
          try {
            const ratio = totalBytes
              ? Math.min(1, loadedBytes / totalBytes)
              : null;
            onProgress(ratio, { loaded: loadedBytes, total: totalBytes });
          } catch {
            // ignore
          }
        }
      }
      const blob = new Blob(chunks, {
        type:
          response.headers.get("content-type") || "application/octet-stream",
      });
      if (onProgress) {
        try {
          const finalTotal = totalBytes ?? blob.size;
          onProgress(1, { loaded: finalTotal, total: finalTotal });
        } catch {
          // ignore
        }
      }
      return URL.createObjectURL(blob);
    }
    // Fallback: no streaming support; cannot measure progress
    const blob = await response.blob();
    if (onProgress) {
      try {
        onProgress(1, { loaded: blob.size, total: blob.size });
      } catch {
        // ignore
      }
    }
    return URL.createObjectURL(blob);
  }

  async loadVideo(primaryUrl, mirrorUrl = null, onProgress) {
    // Use cache if present
    const cached = this.get(primaryUrl);
    if (cached) {
      return cached;
    }

    // Reuse in-flight request if present
    if (this.loadingPromises.has(primaryUrl)) {
      return this.loadingPromises.get(primaryUrl);
    }

    // Compose a single loading promise and store it to dedupe concurrent loads
    const loadingPromise = (async () => {
      // Try mirror first if provided
      if (mirrorUrl) {
        try {
          const objUrl = await this._fetchToObjectUrl(mirrorUrl, onProgress);
          this.set(primaryUrl, objUrl); // cache under primary URL key
          return objUrl;
        } catch {
          // fall through to primary
        }
      }
      const objUrl = await this._fetchToObjectUrl(primaryUrl, onProgress);
      this.set(primaryUrl, objUrl);
      return objUrl;
    })();

    this.loadingPromises.set(primaryUrl, loadingPromise);
    try {
      const result = await loadingPromise;
      return result;
    } finally {
      this.loadingPromises.delete(primaryUrl);
    }
  }
}

const videoCache = new VideoCache();
const VideoContext = createContext();

export const useVideoContext = () => useContext(VideoContext);

export const VideoProvider = ({ children }) => {
  // Track basic per-URL loading state and resolved object URL for consumers
  const [videoStates, setVideoStates] = useState({}); // { [url]: { objectUrl, loadingState, progress?, loadedBytes?, totalBytes? } }

  const getVideoUrl = useCallback(
    (url) => {
      return videoStates[url]?.objectUrl || videoCache.get(url) || null;
    },
    [videoStates]
  );

  const loadVideo = useCallback(async (url, mirrorUrl = null) => {
    if (!url) return null;

    // Fast path: cached
    if (videoCache.isCached(url)) {
      const objectUrl = videoCache.get(url);
      setVideoStates((prev) => ({
        ...prev,
        [url]: {
          objectUrl,
          loadingState: "loaded",
          progress: 1,
          loadedBytes: null,
          totalBytes: null,
        },
      }));
      return objectUrl;
    }

    // Transition to loading
    setVideoStates((prev) => ({
      ...prev,
      [url]: {
        objectUrl: prev[url]?.objectUrl || null,
        loadingState: "loading",
        progress: 0,
        loadedBytes: 0,
        totalBytes: null,
      },
    }));

    try {
      const objectUrl = await videoCache.loadVideo(
        url,
        mirrorUrl,
        (progressRatio, bytes) => {
          setVideoStates((prev) => ({
            ...prev,
            [url]: {
              objectUrl: prev[url]?.objectUrl || null,
              loadingState: "loading",
              progress:
                typeof progressRatio === "number"
                  ? progressRatio
                  : prev[url]?.progress ?? null,
              loadedBytes:
                typeof bytes?.loaded === "number"
                  ? bytes.loaded
                  : prev[url]?.loadedBytes ?? null,
              totalBytes:
                typeof bytes?.total === "number"
                  ? bytes.total
                  : prev[url]?.totalBytes ?? null,
            },
          }));
        }
      );
      setVideoStates((prev) => ({
        ...prev,
        [url]: {
          objectUrl,
          loadingState: "loaded",
          progress: 1,
          loadedBytes: prev?.[url]?.loadedBytes ?? null,
          totalBytes: prev?.[url]?.totalBytes ?? null,
        },
      }));
      return objectUrl;
    } catch (error) {
      setVideoStates((prev) => ({
        ...prev,
        [url]: {
          objectUrl: null,
          loadingState: "error",
          progress: null,
          loadedBytes: null,
          totalBytes: null,
        },
      }));
      return null;
    }
  }, []);

  const removeVideo = useCallback(
    (url) => {
      const existing = videoStates[url]?.objectUrl;
      if (existing) {
        try {
          URL.revokeObjectURL(existing);
        } catch {
          // ignore
        }
      }
      videoCache.remove(url);
      setVideoStates((prev) => {
        const next = { ...prev };
        delete next[url];
        return next;
      });
    },
    [videoStates]
  );

  const clearVideos = useCallback(() => {
    // Revoke any object URLs held in state, then clear cache
    Object.values(videoStates).forEach((entry) => {
      if (entry?.objectUrl) {
        try {
          URL.revokeObjectURL(entry.objectUrl);
        } catch {
          // ignore
        }
      }
    });
    setVideoStates({});
    videoCache.clear();
  }, [videoStates]);

  useEffect(() => {
    return () => {
      // Avoid setState in unmount cleanup to prevent update depth loops
      try {
        videoCache.clear();
      } catch {
        // ignore
      }
    };
  }, []);

  return (
    <VideoContext.Provider
      value={{
        videoStates, // map-like object keyed by URL
        getVideoUrl, // returns object URL or null
        loadVideo, // async, returns object URL or null
        removeVideo,
        clearVideos,
      }}
    >
      {children}
    </VideoContext.Provider>
  );
};

VideoProvider.propTypes = {
  children: PropTypes.any,
};

export default VideoContext;
