import {useCallback, useEffect, useMemo, useState} from 'react'

import {ShopAction, PaginationInfo} from '@shopify/shop-minis-platform/actions'

import {DataHookFetchPolicy, PaginatedDataHookReturnsBase} from '../types'
import {formatError, MiniError} from '../utils/errors'

export interface ShopActionsDataFetchingResult<S>
  extends PaginatedDataHookReturnsBase {
  data: S | null
}

export const useShopActionsPaginatedDataFetching = <
  S = unknown,
  P extends {after?: string; fetchPolicy?: DataHookFetchPolicy} = {
    after?: undefined
    fetchPolicy?: DataHookFetchPolicy
  },
>(
  action: ShopAction<P, {data: S; pageInfo: PaginationInfo}>,
  params: P,
  options: {
    skip?: boolean
    hook?: string
    validator?: (data: S) => void
  }
): ShopActionsDataFetchingResult<S> => {
  const [state, setState] = useState<{
    data: S | null
    pageInfo: PaginationInfo
    loading: boolean
    error: Error | null
  }>({
    data: null,
    pageInfo: {hasNextPage: false, endCursor: null},
    loading: false,
    error: null,
  })

  const skip = options?.skip === true
  const {validator, hook} = options
  const runValidator = useCallback(
    (dataToValidate: S): Error | null => {
      try {
        validator?.(dataToValidate)
        return null
      } catch (err) {
        if (err instanceof Error) return err

        return new MiniError({
          hook,
          message: 'Validation failed',
        })
      }
    },
    [validator, hook]
  )

  // Params object is recreated on every render, so we need to memoize it.
  // We don't know what's inside the params object, but we can stringify it.
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const stableParams = useMemo(() => params, [JSON.stringify(params)])

  // There's a lot of complexity here because each type of fetch has different side effects if we are trying to
  // stay close to how Apollo client works. eg:
  // - Initial fetch: set loading, set error, set data, reset on error (don't throw)
  // - change params fetch: set loading, set error, set data, reset on error (don't throw)
  // - refetch fetch: don't set loading, set error, update data, leave data as is was on error (also throw)
  // - fetchMore fetch: don't set loading, don't set error, update data, leave data as is was on error (also throw)
  const fetch = useCallback(
    async (
      extraParams?: Partial<P>,
      {
        setLoading = true,
        setError = true,
        resetOnError = true,
        throwOnError = true,
        appendData = false,
      }: {
        setLoading?: boolean
        setError?: boolean
        resetOnError?: boolean
        throwOnError?: boolean
        appendData?: boolean
      } = {}
    ) => {
      let queryError: Error | null = null
      let validationError: Error | null = null

      setState(curState => ({
        ...curState,
        loading: setLoading ? true : curState.loading,
      }))

      try {
        const result = await action({...stableParams, ...extraParams})

        if (result.ok) {
          validationError = runValidator(result.data.data)

          setState(curState => {
            let newData = result.data.data

            if (
              appendData &&
              curState.data &&
              Array.isArray(curState.data) &&
              Array.isArray(result.data.data)
            ) {
              newData = [...curState.data, ...result.data.data] as S
            }

            return {
              ...curState,
              data: newData,
              pageInfo: result.data.pageInfo,
              loading: false,
              error: validationError ?? null,
            }
          })
        } else {
          throw result.error
        }
      } catch (err) {
        console.log('caught 1', err)
        queryError = formatError({hook}, err)
      }

      const error = validationError || queryError

      if (error && (setError || resetOnError)) {
        setState(curState => ({
          data: resetOnError ? null : curState.data,
          pageInfo: resetOnError
            ? {hasNextPage: false, endCursor: null}
            : curState.pageInfo,
          loading: false,
          error,
        }))
      }

      if (error && throwOnError) {
        throw error
      }
    },
    [action, stableParams, hook, runValidator]
  )

  const refetch = useCallback(async () => {
    await fetch({fetchPolicy: 'network-only'} as Partial<P>, {
      setLoading: false,
      resetOnError: false,
      throwOnError: true,
    })
  }, [fetch])

  const fetchMore = useCallback(async () => {
    if (!state.pageInfo.hasNextPage || !state.pageInfo.endCursor) return

    await fetch({after: state.pageInfo.endCursor} as Partial<P>, {
      setLoading: false,
      setError: false,
      resetOnError: false,
      throwOnError: true,
      appendData: true,
    })
  }, [state.pageInfo.hasNextPage, state.pageInfo.endCursor, fetch])

  useEffect(() => {
    if (skip) return

    fetch(
      {},
      {
        throwOnError: false,
      }
    )
  }, [fetch, skip])

  return {
    data: state.data,
    loading: state.loading,
    error: state.error,
    hasNextPage: state.pageInfo.hasNextPage,
    refetch,
    fetchMore,
  }
}
