import {
    DataFile,
    PeerInfo,
    QuestionDb,
    Subject,
} from '../../../types/basicTypes'
import { backupData, writeData } from '../../../utils/actions'
import { publicDir } from '../../../utils/files'
import logger from '../../../utils/logger'
import {
    countOfQdb,
    countOfQdbs,
    createQuestion,
    getAvailableQdbIndexes,
    removeCacheFromQuestion,
} from '../../../utils/qdbUtils'
import utils from '../../../utils/utils'
import { WorkerResult } from '../../../worker/worker'
import { msgAllWorker, queueWork } from '../../../worker/workerPool'
import {
    SyncDataResult,
    SyncResponseBase,
    SyncResult,
    peerToString,
    updateLastSync,
    updatePeersFile,
} from './p2putils'

interface MergeResult {
    newData: Subject[]
    newSubjects: Subject[]
    localQdbIndex: number
    e: Error
}

interface SyncQuestionsProps {
    questionData: (SyncDataResult['questions'] & { peer: PeerInfo })[]
    syncStart: number
    getQuestionDbs: () => QuestionDb[]
    setQuestionDbs: (newVal: QuestionDb[]) => void
    selfInfo: PeerInfo
    dbsFile: string
}

// ---------------------------------------------------------------------------------------------
// Getting
// ---------------------------------------------------------------------------------------------
export function getNewQuestionsSince(
    subjects: Subject[],
    date: number
): Subject[] {
    return subjects
        .map((subject) => {
            return {
                ...subject,
                Questions: subject.Questions.filter((question) => {
                    return (question.data.date || 0) >= date
                }).map((question) => removeCacheFromQuestion(question)),
            }
        })
        .filter((subject) => subject.Questions.length !== 0)
}

export function getQuestions(
    questionsSince: number,
    getQuestionDbs: () => QuestionDb[]
): SyncResponseBase & {
    questionDbs: QuestionDb[]
    count: { qdbs: number; subjects: number; questions: number }
} {
    const questionDbsWithNewQuestions = Number.isNaN(questionsSince)
        ? getQuestionDbs()
        : getQuestionDbs()
              .map((qdb) => {
                  return {
                      ...qdb,
                      data: getNewQuestionsSince(qdb.data, questionsSince),
                  }
              })
              .filter((qdb) => {
                  const { questionCount: questionCount } = countOfQdb(qdb)
                  return questionCount > 0
              })

    const { subjCount: subjects, questionCount: questions } = countOfQdbs(
        questionDbsWithNewQuestions
    )

    return {
        success: true,
        questionDbs: questionDbsWithNewQuestions,
        count: {
            qdbs: questionDbsWithNewQuestions.length,
            subjects: subjects,
            questions: questions,
        },
    }
}

// ---------------------------------------------------------------------------------------------
// Syncing utils
// ---------------------------------------------------------------------------------------------

function setupQuestionsForMerge(qdb: QuestionDb, peer: PeerInfo) {
    return {
        ...qdb,
        data: qdb.data.map((subj) => {
            return {
                ...subj,
                Questions: subj.Questions.map((q) => {
                    const initializedQuestion = q.cache ? q : createQuestion(q)
                    initializedQuestion.data.source = peerToString(peer)
                    return initializedQuestion
                }),
            }
        }),
    }
}

async function getMergeResults(
    remoteQuestionDbs: QuestionDb[],
    getQuestionDbs: () => QuestionDb[]
) {
    const mergeJobs: Promise<any>[] = []
    const rawNewQuestionDbs: QuestionDb[] = []
    remoteQuestionDbs.forEach((remoteQdb) => {
        const localQdb = getQuestionDbs().find(
            (lqdb) => lqdb.name === remoteQdb.name
        )

        if (!localQdb) {
            rawNewQuestionDbs.push(remoteQdb)
        } else {
            mergeJobs.push(
                queueWork({
                    type: 'merge',
                    data: {
                        localQdbIndex: localQdb.index,
                        remoteQdb: remoteQdb,
                    },
                })
            )
        }
    })

    const mergeResults: MergeResult[] = await Promise.all(mergeJobs)

    return {
        mergeResults: mergeResults,
        rawNewQuestionDbs: rawNewQuestionDbs,
    }
}

function updateQdbForLocalUse(
    qdb: QuestionDb[],
    getQuestionDbs: () => QuestionDb[]
) {
    const availableIndexes = getAvailableQdbIndexes(
        getQuestionDbs(),
        qdb.length
    )
    return qdb.map((qdb, i) => {
        return {
            ...qdb,
            index: availableIndexes[i],
            path: `${publicDir}questionDbs/${qdb.name}.json`,
        }
    })
}

export function mergeSubjects(
    subjectsToMergeTo: Subject[],
    subjectsToMerge: Subject[],
    newSubjects: Subject[]
): Subject[] {
    return [
        ...subjectsToMergeTo.map((subj) => {
            const newSubjs = subjectsToMerge.filter(
                (subjRes) => subjRes.Name === subj.Name
            )

            if (newSubjs) {
                const newQuestions = newSubjs.flatMap((subj) => {
                    return subj.Questions
                })

                return {
                    ...subj,
                    Questions: [...subj.Questions, ...newQuestions],
                }
            } else {
                return subj
            }
        }),
        ...newSubjects,
    ]
}

export function mergeQdbs(
    qdbToMergeTo: QuestionDb[],
    mergeResults: MergeResult[]
): { mergedQuestionDbs: QuestionDb[]; changedQdbIndexes: number[] } {
    const changedQdbIndexes: number[] = []
    const mergedQuestionDbs = qdbToMergeTo.map((qdb) => {
        const qdbMergeResult = mergeResults.find(
            (mergeRes) => mergeRes.localQdbIndex === qdb.index
        )
        if (
            qdbMergeResult &&
            (qdbMergeResult.newData.length > 0 ||
                qdbMergeResult.newSubjects.length > 0)
        ) {
            const mergedQdb = {
                ...qdb,
                data: mergeSubjects(
                    qdb.data,
                    qdbMergeResult.newData,
                    qdbMergeResult.newSubjects
                ),
            }
            changedQdbIndexes.push(qdb.index)
            return mergedQdb
        } else {
            // unchanged
            return qdb
        }
    })
    return {
        mergedQuestionDbs: mergedQuestionDbs,
        changedQdbIndexes: changedQdbIndexes,
    }
}

function writeNewData(
    newQuestionDbs: QuestionDb[],
    changedQuestionDbs: QuestionDb[],
    dbsFilePath: string
) {
    const qdbsToWrite = [...changedQuestionDbs, ...newQuestionDbs]
    const existingQdbs = utils.ReadJSON<DataFile[]>(dbsFilePath)

    const qdbDataToWrite = qdbsToWrite
        .reduce((acc, qdb) => {
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            const { data, index, ...qdbData } = qdb
            const existingQdbData = acc.find((data) => {
                return data.name === qdbData.name
            })
            if (!existingQdbData) {
                return [...acc, qdbData]
            } else {
                return acc
            }
        }, existingQdbs)
        .map((qdb) => {
            if (qdb.path.includes(publicDir)) {
                return {
                    ...qdb,
                    path: qdb.path.replace(publicDir, ''),
                }
            } else {
                return qdb
            }
        })

    utils.WriteFile(JSON.stringify(qdbDataToWrite, null, 2), dbsFilePath)
    qdbsToWrite.forEach((qdb) => {
        try {
            writeData(qdb.data, qdb.path)
        } catch (e) {
            logger.Log(`Error writing ${qdb.name} qdb to file!`, 'redbg')
            console.error(e)
        }
    })
}

async function sendNewDataToWorkers(
    mergeResults: MergeResult[],
    newQuestionDbs: QuestionDb[]
) {
    // FIXME: this might be slow, maybe make a new type of message for workers?
    const updatePromises: Promise<WorkerResult[]>[] = []
    let newQuestionCount = 0
    let newSubjectCount = 0
    let newQuestionDbCount = 0

    mergeResults.forEach((mergeRes) => {
        if (mergeRes.e) {
            logger.Log(`There was an error processing the merge!`, 'redbg')
            console.error(mergeRes.e)
            return
        }

        mergeRes.newData.forEach((subjectWithNewData) => {
            newQuestionCount += subjectWithNewData.Questions.length
            updatePromises.push(
                msgAllWorker({
                    type: 'newQuestions',
                    data: {
                        subjName: subjectWithNewData.Name,
                        qdbIndex: mergeRes.localQdbIndex,
                        newQuestions: subjectWithNewData.Questions,
                    },
                })
            )
        })

        newSubjectCount += mergeRes.newSubjects.length
        mergeRes.newSubjects.forEach((newSubject) => {
            newQuestionCount += newSubject.Questions.length
            updatePromises.push(
                msgAllWorker({
                    type: 'newQuestions',
                    data: {
                        subjName: newSubject.Name,
                        qdbIndex: mergeRes.localQdbIndex,
                        newQuestions: newSubject.Questions,
                    },
                })
            )
        })
    })
    newQuestionDbCount += newQuestionDbs.length
    newQuestionDbs.forEach((newQdb) => {
        const { subjCount: sc, questionCount: qc } = countOfQdb(newQdb)
        newSubjectCount += sc
        newQuestionCount += qc
        msgAllWorker({
            data: newQdb,
            type: 'newdb',
        })
    })

    await Promise.all(updatePromises)

    return {
        newQuestionDbCount: newQuestionDbCount,
        newSubjectCount: newSubjectCount,
        newQuestionCount: newQuestionCount,
    }
}

// ---------------------------------------------------------------------------------------------
// Syncing
// ---------------------------------------------------------------------------------------------

export async function syncQuestions({
    questionData,
    syncStart,
    getQuestionDbs,
    setQuestionDbs,
    selfInfo,
    dbsFile,
}: SyncQuestionsProps): Promise<SyncResult> {
    logger.Log('Syncing questions...')
    const recievedDataCounts: (number | string)[][] = []
    // all results statistics
    const resultsCount: {
        [key: string]: {
            newQuestionDbs?: number
            newSubjects?: number
            newQuestions?: number
        }
    } = {}

    const resultDataWithoutEmptyDbs: (SyncDataResult['questions'] & {
        peer: PeerInfo
    })[] = []
    questionData.forEach((res) => {
        const qdbCount = res.questionDbs.length
        const { subjCount, questionCount } = countOfQdbs(res.questionDbs)

        recievedDataCounts.push([
            peerToString(res.peer),
            qdbCount,
            subjCount,
            questionCount,
        ])

        if (questionCount > 0) {
            resultDataWithoutEmptyDbs.push(res)
        } else {
            updatePeersFile(res.peer, {
                lastQuestionsSync: syncStart,
            })
        }
    })

    const resultData = resultDataWithoutEmptyDbs.map((res) => {
        return {
            ...res,
            questionDbs: res.questionDbs.map((qdb) => {
                return setupQuestionsForMerge(qdb, res.peer)
            }),
        }
    })

    const hasNewData = resultData.length > 0
    if (!hasNewData) {
        logger.Log(
            `No peers returned any new questions. Question sync successfully finished!`,
            'green'
        )
        updateLastSync(selfInfo, syncStart)
        return {
            msg: 'No peers returned any new questions',
        }
    }

    logger.Log(`\tRecieved data from peers:`)
    logger.logTable(
        [['', 'QDBs', 'Subjs', 'Questions'], ...recievedDataCounts],
        {
            colWidth: [20],
            rowPrefix: '\t',
        }
    )

    // -------------------------------------------------------------------------------------------------------
    // backup
    // -------------------------------------------------------------------------------------------------------
    const { subjCount: oldSubjCount, questionCount: oldQuestionCount } =
        countOfQdbs(getQuestionDbs())
    const oldQuestionDbCount = getQuestionDbs().length
    backupData(getQuestionDbs())
    logger.Log('\tOld data backed up!')

    // -------------------------------------------------------------------------------------------------------
    // adding questions to db
    // -------------------------------------------------------------------------------------------------------
    for (let i = 0; i < resultData.length; i++) {
        const { questionDbs: remoteQuestionDbs, peer } = resultData[i]
        logger.Log(
            `\tProcessing result from "${logger.C('blue')}${peerToString(
                peer
            )}${logger.C()}" (${logger.C('green')}${
                resultData.length
            }${logger.C()}/${logger.C('green')}${i + 1}${logger.C()})`
        )
        // FIXME: if remoteQuestionDbs contain multiple dbs with the same name, then the merging
        // process could get wonky. Ideally it should not contain, but we will see

        const { rawNewQuestionDbs, mergeResults } = await getMergeResults(
            remoteQuestionDbs,
            getQuestionDbs
        )

        const newQuestionDbs = updateQdbForLocalUse(
            rawNewQuestionDbs,
            getQuestionDbs
        )

        const { mergedQuestionDbs, changedQdbIndexes } = mergeQdbs(
            getQuestionDbs(),
            mergeResults
        )
        // setting new index & path
        writeNewData(
            newQuestionDbs,
            getQuestionDbs().filter((qdb) => {
                return changedQdbIndexes.includes(qdb.index)
            }),
            dbsFile
        )

        setQuestionDbs([...mergedQuestionDbs, ...newQuestionDbs])

        const { newQuestionDbCount, newSubjectCount, newQuestionCount } =
            await sendNewDataToWorkers(mergeResults, newQuestionDbs)

        resultsCount[peerToString(peer)] = {
            ...(resultsCount[peerToString(peer)] || { newUsers: 0 }),
            newQuestionDbs: newQuestionDbCount,
            newSubjects: newSubjectCount,
            newQuestions: newQuestionCount,
        }
        // Processing result data is successfull
        updatePeersFile(peer, {
            lastQuestionsSync: syncStart,
        })
    }

    // -------------------------------------------------------------------------------------------------------
    updateLastSync(selfInfo, syncStart)

    const newQdb = getQuestionDbs()
    const { subjCount: newSubjCount, questionCount: newQuestionCount } =
        countOfQdbs(newQdb)
    const newQuestionDbCount = newQdb.length

    const resultsTable = Object.entries(resultsCount).map(([key, value]) => {
        return [
            key.length > 14 ? key.substring(0, 14) + '...' : key,
            value.newQuestionDbs,
            value.newSubjects,
            value.newQuestions,
        ]
    })

    const sumNewCount = (key: string) => {
        return Object.values(resultsCount).reduce(
            (acc, val) => acc + val[key],
            0
        )
    }

    const totalNewQuestions = sumNewCount('newQuestions')
    const totalNewSubjects = sumNewCount('newSubjects')
    const totalNewQdbs = sumNewCount('newQuestionDbs')

    logger.logTable(
        [
            ['', 'QDBs', 'Subjs', 'Questions'],
            ['Old', oldQuestionDbCount, oldSubjCount, oldQuestionCount],
            ...resultsTable,
            ['Added total', totalNewQdbs, totalNewSubjects, totalNewQuestions],
            ['Final', newQuestionDbCount, newSubjCount, newQuestionCount],
        ],
        { colWidth: [20], rowPrefix: '\t' }
    )

    logger.Log(`Successfully synced questions!`, 'green')

    return {
        old: {
            questionDbs: oldQuestionDbCount,
            subjects: oldSubjCount,
            questions: oldQuestionCount,
        },
        added: {
            questionDbs: totalNewQdbs,
            subjects: totalNewSubjects,
            questions: totalNewQuestions,
        },
        final: {
            questionDbs: newQuestionDbCount,
            subjects: newSubjCount,
            questions: newQuestionCount,
        },
    }
}
