const util = require('node:util')

const objectIds = new WeakMap()
const objectStore = new Map()
const MAX_OBJECTS = 2000

let nextId = 1
const makeId = () => `o_${nextId++}`

const trackObject = value => {
    if (!value || (typeof value !== 'object' && typeof value !== 'function')) {
        return null
    }
    if (objectIds.has(value)) {
        return objectIds.get(value)
    }
    const id = makeId()
    objectIds.set(value, id)
    objectStore.set(id, value)
    if (objectStore.size > MAX_OBJECTS) {
        const firstKey = objectStore.keys().next().value
        objectStore.delete(firstKey)
    }
    return id
}

const getObject = id => objectStore.get(id)

const previewValue = value => {
    if (value === null) return 'null'
    if (typeof value === 'undefined') return 'undefined'
    if (typeof value === 'string') return `"${value}"`
    if (typeof value === 'number') return Number.isNaN(value) ? 'NaN' : String(value)
    if (typeof value === 'boolean') return String(value)
    if (typeof value === 'bigint') return `${value.toString()}n`
    if (typeof value === 'symbol') return value.toString()
    if (value instanceof RegExp) return value.toString()
    if (value instanceof Date) return value.toString()
    if (Array.isArray(value)) return `Array(${value.length})`
    if (value instanceof Map) return `Map(${value.size})`
    if (value instanceof Set) return `Set(${value.size})`
    if (value instanceof Promise) {
        const inspected = util.inspect(value)
        const match = inspected.match(/^Promise\s*(\{.*\})$/)
        return match ? `Promise ${match[1]}` : 'Promise {<pending>}'
    }
    if (typeof value === 'function') return 'ƒ'
    if (typeof value === 'object') return '{…}'
    return String(value)
}

const buildParam = (value, level = 0, keyName) => {
    const type = getType(value)
    const origin = previewValue(value)
    const isObject = value && (typeof value === 'object' || typeof value === 'function')
    const objectId = isObject ? trackObject(value) : null
    const hasChildren = isExpandable(value)
    const funcName = typeof value === 'function' ? (value.name || 'anonymous') : null
    return {
        type,
        origin,
        level,
        ...(keyName !== undefined && { keyName }),
        ...(objectId && { objectId }),
        ...(hasChildren && { hasChildren: true }),
        ...(hasChildren && { flod: true })
        ,...(funcName && { funcName })
    }
}

const isExpandable = value => {
    if (!value) return false
    if (value instanceof Date) return false
    if (value instanceof RegExp) return false
    if (typeof value === 'function') return true
    if (value instanceof Promise) return true
    if (value instanceof Map) return true
    if (value instanceof Set) return true
    if (Array.isArray(value)) return true
    if (typeof value === 'object') return true
    return false
}

const getType = value => {
    if (typeof value === 'undefined') return 'Undefined'
    if (value === null) return 'Null'
    if (value instanceof Date) return 'Date'
    if (value instanceof RegExp) return 'RegExp'
    if (value instanceof Map) return 'Map'
    if (value instanceof Set) return 'Set'
    if (value instanceof Promise) return 'Promise'
    if (typeof value === 'bigint') return 'BigInt'
    if (typeof value === 'symbol') return 'Symbol'
    if (Array.isArray(value)) return 'Array'
    if (typeof value === 'function') return 'Function'
    if (typeof value === 'string') return 'String'
    if (typeof value === 'number') return Number.isNaN(value) ? 'NaN' : 'Number'
    if (typeof value === 'boolean') return 'Boolean'
    if (value && value.constructor && value.constructor.name && value.constructor.name !== 'Object') {
        return value.constructor.name
    }
    return 'Object'
}

const anasysArgument = (data, originLog) => {
    const dataArr = []
    Object.keys(data).forEach(key => {
        dataArr[key] = data[key]
    })
    return dataArr.map(param => buildParam(param))
}

const expandObject = (objectId, level = 0) => {
    const target = getObject(objectId)
    if (!target) {
        return { error: 'Object not found' }
    }

    const type = getType(target)
    if (type === 'Promise') {
        const inspected = util.inspect(target)
        const match = inspected.match(/^Promise\s*(\{.*\})$/)
        const stateText = match ? match[1] : '{<pending>}'
        const state = stateText.includes('<pending>') ? 'pending'
            : stateText.includes('<rejected>') ? 'rejected'
                : 'fulfilled'
        return {
            type: 'Promise',
            objectId,
            children: [
                buildParam(state, level + 1, '[[PromiseState]]'),
                buildParam(undefined, level + 1, '[[PromiseResult]]')
            ]
        }
    }

    if (type === 'Map') {
        const entries = Array.from(target.entries()).map(([mapKey, mapValue], index) => {
            const entryPreview = `{${previewValue(mapKey)} => ${previewValue(mapValue)}}`
            return {
                type: 'MapEntry',
                origin: entryPreview,
                level: level + 2,
                keyName: index,
                hasChildren: true,
                flod: true,
                source: {
                    key: buildParam(mapKey, level + 3, 'key'),
                    value: buildParam(mapValue, level + 3, 'value')
                }
            }
        })
        return {
            type: 'Map',
            objectId,
            children: [
                {
                    type: 'Entries',
                    origin: '',
                    level: level + 1,
                    keyName: '[Entries]',
                    hasChildren: true,
                    flod: true,
                    source: entries
                },
                buildParam(target.size, level + 1, 'size')
            ]
        }
    }

    if (type === 'Set') {
        const items = Array.from(target.values()).map((item, index) => {
            return buildParam(item, level + 1, index)
        })
        return {
            type: 'Set',
            objectId,
            children: items
        }
    }

    if (type === 'Array') {
        const items = target.map((item, index) => buildParam(item, level + 1, index))
        return {
            type: 'Array',
            objectId,
            children: items
        }
    }

    if (typeof target === 'function') {
        return {
            type: 'Function',
            objectId,
            children: []
        }
    }

    const descriptors = Object.getOwnPropertyDescriptors(target)
    const props = Object.keys(descriptors).map(key => {
        const desc = descriptors[key]
        if (desc.get || desc.set) {
            return buildParam('[Getter]', level + 1, key)
        }
        return buildParam(desc.value, level + 1, key)
    })
    return {
        type: 'Object',
        objectId,
        children: props
    }
}

module.exports = {
    anasysArgument,
    expandObject
}
