import express from 'express';
const cors = require('cors');
import helmet from 'helmet';
import passport from 'passport';
import passportHttp from 'passport-http';
const BasicStrategy = passportHttp.BasicStrategy;
const session = require('express-session');
const pgSession = require('connect-pg-simple')(session);
const exphbs = require('express-handlebars');
const methodOverride = require('method-override');

import {Server} from '../server';
import {
    authenticateRoute,
    offsetLimitReplacer,
    containerContext,
    metatypeContext,
    metatypeRelationshipContext,
    metatypeRelationshipPairContext,
    metatypeKeyContext,
    metatypeRelationshipKeyContext,
    currentUser,
    userContext,
    oauthAppContext,
    nodeContext,
    edgeContext,
    typeTransformationContext,
    typeMappingContext,
    exporterContext,
    importContext,
    dataStagingContext,
    dataSourceContext,
    fileContext,
    taskContext,
    eventActionContext,
    eventActionStatusContext,
    ontologyVersionContext,
    serviceUserContext,
} from '../middleware';
import ContainerRoutes from './data_warehouse/ontology/container_routes';
import MetatypeRoutes from './data_warehouse/ontology/metatype_routes';
import MetatypeKeyRoutes from './data_warehouse/ontology/metatype_key_routes';
import MetatypeRelationshipRoutes from './data_warehouse/ontology/metatype_relationship_routes';
import MetatypeRelationshipKeyRoutes from './data_warehouse/ontology/metatype_relationship_key_routes';
import MetatypeRelationshipPairRoutes from './data_warehouse/ontology/metatype_relationship_pair_routes';
import PostgresAdapter from '../../data_access_layer/mappers/db_adapters/postgres/postgres';
import Config from '../../services/config';
import {SetSamlAdfs} from '../authentication/saml/saml-adfs';
import UserRoutes from './access_management/user_routes';
import DataSourceRoutes from './data_warehouse/import/data_source_routes';
import {SetJWTAuthMethod} from '../authentication/jwt';
import {SetLocalAuthMethod} from '../authentication/local';
import QueryRoutes from './data_warehouse/data/legacy_query/query_routes';
import RSARoutes from './access_management/rsa_routes';
import GraphRoutes from './data_warehouse/data/graph_routes';
import OAuthRoutes from './access_management/oauth_routes';
import UserMapper from '../../data_access_layer/mappers/access_management/user_mapper';
import EventRoutes from './event_system/event_routes';
import ExportRoutes from './data_warehouse/export/export_routes';
import TypeMappingRoutes from './data_warehouse/etl/type_mapping_routes';
import {serialize} from 'class-transformer';
import {SuperUser} from '../../domain_objects/access_management/user';
import ImportRoutes from './data_warehouse/import/import_routes';
import DataQueryRoutes from './data_warehouse/data/data_query_routes';
import TaskRoutes from './task_runner/task_routes';
import OntologyVersionRoutes from './data_warehouse/ontology/versioning/ontology_version_routes';

const winston = require('winston');
const expressWinston = require('express-winston');

/*
 Router is a self contained set of routes and middleware that the main express.js
 application should call. It should be called only once.
*/
export class Router {
    // This may little convoluted, but it allows use to keep the `app` name.
    // I'm choosing to do this so that new developers to the project to use
    // documentation from node.js and express.js to understand what we're doing
    // with the underlying express.js application
    private app: express.Application;

    public constructor(app: Server) {
        this.app = app.UnderlyingExpressApplication;
    }

    public mount() {
        // DO NOT REMOVE - this is required for some auth methods to work correctly
        this.app.use(
            express.urlencoded({
                extended: false,
                limit: `${Config.max_request_body_size}mb`,
            }),
        );

        // single, raw endpoint for a health check
        this.app.get('/health', (req: express.Request, res: express.Response, next: express.NextFunction) => {
            res.sendStatus(200);
            next();
        });

        // Auth middleware is mounted as part of the pre-middleware, making all middleware
        // mounted afterwards secure
        this.mountPreMiddleware();

        // Mount application controllers, middleware is passed in as an array of functions
        ImportRoutes.mount(this.app, [authenticateRoute(), containerContext(), importContext(), dataStagingContext(), dataSourceContext(), currentUser()]);

        // we have to mount the json middleware after the data import middleware as this reads the request body but does
        // not reset it, meaning that the data import routes that use streams would not work as intended (apart from reading
        // large json bodies into memory)
        this.app.use(express.json({limit: `${Config.max_request_body_size}mb`}));

        UserRoutes.mount(this.app, [authenticateRoute(), containerContext(), userContext(), currentUser(), serviceUserContext()]);
        ContainerRoutes.mount(this.app, [authenticateRoute(), containerContext(), currentUser()]);
        ExportRoutes.mount(this.app, [authenticateRoute(), containerContext(), exporterContext(), currentUser()]);
        DataSourceRoutes.mount(this.app, [authenticateRoute(), containerContext(), dataSourceContext(), currentUser()]);
        TypeMappingRoutes.mount(this.app, [
            authenticateRoute(),
            containerContext(),
            typeTransformationContext(),
            typeMappingContext(),
            dataSourceContext(),
            currentUser(),
        ]);
        MetatypeRoutes.mount(this.app, [authenticateRoute(), containerContext(), metatypeContext(), currentUser()]);
        MetatypeKeyRoutes.mount(this.app, [authenticateRoute(), containerContext(), metatypeContext(), metatypeKeyContext(), currentUser()]);
        MetatypeRelationshipRoutes.mount(this.app, [authenticateRoute(), containerContext(), metatypeRelationshipContext(), currentUser()]);
        MetatypeRelationshipKeyRoutes.mount(this.app, [
            authenticateRoute(),
            containerContext(),
            metatypeRelationshipContext(),
            metatypeRelationshipKeyContext(),
            currentUser(),
        ]);
        MetatypeRelationshipPairRoutes.mount(this.app, [authenticateRoute(), containerContext(), metatypeRelationshipPairContext(), currentUser()]);
        /* This query route is considered deprecated */
        QueryRoutes.mount(this.app, [authenticateRoute(), containerContext(), currentUser()]);
        GraphRoutes.mount(this.app, [authenticateRoute(), containerContext(), nodeContext(), edgeContext(), fileContext(), metatypeContext(), currentUser()]);
        EventRoutes.mount(this.app, [authenticateRoute(), containerContext(), eventActionContext(), eventActionStatusContext(), currentUser()]);
        DataQueryRoutes.mount(this.app, [authenticateRoute(), containerContext(), currentUser()]);
        TaskRoutes.mount(this.app, [authenticateRoute(), containerContext(), taskContext(), currentUser()]);
        OntologyVersionRoutes.mount(this.app, [authenticateRoute(), containerContext(), currentUser(), ontologyVersionContext()]);

        // OAuth and Identity Provider routes - these are the only routes that serve up
        // webpages. WE ALSO MOUNT THE '/' ENDPOINT HERE
        OAuthRoutes.mount(this.app, [oauthAppContext()]);

        RSARoutes.mount(this.app, [authenticateRoute(), containerContext(), currentUser()]);

        this.mountPostMiddleware();
    }

    private mountPreMiddleware() {
        this.app.use(methodOverride('_method'));

        // templating engine
        this.app.engine('.hbs', exphbs({extname: '.hbs'}));
        this.app.set('view engine', '.hbs');
        this.app.set('views', Config.template_dir);

        // assets
        this.app.use(express.static(Config.asset_dir));

        // web gui
        this.app.use('/', express.static(Config.web_gui_dir));

        this.app.use(
            helmet({
                // set the max age of the strict transport security header
                hsts: {
                    maxAge: 31536000,
                },
            }),
        ); // helmet contains a bunch of pre-built http protections

        // TODO: change before attempting to deploy this application to production
        this.app.use(
            cors({
                origin: '*',
            }),
        );

        this.app.use(
            session({
                store: new pgSession({
                    pool: PostgresAdapter.Instance.Pool, // Connection pool
                    tableName: 'session',
                    createTableIfMissing: true,
                }),
                secret: Config.session_secret,
                resave: false,
                saveUninitialized: true,
                secure: true,
                cookie: {maxAge: 30 * 24 * 60 * 60 * 1000}, // 30 days
            }),
        );

        this.app.use(offsetLimitReplacer());

        this.app.use(
            expressWinston.logger({
                transports: [new winston.transports.Console()],
                format: winston.format.combine(
                    winston.format.colorize(),
                    winston.format.timestamp(),
                    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                    // @ts-ignore
                    winston.format.printf(({level, message, label, timestamp}) => {
                        return `${timestamp} ${level}: ${message}`;
                    }),
                ),
                meta: false,
                expressFormat: true,
                colorize: true,
            }),
        );

        // we call mount auth here because we depend on the session functionality
        this.mountAuthMiddleware();
    }

    private mountAuthMiddleware(): void {
        // BasicStrategy should be used only on the endpoints that we want secured, but don't care if they're "secure"
        // basic auth is considered insufficient for production applications.
        passport.use(
            new BasicStrategy((userID, password, done) => {
                if (userID === Config.basic_user && password === Config.basic_password) {
                    return done(null, serialize(SuperUser));
                }

                return done(null, false);
            }),
        );

        // SetSaml will initialize and assign the saml auth strategy
        if (Config.saml_enabled) SetSamlAdfs(this.app);
        SetLocalAuthMethod(this.app);

        // Once a user has authed against one of the accepted auth methods - the application using the API must
        // use a JWT for each subsequent request
        SetJWTAuthMethod(this.app);

        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore - as of 1/6/2021 passport.js types haven't been updated
        passport.serializeUser((user: User, done: any) => {
            if (typeof user === 'string') {
                user = JSON.parse(user);
            }
            user.password = '';
            done(null, user.id);
        });

        passport.deserializeUser((user: string, done: any) => {
            void UserMapper.Instance.Retrieve(user).then((result) => {
                if (result.isError) done('unable to retrieve user', null);

                done(null, result.value);
            });
        });

        // finalize passport.js usage
        this.app.use(passport.initialize());
        this.app.use(passport.session());
        this.app.use(currentUser()); // current user can be pulled after passport runs
    }

    // eslint-disable-next-line @typescript-eslint/no-empty-function,no-empty-function
    private mountPostMiddleware() {}
}