/**
 * StreamProvider - HTTP Range streaming and Blob video player
 * Supports:
 * - HTTP Range requests with authorization (for large files)
 * - Blob/ArrayBuffer sources
 * - Data URL sources
 * - Fill parent container mode
 * - Custom error fallback
 */

'use client';

import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';

import { cn } from '@djangocfg/ui-core/lib';
import { Preloader, AspectRatio } from '@djangocfg/ui-core';
import { useMediaCacheStore, generateContentKey } from '../../../stores/mediaCache';
import { useVideoPlayerSettings } from '../hooks/useVideoPlayerSettings';

import type { StreamProviderProps, VideoPlayerRef, StreamSource, BlobSource, DataUrlSource, ErrorFallbackProps } from '../types';
import { videoDebug } from '../utils/debug';

/** Default error fallback UI */
function DefaultErrorFallback({ error }: ErrorFallbackProps) {
  return (
    <div className="absolute inset-0 flex flex-col items-center justify-center gap-4 text-white">
      <svg
        className="w-16 h-16 text-muted-foreground"
        fill="none"
        stroke="currentColor"
        viewBox="0 0 24 24"
      >
        <path
          strokeLinecap="round"
          strokeLinejoin="round"
          strokeWidth={2}
          d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
        />
      </svg>
      <p className="text-lg">{error || 'Video cannot be previewed'}</p>
    </div>
  );
}

export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
  (
    {
      source,
      aspectRatio = 16 / 9,
      autoPlay = false,
      muted = false,
      loop = false,
      playsInline = true,
      preload = 'metadata',
      controls = true,
      disableContextMenu = false,
      showPreloader = true,
      preloaderTimeout = 10000,
      className,
      videoClassName,
      errorFallback,
      onPlay,
      onPause,
      onEnded,
      onError,
      onLoadStart,
      onCanPlay,
      onTimeUpdate,
      onBufferProgress,
    },
    ref
  ) => {
    const [videoUrl, setVideoUrl] = useState<string | null>(null);
    const [isLoading, setIsLoading] = useState(true);
    const [hasError, setHasError] = useState(false);
    const [errorMessage, setErrorMessage] = useState<string>('Video cannot be previewed');
    const videoRef = useRef<HTMLVideoElement>(null);
    const contentKeyRef = useRef<string | null>(null);
    const lastSavedTimeRef = useRef<number>(0);

    // Get stable function references from store (not from hook to avoid re-renders)
    const getOrCreateBlobUrl = useMediaCacheStore.getState().getOrCreateBlobUrl;
    const releaseBlobUrl = useMediaCacheStore.getState().releaseBlobUrl;
    const getOrCreateStreamUrl = useMediaCacheStore.getState().getOrCreateStreamUrl;
    const saveVideoPosition = useMediaCacheStore.getState().saveVideoPosition;
    const getVideoPosition = useMediaCacheStore.getState().getVideoPosition;

    // Persisted player settings
    const { settings: savedSettings, updateVolume } = useVideoPlayerSettings();

    // Retry function for error fallback
    // Regenerates URL for stream sources to get fresh token/session
    const retry = useCallback(() => {
      setHasError(false);
      setIsLoading(true);

      // For stream sources, regenerate URL bypassing cache
      if (source.type === 'stream') {
        const streamSource = source as StreamSource;
        const freshUrl = streamSource.getStreamUrl(streamSource.sessionId, streamSource.path);
        setVideoUrl(freshUrl);
        return;
      }

      // For other sources, just reload
      const video = videoRef.current;
      if (video && videoUrl) {
        video.load();
      }
    }, [source, videoUrl]);

    // Expose video element methods via ref
    useImperativeHandle(
      ref,
      () => ({
        play: () => videoRef.current?.play(),
        pause: () => videoRef.current?.pause(),
        togglePlay: () => {
          const video = videoRef.current;
          if (video) {
            video.paused ? video.play() : video.pause();
          }
        },
        seekTo: (time: number) => {
          if (videoRef.current) videoRef.current.currentTime = time;
        },
        setVolume: (volume: number) => {
          if (videoRef.current) videoRef.current.volume = Math.max(0, Math.min(1, volume));
        },
        toggleMute: () => {
          if (videoRef.current) videoRef.current.muted = !videoRef.current.muted;
        },
        enterFullscreen: () => videoRef.current?.requestFullscreen(),
        exitFullscreen: () => document.exitFullscreen(),
        get currentTime() {
          return videoRef.current?.currentTime ?? 0;
        },
        get duration() {
          return videoRef.current?.duration ?? 0;
        },
        get paused() {
          return videoRef.current?.paused ?? true;
        },
      }),
      []
    );

    // Track unmount for cleanup
    const isMountedRef = useRef(true);
    useEffect(() => {
      isMountedRef.current = true;
      return () => {
        isMountedRef.current = false;
        // Release blob URL only on actual unmount
        if (contentKeyRef.current) {
          useMediaCacheStore.getState().releaseBlobUrl(contentKeyRef.current);
          contentKeyRef.current = null;
        }
      };
    }, []);

    // Create video URL based on source type with caching
    useEffect(() => {
      // Release previous blob URL if source changed
      if (contentKeyRef.current) {
        const newKey = source.type === 'blob'
          ? generateContentKey((source as BlobSource).data)
          : null;
        if (newKey !== contentKeyRef.current) {
          releaseBlobUrl(contentKeyRef.current);
          contentKeyRef.current = null;
        }
      }

      setHasError(false);
      setIsLoading(true);

      switch (source.type) {
        case 'stream': {
          const streamSource = source as StreamSource;
          // Use cached stream URL
          const url = getOrCreateStreamUrl(
            streamSource.sessionId,
            streamSource.path,
            streamSource.getStreamUrl
          );
          videoDebug.load(url, 'stream');
          setVideoUrl(url);
          break;
        }

        case 'blob': {
          const blobSource = source as BlobSource;
          // Generate content key for caching
          const contentKey = generateContentKey(blobSource.data);
          contentKeyRef.current = contentKey;
          // Use cached blob URL
          const url = getOrCreateBlobUrl(
            contentKey,
            blobSource.data,
            blobSource.mimeType || 'video/mp4'
          );
          videoDebug.load(url, 'blob');
          setVideoUrl(url);
          break;
        }

        case 'data-url': {
          const dataUrlSource = source as DataUrlSource;
          videoDebug.load(dataUrlSource.data.slice(0, 50) + '...', 'data-url');
          setVideoUrl(dataUrlSource.data);
          break;
        }

        default:
          videoDebug.error('Invalid video source type', { type: (source as { type: string }).type });
          setVideoUrl(null);
          setHasError(true);
          setErrorMessage('Invalid video source');
      }

      // No cleanup here - cleanup happens in unmount effect above
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [source]);

    // Get source key for position caching
    const getSourceKey = useCallback(() => {
      switch (source.type) {
        case 'stream':
          return `stream:${(source as StreamSource).sessionId}:${(source as StreamSource).path}`;
        case 'blob':
          return contentKeyRef.current ? `blob:${contentKeyRef.current}` : null;
        case 'data-url':
          return `data:${(source as DataUrlSource).data.slice(0, 50)}`;
        default:
          return null;
      }
    }, [source]);

    // Restore cached playback position and settings when video is ready
    const handleCanPlay = useCallback(() => {
      const video = videoRef.current;
      if (video) {
        videoDebug.state('canplay', { duration: video.duration, buffered: video.buffered.length });
        videoDebug.buffer(video.buffered, video.duration);

        // Apply saved volume (user preference)
        video.volume = savedSettings.volume;
      }
      setIsLoading(false);

      // Restore position from cache
      const sourceKey = getSourceKey();
      if (sourceKey && video) {
        const cachedPosition = getVideoPosition(sourceKey);
        if (cachedPosition && cachedPosition > 0) {
          const duration = video.duration;
          // Only restore if position is valid (not at the end)
          if (cachedPosition < duration - 1) {
            videoDebug.debug(`Restoring position: ${cachedPosition}s`);
            video.currentTime = cachedPosition;
          }
        }
      }

      onCanPlay?.();
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [getSourceKey, onCanPlay, savedSettings.volume]);

    // Save playback position periodically
    const handleTimeUpdate = useCallback(() => {
      const video = videoRef.current;
      if (!video) return;

      // Save position every 5 seconds
      const sourceKey = getSourceKey();
      if (sourceKey && video.currentTime > 0) {
        const timeSinceLastSave = video.currentTime - lastSavedTimeRef.current;
        if (timeSinceLastSave >= 5 || timeSinceLastSave < 0) {
          saveVideoPosition(sourceKey, video.currentTime);
          lastSavedTimeRef.current = video.currentTime;
        }
      }

      onTimeUpdate?.(video.currentTime, video.duration);
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [getSourceKey, onTimeUpdate]);

    // Save position on pause
    const handlePause = useCallback(() => {
      const video = videoRef.current;
      const sourceKey = getSourceKey();
      if (sourceKey && video && video.currentTime > 0) {
        saveVideoPosition(sourceKey, video.currentTime);
        lastSavedTimeRef.current = video.currentTime;
      }
      onPause?.();
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [getSourceKey, onPause]);

    // Handle buffer progress
    const handleProgress = useCallback(() => {
      const video = videoRef.current;
      if (!video || !onBufferProgress) return;

      // Get the buffered time ranges
      if (video.buffered.length > 0) {
        // Get the end of the last buffered range
        const bufferedEnd = video.buffered.end(video.buffered.length - 1);
        const duration = video.duration;

        if (duration > 0 && !isNaN(bufferedEnd)) {
          onBufferProgress(bufferedEnd, duration);
        }
      }
    }, [onBufferProgress]);

    // Preloader timeout
    useEffect(() => {
      if (!showPreloader || !isLoading) return;

      const timeout = setTimeout(() => {
        setIsLoading(false);
      }, preloaderTimeout);

      return () => clearTimeout(timeout);
    }, [showPreloader, isLoading, preloaderTimeout]);

    const handleContextMenu = (e: React.MouseEvent) => {
      if (disableContextMenu) {
        e.preventDefault();
      }
    };

    const handleLoadedData = () => {
      setIsLoading(false);
    };

    const handleError = () => {
      const video = videoRef.current;
      if (video) {
        videoDebug.error('Video error', { code: video.error?.code, message: video.error?.message });
      }
      setIsLoading(false);
      setHasError(true);
      setErrorMessage('Failed to load video');
      onError?.('Video playback error');
    };

    // Debug: Log video events
    useEffect(() => {
      const video = videoRef.current;
      if (!video) return;

      const handleLoadedMetadata = () => {
        videoDebug.state('loadedmetadata', { duration: video.duration });
      };

      const handleSeeking = () => {
        videoDebug.event('seeking', { currentTime: video.currentTime });
      };

      const handleSeeked = () => {
        videoDebug.event('seeked', { currentTime: video.currentTime });
        videoDebug.buffer(video.buffered, video.duration);
      };

      const handleWaiting = () => {
        videoDebug.warn('WAITING - buffering...');
        videoDebug.buffer(video.buffered, video.duration);
      };

      const handleStalled = () => {
        videoDebug.warn('STALLED - network issue');
        videoDebug.buffer(video.buffered, video.duration);
      };

      video.addEventListener('loadedmetadata', handleLoadedMetadata);
      video.addEventListener('seeking', handleSeeking);
      video.addEventListener('seeked', handleSeeked);
      video.addEventListener('waiting', handleWaiting);
      video.addEventListener('stalled', handleStalled);

      return () => {
        video.removeEventListener('loadedmetadata', handleLoadedMetadata);
        video.removeEventListener('seeking', handleSeeking);
        video.removeEventListener('seeked', handleSeeked);
        video.removeEventListener('waiting', handleWaiting);
        video.removeEventListener('stalled', handleStalled);
      };
    }, [videoUrl]);

    // Persist volume when user changes it via native controls
    useEffect(() => {
      const video = videoRef.current;
      if (!video) return;

      const handleVolumeChange = () => {
        updateVolume(video.volume);
      };

      video.addEventListener('volumechange', handleVolumeChange);
      return () => video.removeEventListener('volumechange', handleVolumeChange);
    }, [videoUrl, updateVolume]);

    // Determine if we should use AspectRatio wrapper or fill mode
    const isFillMode = aspectRatio === 'fill';
    const computedAspectRatio = aspectRatio === 'auto' || aspectRatio === 'fill' ? undefined : aspectRatio;

    // Render error fallback
    const renderErrorFallback = () => {
      const fallbackProps: ErrorFallbackProps = { error: errorMessage, retry };

      if (typeof errorFallback === 'function') {
        return errorFallback(fallbackProps);
      }

      if (errorFallback) {
        return errorFallback;
      }

      return <DefaultErrorFallback {...fallbackProps} />;
    };

    // Error state
    if (!videoUrl || hasError) {
      if (isFillMode) {
        return (
          <div className={cn('relative w-full h-full overflow-hidden bg-black', className)}>
            {renderErrorFallback()}
          </div>
        );
      }

      return (
        <div className={cn('relative overflow-hidden bg-black', className)}>
          <AspectRatio ratio={computedAspectRatio}>
            {renderErrorFallback()}
          </AspectRatio>
        </div>
      );
    }

    // Video content
    const videoContent = (
      <>
        {/* Loading indicator */}
        {showPreloader && isLoading && (
          <div className="absolute inset-0 flex items-center justify-center bg-black/50 z-10">
            <Preloader size="lg" spinnerClassName="text-white" />
          </div>
        )}

        {/* Video element */}
        <video
          ref={videoRef}
          src={videoUrl}
          className={cn(
            'w-full h-full object-contain',
            isLoading && 'opacity-0',
            videoClassName
          )}
          autoPlay={autoPlay}
          muted={muted}
          loop={loop}
          playsInline={playsInline}
          preload={preload}
          controls={controls}
          crossOrigin="anonymous"
          poster={source.poster}
          onContextMenu={handleContextMenu}
          onLoadStart={onLoadStart}
          onCanPlay={handleCanPlay}
          onLoadedData={handleLoadedData}
          onPlay={onPlay}
          onPause={handlePause}
          onEnded={onEnded}
          onError={handleError}
          onTimeUpdate={handleTimeUpdate}
          onProgress={handleProgress}
        />
      </>
    );

    // Fill mode - no AspectRatio wrapper
    if (isFillMode) {
      return (
        <div className={cn('relative w-full h-full overflow-hidden bg-black', className)}>
          {videoContent}
        </div>
      );
    }

    // Normal mode with AspectRatio
    return (
      <div className={cn('relative overflow-hidden bg-black', className)}>
        <AspectRatio ratio={computedAspectRatio}>
          {videoContent}
        </AspectRatio>
      </div>
    );
  }
);

StreamProvider.displayName = 'StreamProvider';
