import { getHSEnv, HSEnvironment } from '@hubspot/cms-components';
import {
  useVideoPlayer,
  PlayerState,
  VideoCrmObject,
} from '@hubspot/video-player-core';
import { useCallback, useEffect, useMemo, useRef } from 'react';

import { useUserToken } from './hooks/useUserToken.js';
import { usePageMeta } from './hooks/usePageMeta.js';
import {
  trackCompletedPlay,
  trackPlayEvent,
  trackSecondsViewed,
} from './utils/videoAnalytics.js';
import { PageMeta } from './types.js';

type TrackingProps = {
  env: HSEnvironment;
  portalId: number;
  eventProps: {
    crmObjectId: number;
    sessionId: string;
    contactUtk: string | null;
    pageMeta: PageMeta | null;
  };
};

const CHUNK_DURATION_SECONDS = 1;
const RETENTION_TRACKING_INTERVAL_SECONDS = 10;
const RETENTION_MAX_SESSION_DURATION_SECONDS = 60 * 60; // 1 hour

const getTimeRangesDuration = (timeRanges?: TimeRanges) => {
  if (!timeRanges) {
    return 0;
  }

  const durations = [];
  let i = 0;
  while (i < timeRanges.length) {
    durations.push(timeRanges.end(i) - timeRanges.start(i));
    i++;
  }

  return durations.reduce((sum, d) => sum + d, 0);
};

const useTrackVisibilityChange = ({
  isTrackingEnabled,
  trackingStartedAt,
  onVisibilityHidden,
}: {
  isTrackingEnabled: boolean;
  trackingStartedAt?: number;
  onVisibilityHidden: () => void;
}) => {
  useEffect(() => {
    const handleVisibilityChange = () => {
      if (
        !isTrackingEnabled ||
        !trackingStartedAt ||
        document.visibilityState !== 'hidden'
      ) {
        return;
      }

      const sessionDuration = (Date.now() - trackingStartedAt) / 1000;
      if (sessionDuration >= RETENTION_MAX_SESSION_DURATION_SECONDS) {
        // Detach visibilitychange listener after 1 hour max session duration
        document.removeEventListener(
          'visibilitychange',
          handleVisibilityChange
        );
      }

      onVisibilityHidden();
    };

    document.addEventListener('visibilitychange', handleVisibilityChange);
    return () => {
      document.removeEventListener('visibilitychange', handleVisibilityChange);
    };
  }, [isTrackingEnabled, trackingStartedAt, onVisibilityHidden]);
};

const useInitialPlayTracking = ({
  hasStarted,
  env,
  portalId,
  eventProps,
}: TrackingProps & {
  hasStarted: boolean;
}) => {
  const { contactUtk, pageMeta } = eventProps;
  const hasTrackedInitialPlay = useRef(false);

  const trackInitialPlay = async () => {
    hasTrackedInitialPlay.current = true;
    await trackPlayEvent(env, portalId, {
      ...eventProps,
      contactUtk: contactUtk!,
      pageMeta: pageMeta!,
    });
  };

  useEffect(() => {
    if (!contactUtk || !pageMeta) {
      return;
    }

    if (!hasTrackedInitialPlay.current && hasStarted) {
      trackInitialPlay();
    }
  }, [hasStarted, contactUtk, pageMeta]);
};

const useRetentionTracking = ({
  chunksViewed,
  currentTime,
  hasEnded,
  element,
  trackingStartedAt,
  videoDurationSeconds,
  env,
  portalId,
  eventProps,
}: TrackingProps &
  Pick<PlayerState, 'currentTime' | 'hasEnded' | 'element'> & {
    chunksViewed: Map<number, number>;
    trackingStartedAt?: number;
    videoDurationSeconds: number;
  }) => {
  const { contactUtk, pageMeta } = eventProps;
  const { loop, played } = element || {};
  const lastRetentionReportedAt = useRef(0);
  const chunksViewedSinceReportRef = useRef(new Map<number, number>());
  const hasStoppedTracking = useRef(false);
  const currentChunk = Math.floor(currentTime / CHUNK_DURATION_SECONDS);

  const trackNewChunksViewed = useCallback(
    async (endState = false) => {
      if (endState === true) {
        hasStoppedTracking.current = true;
      }

      if (!chunksViewedSinceReportRef.current.size) {
        return;
      }

      const chunksToReport = Object.fromEntries(
        chunksViewedSinceReportRef.current
      );
      chunksViewedSinceReportRef.current.clear();
      lastRetentionReportedAt.current = Date.now();

      const retentionParams = {
        secondsToViews: chunksToReport,
        endState,
      };

      await trackSecondsViewed(
        env,
        portalId,
        { ...eventProps, contactUtk: contactUtk!, pageMeta: pageMeta! },
        retentionParams
      );
    },
    [env, portalId, eventProps]
  );

  const fillRemainingSecondsViewed = () => {
    const totalChunks = Math.ceil(
      videoDurationSeconds / CHUNK_DURATION_SECONDS
    );
    if (chunksViewed.size < totalChunks) {
      // Fill remaining chunks viewed to track for looped video
      for (let i = 0; i < totalChunks; i++) {
        if (
          !chunksViewed.has(i) &&
          !chunksViewedSinceReportRef.current.has(i)
        ) {
          chunksViewedSinceReportRef.current.set(i, 1);
        }
      }
    }
  };

  // Since looped videos don't fire an 'ended' event, determine completion based
  // on played duration or total viewed chunks and track remaining chunks viewed
  const handleLoopedVideo = () => {
    const playedDuration = getTimeRangesDuration(played);
    if (playedDuration >= videoDurationSeconds) {
      // End retention tracking for looped video based on played duration
      fillRemainingSecondsViewed();
      trackNewChunksViewed(true);
      return;
    }

    const totalChunks = Math.floor(
      videoDurationSeconds / CHUNK_DURATION_SECONDS
    );
    if (chunksViewed.size >= totalChunks) {
      // End retention tracking for looped video watched completely
      trackNewChunksViewed(true);
    }
  };

  useEffect(() => {
    if (
      !contactUtk ||
      !pageMeta ||
      !trackingStartedAt ||
      hasStoppedTracking.current
    ) {
      return;
    }

    // Check if max session duration exceeded
    const sessionDuration = (Date.now() - trackingStartedAt) / 1000;
    if (sessionDuration >= RETENTION_MAX_SESSION_DURATION_SECONDS) {
      // End retention tracking due to session duration reaching max
      trackNewChunksViewed(false);
      hasStoppedTracking.current = true;
      if (element?.loop) {
        element.loop = false;
      }
      return;
    }

    if (hasEnded && chunksViewedSinceReportRef.current.size > 0) {
      trackNewChunksViewed(true);
      return;
    }

    if (loop) {
      handleLoopedVideo();
    }

    if (chunksViewed.has(currentChunk)) {
      return;
    }

    chunksViewed.set(currentChunk, (chunksViewed.get(currentChunk) || 0) + 1);
    chunksViewedSinceReportRef.current.set(
      currentChunk,
      (chunksViewedSinceReportRef.current.get(currentChunk) || 0) + 1
    );

    if (!lastRetentionReportedAt.current) {
      lastRetentionReportedAt.current = Date.now();
    }

    const secondsSinceLastReport =
      (Date.now() - lastRetentionReportedAt.current) / 1000;

    if (secondsSinceLastReport >= RETENTION_TRACKING_INTERVAL_SECONDS) {
      trackNewChunksViewed(false);
    }
  }, [
    trackNewChunksViewed,
    trackingStartedAt,
    currentChunk,
    hasEnded,
    contactUtk,
    pageMeta,
    loop,
    played,
    videoDurationSeconds,
  ]);

  const onVisibilityHidden = useCallback(() => {
    if (!hasStoppedTracking.current) {
      // End retention tracking on visibility change
      trackNewChunksViewed(true);
    }
  }, [trackNewChunksViewed]);

  useTrackVisibilityChange({
    isTrackingEnabled: Boolean(contactUtk && pageMeta),
    trackingStartedAt,
    onVisibilityHidden,
  });
};

const useCompletedPlayTracking = ({
  chunksViewed,
  hasEnded,
  element,
  trackingStartedAt,
  videoDurationSeconds,
  env,
  portalId,
  eventProps,
}: TrackingProps &
  Pick<PlayerState, 'hasEnded' | 'element'> & {
    chunksViewed: Map<number, number>;
    trackingStartedAt?: number;
    videoDurationSeconds: number;
  }) => {
  const { contactUtk, pageMeta } = eventProps;
  const { loop, played } = element || {};
  const hasTrackedCompletion = useRef(false);

  const trackCompletion = useCallback(async () => {
    const secondsViewed = chunksViewed.size * CHUNK_DURATION_SECONDS;
    if (
      !videoDurationSeconds ||
      !secondsViewed ||
      hasTrackedCompletion.current
    ) {
      return;
    }

    hasTrackedCompletion.current = true;
    const completedPlayParams = {
      secondsViewed,
      percentageViewed: Math.round(
        (secondsViewed / videoDurationSeconds) * 100
      ),
    };

    await trackCompletedPlay(
      env,
      portalId,
      { ...eventProps, contactUtk: contactUtk!, pageMeta: pageMeta! },
      completedPlayParams
    );
  }, [env, portalId, eventProps, videoDurationSeconds]);

  // Since looped videos don't fire an 'ended' event, determine completion based
  // on played duration or total viewed chunks and track completed-play
  const handleLoopedVideo = () => {
    const playedDuration = getTimeRangesDuration(played);
    if (playedDuration >= videoDurationSeconds) {
      // Track completed play for looped video based on played duration
      trackCompletion();
      return;
    }

    const totalChunks = Math.floor(
      videoDurationSeconds / CHUNK_DURATION_SECONDS
    );
    if (chunksViewed.size >= totalChunks) {
      // Track completed play for looped video watched completely
      trackCompletion();
    }
  };

  useEffect(() => {
    if (!contactUtk || !pageMeta || hasTrackedCompletion.current) {
      return;
    }

    if (hasEnded) {
      trackCompletion();
      return;
    }

    if (loop) {
      handleLoopedVideo();
    }
  }, [
    trackCompletion,
    hasEnded,
    contactUtk,
    pageMeta,
    loop,
    played,
    videoDurationSeconds,
  ]);

  const onVisibilityHidden = useCallback(() => {
    if (!hasTrackedCompletion.current) {
      // End retention tracking on visibility change
      trackCompletion();
    }
  }, [trackCompletion]);

  useTrackVisibilityChange({
    isTrackingEnabled: Boolean(contactUtk && pageMeta),
    trackingStartedAt,
    onVisibilityHidden,
  });
};

const TrackHSVideoAnalytics = ({
  video,
  portalId,
}: {
  video: VideoCrmObject;
  portalId: number;
}) => {
  const hsEnv = getHSEnv();
  const { utk: contactUtk } = useUserToken();
  const { state } = useVideoPlayer();
  const { currentTime, hasStarted, hasEnded, element, playbackStartedAt } =
    state;
  const chunksViewedRef = useRef(new Map<number, number>());
  const pageMeta = usePageMeta();
  // session ID used to match events across initial/secondsViewed/completed events
  const sessionId = useRef(String(Date.now()));

  const eventProps = useMemo(
    () => ({
      pageMeta,
      crmObjectId: video.crmObjectId,
      sessionId: sessionId.current,
      contactUtk,
    }),
    [pageMeta, video, contactUtk]
  );
  const videoDurationSeconds = video.duration / 1000;
  const sharedProps = { env: hsEnv, portalId, eventProps };

  useInitialPlayTracking({
    ...sharedProps,
    hasStarted,
  });

  useRetentionTracking({
    ...sharedProps,
    chunksViewed: chunksViewedRef.current,
    currentTime,
    hasEnded,
    element,
    trackingStartedAt: playbackStartedAt,
    videoDurationSeconds,
  });

  useCompletedPlayTracking({
    ...sharedProps,
    chunksViewed: chunksViewedRef.current,
    hasEnded,
    element,
    trackingStartedAt: playbackStartedAt,
    videoDurationSeconds,
  });

  return null;
};

export default TrackHSVideoAnalytics;
