import { RPCServer, Socket } from "rpclibrary";
import { Inject, Module } from "../../Injector/ServiceDecorator";
import { FrontworkAdmin } from "../../Admin/Admin";
import { GuildManager } from "../Guild/GuildManager";
import { ItemManager } from "../Item/ItemManager";
import { RaidManager } from "../Raid/RaidManager";
import { CharacterManager } from "./CharacterManager";
import { LoginManagerIfc, LoginManagerFeatureIfc } from "./RPCInterface";
import { FrontworkComponent } from "../../Types/FrontworkComponent";
import { Rank, User, Auth, _Rank, TableDefiniton, RPCPermission, FrontcraftFeatureIfc, AnyRPCExporter, Token } from "../../Types/Types";
import { IAdmin } from "../../Admin/Interface";

const uuid = require('uuid/v4')

const ONE_WEEK = 604800000 

type Serverstate = {
    server: RPCServer,
    port : number,
    allowed: string[]
}


@Module()
export class LoginManager
implements FrontworkComponent<LoginManagerIfc, LoginManagerFeatureIfc>{
    name = "Authenticator" as "Authenticator";   

    @Inject(IAdmin)
    private admin: FrontworkAdmin

    @Inject(GuildManager)
    private guild : GuildManager
    
    @Inject(ItemManager)
    private item : ItemManager
    
    @Inject(RaidManager)
    private raid : RaidManager
    
    @Inject(CharacterManager)
    private character : CharacterManager
    
    exporters :any[] = []
    rankServers : {[rank in Rank] : Serverstate}
    userLogins : {[username in string] : {
        user: User
        connections: {[port in number]: Socket}
        auth: Auth
    }} = {}

    exportRPCs = () => [
        {
            name: 'login' as 'login',
            call: this.login 
        },{
            name: 'logout' as 'logout',
            call: this.logout
        },{
            name: 'getAuth' as 'getAuth',
            call: this.getAuth
        },{
            name: 'checkToken' as 'checkToken',
            call: async (tokenValue : string, rank: Rank) => this.checkToken(tokenValue, rank)
        },{
            name: 'createUser' as 'createUser',
            call: this.createUser
        }
    ]

    exportRPCFeatures = () => [
        {
            name: 'modifyPermissions' as 'modifyPermissions',
            exportRPCs: () => [{
                name: 'getPermissions' as 'getPermissions',
                call: this.getPermissions 
            },{
                name: 'setPermission' as 'setPermission',
                call: this.setPermission
            }]
        }
    ]
    
    
    getTableDefinitions = (): TableDefiniton[] => [
        {
            name: 'users',
            tableBuilder: (table) => {
                table.increments("id").primary()
                table.string("name").notNullable().unique()
                table.string("pwhash").notNullable()
                table.string("rank").notNullable()
                table.string("email").nullable().unique()
                table.boolean("locked").defaultTo(true)
            }
        },{
            name: 'rpcpermissions',
            tableBuilder: (table) => {
                table.string("name").primary().notNullable()
                _Rank.forEach(r => {
                    if(r === 'ADMIN')
                        table.boolean(r).defaultTo(true).notNullable()
                    else
                        table.boolean(r).defaultTo(false).notNullable()
                })
            }
        }
        ,...this.exporters.flatMap(exp => exp['getTableDefinitions']?exp['getTableDefinitions']():undefined)
    ]

    initialize = async () => {
        this.exporters = [this.guild, this.item, this.raid, this.character] 
        //set up permissions
        await Promise.all( 
            [this, ...this.exporters].flatMap(exp => exp.exportRPCFeatures().map(async (feature) => {
            try{
                await this.admin.knex.insert({ name: feature.name }).into('rpcpermissions')
            }catch(e){}
        })))

        //start rankServers
        const rankServers : any = {}   

        for(let i = 0; i < _Rank.length; i++){
            const rank:Rank = _Rank[i]
            const port = 20001 + i
            const rankServer = await this.startRankServer(rank, port)
            rankServers[rank] = {
                server: rankServer,
                port: port,
                allowed: []
            }
        }
        this.rankServers = rankServers
    
        setInterval(this.checkExpiredSessions, 600_000)   
    }

    checkExpiredSessions = () => {
        Object.values(this.userLogins).map(userLogin => {
            const auth = userLogin.auth
            if(!this.checkToken(auth.token.value, auth.user.rank)){
                this.logout(auth.user.name, auth.token.value)
            }
        })
    }

    checkConnection = async (socket: Socket) => {
        let data : any = false
        let tries = 0
        while(!data){
            tries ++
            if(tries === 5){
                socket.destroy()
                return
            }
            data = await Promise.race([socket.call('getUserData'), new Promise((res, rej) => { setTimeout(res, 250);})])
        } 
        this.userLogins[data.user.name].connections[socket.port] = socket
        await socket.call('navigate', ["/frontcraft/dashboard"])
        return true
    }

    setPermission = async (permission: RPCPermission) => {
        await this.admin.knex('rpcpermissions')
        .where('rpcname', '=', permission.name)
        .update(permission)
    }

    getPermissions = async () : Promise<RPCPermission[]> => {
        return await this.admin.knex.select('*').from('rpcpermissions')
    }

    getPermission = async (feature: keyof FrontcraftFeatureIfc, rank:Rank) : Promise<boolean> => {
        const perm : RPCPermission[] = await this.admin.knex
            .select(rank)
            .from('rpcpermissions')
            .where('name', '=', <string>feature)

        if(perm.length === 0) return false
        return perm[0][rank]
    }

    getRPCForRank = async (rank: Rank): Promise<AnyRPCExporter[]> => {
        let rpcs = [
            ...this.exportRPCFeatures(), 
            ...this.exporters.flatMap((exp) => exp.exportRPCFeatures())
        ]
        
        const bits = await  Promise.all(rpcs.map(async (feature) => {
            const allowed = await this.getPermission(<keyof FrontcraftFeatureIfc> feature.name, rank)
            return allowed
        }))

        return rpcs.filter(entry => bits.shift())
    }

    createUser = async(user:User): Promise<User> => {
        if(user.rank === 'ADMIN'){
            const admins = await this.admin.knex
                .select("*")
                .from('users')
                .where({rank: 'ADMIN'})

            if(admins.length > 0){
                return {} as User
            }

            user.locked = false
        }
        await this.admin.knex('users')
        .insert(user)

        const users = await this.admin.knex
        .select("*")
        .from('users')
        .where(user)

        return users[0]
    }

    logout = async (username:string, tokenValue : string) : Promise<void> => {
        try{

            await Promise.all (Object.values(this.userLogins[username].connections).map(async (sock) => {
                await sock.call('navigate', '/auth/login')
            }))

            Object.values(this.rankServers)
                .forEach(state => {
                    state.allowed = state.allowed.filter(allowed => allowed !== tokenValue)
            })

            delete this.userLogins[username]
        }catch(e){
            console.log(e)
        }
    }

    login = async(username:string, pwHash:string) : Promise<Auth> => {
        const res:User[] = await this.admin.knex
        .select('*')
        .from('users')
        .where({ name: username })

        if(res.length > 0 && pwHash === res[0].pwhash){
            const user:User = res[0]
            delete user.pwhash

            //return existing auth
            if(this.userLogins[username] != null){
                return this.userLogins[username].auth
            }
            
            const token = this.createToken(user)
            const userAuth : Auth = {
                token: token,
                user: user,
                port: this.rankServers[user.rank].port
            }

            this.userLogins[user.name] = {connections: {}, auth: userAuth, user:user}
            this.rankServers[user.rank].allowed.push(token.value)

            return userAuth 
        }

        throw new Error('login failed')
    }

    getUserRecordByToken(tokenValue: string){
        const maybeRecord = Object.values(this.userLogins).find(login => login.auth.token.value === tokenValue)
        return maybeRecord?maybeRecord:undefined
    }

    getAuth = async (tokenValue:string) : Promise<Auth> => {
        const maybeAuth = this.getUserRecordByToken(tokenValue)
        if(maybeAuth)
            return maybeAuth.auth

        throw new Error("Bad token")
    }

    startRankServer = async (rank : Rank, port: number) : Promise<RPCServer> => {
        const allowedRPCs = await this.getRPCForRank(rank)
        let rpcServer : RPCServer = new RPCServer(port, allowedRPCs, {
            closeHandler: (socket) => { 
                Object.values(this.userLogins)
                    .forEach(login => delete login.connections[socket.port])

            },
            connectionHandler: (socket) => {
                this.checkConnection(socket).catch(() => {}) //sometimes times out if you go too fast
            },
            sesame: (sesame) => this.checkToken(sesame, rank)
        })
        return rpcServer
    }

    checkToken = (token: string, rank: Rank) : boolean => this.rankServers[rank].allowed.includes(token)
                                                       && Object.values(this.userLogins).find(login => login.auth.token.value === token)!.auth.token.created > Date.now() - ONE_WEEK

    createToken = (user:User): Token => {
        
        if(this.userLogins[user.name]){
            return this.userLogins[user.name].auth.token
        }

        const token:Token = {
            value: uuid(),
            user_id: user.id!,
            created: Date.now()
        }

        return token
    }
}