import type {
  ActionFromMatcher,
  Matcher,
  UnionToIntersection,
} from './tsHelpers'
import { hasMatchFunction } from './tsHelpers'
import type {
  AsyncThunk,
  AsyncThunkFulfilledActionCreator,
  AsyncThunkPendingActionCreator,
  AsyncThunkRejectedActionCreator,
} from './createAsyncThunk'

/** @public */
export type ActionMatchingAnyOf<
  Matchers extends [Matcher<any>, ...Matcher<any>[]]
> = ActionFromMatcher<Matchers[number]>

/** @public */
export type ActionMatchingAllOf<
  Matchers extends [Matcher<any>, ...Matcher<any>[]]
> = UnionToIntersection<ActionMatchingAnyOf<Matchers>>

const matches = (matcher: Matcher<any>, action: any) => {
  if (hasMatchFunction(matcher)) {
    return matcher.match(action)
  } else {
    return matcher(action)
  }
}

/**
 * A higher-order function that returns a function that may be used to check
 * whether an action matches any one of the supplied type guards or action
 * creators.
 *
 * @param matchers The type guards or action creators to match against.
 *
 * @public
 */
export function isAnyOf<Matchers extends [Matcher<any>, ...Matcher<any>[]]>(
  ...matchers: Matchers
) {
  return (action: any): action is ActionMatchingAnyOf<Matchers> => {
    return matchers.some((matcher) => matches(matcher, action))
  }
}

/**
 * A higher-order function that returns a function that may be used to check
 * whether an action matches all of the supplied type guards or action
 * creators.
 *
 * @param matchers The type guards or action creators to match against.
 *
 * @public
 */
export function isAllOf<Matchers extends [Matcher<any>, ...Matcher<any>[]]>(
  ...matchers: Matchers
) {
  return (action: any): action is ActionMatchingAllOf<Matchers> => {
    return matchers.every((matcher) => matches(matcher, action))
  }
}

/**
 * @param action A redux action
 * @param validStatus An array of valid meta.requestStatus values
 *
 * @internal
 */
export function hasExpectedRequestMetadata(
  action: any,
  validStatus: readonly string[]
) {
  if (!action || !action.meta) return false

  const hasValidRequestId = typeof action.meta.requestId === 'string'
  const hasValidRequestStatus =
    validStatus.indexOf(action.meta.requestStatus) > -1

  return hasValidRequestId && hasValidRequestStatus
}

function isAsyncThunkArray(a: [any] | AnyAsyncThunk[]): a is AnyAsyncThunk[] {
  return (
    typeof a[0] === 'function' &&
    'pending' in a[0] &&
    'fulfilled' in a[0] &&
    'rejected' in a[0]
  )
}

export type UnknownAsyncThunkPendingAction = ReturnType<
  AsyncThunkPendingActionCreator<unknown>
>

export type PendingActionFromAsyncThunk<T extends AnyAsyncThunk> =
  ActionFromMatcher<T['pending']>

/**
 * A higher-order function that returns a function that may be used to check
 * whether an action was created by an async thunk action creator, and that
 * the action is pending.
 *
 * @public
 */
export function isPending(): (
  action: any
) => action is UnknownAsyncThunkPendingAction
/**
 * A higher-order function that returns a function that may be used to check
 * whether an action belongs to one of the provided async thunk action creators,
 * and that the action is pending.
 *
 * @param asyncThunks (optional) The async thunk action creators to match against.
 *
 * @public
 */
export function isPending<
  AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
>(
  ...asyncThunks: AsyncThunks
): (action: any) => action is PendingActionFromAsyncThunk<AsyncThunks[number]>
/**
 * Tests if `action` is a pending thunk action
 * @public
 */
export function isPending(action: any): action is UnknownAsyncThunkPendingAction
export function isPending<
  AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
>(...asyncThunks: AsyncThunks | [any]) {
  if (asyncThunks.length === 0) {
    return (action: any) => hasExpectedRequestMetadata(action, ['pending'])
  }

  if (!isAsyncThunkArray(asyncThunks)) {
    return isPending()(asyncThunks[0])
  }

  return (
    action: any
  ): action is PendingActionFromAsyncThunk<AsyncThunks[number]> => {
    // note: this type will be correct because we have at least 1 asyncThunk
    const matchers: [Matcher<any>, ...Matcher<any>[]] = asyncThunks.map(
      (asyncThunk) => asyncThunk.pending
    ) as any

    const combinedMatcher = isAnyOf(...matchers)

    return combinedMatcher(action)
  }
}

export type UnknownAsyncThunkRejectedAction = ReturnType<
  AsyncThunkRejectedActionCreator<unknown, unknown>
>

export type RejectedActionFromAsyncThunk<T extends AnyAsyncThunk> =
  ActionFromMatcher<T['rejected']>

/**
 * A higher-order function that returns a function that may be used to check
 * whether an action was created by an async thunk action creator, and that
 * the action is rejected.
 *
 * @public
 */
export function isRejected(): (
  action: any
) => action is UnknownAsyncThunkRejectedAction
/**
 * A higher-order function that returns a function that may be used to check
 * whether an action belongs to one of the provided async thunk action creators,
 * and that the action is rejected.
 *
 * @param asyncThunks (optional) The async thunk action creators to match against.
 *
 * @public
 */
export function isRejected<
  AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
>(
  ...asyncThunks: AsyncThunks
): (action: any) => action is RejectedActionFromAsyncThunk<AsyncThunks[number]>
/**
 * Tests if `action` is a rejected thunk action
 * @public
 */
export function isRejected(
  action: any
): action is UnknownAsyncThunkRejectedAction
export function isRejected<
  AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
>(...asyncThunks: AsyncThunks | [any]) {
  if (asyncThunks.length === 0) {
    return (action: any) => hasExpectedRequestMetadata(action, ['rejected'])
  }

  if (!isAsyncThunkArray(asyncThunks)) {
    return isRejected()(asyncThunks[0])
  }

  return (
    action: any
  ): action is RejectedActionFromAsyncThunk<AsyncThunks[number]> => {
    // note: this type will be correct because we have at least 1 asyncThunk
    const matchers: [Matcher<any>, ...Matcher<any>[]] = asyncThunks.map(
      (asyncThunk) => asyncThunk.rejected
    ) as any

    const combinedMatcher = isAnyOf(...matchers)

    return combinedMatcher(action)
  }
}

export type UnknownAsyncThunkRejectedWithValueAction = ReturnType<
  AsyncThunkRejectedActionCreator<unknown, unknown>
>

export type RejectedWithValueActionFromAsyncThunk<T extends AnyAsyncThunk> =
  ActionFromMatcher<T['rejected']> &
    (T extends AsyncThunk<any, any, { rejectValue: infer RejectedValue }>
      ? { payload: RejectedValue }
      : unknown)

/**
 * A higher-order function that returns a function that may be used to check
 * whether an action was created by an async thunk action creator, and that
 * the action is rejected with value.
 *
 * @public
 */
export function isRejectedWithValue(): (
  action: any
) => action is UnknownAsyncThunkRejectedAction
/**
 * A higher-order function that returns a function that may be used to check
 * whether an action belongs to one of the provided async thunk action creators,
 * and that the action is rejected with value.
 *
 * @param asyncThunks (optional) The async thunk action creators to match against.
 *
 * @public
 */
export function isRejectedWithValue<
  AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
>(
  ...asyncThunks: AsyncThunks
): (
  action: any
) => action is RejectedWithValueActionFromAsyncThunk<AsyncThunks[number]>
/**
 * Tests if `action` is a rejected thunk action with value
 * @public
 */
export function isRejectedWithValue(
  action: any
): action is UnknownAsyncThunkRejectedAction
export function isRejectedWithValue<
  AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
>(...asyncThunks: AsyncThunks | [any]) {
  const hasFlag = (action: any): action is any => {
    return action && action.meta && action.meta.rejectedWithValue
  }

  if (asyncThunks.length === 0) {
    return (action: any) => {
      const combinedMatcher = isAllOf(isRejected(...asyncThunks), hasFlag)

      return combinedMatcher(action)
    }
  }

  if (!isAsyncThunkArray(asyncThunks)) {
    return isRejectedWithValue()(asyncThunks[0])
  }

  return (
    action: any
  ): action is RejectedActionFromAsyncThunk<AsyncThunks[number]> => {
    const combinedMatcher = isAllOf(isRejected(...asyncThunks), hasFlag)

    return combinedMatcher(action)
  }
}

export type UnknownAsyncThunkFulfilledAction = ReturnType<
  AsyncThunkFulfilledActionCreator<unknown, unknown>
>

export type FulfilledActionFromAsyncThunk<T extends AnyAsyncThunk> =
  ActionFromMatcher<T['fulfilled']>

/**
 * A higher-order function that returns a function that may be used to check
 * whether an action was created by an async thunk action creator, and that
 * the action is fulfilled.
 *
 * @public
 */
export function isFulfilled(): (
  action: any
) => action is UnknownAsyncThunkFulfilledAction
/**
 * A higher-order function that returns a function that may be used to check
 * whether an action belongs to one of the provided async thunk action creators,
 * and that the action is fulfilled.
 *
 * @param asyncThunks (optional) The async thunk action creators to match against.
 *
 * @public
 */
export function isFulfilled<
  AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
>(
  ...asyncThunks: AsyncThunks
): (action: any) => action is FulfilledActionFromAsyncThunk<AsyncThunks[number]>
/**
 * Tests if `action` is a fulfilled thunk action
 * @public
 */
export function isFulfilled(
  action: any
): action is UnknownAsyncThunkFulfilledAction
export function isFulfilled<
  AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
>(...asyncThunks: AsyncThunks | [any]) {
  if (asyncThunks.length === 0) {
    return (action: any) => hasExpectedRequestMetadata(action, ['fulfilled'])
  }

  if (!isAsyncThunkArray(asyncThunks)) {
    return isFulfilled()(asyncThunks[0])
  }

  return (
    action: any
  ): action is FulfilledActionFromAsyncThunk<AsyncThunks[number]> => {
    // note: this type will be correct because we have at least 1 asyncThunk
    const matchers: [Matcher<any>, ...Matcher<any>[]] = asyncThunks.map(
      (asyncThunk) => asyncThunk.fulfilled
    ) as any

    const combinedMatcher = isAnyOf(...matchers)

    return combinedMatcher(action)
  }
}

export type UnknownAsyncThunkAction =
  | UnknownAsyncThunkPendingAction
  | UnknownAsyncThunkRejectedAction
  | UnknownAsyncThunkFulfilledAction

export type AnyAsyncThunk = {
  pending: { match: (action: any) => action is any }
  fulfilled: { match: (action: any) => action is any }
  rejected: { match: (action: any) => action is any }
}

export type ActionsFromAsyncThunk<T extends AnyAsyncThunk> =
  | ActionFromMatcher<T['pending']>
  | ActionFromMatcher<T['fulfilled']>
  | ActionFromMatcher<T['rejected']>

/**
 * A higher-order function that returns a function that may be used to check
 * whether an action was created by an async thunk action creator.
 *
 * @public
 */
export function isAsyncThunkAction(): (
  action: any
) => action is UnknownAsyncThunkAction
/**
 * A higher-order function that returns a function that may be used to check
 * whether an action belongs to one of the provided async thunk action creators.
 *
 * @param asyncThunks (optional) The async thunk action creators to match against.
 *
 * @public
 */
export function isAsyncThunkAction<
  AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
>(
  ...asyncThunks: AsyncThunks
): (action: any) => action is ActionsFromAsyncThunk<AsyncThunks[number]>
/**
 * Tests if `action` is a thunk action
 * @public
 */
export function isAsyncThunkAction(
  action: any
): action is UnknownAsyncThunkAction
export function isAsyncThunkAction<
  AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
>(...asyncThunks: AsyncThunks | [any]) {
  if (asyncThunks.length === 0) {
    return (action: any) =>
      hasExpectedRequestMetadata(action, ['pending', 'fulfilled', 'rejected'])
  }

  if (!isAsyncThunkArray(asyncThunks)) {
    return isAsyncThunkAction()(asyncThunks[0])
  }

  return (
    action: any
  ): action is ActionsFromAsyncThunk<AsyncThunks[number]> => {
    // note: this type will be correct because we have at least 1 asyncThunk
    const matchers: [Matcher<any>, ...Matcher<any>[]] = [] as any

    for (const asyncThunk of asyncThunks) {
      matchers.push(
        asyncThunk.pending,
        asyncThunk.rejected,
        asyncThunk.fulfilled
      )
    }

    const combinedMatcher = isAnyOf(...matchers)

    return combinedMatcher(action)
  }
}
