import {
    type IRouter,
    Router,
    type Request,
    type Response,
    type RequestHandler,
} from 'express';
import type { Logger } from '../logger';
import { type IUnleashConfig, NONE } from '../types';
import { handleErrors } from './util';
import requireContentType from '../middleware/content_type_checker';
import { PermissionError } from '../error';
import { storeRequestedRoute } from '../middleware/response-time-metrics';

type IRequestHandler<P = any, ResBody = any, ReqBody = any, ReqQuery = any> = (
    req: Request<P, ResBody, ReqBody, ReqQuery>,
    res: Response<ResBody>,
) => Promise<void> | void;

type Permission = string | string[];

interface IRouteOptionsBase {
    path: string;
    permission: Permission;
    middleware?: RequestHandler[];
    handler: IRequestHandler;
    acceptedContentTypes?: string[];
}

interface IRouteOptionsGet extends IRouteOptionsBase {
    method: 'get';
}

interface IRouteOptionsNonGet extends IRouteOptionsBase {
    method: 'post' | 'put' | 'patch' | 'delete';
    acceptAnyContentType?: boolean;
}

type IRouteOptions = IRouteOptionsNonGet | IRouteOptionsGet;

const checkPermission =
    (permission: Permission = []) =>
    async (req, res, next) => {
        const permissions = (
            Array.isArray(permission) ? permission : [permission]
        ).filter((p) => p !== NONE);

        if (!permissions.length) {
            return next();
        }
        if (req.checkRbac && (await req.checkRbac(permissions))) {
            return next();
        }
        return res.status(403).json(new PermissionError(permissions)).end();
    };

const checkPrivateProjectPermissions = () => async (req, res, next) => {
    if (
        !req.checkPrivateProjectPermissions ||
        (await req.checkPrivateProjectPermissions())
    ) {
        return next();
    }
    return res.status(404).end();
};

/**
 * Base class for Controllers to standardize binding to express Router.
 *
 * This class will take care of the following:
 * - try/catch inside RequestHandler
 * - await if the RequestHandler returns a promise.
 * - access control
 */
export default class Controller {
    private ownLogger: Logger;

    app: IRouter;

    config: IUnleashConfig;

    constructor(config: IUnleashConfig) {
        this.ownLogger = config.getLogger(
            `controller/${this.constructor.name}`,
        );
        this.app = Router();
        this.config = config;
    }

    private useRouteErrorHandler(handler: IRequestHandler): IRequestHandler {
        return async (req: Request, res: Response) => {
            try {
                await handler(req, res);
            } catch (error) {
                handleErrors(res, this.ownLogger, error);
            }
        };
    }

    private useContentTypeMiddleware(options: IRouteOptions): RequestHandler[] {
        const { middleware = [], acceptedContentTypes = [] } = options;

        return options.method === 'get' || options.acceptAnyContentType
            ? middleware
            : [requireContentType(...acceptedContentTypes), ...middleware];
    }

    route(options: IRouteOptions): void {
        this.app[options.method](
            options.path,
            storeRequestedRoute,
            checkPermission(options.permission),
            checkPrivateProjectPermissions(),
            this.useContentTypeMiddleware(options),
            this.useRouteErrorHandler(options.handler.bind(this)),
        );
    }

    get(
        path: string,
        handler: IRequestHandler,
        permission: Permission = NONE,
    ): void {
        this.route({
            method: 'get',
            path,
            handler,
            permission,
        });
    }

    post(
        path: string,
        handler: IRequestHandler,
        permission: Permission = NONE,
        ...acceptedContentTypes: string[]
    ): void {
        this.route({
            method: 'post',
            path,
            handler,
            permission,
            acceptedContentTypes,
        });
    }

    put(
        path: string,
        handler: IRequestHandler,
        permission: Permission = NONE,
        ...acceptedContentTypes: string[]
    ): void {
        this.route({
            method: 'put',
            path,
            handler,
            permission,
            acceptedContentTypes,
        });
    }

    patch(
        path: string,
        handler: IRequestHandler,
        permission: Permission = NONE,
        ...acceptedContentTypes: string[]
    ): void {
        this.route({
            method: 'patch',
            path,
            handler,
            permission,
            acceptedContentTypes,
        });
    }

    delete(
        path: string,
        handler: IRequestHandler,
        permission: Permission = NONE,
    ): void {
        this.route({
            method: 'delete',
            path,
            handler,
            permission,
            acceptAnyContentType: true,
        });
    }

    fileupload(
        path: string,
        filehandler: IRequestHandler,
        handler: Function,
        permission: Permission = NONE,
    ): void {
        this.app.post(
            path,
            storeRequestedRoute,
            checkPermission(permission),
            checkPrivateProjectPermissions(),
            filehandler.bind(this),
            this.useRouteErrorHandler(handler.bind(this)),
        );
    }

    use(path: string, router: IRouter): void {
        this.app.use(path, router);
    }

    useWithMiddleware(path: string, router: IRouter, middleware: any): void {
        this.app.use(path, middleware, router);
    }

    get router(): IRouter {
        return this.app;
    }
}

module.exports = Controller;