import {expacl} from "../../expacl";
import {Request, Response, NextFunction} from 'express-serve-static-core';


const middleware: expacl.MiddlewareFactory = (opts: expacl.ACLOptions) => {
    const MAX_SUB_VARIANTS = 20;

    const route2parsed = (route: expacl.ACLRoute): expacl.ACLParsedRoute => {
        const splittedPart: (string | object)[] = (typeof route.path === 'string') ? route.path.split("/").filter(p => p.length > 0) : [route.path];

        return {
            path: splittedPart,
            pathLen: splittedPart.length,
            roles: route.roles ? ((Array.isArray(route.roles) ? route.roles : [route.roles])) : ['*'],
            methods: route.methods ?
                (Array.isArray(route.methods) ? route.methods.map(m => m.toLowerCase() as expacl.Method) :
                    [route.methods.toLowerCase() as expacl.Method]) : ['*'],
            action: route.action || opts.defaultAction || 'allow',
        };
    };

    const parsedRouteSubVariants = (route: expacl.ACLParsedRoute): expacl.ACLParsedRoute[] => {
        const arr = [route]
            .concat(
                new Array(MAX_SUB_VARIANTS)
                    .fill(undefined)
                    .map((_: any, idx: number) => Object.assign(
                        {},
                        route,
                        {
                            path: route.path.concat(new Array(idx + 1).fill('*')),
                            pathLen: route.pathLen + idx + 1
                        }))
            );

        return arr;
    };

    const parseRoute = (route: expacl.ACLRoute): expacl.ACLParsedRoute[] => {
        const parsedRoute = route2parsed(route);

        if (route.subroutes && route.subroutes.length > 0) {
            if (parsedRoute.path[parsedRoute.pathLen - 1] === '*') {
                throw new Error(`${'*'} (any) path route should not have subroutes`);
            }

            const subroutes = parseRoutes(route.subroutes);
            return subroutes.map(r => {
                return {
                    path: parsedRoute.path.concat(r.path),
                    pathLen: parsedRoute.pathLen + r.pathLen,
                    roles: r.roles,
                    methods: r.methods,
                    action: r.action,
                } as expacl.ACLParsedRoute
            }).concat(route.transient ? [] : [parsedRoute]);
        } else {
            return route.transient ? [] : ((parsedRoute.path[parsedRoute.pathLen - 1] === '*') ? parsedRouteSubVariants(parsedRoute) : [parsedRoute]);
        }
    };

    const parseRoutes = (routes: expacl.ACLRoute[]): expacl.ACLParsedRoute[] => {
        return routes
            .map((route: expacl.ACLRoute): expacl.ACLParsedRoute[] => parseRoute(route))
            .reduce((acc: expacl.ACLParsedRoute[], val: expacl.ACLParsedRoute[]) => acc.concat(val), []);
    };

    const routes = parseRoutes(opts.routes);

    const _middleware: expacl.Middleware = (req: Request, res: Response, next: NextFunction): any => {
        const path: string[] = (opts.resource ? opts.resource(req as expacl.ACLRequest) : req.url).split('/').filter((p: string) => p.length > 0);
        const pathLen = path.length;
        const method: expacl.Method = req.method.toLowerCase() as expacl.Method;

        const notAllowed = () => {
            const authenticated = opts.authenticated || ((req: expacl.ACLRequest) => !!req.user);
            if (authenticated(req as expacl.ACLRequest)) {
                return (opts.onNotAuthorized) ? opts.onNotAuthorized(req as expacl.ACLRequest, res, next) : res.status(403).send("403 Not authorized");
            } else {
                return (opts.onNotAuthenticated) ? opts.onNotAuthenticated(req as expacl.ACLRequest, res, next) : res.status(401).send("401 Not authenticated");
            }
        };

        const notFound = () => (opts.onNotFound) ? opts.onNotFound(req as expacl.ACLRequest, res, next) : res.status(404).send("404 Not found");

        const route: expacl.ACLParsedRoute | undefined = routes.find(r => {
            const shallowCheck = (r.pathLen === pathLen) && (r.methods.includes('*') || r.methods.includes(method));
            if (!shallowCheck) {
                return false;
            }

            const pathCheck = r.path.reduce((acc, p, idx) => {
                return acc && ((typeof p === 'string') ? ((p === '*') || (p === path[idx])) : ((p as RegExp).test(path[idx])));
            }, true);

            return pathCheck;
        });

        if (!route) {
            return (opts.missingRoute != 'allow') ? notFound() : next();
        }

        if (route.roles.includes('*')) {
            return next();
        }

        const roles: string[] = (opts.roles ? opts.roles(req as expacl.ACLRequest) : ((req as expacl.ACLRequest).user ? (req as expacl.ACLRequest).user.roles : undefined)) || [];
        const roleCheck = roles.findIndex(r => route.roles.includes(r));

        if (roleCheck === -1) {
            notAllowed();
        }

        return next();
    };

    return _middleware;
};

export {middleware};