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";

@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

    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
            ]
        },{
            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.enu('tier', _Tiers).defaultTo(null as any)
                }
            },{
                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')
                }
            }            
        ]
    }

    createRaid = async (raid:Raid) => await this.admin
    .knex('raids')
    .insert(raid)
    
    addSignup = async (signup: Signup) => await this.admin
    .knex('signups')
    .insert(signup)
    
    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)
        delete archived.participants.late

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

        await Promise.all([
            ...archived.participants.bench.map(giveCurrency),
            ...Object.values(archived.participants).map((group: any) => group.map(giveCurrency))
        ])

        return archived
    }

    archiveRaid = async (raid:Raid) : Promise<RaidData> => {
        const raidData = await this.getRaidData(raid)
        await this.admin.knex('archive')
        .insert({
            id:raidData.id,
            raiddata: JSON.stringify(raidData)
        })       

        await Promise.all(
            Object.values(raidData.participants).flat().flatMap((signup) =>  this.admin
                .knex('tokens')
                .where({
                    characterid: signup.characterid,
                    signupid: null
                })
                .del()
        ))

        await this.admin.knex('raids')
        .where('id', '=', raid.id)
        .del()
    
        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 ret = {
            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:{}
        }

        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)
        .where('id','=',raid.id)
        .first()

        const characterData: (Signup & Character & Spec)[] = await this.admin
        .knex('signups as s')
        .select('s.id as id', 'charactername', 'class', 'specname', 'race', 'userid', 'benched', 'late', 'raidid', 'characterid')
        .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){
                ret.participants.bench.push(data)
                return
            }
            if(data.late){
                ret.participants.late.push(data)
                return
            }
            ret.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')
        .where('r.id','=',raid.id)
        .andWhere(function(){
            this.whereNotNull('t.signupid')
        })
        .join('characters as c', 's.characterid','=','c.id')
        .join('tokens as t', 't.characterid','=','c.id')
        .join('items as i', 'i.itemname','=','t.itemname')

        tokenData.forEach(data => {
            if(!ret.tokens[data.itemname])
                ret.tokens[data.itemname] = []
            ret.tokens[data.itemname].push(data)
        })
        return {
            ...raidInDb,
            ...ret
        }
    }
    
    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 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
            })
        }else{
            await this.admin
            .knex('signups')
            .update({
                raidid: raid.id!,
                characterid: character.id!,
                late: late
            })
        }

        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")
        }

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

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