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, Character, SRToken, SRPriority, Spec, Signup, Raid, Stats } from "../../Types/Types";
import { IAdmin } from "../../Admin/Interface";
import { IItemManager } from "./Interface";
import { IUserManager } from "../User/Interface";
import { ICharacterManager } from "../Character/Interface";
import { IPubSub } from "../PubSub/Interface";
import { IRaidManager } from "../Raid/Interface";
import { getLogger } from "log4js";

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

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

    RPCFeatures = () => [{
        name: 'managePriorities' as 'managePriorities',
        RPCs: () => [
            this.setPriority,
            this.deletePriority
        ],
    }, {
        name: 'reset' as 'reset',
        RPCs: () => [
            this.wipeCurrencyAndItems
        ]
    },{
        name: 'decay' as 'decay',
        RPCs: () => [
            this.decayTokensOfCharacter
        ]
    }, {
        name: 'changeReserve' as 'changeReserve',
        RPCs: () => [
            this.decreaseReserve,
            this.increaseReserve
        ]
    }]

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

    notifyRaids = async () => {
        await 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, tier?:Tiers): Promise<Item | undefined> => {
        let item = await this.admin.knex('items').select('*').where('itemname', '=', name).first()
        if(!item){
            item = await this.fetchItem(name, tier)
            if (!item)
                return
            
            await this.admin
                .knex('items')
                .insert({
                    ...item,
                    stats: JSON.stringify(item.stats)
                })

            return this.getItem(name, tier)
        }

        item.stats = JSON.parse(item.stats)
        return item
    }

    fetchItem = async (name: string, tier?:Tiers): Promise<Item | undefined> => {
        const res = await fetch('https://classic.wowhead.com/item=' + name + '&xml');
        const txt = await res.text();
        const r = await parser.parseStringPromise(txt);
        try{
            if(!r.wowhead.item || !r.wowhead.item[0]) 
                return

            const j = JSON.parse('{'+r.wowhead.item[0].json[0]+"}")
            if(!tier){
                if(j.sourcemore && Number.isInteger(j.sourcemore[0].t) && j.sourcemore[0].t > 0){
                    tier = _Tiers[j.sourcemore[0].t-1]
                }else{
                    tier = "MC"
                }
            }
            const stats = JSON.parse('{'+r.wowhead.item[0].jsonEquip[0]+"}")

            const item =  <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]._,
                tooltip: r.wowhead.item[0].htmlTooltip[0],
                stats: toStats(stats),
                tier: tier
            }
            return item

        }catch(e){
            getLogger('ItemManager').error(name, 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.string('tooltip').notNullable()
                    table.enu('tier', _Tiers).notNullable()
                    table.json('stats')
                }
            }]
    }

    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, currency)
        const modifier = await this.calculatePriorities(itemname, character)

        const tx = await this.admin.knex.transaction()

        if (streaks.length > 0) { //if an old streak already exists
            const myStreak = streaks.find(token => token.itemname === itemname)
            if (myStreak) { //and the streak is for this item
                //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 + currency + 1
                    })

            }

            //getLogger('ItemManager').debug('delete other 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 there was a streak for this item we're done
            if (myStreak) {
                await tx.commit()
                await this.notifyRaid({ id: signup.raidid })
                return await this.getToken(character, item)
            }
        }

        //check if there is an active reserve for this 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', currency + 1)
                .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 + currency,
                    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:boolean): 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()
    }

    decayTokens = async (tier: Tiers, amount = 1) => {
        //getLogger('ItemManager#decayTokens').debug(tier)

        await this.admin
        .knex(tier + 'tokens')
        .decrement('level', Math.max(amount, 1))

        await this.admin
        .knex(tier + 'tokens as t')
        .where('level', '<=', 0)
        .del()
    }

    decayTokensOfCharacter = async (tier: Tiers, character:Character) => {
        //getLogger('ItemManager#decayTokensOfCharacter').debug(character, tier)
        await this.admin
        .knex(tier + 'tokens')
        .where({
            characterid: character.id
        })
        .decrement('level')

        await this.admin
        .knex(tier + 'tokens as t')
        .where('level', '<=', 0)
        .del()
    }

    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: [string, string[]]) => Promise.all(
                        kv[1].map(i => this.getItem(i, kv[0] as Tiers)                        )
                    ))
            )
        }
    }

    increaseReserve = async (token: Character & SRToken & Item, raidid?:number) : Promise<void> => {
        await this.admin
        .knex(token.tier+"tokens")
        .where({
            characterid: token.characterid,
            itemname: token.itemname
        }).update({
            level: token.level+1
        })

        if(raidid)
            this.raidManager.notifyRaid({id: raidid})
    }
       
    decreaseReserve = async (token: Character & SRToken & Item, raidid?:number) : Promise<void> => {
        await this.admin
        .knex(token.tier+"tokens")
        .where({
            characterid: token.characterid,
            itemname: token.itemname
        }).update({
            level: Math.max(1, token.level-1)
        })

        if(raidid)
            this.raidManager.notifyRaid({id: raidid})
    }

}

function toStats(obj:any){
    if(!obj) obj = {}
    return {
      stamina: obj.sta || 0,
      agility: obj.agi || 0,
      strength: obj.str || 0,
      intellect: obj.int || 0,
      spirit: obj.spi || 0,
  
      attackpower: obj.atkpwr || 0,
  
      meleehit: obj.mlehitpct || 0,
      meleecrit: obj.mlecritstrkpct || 0,
  
      rangeattackpower: obj.rgdatkpwr || 0,
      rangehit: obj.rgdhitpct || 0,
      rangecrit: obj.rgdcritstrkpct || 0,
  
      spellpower: obj.splpwr || 0,
      spellhit: obj.splhitpct || 0,
      spellcrit: obj.splcritstrkpct || 0,
      heal: obj.splheal || 0,

      frostspellpower: obj.frosplpwr || 0,
      firespellpower: obj.firsplpwr || 0,
      naturespellpower: obj.natsplpwr || 0,
      shadowspellpower: obj.shasplpwr || 0,
      arcanespellpower: obj.arcsplpwr || 0,
  
      fireresist: obj.firres || 0,
      arcaneresist: obj.arcres || 0,
      frostresist: obj.frores || 0,
      natureresist: obj.natres || 0,
      shadowresist: obj.shares || 0,
      
      armor: obj.armor || 0,
      defense: obj.def || 0,
      block: obj.blockpct || 0,
      blockvalue: obj.blockamount || 0,
      dodge: obj.dodgepct || 0,
      parry: obj.parrypct || 0,
    } as Stats
    
  }