import type {
  Reducer,
  ReducersMapObject,
  Middleware,
  Action,
  StoreEnhancer,
  Store,
  UnknownAction,
} from 'redux'
import {
  applyMiddleware,
  createStore,
  compose,
  combineReducers,
  isPlainObject,
} from 'redux'
import type { DevToolsEnhancerOptions as DevToolsOptions } from './devtoolsExtension'
import { composeWithDevTools } from './devtoolsExtension'

import type {
  ThunkMiddlewareFor,
  GetDefaultMiddleware,
} from './getDefaultMiddleware'
import { buildGetDefaultMiddleware } from './getDefaultMiddleware'
import type {
  ExtractDispatchExtensions,
  ExtractStoreExtensions,
  ExtractStateExtensions,
  UnknownIfNonSpecific,
} from './tsHelpers'
import type { Tuple } from './utils'
import type { GetDefaultEnhancers } from './getDefaultEnhancers'
import { buildGetDefaultEnhancers } from './getDefaultEnhancers'

/**
 * Options for `configureStore()`.
 *
 * @public
 */
export interface ConfigureStoreOptions<
  S = any,
  A extends Action = UnknownAction,
  M extends Tuple<Middlewares<S>> = Tuple<Middlewares<S>>,
  E extends Tuple<Enhancers> = Tuple<Enhancers>,
  P = S,
> {
  /**
   * A single reducer function that will be used as the root reducer, or an
   * object of slice reducers that will be passed to `combineReducers()`.
   */
  reducer: Reducer<S, A, P> | ReducersMapObject<S, A, P>

  /**
   * An array of Redux middleware to install, or a callback receiving `getDefaultMiddleware` and returning a Tuple of middleware.
   * If not supplied, defaults to the set of middleware returned by `getDefaultMiddleware()`.
   *
   * @example `middleware: (gDM) => gDM().concat(logger, apiMiddleware, yourCustomMiddleware)`
   * @see https://redux-toolkit.js.org/api/getDefaultMiddleware#intended-usage
   */
  middleware?: (getDefaultMiddleware: GetDefaultMiddleware<S>) => M

  /**
   * Whether to enable Redux DevTools integration. Defaults to `true`.
   *
   * Additional configuration can be done by passing Redux DevTools options
   */
  devTools?: boolean | DevToolsOptions

  /**
   * Whether to check for duplicate middleware instances. Defaults to `true`.
   */
  duplicateMiddlewareCheck?: boolean

  /**
   * The initial state, same as Redux's createStore.
   * You may optionally specify it to hydrate the state
   * from the server in universal apps, or to restore a previously serialized
   * user session. If you use `combineReducers()` to produce the root reducer
   * function (either directly or indirectly by passing an object as `reducer`),
   * this must be an object with the same shape as the reducer map keys.
   */
  // we infer here, and instead complain if the reducer doesn't match
  preloadedState?: P

  /**
   * The store enhancers to apply. See Redux's `createStore()`.
   * All enhancers will be included before the DevTools Extension enhancer.
   * If you need to customize the order of enhancers, supply a callback
   * function that will receive a `getDefaultEnhancers` function that returns a Tuple,
   * and should return a Tuple of enhancers (such as `getDefaultEnhancers().concat(offline)`).
   * If you only need to add middleware, you can use the `middleware` parameter instead.
   */
  enhancers?: (getDefaultEnhancers: GetDefaultEnhancers<M>) => E
}

export type Middlewares<S> = ReadonlyArray<Middleware<{}, S>>

type Enhancers = ReadonlyArray<StoreEnhancer>

/**
 * A Redux store returned by `configureStore()`. Supports dispatching
 * side-effectful _thunks_ in addition to plain actions.
 *
 * @public
 */
export type EnhancedStore<
  S = any,
  A extends Action = UnknownAction,
  E extends Enhancers = Enhancers,
> = ExtractStoreExtensions<E> &
  Store<S, A, UnknownIfNonSpecific<ExtractStateExtensions<E>>>

/**
 * A friendly abstraction over the standard Redux `createStore()` function.
 *
 * @param options The store configuration.
 * @returns A configured Redux store.
 *
 * @public
 */
export function configureStore<
  S = any,
  A extends Action = UnknownAction,
  M extends Tuple<Middlewares<S>> = Tuple<[ThunkMiddlewareFor<S>]>,
  E extends Tuple<Enhancers> = Tuple<
    [StoreEnhancer<{ dispatch: ExtractDispatchExtensions<M> }>, StoreEnhancer]
  >,
  P = S,
>(options: ConfigureStoreOptions<S, A, M, E, P>): EnhancedStore<S, A, E> {
  const getDefaultMiddleware = buildGetDefaultMiddleware<S>()

  const {
    reducer = undefined,
    middleware,
    devTools = true,
    duplicateMiddlewareCheck = true,
    preloadedState = undefined,
    enhancers = undefined,
  } = options || {}

  let rootReducer: Reducer<S, A, P>

  if (typeof reducer === 'function') {
    rootReducer = reducer
  } else if (isPlainObject(reducer)) {
    rootReducer = combineReducers(reducer) as unknown as Reducer<S, A, P>
  } else {
    throw new Error(
      '`reducer` is a required argument, and must be a function or an object of functions that can be passed to combineReducers',
    )
  }

  if (
    process.env.NODE_ENV !== 'production' &&
    middleware &&
    typeof middleware !== 'function'
  ) {
    throw new Error('`middleware` field must be a callback')
  }

  let finalMiddleware: Tuple<Middlewares<S>>
  if (typeof middleware === 'function') {
    finalMiddleware = middleware(getDefaultMiddleware)

    if (
      process.env.NODE_ENV !== 'production' &&
      !Array.isArray(finalMiddleware)
    ) {
      throw new Error(
        'when using a middleware builder function, an array of middleware must be returned',
      )
    }
  } else {
    finalMiddleware = getDefaultMiddleware()
  }
  if (
    process.env.NODE_ENV !== 'production' &&
    finalMiddleware.some((item: any) => typeof item !== 'function')
  ) {
    throw new Error(
      'each middleware provided to configureStore must be a function',
    )
  }

  if (process.env.NODE_ENV !== 'production' && duplicateMiddlewareCheck) {
    let middlewareReferences = new Set<Middleware<any, S>>()
    finalMiddleware.forEach((middleware) => {
      if (middlewareReferences.has(middleware)) {
        throw new Error(
          'Duplicate middleware references found when creating the store. Ensure that each middleware is only included once.',
        )
      }
      middlewareReferences.add(middleware)
    })
  }

  let finalCompose = compose

  if (devTools) {
    finalCompose = composeWithDevTools({
      // Enable capture of stack traces for dispatched Redux actions
      trace: process.env.NODE_ENV !== 'production',
      ...(typeof devTools === 'object' && devTools),
    })
  }

  const middlewareEnhancer = applyMiddleware(...finalMiddleware)

  const getDefaultEnhancers = buildGetDefaultEnhancers<M>(middlewareEnhancer)

  if (
    process.env.NODE_ENV !== 'production' &&
    enhancers &&
    typeof enhancers !== 'function'
  ) {
    throw new Error('`enhancers` field must be a callback')
  }

  let storeEnhancers =
    typeof enhancers === 'function'
      ? enhancers(getDefaultEnhancers)
      : getDefaultEnhancers()

  if (process.env.NODE_ENV !== 'production' && !Array.isArray(storeEnhancers)) {
    throw new Error('`enhancers` callback must return an array')
  }
  if (
    process.env.NODE_ENV !== 'production' &&
    storeEnhancers.some((item: any) => typeof item !== 'function')
  ) {
    throw new Error(
      'each enhancer provided to configureStore must be a function',
    )
  }
  if (
    process.env.NODE_ENV !== 'production' &&
    finalMiddleware.length &&
    !storeEnhancers.includes(middlewareEnhancer)
  ) {
    console.error(
      'middlewares were provided, but middleware enhancer was not included in final enhancers - make sure to call `getDefaultEnhancers`',
    )
  }

  const composedEnhancer: StoreEnhancer<any> = finalCompose(...storeEnhancers)

  return createStore(rootReducer, preloadedState as P, composedEnhancer)
}
