import { Type } from './types'
import { AttrGroup, Attribute } from './attributes'
import { Relation } from './relations'
import { Language } from './languages'
import { sequelize } from '../models'
import { Role, User } from './users'
import { Action } from './actions'
import { Dashboard } from './dashboards'
import { WhereOptions } from 'sequelize'
import { Channel } from './channels'

import logger from '../logger'
import NodeCache from 'node-cache'

export class ModelManager {
    private typeRoot: TreeNode<void> = new TreeNode<void>()
    private tenantId: string
    private attrGroups: AttrGroupWrapper[] = []
    private relations: Relation[] = []
    private languages: Language[] = []
    private channels: Channel[] = []
    private actions: Action[] = []
    private dashboards: Dashboard[] = []
    private actionsCache:any = {}
    private roles: Role[] = []
    private users: UserWrapper[] = []
    private cache = new NodeCache()

    public constructor(tenantId: string) { this.tenantId = tenantId }

    public getTenantId() { return this.tenantId }

    public getRoot() { return this.typeRoot }

    public getRoles() { return this.roles }
    public getUsers() { return this.users }

    public getCache() { return this.cache }

    public getLanguages(): Language[] {
        return this.languages
    }

    public getChannels(): Channel[] {
        return this.channels
    }

    public getActions(): Action[] {
        return this.actions
    }

    public getDashboards(): Dashboard[] {
        return this.dashboards
    }

    public getActionsCache(): any {
        return this.actionsCache
    }

    public getRelations(): Relation[] {
        return this.relations
    }

    public dumpRelations() {
        return this.relations.map((rel) => {
            const data = {internalId: 0}
            Object.assign(data, rel.get({ plain: true }))
            data.internalId = rel.id
            return data
        })
    }

    public getRelationById(id: number): Relation | undefined {
        return this.relations.find( (rel) => rel.id === id)    
    }

    public getRelationByIdentifier(identifier: string): Relation | undefined {
        return this.relations.find( (rel) => rel.identifier === identifier)    
    }

    public getTypes() : any {
        const result:any[] = []
        this.dumpChildren(result, this.typeRoot.getChildren())
        return result
    }

    private dumpChildren(arr:any[], children: TreeNode<Type>[]) {
        for (var i = 0; i < children.length; i++) {
            const child = children[i]
            const data = {children: [], internalId: 0, link: 0, name: '', icon: '', iconColor: ''}
            Object.assign(data, child.getValue()?.get({ plain: true }))
            data.internalId = child.getValue()!.id
            if (data.link !== 0) {
                const linkParent = this.getTypeById(data.link)
                const linkType = <Type>linkParent!.getValue()
                data.name = linkType.name
                data.icon = linkType.icon
                data.iconColor = linkType.iconColor
            }
            arr.push(data)
            // console.log(data)
            this.dumpChildren(data.children, child.getChildren())
        }
    }

    public addType(parentId: number, type: Type) {
        if (parentId) {
            const parent = this.getTypeById(parentId)
            if (parent) {
                const node = new TreeNode<Type>(type, parent)
                parent.getChildren().push(node)
            } else {
                logger.error('Failed to find parent by id: ' + parentId + ' in manager: ' + this.tenantId)
            }
        } else {
            const node = new TreeNode<Type>(type, this.typeRoot)
            this.typeRoot.getChildren().push(node)
        }
    }

    public getTypeById(id: number): TreeNode<any> | null {
        const res =  this.findNode(id, this.typeRoot.getChildren(), (id, item) => item.getValue().id === id)
        return res
    }

    public getTypeByLinkId(id: number): TreeNode<any> | null {
        const res =  this.findNode(id, this.typeRoot.getChildren(), (id, item) => item.getValue().link === id)
        return res
    }

    public getTypeByIdentifier(identifier: string): TreeNode<any> | null {
        return this.findNode(identifier, this.typeRoot.getChildren(), (id, item) => item.getValue().identifier === identifier)
    }

    private findNode (id: any, children:TreeNode<any>[], comparator: ((id: any, item: TreeNode<any>) => boolean)): TreeNode<any> | null {
        for (var i = 0; i < children.length; i++) {
          const item = children[i]
          // console.log('check-', item.getValue().id, typeof item.getValue().id, id, typeof id)
          if (comparator(id, item)) {
            return item
          } else {
            const found = this.findNode(id, item.getChildren(), comparator)
            if (found) {
              return found
            }
          }
        }
        return null
    }

    public getAttrGroups(): AttrGroupWrapper[] {
        return this.attrGroups
    }

    public getAttributesInfo() : any[] {
        const result:any[] = []
        let attrId = Date.now()
        this.attrGroups.forEach((grp) => {
            const attributes :any[] = []
            const group: {attributes:any[], internalId: number, group: boolean, Attributes?: []} = {attributes: attributes, internalId: 0, group: true, Attributes: []}
            Object.assign(group, grp.getGroup().get({ plain: true }))
            delete group.Attributes
            group.internalId = grp.getGroup().id
            group.attributes = grp.getAttributes().map( attr => {
                const data: {internalId: number, group: boolean, id:number, GroupsAttributes?:string} = {internalId: 0, group: false, id:0, GroupsAttributes:''}
                Object.assign(data, attr.get({ plain: true }))
                data.internalId = attr.id
                data.id = attrId++ // we have to assign unique id
    
                delete data.GroupsAttributes
                return data
            })
            result.push(group)
        }) 
        return result
    }

    public getAttribute(id: number) : { attr: Attribute, groups: AttrGroup[] } | null {
        const groups: AttrGroup[] = []
        let attr: Attribute | null = null
        for (var i = 0; i < this.attrGroups.length; i++) {
            const group = this.attrGroups[i]
            const attributes = group.getAttributes()
            for (var j = 0; j < attributes.length; j++) {
                if (attributes[j].id === id) {
                    attr = attributes[j]
                    groups.push(group.getGroup())
                }
            }
        }  
        return attr ? {attr:attr, groups:groups} : null
    }

    public getAttributeByIdentifier(identifier: string) : { attr: Attribute, groups: AttrGroup[] } | null {
        const groups: AttrGroup[] = []
        let attr: Attribute | null = null
        for (var i = 0; i < this.attrGroups.length; i++) {
            const group = this.attrGroups[i]
            const attributes = group.getAttributes()
            for (var j = 0; j < attributes.length; j++) {
                if (attributes[j].identifier === identifier) {
                    attr = attributes[j]
                    groups.push(group.getGroup())
                }
            }
        }  
        return attr ? {attr:attr, groups:groups} : null
    }
}

export class ModelsManager {
    private static instance: ModelsManager
    private tenantMap: Record<string, ModelManager> = {}
    private channelTypes: number[] = [1]
    
    private constructor() { }
   
    public static getInstance(): ModelsManager {
        if (!ModelsManager.instance) {
            ModelsManager.instance = new ModelsManager()
        }

        return ModelsManager.instance
    }

    public getTenants() {
        return Object.entries(this.tenantMap).map(entry => entry[0])
    }

    public getModelManager(tenant: string): ModelManager {
        let tst = this.tenantMap[tenant]
        if (!tst) {
            logger.warn('Can not find model for tenant: ' + tenant);
            // const mng = new ModelManager(tenant)
            // this.tenantMap[tenant] = mng
        }
        return tst
    }

    public getChannelTypes() { return this.channelTypes }

    public async init(channelTypes?: number[]) {
        if (channelTypes) this.channelTypes = channelTypes
        let where: WhereOptions | undefined = undefined
        if (process.argv.length > 3) {
            where = {tenantId: process.argv.splice(3)}
        }
        await this.initModels(where)
    }

    public async reloadModel(tenantId: string) {
        delete this.tenantMap[tenantId]
        this.initModels({tenantId: [tenantId]})
    }

    public async initModels(where: WhereOptions | undefined) {
        await this.initLanguages(where)

        /*
        const types: Type[] = await sequelize.query('SELECT * FROM types order by "tenantId", path', {
            model: Type,
            mapToModel: true
        }); */

        const types: Type[] = await Type.findAll({
            where: where,
            order: [['tenantId', 'DESC'],['path', 'ASC']]})

        let mng: ModelManager
        let currentNode: TreeNode<any>
        let currentLevel: number
        types.forEach( (type) => {
            // console.log('loading type-' + type.path)
            if (!mng || mng.getTenantId() !== type.tenantId) {
                mng = this.tenantMap[type.tenantId]
                currentNode = mng.getRoot()
                currentLevel = 1
            }

            const arr = type.path.split('.')
            /* console.log(currentLevel, arr.length, 
                (currentNode.getValue() ? 'cur-'+currentNode.getValue() : 'null'), 
                (currentNode.getParent() ? 'par-'+currentNode.getParent()! : "no parent")) */
            if (currentLevel > arr.length) {
                // go to one parent up
                while(currentLevel > arr.length) {
                    currentNode = currentNode.getParent()!
                    currentLevel--
                }
            }

            const node = new TreeNode<Type>(type, currentNode)
            // console.log('parent -' + JSON.stringify(currentNode.getValue()))
            currentNode.getChildren().push(node)
            currentNode = node
            currentLevel++
        })

        await this.initAttributes(where)
        await this.initRelations(where)
        await this.initRoles(where)
        await this.initActions(where)
        await this.initDashboards(where)
        await this.initChannels(where)

        logger.info('Data models were loaded')
    }

    public async initRoles(where: WhereOptions | undefined) {
        // TODO optimize this to load data by 1 select with join
        const roles = await Role.findAll({
            where: where,
            order: [['tenantId', 'DESC']]})
        if (!roles) return

        let mng: ModelManager | null = null
        for (var i = 0; i < roles.length; i++) {
            const role = roles[i];
            if (!mng || mng.getTenantId() !== role.tenantId) {
                mng = this.tenantMap[role.tenantId]
            }
            (<any>role).internalId = role.id
            mng.getRoles().push(role)
        }

        const users = await User.findAll({
            where: where,
            order: [['tenantId', 'DESC']]})
        if (!users) return

        mng = null
        for (var i = 0; i < users.length; i++) {
            const user = users[i];

            if (user.tenantId === '0') continue // super user, skip it

            if (!mng || mng.getTenantId() !== user.tenantId) {
                mng = this.tenantMap[user.tenantId]
            }
            (<any>user).internalId = user.id;
            const roles = user.roles ? user.roles.map((roleId: number) => mng!.getRoles().find(role => role.id === roleId)) : []
            mng.getUsers().push(new UserWrapper(user, roles))
        }
    }

    public async initAttributes(where: WhereOptions | undefined) {
        // TODO optimize this to load data by 1 select with join
        const groups = await AttrGroup.findAll({
            where: where,
            include: [{model: Attribute}],
            order: [['tenantId', 'DESC']]})
        if (!groups) return

        let mng: ModelManager | null = null
        for (var i = 0; i < groups.length; i++) {
            const grp = groups[i];
            if (!mng || mng.getTenantId() !== grp.tenantId) {
                mng = this.tenantMap[grp.tenantId]
            }
            mng.getAttrGroups().push(new AttrGroupWrapper(grp, await grp.getAttributes()))
        }
    }

    public async initRelations(where: WhereOptions | undefined) {
        const rels = await Relation.findAll({
            where: where,
            order: [['tenantId', 'DESC']]})
        if (!rels) return

        let mng: ModelManager | null = null
        for (var i = 0; i < rels.length; i++) {
            const rel = rels[i];
            if (!mng || mng.getTenantId() !== rel.tenantId) {
                mng = this.tenantMap[rel.tenantId]
            }
            mng.getRelations().push(rel)
        }
    }

    public async initActions(where: WhereOptions | undefined) {
        const actions = await Action.findAll({
            where: where,
            order: [['tenantId', 'DESC']]})
        if (!actions) return

        let mng: ModelManager | null = null
        for (var i = 0; i < actions.length; i++) {
            const action = actions[i];
            if (!mng || mng.getTenantId() !== action.tenantId) {
                mng = this.tenantMap[action.tenantId]
            }
            mng.getActions().push(action)
        }
    }

    public async initDashboards(where: WhereOptions | undefined) {
        const dashboards = await Dashboard.findAll({
            where: where,
            order: [['tenantId', 'DESC']]})
        if (!dashboards) return

        let mng: ModelManager | null = null
        for (var i = 0; i < dashboards.length; i++) {
            const dashboard = dashboards[i];
            if (!mng || mng.getTenantId() !== dashboard.tenantId) {
                mng = this.tenantMap[dashboard.tenantId]
            }
            mng.getDashboards().push(dashboard)
        }
    }

    public async initChannels(where: WhereOptions | undefined) {
        const items = await Channel.findAll({
            where: where,
            order: [['tenantId', 'DESC']]})
        if (!items) return

        let mng: ModelManager | null = null
        for (var i = 0; i < items.length; i++) {
            const chan = items[i];
            if (!mng || mng.getTenantId() !== chan.tenantId) {
                mng = this.tenantMap[chan.tenantId]
            }
            mng.getChannels().push(chan)
        }
    }

    public async initLanguages(where: WhereOptions | undefined) {
        const languages = await Language.findAll({
            where: where,
            order: [['tenantId', 'DESC'], ['id', 'ASC']]})
        if (!languages) return

        let mng: ModelManager | null = null
        for (var i = 0; i < languages.length; i++) {
            const lang = languages[i];
            if (!mng || mng.getTenantId() !== lang.tenantId) {
                mng = new ModelManager(lang.tenantId)
                this.tenantMap[lang.tenantId] = mng
            }
            mng.getLanguages().push(lang)
        }
    }

}

export class TreeNode<T> {
    private parent: TreeNode<any> | null = null
    private children: TreeNode<any>[] = []
    private value: T | null = null

    public constructor(value?: T, parent?: TreeNode<any>) {
        if (parent) {
            this.parent = parent
        }
        if (value) {
            this.value = value
        }
    }

    public getParent(): TreeNode<any> | null {
        return this.parent
    }

    public getChildren(): TreeNode<any>[] {
        return this.children
    }

    public getValue(): T | null {
        return this.value
    }

    public deleteChild(child: TreeNode<any>): void {
        const idx = this.children.indexOf(child)
        if (idx !== -1) this.children.splice(idx, 1)
    }
}

export class AttrGroupWrapper {
    private group: AttrGroup
    private attributes: Attribute[] = []

    public constructor(group: AttrGroup, attributes?: Attribute[]) {
        this.group = group
        if (attributes && attributes.length > 0) {
            this.attributes = attributes
        }
    }

    public getGroup() {
        return this.group
    }

    public getAttributes() {
        return this.attributes
    }
}

export class UserWrapper {
    private user: User
    private roles: Role[] = []

    public constructor(user: User, roles?: Role[]) {
        this.user = user
        if (roles && roles.length > 0) {
            this.roles = roles
        }
    }

    public getUser() {
        return this.user
    }

    public getRoles() {
        return this.roles
    }

    public setRoles(roles: Role[]) {
        this.roles = roles
    }

}