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

import {ShopActionResult} from '@shopify/shop-minis-platform/actions'

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

export interface ShopActionsDataFetchingResult<R> extends DataHookReturnsBase {
  data: R | null
}

export const useShopActionsDataFetching = <
  S = unknown,
  P extends {fetchPolicy?: DataHookFetchPolicy} = {
    fetchPolicy?: DataHookFetchPolicy
  },
>(
  action: (params: P) => Promise<ShopActionResult<{data: S}>>,
  params: P,
  options: {
    skip?: boolean
    hook?: string
    validator?: (data: S) => void
  }
): ShopActionsDataFetchingResult<S> => {
  const [state, setState] = useState<{
    data: S | null
    loading: boolean
    error: Error | null
  }>({
    data: null,
    loading: true,
    error: null,
  })

  const skip = options?.skip === true
  const {validator, hook} = options
  const runValidator = useCallback(
    (dataToValidate: S) => {
      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,
      }: {
        setLoading?: boolean
        setError?: boolean
        resetOnError?: boolean
        throwOnError?: 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 => ({
            ...curState,
            data: result.data.data,
            loading: false,
            error: validationError ?? null,
          }))
        } else {
          throw result.error
        }
      } catch (err) {
        queryError = formatError({hook}, err)
      }

      const error = validationError || queryError

      if (error && (setError || resetOnError)) {
        setState(curState => ({
          data: resetOnError ? null : curState.data,
          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])

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

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

  return {
    data: state.data,
    loading: state.loading,
    error: state.error,
    refetch,
  }
}
