import { RPCServer, Socket, RPCInterface } from "rpclibrary";
import { Inject, Injectable } from "../../Injector/ServiceDecorator";
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, Token, UserRecord } from "../../Types/Types";
import { IAdmin } from "../../Admin/Interface";
import { IUserManager } from "./Interface";
import { getLogger } from "log4js";
import { saltedHash } from "../../Util/hash";
import { _Tiers, Tiers } from "../../Types/Items";
import { ICharacterManager } from "../Character/Interface";
import { IRaidManager } from "../Raid/Interface";
import { IItemManager } from "../Item/Interface";
import { IGuildManager } from "../Guild/Interface";

const uuid = require('uuid/v4')

const salt = "6pIbc6yjSN"
const ONE_WEEK = 604800000

type Serverstate<InterfaceT extends RPCInterface> = {
    server: RPCServer<InterfaceT>,
    port: number,
};


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

    @Inject(IAdmin)
    private admin: IAdmin

    @Inject(IGuildManager)
    private guild: GuildManager

    @Inject(IItemManager)
    private item: ItemManager

    @Inject(IRaidManager)
    private raid: RaidManager

    @Inject(ICharacterManager)
    private character: CharacterManager

    exporters: any[] = []
    rankServer: Serverstate<FrontcraftFeatureIfc>
    userLogins: { [username in string]: UserRecord } = {}
    allowed: string[] = []

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

    RPCFeatures = () => [
        {
            name: 'manageUser' as 'manageUser',
            RPCs: () => [
                this.adminLogout,
                this.changeRank,
                this.adminChangePassword
            ]
        }, {
            name: 'modifyPermissions' as 'modifyPermissions',
            RPCs: () => [
                this.getPermissions,
                this.setPermission,
            ]
        }, {
            name: 'softreserveCurrency' as 'softreserveCurrency',
            RPCs: () => [
                this.incrementCurrency,
                this.decrementCurrency,
                this.setCurrency
            ]
        }]

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

        await this.adminLogout(user.username)

        return await this.admin
            .knex('users')
            .where({
                username: 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#initialize').debug('setting up permissions')

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

        const rankServer = await this.startRankServer(20002)
        this.rankServer = {
            server: rankServer,
            port: 20002
        }
        //setInterval(this.checkExpiredSessions, 600_000)
    }

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

        try {
            return this.rankServer.server.close()
        } catch (e) {
            getLogger('UserManager#stop').debug(String(e))
        }
    }

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

    checkConnection = async (socket: Socket, retries = 0) : Promise<boolean> => {
        if(retries < 0){
            return false
        }
        //give the client some time to bind its hooks
        await new Promise((res, rej) => { setTimeout(res, 150);})

        let data
        try{
            data = await Promise.race([socket.call('getUserData'), new Promise((res, rej) => { setTimeout(res, 250);})])
        }catch(e){
            getLogger('UserManager#checkConnection').debug('Client didn\'t respond', e)
            return false
        }

        if(!data) {
            if(retries > 0){
                return await this.checkConnection(socket, retries-1)
            }
            return false
        }

        if (!this.userLogins[data.user.username]) {
            await this.logout(data.user.username, data.token.value)
            return false
        }

        this.userLogins[data.user.username].connections[socket.id] = socket
        //getLogger('UserManager#checkConnection').debug(this.userLogins, this.allowed);
        
        return true
    }

    changePassword = async (userToken: string, pwHash: string): Promise<User> => {
        const record = this.getUserRecordByToken(userToken)
        if (!record) return {} as User
        return await this.adminChangePassword(record.user, pwHash)
    }

    adminChangePassword = async (user: User, pwHash: string): Promise<User> => {
        const salted = await saltedHash(pwHash, salt)
        await this.admin.knex('users')
            .update({ pwhash: salted })
            .where({
                username: user.username
            })

        const usr = await this.getUser(user.username)
        if (!usr) return {} as any
        await this.adminLogout(usr.username)
        return usr
    }

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

        await Promise.all(
            Object.entries(this.userLogins).map(([username, record]) => {
                if (record.user.rank === "ADMIN") return
                return this.adminLogout(username)
            })
        )
    }

    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]
    }

    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
            return await this.adminLogout(username)
        } catch (e) {

        }
    }

    adminLogout = async (username: string): Promise<void> => {
        try {
            if (this.userLogins[username]) {
                const token = this.userLogins[username].auth.token.value
                //doesn't matter if error or success
                Object.values(this.userLogins[username].connections).forEach(async (sock) => {
                    try{
                        await sock.call('kick')
                    }catch(e){
                        //getLogger('UserManager#adminLogout#kick').debug(String(e))
                    }
                })
                this.allowed = this.allowed.filter(allowed => {
                    return allowed != token
                })
                delete this.userLogins[username]
            }
        } catch (e) {
            getLogger('UserManager#adminLogout').debug(String(e), this.userLogins)
        }
    }

    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.rankServer.port
            }

            this.userLogins[user.username] = { connections: {}, auth: userAuth, user: user }
            this.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 (port: number): Promise<RPCServer<FrontcraftFeatureIfc>> => {
        let rpcs = [
            ...this.RPCFeatures(),
            ...this.exporters.flatMap((exp) => exp.RPCFeatures())
        ]
        let rpcServer = new RPCServer<FrontcraftFeatureIfc>(rpcs, {
            accessFilter: async (sesame, exporter) => {
                const record = this.getUserRecordByToken(sesame!)
                if (!record) return false
                return await this.getPermission(exporter.name, record.user.rank)
            },
            closeHandler: (socket) => {
                Object.values(this.userLogins).forEach(login => delete login.connections[socket.id])
                //getLogger('UserManager#closeHandler').debug(this.userLogins, this.allowed)
            },
            connectionHandler: (socket) => {
                this.checkConnection(socket, 10).then(res => {
                    if (!res) {
                        getLogger('UserManager#connectionHandler').debug("Connection "+socket.id+" failed to authenticate")
                        socket.close();
                    }
                }).catch((e) => {
                    getLogger('UserManager#connectionHandler').debug(String(e))
                    socket.close();
                })
            },
            errorHandler: (socket, e, rpcName, args) => {
                getLogger('UserManager#errorHandler').error(rpcName, args, e);
                throw(new Error("RPC failed"))
            },
            sesame: (sesame) => this.checkToken(sesame),
        }).listen(port)

        return rpcServer
    }

    checkToken = (token: string): boolean => {
        return this.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) {
            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 (value < 1) return

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

        if (!usr || usr[tier]!-value < 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 (value < 0) return
        await this.admin
            .knex('users')
            .where('id', '=', user.id)
            .update(tier, value)
    }
}