import * as React from 'react';
import {
  FC, useState, useCallback, MouseEvent, KeyboardEvent, useRef, useEffect, FocusEvent,
} from 'react';
import { style } from 'typestyle';
import { EventMap, IMedia } from '../../types';
import { useStore } from '../../services/store';
import ThumbnailPlayIcon from '../Icons/ThumbnailPlay';
import { Helpers } from '../../services/helpers';
import { onKeyDownEventCallback } from '../../utils/onKeyDown';
import useBorderRadius from '../../hooks/useBorderRadius';
import useCarouselThumbnailVideoLoading from '../../hooks/useCarouselThumbnailVideoLoading';
import { useThumbnailViewEvent } from '../../hooks/useThumbnailViewEvent';
import { usePostViewerEvents } from '../../hooks/usePostViewerEvents';
import { Packshot, getHeightPercentage } from '../Packshot';
import {
  ugcZIndex,
  videoZIndex,
  activeSquareAspectRatio,
  activePortraitAspectRatio,
  activeStoriesAspectRatio,
  thumbnailTransitionDurationSec,
} from '../consts';
import { resolveCarouselType } from '../../utils/carousel';
import ThumbnailPills from '../ThumbnailPills';

// Module-level cache of image URLs that have been successfully loaded by any Thumbnail instance.
const loadedImageUrls = new Set<string>();

const ASPECT_RATIOS = {
  square: 100,
  portrait: 125,
  stories: 177.78,
} as const;

const ACTIVE_SLIDE_ASPECT_RATIOS = {
  square: activeSquareAspectRatio,
  portrait: activePortraitAspectRatio,
  stories: activeStoriesAspectRatio,
} as const;

const SMALL_THUMBNAIL_WIDTH_PX = 250;

interface ThumbnailProps {
  media: IMedia;
  position: number;
  isCarouselClone?: boolean;
}

export const Thumbnail: FC<ThumbnailProps> = ({ media, position, isCarouselClone = false }) => {
  const store = useStore();
  const thumbnailImage = media.thumbnail ?? media.image;
  const alreadyLoaded = !!thumbnailImage && loadedImageUrls.has(thumbnailImage);
  const [imageVisible, setImageVisible] = useState(alreadyLoaded);
  const [isLoaded, setIsLoaded] = useState(alreadyLoaded);
  const [isMuted, setIsMuted] = useState(true);
  const [isHovered, setIsHovered] = useState(false);
  const [thumbnailWidth, setThumbnailWidth] = useState(0);
  const thumbnailRef = useRef<HTMLDivElement>(null);
  const videoRef = useRef<HTMLVideoElement>(null);
  const {
    thumbnail_corners: cornerType,
    thumbnail_shape: thumbnailShape = 'square',
    thumbnail_packshot: thumbnailPackshot,
    shop_this_look_display: shopThisLookDisplay,
    type: widgetLayout,
    thumbnail_video_autoplay_hover: videoAutoplayOnHover = true,
  } = store.data.settings;

  const borderRadius = useBorderRadius(100, 100, cornerType);
  const { triggerOpenEvent } = usePostViewerEvents();
  useThumbnailViewEvent(thumbnailRef, media, position);
  const productListMode = shopThisLookDisplay === 'product_list';
  const carouselType = resolveCarouselType(store.data.settings);
  const isSpotlightCarousel =
    widgetLayout === 'carousel' && !store.isMobile && carouselType === 'spotlight';
  const isUniformCarousel =
    widgetLayout === 'carousel' && !store.isMobile && carouselType === 'uniform';
  const allowSpotlightGrow = isSpotlightCarousel && !store.productListUniformMode;
  const isActiveSlide = store.currentPosition === media.postIndex;
  const {
    isCarouselThumbnailVideoEnabled,
    shouldLoadCarouselVideo,
    handleCarouselVideoReady,
  } = useCarouselThumbnailVideoLoading(media);
  const shouldHideUsername =
    store.isMobile &&
    // (1) mobile and mosaic/wall,
    (store.data.settings.type !== 'carousel' ||
      (typeof store.data.settings.mobile_carousel_content_size !== 'undefined' &&
         // (2) mobile and content size is more that 1
        store.data.settings.mobile_carousel_content_size !== 1));

  const isLayoutWithSmallThumbnailRule =
    widgetLayout === 'wall' ||
    widgetLayout === 'mosaic' ||
    (widgetLayout === 'carousel' && carouselType === 'buy_box');

  const isSmallDesktopThumbnail =
    isLayoutWithSmallThumbnailRule &&
    !store.isMobile &&
    thumbnailWidth > 0 &&
    thumbnailWidth < SMALL_THUMBNAIL_WIDTH_PX;

  // Show the inline video only when the thumbnail is loaded and the current
  // carousel video is ready to unlock shoulder videos, or on desktop hover.
  const shouldShowInlineActiveVideo =
    (productListMode || store.isMobile) && widgetLayout !== 'carousel' && isActiveSlide;

  const shouldShowVideo =
    media.type === 'video' &&
    media.video &&
    isLoaded &&
    !isCarouselClone &&
    ((isCarouselThumbnailVideoEnabled && shouldLoadCarouselVideo) ||
      shouldShowInlineActiveVideo ||
      (!store.isMobile && videoAutoplayOnHover && isHovered));
  const isMobileCarouselVideo =
    store.isMobile && widgetLayout === 'carousel' && media.type === 'video';
  const isForceMuted = media.isMuted === true;

  const totalPosts = store.data.content.medias.length;
  const postNumber = Math.min(Math.max(media.postIndex + 1, 1), Math.max(totalPosts, 1));

  const handleEvent = useCallback(
    (eventName: keyof EventMap, originalEvent?: Event) => {
      store.triggerEvent(eventName, { post: media, position, originalEvent }, store.data.id);
    },
    [media, position, store],
  );

  const handleImageLoad = useCallback(() => {
    const alreadyFired = !!thumbnailImage && loadedImageUrls.has(thumbnailImage);
    if (thumbnailImage) loadedImageUrls.add(thumbnailImage);
    setIsLoaded(true);
    if (!isCarouselClone && !alreadyFired) handleEvent('thumbnailLoaded');
  }, [handleEvent, isCarouselClone, thumbnailImage]);

  const handleImageError = useCallback(() => {
    // Remove a broken thumbnail from the content list so it does not stay in the carousel.
    handleEvent('thumbnailUnavailable');
    store.setStoreState((state) => ({
      ...state,
      data: {
        ...state.data,
        content: {
          ...state.data.content,
          medias: state.data.content.medias
            .filter((_, idx) => idx !== media.postIndex)
            .map((m, idx) => ({ ...m, postIndex: idx })),
        },
      },
    }));
  }, [handleEvent, media.postIndex, store]);

  const handleImageLoadRef = useRef(handleImageLoad);
  useEffect(() => { handleImageLoadRef.current = handleImageLoad; }, [handleImageLoad]);
  const handleImageErrorRef = useRef(handleImageError);
  useEffect(() => { handleImageErrorRef.current = handleImageError; }, [handleImageError]);

  // Detect image load via off-DOM Image() so background-image is the only fetch.
  // Callbacks are accessed via refs so the Image() is created only once per URL,
  // preventing duplicate thumbnailLoaded events when store updates recycle the callbacks.
  useEffect(() => {
    if (!imageVisible || isLoaded || !thumbnailImage) return undefined;
    const img = new Image();
    img.onload = () => handleImageLoadRef.current();
    img.onerror = () => handleImageErrorRef.current();
    img.src = thumbnailImage;
    return () => { img.onload = null; img.onerror = null; };
  }, [imageVisible, isLoaded, thumbnailImage]);

  const handleOpen = useCallback(
    (e: MouseEvent | KeyboardEvent) => {
      // Open the selected post and keep the carousel position in sync.
      store.setStoreState((state) => ({
        ...state,
        currentPosition: media.postIndex,
      }));

      if (store.data.settings.shop_this_look_display === 'product_list') {
        if (!media.products || media.products.length === 0) {
          return; // don't open product list if no products
        }
      }

      handleEvent('thumbnailClicked', e.nativeEvent);
      store.setStoreState((state) => ({
        ...state,
        post: media,
        currentPosition: media.postIndex,
      }));
      Helpers.pushPostToHistory(media);
      triggerOpenEvent(media);
    },
    [handleEvent, media, store, triggerOpenEvent],
  );

  useEffect(() => {
    if (widgetLayout === 'carousel') {
      const contentSize = store.isMobile
        ? (store.data.settings.mobile_carousel_content_size || 1)
        : (store.data.settings.content_size || 1);
      const total = store.data.content.medias.length;
      const buffer = 2;
      const windowSize = contentSize + buffer * 2;

      if (total <= windowSize) {
        // Small carousel — just load everything
        setImageVisible(true);
        return;
      }

      // Circular window: start = currentPosition - buffer (mod total)
      const start = (((store.currentPosition - buffer) % total) + total) % total;
      // Check if postIndex falls within [start, start + windowSize) on the ring
      const dist = (((media.postIndex - start) % total) + total) % total;
      if (dist < windowSize) {
        setImageVisible(true);
      }
      return;
    }

    const el = thumbnailRef.current;
    if (!el) return;

    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            setImageVisible(true);
            observer.unobserve(entry.target); // stop since we don't need updates
          }
        });
      },
      { rootMargin: '0px', threshold: 0.1 }, // no margin around viewport, trigger when 10% visible
    );

    observer.observe(el);

    // eslint-disable-next-line consistent-return
    return () => observer.unobserve(el);
  }, [
    widgetLayout,
    imageVisible,
    store.currentPosition,
    media.postIndex,
    store.isMobile,
    store.data.settings.content_size,
    store.data.settings.mobile_carousel_content_size,
    store.data.content.medias.length]);

  // Track thumbnail width to hide username when the thumbnail is too small (desktop only)
  useEffect(() => {
    const el = thumbnailRef.current;
    if (!el) return undefined;
    const observer = new ResizeObserver((entries) => {
      setThumbnailWidth(entries[0].contentRect.width);
    });
    observer.observe(el);
    return () => observer.disconnect();
  }, []);

  const showMobilePlayer =
    store.isMobile && store.data.settings.mobile_player_display !== false;
  const showDesktopPlayer =
    !store.isMobile &&
    productListMode &&
    store.data.settings.display_desktop_player === true;

  // When a portal clone is active, hide the original active thumbnail so it
  // does not interfere with the cloned preview that is shown instead.
  const cloneIsActive =
    productListMode && !!store.post && !showMobilePlayer && !showDesktopPlayer;

  let imageClassName = '';
  if (isLoaded) {
    imageClassName = isActiveSlide && !cloneIsActive ? activeImageClass : inactiveImageClass;
  }

  // Get the aspect ratio based on active state and mode
  const getAspectRatio = () => {
    if (allowSpotlightGrow && isActiveSlide) {
      return ACTIVE_SLIDE_ASPECT_RATIOS[thumbnailShape];
    }
    return ASPECT_RATIOS[thumbnailShape];
  };

  // Calculate packshot height percentage, adjusting for active slides
  const getAdjustedPackshotHeight = () => {
    const baseHeight = getHeightPercentage(thumbnailShape, store.isMobile);

    if (allowSpotlightGrow && isActiveSlide) {
      // Calculate ratio between standard and active aspect ratios
      const aspectRatio = ASPECT_RATIOS[thumbnailShape];
      const activeAspectRatio = ACTIVE_SLIDE_ASPECT_RATIOS[thumbnailShape];
      const ratio = aspectRatio / activeAspectRatio;

      // Apply this ratio to maintain consistent visual height
      return baseHeight * ratio;
    }

    return baseHeight;
  };

  const handleMouseEnter = useCallback(() => {
    handleEvent('thumbnailHover');
    setIsHovered(true);
  }, [handleEvent]);

  const handleMouseLeave = useCallback(() => {
    setIsHovered(false);
  }, []);

  const handleFocus = useCallback((e: FocusEvent<HTMLDivElement>) => {
    // Don't update position when modal is open - let focus stay in the modal
    if (store.post) {
      return;
    }

    // Only update carousel position for keyboard-driven focus (Tab navigation).
    // When focus comes from a pointer/mouse click, :focus-visible is not set, so we
    // skip navigation here — handleOpen will take care of it in the same event batch
    // and the carousel will update without triggering a visible slide animation first.
    if (!e.currentTarget.matches(':focus-visible')) {
      return;
    }

    // Update carousel position when thumbnail receives focus (e.g., via Tab navigation)
    // This ensures the carousel slides one thumbnail at a time, staying in sync
    if (store.currentPosition !== media.postIndex) {
      store.setStoreState((state) => ({
        ...state,
        currentPosition: media.postIndex,
      }));
    }
  }, [media.postIndex, store]);

  // Play/pause the thumbnail video based on active-slide state.
  useEffect(() => {
    const video = videoRef.current;
    if (!video || !shouldShowVideo) return;
    video.muted = isForceMuted ? true : isMuted;
    if (isActiveSlide) {
      video.play().catch(() => {
        // Silent catch – browser may block autoplay
      });
    } else {
      video.pause();
    }
  }, [shouldShowVideo, isActiveSlide]);

  // Control video playback based on hover state
  useEffect(() => {
    if (!isMobileCarouselVideo && !store.isMobile && videoAutoplayOnHover && videoRef.current) {
      videoRef.current.muted = isForceMuted ? true : isMuted;
      if (isHovered) {
        videoRef.current.play().catch(() => {
          // Silent catch for browsers that block autoplay
        });
      } else if (!isActiveSlide || !productListMode) {
        videoRef.current.pause();
      }
    }
  }, [isHovered, videoAutoplayOnHover, store.isMobile, isActiveSlide, productListMode]);

  useEffect(() => {
    if (isForceMuted) {
      setIsMuted(true);
    }
  }, [isForceMuted]);

  return (
    <div
      ref={thumbnailRef}
      data-url={media.post}
      data-position={position}
      className={`${store.config.classPrefix}-thumbnail ${thumbnailGrowTransitionClass}`}
      style={{
        ...styles.thumbnail(),
        paddingTop: `${getAspectRatio()}%`,
      }}
      onClick={handleOpen}
      onKeyDown={(e) => onKeyDownEventCallback(e, handleOpen)}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      onFocus={handleFocus}
      tabIndex={store.post ? -1 : 0}
      aria-hidden={store.post ? 'true' : undefined}
      {...(store.post ? { inert: '' } : {})}
      role='link'
      aria-haspopup='dialog'
      aria-label={`Open preview of ${media.source} content created by ${media.username}. Item ${
        postNumber
      } of ${totalPosts}.`}
    >
      <div className={containerClass}>
        <div
          className={`${imageClassName} ${
            isActiveSlide && !cloneIsActive
              ? `${store.config.classPrefix}-thumbnail-content-active`
              : `${store.config.classPrefix}-thumbnail-content-inactive`
          }`}
          {...(isActiveSlide ? { 'data-adl-thumbnail-active': 'true' } : {})}
          style={{
            ...styles.image(borderRadius),
            ...(imageVisible && thumbnailImage && { backgroundImage: `url("${thumbnailImage}")` }),
            // When the portal clone is covering this element, hide it completely.
            ...(isActiveSlide && cloneIsActive && { visibility: 'hidden' }),
          }}
        >
          {media.type === 'video'
          && (!shouldShowVideo || !isActiveSlide)
            ? <ThumbnailPlayIcon style={{ flex: 1 }} />
            : null
          }
          {shouldShowVideo ? (
            <video
              ref={videoRef}
              autoPlay={isActiveSlide || (videoAutoplayOnHover && isHovered)}
              loop
              muted={isForceMuted ? true : isMuted}
              playsInline
              preload={isActiveSlide ? 'auto' : 'metadata'}
              className={`${videoClass} ${store.config.classPrefix}-thumbnail-video`}
              onCanPlay={handleCarouselVideoReady}
              // Hidden until active so the preloaded first frame doesn't cover the thumbnail.
              style={{ opacity: isActiveSlide ? 1 : 0 }}
            >
              <source src={media.video} type='video/mp4' />
            </video>
          ) : null}
          <ThumbnailPills
            post={media}
            showMute={!!shouldShowVideo && !isForceMuted}
            isMuted={isMuted}
            onMuteToggle={() => setIsMuted(!isMuted)}
            showUsernamePill={!!media.username
              && isLoaded
              && !isSmallDesktopThumbnail
              && !shouldHideUsername}
            isMobile={store.isMobile}
            hideUsername={shouldHideUsername}
            classPrefix={store.config.classPrefix || ''}
            showUsernamePillTransition={!store.isMobile}
          />

          {thumbnailPackshot && media.products && media.products.length > 0 && isLoaded
            && !(isActiveSlide && isUniformCarousel && productListMode && store.post !== null)
            ? (
                <Packshot
                  products={media.products}
                  shape={thumbnailShape}
                  cornerType={cornerType}
                  heightPercentage={getAdjustedPackshotHeight()}
                  hidden={
                    isHovered
                    || (isActiveSlide && store.isMobile)
                    || (isActiveSlide && isSpotlightCarousel)
                  }
                />
              )
            : null}
        </div>
      </div>
    </div>
  );
};

const styles = {
  thumbnail: () => ({
    width: '100%',
    position: 'relative' as const,
    cursor: 'pointer',
  }),

  image: (cornerRadius: string) => ({
    position: 'relative' as const,
    flex: 1,
    margin: 4,
    backgroundSize: 'cover',
    backgroundPosition: 'center',
    backgroundColor: '#282828',
    display: 'flex',
    flexDirection: 'column' as const,
    alignItems: 'center',
    overflow: 'hidden',
    borderRadius: cornerRadius,
  }),

} as const;

const containerClass = style({
  position: 'absolute',
  height: '100%',
  width: '100%',
  top: 0,
  left: 0,
  display: 'flex',
  $nest: {
    '&:hover .username': {
      opacity: 1,
    },
  },
});

const activeImageClass = style({
  zIndex: ugcZIndex,
  transition: `transform ${thumbnailTransitionDurationSec}s ease`,
});

const inactiveImageClass = style({
  zIndex: 1,
  transition: `transform ${thumbnailTransitionDurationSec}s ease`,
});

const videoClass = style({
  width: '100%',
  height: '100%',
  objectFit: 'cover',
  position: 'absolute',
  top: 0,
  left: 0,
  zIndex: videoZIndex,
});

const thumbnailGrowTransitionClass = style({
  transition: 'padding-top 0.4s ease',
});

export default Thumbnail;
