import { defineStore } from 'pinia'
import { endsWith, get, isEmpty, map, now, size } from 'lodash'
import {
    AddHashField,
    AddListItem,
    AddStreamValue,
    AddZSetValue,
    BatchSetTTL,
    CleanCmdHistory,
    CloseConnection,
    ConvertValue,
    DeleteKey,
    DeleteKeys,
    ExportKey,
    FlushDB,
    GetClientList,
    GetCmdHistory,
    GetKeyDetail,
    GetKeySummary,
    GetKeyType,
    GetSlowLogs,
    ImportCSV,
    LoadAllKeys,
    LoadNextAllKeys,
    LoadNextKeys,
    OpenConnection,
    OpenDatabase,
    RemoveStreamValues,
    RenameKey,
    ServerInfo,
    SetHashValue,
    SetKeyTTL,
    SetKeyValue,
    SetListItem,
    SetSetItem,
    UpdateSetItem,
    UpdateZSetValue,
} from 'wailsjs/go/services/browserService.js'
import useTabStore from 'stores/tab.js'
import { nativeRedisKey } from '@/utils/key_convert.js'
import { BrowserTabType } from '@/consts/browser_tab_type.js'
import { KeyViewType } from '@/consts/key_view_type.js'
import { ConnectionType } from '@/consts/connection_type.js'
import useConnectionStore from 'stores/connections.js'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import { isRedisGlob } from '@/utils/glob_pattern.js'
import { i18nGlobal } from '@/utils/i18n.js'
import { EventsEmit, EventsOn } from 'wailsjs/runtime/runtime.js'
import { RedisNodeItem } from '@/objects/redisNodeItem.js'
import { RedisServerState } from '@/objects/redisServerState.js'
import { RedisDatabaseItem } from '@/objects/redisDatabaseItem.js'
import { timeout } from '@/utils/promise.js'

const useBrowserStore = defineStore('browser', {
    /**
     * @typedef {Object} FilterItem
     * @property {string} pattern key pattern filter
     * @property {string} type type filter
     */

    /**
     * @typedef {Object} HistoryItem
     * @property {string} time
     * @property {string} server
     * @property {string} cmd
     * @property {number} cost
     */

    /**
     * @typedef {Object} BrowserState
     * @property {Object.<string, RedisServerState>} servers
     */

    /**
     *
     * @returns {BrowserState}
     */
    state: () => ({
        servers: {},
    }),
    getters: {
        anyConnectionOpened() {
            return !isEmpty(this.servers)
        },
    },
    actions: {
        /**
         * check if connection is connected
         * @param name
         * @returns {boolean}
         */
        isConnected(name) {
            return this.servers.hasOwnProperty(name)
        },

        /**
         * close all connections
         * @returns {Promise<void>}
         */
        async closeAllConnection() {
            for (const serverName in this.servers) {
                await CloseConnection(serverName)
                this.servers[serverName].dispose()
            }

            const tabStore = useTabStore()
            tabStore.removeAllTab()
        },

        /**
         * get database info list
         * @param server
         * @return {RedisDatabaseItem[]}
         */
        getDBList(server) {
            const serverInst = this.servers[server]
            if (serverInst != null) {
                return serverInst.getDatabase()
            }
            return []
        },

        /**
         * get database by server name and database index
         * @param {string} server
         * @param {number} db
         * @return {RedisDatabaseItem|null}
         */
        getDatabase(server, db) {
            /** @type {RedisServerState} **/
            const serverInst = this.servers[server]
            if (serverInst != null) {
                return serverInst.databases[db] || null
            }
            return null
        },

        /**
         * get current selection database by server
         * @param server
         * @return {number}
         */
        getSelectedDB(server) {
            /** @type {RedisServerState} **/
            const serverInst = this.servers[server]
            if (serverInst != null) {
                return serverInst.db
            }
            return 0
        },

        /**
         * get key struct in current database
         * @param {string} server
         * @param {boolean} [includeRoot]
         * @return {RedisNodeItem[]}
         */
        getKeyStruct(server, includeRoot) {
            /** @type {RedisServerState} **/
            const serverInst = this.servers[server]
            let rootNode = null
            if (serverInst != null) {
                rootNode = serverInst.getRoot()
            }
            if (includeRoot === true) {
                return [rootNode]
            }
            return get(rootNode, 'children', [])
        },

        getReloadKey(server) {
            /** @type {RedisServerState} **/
            const serverInst = this.servers[server]
            return serverInst != null ? serverInst.reloadKey : 0
        },

        reloadServer(server) {
            /** @type {RedisServerState} **/
            const serverInst = this.servers[server]
            if (serverInst != null) {
                serverInst.reloadKey = Date.now()
            }
        },

        /**
         * switch key view
         * @param {string} connName
         * @param {number} viewType
         */
        // async switchKeyView(connName, viewType) {
        //     if (viewType !== KeyViewType.Tree && viewType !== KeyViewType.List) {
        //         return
        //     }
        //
        //     const t = get(this.viewType, connName, KeyViewType.Tree)
        //     if (t === viewType) {
        //         return
        //     }
        //
        //     this.viewType[connName] = viewType
        //     const dbs = get(this.databases, connName, [])
        //     for (const dbItem of dbs) {
        //         if (!dbItem.opened) {
        //             continue
        //         }
        //
        //         dbItem.children = undefined
        //         dbItem.keyCount = 0
        //         const { db = 0 } = dbItem
        //         this._getNodeMap(connName, db).clear()
        //         this._addKeyNodes(connName, db, keys)
        //         this._tidyNode(connName, db, '')
        //     }
        // },

        /**
         * open connection
         * @param {string} name
         * @param {boolean} [reload]
         * @returns {Promise<void>}
         */
        async openConnection(name, reload) {
            if (this.isConnected(name)) {
                if (reload !== true) {
                    return
                } else {
                    // reload mode, try close connection first
                    await CloseConnection(name)
                }
            }

            const { data, success, msg } = await OpenConnection(name)
            if (!success) {
                throw new Error(msg)
            }
            // append to db node to current connection
            // const connNode = this.getConnection(name)
            // if (connNode == null) {
            //     throw new Error('no such connection')
            // }
            const { db, view = KeyViewType.Tree, lastDB } = data
            if (isEmpty(db)) {
                throw new Error('no db loaded')
            }
            const serverInst = new RedisServerState({
                name,
                separator: this.getSeparator(name),
                db: -1,
                viewType: view,
            })
            /** @type {Object.<number,RedisDatabaseItem>} **/
            const databases = {}
            for (const dbItem of db) {
                databases[dbItem.index] = new RedisDatabaseItem({
                    db: dbItem.index,
                    alias: dbItem.alias,
                    maxKeys: dbItem.maxKeys,
                })
                if (dbItem.index === lastDB) {
                    // set last opened database as default
                    serverInst.db = dbItem.index
                } else if (serverInst.db === -1) {
                    // set the first database as default
                    serverInst.db = dbItem.index
                }
            }
            serverInst.databases = databases
            this.servers[name] = serverInst
        },

        /**
         * close connection
         * @param {string} name
         * @returns {Promise<boolean>}
         */
        async closeConnection(name) {
            const { success, msg } = await CloseConnection(name)
            if (!success) {
                // throw new Error(msg)
                return false
            }
            delete this.servers[name]

            const tabStore = useTabStore()
            tabStore.removeTabByName(name)
            return true
        },

        /**
         * open database and load all keys
         * @param server
         * @param db
         * @returns {Promise<void>}
         */
        async openDatabase(server, db) {
            const { data, success, msg } = await OpenDatabase(server, db)
            if (!success) {
                throw new Error(msg)
            }
            const { keys = [], end = false, maxKeys = 0 } = data

            /** @type {RedisServerState} **/
            const serverInst = this.servers[server]
            if (serverInst == null) {
                return
            }
            serverInst.db = db
            serverInst.setDatabaseKeyCount(db, maxKeys)
            serverInst.loadingState.fullLoaded = end

            if (isEmpty(keys)) {
                serverInst.nodeMap.clear()
            } else {
                // append db node to current connection's children
                serverInst.addKeyNodes(keys)
            }
            serverInst.tidyNode('', false)
        },

        /**
         * close database
         * @param server
         * @param db
         */
        closeDatabase(server, db) {
            /** @type {RedisServerState} **/
            const serverInst = this.servers[server]
            if (serverInst == null) {
                return
            }
            if (serverInst.db !== db) {
                return
            }
            serverInst.closeDatabase()

            /** @type {RedisDatabaseItem} **/
            const selDB = this.getDatabase(server, db)
            if (selDB == null) {
                return
            }
            selDB.keyCount = 0
        },

        /**
         *
         * @param server
         * @returns {Promise<{}>}
         */
        async getServerInfo(server) {
            try {
                const { success, data, msg } = await ServerInfo(server)
                if (success) {
                    /** @type {RedisServerState} **/
                    const serverInst = this.servers[server]
                    if (serverInst != null) {
                        serverInst.stats = data
                    }
                    return data
                } else if (!isEmpty(msg)) {
                    $message.warning(msg)
                }
            } finally {
            }
            return {}
        },

        /**
         * load key summary info
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} [key] null or blank indicate that update tab to display normal content (blank content or server status)
         * @param {boolean} [clearValue]
         * @return {Promise<void>}
         */
        async loadKeySummary({ server, db, key, clearValue }) {
            try {
                const tab = useTabStore()
                if (!isEmpty(key)) {
                    const { data, success, msg } = await GetKeySummary({
                        server,
                        db,
                        key,
                    })
                    if (success) {
                        const { type, ttl, size, length } = data
                        const k = nativeRedisKey(key)
                        const binaryKey = k !== key
                        tab.upsertTab({
                            subTab: BrowserTabType.KeyDetail,
                            server,
                            db,
                            type,
                            ttl,
                            keyCode: binaryKey ? key : undefined,
                            key: k,
                            size,
                            length,
                            clearValue,
                        })
                        return
                    } else {
                        if (!isEmpty(msg)) {
                            $message.error('load key summary fail: ' + msg)
                        }
                        // its danger to delete "non-exists" key, just remove from tree view
                        // await this.deleteKey(server, db, key, true)
                        // TODO: show key not found page or check exists on server first?
                    }
                }

                tab.upsertTab({
                    subTab: BrowserTabType.Status,
                    server,
                    db,
                    type: 'none',
                    ttl: -1,
                    key: null,
                    keyCode: null,
                    size: 0,
                    length: 0,
                    clearValue,
                })
            } catch (e) {
                $message.error(e.message || 'unknown error')
            } finally {
            }
        },

        /**
         * load key type
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {number[]} keyCode
         * @return {Promise<void>}
         */
        async loadKeyType({ server, db, key }) {
            /** @type {RedisServerState} **/
            const serverInst = this.servers[server]
            if (serverInst == null) {
                return
            }
            const node = serverInst.getNode(ConnectionType.RedisValue, nativeRedisKey(key))
            if (node == null || !isEmpty(node.redisType)) {
                return
            }
            try {
                node.redisType = 'loading'
                const { data, success, msg } = await GetKeyType({ server, db, key })
                if (success) {
                    const { type } = data || {}
                    node.redisType = type
                } else {
                    node.redisType = 'NONE'
                }
            } catch (e) {
                node.redisType = 'NONE'
            } finally {
            }
        },

        /**
         * reload key
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {string} [decode]
         * @param {string} [format]
         * @param {string} [matchPattern]
         * @param {boolean} [showLoading]
         * @return {Promise<void>}
         */
        async reloadKey({ server, db, key, decode, format, matchPattern, showLoading = true }) {
            const tab = useTabStore()
            try {
                if (showLoading) {
                    tab.updateLoading({ server, db, loading: true })
                }
                await this.loadKeySummary({ server, db, key, clearValue: true })
                await this.loadKeyDetail({
                    server,
                    db,
                    key,
                    decode,
                    format,
                    matchPattern,
                    reset: true,
                    showLoading: false,
                })
            } finally {
                if (showLoading) {
                    tab.updateLoading({ server, db, loading: false })
                }
            }
        },

        /**
         * load key content
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {string} [format]
         * @param {string} [decode]
         * @param {string} [matchPattern]
         * @param {boolean} [reset]
         * @param {boolean} [full]
         * @param {boolean} [showLoading]
         * @return {Promise<void>}
         */
        async loadKeyDetail({ server, db, key, format, decode, matchPattern, reset, full, showLoading = true }) {
            const tab = useTabStore()
            const serverInst = this.servers[server]
            if (serverInst == null) {
                return
            }
            try {
                if (showLoading) {
                    tab.updateLoading({ server, db, loading: true })
                }
                const [storeFormat, storeDecode] = serverInst.getDecodeHistory(key, db)
                const { data, success, msg } = await GetKeyDetail({
                    server,
                    db,
                    key,
                    format: isEmpty(format) ? storeFormat : format,
                    decode: isEmpty(decode) ? storeDecode : decode,
                    matchPattern,
                    full: full === true,
                    reset,
                    lite: true,
                })
                if (success) {
                    const {
                        value,
                        keyType,
                        decode: retDecode,
                        format: retFormat,
                        match: retMatch,
                        reset: retReset,
                        end,
                    } = data
                    tab.updateValue({
                        server,
                        db,
                        key: nativeRedisKey(key),
                        value,
                        decode: retDecode || storeDecode,
                        format: retFormat || storeFormat,
                        reset: retReset,
                        matchPattern: retMatch || '',
                        end,
                    })
                } else {
                    $message.error('load key detail fail:' + msg)
                }
            } finally {
                if (showLoading) {
                    tab.updateLoading({ server, db, loading: false })
                }
            }
        },

        /**
         * convert value by decode type or format
         * @param {string|number[]} value
         * @param {string} [decode]
         * @param {string} [format]
         * @return {Promise<{[format]: string, [decode]: string, value: string}>}
         */
        async convertValue({ value, decode, format }) {
            try {
                const { data, success } = await ConvertValue(value, decode, format)
                if (success) {
                    const { value: retVal, decode: retDecode, format: retFormat } = data
                    return { value: retVal, decode: retDecode, format: retFormat }
                }
            } catch (e) {}
            return { value, decode, format }
        },

        /**
         * scan keys with prefix
         * @param {string} server
         * @param {number} db
         * @param {string} match
         * @param {boolean} exact
         * @param {string} [matchType]
         * @param {number} [loadType] 0.load next; 1.load next full; 2.reload load all
         * @returns {Promise<{keys: string[], maxKeys: number, end: boolean}>}
         */
        async scanKeys({ server, db, match = '*', exact = false, matchType = '', loadType = 0 }) {
            let resp
            switch (loadType) {
                case 0:
                default:
                    resp = await LoadNextKeys(server, db, match, matchType, exact)
                    break
                case 1:
                    resp = await LoadNextAllKeys(server, db, match, matchType, exact)
                    break
                case 2:
                    resp = await LoadAllKeys(server, db, match, matchType, exact)
                    break
            }
            const { data, success, msg } = resp || {}
            if (!success) {
                throw new Error(msg)
            }
            const { keys = [], maxKeys, end } = data
            return { keys, end, maxKeys, success }
        },

        /**
         *
         * @param {string} server
         * @param {number} db
         * @param {string|null} match
         * @param {boolean} exact
         * @param {string|null} matchType
         * @param {boolean} [all]
         * @return {Promise<{keys: Array<string|number[]>, maxKeys: number, end: boolean}>}
         * @private
         */
        async _loadKeys({ server, db, match, exact, matchType, all }) {
            if (isEmpty(match)) {
                match = '*'
            }

            if (!isRedisGlob(match) && !exact) {
                if (!endsWith(match, '*')) {
                    match = match + '*'
                }
            }
            return this.scanKeys({ server, db, match, exact, matchType, loadType: all ? 1 : 0 })
        },

        /**
         * load more keys within the database
         * @param {string} server
         * @param {number} db
         * @return {Promise<boolean>}
         */
        async loadMoreKeys(server, db) {
            const { match, type: keyType, exact } = this.getKeyFilter(server)
            const { keys, maxKeys, end } = await this._loadKeys({
                server,
                db,
                match,
                exact,
                matchType: keyType,
                all: false,
            })
            /** @type RedisServerState **/
            const serverInst = this.servers[server]
            if (serverInst != null) {
                serverInst.setDBKeyCount(db, maxKeys)
                // remove current keys below prefix
                serverInst.addKeyNodes(keys)
                serverInst.tidyNode('')
            }
            return end
        },

        /**
         * load all left keys within the database
         * @param {string} server
         * @param {number} db
         * @return {Promise<void>}
         */
        async loadAllKeys(server, db) {
            const { match, type: keyType, exact } = this.getKeyFilter(server)
            const { keys, maxKeys } = await this._loadKeys({ server, db, match, exact, matchType: keyType, all: true })
            /** @type RedisServerState **/
            const serverInst = this.servers[server]
            if (serverInst != null) {
                serverInst.setDBKeyCount(db, maxKeys)
                serverInst.addKeyNodes(keys)
                serverInst.tidyNode('')
            }
        },

        /**
         * reload keys under layer
         * @param {string} server
         * @param {number} db
         * @param {string} prefix
         * @return {Promise<void>}
         */
        async reloadLayer(server, db, prefix) {
            if (isEmpty(prefix)) {
                return
            }
            let match = prefix
            const separator = this.getSeparator(server)
            if (!endsWith(match, separator)) {
                match += separator + '*'
            } else {
                match += '*'
            }
            // FIXME: ignore original match pattern due to redis not support combination matching
            const { match: originMatch, type: keyType, exact } = this.getKeyFilter(server)
            const { keys, maxKeys, success } = await this._loadKeys({
                server,
                db,
                match: originMatch,
                exact: false,
                matchType: keyType,
                all: true,
            })
            if (!success) {
                return
            }

            /** @type RedisServerState **/
            const serverInst = this.servers[server]
            if (serverInst != null) {
                serverInst.setDBKeyCount(db, maxKeys)
                // remove current keys below prefix
                serverInst.removeKeyNode(prefix, true)
                serverInst.addKeyNodes(keys)
                serverInst.tidyNode(prefix)
            }
        },

        /**
         * get custom separator of connection
         * @param server
         * @returns {string}
         * @private
         */
        getSeparator(server) {
            const connStore = useConnectionStore()
            const { keySeparator } = connStore.getDefaultSeparator(server)
            if (isEmpty(keySeparator)) {
                return ':'
            }
            return keySeparator
        },

        /**
         * get tree node by key name
         * @param key
         * @return {RedisNodeItem|null}
         */
        getNode(key) {
            let idx = key.indexOf('#')
            if (idx < 0) {
                idx = size(key)
            }
            const dbPart = key.substring(0, idx)
            // parse server and db index
            const idx2 = dbPart.lastIndexOf('/db')
            if (idx2 < 0) {
                return null
            }
            const server = dbPart.substring(0, idx2)
            /** @type {RedisServerState} **/
            const serverInst = this.servers[server]
            if (serverInst == null) {
                return null
            }

            const db = parseInt(dbPart.substring(idx2 + 3))
            if (isNaN(db)) {
                return null
            }

            if (size(key) <= idx + 1) {
                return null
            }
            // contains redis key
            const keyPart = key.substring(idx + 1)
            return serverInst.nodeMap.get(keyPart)
        },

        /**
         * set redis key
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {string} keyType
         * @param {any} value
         * @param {number} ttl
         * @param {string} [format]
         * @param {string} [decode]
         * @returns {Promise<{[msg]: string, success: boolean, [nodeKey]: {string}}>}
         */
        async setKey({ server, db, key, keyType, value, ttl, format = formatTypes.RAW, decode = decodeTypes.NONE }) {
            try {
                const { data, success, msg } = await SetKeyValue({
                    server,
                    db,
                    key,
                    keyType,
                    value,
                    ttl,
                    format,
                    decode,
                })
                if (success) {
                    /** @type RedisServerState **/
                    const serverInst = this.servers[server]
                    if (serverInst != null) {
                        // const { value } = data
                        // update tree view data
                        const { newKey = 0 } = serverInst.addKeyNodes([key], true)
                        if (newKey > 0) {
                            serverInst.tidyNode(key)
                            serverInst.updateDBKeyCount(db, newKey)
                        }
                    }
                    const { value: updatedValue } = data
                    if (updatedValue != null) {
                        const tab = useTabStore()
                        tab.updateValue({ server, db, key, value: updatedValue })
                    }
                    // this.loadKeySummary({ server, db, key })
                    return {
                        success,
                        nodeKey: `${server}/db${db}#${ConnectionType.RedisValue}/${key}`,
                        updatedValue: value,
                    }
                } else {
                    return { success, msg }
                }
            } catch (e) {
                return { success: false, msg: e.message }
            }
        },

        /**
         * update hash entry
         * when field is set, newField is null, delete field
         * when field is null, newField is set, add new field
         * when both field and newField are set, and field === newField, update field
         * when both field and newField are set, and field !== newField, delete field and add newField
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {string} field
         * @param {string} [newField]
         * @param {string} [value]
         * @param {decodeTypes} [decode]
         * @param {formatTypes} [format]
         * @param {decodeTypes} [retDecode]
         * @param {formatTypes} [retFormat]
         * @param {boolean} [refresh]
         * @param {number} [index] index for retrieve affect entries quickly
         * @param {boolean} [reload]
         * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
         */
        async setHash({
            server,
            db,
            key,
            field,
            newField = '',
            value = '',
            decode = decodeTypes.NONE,
            format = formatTypes.RAW,
            retDecode,
            retFormat,
            index,
            reload,
        }) {
            try {
                const { data, success, msg } = await SetHashValue({
                    server,
                    db,
                    key,
                    field,
                    newField,
                    value,
                    decode,
                    format,
                    retDecode,
                    retFormat,
                })
                if (success) {
                    /**
                     * @type {{updated: HashEntryItem[], removed: HashEntryItem[], updated: HashEntryItem[], replaced: HashReplaceItem[]}}
                     */
                    const { updated = [], removed = [], added = [], replaced = [] } = data
                    const tab = useTabStore()
                    if (!isEmpty(removed)) {
                        const removedKeys = map(removed, 'k')
                        tab.removeValueEntries({ server, db, key, type: 'hash', entries: removedKeys })
                    }
                    if (!isEmpty(updated)) {
                        tab.updateValueEntries({ server, db, key, type: 'hash', entries: updated })
                    }
                    if (!isEmpty(added)) {
                        tab.insertValueEntries({ server, db, key, type: 'hash', entries: added })
                    }
                    if (!isEmpty(replaced)) {
                        tab.replaceValueEntries({
                            server,
                            db,
                            key,
                            type: 'hash',
                            entries: replaced,
                            index: [index],
                        })
                    }
                    if (reload === true) {
                        this.reloadKey({ server, db, key })
                    } else {
                        // reload summary only
                        this.loadKeySummary({ server, db, key })
                    }
                    return { success, updated }
                } else {
                    return { success, msg }
                }
            } catch (e) {
                return { success: false, msg: e.message }
            }
        },

        /**
         * insert or update hash field item
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {number }action 0:ignore duplicated fields 1:overwrite duplicated fields
         * @param {string[]} fieldItems field1, value1, filed2, value2...
         * @param {boolean} [reload]
         * @returns {Promise<{[msg]: string, success: boolean, [updated]: [], [added]: []}>}
         */
        async addHashField({ server, db, key, action, fieldItems, reload }) {
            try {
                const { data, success, msg } = await AddHashField(server, db, key, action, fieldItems)
                if (success) {
                    const { updated = [], added = [] } = data
                    const tab = useTabStore()
                    if (!isEmpty(updated)) {
                        tab.updateValueEntries({ server, db, key, type: 'hash', entries: updated })
                    }
                    if (!isEmpty(added)) {
                        tab.insertValueEntries({ server, db, key, type: 'hash', entries: added })
                    }
                    if (reload === true) {
                        this.reloadKey({ server, db, key })
                    } else {
                        // reload summary only
                        this.loadKeySummary({ server, db, key })
                    }
                    return { success, updated, added }
                } else {
                    return { success: false, msg }
                }
            } catch (e) {
                return { success: false, msg: e.message }
            }
        },

        /**
         * remove hash field
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {string} field
         * @param {boolean} reload
         * @returns {Promise<{[msg]: {}, success: boolean, [removed]: string[]}>}
         */
        async removeHashField({ server, db, key, field, reload }) {
            try {
                const { data, success, msg } = await SetHashValue({ server, db, key, field, newField: '' })
                if (success) {
                    const { removed = [] } = data
                    // if (!isEmpty(removed)) {
                    //     const tab = useTabStore()
                    //     tab.removeValueEntries({ server, db, key, type: 'hash', entries: removed })
                    // }
                    if (reload === true) {
                        this.reloadKey({ server, db, key })
                    } else {
                        // reload summary only
                        this.loadKeySummary({ server, db, key })
                    }
                    return { success, removed }
                } else {
                    return { success, msg }
                }
            } catch (e) {
                return { success: false, msg: e.message }
            }
        },

        /**
         * prepend item to head of list
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {string[]} values
         * @param {boolean} reload
         * @returns {Promise<{[msg]: string, success: boolean, [item]: []}>}
         */
        async prependListItem({ server, db, key, values, reload }) {
            try {
                const { data, success, msg } = await AddListItem(server, db, key, 0, values)
                if (success) {
                    const { left = [] } = data
                    if (!isEmpty(left)) {
                        const tab = useTabStore()
                        tab.insertValueEntries({
                            server: server,
                            db,
                            key,
                            type: 'list',
                            entries: left,
                            prepend: true,
                        })
                        if (reload === true) {
                            this.reloadKey({ server, db, key })
                        } else {
                            // reload summary only
                            this.loadKeySummary({ server, db, key })
                        }
                    }
                    return { success, item: left }
                } else {
                    return { success: false, msg }
                }
            } catch (e) {
                return { success: false, msg: e.message }
            }
        },

        /**
         * append item to tail of list
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {string[]} values
         * @param {boolean} [reload]
         * @returns {Promise<{[msg]: string, success: boolean, [item]: any[]}>}
         */
        async appendListItem({ server, db, key, values, reload }) {
            try {
                const { data, success, msg } = await AddListItem(server, db, key, 1, values)
                if (success) {
                    const { right = [] } = data
                    // FIXME: do not append items if not all items loaded
                    if (!isEmpty(right)) {
                        const tab = useTabStore()
                        tab.insertValueEntries({
                            server: server,
                            db,
                            key,
                            type: 'list',
                            entries: right,
                            prepend: false,
                        })
                        if (reload === true) {
                            this.reloadKey({ server, db, key })
                        } else {
                            // reload summary only
                            this.loadKeySummary({ server, db, key })
                        }
                    }
                    return { success, item: right }
                } else {
                    return { success: false, msg }
                }
            } catch (e) {
                return { success: false, msg: e.message }
            }
        },

        /**
         * update value of list item by index
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {number} index
         * @param {string|number[]} value
         * @param {decodeTypes} decode
         * @param {formatTypes} format
         * @param {decodeTypes} [retDecode]
         * @param {formatTypes} [retFormat]
         * @param {boolean} [reload]
         * @returns {Promise<{[msg]: string, success: boolean}>}
         */
        async updateListItem({
            server,
            db,
            key,
            index,
            value,
            decode = decodeTypes.NONE,
            format = formatTypes.RAW,
            retDecode,
            retFormat,
            reload,
        }) {
            try {
                const { data, success, msg } = await SetListItem({
                    server,
                    db,
                    key,
                    index,
                    value,
                    decode,
                    format,
                    retDecode,
                    retFormat,
                })
                if (success) {
                    /** @type {{replaced: ListReplaceItem[]}} **/
                    const { replaced = [], removed = [] } = data
                    const tab = useTabStore()
                    if (!isEmpty(replaced)) {
                        tab.replaceValueEntries({
                            server,
                            db,
                            key,
                            type: 'list',
                            entries: replaced,
                        })
                    }
                    if (!isEmpty(removed)) {
                        const removedIndex = map(removed, 'index')
                        tab.removeValueEntries({
                            server,
                            db,
                            key,
                            type: 'list',
                            entries: removedIndex,
                        })
                    }
                    if (reload === true) {
                        this.reloadKey({ server, db, key })
                    } else {
                        // reload summary only
                        this.loadKeySummary({ server, db, key })
                    }
                    return { success }
                } else {
                    return { success, msg }
                }
            } catch (e) {
                return { success: false, msg: e.message }
            }
        },

        /**
         * remove list item
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {number} index
         * @param {boolean} [reload]
         * @returns {Promise<{[msg]: string, success: boolean, [removed]: string[]}>}
         */
        async removeListItem({ server, db, key, index, reload }) {
            try {
                const { data, success, msg } = await SetListItem({ server, db, key, index })
                if (success) {
                    const { removed = [] } = data
                    const tab = useTabStore()
                    if (!isEmpty(removed)) {
                        const removedIndexes = map(removed, 'index')
                        tab.removeValueEntries({
                            server,
                            db,
                            key,
                            type: 'list',
                            entries: removedIndexes,
                        })
                        if (reload === true) {
                            this.reloadKey({ server, db, key })
                        } else {
                            // reload summary only
                            this.loadKeySummary({ server, db, key })
                        }
                    }
                    return { success, removed }
                } else {
                    return { success, msg }
                }
            } catch (e) {
                return { success: false, msg: e.message }
            }
        },

        /**
         * add item to set
         * @param {string} server
         * @param {number} db
         * @param {string|number} key
         * @param {string|string[]} value
         * @param {boolean} [reload]
         * @returns {Promise<{[msg]: string, success: boolean}>}
         */
        async addSetItem({ server, db, key, value, reload }) {
            try {
                if ((!value) instanceof Array) {
                    value = [value]
                }
                const { data, success, msg } = await SetSetItem(server, db, key, false, value)
                if (success) {
                    const { added } = data
                    if (!isEmpty(added)) {
                        const tab = useTabStore()
                        tab.insertValueEntries({ server, db, key, type: 'set', entries: added })
                    }
                    if (reload === true) {
                        this.reloadKey({ server, db, key })
                    } else {
                        // reload summary only
                        this.loadKeySummary({ server, db, key })
                    }
                    return { success }
                } else {
                    return { success, msg }
                }
            } catch (e) {
                return { success: false, msg: e.message }
            }
        },

        /**
         * update value of set item
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {string|number[]} value
         * @param {string|number[]} newValue
         * @param {decodeTypes} [decode]
         * @param {formatTypes} [format]
         * @param {decodeTypes} [retDecode]
         * @param {formatTypes} [retFormat]
         * @param {boolean} [reload]
         * @returns {Promise<{[msg]: string, success: boolean}>}
         */
        async updateSetItem({
            server,
            db,
            key,
            value,
            newValue,
            decode = decodeTypes.NONE,
            format = formatTypes.RAW,
            retDecode,
            retFormat,
            reload,
        }) {
            try {
                const { data, success, msg } = await UpdateSetItem({
                    server,
                    db,
                    key,
                    value,
                    newValue,
                    decode,
                    format,
                    retDecode,
                    retFormat,
                })
                if (success) {
                    const { added, removed } = data
                    const tab = useTabStore()
                    if (!isEmpty(removed)) {
                        const removedValues = map(removed, 'v')
                        tab.removeValueEntries({ server, db, key, type: 'set', entries: removedValues })
                    }
                    if (!isEmpty(added)) {
                        tab.insertValueEntries({ server, db, key, type: 'set', entries: added })
                    }
                    if (reload === true) {
                        this.reloadKey({ server, db, key })
                    } else {
                        // reload summary only
                        this.loadKeySummary({ server, db, key })
                    }
                    return { success }
                } else {
                    return { success: false, msg }
                }
            } catch (e) {
                return { success: false, msg: e.message }
            }
        },

        /**
         * remove item from set
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {string} value
         * @param {boolean} [reload]
         * @returns {Promise<{[msg]: string, success: boolean}>}
         */
        async removeSetItem({ server, db, key, value, reload }) {
            try {
                const { data, success, msg } = await SetSetItem(server, db, key, true, [value])
                if (success) {
                    const { removed } = data
                    const tab = useTabStore()
                    if (!isEmpty(removed)) {
                        const removedValues = map(removed, 'v')
                        tab.removeValueEntries({ server, db, key, type: 'set', entries: removedValues })
                    }
                    if (reload === true) {
                        this.reloadKey({ server, db, key })
                    } else {
                        // reload summary only
                        this.loadKeySummary({ server, db, key })
                    }
                    return { success }
                } else {
                    return { success, msg }
                }
            } catch (e) {
                return { success: false, msg: e.message }
            }
        },

        /**
         * add item to sorted set
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {number} action
         * @param {Object.<string, number>} vs value: score
         * @param {boolean} [reload]
         * @returns {Promise<{[msg]: string, success: boolean}>}
         */
        async addZSetItem({ server, db, key, action, vs, reload }) {
            try {
                const { data, success, msg } = await AddZSetValue(server, db, key, action, vs)
                if (success) {
                    const { added, updated } = data
                    const tab = useTabStore()
                    if (!isEmpty(added)) {
                        tab.insertValueEntries({ server, db, key, type: 'zset', entries: added })
                    }
                    if (!isEmpty(updated)) {
                        tab.updateValueEntries({ server, db, key, type: 'zset', entries: updated })
                    }
                    if (reload === true) {
                        this.reloadKey({ server, db, key })
                    } else {
                        // reload summary only
                        this.loadKeySummary({ server, db, key })
                    }
                    return { success }
                } else {
                    return { success, msg }
                }
            } catch (e) {
                return { success: false, msg: e.message }
            }
        },

        /**
         * update item of sorted set
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {string} value
         * @param {string} newValue
         * @param {number} score
         * @param {decodeTypes} decode
         * @param {formatTypes} format
         * @param {decodeTypes} [retDecode]
         * @param {formatTypes} [retFormat]
         * @param {number} [index] index for retrieve affect entries quickly
         * @param {boolean} [reload]
         * @returns {Promise<{[msg]: string, success: boolean}>}
         */
        async updateZSetItem({
            server,
            db,
            key,
            value = '',
            newValue,
            score,
            decode = decodeTypes.NONE,
            format = formatTypes.RAW,
            retDecode,
            retFormat,
            index,
            reload,
        }) {
            try {
                const { data, success, msg } = await UpdateZSetValue({
                    server,
                    db,
                    key,
                    value,
                    newValue,
                    score,
                    decode,
                    format,
                    retDecode,
                    retFormat,
                })
                if (success) {
                    const { updated = [], added = [], removed = [], replaced = [] } = data
                    const tab = useTabStore()
                    if (!isEmpty(removed)) {
                        const removedValues = map(removed, 'v')
                        tab.removeValueEntries({ server, db, key, type: 'zset', entries: removedValues })
                    }
                    if (!isEmpty(updated)) {
                        tab.updateValueEntries({ server, db, key, type: 'zset', entries: updated })
                    }
                    if (!isEmpty(added)) {
                        tab.insertValueEntries({ server, db, key, type: 'zset', entries: added })
                    }
                    if (!isEmpty(replaced)) {
                        tab.replaceValueEntries({ server, db, key, type: 'zset', entries: replaced, index: [index] })
                    }
                    if (reload === true) {
                        this.reloadKey({ server, db, key })
                    } else {
                        // reload summary only
                        this.loadKeySummary({ server, db, key })
                    }
                    return { success, updated, removed }
                } else {
                    return { success, msg }
                }
            } catch (e) {
                return { success: false, msg: e.message }
            }
        },

        /**
         * remove item from sorted set
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {string} value
         * @param {boolean} [reload]
         * @returns {Promise<{[msg]: string, success: boolean, [removed]: []}>}
         */
        async removeZSetItem({ server, db, key, value, reload }) {
            try {
                const { data, success, msg } = await UpdateZSetValue({ server, db, key, value, newValue: '', score: 0 })
                if (success) {
                    const { removed } = data
                    const tab = useTabStore()
                    if (!isEmpty(removed)) {
                        const removeValues = map(removed, 'v')
                        tab.removeValueEntries({ server, db, key, type: 'zset', entries: removeValues })
                    }
                    if (reload === true) {
                        this.reloadKey({ server, db, key })
                    } else {
                        // reload summary only
                        this.loadKeySummary({ server, db, key })
                    }
                    return { success, removed }
                } else {
                    return { success, msg }
                }
            } catch (e) {
                return { success: false, msg: e.message }
            }
        },

        /**
         * insert new stream field item
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {string} id
         * @param {string[]} values field1, value1, filed2, value2...
         * @param {boolean} [reload]
         * @returns {Promise<{[msg]: string, success: boolean}>}
         */
        async addStreamValue({ server, db, key, id, values, reload }) {
            try {
                const { data = {}, success, msg } = await AddStreamValue(server, db, key, id, values)
                if (success) {
                    const { added = [] } = data
                    if (!isEmpty(added)) {
                        const tab = useTabStore()
                        tab.insertValueEntries({
                            server,
                            db,
                            key,
                            type: 'stream',
                            entries: added,
                        })
                        if (reload === true) {
                            this.reloadKey({ server, db, key })
                        } else {
                            // reload summary only
                            this.loadKeySummary({ server, db, key })
                        }
                    }
                    return { success }
                } else {
                    return { success: false, msg }
                }
            } catch (e) {
                return { success: false, msg: e.message }
            }
        },

        /**
         * remove stream field
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {string[]|string} ids
         * @param {boolean} [reload]
         * @returns {Promise<{[msg]: {}, success: boolean}>}
         */
        async removeStreamValues({ server, db, key, ids, reload }) {
            if (typeof ids === 'string') {
                ids = [ids]
            }
            try {
                const { data = {}, success, msg } = await RemoveStreamValues(server, db, key, ids)
                if (success) {
                    const tab = useTabStore()
                    tab.removeValueEntries({ server, db, key, type: 'stream', entries: ids })
                    if (reload === true) {
                        this.reloadKey({ server, db, key })
                    } else {
                        // reload summary only
                        this.loadKeySummary({ server, db, key })
                    }
                    return { success }
                } else {
                    return { success, msg }
                }
            } catch (e) {
                return { success: false, msg: e.message }
            }
        },

        /**
         * reset key's ttl
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {number} ttl
         * @returns {Promise<boolean>}
         */
        async setTTL(server, db, key, ttl) {
            try {
                const { success, msg } = await SetKeyTTL(server, db, key, ttl)
                if (success) {
                    const tabStore = useTabStore()
                    tabStore.updateTTL({
                        server,
                        db,
                        key: nativeRedisKey(key),
                        ttl,
                    })
                }
                return success === true
            } catch (e) {
                return false
            }
        },

        async setTTLs(server, db, keys, ttl) {
            // const msgRef = $message.loading('', { duration: 0, closable: true })
            // let updated = []
            // let failCount = 0
            // let canceled = false
            const serialNo = Date.now().valueOf().toString()
            // const eventName = 'ttling:' + serialNo
            // const cancelEvent = 'ttling:stop:' + serialNo
            try {
                // let maxProgress = 0
                // EventsOn(eventName, ({ total, progress, processing }) => {
                //     // update delete progress
                //     if (progress > maxProgress) {
                //         maxProgress = progress
                //     }
                //     const k = decodeRedisKey(processing)
                //     msgRef.content = i18nGlobal.t('dialogue.delete.doing', {
                //         key: k,
                //         index: maxProgress,
                //         count: total,
                //     })
                // })
                // msgRef.onClose = () => {
                //     EventsEmit(cancelEvent)
                // }
                const { data, success, msg } = await BatchSetTTL(server, db, keys, ttl, serialNo)
                if (success) {
                    // canceled = get(data, 'canceled', false)
                    // updated = get(data, 'updated', [])
                    // failCount = get(data, 'failed', 0)
                } else {
                    $message.error(msg)
                }
            } finally {
                // msgRef.destroy()
                // EventsOff(eventName)
            }
            $message.success(i18nGlobal.t('dialogue.ttl.success'))
            // const deletedCount = size(updated)
            // if (canceled) {
            //     $message.info(i18nGlobal.t('dialogue.handle_cancel'))
            // } else if (failCount <= 0) {
            //     // no fail
            //     $message.success(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
            // } else if (failCount >= deletedCount) {
            //     // all fail
            //     $message.error(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
            // } else {
            //     // some fail
            //     $message.warning(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
            // }
        },

        /**
         * delete redis key
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {boolean} [soft] do not try to remove from redis if true, just remove from tree data
         * @returns {Promise<boolean>}
         */
        async deleteKey(server, db, key, soft) {
            try {
                let deleteCount = 1
                if (soft !== true) {
                    const { data } = await DeleteKey(server, db, key)
                    deleteCount = get(data, 'deleteCount', 0)
                }

                const k = nativeRedisKey(key)
                // update tree view data
                /** @type RedisServerState **/
                const serverInst = this.servers[server]
                if (serverInst != null) {
                    serverInst.removeKeyNode(k)
                    serverInst.tidyNode(k, true)
                    serverInst.updateDBKeyCount(db, -deleteCount)
                }

                // set tab content empty
                const tab = useTabStore()
                tab.emptyTab(server)
                tab.setSelectedKeys(server)
                tab.setCheckedKeys(server)
                return true
            } finally {
            }
            return false
        },

        /**
         * delete multiple keys
         * @param {string} server
         * @param {number} db
         * @param {string[]|number[][]} keys
         * @return {Promise<void>}
         */
        async deleteKeys(server, db, keys) {
            const msgRef = $message.loading(i18nGlobal.t('dialogue.delete.deleting'), { duration: 0, closable: true })
            let deleted = []
            let failCount = 0
            let canceled = false
            const serialNo = Date.now().valueOf().toString()
            msgRef.onClose = () => {
                EventsEmit('delete:stop:' + serialNo)
            }
            try {
                const { success, msg, data } = await DeleteKeys(server, db, keys, serialNo)
                if (success) {
                    canceled = get(data, 'canceled', false)
                    deleted = get(data, 'deleted', [])
                    failCount = get(data, 'failed', 0)
                } else {
                    $message.error(msg)
                }
            } finally {
                msgRef.destroy()
                // clear checked keys
                const tab = useTabStore()
                tab.setCheckedKeys(server)
            }
            // refresh model data
            const deletedCount = size(deleted)
            if (canceled) {
                $message.info(i18nGlobal.t('dialogue.handle_cancel'))
            } else if (failCount <= 0) {
                // no fail
                $message.success(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
            } else if (failCount >= deletedCount) {
                // all fail
                $message.error(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
            } else {
                // some fail
                $message.warning(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
            }
            // update ui
            timeout(100).then(async () => {
                /** @type RedisServerState **/
                const serverInst = this.servers[server]
                if (serverInst != null) {
                    let start = now()
                    for (let i = 0; i < deleted.length; i++) {
                        serverInst.removeKeyNode(deleted[i], false)
                        if (now() - start > 300) {
                            await timeout(100)
                            start = now()
                        }
                    }
                    serverInst.tidyNode('', true)
                    serverInst.updateDBKeyCount(db, -deletedCount)
                }
            })
        },

        /**
         * export multiple keys
         * @param {string} server
         * @param {number} db
         * @param {string[]|number[][]} keys
         * @param {string} path
         * @param {boolean} [expire]
         * @returns {Promise<void>}
         */
        async exportKeys(server, db, keys, path, expire) {
            const msgRef = $message.loading('', { duration: 0, closable: true })
            let exported = 0
            let failCount = 0
            let canceled = false
            const cancelEventFn = EventsOn('exporting:' + path, ({ total, progress, processing }) => {
                // update export progress
                msgRef.content = i18nGlobal.t('dialogue.export.exporting', {
                    // key: decodeRedisKey(processing),
                    index: progress,
                    count: total,
                })
            })
            msgRef.onClose = () => {
                EventsEmit('export:stop:' + path)
            }
            try {
                const { data, success, msg } = await ExportKey(server, db, keys, path, expire)
                if (success) {
                    canceled = get(data, 'canceled', false)
                    exported = get(data, 'exported', 0)
                    failCount = get(data, 'failed', 0)
                } else {
                    $message.error(msg)
                }
            } finally {
                msgRef.destroy()
                cancelEventFn()
            }
            if (canceled) {
                $message.info(i18nGlobal.t('dialogue.handle_cancel'))
            } else if (failCount <= 0) {
                // no fail
                $message.success(
                    i18nGlobal.t('dialogue.export.export_completed', { success: exported, fail: failCount }),
                )
            } else if (failCount >= exported) {
                // all fail
                $message.error(i18nGlobal.t('dialogue.export.export_completed', { success: exported, fail: failCount }))
            } else {
                // some fail
                $message.warning(
                    i18nGlobal.t('dialogue.export.export_completed', {
                        success: exported,
                        fail: failCount,
                    }),
                )
            }
        },

        /**
         * import multiple keys from csv file
         * @param {string} server
         * @param {number} db
         * @param {string} path
         * @param {number} conflict
         * @param {number} [ttl] <0:use previous; ==0: persist; >0: custom ttl
         * @param {boolean} [reload]
         * @return {Promise<void>}
         */
        async importKeysFromCSVFile(server, db, path, conflict, ttl, reload) {
            const msgRef = $message.loading('', { duration: 0, closable: true })
            let imported = 0
            let ignored = 0
            let canceled = false
            const cancelEventFn = EventsOn('importing:' + path, ({ imported = 0, ignored = 0 }) => {
                // update export progress
                msgRef.content = i18nGlobal.t('dialogue.import.importing', {
                    // key: decodeRedisKey(processing),
                    imported,
                    conflict: ignored,
                })
            })
            msgRef.onClose = () => {
                EventsEmit('import:stop:' + path)
            }
            try {
                const { data, success, msg } = await ImportCSV(server, db, path, conflict, ttl)
                if (success) {
                    canceled = get(data, 'canceled', false)
                    imported = get(data, 'imported', 0)
                    ignored = get(data, 'ignored', 0)
                } else {
                    $message.error(msg)
                }
            } finally {
                cancelEventFn()
                msgRef.destroy()
            }
            if (canceled) {
                $message.info(i18nGlobal.t('dialogue.handle_cancel'))
            } else {
                // finish
                $message.success(i18nGlobal.t('dialogue.import.import_completed', { success: imported, ignored }))
                if (reload) {
                    this.reloadServer(server)
                }
            }
        },

        /**
         * flush database
         * @param {string} server
         * @param {number} db
         * @param {boolean} async
         * @return {Promise<boolean>}
         */
        async flushDatabase(server, db, async) {
            try {
                const { success = false } = await FlushDB(server, db, async)

                if (success === true) {
                    /** @type RedisServerState **/
                    const serverInst = this.servers[server]
                    if (serverInst != null) {
                        // update tree view data
                        serverInst.removeKeyNode()
                    }
                    // set tab content empty
                    const tab = useTabStore()
                    tab.emptyTab(server)
                    tab.setSelectedKeys(server)
                    tab.setCheckedKeys(server)
                    tab.setExpandedKeys(server)
                    return true
                }
            } finally {
            }
            return true
        },

        /**
         * rename key
         * @param {string} server
         * @param {number} db
         * @param {string} key
         * @param {string} newKey
         * @returns {Promise<{[msg]: string, success: boolean, [nodeKey]: string}>}
         */
        async renameKey(server, db, key, newKey) {
            const { success = false, msg } = await RenameKey(server, db, key, newKey)
            if (success) {
                // delete old key and add new key struct
                /** @type RedisServerState **/
                const serverInst = this.servers[server]
                if (serverInst != null) {
                    serverInst.renameKey(key, newKey)
                }
                return { success: true, nodeKey: `${server}/db${db}#${ConnectionType.RedisValue}/${newKey}` }
            } else {
                return { success: false, msg }
            }
        },

        /**
         * get command history
         * @param {number} [pageNo]
         * @param {number} [pageSize]
         * @returns {Promise<HistoryItem[]>}
         */
        async getCmdHistory(pageNo, pageSize) {
            if (pageNo === undefined || pageSize === undefined) {
                pageNo = -1
                pageSize = -1
            }
            try {
                const { success, data = { list: [] } } = await GetCmdHistory(pageNo, pageSize)
                const { list } = data
                return list
            } catch {
                return []
            }
        },

        /**
         * clean cmd history
         * @return {Promise<boolean>}
         */
        async cleanCmdHistory() {
            try {
                const { success } = await CleanCmdHistory()
                return success === true
            } catch {
                return false
            }
        },

        /**
         * get client list info
         * @param {string} server
         * @return {Promise<{idle: number, name: string, addr: string, age: number, db: number}[]>}
         */
        async getClientList(server) {
            const { success, msg, data } = await GetClientList(server)
            if (success) {
                const { list = [] } = data
                return map(list, (item) => ({
                    addr: item['addr'],
                    name: item['name'],
                    age: item['age'] || 0,
                    idle: item['idle'] || 0,
                    db: item['db'] || 0,
                }))
            }
            return []
        },

        /**
         * get slow log list
         * @param {string} server
         * @param {number} num
         * @return {Promise<[]>}
         */
        async getSlowLog(server, num) {
            try {
                const { success, data = { list: [] } } = await GetSlowLogs(server, num)
                const { list } = data
                return list
            } catch {
                return []
            }
        },

        /**
         * get key filter pattern and filter type
         * @param {string} server
         * @returns {{match: string, type: string, exact: boolean}}
         */
        getKeyFilter(server) {
            let serverInst = this.servers[server]
            if (serverInst == null) {
                serverInst = new RedisServerState({
                    name: server,
                    separator: this.getSeparator(name),
                })
            }
            return serverInst.getFilter()
        },

        /**
         *
         * @param {string} server
         * @param {string} [pattern]
         * @param {string} [type]
         * @param {boolean} [exact]
         */
        setKeyFilter(server, { pattern, type, exact = false }) {
            const serverInst = this.servers[server]
            if (serverInst != null) {
                serverInst.setFilter({ pattern, type, exact })
            }
        },

        /**
         *
         * @param {string} server
         * @param {string} key
         * @param {number} db
         * @param {string} format
         * @param {string} decode
         */
        setSelectedFormat(server, key, db, format, decode) {
            const serverInst = this.servers[server]
            if (serverInst == null) {
                return
            }
            serverInst.addDecodeHistory(key, db, format, decode)
        },
    },
})

export default useBrowserStore
