import { allItems, _Tiers, Tiers } from "../../Types/Items";
import { Inject, Injectable } from "../../Injector/ServiceDecorator";
import { ItemManagerFeatureIfc, ItemManagerIfc } from "./RPCInterface";
import { FrontworkComponent } from "../../Types/FrontworkComponent";
import { TableDefinitionExporter } from "../../Types/Interfaces";
import { TableDefiniton, Item, User, Character, SRToken, SRPriority, Spec, Signup, Raid } from "../../Types/Types";
import { IAdmin } from "../../Admin/Interface";
import { IItemManager } from "./Interface";
import { getLogger } from "log4js";
import { IUserManager } from "../User/Interface";
import { ICharacterManager } from "../Character/Interface";
import { IPubSub } from "../PubSub/Interface";
import { IRaidManager } from "../Raid/Interface";

const fetch = require('node-fetch')
const xml2js = require('xml2js');
const parser = new xml2js.Parser(/* options */);

@Injectable(IItemManager)
export class ItemManager
implements FrontworkComponent<ItemManagerIfc, ItemManagerFeatureIfc>, TableDefinitionExporter, IItemManager{
    name = "ItemManager" as "ItemManager";    
    
    @Inject(IAdmin)
    private admin: IAdmin

    @Inject(IUserManager)
    private userManager: IUserManager

    @Inject(ICharacterManager)
    private character: ICharacterManager

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

    @Inject(IRaidManager)
    private raidManager: IRaidManager

    exportRPCs = () => [
        this.getItems,
        this.getItem,
        this.buyToken,
        this.calculatePriorities,
        this.getToken,
        this.getTokens,
        this.getAllPriorities
    ]

    exportRPCFeatures = () => [{
        name: 'managePriorities' as 'managePriorities',
        exportRPCs: () => [
            this.setPriority,
            this.deletePriority
        ],
    },{
        name: 'reset' as 'reset',
        exportRPCs: () => [
            this.wipeCurrencyAndItems
        ]
    }]

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

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

    wipeCurrencyAndItems = async () => {
        await Promise.all([
            this.userManager.wipeCurrency(),
            Promise.all(_Tiers.map(tier => this.admin.knex(tier+'tokens').where(true).del()))
        ])
    }

    getItems = async () :Promise<Item[]> => await this.admin.knex.select('*').from('items')

    getItem = async(name:string):Promise<Item> => {
        return await this.admin.knex('items').select('*').where('itemname','=',name).first()
    }

    fetchItem  = async (name:string):Promise<Item> => {
        const res = await fetch('https://classic.wowhead.com/item='+name+'&xml'); 
        const txt = await res.text(); 
        const r = await parser.parseStringPromise(txt); 

        try{
            return <Item>{
                itemname: r.wowhead.item[0].name[0],
                iconname: r.wowhead.item[0].icon[0]._,
                url: r.wowhead.item[0].link[0],
                quality: r.wowhead.item[0].quality[0]._,
            } 
        }catch (e){
            console.log(name)
            throw e
        }
    }

    getTableDefinitions(): TableDefiniton[] {
        return [
            ...['null',..._Tiers].map(tier => {
                return {
                    name: tier+'tokens',
                    tableBuilder: (table) => {
                        table.primary(['characterid', 'itemname'])
                        table.integer("characterid")
                        table.foreign("characterid").references("id").inTable('characters')
                        table.string("itemname")
                        table.foreign("itemname").references("itemname").inTable('items')
                        table.string("signupid").nullable()
                        table.foreign("signupid").references("id").inTable('signups').onDelete('SET NULL')
                        table.integer("level").defaultTo(1)
                    }
                }
            }),{
                name: 'priorities',
                tableBuilder: (table) => {
                    table.increments('id').primary()
                    table.string('itemname')
                    table.foreign('itemname').references('itemname').inTable('items')
                    table.string('race').nullable()
                    table.integer('specid').nullable()
                    table.foreign('specid').references('id').inTable('specs')
                    table.integer('modifier')
                    table.string('description').nullable()
                }
            },{
                name: 'items',
                tableBuilder: (table) => {
                    table.string('itemname').unique().notNullable().primary()
                    table.string('iconname').notNullable()
                    table.string('url').notNullable()
                    table.string('quality').defaultTo('Epic').notNullable()
                    table.boolean('hidden').defaultTo(false).notNullable()
                    table.enu('tier', _Tiers).notNullable()
                }
            }]
    }

    buyToken = async (usertoken: string, charactername:string, itemname:string, signup: Signup): Promise<(SRToken & Character & Item) | undefined> => {
        const record = this.userManager.getUserRecordByToken(usertoken)
        const character = await this.character.getCharacterByName(charactername)

        if(!record || !character || record.user.username !== character.username) return
      
        const item = await this.getItem(itemname)
        if(!item) return

        const currency = await this.userManager.getCurrency(record.user, item.tier)
        if(currency < 1) return

        const streaks = await this.getTokens(character, [item.tier], false)
        const activeTokens = await this.getTokens(character, [item.tier], true)

        await this.userManager.decrementCurrency(record.user, item.tier, 1)
        const modifier = await this.calculatePriorities(itemname, character)
        
        const tx = await this.admin.knex.transaction()

        if(streaks.length > 0){
            const myStreak = streaks.find(token => token.itemname === itemname)
            if(myStreak){
                //getLogger('ItemManager').debug('update signupid and increment level')
                await this.admin
                .knex(item.tier+'tokens')
                .transacting(tx)
                .where({
                    characterid: character.id,
                    itemname: item.itemname
                })
                .update({
                    signupid: signup.id,
                    level: myStreak.level+1
                })

            }

            //getLogger('ItemManager').debug('delete streaks')
            await Promise.all(streaks.map(async s => await this.admin
                .knex(item.tier+'tokens')
                .transacting(tx)
                .where({
                    itemname: s.itemname,
                    characterid: character.id,
                    signupid: null
                })
                .del()
            ))

            if(myStreak){
                await tx.commit()
                await this.notifyRaid({id: signup.raidid})

                return await this.getToken(character, item)
            }
        }

        const matchingReserve = activeTokens.find(token => token.itemname === itemname)
        if(matchingReserve){
            //getLogger('ItemManager').debug('upgrade reserve')
            await this.admin
            .knex(item.tier+'tokens')
            .transacting(tx)
            .increment('level')
            .where({
                signupid: signup.id,
                characterid: character.id,
                itemname: item.itemname
            })
        }else{
            //getLogger('ItemManager').debug('new reserve')
            await this.admin
            .knex(item.tier+'tokens')
            .transacting(tx)
            .insert({
                characterid: character.id,
                itemname: item.itemname,
                level: 1+modifier,
                signupid: signup.id
            })
        }
        await tx.commit()
        await this.notifyRaid({id: signup.raidid})

        return await this.getToken(character, item)
    }

    getAllPriorities = async() :Promise<(SRPriority & Spec & Item)[]> => this.admin
    .knex('priorities as p')
    .join('items as i', 'p.itemname', '=', 'i.itemname')
    .leftJoin('specs as s', 'p.specid', '=', 's.id')
    .select('*') 

    deletePriority = async(priority:SRPriority) : Promise<void> => {
        await this.admin.knex('priorities')
        .where(priority)
        .del()
    }

    setPriority = async(itemname:string, priority: SRPriority) : Promise<void> => {
        const item = await this.getItem(itemname)
      
        await this.admin
        .knex('priorities')
        .insert(<SRPriority>{
            itemname: item.itemname,
            ...priority
        })
    }

    getPriorities = async(itemname:string) : Promise<SRPriority[]> => {
        return await this.admin
        .knex('priorities as p')
        .where('p.itemname', '=', itemname)
        .select('*')
    }

    calculatePriorities = async (itemname: string, character:Character):Promise<number>=> {
        const rules : SRPriority[] = await this.admin
        .knex('priorities as p')
        .select('*')
        .join('items as i', 'i.itemname', '=', 'p.itemname')
        .where('p.itemname', '=', itemname)

        return rules.map(rule => {
            if(rule.specid && rule.race){
                if(rule.specid === character.specid && rule.race === character.race) 
                    return rule.modifier
                else
                    return 0
            }

            if(rule.specid){
                if(rule.specid === character.specid)
                    return rule.modifier
                else
                    return 0
            }

            if(rule.race){
                if(rule.race === character.race) 
                    return rule.modifier
                else
                    return 0
            }
            return 0
        }).reduce((prev, curr) => prev+curr, 0)
    }

    getToken = async (character:Character, item:Item, valid=true): Promise<(SRToken & Character & Item) | undefined>=> {
        return await this.admin
        .knex(item.tier+'tokens as t')
        .select('*')
        .join('characters as c', 'c.id', '=', 't.characterid')
        .join('items as i', 'i.itemname', '=', 't.itemname')
        .where({
            characterid: character.id,
            "i.itemname": item.itemname
        })
        .andWhere(function(){
            if(valid){
                this.whereNotNull('t.signupid')
            }else{
                this.whereNull('t.signupid')
            }
        })
        .first()
    }

    getTokens = async (character:Character, tiers:Tiers[], valid=true) : Promise<(SRToken & Character & Item)[]> => {
        const ret = await Promise.all(tiers.map(async tier => {
            return await this.admin
            .knex(tier+'tokens as t')
            .select('*')
            .join('characters as c', 'c.id', '=', 't.characterid')
            .join('items as i', 'i.itemname', '=', 't.itemname')
            .where({
                characterid: character.id,
            })
            .andWhere(function(){
                if(valid){
                    this.whereNotNull('t.signupid')
                }else{
                    this.whereNull('t.signupid')
                }
            })
        }))
        return ret.flat()
    }

    countItems = async() :Promise<number> => {
        const count = await this.admin
        .knex('items')
        .count('*');
        
        return <number>count[0]['count(*)']
    }

    private initialized = false
    initialize = async () => {
        if(this.initialized) return
        this.initialized = true        
        
        const itemTiers = allItems

        const countCache = await this.countItems()

        if(countCache != Object.values(itemTiers).flat().length){
            await Promise.all(
                Object.entries(itemTiers)
                .map((kv) => Promise.all(
                    kv[1].map(i => this.fetchItem(i)
                        .then(item => this.admin
                            .knex('items')
                            .insert({
                                tier: kv[0],
                                ...item
                            })
                    ))
                ))
            )
        }
    }
}