import { RPCServer, Socket } from "rpclibrary";
import { Inject, Injectable } 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 "../Character/CharacterManager";
import { UserManagerFeatureIfc, UserManagerIfc } from "./RPCInterface";
import { FrontworkComponent } from "../../Types/FrontworkComponent";
import { Rank, User, Auth, _Rank, TableDefiniton, RPCPermission, FrontcraftFeatureIfc, AnyRPCExporter, Token, UserRecord } from "../../Types/Types";
import { IAdmin } from "../../Admin/Interface";
import { IUserManager } from "./Interface";
import { getLogger, Logger } from "log4js";
import { saltedHash } from "../../Util/hash";
import { _Tiers, Tiers } from "../../Types/Items";

const uuid = require('uuid/v4')

const salt = "6pIbc6yjSN"
const ONE_WEEK = 604800000 

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


@Injectable(IUserManager)
export class UserManager
implements FrontworkComponent<UserManagerIfc, UserManagerFeatureIfc>, IUserManager{
    name = "UserManager" as "UserManager"

    @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] : UserRecord} = {}

    exportRPCs = () => [
        this.login,
        this.logout,
        this.getAuth,
        this.checkToken,
        this.createUser,
        this.getUser
    ]

    exportRPCFeatures = () => [{
        name: 'modifyPermissions' as 'modifyPermissions',
        exportRPCs: () => [
            this.getPermissions,
            this.setPermission
        ]
    },{
        name: 'softreserveCurrency' as 'softreserveCurrency',
        exportRPCs: () => [
            this.incrementCurrency,
            this.decrementCurrency,
            this.setCurrency
        ]
    }]

    changeRank = async (user:User, rank:Rank): Promise<User> => {
        await this.admin
        .knex('users')
        .where({
            user:user.username
        }).update({
            rank: rank
        })

        return await this.admin
        .knex('users')
        .where({
            user: user.username
        }).first()
    }
    
    getTableDefinitions = (): TableDefiniton[] => [
        {
            name: 'users',
            tableBuilder: (table) => {
                table.increments("id").primary()
                table.string("username").notNullable().unique()
                table.string("pwhash").notNullable()
                table.string("rank").notNullable()
                _Tiers.forEach(tier => {
                    table.integer(tier).defaultTo(1)
                })
            }
        },{
            name: 'rpcpermissions',
            tableBuilder: (table) => {
                table.string("rpcname").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
        getLogger('UserManager').debug('setting up permissions')

        await Promise.all( 
            [this, ...this.exporters].flatMap(exp => exp.exportRPCFeatures().map(async (feature) => {
            try{
                await this.admin.knex.insert({ rpcname: feature.name }).into('rpcpermissions')
            }catch(e){
            }
        })))

        //start rankServers
        getLogger('UserManager').debug('Starting rank servers')

        let rankServers = { } as any
        await Promise.all(_Rank.map(async (r,i) => {
            const port = 20001 + i
            const rankServer = await this.startRankServer(r, port)
            rankServers[r] = {
                server: rankServer,
                port: port,
                allowed: []
            }
        }))
        this.rankServers = rankServers
        
    
        setInterval(this.checkExpiredSessions, 600_000)   
    }

    stop = async () => {
        Object.values(this.userLogins).forEach(x => Object.values(x.connections).forEach(c => c.destroy()))

        await Promise.all(Object
            .values(this.rankServers)
            .map(async state => {
                try{
                    //return await state.server.destroy()
                }catch(e){
                    getLogger('UserManager').warn(e)
                }
            })
        );
        
    }

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

    checkConnection = async (socket: Socket) => {
        
        let data : any
        let tries = 0
        while(!data){
            tries ++
            if(tries === 5){
                getLogger('UserManager').debug('Connection check failed for connection *'+socket.port)
                socket.destroy()
                return false
            }
            data = await Promise.race([socket.call('getUserData'), new Promise((res, rej) => { setTimeout(res, 1000);})])
        }
        if(!this.userLogins[data.user.username]){
            await this.logout(data.user.username, data.token.value)
            return false
        }

        this.userLogins[data.user.username].connections[socket.port] = socket
        await socket.call('navigate', ["/frontcraft/dashboard"])
        return true
    }

    setPermission = async (permission: RPCPermission) => {
        await this.admin.knex('rpcpermissions')
        .where('rpcname', '=', permission.rpcname)
        .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('rpcname', '=', <string>feature)
            .first()

        if(!perm) return false
        return perm[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.username = user.username.toLowerCase()
        user.pwhash = await saltedHash(user.pwhash, salt)

        await this.admin.knex('users')
        .insert(user)

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

        return userRecord
    }

    getUser = async (username: string) : Promise<User | void> => await this.admin
    .knex('users')
    .select('*')
    .where({
        username: username.toLowerCase()
    })
    .first()

    logout = async (username:string, tokenValue : string) : Promise<void> => {
        try{
            if(!this.checkTokenOwnedByUser(username, tokenValue)) return

            if(this.userLogins[username]){
                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)
        }
    }

    wipeCurrency = async () => {
        await Promise.all(_Tiers.map(tier => {
            const update = {}
            update[tier] = 1
    
            return this.admin.knex('users')
            .update(update)
        }))
    }

    login = async(username:string, pwHash:string) : Promise<Auth> => {
        username = username.toLowerCase()
        const user:User = await this.admin.knex
        .select('*')
        .from('users')
        .where({ username: username })
        .first()
         
        const salted = await saltedHash(pwHash, salt)

        if(user && salted === user.pwhash){
            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.username] = {connections: {}, auth: userAuth, user:user}
            this.rankServers[user.rank].allowed.push(token.value)
            return userAuth 
        }

        throw new Error('login failed')
    }

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

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

    startRankServer = async (rank : Rank, port: number) : Promise<RPCServer> => {

        const allowedRPCs = await this.getRPCForRank(rank)
        let rpcServer
        let n = 0
        while(!rpcServer){
            n++            
            await Promise.race([
                new Promise((res, rej) => {
                    rpcServer = new RPCServer(port, allowedRPCs, {
                        closeHandler: (socket) => { 
                            Object.values(this.userLogins)
                                .forEach(login => delete login.connections[socket.port])
            
                        },
                        connectionHandler: (socket) => {
                            this.checkConnection(socket).then(res => {
                                if(!res){
                                    socket.destroy();
                                }
                            }).catch((e) => {
                                socket.destroy();
                                getLogger('UserManager').warn(e);
                            })
                        },
                        errorHandler: (socket, e, rpcName, args) => {
                            getLogger('UserManager').error(rpcName, args, e);
                        },
                        sesame: (sesame) => this.checkToken(sesame, rank),
                        visibility: '0.0.0.0'
                    })
                    res()
                }),
                new Promise((res, rej) => setTimeout(res, 500))
            ])
            if(!rpcServer && n>1) 
                getLogger('UserManager').warn("createServer retry nr.", n, 'port', port)
        }
        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

    checkTokenOwnedByUser = (username: string, tokenValue: string) => {
        username = username.toLowerCase()
        const maybeRecord = this.getUserRecordByToken(tokenValue)
        if(!maybeRecord || maybeRecord.auth.user.username != username){
            getLogger('UserManager').warn(`Bad logout attempt
            token by: ${maybeRecord?maybeRecord.auth.user.username:tokenValue}
            tried to logout: ${username}`)
            return false
        } 
        return true
    }
                                                       
    createToken = (user:User): Token => {
        
        if(this.userLogins[user.username]){
            return this.userLogins[user.username].auth.token
        }

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

        return token
    }

    getCurrency = async(user:User, tier?: Tiers) : Promise<number> => { 
        if(!tier) return 0
        const usr : User = await this.admin
        .knex('users')
        .where('username', '=', user.username)
        .select('*')
        .first()

        return usr[tier]!
    }

    decrementCurrency = async (user: User, tier?: Tiers, value = 1) => {
        if(!tier || value < 1) return

        const usr : User = await this.admin
        .knex('users')
        .where('id', '=', user.id)
        .select('*')
        .first()

        if(!usr || usr[tier]! <= 0) return

        await this.admin
        .knex('users')
        .where('id', '=', user.id)
        .decrement(tier, value)
    }

    incrementCurrency = async (user: User, tier: Tiers, value = 1) => {
        if(!tier || value < 1) return
        await this.admin
        .knex('users')
        .where('id', '=', user.id)
        .increment(tier, value)
    }

    setCurrency = async (user: User, tier?:Tiers, value = 0) => {
        if(!tier || value < 0) return
        await this.admin
        .knex('users')
        .where('id', '=', user.id)
        .update(tier, value)
    }
}