'use strict'

import * as Logger from 'log4js'
import { Plugin, socketioRPC } from 'frontblock-generic/Plugin';
import { ErrorResponse, SuccessResponse } from 'frontblock-generic/Service';

const pluginList = [
    '../../paymentmanager/static/Plugin',
    'frontblock/FrontblockLib'
]


const express = require('express')
const http = require('http')
const bsock = require('bsock')
const kfs = require("key-file-storage")('conf') //'conf' is a directory that will be generated if it doesn't exist

const logger = Logger.getLogger() // logs to STDOUT  
logger.level = 'debug'


type hookRpc = { type: 'hook', generator: (socket) => Function, unhook:(uid:string)=>Promise<ErrorResponse|SuccessResponse>}
type unhookRPC = { type: 'unhook', fn: Function}
type callRPC = { type: 'call', fn: Function}

export type rpcInfo = {
    name: string,
    args: string,
    info: hookRpc | unhookRPC | callRPC
}

/**
 * FrontblockAdmin
 * 
 * The customer-facing dynamic component of the customer backend
 * Supports (un)loading plugins which will be communicated to its library component (See FrontblockLib.ts and the info() RPC)
 * 
 * The list of available plugins is published via the frontblock API and downloaded via gitea-releases
 */
export class FrontblockAdmin{
    private plugins: Plugin[]

    private express
    private httpServer
    private io = bsock.createServer()
    private wsServer = http.createServer()

    constructor(){
        if(!('FrontblockService' in kfs)){
            logger.warn('No config file found! Generating one')
            kfs.FrontblockService = { httpPort: 8080 }
        }

        this.initialize()
    }

    private async initialize(){
        await this.loadPlugins()
        this.plugins.forEach(plugin => plugin.start())
        this.startWebsocket()
        this.startWebserver()
    }

    private async loadPlugins(){
        const promises = pluginList.map(path => {
            return import(path)
        })
        const pluginsClasses = await Promise.all(promises)
        this.plugins = pluginsClasses.map(clazz => new clazz.default())
    }

    private initApis(socket){
        const rpcInfos:rpcInfo[] = [
            {
                name: 'restartWebserver',
                args: 'port',
                info:{
                    type:'call', 
                    fn: (port:number) => { this.restartWebserver(port) } 
                }
            },{
                name: 'info', 
                args: '',
                info:{
                    type:'call', 
                    fn: () => { return rpcInfos } 
                }
            }
        ]

        this.plugins.forEach(plugin => {
            plugin.exportRPCs().forEach(rpc => rpcInfos.push(this.rpcToRpcInfo(rpc)))
        })

        for(const api of rpcInfos){
            switch(api.info.type){
                case 'call':
                    socket.hook(api.name, api.info.fn)
                break
                case 'hook':
                    const hook = api.info.generator(socket)
                    hook.bind(this)
                    socket.hook(api.name, hook)
                break
                case 'unhook':
                    socket.hook(api.name, api.info.fn)
                break
            }
        }

        socket.on('close', () => {
            logger.info("Client disconnected")
            rpcInfos.forEach(rpc => {
                socket.unhook(rpc.name)
            })
        })
  
    }

    private startWebserver(){
        if(this.httpServer != null || this.express != null){
            logger.warn("Webserver is already running")
            return
        }
        
        let port:number = kfs.FrontblockService.httpPort
        this.express = express()
        this.express.use(express.static('static'))

        this.httpServer = http.Server(this.express)
        this.httpServer.listen(port, () => {
            logger.info('listening on *'+port)
        })
    }

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

    private restartWebserver(port:number){
        this.stopWebserver()
        kfs.FrontblockService = { httpPort: port }
        this.startWebserver()
    }

    private startWebsocket(){
        try{
            this.io.attach(this.wsServer)
            this.io.on('socket', (socket) => {
                logger.info("New Websocket connection on port", socket.port)

                const handleError = (e: any) => {
                    logger.info("Websocket closing", String(e))
                    socket.close()
                }
                
                socket.on('error', handleError)
                this.initApis(socket)
            })
            logger.info('websocket listening on *20000')
            this.wsServer.listen(20000)
        }catch(e){
            logger.error(String(e))
        }
    }

    private rpcToRpcInfo(rpc:socketioRPC):rpcInfo{
        switch(rpc.type){
            case 'hook':
                let f = this.hookGenerator(rpc)
                return {name: rpc.name, args: this.extractArgs(f(null)), info: { type: 'hook', generator: f, unhook: rpc.unhook } }
            case 'unhook':
                return {name: rpc.name, args: this.extractArgs(rpc.rpc), info: { type: 'unhook', fn: rpc.rpc } }
            case 'call':
                return {name: rpc.name, args: this.extractArgs(rpc.rpc), info: { type: 'call', fn: rpc.rpc } }
        }
    }

    /**
     * Generates RPC hooks which support a callback. 
     * 
     * Note callbacks *need* to accept a singular argument and they have to be the last parameter!
     * 
     * TODO: Maybe kill open callbacks when socket closes? See rpc.info.unhook for the appropriate function to call.
     */
    hookGenerator = (rpc) => { 
        const argsArr = this.extractArgs(rpc.rpc).split(',')
        argsArr.pop()
        const args = argsArr.join(',')

        return eval(`(socket) => async (`+args+`) => { 
            const res = await rpc.rpc(`+args+(args.length!==0?',':'')+` (x) => {
                if(res.uid != null){
                    console.log("calling "+res.uid)

                    socket.call(res.uid, x).catch(e => {
                        rpc.unhook(res.uid)
                    })
                }
            })
            return res
        }`)
    }

    private extractArgs(f:Function):string{
        let fn = String(f)
        let args = fn.substr(0, fn.indexOf(")"))
        args = args.substr(fn.indexOf("(")+1)
        return args
    }
}

(async() => {
    new FrontblockAdmin()
})()