'use client';

import { useCallback, useEffect, useRef } from 'react';
import Spinner from './Spinner';
import SiteGrid from './SiteGrid';
import {
  MoreComponentsKey,
  MoreComponentsStateForKeyArgument,
  useMoreComponentsState,
} from '@/state/MoreComponentsState';

const MAX_ATTEMPTS_PER_REQUEST = 5;
const MAX_TOTAL_REQUESTS = 100;
const RETRY_DELAY_IN_SECONDS = 1;

export default function MoreComponents({
  stateKey,
  initialOffset,
  itemsPerRequest,
  getNextComponent,
  label = 'Load more',
  triggerOnView = true,
  prefetch = true,
  itemsClass,
  wrapMoreButtonInSiteGrid,
  debug,
}: {
  stateKey: MoreComponentsKey
  initialOffset: number
  itemsPerRequest: number
  getNextComponent: (offset: number, limit: number) => Promise<{
    nextComponent?: JSX.Element,
    isFinished?: boolean,
    didFail?: boolean,
  }>
  label?: string
  triggerOnView?: boolean
  prefetch?: boolean
  itemsClass?: string
  wrapMoreButtonInSiteGrid?: boolean
  debug?: boolean
}) {
  const { state, setStateForKey } = useMoreComponentsState();

  const setState = useCallback(
    (stateForKey: MoreComponentsStateForKeyArgument) =>
      setStateForKey(stateKey, stateForKey),
    [setStateForKey, stateKey]);

  useEffect(() => {
    setState({ hasMounted: true });
  }, [setState]);

  const {
    hasMounted,
    isLoading,
    indexInView,
    finalIndex,
    didReachMaximumRequests,
    components,
  } = state[stateKey];

  // When prefetching, always stay one request ahead of what's visible
  const furthestIndexToLoad = Math.min(
    prefetch ? (indexInView ?? 0) + 1 : (indexInView ?? 0),
    finalIndex ?? Infinity,
  );

  const indexToLoad = Math.min(
    components.length,
    furthestIndexToLoad,
  );

  const attemptsPerRequest = useRef(0);
  const totalRequests = useRef(0);

  const hasFinalIndexBeenReached =
    finalIndex !== undefined &&
    finalIndex <= components.length - 1;
  
  const areAllComponentsVisible =
    (indexInView ?? 0) >= components.length - 1;

  const showMoreButton =
    isLoading ||
    !hasFinalIndexBeenReached ||
    !areAllComponentsVisible;

  const currentTimeout = useRef<NodeJS.Timeout>();

  const attempt = useCallback(() => {
    // Consider creating temp, anonymous function
    // for error handling
    const attemptRetry = () => {
      if (currentTimeout.current) {
        clearTimeout(currentTimeout.current);
      }
      currentTimeout.current =
        setTimeout(attempt, RETRY_DELAY_IN_SECONDS * 1000);
    };
    if (attemptsPerRequest.current < MAX_ATTEMPTS_PER_REQUEST) {
      if (totalRequests.current < MAX_TOTAL_REQUESTS) {
        attemptsPerRequest.current += 1;
        totalRequests.current += 1;
        setState({ isLoading: true });
        if (debug) {
          // eslint-disable-next-line max-len
          console.log(`GETTING INDEX: #${indexToLoad}, ATTEMPT: #${attemptsPerRequest.current}`);
        }
        getNextComponent(
          initialOffset + indexToLoad * itemsPerRequest,
          itemsPerRequest,
        )
          .then(({ nextComponent, isFinished, didFail }) => {
            if (!didFail) {
              attemptsPerRequest.current = 0;
              setState(state => {
                const updatedComponents = [...state.components];
                if (nextComponent) {
                  updatedComponents[indexToLoad] = nextComponent;
                }
                return {
                  ...state,
                  ...nextComponent && { components: updatedComponents},
                  latestIndexLoaded: indexToLoad,
                  isLoading: false,
                  didReachMaximumRequests: false,
                  ...isFinished && {
                    // Special case when finished on first request
                    finalIndex: indexToLoad === 0 ? -1 : indexToLoad,
                  },
                };
              });
            } else {
              attemptRetry();
            }
          })
          .catch(attemptRetry);
      } else {
        console.log(
          `Max total attempts reached (${MAX_TOTAL_REQUESTS})`
        );
        setState({
          isLoading: false,
          didReachMaximumRequests: true,
        });
      }
    } else {
      console.log(
        `Max attempts per request reached ${MAX_ATTEMPTS_PER_REQUEST}`
      );
      setState({
        isLoading: false,
        didReachMaximumRequests: true,
      });
    }
  }, [
    setState,
    getNextComponent,
    initialOffset,
    indexToLoad,
    itemsPerRequest,
    debug,
  ]);

  useEffect(() => {
    if (
      !isLoading &&
      indexToLoad >= components.length
    ) {
      attempt();
    }
  }, [isLoading, indexToLoad, attempt, components.length]);

  const buttonRef = useRef<HTMLButtonElement>(null);

  const resetRequestsAndRetry = useCallback(() => {
    attemptsPerRequest.current = 0;
    totalRequests.current = 0;
    setState({ didReachMaximumRequests: false });
    attempt();
  }, [attempt, setState]);

  const advance = useCallback(() => {
    if (indexInView === undefined) {
      setState({ indexInView: 0 });
    } else if (
      indexInView <= components.length - 1 && (
        finalIndex === undefined ||
        indexInView < finalIndex
      )
    ) {
      setState({ indexInView: indexInView + 1});
    }
  }, [components.length, finalIndex, indexInView, setState]);

  useEffect(() => {
    // Only add observer if button is rendered
    if (buttonRef.current) {
      const observer = new IntersectionObserver(e => {
        if (triggerOnView && e[0].isIntersecting) {
          advance();
        }
      }, {
        root: null,
        threshold: 0,
      });
      observer.observe(buttonRef.current);
      return () => observer.disconnect();
    }
  }, [triggerOnView, advance]);

  const renderMoreButton = () => 
    <button
      ref={buttonRef}
      className="block w-full subtle"
      onClick={didReachMaximumRequests ? resetRequestsAndRetry : advance}
      disabled={isLoading}
    >
      {isLoading || !hasMounted
        ? <span className="relative inline-block translate-y-[3px]">
          <Spinner size={16} />
        </span>
        : didReachMaximumRequests
          ? 'Try again …'
          : label}
    </button>;

  if (debug) {
    console.log({
      indexInView,
      componentsLength: components.length,
      finalIndex,
      hasFinalIndexBeenReached,
      areAllComponentsVisible,
      isLoading,
    });
  }
  
  return <>
    <div className="space-y-4">
      <div className={itemsClass}>
        {components.slice(0, (indexInView ?? 0) + 1)}
      </div>
      {showMoreButton && (
        wrapMoreButtonInSiteGrid
          ? <SiteGrid contentMain={renderMoreButton()} />
          : renderMoreButton()
      )}
    </div>
  </>;
}
