import React from 'react';

type ProviderProps<T extends React.ComponentType> =
  React.ComponentProps<T> extends React.PropsWithChildren<infer P> ? P : never;

type ProviderObject<T extends React.ComponentType> = {
  provider: T;
  props: ProviderProps<T>;
  key?: string;
};

type ProviderArray<T extends React.ComponentType> = [
  provider: T,
  props: ProviderProps<T> | string,
  key?: string,
];

type ProviderEntry<
  T extends React.ComponentType<React.PropsWithChildren> = React.ComponentType<
    React.PropsWithChildren<any>
  >,
> = T | ProviderObject<T> | ProviderArray<T>;

/**
 * Composes multiple nested `Provider` components into a single component
 *
 * For Providers that need configuration, they can be defined as part of the
 * provider entry configuration (preferably), or you can define a `key` that
 * can be used for supplying the props at runtime to the `ProviderStack`
 *
 * @example Provider composition and syntax options
 * ```tsx
 * const AppProviders = composeProviders(
 *   // no required configuration
 *   SimpleProvider,
 *   // configured with array syntax
 *   [ThemeProvider, { theme: 'dark' }],
 *   // configured with object syntax
 *   { provider: ThemeProvider, props: { theme: 'dark' } }
 * );
 *
 * <AppProviders>
 *   <App />
 * </AppProviders>
 * ```
 *
 * @example With keys for runtime configuration overrides
 * ```tsx
 * const AppProviders = composeProviders(
 *   // key-only, no initial configuration
 *   [ThemeProvider, 'theme'],
 *   // initial configuration, overrides possible
 *   [ThemeProvider, { theme: 'dark' }, 'theme'],
 *   // configured with object syntax
 *   { provider: ThemeProvider, key: 'theme' }
 * );
 *
 * // Runtime overrides correspond to config key
 * <AppProviders theme={{ theme: 'light' }}>
 *   <App />
 * </AppProviders>
 * ```
 *
 * @param providers - Array of Provider entries
 * @returns A composed `ProviderStack` component that accepts override props
 * for Providers that have a key defined
 */
export default function composeProviders<
  P extends Record<string, object> = Record<string, object>,
>(
  ...providers: ProviderEntry[]
): React.FC<React.PropsWithChildren<P | Record<string, React.ReactNode>>> {
  return function ProviderStack({ children, ...rest }) {
    const restProps = rest as P;
    return providers.reduceRight<React.ReactNode>(
      (accumulatedChildren, currentProvider) => {
        if (Array.isArray(currentProvider)) {
          const [Provider, propsOrKey, maybeKey] = currentProvider;
          const [props, key = ''] =
            typeof propsOrKey === 'string'
              ? [{}, propsOrKey]
              : [propsOrKey, maybeKey];
          const overrideProps = key in restProps ? restProps[key] : {};
          return (
            <Provider {...props} {...overrideProps}>
              {accumulatedChildren}
            </Provider>
          );
        } else if (
          typeof currentProvider === 'object' &&
          'provider' in currentProvider
        ) {
          const { provider: Provider, props = {}, key = '' } = currentProvider;
          const overrideProps = key in restProps ? restProps[key] : {};
          return (
            <Provider {...props} {...overrideProps}>
              {accumulatedChildren}
            </Provider>
          );
        } else {
          const Provider = currentProvider;
          return <Provider>{accumulatedChildren}</Provider>;
        }
      },
      children
    );
  };
}
