'use strict'

import { getLogger, Logger, } from "log4js";
import { promises as fs, mkdirSync } from "fs"
import { RPCServer } from 'rpclibrary'
import * as Path from 'path'
import * as Knex from 'knex';
import * as http from 'http';
import * as express from 'express';
import { existsSync } from "fs";
import { GuildManager } from '../Components/Guild/GuildManager';
import { ItemManager } from '../Components/Item/ItemManager';
import { RaidManager } from '../Components/Raid/RaidManager';
import { CharacterManager } from '../Components/Character/CharacterManager';
import { UserManager } from '../Components/User/UserManager';
import { RootComponent } from '../Injector/ServiceDecorator';
import { TableDefinitionExporter } from '../Types/Interfaces';
import { AdminConf, TableDefiniton, SomeOf } from '../Types/Types';
import { RPCConfigLoader } from '../Components/RPCConfigLoader';
import { FrontworkComponent } from '../Types/FrontworkComponent';
import { IAdmin } from './Interface';
import { Injector } from '../Injector/Injector';
import { PubSub } from '../Components/PubSub/PubSub';
import { BurstyRateLimiter, RateLimiterMemory, RateLimiterAbstract } from 'rate-limiter-flexible'


@RootComponent({
    injectable: IAdmin,
    injects: [
        GuildManager,
        ItemManager,
        RaidManager,
        CharacterManager,
        UserManager,
        PubSub
    ]
})
export class FrontworkAdmin
    implements TableDefinitionExporter, IAdmin {
    knex: Knex
    config: RPCConfigLoader<AdminConf>
    rpcServer: RPCServer

    private express : express.Application
    private httpServer

    constructor(private frontworkComponents: FrontworkComponent[] = []) {
        this.config = new RPCConfigLoader<AdminConf>({
            name: "FrontworkAdminConf",
            getDefaultConfig: () => {
                return {
                    httpPort: 8080,
                    eventBusConf: {},
                    dbConf: {
                        client: 'sqlite3',
                        connection: {
                            filename: Path.resolve(__dirname, '../../../..', "data/frontworkAdmin.sqlite"),
                        },
                        migrations: {
                            directory: Path.resolve(__dirname, '../../../..', "migrations"),
                            extension: 'ts'
                        },
                        useNullAsDefault: true,
                    }
                }
            }
        }, './config', this.configChangeHandler)
    }

    async start() {
        const ONE_REQUEST = 15
        let port: number = this.config.getConfig().httpPort
        const rateLimiter = new BurstyRateLimiter(
            new RateLimiterMemory({
              points: 2*ONE_REQUEST,
              duration: 1,
            }),
            new RateLimiterMemory({
              keyPrefix: 'burst',
              points: 5*ONE_REQUEST,
              duration: 60,
            })
          )

        await this.makeKnex()
        const app = await this.makeExpress(rateLimiter)
        const httpServer = new http.Server(app)
        await this.startWebsocket(httpServer)
        httpServer.listen(port)
        await this.attachAngularSSR(app, rateLimiter)
        getLogger('Admin#startWebsocket').debug("Webserver up on", port)

        await Promise.all(this.frontworkComponents.map(c => c.initialize ? c.initialize() : undefined))
        getLogger('Admin#start').debug(this.frontworkComponents.length + " components initialized")
    }

    stop() {
        Promise.all([
            ...this.frontworkComponents.map(c => c.stop ? c.stop() : undefined),
        ])
            .catch(e => getLogger('Admin#stop').warn(e))
            .finally(() => {
                this.rpcServer.close()
                process.exit(0)
            })
    }

    protected configChangeHandler = (conf: AdminConf, key?: string) => {
        if (key === 'dbConf') {
            this.makeKnex()
        }
    }

    getConfigKey(key: string) {
        return this.config.getConfigKey(key)
    }

    setConfigKey(key: string, value: any) {
        return this.config.setConfigKey(key, value)
    }

    getTableDefinitions(): TableDefiniton[] {
        return [
            ...this.frontworkComponents
        ]
            .filter(exp => exp.getTableDefinitions != null)
            .flatMap(exp => exp.getTableDefinitions())
    }

    private startWebsocket(httpServer: http.Server) {
        this.rpcServer = new RPCServer([
            ...this.frontworkComponents,
            {
                name: "debug",
                RPCs: () => [{
                    name: 'dumpDb',
                    call: async (table) => await this.knex(table).select('*')
                }]
            }
        ], {
            errorHandler: (sock, err, rpc, args) => {
                getLogger("startWebsocket#errorHandler").error("RPC", rpc)
                getLogger("startWebsocket#errorHandler").error("ERR", err)
                getLogger("startWebsocket#errorHandler").error("ARGS", args)
            }
        })
        .attach(httpServer)
    }

    private async makeExpress(rateLimiter : SomeOf<RateLimiterAbstract> = new RateLimiterMemory({ points: 6, duration: 3 })) {
        if (this.httpServer != null || this.express != null) {
            getLogger('Admin#startWebserver').warn("Webserver is already running")
            return
        }

        this.express = express()
        const distFolder = "../../../../dist"
        this.express.get('*.*', (req, res) => {
            rateLimiter.consume(req.ip)
            .then(_ => {
                    const filepath = Path.join(__dirname, distFolder, 'browser', decodeURIComponent(req.path))
                    if(!existsSync(filepath)){
                        getLogger('Admin#startWebserver#serveFile').error(String(new Error("404 BAD REQUEST "+filepath+ " FROM "+req.ip)))
                        res.send("404 Not found")
                        res.status(404)
                        return
                    }
                    res.sendFile(filepath)
                    res.status(200)
            })
            .catch(_ => {
                getLogger('Admin#startWebserver#serveFile').error(String(new Error("429 BAD REQUEST "+req.path+" FROM "+req.ip)))
                res.send('429 Too many requests')
                res.status(429)
            })
        })

        return this.express
    }

    attachAngularSSR = async (express : express.Application, rateLimiter : SomeOf<RateLimiterAbstract> = new RateLimiterMemory({ points: 6, duration: 3 })) => {
        const distFolder = "../../../../dist"
        const ngExpressServer = Path.join(__dirname, distFolder, 'server.js')
        let port: number = this.config.getConfig().httpPort

        try {
            const req = require(distFolder + "/server.js")
            await req.attachExpress(express, './dist', getLogger('angularSSR#'), rateLimiter)
            getLogger('Admin#startWebserver').debug('Frontend from ' + ngExpressServer + " loaded")
        } catch (e) {
            getLogger('Admin#startWebserver').error(e)
            getLogger('Admin#startWebserver').warn("No angular SSR module was provided in " + ngExpressServer)
            getLogger('Admin#startWebserver').warn("This is not fatal, but your page will not render on *" + port)
        }
    }

    private stopWebserver() {
        if (this.httpServer == null || this.express == null) {
            getLogger('Admin#stopWebserver').warn("Webserver is not running")
            return
        }
        this.httpServer.close()
        this.httpServer = null
        this.express = null
        getLogger('Admin#stopWebserver').info("Webserver stopped")
    }

    async makeKnex(): Promise<Knex> {
        const conf: Knex.Config = this.config.getConfigKey("dbConf")

        getLogger('Admin#makeKnex').debug("Making new knex:", conf)
        if (conf.client === 'sqlite3') {
            mkdirSync(Path.dirname((<any>conf.connection).filename), { recursive: true })
        }

        this.knex = Knex(conf)
        if (conf.client === 'sqlite3') {
            await this.knex.raw('PRAGMA foreign_keys = ON');

        }

        await Promise.all(
            this.getTableDefinitions()
                //make unique by name
                .filter((other, index, self) => index === self.findIndex(
                    (self) => self.name === other.name)
                )
                //create table if not exists
                .map(async (def) => {
                    const hasTable = await this.knex.schema.hasTable(def.name)
                    if (!hasTable)
                        return await this.knex.schema.createTable(def.name, def.tableBuilder)
                })
        )
        await this.knex.migrate.latest().catch(e => {
            getLogger('Admin#makeKnex').error(e)
            process.exit(-1)
        })
        return this.knex
    }
}

process.on('SIGINT', function () {
    getLogger('process#SIGINT').info("Shutting down from SIGINT (Ctrl-C)");
    Injector.resolve<FrontworkAdmin>(FrontworkAdmin).stop()
})
