import React, { JSXElementConstructor } from 'react';
import { Action, createLocation, Location } from 'history';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, mergeMap } from 'rxjs/operators';

import { RouterProvider } from './Router';
import { ResolvedPath } from './PageRouter';
import { MwFn } from './middleware';
import { processRoute } from './processRoute';

export interface PageModule {
    default: JSXElementConstructor<any> | any; // the page to be rendered by the router
    middleware?: MwFn[];
    loadComponent?(resolved: ResolvedPath, ctx: AppContext): Observable<any> | Promise<any>; // a function that can alter the component loaded
}

export type Store = BehaviorSubject<any>;

export const Ctx = React.createContext<{
    Cmp: JSXElementConstructor<any>;
    resolved: ResolvedPath;
    appContext: AppContext;
    location: Location;
    action: Action;
    store: BehaviorSubject<any>;
    pages: string[];
}>({
    store: new BehaviorSubject({}),
    location: createLocation('/'),
    appContext: {
        componentLoader: (resolved: ResolvedPath) =>
            Promise.resolve({
                default: () => {
                    // eslint-disable-next-line no-console
                    console.log('not implemented');
                },
            }),
    },
    action: 'POP',
    Cmp: function NotImplemented() {
        return null;
    },
    pages: [],
    resolved: { id: -1, pageName: 'None', pathname: '/', chunkName: '' },
});

export type PageResponse =
    | {
          kind: 'success';
      }
    | {
          kind: 'error';
          statusCode: number;
          message: string;
      }
    | {
          kind: 'redirect';
          statusCode: number;
          location: string;
      }
    | {
          kind: 'not-found';
          statusCode: number;
      };

export interface AppOutput {
    app?: React.ReactElement;
    appContext: { [index: string]: any };
    response: PageResponse;
}

export interface AppContext {
    componentLoader(resolved: ResolvedPath): Promise<PageModule>;
    [index: string]: any;
}

export type PluginFn = (params: PluginParams, appCtx: AppContext) => AppContext;
export type PluginParams = { initialGQLState: any };

export function app(
    pathname: string,
    search: string,
    initialGQLState = {},
    pages: string[] = [],
    storeState = {},
    el: React.ReactElement,
    plugins: PluginFn[] = [],
): Observable<AppOutput> {
    const store = new BehaviorSubject(storeState);
    const location = createLocation(pathname);
    location.search = search;

    const context: AppContext = {
        componentLoader: (resolved: ResolvedPath) =>
            Promise.resolve({
                default: () => {
                    // eslint-disable-next-line no-console
                    console.log('not implemented');
                },
            }),
    };

    const nextContext = plugins.reduce((ctx, plugin) => {
        return plugin({ initialGQLState }, ctx);
    }, context);

    return processRoute(pathname, location, store, pages, nextContext).pipe(
        mergeMap(({ mod, resolved, response }) => {
            return of({
                response,
                appContext: nextContext,
                app: (
                    <Ctx.Provider
                        value={{
                            Cmp: mod.default,
                            resolved,
                            appContext: nextContext,
                            location,
                            action: 'POP',
                            pages,
                            store,
                        }}
                    >
                        <RouterProvider>{el}</RouterProvider>
                    </Ctx.Provider>
                ),
            });
        }),
        catchError((e) => {
            console.error('error from process', e);
            return of({
                appContext: nextContext,
                response: { kind: 'error' as const, message: e.message, statusCode: 500 },
            });
        }),
    );
}
