const ObjectId = require('mongodb').ObjectID
const getHash = require('pbkdf2-password')()

const { getDB } = require('../../utils/mongo')

const USERS_COLLECTION = 'users'
const LISTS_COLLECTION = 'lists'
const CATEGORY_COLLECTION = 'default_categories'
const USER_CATEGROY_COLLECTION = 'user_categories'
const ITEM_COLLECTION = 'items'
const fakeUserId = 'fakeUserId'

let db = null

const connect = async () => {
    db = await getDB('basket')
}

const init = async () => {
    await connect()
    const categoriesCollection = db.collection(CATEGORY_COLLECTION)
    const findData = await categoriesCollection.find({
    }).toArray()
    if (findData.length === 0) {
        await categoriesCollection.insertMany([
            {
                name: 'Продукты',
                color: '#08AE0F',
            },
            {
                name: 'Одежда',
                color: '#9D79B9',
            },
            {
                name: 'Бытовая химия',
                color: '#B11F1F',
            },
            {
                name: 'Лекарства',
                color: '#3414F5',
            },
        ])
    }
}

init()

const _idToId = (data) => {
    const { _id, ...rest } = data

    return {
        id: _id,
        ...rest,
    }
}

const _idToIdArray = (data) => {
    const _idToIdMap = data.map((item) => _idToId(item))

    return _idToIdMap
}

const getResponse = (error, data, success = true) => {
    if (error) {
        return {
            success: false,
            error,
        }
    }

    return {
        success,
        data,
    }
}

const signUp = async ({ email, login, password }) => {
    if (db === null) throw new Error('no db connection')

    try {
        const usersCollection = db.collection(USERS_COLLECTION)
        const userData = await usersCollection.findOne({
            $or: [{
                login,
            }, {
                email,
            }],
        })

        if (userData?.login === login) {
            throw new Error('Логин занят')
        }
        if (userData?.email === email) {
            throw new Error('Email занят')
        }

        getHash({ password }, async (err, pass, salt, hash) => {
            if (err) throw new Error(err)
            // eslint-disable-next-line max-len
            const { insertedCount } = await usersCollection.insertOne({ email, login, pwd: hash, salt })
            if (!insertedCount) throw new Error('insert error')
        })

        return {}
    } catch (e) {
        throw new Error(e)
    }
}

const getUser = async ({ email }) => {
    if (db === null) throw new Error('no db connection')

    try {
        const usersCollection = db.collection(USERS_COLLECTION)
        const userData = await usersCollection.findOne(
            {
                email,
            },
        )
        if (userData) return userData
        throw new Error('Неправильный email или пароль')
    } catch (e) {
        throw new Error(e)
    }
}

const addList = async ({ userId = fakeUserId, ...data }) => {
    if (db === null) throw new Error('no db connection')

    try {
        const listsCollection = db.collection(LISTS_COLLECTION)
        const insertData = await listsCollection.insertOne({
            userId,
            timeStamp: Date.now(),
            ...data,
        })

        const { insertedCount, ops } = insertData
        if (insertedCount) { return _idToId(ops[0]) }
        throw new Error('insert error')
    } catch (e) {
        throw new Error(e)
    }
}

const getLists = async ({ userId = fakeUserId }) => {
    if (db === null) throw new Error('no db connection')

    try {
        const listsCollection = db.collection(LISTS_COLLECTION)
        const itemsCollection = db.collection(ITEM_COLLECTION)
        let newLists = []

        const data = await listsCollection.find({
            userId,
        }).toArray()

        await Promise.all(data.map(async (element) => {
            const total = await itemsCollection.countDocuments({
                parentId: element._id,
            })
            const purchased = await itemsCollection.countDocuments({
                parentId: element._id,
                bought: true,
            })

            newLists.push({
                ...element, total, purchased,
            })
        }))

        newLists.sort((a, b) => (b.timeStamp - a.timeStamp))

        return _idToIdArray(newLists)
    } catch (e) {
        throw new Error(e)
    }
}

/* добавил логику рекурсивного удаления дочерних документов */
const deleteDoc = async ({ id, tag = false }) => {
    if (db === null) throw new Error('no db connection')

    try {
        const listsCollection = db.collection(LISTS_COLLECTION)
        const itemsCollection = db.collection(ITEM_COLLECTION)

        const findData = await itemsCollection.find({
            parentId: new ObjectId(id),
        }).toArray()

        findData.forEach(async (element) => {
            await deleteDoc({
                id: element._id,
                tag: true,
            })
        })

        let delData = null
        if (tag) {
            delData = await itemsCollection.deleteOne({
                _id: new ObjectId(id),
            })
        } else {
            delData = await listsCollection.deleteOne({
                _id: new ObjectId(id),
            })
        }

        const { deletedCount } = delData
        if (deletedCount) {
            return {
            }
        } throw new Error('no data to delete')
    } catch (e) {
        throw new Error(e)
    }
}

const renameList = async ({ id, listName }) => {
    if (db === null) throw new Error('no db connection')

    try {
        const listsCollection = db.collection(LISTS_COLLECTION)
        const data = await listsCollection.updateOne({
            _id: new ObjectId(id),
        },
        {
            $set: {
                listName,
            },
        })

        const { matchedCount } = data
        if (matchedCount) {
            const findData = await listsCollection.findOne({
                _id: new ObjectId(id),
            })
            return _idToId(findData)
        } throw new Error('no data to rename')
    } catch (e) {
        throw new Error(e)
    }
}

const duplicateList = async ({ id, parentId = null }) => {
    if (db === null) throw new Error('no db connection')

    try {
        const listsCollection = db.collection(LISTS_COLLECTION)
        const itemsCollection = db.collection(ITEM_COLLECTION)

        let addListData = null
        let newId = null

        if (parentId) {
            const findData = await itemsCollection.findOne(
                {
                    _id: new ObjectId(id),
                },
            )

            const { _id, ...item } = findData
            item.parentId = parentId
            const insertData = await itemsCollection.insertOne({
                ...item,
            })
            const { insertedCount } = insertData
            if (!insertedCount) throw new Error('insert new item error')
        } else {
            const findData = await listsCollection.findOne(
                {
                    _id: new ObjectId(id),
                },
            )

            const { _id, timeStamp, ...item } = findData
            item.listName = `(КОПИЯ) ${item.listName}`
            addListData = await addList({
                ...item,
            })
            newId = addListData.id
        }

        const childData = await itemsCollection.find({
            parentId: new ObjectId(id),
        }).toArray()

        childData.forEach(async (element) => {
            await duplicateList({
                id: element._id, parentId: newId,
            })
        })

        if (addListData) return _idToId(addListData)
    } catch (e) {
        throw new Error(e)
    }
}

const getCategory = async ({ userId }) => {
    if (db === null) throw new Error('no db connection')

    try {
        const categoriesCollection = db.collection(CATEGORY_COLLECTION)
        const defaultCategories = await categoriesCollection.find({
        }).toArray()
        const defaultCategoriesData = _idToIdArray(defaultCategories).map((dc) => ({
            ...dc, userId,
        }))

        const userCollection = db.collection(USER_CATEGROY_COLLECTION)
        const userCategoriesFilter = {}

        if (userId) {
            userCategoriesFilter.userId = userId
        }
        const userFindData = await userCollection.find(userCategoriesFilter).toArray()

        return [...defaultCategoriesData, ..._idToIdArray(userFindData)]
    } catch (e) {
        throw new Error(e)
    }
}

const postCategory = async ({ userId = fakeUserId, ...categoryData }) => {
    if (db === null) throw new Error('no db connection')

    try {
        const userCollection = db.collection(USER_CATEGROY_COLLECTION)
        const insertData = await userCollection.insertOne({
            userId, ...categoryData,
        })

        // const {insertedCount, ops} = insertData
        // if (insertedCount)
        //     _idToId(ops[0])
        // else
        //     throw new Error('insert error')
        const userFindData = await userCollection.find({
            userId,
        }).toArray()
        return _idToIdArray(userFindData)
    } catch (e) {
        throw new Error(e)
    }
}

const getShoppingList = async ({ userId = fakeUserId, id }) => {
    if (db === null) throw new Error('no db connection')

    try {
        const listsCollection = db.collection(ITEM_COLLECTION)
        const itemsList = await listsCollection.find({
            parentId: new ObjectId(id),
        }).toArray()
        const categoryList = await getCategory({ })
        const coloredItemsList = itemsList.map((item) => ({
            ...item,
            // eslint-disable-next-line max-len
            color: categoryList.find((category) => String(category.id) === String(item.categoryId))?.color,
        }))

        return _idToIdArray(coloredItemsList)
    } catch (e) {
        throw new Error(e)
    }
}

const addListItem = async ({ userId = fakeUserId, listId, categoryId, text }) => {
    if (db === null) throw new Error('no db connection')

    try {
        const dataToInsert = {
            parentId: new ObjectId(listId),
            categoryId: new ObjectId(categoryId),
            text,
            count: 1,
            bought: false,
            createdBy: userId,
            createdDt: Date.now(),
            modifiedBy: userId,
            modifiedDt: Date.now(),
        }
        const itemCollection = db.collection(ITEM_COLLECTION)
        await itemCollection.insertOne(dataToInsert)

        return _idToId(dataToInsert)
    } catch (e) {
        throw new Error(e)
    }
}

const boughtItem = async ({ userId = fakeUserId, itemId, bought }) => {
    if (db === null) throw new Error('no db connection')
    try {
        const itemCollection = db.collection(ITEM_COLLECTION)
        const chengedData = await itemCollection.findOneAndUpdate({
            _id: new ObjectId(itemId),
        },
        [{
            $set: {
                bought: { $eq: [false, '$bought'] },
                modifiedBy: userId,
                modifiedDt: Date.now(),
            },
        }])
        return _idToId(chengedData)
    } catch (e) {
        throw new Error(e)
    }
}

const incCountItem = async ({ userId = fakeUserId, itemId, count }) => {
    if (db === null) throw new Error('no db connection')

    try {
        const itemCollection = db.collection(ITEM_COLLECTION)
        const chengedData = await itemCollection.findOneAndUpdate({
            _id: new ObjectId(itemId),
        },
        {
            $inc: {
                count,
            },
        },
        {
            $set: {
                modifiedBy: userId,
                modifiedDt: Date.now(),
            },
        })
        const chengeData = await itemCollection.findOneAndUpdate({
            _id: new ObjectId(itemId),
            count: {
                $lt: 1,
            },
        },
        {
            $set: {
                count: 1,
                modifiedBy: userId,
                modifiedDt: Date.now(),
            },
        })
        return _idToId(chengedData || chengeData)
    } catch (e) {
        throw new Error(e)
    }
}

const deleteItem = async ({ itemId }) => {
    if (db === null) throw new Error('no db connection')

    try {
        const itemCollection = db.collection(ITEM_COLLECTION)
        const findItemData = await itemCollection.find({
            _id: new ObjectId(itemId),
        })

        findItemData.forEach((item) => {
            deleteItem({
                id: item._id,
            })
        })

        const deleteItemData = await itemCollection.deleteOne({
            _id: new ObjectId(itemId),
        })

        const { deletedButton } = deleteItemData
        if (deletedButton) {
            return {
            }
        }
    } catch (e) {
        throw new Error(e)
    }
}

const requiredFields = (fields) => (req, res, next) => {
    // eslint-disable-next-line no-restricted-syntax
    for (const fieldName of fields) {
        if (!req.body[fieldName]) {
            throw new Error(`Параметр ${fieldName} не установлен`)
        }
    }

    next()
}

module.exports = {
    getResponse,
    addList,
    getLists,
    deleteDoc,
    renameList,
    duplicateList,
    getCategory,
    postCategory,
    getShoppingList,
    addListItem,
    boughtItem,
    deleteItem,
    incCountItem,
    signUp,
    getUser,
    _idToId,
    requiredFields,
}