import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  type RefObject,
} from 'react';
import axios from 'axios';
import { logger } from '../lib/logger';
import { markPresignedUrlFailed, isRelativeStoragePath } from '../lib/assetUrl';
import { baseURLApi } from '../config';
import { useReversePlayback } from './useReversePlayback';

export type ReverseMode = 'none' | 'reverse' | 'separate';

export interface TransitionConfig {
  videoUrl: string;
  storageKey?: string; // Raw storage path for cache lookup
  reverseMode: ReverseMode;
  reverseVideoUrl?: string;
  durationSec?: number;
  targetPageId?: string;
  displayName?: string;
}

export interface UseTransitionPlaybackOptions {
  videoRef: RefObject<HTMLVideoElement | null>;
  transition: TransitionConfig | null;
  onComplete: (targetPageId?: string) => void;
  onError?: (reason: string) => void;

  timeouts?: {
    playbackStartMs?: number;
    durationBufferMs?: number;
    hardTimeoutMs?: number;
  };
  features?: {
    useBlobUrl?: boolean;
    preDecodeImages?: boolean;
    getTargetPageImages?: () => string[];
  };
  preload?: {
    preloadedUrls?: Set<string>;
    getCachedBlobUrl?: (url: string) => Promise<string | null>;
    getReadyBlobUrl?: (url: string) => string | null;
  };
}

export type PlaybackPhase =
  | 'idle'
  | 'preparing'
  | 'playing'
  | 'reversing'
  | 'finishing'
  | 'completed';

export interface UseTransitionPlaybackResult {
  phase: PlaybackPhase;
  isBuffering: boolean;
  isReversing: boolean;
  cancel: () => void;
  forceComplete: () => void;
}

const DEFAULT_TIMEOUTS = {
  playbackStartMs: 3000,
  durationBufferMs: 200,
  hardTimeoutMs: 45000,
};

function getBufferedEnd(video: HTMLVideoElement): number {
  return video.buffered.length > 0
    ? video.buffered.end(video.buffered.length - 1)
    : 0;
}

function shouldLoadViaBlob(
  url: string,
  reverseMode: ReverseMode,
  useBlobUrlOption?: boolean,
): boolean {
  if (useBlobUrlOption === false) return false;
  if (reverseMode === 'reverse') return true;
  if (useBlobUrlOption === true) return true;

  try {
    const parsedUrl = new URL(url, window.location.origin);
    const isSameOrigin = parsedUrl.origin === window.location.origin;
    if (!isSameOrigin) return false;
    return (
      parsedUrl.pathname === '/api/file/download' ||
      parsedUrl.pathname === '/file/download'
    );
  } catch {
    return false;
  }
}

function buildBlobRequestUrl(url: string): string {
  if (url.startsWith('/api/')) {
    return url.replace(/^\/api(?=\/)/, '');
  }
  return url;
}

/**
 * Check if a URL is a presigned S3 URL (contains X-Amz-Signature)
 */
function isPresignedUrl(url: string): boolean {
  return url.includes('X-Amz-Signature=') || url.includes('x-amz-signature=');
}

/**
 * Convert a presigned URL back to proxy URL
 * Extracts the storage key from the S3 path and builds a proxy URL
 */
function getProxyUrlFallback(
  presignedUrl: string,
  originalStorageKey?: string,
): string | null {
  // If we have the original storage key, use it directly
  if (originalStorageKey && isRelativeStoragePath(originalStorageKey)) {
    const normalizedPath = originalStorageKey.replace(/^\/+/, '');
    return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(normalizedPath)}`;
  }

  // Try to extract path from presigned URL
  try {
    const url = new URL(presignedUrl);
    // S3 path format: /bucket-prefix/assets/project-id/filename.ext
    const pathParts = url.pathname.split('/').filter(Boolean);
    // Skip the bucket prefix, take the rest as the storage path
    if (pathParts.length >= 2) {
      const storagePath = pathParts.slice(1).join('/');
      return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(storagePath)}`;
    }
  } catch {
    // URL parsing failed
  }
  return null;
}

async function waitForImages(urls: string[], timeoutMs = 2000): Promise<void> {
  if (urls.length === 0) return;

  const decodePromises = urls.map(
    (url) =>
      new Promise<void>((resolve) => {
        const img = new Image();
        img.src = url;
        if (typeof img.decode === 'function') {
          img
            .decode()
            .then(() => resolve())
            .catch(() => resolve());
        } else {
          img.onload = () => resolve();
          img.onerror = () => resolve();
        }
      }),
  );

  await Promise.race([
    Promise.all(decodePromises),
    new Promise<void>((resolve) => setTimeout(resolve, timeoutMs)),
  ]);
}

export function useTransitionPlayback(
  options: UseTransitionPlaybackOptions,
): UseTransitionPlaybackResult {
  const {
    videoRef,
    transition,
    onComplete,
    onError,
    timeouts: customTimeouts,
    features,
    preload,
  } = options;

  const playbackStartMs =
    customTimeouts?.playbackStartMs ?? DEFAULT_TIMEOUTS.playbackStartMs;
  const durationBufferMs =
    customTimeouts?.durationBufferMs ?? DEFAULT_TIMEOUTS.durationBufferMs;
  const hardTimeoutMs =
    customTimeouts?.hardTimeoutMs ?? DEFAULT_TIMEOUTS.hardTimeoutMs;

  const [phase, setPhase] = useState<PlaybackPhase>('idle');
  const [isReverseBufferingLocal, setIsReverseBufferingLocal] = useState(false);

  const didFinishRef = useRef(false);
  const didStartPlaybackRef = useRef(false);
  const activeSourceUrlRef = useRef<string | null>(null);
  const lastLoadedBlobUrlRef = useRef<string | null>(null);
  const lastLoadedSourceUrlRef = useRef<string | null>(null);
  const didTryFallbackRef = useRef(false);
  const currentPlayableUrlRef = useRef<string | null>(null);
  const startWatchdogTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
    null,
  );
  const finishTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const hardTimeoutTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
    null,
  );

  const onCompleteRef = useRef(onComplete);
  const onErrorRef = useRef(onError);
  const transitionRef = useRef(transition);
  const featuresRef = useRef(features);
  const preloadRef = useRef(preload);
  const startReverseRef = useRef<(() => Promise<void>) | null>(null);
  const stopReverseRef = useRef<(() => void) | null>(null);

  const sourceUrl = useMemo(() => {
    if (!transition) return '';
    return transition.reverseMode === 'separate' && transition.reverseVideoUrl
      ? transition.reverseVideoUrl
      : transition.videoUrl;
  }, [transition]);

  const clearTimers = useCallback(() => {
    if (startWatchdogTimerRef.current) {
      clearTimeout(startWatchdogTimerRef.current);
      startWatchdogTimerRef.current = null;
    }
    if (finishTimerRef.current) {
      clearTimeout(finishTimerRef.current);
      finishTimerRef.current = null;
    }
    if (hardTimeoutTimerRef.current) {
      clearTimeout(hardTimeoutTimerRef.current);
      hardTimeoutTimerRef.current = null;
    }
  }, []);

  const revokeBlobUrl = useCallback((force = false) => {
    if (!force || !lastLoadedBlobUrlRef.current) return;
    URL.revokeObjectURL(lastLoadedBlobUrlRef.current);
    lastLoadedBlobUrlRef.current = null;
  }, []);

  const finishPlayback = useCallback(
    async (reason: string) => {
      if (didFinishRef.current) return;
      didFinishRef.current = true;
      activeSourceUrlRef.current = null;
      clearTimers();

      const video = videoRef.current;
      if (video) {
        video.pause();
        // Seek back slightly to ensure last frame is visible
        // Some browsers show black after 'ended' event when currentTime === duration
        if (
          video.duration &&
          Number.isFinite(video.duration) &&
          video.currentTime >= video.duration - 0.1
        ) {
          video.currentTime = Math.max(0, video.duration - 0.05);
        }
      }

      const currentTransition = transitionRef.current;
      const currentFeatures = featuresRef.current;

      logger.info('Transition playback finished', {
        reason,
        displayName: currentTransition?.displayName,
        targetPageId: currentTransition?.targetPageId,
      });

      setPhase('finishing');

      if (
        currentFeatures?.preDecodeImages &&
        currentFeatures.getTargetPageImages &&
        currentTransition?.targetPageId
      ) {
        try {
          const imageUrls = currentFeatures.getTargetPageImages();
          await waitForImages(imageUrls);
        } catch {
          // Ignore pre-decode errors
        }
      }

      setPhase('completed');
      onCompleteRef.current(currentTransition?.targetPageId);
    },
    [clearTimers, videoRef],
  );

  const handleError = useCallback(
    (reason: string) => {
      if (didFinishRef.current) return;
      logger.error('Transition playback error', { reason });
      onErrorRef.current?.(reason);
      finishPlayback(reason);
    },
    [finishPlayback],
  );

  const handleReverseComplete = useCallback(() => {
    finishPlayback('reverse-complete');
  }, [finishPlayback]);

  const {
    startReverse,
    stopReverse,
    isReversing,
    isBuffering: isReverseBuffering,
  } = useReversePlayback({
    videoRef,
    onComplete: handleReverseComplete,
    preloadedUrls: preload?.preloadedUrls,
    videoUrl: sourceUrl,
    getCachedBlobUrl: preload?.getCachedBlobUrl,
  });

  useEffect(() => {
    onCompleteRef.current = onComplete;
    onErrorRef.current = onError;
    transitionRef.current = transition;
    featuresRef.current = features;
    preloadRef.current = preload;
    startReverseRef.current = startReverse;
    stopReverseRef.current = stopReverse;
  });

  useEffect(() => {
    setIsReverseBufferingLocal(isReverseBuffering);
  }, [isReverseBuffering]);

  const cancel = useCallback(() => {
    if (phase === 'idle') return;
    clearTimers();
    stopReverseRef.current?.();
    didFinishRef.current = true;
    setPhase('idle');
    const video = videoRef.current;
    if (video) {
      video.pause();
      video.removeAttribute('src');
      video.load();
    }
    revokeBlobUrl(true);
  }, [phase, clearTimers, videoRef, revokeBlobUrl]);

  const forceComplete = useCallback(() => {
    finishPlayback('forced');
  }, [finishPlayback]);

  useEffect(() => {
    const video = videoRef.current;
    const currentTransition = transitionRef.current;
    if (!currentTransition || !video || !sourceUrl) {
      return;
    }

    // Include reverseMode in the key so same video can play forward then reverse
    const sourceKey = `${sourceUrl}|${currentTransition.reverseMode}`;
    if (activeSourceUrlRef.current === sourceKey) {
      logger.info('Skipping duplicate effect for same source', {
        sourceUrl,
        reverseMode: currentTransition.reverseMode,
      });
      return;
    }

    activeSourceUrlRef.current = sourceKey;
    didFinishRef.current = false;
    didStartPlaybackRef.current = false;
    didTryFallbackRef.current = false;
    currentPlayableUrlRef.current = null;
    setPhase('preparing');

    const isReverseMode = currentTransition.reverseMode === 'reverse';
    const configuredDurationSec = Number(currentTransition.durationSec);

    const getMediaErrorDetails = () => {
      if (!video.error) return null;
      const mediaError = video.error as MediaError & { message?: string };
      return {
        code: mediaError.code,
        message: mediaError.message || '',
      };
    };

    const logIssue = (reason: string, error?: unknown) => {
      logger.error('Transition playback issue:', {
        reason,
        src: video.currentSrc || sourceUrl,
        readyState: video.readyState,
        networkState: video.networkState,
        duration: video.duration,
        configuredDurationSec,
        reverseMode: currentTransition.reverseMode,
        mediaError: getMediaErrorDetails(),
        error: error instanceof Error ? error : { error },
      });
    };

    const scheduleFinishByDuration = (durationSec: number) => {
      if (
        !Number.isFinite(durationSec) ||
        durationSec <= 0 ||
        finishTimerRef.current
      ) {
        return;
      }
      // Finish slightly BEFORE the video ends to ensure last frame is visible
      // and prevent browser-specific 'ended' event quirks (black frame)
      const finishBeforeEndMs = 50; // 50ms before video naturally ends
      const finishMs = Math.max(100, durationSec * 1000 - finishBeforeEndMs);
      finishTimerRef.current = setTimeout(
        () => finishPlayback('duration-timer'),
        finishMs,
      );
    };

    const attemptPlay = () => {
      video.play().catch((playError) => {
        if (!isReverseMode) {
          logIssue('play-failed', playError);
        }
      });
    };

    const resolvePlayableSource = async (): Promise<string> => {
      // 1. Try storage key lookup first (most reliable for cache hits)
      const getReadyBlobUrl = preloadRef.current?.getReadyBlobUrl;
      const storageKey = currentTransition.storageKey;
      if (getReadyBlobUrl && storageKey) {
        const readyUrl = getReadyBlobUrl(storageKey);
        if (readyUrl) {
          logger.info('Using ready blob URL from storage key', {
            storageKey: storageKey.slice(-50),
          });
          return readyUrl;
        }
      }

      // 2. Try cached blob URL by storage key (post-refresh scenario)
      const getCachedBlobUrl = preloadRef.current?.getCachedBlobUrl;
      if (getCachedBlobUrl && storageKey) {
        try {
          const cachedBlobUrl = await getCachedBlobUrl(storageKey);
          if (cachedBlobUrl) {
            logger.info('Using cached blob URL from storage key', {
              storageKey: storageKey.slice(-50),
            });
            lastLoadedBlobUrlRef.current = cachedBlobUrl;
            lastLoadedSourceUrlRef.current = sourceUrl;
            return cachedBlobUrl;
          }
        } catch {
          // Fall through
        }
      }

      // 3. Reuse cached blob URL if same source (existing logic)
      if (
        lastLoadedBlobUrlRef.current &&
        lastLoadedSourceUrlRef.current === sourceUrl
      ) {
        logger.info('Reusing cached blob URL');
        return lastLoadedBlobUrlRef.current;
      }

      if (
        lastLoadedBlobUrlRef.current &&
        lastLoadedSourceUrlRef.current !== sourceUrl
      ) {
        revokeBlobUrl(true);
      }

      const needsBlobUrl = shouldLoadViaBlob(
        sourceUrl,
        currentTransition.reverseMode,
        featuresRef.current?.useBlobUrl,
      );

      if (!needsBlobUrl) {
        return sourceUrl;
      }

      // 4. Try ready blob URL by resolved URL
      if (getReadyBlobUrl) {
        const readyUrl = getReadyBlobUrl(sourceUrl);
        if (readyUrl) {
          logger.info('Using ready blob URL from resolved URL', {
            url: sourceUrl.slice(-50),
          });
          return readyUrl;
        }
      }

      // 5. Try cached blob URL by resolved URL
      if (getCachedBlobUrl) {
        try {
          const cachedBlobUrl = await getCachedBlobUrl(sourceUrl);
          if (cachedBlobUrl) {
            logger.info('Using preloaded blob URL from cache', {
              reverseMode: currentTransition.reverseMode,
            });
            lastLoadedBlobUrlRef.current = cachedBlobUrl;
            lastLoadedSourceUrlRef.current = sourceUrl;
            return cachedBlobUrl;
          }
        } catch (cacheError) {
          logger.warn('Cache lookup failed, falling back to fetch', {
            cacheError,
          });
        }
      }

      // 6. Fetch video as blob (network fallback)
      logger.info('Fetching video as blob for seeking support', {
        reverseMode: currentTransition.reverseMode,
      });
      const token =
        typeof window !== 'undefined'
          ? localStorage.getItem('token') || ''
          : '';
      const requestUrl = buildBlobRequestUrl(sourceUrl);
      const response = await axios.get(requestUrl, {
        responseType: 'blob',
        headers: token ? { Authorization: `Bearer ${token}` } : undefined,
      });

      const blobUrl = URL.createObjectURL(response.data);
      lastLoadedBlobUrlRef.current = blobUrl;
      lastLoadedSourceUrlRef.current = sourceUrl;
      logger.info('Created blob URL for video', {
        blobUrl: blobUrl.substring(0, 50),
      });
      return blobUrl;
    };

    const loadAndPlay = async () => {
      logger.info('loadAndPlay called', {
        reverseMode: currentTransition.reverseMode,
        sourceUrl,
      });

      didStartPlaybackRef.current = false;

      if (startWatchdogTimerRef.current) {
        clearTimeout(startWatchdogTimerRef.current);
      }

      try {
        const playableSourceUrl = await resolvePlayableSource();
        if (didFinishRef.current) return;

        video.pause();
        stopReverseRef.current?.();

        const isSameSource =
          lastLoadedSourceUrlRef.current === playableSourceUrl;

        if (isReverseMode && isSameSource && video.readyState >= 2) {
          logger.info('Reusing buffered video for reverse', {
            readyState: video.readyState,
            duration: video.duration,
            bufferedEnd: getBufferedEnd(video),
          });
          setPhase('reversing');
          void startReverseRef.current?.();
          return;
        }

        video.src = playableSourceUrl;
        video.currentTime = 0;
        video.load();
        lastLoadedSourceUrlRef.current = playableSourceUrl;
        currentPlayableUrlRef.current = playableSourceUrl;

        attemptPlay();

        startWatchdogTimerRef.current = setTimeout(() => {
          if (didStartPlaybackRef.current || didFinishRef.current) return;
          logIssue('playback-start-slow');
          if (isReverseMode) {
            didStartPlaybackRef.current = true;
            setPhase('reversing');
            void startReverseRef.current?.();
          } else {
            attemptPlay();
          }
        }, playbackStartMs);
      } catch (error) {
        logIssue('source-prepare-failed', error);
        handleError('source-prepare-failed');
      }
    };

    const onLoadedMetadata = () => {
      if (didFinishRef.current) return;
      if (!isReverseMode) {
        video.currentTime = 0;
        attemptPlay();
      }
    };

    const onCanPlayThrough = () => {
      if (didFinishRef.current) return;
      logger.info('canplaythrough fired', {
        reverseMode: currentTransition.reverseMode,
        didStartPlayback: didStartPlaybackRef.current,
      });

      if (isReverseMode && !didStartPlaybackRef.current) {
        didStartPlaybackRef.current = true;
        if (startWatchdogTimerRef.current) {
          clearTimeout(startWatchdogTimerRef.current);
          startWatchdogTimerRef.current = null;
        }
        video.pause();
        setPhase('reversing');
        void startReverseRef.current?.();
      }
    };

    const onCanPlay = () => {
      if (didFinishRef.current) return;
      if (isReverseMode && didStartPlaybackRef.current) return;
      attemptPlay();
    };

    const onPlaying = () => {
      logger.info('onPlaying fired', {
        reverseMode: currentTransition.reverseMode,
        didStartPlayback: didStartPlaybackRef.current,
        didFinish: didFinishRef.current,
      });

      if (didFinishRef.current) return;

      if (isReverseMode && !didStartPlaybackRef.current) {
        logger.info('Triggering reverse from onPlaying');
        didStartPlaybackRef.current = true;
        if (startWatchdogTimerRef.current) {
          clearTimeout(startWatchdogTimerRef.current);
          startWatchdogTimerRef.current = null;
        }
        video.pause();
        setPhase('reversing');
        void startReverseRef.current?.();
        return;
      }

      didStartPlaybackRef.current = true;
      setPhase('playing');

      if (startWatchdogTimerRef.current) {
        clearTimeout(startWatchdogTimerRef.current);
        startWatchdogTimerRef.current = null;
      }

      if (!isReverseMode) {
        const mediaDurationSec = Number(video.duration);
        const durationSec =
          Number.isFinite(configuredDurationSec) && configuredDurationSec > 0
            ? configuredDurationSec
            : Number.isFinite(mediaDurationSec) && mediaDurationSec > 0
              ? mediaDurationSec
              : NaN;
        if (Number.isFinite(durationSec) && durationSec > 0) {
          scheduleFinishByDuration(durationSec);
        }
      }
    };

    const onEnded = () => finishPlayback('ended');

    const onVideoError = async () => {
      if (didFinishRef.current) return;
      logIssue('video-error');

      // Check if this is a presigned URL failure (likely CORS)
      const currentUrl = currentPlayableUrlRef.current;
      if (
        currentUrl &&
        isPresignedUrl(currentUrl) &&
        !didTryFallbackRef.current
      ) {
        logger.info('Presigned URL failed, trying proxy fallback', {
          url: currentUrl.slice(0, 80),
        });

        // Mark presigned URL as failed so future resolves use proxy
        // Extract storage key from the original transition videoUrl
        const originalVideoUrl = currentTransition.videoUrl;
        if (originalVideoUrl && isRelativeStoragePath(originalVideoUrl)) {
          markPresignedUrlFailed(originalVideoUrl);
        }

        // Get proxy fallback URL
        const fallbackUrl = getProxyUrlFallback(
          currentUrl,
          currentTransition.videoUrl,
        );
        if (fallbackUrl) {
          didTryFallbackRef.current = true;
          video.pause();
          video.src = fallbackUrl;
          currentPlayableUrlRef.current = fallbackUrl;
          video.currentTime = 0;
          video.load();
          attemptPlay();
          return;
        }
      }

      handleError('video-error');
    };

    const onAbort = () => {
      if (didFinishRef.current) return;
      logIssue('video-abort');
      handleError('video-abort');
    };

    const onStalled = () => {
      if (didFinishRef.current) return;
      logIssue('video-stalled');
    };

    video.addEventListener('loadedmetadata', onLoadedMetadata);
    video.addEventListener('canplaythrough', onCanPlayThrough);
    video.addEventListener('canplay', onCanPlay);
    video.addEventListener('playing', onPlaying);
    video.addEventListener('ended', onEnded);
    video.addEventListener('error', onVideoError);
    video.addEventListener('abort', onAbort);
    video.addEventListener('stalled', onStalled);

    hardTimeoutTimerRef.current = setTimeout(() => {
      if (didFinishRef.current) return;
      logIssue('hard-timeout');
      handleError('hard-timeout');
    }, hardTimeoutMs);

    void loadAndPlay();

    return () => {
      video.removeEventListener('loadedmetadata', onLoadedMetadata);
      video.removeEventListener('canplaythrough', onCanPlayThrough);
      video.removeEventListener('canplay', onCanPlay);
      video.removeEventListener('playing', onPlaying);
      video.removeEventListener('ended', onEnded);
      video.removeEventListener('error', onVideoError);
      video.removeEventListener('abort', onAbort);
      video.removeEventListener('stalled', onStalled);
      clearTimers();
      stopReverseRef.current?.();
    };
  }, [
    sourceUrl,
    transition?.reverseMode, // Re-run effect when reverseMode changes (same video, different direction)
    videoRef,
    playbackStartMs,
    hardTimeoutMs,
    clearTimers,
    revokeBlobUrl,
    finishPlayback,
    handleError,
  ]);

  useEffect(() => {
    if (!transition) {
      setPhase('idle');
      activeSourceUrlRef.current = null;
      didFinishRef.current = false;
      didStartPlaybackRef.current = false;
    }
  }, [transition]);

  return {
    phase,
    isBuffering: isReverseBufferingLocal,
    isReversing,
    cancel,
    forceComplete,
  };
}
