import React, { useEffect, useMemo, useRef, useState } from 'react';
import { PlayCircleIcon } from 'tdesign-icons-react';
import { isCanvasFrameMostlyBlack } from '@aitu/utils';
import { useThumbnailUrl } from '../../hooks/useThumbnailUrl';
import './VideoPosterPreview.scss';

const THUMBNAIL_PLACEHOLDER_SIZE = 1;
const MAX_THUMBNAIL_RETRIES = 4;
const FAILED_POSTER_TTL_MS = 10 * 60 * 1000;
const RESOLVED_POSTER_TTL_MS = 10 * 60 * 1000;
const failedPosterCache = new Map<string, number>();
const resolvedPosterCache = new Map<
  string,
  { resolvedPoster: string; resolvedAt: number }
>();
const extractedVideoPosterCache = new Map<
  string,
  { poster: string; capturedAt: number }
>();
const videoPosterProbeTimes = [0.6, 1.2, 2, 0.12];

function cleanupFailedPosterCache(now: number): void {
  failedPosterCache.forEach((failedAt, url) => {
    if (now - failedAt > FAILED_POSTER_TTL_MS) {
      failedPosterCache.delete(url);
    }
  });
}

function markPosterAsFailed(url: string): void {
  const now = Date.now();
  cleanupFailedPosterCache(now);
  failedPosterCache.set(url, now);
}

function hasRecentFailedPoster(url: string): boolean {
  const now = Date.now();
  cleanupFailedPosterCache(now);
  const failedAt = failedPosterCache.get(url);
  return typeof failedAt === 'number' && now - failedAt <= FAILED_POSTER_TTL_MS;
}

function cleanupResolvedPosterCache(now: number): void {
  resolvedPosterCache.forEach((entry, url) => {
    if (now - entry.resolvedAt > RESOLVED_POSTER_TTL_MS) {
      resolvedPosterCache.delete(url);
    }
  });
}

function cacheResolvedPoster(url: string, resolvedPoster: string): void {
  const now = Date.now();
  cleanupResolvedPosterCache(now);
  resolvedPosterCache.set(url, { resolvedPoster, resolvedAt: now });
}

function getCachedResolvedPoster(url: string | undefined): string | null {
  if (!url) {
    return null;
  }

  const now = Date.now();
  cleanupResolvedPosterCache(now);
  const entry = resolvedPosterCache.get(url);
  if (!entry) {
    return null;
  }

  return now - entry.resolvedAt <= RESOLVED_POSTER_TTL_MS
    ? entry.resolvedPoster
    : null;
}

function withPosterRetryParam(url: string, attempt: number): string {
  if (attempt <= 0 || !url) {
    return url;
  }

  try {
    const resolved = new URL(url, window.location.origin);
    resolved.searchParams.set('_poster_retry', String(attempt));
    return resolved.toString();
  } catch {
    const separator = url.includes('?') ? '&' : '?';
    return `${url}${separator}_poster_retry=${attempt}`;
  }
}

function isImageMostlyBlack(image: HTMLImageElement): boolean {
  const sourceWidth = image.naturalWidth;
  const sourceHeight = image.naturalHeight;
  if (
    !sourceWidth ||
    !sourceHeight ||
    sourceWidth <= THUMBNAIL_PLACEHOLDER_SIZE ||
    sourceHeight <= THUMBNAIL_PLACEHOLDER_SIZE
  ) {
    return false;
  }

  const maxSide = 160;
  const scale = Math.min(1, maxSide / Math.max(sourceWidth, sourceHeight));
  const width = Math.max(1, Math.round(sourceWidth * scale));
  const height = Math.max(1, Math.round(sourceHeight * scale));

  try {
    const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
    const context = canvas.getContext('2d');
    if (!context) {
      return false;
    }
    context.drawImage(image, 0, 0, width, height);
    return isCanvasFrameMostlyBlack(context, width, height);
  } catch {
    return false;
  }
}

function cacheExtractedVideoPoster(src: string, poster: string): void {
  const now = Date.now();
  extractedVideoPosterCache.forEach((entry, url) => {
    if (now - entry.capturedAt > RESOLVED_POSTER_TTL_MS) {
      extractedVideoPosterCache.delete(url);
    }
  });
  extractedVideoPosterCache.set(src, { poster, capturedAt: now });
}

function getCachedExtractedVideoPoster(src: string): string | null {
  const entry = extractedVideoPosterCache.get(src);
  if (!entry) {
    return null;
  }

  if (Date.now() - entry.capturedAt > RESOLVED_POSTER_TTL_MS) {
    extractedVideoPosterCache.delete(src);
    return null;
  }

  return entry.poster;
}

function captureVideoFrame(video: HTMLVideoElement): string | null {
  const sourceWidth = video.videoWidth;
  const sourceHeight = video.videoHeight;
  if (!sourceWidth || !sourceHeight) {
    return null;
  }

  const maxSide = 720;
  const scale = Math.min(1, maxSide / Math.max(sourceWidth, sourceHeight));
  const width = Math.max(1, Math.round(sourceWidth * scale));
  const height = Math.max(1, Math.round(sourceHeight * scale));

  try {
    const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
    const context = canvas.getContext('2d');
    if (!context) {
      return null;
    }
    context.drawImage(video, 0, 0, width, height);
    if (isCanvasFrameMostlyBlack(context, width, height)) {
      return null;
    }
    return canvas.toDataURL('image/jpeg', 0.78);
  } catch {
    return null;
  }
}

function shouldCaptureWithCors(src: string): boolean {
  try {
    return new URL(src, window.location.href).origin !== window.location.origin;
  } catch {
    return false;
  }
}

export interface VideoPosterPreviewProps {
  src: string;
  poster?: string;
  alt?: string;
  className?: string;
  fit?: 'cover' | 'contain';
  thumbnailSize?: 'small' | 'large';
  imageLoading?: 'lazy' | 'eager';
  activateVideoOnClick?: boolean;
  playOnActivate?: boolean;
  playOnHover?: boolean;
  onClick?: React.MouseEventHandler<HTMLImageElement | HTMLVideoElement>;
  videoProps?: Omit<
    React.VideoHTMLAttributes<HTMLVideoElement>,
    'src' | 'poster' | 'className' | 'onClick'
  >;
}

function renderPreviewWithOverlay(
  media: React.ReactNode,
  showPlayOverlay: boolean,
  fit: 'cover' | 'contain'
): React.ReactElement {
  return (
    <div className={`video-poster-preview video-poster-preview--fit-${fit}`}>
      {media}
      {showPlayOverlay && (
        <div className="video-poster-preview__play-overlay" aria-hidden="true">
          <PlayCircleIcon size="32px" />
        </div>
      )}
    </div>
  );
}

export const VideoPosterPreview: React.FC<VideoPosterPreviewProps> = ({
  src,
  poster,
  alt = '',
  className,
  fit = 'cover',
  thumbnailSize = 'small',
  imageLoading = 'lazy',
  activateVideoOnClick = false,
  playOnActivate = false,
  playOnHover = false,
  onClick,
  videoProps,
}) => {
  const normalizedPoster = useMemo(
    () =>
      typeof poster === 'string' && poster.trim() ? poster.trim() : undefined,
    [poster]
  );
  const shouldIgnoreExplicitPoster = useMemo(
    () => (normalizedPoster ? hasRecentFailedPoster(normalizedPoster) : false),
    [normalizedPoster]
  );
  const generatedPoster = useThumbnailUrl(src, 'video', thumbnailSize);
  const [preferGeneratedPoster, setPreferGeneratedPoster] = useState(
    shouldIgnoreExplicitPoster
  );
  const posterCandidate = preferGeneratedPoster
    ? generatedPoster
    : normalizedPoster || generatedPoster;
  const hasExplicitPoster = Boolean(normalizedPoster && !preferGeneratedPoster);
  const canRetryGeneratedPoster = Boolean(
    posterCandidate &&
      posterCandidate === generatedPoster &&
      posterCandidate !== src
  );
  const videoRef = useRef<HTMLVideoElement>(null);
  const activatedByClickRef = useRef(false);
  const retryTimerRef = useRef<number | null>(null);
  const [resolvedPoster, setResolvedPoster] = useState<string | null>(() =>
    getCachedResolvedPoster(posterCandidate)
  );
  const [extractedPoster, setExtractedPoster] = useState<string | null>(() =>
    getCachedExtractedVideoPoster(src)
  );
  const [retryCount, setRetryCount] = useState(0);
  const [showVideo, setShowVideo] = useState(false);
  const shouldRequireExplicitActivation = activateVideoOnClick;
  const displayPoster =
    playOnHover && extractedPoster
      ? extractedPoster
      : resolvedPoster || extractedPoster;
  const shouldRenderVideoElement = showVideo || (playOnHover && !displayPoster);
  const videoPoster = displayPoster || undefined;

  useEffect(() => {
    setPreferGeneratedPoster(shouldIgnoreExplicitPoster);
  }, [shouldIgnoreExplicitPoster]);

  useEffect(() => {
    setResolvedPoster(getCachedResolvedPoster(posterCandidate));
    setExtractedPoster(getCachedExtractedVideoPoster(src));
    setRetryCount(0);
    setShowVideo(false);
    activatedByClickRef.current = false;
    if (retryTimerRef.current !== null) {
      window.clearTimeout(retryTimerRef.current);
      retryTimerRef.current = null;
    }
  }, [
    src,
    posterCandidate,
    activateVideoOnClick,
    generatedPoster,
    normalizedPoster,
    thumbnailSize,
  ]);

  useEffect(() => {
    if (showVideo || (resolvedPoster && resolvedPoster === posterCandidate)) {
      return;
    }

    if (!posterCandidate) {
      if (!shouldRequireExplicitActivation) {
        setShowVideo(true);
      }
      return;
    }

    const imageUrl = hasExplicitPoster
      ? posterCandidate
      : withPosterRetryParam(posterCandidate, retryCount);
    let cancelled = false;
    const probeImage = new Image();

    const scheduleRetry = () => {
      if (!canRetryGeneratedPoster || retryCount >= MAX_THUMBNAIL_RETRIES) {
        if (!shouldRequireExplicitActivation) {
          setShowVideo(true);
        }
        return;
      }

      retryTimerRef.current = window.setTimeout(() => {
        setRetryCount((value) => value + 1);
      }, 180 + retryCount * 220);
    };

    probeImage.onload = () => {
      if (cancelled) {
        return;
      }

      const isUsablePoster =
        (probeImage.naturalWidth > THUMBNAIL_PLACEHOLDER_SIZE ||
          probeImage.naturalHeight > THUMBNAIL_PLACEHOLDER_SIZE) &&
        !isImageMostlyBlack(probeImage);

      if (isUsablePoster) {
        cacheResolvedPoster(posterCandidate, posterCandidate);
        setResolvedPoster(posterCandidate);
        return;
      }

      if (hasExplicitPoster && normalizedPoster) {
        markPosterAsFailed(normalizedPoster);
        setPreferGeneratedPoster(true);
        setResolvedPoster(null);
        setRetryCount(0);
        return;
      }

      scheduleRetry();
    };

    probeImage.onerror = () => {
      if (cancelled) {
        return;
      }

      if (hasExplicitPoster && normalizedPoster) {
        markPosterAsFailed(normalizedPoster);
        setPreferGeneratedPoster(true);
        setResolvedPoster(null);
        setRetryCount(0);
        return;
      }

      scheduleRetry();
    };

    probeImage.src = imageUrl;

    return () => {
      cancelled = true;
      probeImage.onload = null;
      probeImage.onerror = null;
      if (retryTimerRef.current !== null) {
        window.clearTimeout(retryTimerRef.current);
        retryTimerRef.current = null;
      }
    };
  }, [
    src,
    posterCandidate,
    retryCount,
    hasExplicitPoster,
    canRetryGeneratedPoster,
    showVideo,
    normalizedPoster,
    resolvedPoster,
    shouldRequireExplicitActivation,
  ]);

  useEffect(() => {
    if (!playOnHover || extractedPoster) {
      return;
    }

    const cachedPoster = getCachedExtractedVideoPoster(src);
    if (cachedPoster) {
      setExtractedPoster(cachedPoster);
      return;
    }

    let cancelled = false;
    const probeVideo = document.createElement('video');
    probeVideo.muted = true;
    probeVideo.playsInline = true;
    probeVideo.preload = 'auto';
    if (shouldCaptureWithCors(src)) {
      probeVideo.crossOrigin = 'anonymous';
    }

    let seekIndex = 0;

    const nextProbeTime = (): number | null => {
      const duration = Number.isFinite(probeVideo.duration)
        ? probeVideo.duration
        : 0;
      while (seekIndex < videoPosterProbeTimes.length) {
        const nextTime = videoPosterProbeTimes[seekIndex];
        seekIndex += 1;
        if (duration > 0 && nextTime >= duration) {
          continue;
        }
        return duration > 0 ? Math.min(nextTime, duration - 0.05) : nextTime;
      }
      return null;
    };

    const seekNextFrame = () => {
      const targetTime = nextProbeTime();
      if (targetTime === null) {
        return;
      }

      try {
        if (Math.abs(probeVideo.currentTime - targetTime) < 0.03) {
          capture();
        } else {
          probeVideo.currentTime = targetTime;
        }
      } catch {
        seekNextFrame();
      }
    };

    const capture = () => {
      if (cancelled) {
        return;
      }

      const posterDataUrl = captureVideoFrame(probeVideo);
      if (!posterDataUrl) {
        seekNextFrame();
        return;
      }

      cacheExtractedVideoPoster(src, posterDataUrl);
      setExtractedPoster(posterDataUrl);
    };

    const seekFirstFrame = () => {
      capture();
      seekNextFrame();
    };

    probeVideo.addEventListener('loadedmetadata', seekFirstFrame);
    probeVideo.addEventListener('loadeddata', capture);
    probeVideo.addEventListener('seeked', capture);
    probeVideo.src = src;
    probeVideo.load();

    return () => {
      cancelled = true;
      probeVideo.removeEventListener('loadedmetadata', seekFirstFrame);
      probeVideo.removeEventListener('loadeddata', capture);
      probeVideo.removeEventListener('seeked', capture);
      try {
        probeVideo.removeAttribute('src');
        probeVideo.load();
      } catch {
        // Ignore cleanup errors from partially loaded media.
      }
    };
  }, [src, playOnHover, extractedPoster]);

  useEffect(() => {
    if (
      !shouldRenderVideoElement ||
      !playOnActivate ||
      !activatedByClickRef.current
    ) {
      return;
    }

    activatedByClickRef.current = false;
    videoRef.current?.play().catch(() => {
      // Browsers can block programmatic playback; the card still opens preview on click.
    });
  }, [shouldRenderVideoElement, playOnActivate]);

  useEffect(() => {
    if (!playOnHover || !showVideo || !shouldRenderVideoElement) {
      return;
    }

    const video = videoRef.current;
    if (!video) {
      return;
    }

    const playPromise = video.play();
    if (playPromise && typeof playPromise.catch === 'function') {
      playPromise.catch(() => {
        // Hover playback is a preview nicety; browsers may still block it.
      });
    }
  }, [playOnHover, showVideo, shouldRenderVideoElement]);

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

      try {
        video.pause();
        video.removeAttribute('src');
        video.load();
      } catch {
        // 忽略释放失败
      }
    };
  }, []);

  const handlePosterClick: React.MouseEventHandler<HTMLImageElement> = (
    event
  ) => {
    if (activateVideoOnClick) {
      activatedByClickRef.current = true;
      setShowVideo(true);
    }
    onClick?.(event);
  };

  const handleVideoClick: React.MouseEventHandler<HTMLVideoElement> = (
    event
  ) => {
    onClick?.(event);
  };

  const handleVideoLoadedData: React.ReactEventHandler<HTMLVideoElement> = (
    event
  ) => {
    videoProps?.onLoadedData?.(event);
    if (!playOnHover || extractedPoster) {
      return;
    }

    const posterDataUrl = captureVideoFrame(event.currentTarget);
    if (!posterDataUrl) {
      return;
    }

    cacheExtractedVideoPoster(src, posterDataUrl);
    setExtractedPoster(posterDataUrl);
  };

  const handlePreviewMouseEnter: React.MouseEventHandler<
    HTMLImageElement | HTMLDivElement | HTMLVideoElement
  > = () => {
    if (!playOnHover) {
      return;
    }

    setShowVideo(true);
  };

  const handlePreviewMouseLeave: React.MouseEventHandler<
    HTMLImageElement | HTMLDivElement | HTMLVideoElement
  > = () => {
    if (!playOnHover) {
      return;
    }

    const video = videoRef.current;
    if (video) {
      try {
        video.pause();
        video.currentTime = 0;
      } catch {
        // Ignore browsers that reject seeking before metadata is ready.
      }
    }

    setShowVideo(false);
  };

  const showPlayOverlay = activateVideoOnClick && !showVideo;

  if (!shouldRenderVideoElement && displayPoster) {
    return renderPreviewWithOverlay(
      <img
        src={displayPoster}
        alt={alt}
        className={`video-poster-preview__media${
          className ? ` ${className}` : ''
        }`}
        loading={imageLoading}
        decoding="async"
        draggable={false}
        referrerPolicy="no-referrer"
        onClick={handlePosterClick}
        onMouseEnter={handlePreviewMouseEnter}
        onMouseLeave={handlePreviewMouseLeave}
      />,
      showPlayOverlay,
      fit
    );
  }

  if (!shouldRenderVideoElement && !resolvedPoster) {
    return renderPreviewWithOverlay(
      <div
        className={`video-poster-preview__placeholder${
          className ? ` ${className}` : ''
        }`}
        aria-hidden="true"
        onClick={
          activateVideoOnClick
            ? () => {
                activatedByClickRef.current = true;
                setShowVideo(true);
              }
            : undefined
        }
        onMouseEnter={handlePreviewMouseEnter}
        onMouseLeave={handlePreviewMouseLeave}
      />,
      showPlayOverlay,
      fit
    );
  }

  return renderPreviewWithOverlay(
    <video
      ref={videoRef}
      src={src}
      poster={videoPoster}
      className={`video-poster-preview__media${
        className ? ` ${className}` : ''
      }`}
      playsInline
      {...videoProps}
      muted={playOnHover || videoProps?.muted}
      onClick={handleVideoClick}
      onLoadedData={handleVideoLoadedData}
      onMouseEnter={handlePreviewMouseEnter}
      onMouseLeave={handlePreviewMouseLeave}
    />,
    false,
    fit
  );
};
