import {useCallback, useMemo, useRef} from 'react';

import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
import useFetchParallelPages from 'sentry/utils/api/useFetchParallelPages';
import useFetchSequentialPages from 'sentry/utils/api/useFetchSequentialPages';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import parseLinkHeader from 'sentry/utils/parseLinkHeader';
import type {ApiQueryKey} from 'sentry/utils/queryClient';
import {useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
import {mapResponseToReplayRecord} from 'sentry/utils/replays/replayDataUtils';
import type RequestError from 'sentry/utils/requestError/requestError';
import useProjects from 'sentry/utils/useProjects';
import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types';

type Options = {
  /**
   * The organization slug
   */
  orgSlug: string;

  /**
   * The replayId
   */
  replayId: string;

  /**
   * Default: 50
   * You can override this for testing
   */
  errorsPerPage?: number;

  /**
   * Default: 100
   * You can override this for testing
   */
  segmentsPerPage?: number;
};

interface Result {
  attachments: unknown[];
  errors: ReplayError[];
  fetchError: undefined | RequestError;
  fetching: boolean;
  onRetry: () => void;
  projectSlug: string | null;
  replayRecord: ReplayRecord | undefined;
}

/**
 * A react hook to load core replay data over the network.
 *
 * Core replay data includes:
 * 1. The root replay EventTransaction object
 *    - This includes `startTimestamp`, and `tags`
 * 2. RRWeb, Breadcrumb, and Span attachment data
 *    - We make an API call to get a list of segments, each segment contains a
 *      list of attachments
 *    - There may be a few large segments, or many small segments. It depends!
 *      ie: If the replay has many events/errors then there will be many small segments,
 *      or if the page changes rapidly across each pageload, then there will be
 *      larger segments, but potentially fewer of them.
 * 3. Related Event data
 *    - Event details are not part of the attachments payload, so we have to
 *      request them separately
 *
 * This function should stay focused on loading data over the network.
 * Front-end processing, filtering and re-mixing of the different data streams
 * must be delegated to the `ReplayReader` class.
 *
 * @param {orgSlug, replayId} Where to find the root replay event
 * @returns An object representing a unified result of the network requests. Either a single `ReplayReader` data object or fetch errors.
 */
function useReplayData({
  replayId,
  orgSlug,
  errorsPerPage = 50,
  segmentsPerPage = 100,
}: Options): Result {
  const hasFetchedAttachments = useRef(false);
  const projects = useProjects();
  const queryClient = useQueryClient();

  // Fetch every field of the replay. The TS type definition lists every field
  // that's available. It's easier to ask for them all and not have to deal with
  // partial types or nullable fields.
  // We're overfetching for sure.
  const {
    data: replayData,
    isFetching: isFetchingReplay,
    error: fetchReplayError,
  } = useApiQuery<{data: unknown}>([`/organizations/${orgSlug}/replays/${replayId}/`], {
    staleTime: Infinity,
    retry: false,
  });
  const replayRecord = useMemo(
    () => (replayData?.data ? mapResponseToReplayRecord(replayData.data) : undefined),
    [replayData?.data]
  );

  const projectSlug = useMemo(() => {
    if (!replayRecord) {
      return null;
    }
    return projects.projects.find(p => p.id === replayRecord.project_id)?.slug ?? null;
  }, [replayRecord, projects.projects]);

  const getAttachmentsQueryKey = useCallback(
    ({cursor, per_page}): ApiQueryKey => {
      return [
        `/projects/${orgSlug}/${projectSlug}/replays/${replayId}/recording-segments/`,
        {
          query: {
            download: true,
            per_page,
            cursor,
          },
        },
      ];
    },
    [orgSlug, projectSlug, replayId]
  );

  const {
    pages: attachmentPages,
    isFetching: isFetchingAttachments,
    error: fetchAttachmentsError,
  } = useFetchParallelPages({
    enabled: !fetchReplayError && Boolean(projectSlug) && Boolean(replayRecord),
    hits: replayRecord?.count_segments ?? 0,
    getQueryKey: getAttachmentsQueryKey,
    perPage: segmentsPerPage,
  });

  const getErrorsQueryKey = useCallback(
    ({cursor, per_page}): ApiQueryKey => {
      // Clone the `finished_at` time and bump it up one second because finishedAt
      // has the `ms` portion truncated, while replays-events-meta operates on
      // timestamps with `ms` attached. So finishedAt could be at time `12:00:00.000Z`
      // while the event is saved with `12:00:00.450Z`.
      const finishedAtClone = new Date(replayRecord?.finished_at ?? '');
      finishedAtClone.setSeconds(finishedAtClone.getSeconds() + 1);

      return [
        `/organizations/${orgSlug}/replays-events-meta/`,
        {
          query: {
            dataset: DiscoverDatasets.DISCOVER,
            start: replayRecord?.started_at.toISOString(),
            end: finishedAtClone.toISOString(),
            project: ALL_ACCESS_PROJECTS,
            query: `replayId:[${replayRecord?.id}]`,
            per_page,
            cursor,
          },
        },
      ];
    },
    [orgSlug, replayRecord]
  );

  const getPlatformErrorsQueryKey = useCallback(
    ({cursor, per_page}): ApiQueryKey => {
      // Clone the `finished_at` time and bump it up one second because finishedAt
      // has the `ms` portion truncated, while replays-events-meta operates on
      // timestamps with `ms` attached. So finishedAt could be at time `12:00:00.000Z`
      // while the event is saved with `12:00:00.450Z`.
      const finishedAtClone = new Date(replayRecord?.finished_at ?? '');
      finishedAtClone.setSeconds(finishedAtClone.getSeconds() + 1);

      return [
        `/organizations/${orgSlug}/replays-events-meta/`,
        {
          query: {
            dataset: DiscoverDatasets.ISSUE_PLATFORM,
            start: replayRecord?.started_at.toISOString(),
            end: finishedAtClone.toISOString(),
            project: ALL_ACCESS_PROJECTS,
            query: `replayId:[${replayRecord?.id}]`,
            per_page,
            cursor,
          },
        },
      ];
    },
    [orgSlug, replayRecord]
  );

  const {
    pages: errorPages,
    isFetching: isFetchingErrors,
    getLastResponseHeader: lastErrorsResponseHeader,
  } = useFetchParallelPages<{data: ReplayError[]}>({
    enabled: !fetchReplayError && Boolean(projectSlug) && Boolean(replayRecord),
    hits: replayRecord?.count_errors ?? 0,
    getQueryKey: getErrorsQueryKey,
    perPage: errorsPerPage,
  });

  const linkHeader = lastErrorsResponseHeader?.('Link') ?? null;
  const links = parseLinkHeader(linkHeader);
  const {pages: extraErrorPages, isFetching: isFetchingExtraErrors} =
    useFetchSequentialPages<{data: ReplayError[]}>({
      enabled:
        !fetchReplayError &&
        !isFetchingErrors &&
        (!replayRecord?.count_errors || Boolean(links.next?.results)),
      initialCursor: links.next?.cursor,
      getQueryKey: getErrorsQueryKey,
      perPage: errorsPerPage,
    });

  const {pages: platformErrorPages, isFetching: isFetchingPlatformErrors} =
    useFetchSequentialPages<{data: ReplayError[]}>({
      enabled: true,
      getQueryKey: getPlatformErrorsQueryKey,
      perPage: errorsPerPage,
    });

  const clearQueryCache = useCallback(() => {
    queryClient.invalidateQueries({
      queryKey: [`/organizations/${orgSlug}/replays/${replayId}/`],
    });
    queryClient.invalidateQueries({
      queryKey: [
        `/projects/${orgSlug}/${projectSlug}/replays/${replayId}/recording-segments/`,
      ],
    });
    // The next one isn't optimized
    // This statement will invalidate the cache of fetched error events for all replayIds
    queryClient.invalidateQueries({
      queryKey: [`/organizations/${orgSlug}/replays-events-meta/`],
    });
  }, [orgSlug, replayId, projectSlug, queryClient]);

  return useMemo(() => {
    // This hook can enter a state where `fetching` below is false
    // before it is entirely ready (i.e. it has not fetched
    // attachemnts yet). This can cause downstream components to
    // think it is no longer fetching and will display an error
    // because there are no attachments. The below will require
    // that we have attempted to fetch an attachment once (or it
    // errors) before we toggle fetching state to false.
    hasFetchedAttachments.current =
      hasFetchedAttachments.current || isFetchingAttachments;

    const fetching =
      isFetchingReplay ||
      isFetchingAttachments ||
      isFetchingErrors ||
      isFetchingExtraErrors ||
      isFetchingPlatformErrors ||
      (!hasFetchedAttachments.current &&
        !fetchAttachmentsError &&
        Boolean(replayRecord?.count_segments));

    const allErrors = errorPages
      .concat(extraErrorPages)
      .concat(platformErrorPages)
      .flatMap(page => page.data);
    return {
      attachments: attachmentPages.flat(2),
      errors: allErrors,
      fetchError: fetchReplayError ?? undefined,
      fetching,
      onRetry: clearQueryCache,
      projectSlug,
      replayRecord,
    };
  }, [
    attachmentPages,
    clearQueryCache,
    errorPages,
    extraErrorPages,
    fetchReplayError,
    fetchAttachmentsError,
    isFetchingAttachments,
    isFetchingErrors,
    isFetchingExtraErrors,
    isFetchingPlatformErrors,
    isFetchingReplay,
    platformErrorPages,
    projectSlug,
    replayRecord,
  ]);
}

export default useReplayData;
