import { Inject, Injectable } from "../../Injector/ServiceDecorator";
import { RaidManagerIfc, RaidManagerFeatureIfc } from "./RPCInterface";
import { FrontworkComponent } from "../../Types/FrontworkComponent";
import { TableDefiniton, Signup, Raid, Character, RaidData, Spec, SRToken, Item, User } from "../../Types/Types";
import { IAdmin } from "../../Admin/Interface";
import { IRaidManager } from "./Interface";
import { IUserManager } from "../User/Interface";
import { ICharacterManager } from "../Character/Interface";
import { _Tiers } from "../../Types/Items";
import { MAX_MEMO_LENGTH } from "../../Types/Constants";
import { IItemManager } from "../Item/Interface";
import { ItemManager } from "../Item/ItemManager";
import { IPubSub } from "../PubSub/Interface";
import { getLogger } from "log4js";



@Injectable(IRaidManager)
export class RaidManager
    implements FrontworkComponent<RaidManagerIfc, RaidManagerFeatureIfc>, IRaidManager {
    name = "RaidManager" as "RaidManager";

    @Inject(IAdmin)
    private admin: IAdmin

    @Inject(IUserManager)
    private userManager: IUserManager

    @Inject(ICharacterManager)
    private characterManager: ICharacterManager

    @Inject(IItemManager)
    private itemManager: IItemManager

    @Inject(IPubSub)
    private pubsub: IPubSub<any>

    RPCs = () => [
        this.getRaids,
        this.getRaidData,
        this.getPastRaids,
        this.getArchiveRaid
    ]

    RPCFeatures() {
        return [{
            name: 'manageRaid' as 'manageRaid',
            RPCs: () => [
                this.createRaid,
                this.addSignup,
                this.removeSignup,
                this.archiveRaid,
                this.updateSignup,
                this.startRaid,
                this.adminUnsign,
                this.cancelRaid
            ]
        }, {
            name: 'signup' as 'signup',
            RPCs: () => [
                this.getSignups,
                this.sign,
                this.unsign
            ]
        }]
    }

    getTableDefinitions(): TableDefiniton[] {
        return [
            {
                name: 'raids',
                tableBuilder: (table) => {
                    table.increments('id').primary()
                    table.dateTime('start').notNullable()
                    table.string('description').notNullable()
                    table.string('title').notNullable()
                    table.integer('size').defaultTo(40)
                    table.string('tier').defaultTo('null')
                }
            }, {
                name: 'archive',
                tableBuilder: (table) => {
                    table.integer('id').primary()
                    table.json('raiddata').notNullable()
                }
            }, {
                name: 'signups',
                tableBuilder: (table) => {
                    table.increments('id').primary()
                    table.unique(['raidid', 'characterid'])
                    table.integer('raidid')
                    table.foreign('raidid').references('id').inTable('raids').onDelete('CASCADE')
                    table.integer('characterid')
                    table.foreign('characterid').references('id').inTable('characters').onDelete('CASCADE')
                    table.boolean('benched').defaultTo('false')
                    table.boolean('late')
                    table.timestamp('timestamp').defaultTo(this.admin.knex.fn.now())
                    table.string('memo').nullable()
                }
            }
        ]
    }

    notifyRaid = async (raid: Raid | { id: number }) => {
        const data = await this.getRaidData(<Raid>raid)
        try {
            await this.pubsub.publish(String(raid.id), data)
            await this.notifyRaids()
        } catch (e) {
            getLogger('RaidManager#notifyRaid').debug(e);
        }
    }

    notifyRaids = async () => {
        try {
            await this.pubsub.publish('raids', undefined)
        } catch (e) {
            getLogger('RaidManager#notifyRaids').debug(e);
        }
    }

    createRaid = async (raid: Raid): Promise<Raid> => {
        const ids: number[] = await this.admin
            .knex('raids')
            .insert(raid)

        await this.notifyRaid({ id: ids[0] })

        return await this.admin.knex('raids').where({ id: ids[0] }).first()
    }

    addSignup = async (signup: Signup) => {

        const ids: number[] = await this.admin
            .knex('signups')
            .insert(signup)
        return await this.admin.knex('signups').where({ id: ids[0] }).first()

    }

    removeSignup = async (signup: Signup) => await this.admin
        .knex('signups')
        .where({
            raid_id: signup.raidid,
            character_id: signup.characterid
        })
        .del()

    getRaids = async (): Promise<Raid[]> => {

        const countSignups = this.admin
            .knex('signups')
            .count('*')
            .where({
                raidid: this.admin.knex.ref('raids.id'),
                benched: false,
                late: false,
                absent: false
            })
            .as('signupcount')

        const countBenches = this.admin
            .knex('signups')
            .count('*')
            .where({
                raidid: this.admin.knex.ref('raids.id'),
                benched: true,
                late: false
            })
            .as('benchcount')

        return await this.admin.knex('raids')
            .select('*', countSignups, countBenches)
            .orderBy('start', 'asc')
    }

    startRaid = async (raid: Raid): Promise<RaidData> => {
        const data = await this.getRaidData(raid)
        await Promise.all(data.participants.absent.map(p => this.itemManager.decayTokensOfCharacter(raid.tier, {
            charactername: p.charactername,
            id: p.characterid,
            race: p.race,
            specid: p.specid,
            userid: p.userid
        }, 1)))
        await this.itemManager.decayTokens(data.tier)      
        const archived = await this.archiveRaid(raid)

        const giveCurrency = async (b: Character) => {
            const usr = await this.characterManager.getUserOfCharacter(b)
            await this.userManager.incrementCurrency(usr, raid.tier, 1)
        }

        const THREE_DAYS = 259200000;

        await Promise.all([
            ...archived.participants.bench.filter(signup => Number(signup.timestamp) < (Number(archived.start)-THREE_DAYS)).map(giveCurrency),
            ...Object.values(archived.participants).flat().map((b: Signup & Character & Spec) => giveCurrency(b))
        ])

        await this.notifyRaids()
        return archived
    }

    archiveRaid = async (raid: Raid): Promise<RaidData> => {
        const raidData = await this.getRaidData(raid)
        const tx = await this.admin.knex.transaction()

        await this.admin.knex('archive')
            .transacting(tx)
            .insert({
                id: raidData.id,
                raiddata: JSON.stringify(raidData)
            })

        await Promise.all(
            Object.values(raidData.participants).flat().flatMap((p: (Signup & Character & Spec)) =>
                this.admin
                    .knex(raid.tier + 'tokens')
                    .transacting(tx)
                    .where({
                        characterid: p.characterid,
                        signupid: null
                    })
                    .del()
            ))

        await this.admin.knex('raids')
            .transacting(tx)
            .where('id', '=', raid.id)
            .del()
        await tx.commit()


        const row = await this.admin.knex('archive')
            .select('*')
            .where({
                id: raidData.id,
            })
            .first()

        try {
            return JSON.parse(row.raiddata)
        } catch (e) {
            getLogger('RaidManager#archiveRaid').error(row.id + " could not get parsed")
            return {} as RaidData
        }
    }

    getArchiveRaid = async (id: number): Promise<RaidData> => {
        const data = await this.admin
            .knex('archive')
            .select('raiddata')
            .where({
                id: id
            })
            .first()

        try {
            return JSON.parse(data.raiddata)
        } catch (e) {
            getLogger('RaidManager#getArchiveRaid').error(id + " could not get parsed")
            return {} as RaidData
        }
    }

    getPastRaids = async (limit: number): Promise<RaidData[]> => {
        const raids = await this.admin.knex('archive')
            .select('*')
            .orderBy('id', 'desc')
            .limit(limit)

        return raids.map(raid => JSON.parse(raid.raiddata))
    }

    getRaidData = async (raid: Raid): Promise<RaidData> => {
        const raiddata = {
            participants: {
                Druid: <(Signup & Character & Spec)[]>[],
                Hunter: <(Signup & Character & Spec)[]>[],
                Mage: <(Signup & Character & Spec)[]>[],
                Paladin: <(Signup & Character & Spec)[]>[],
                Priest: <(Signup & Character & Spec)[]>[],
                Rogue: <(Signup & Character & Spec)[]>[],
                Shaman: <(Signup & Character & Spec)[]>[],
                Warlock: <(Signup & Character & Spec)[]>[],
                Warrior: <(Signup & Character & Spec)[]>[],
                late: <(Signup & Character & Spec)[]>[],
                bench: <(Signup & Character & Spec)[]>[],
                absent: <(Signup & Character & Spec)[]>[]
            },
            tokens: {},
            healers: <(Signup & Character & Spec)[]>[],
            tanks: <(Signup & Character & Spec)[]>[]
        }

        const subQuery = this.admin
            .knex('signups')
            .count('*')
            .where({
                raidid: this.admin.knex.ref('raids.id'),
                benched: false,
                late: false,
                absent: false
            })
            .as('signupcount')


        const raidInDb: Raid = await this.admin.knex('raids')
            .select('*', subQuery)
            .where('id', '=', raid.id)
            .first()

        if(!raidInDb){
            return {
                title : "Not found", 
                description : "Not found", 
                start: "0", 
                size: 0, 
                tier: null,
                ... raiddata
            }
        }

        const characterData: (Signup & Character & Spec)[] = await this.admin
            .knex('signups as s')
            .select('s.id as id', 'charactername', 'rank', 'class', 'specid', 'specname', 'race', 'userid', 'benched', 'late', 'raidid', 'characterid', 'specid', 'memo', 'timestamp', 'absent')
            .join('raids as r', 's.raidid', '=', 'r.id')
            .join('characters as c', 's.characterid', '=', 'c.id')
            .join('users as u', 'c.userid', '=', 'u.id')
            .join('specs as sp', 'specid', '=', 'sp.id')
            .where('r.id', '=', raid.id)

        characterData.forEach(data => {
            if(data.absent){
                raiddata.participants.absent.push(data)
                return
            }
            if (data.benched) {
                raiddata.participants.bench.push(data)
                return
            }
            if (data.late) {
                raiddata.participants.late.push(data)
                return
            }
            raiddata.participants[data.class].push(data)
        })

        const tokenData: (Character & SRToken & Item)[] = await this.admin
            .knex('signups as s')
            .select('*', 's.id as id')
            .join('raids as r', 's.raidid', '=', 'r.id')
            .join('characters as c', 's.characterid', '=', 'c.id')
            .join('users as u', 'u.id', '=', 'c.userid')
            .join(raidInDb.tier + 'tokens as t', 't.characterid', '=', 'c.id')
            .join('items as i', 'i.itemname', '=', 't.itemname')
            .where({
                'r.id': raid.id,
            })
            .andWhere(function () {
                //this.whereNotIn('u.rank', ['Trial', 'Guest'])
                this.whereNotNull('t.signupid')
            })

        tokenData.forEach(data => {
            if (!raiddata.tokens[data.itemname])
                raiddata.tokens[data.itemname] = []
            raiddata.tokens[data.itemname].push(data)
        })

        raiddata.tanks = Object.values(raiddata.participants).flatMap(
            (tanks: any[]) => tanks.filter((p: any) =>
                !p.benched
                && !p.absent
                && !p.late
                && (p.specname === "Protection"
                    || p.specname === "Feral (Tank)"))
        )
        raiddata.healers = Object.values(raiddata.participants).flatMap(
            (healers: any[]) => healers.filter((p: any) =>
                !p.benched
                && !p.absent
                && !p.late
                && (p.specname === "Holy"
                    || p.specname === "Discipline"
                    || p.specname === "Restoration"))
        )

        return {
            ...raidInDb,
            ...raiddata
        }
    }

    getSignups = async (raid: Raid): Promise<(Signup & Character & Spec & User)[]> => await this.admin
        .knex('signups as si')
        .join('characters as c', 'c.id', '=', 'characterid')
        .join('specs as s', 's.id', '=', 'specid')
        .join('users as u', 'u.id', '=', 'userid')
        .select('*', 'si.id as id')
        .where('raidid', '=', raid.id!)

    sign = async (usertoken: string, character: Character, raid: Raid, late: boolean, absent:boolean, memo?: string) => {
        const maybeUserRecord = this.userManager.getUserRecordByToken(usertoken)
        if (!maybeUserRecord || maybeUserRecord.user.id != character.userid) {
            throw new Error("Bad Usertoken")
        }

        if (memo) memo = memo.substring(0, MAX_MEMO_LENGTH)
        const exists = await this.admin
            .knex('signups')
            .select('*')
            .where({
                raidid: raid.id!,
                characterid: character.id!,
            })
            .first()

        if (!exists) {
            await this.admin
                .knex('signups')
                .insert({
                    raidid: raid.id!,
                    characterid: character.id!,
                    late: late,
                    absent: absent,
                    benched: false,
                    memo: memo
                })
        } else {
            await this.admin
                .knex('signups')
                .where({
                    id: exists.id
                })
                .update({
                    raidid: raid.id!,
                    characterid: character.id!,
                    late: late,
                    absent: absent,
                    benched: false,
                    memo: memo
                })
        }

        await this.notifyRaid(raid)

        return await this.admin
            .knex('signups')
            .select('*')
            .where({
                raidid: raid.id!,
                characterid: character.id!,
            })
            .first()
    }

    cancelRaid = async (raid: Raid) => {
        const data = await this.getRaidData(raid)
        const participants = Object.values(data.participants).flat()
        await Promise.all(
            participants.map(async p => await this.adminUnsign({...p, id: p.characterid}, raid))
        )
        await this.admin.knex('raids').where({id: raid.id}).del()
        await this.notifyRaids()
    }

    unsign = async (usertoken: string, character: Character, raid: Raid) => {
        const maybeUserRecord = this.userManager.getUserRecordByToken(usertoken)
        if (!maybeUserRecord || maybeUserRecord.user.id != character.userid) {
            throw new Error("Bad Usertoken")
        }

        return await this.adminUnsign(character, raid)
    }

    adminUnsign = async (character: Character, raid: Raid) => {
        const user = await this.characterManager.getUserOfCharacter(character)
        const tokens = await this.itemManager.getTokens(character, [raid.tier], true)

        //check if token has to be deleted
        if (tokens) {
            await Promise.all(
                tokens.map(async token => {
                    await this.userManager.incrementCurrency(user, raid.tier, 1)
                    const prio = await this.itemManager.calculatePriorities(token.itemname, character)
                    if (token.level <= prio + 2) {
                        await this.admin
                            .knex(raid.tier + 'tokens')
                            .where({
                                characterid: character.id,
                                itemname: token.itemname
                            }).del()
                    } else {
                        await this.admin
                            .knex(raid.tier + 'tokens')
                            .where({
                                characterid: character.id,
                                itemname: token.itemname
                            }).update({
                                signupid: null,
                                level: token.level - 2
                            })
                    }
                })
            )
        }

        await this.admin.knex('signups')
            .where({
                raidid: raid.id!,
                characterid: character.id!,
            })
            .del()

        await this.notifyRaid(raid)
    }

    updateSignup = async (signup: Signup): Promise<void> => {
        await this.admin.knex('signups')
            .where({
                raidid: signup.raidid,
                characterid: signup.characterid
            })
            .update(signup)

        await this.notifyRaid({ id: signup.raidid })
    }
}