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 { IItemManager } from "../Item/Interface";
import { ItemManager } from "../Item/ItemManager";
import { IPubSub } from "../PubSub/Interface";

@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(ItemManager)
    private itemManager: IItemManager

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

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

    exportRPCFeatures() {
        return [{
            name: 'manageRaid' as 'manageRaid',
            exportRPCs: () => [
                this.createRaid,
                this.addSignup,
                this.removeSignup,
                this.archiveRaid,
                this.setBenched,
                this.startRaid,
                this.adminUnsign
            ]
        }, {
            name: 'signup' as 'signup',
            exportRPCs: () => [
                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')
                }
            }
        ]
    }

    notifyRaid = async (raid:Raid | {id:number}) => {
        const data = await this.getRaidData(<Raid>raid)
        this.pubsub.publish(""+raid.id, data)
        await this.notifyRaids()
    }

    notifyRaids = async () => {
        this.pubsub.publish('raids', undefined)
    }

    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 subQuery = this.admin
            .knex('signups')
            .count('*')
            .where({
                raidid: this.admin.knex.ref('raids.id'),
                benched: false,
                late: false
            })
            .as('signupcount')

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

    startRaid = async (raid: Raid): Promise<RaidData> => {
        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)
        }

        await Promise.all([
            ...archived.participants.bench.map(giveCurrency),
            ...Object.values(archived.participants).flat().flatMap((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()
        return JSON.parse(row.raiddata)
    }

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

        return JSON.parse(data.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)[]>[],
            },
            tokens: {},
            healers: <(Signup & Character & Spec)[]>[],
            tanks: <(Signup & Character & Spec)[]>[]
        }
        //const tx = await this.admin.knex.transaction()

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


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

        const characterData: (Signup & Character & Spec)[] = await this.admin
            .knex('signups as s')
            //.transacting(tx)
            .select('s.id as id', 'charactername', 'class', 'specid', 'specname', 'race', 'userid', 'benched', 'late', 'raidid', 'characterid', 'specid')
            .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.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')
            //.transacting(tx)
            .select('*', 's.id as id')
            .join('raids as r', 's.raidid', '=', 'r.id')
            .join('characters as c', 's.characterid', '=', 'c.id')
            .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.whereNotNull('t.signupid')
            })

        //await tx.commit()

        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.late
                && (p.specname === "Protection"
                    || p.specname === "Feral (Tank)"))
        )
        raiddata.healers = Object.values(raiddata.participants).flatMap(
            (healers: any[]) => healers.filter((p: any) =>
                !p.benched
                && !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) => {
        const maybeUserRecord = this.userManager.getUserRecordByToken(usertoken)
        if (!maybeUserRecord || maybeUserRecord.user.id != character.userid) {
            throw new Error("Bad Usertoken")
        }
        //const tx = await this.admin.knex.transaction()

        const exists = await this.admin
            .knex('signups')
            //.transacting(tx)
            .select('*')
            .where({
                raidid: raid.id!,
                characterid: character.id!,
            })
            .first()

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

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

    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 signup = await this.admin.knex('signups as si')
            .where({
                "si.raidid": raid.id!,
                "si.characterid": character.id!,
            }).first()

        const tokens = await this.itemManager.getTokens(character, [raid.tier], true)

        //check if token has to be deleted
        if (tokens) {
            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 + 1) {
                        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 - 1
                            })
                    }
                })
            )
        }

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

        await this.notifyRaid(raid)
    }

    setBenched = 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 })
    }
}