import joi from "@hapi/joi";
import moduleImporter from "./moduleImporter";
import { RequestParamHandler } from "express";

export interface AutoRoutePropsConfigs {
    routeBasePath?: string;
}

export interface AutoRouteProps {
    server: any;
    callbackHandler: any;
    configs?: AutoRoutePropsConfigs;
}

export enum Methods {
    GET = "GET",
    POST = "POST",
    HEAD = "HEAD",
    PUT = "PUT",
    DELETE = "DELETE",
    USE = "USE",
}

export interface Route {
    id: string;
    method: Methods;
    path: string;
    modulePath: string;
    requestJsonSchema?: {
        body: string;
    };
    responseJsonSchema?: string;
    middleware?: RequestParamHandler[];
}

class AutoRoute<AutoRouteProps> {
    private readonly server: any;
    private readonly callbackHandler: any;
    private readonly configs?: AutoRoutePropsConfigs = {};

    constructor(server, callbackHandler, configs) {
        this.server = server;
        this.callbackHandler = callbackHandler;
        this.configs = configs;
    }

    async mount(routes: Route[]): Promise<string[]> {
        this._guardParams(routes);

        return Promise.all(routes.map(async route => this._addRoute(route)));
    }

    _guardParams(routes) {
        joi.assert(
            routes,
            joi.array().items(
                joi.object({
                    id: joi.string().required(),
                    method: joi.string()
                        .regex(/^GET|POST|HEAD|PUT|DELETE|USE$/)
                        .required(),
                    path: joi.string().min(1).required(),
                    modulePath: joi.string().min(1).required(),
                }).unknown(true),
            ).required(),
        );
    }

    async _addRoute(route) {
        try {
            const {method, path, middleware = []} = route;
            const autoRouteCallback = await this._buildCallback(route);

            if (middleware.length) {
                const middlewareCallbacks: any[] = await this._buildMiddlewareCallbacks(middleware);
                this.server[method.toLowerCase()](path, ...middlewareCallbacks, ...autoRouteCallback);
            } else {
                this.server[method.toLowerCase()](path, ...autoRouteCallback);
            }

            return `${route.id}: mounted`;
        } catch (error) {
            return `${route.id} did not mount due to ${error.message}`;
        }
    }

    async _buildCallback(route) {
        let callbacks: ((req: any, res: any, next: any) => any)[] = [];

        if (this.callbackHandler) {
            callbacks = this.callbackHandler.buildCallback(route);
        }

        const {modulePath} = route;
        const callbackModule = await this._buildMiddlewareCallback(modulePath);

        if (callbackModule) {
            const callback = (req, res, next) => {
                req.config = {
                    route,
                };

                callbackModule(req, res, next);
            };

            callbacks.push(callback);
        }

        return callbacks;
    }

    async _buildMiddlewareCallbacks(middlewares: string[]): Promise<any[]> {
        return Promise.all(middlewares.map(async middleware => this._buildMiddlewareCallback(middleware)));
    }

    async _buildMiddlewareCallback(middlewarePath: string): Promise<(req: any, res: any, next: () => any) => any> {
        return moduleImporter(middlewarePath, (this.configs && this.configs.routeBasePath));
    }
}

export {
    AutoRoute
};
