/* ----------------------------------------------------------------------------

 Question Server
 GitLab: <https://gitlab.com/MrFry/mrfrys-node-server>

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program. If not, see <https://www.gnu.org/licenses/>.

 ------------------------------------------------------------------------- */

import { Response } from 'express'

import logger from '../../../utils/logger'
import {
    Request,
    SubmoduleData,
    Submodule,
    PeerInfo,
    Subject,
    QuestionDb,
    User,
    DataFile,
} from '../../../types/basicTypes'
import utils from '../../../utils/utils'
import { backupData, writeData } from '../../../utils/actions'
import { WorkerResult } from '../../../utils/classes'
import dbtools from '../../../utils/dbtools'
import {
    createKeyPair,
    decrypt,
    encrypt,
    isKeypairValid,
} from '../../../utils/encryption'
import {
    doALongTask,
    msgAllWorker,
    setPendingJobsAlertCount,
} from '../../../utils/workerPool'
import {
    countOfQdb,
    countOfQdbs,
    createQuestion,
    getAvailableQdbIndexes,
    removeCacheFromQuestion,
} from '../../../utils/qdbUtils'
import {
    isJsonValidAndLogError,
    PeersInfoSchema,
    SelfInfoSchema,
} from '../../../types/typeSchemas'
import { paths } from '../../../utils/files'
import { GetResult, get, post } from '../../../utils/networkUtils'

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

interface RemotePeerInfo {
    selfInfo: PeerInfo
    myPeers: PeerInfo[]
    serverRevision?: string
    scriptRevision?: string
    qminingPageRevision?: string
    dataEditorRevision?: string
    serverBuildTime?: number
    qminingPageBuildTime?: number
    dataEditorBuildTime?: number
    scriptVersion?: string
    userCount?: number
    qdbInfo?: {
        questionDbCount: number
        subjectCount: number
        questionCount: number
    }
}

interface SyncDataRes {
    questionDbs?: QuestionDb[]
    remoteInfo?: RemotePeerInfo
    encryptedUsers?: string
    count?: {
        qdbs: number
        subjects: number
        questions: number
    }
}

function updateThirdPartyPeers(
    newVal: Omit<PeerInfo, 'publicKey' | 'name' | 'contact'>[]
) {
    const prevVal = utils.ReadJSON<PeerInfo[]>(paths.thirdPartyPeersFile)
    const dataToWrite = newVal.reduce((acc, peer) => {
        const isIncluded = acc.find((x) => {
            return peerToString(x) === peerToString(peer)
        })
        if (!isIncluded) {
            return [...acc, peer]
        }
        return acc
    }, prevVal)

    utils.WriteFile(
        JSON.stringify(dataToWrite, null, 2),
        paths.thirdPartyPeersFile
    )
}

function peerToString(peer: { host: string; port: string | number }) {
    return `${peer.host}:${peer.port}`
}

function isPeerSameAs(peer1: PeerInfo, peer2: PeerInfo) {
    return peer1.host === peer2.host && peer1.port === peer2.port
}

export function getNewDataSince(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 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) {
            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,
    }
}

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,
    }
}

function writeNewData(
    newQuestionDbs: QuestionDb[],
    changedQuestionDbs: QuestionDb[],
    dbsFilePath: string,
    publicDir: string
) {
    const qdbsToWrite = [...changedQuestionDbs, ...newQuestionDbs]
    const qdbsFile: DataFile[] = qdbsToWrite.map((qdb) => {
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        const { data, index, ...restOfQdb } = qdb
        return restOfQdb
    })

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

function updateLastSync(selfInfo: PeerInfo, newDate: number) {
    utils.WriteFile(
        JSON.stringify({ ...selfInfo, lastSync: newDate }, null, 2),
        paths.selfInfoFile
    )
}

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 authAndGetNewData({
    peer,
    selfInfo,
    lastSyncWithPeer,
    lastSync,
}: {
    peer: PeerInfo
    selfInfo: PeerInfo
    lastSyncWithPeer: number
    lastSync: number
}): Promise<GetResult<SyncDataRes & { peer: PeerInfo }>> {
    const { data, error, cookies } = await post<{
        result: string
        msg: string
    }>({
        hostname: peer.host,
        path: '/api/login',
        port: peer.port,
        bodyObject: { pw: peer.pw },
        http: peer.http,
    })

    if (error || !data || data.result !== 'success') {
        return {
            error: data ? new Error(data.msg) : error,
            data: {
                peer: peer,
            },
        }
    }

    const getRes = await get<SyncDataRes>(
        {
            headers: {
                cookie: cookies.join(),
            },
            host: peer.host,
            port: peer.port,
            path: `/api/getnewdatasince?host=${encodeURIComponent(
                peerToString(selfInfo)
            )}${lastSync ? `&since=${lastSyncWithPeer}` : ''}`,
        },
        peer.http
    )

    return { ...getRes, data: { ...getRes.data, peer: peer } }
}

function setup(data: SubmoduleData): Submodule {
    const {
        app,
        userDB,
        publicdirs,
        moduleSpecificData: { setQuestionDbs, getQuestionDbs, dbsFile },
    } = data

    const publicDir = publicdirs[0]
    let syncInProgress = false

    // ---------------------------------------------------------------------------------------
    // SETUP
    // ---------------------------------------------------------------------------------------

    if (!utils.FileExists(paths.peersFile)) {
        logger.Log(
            `Warning: peers file was missing, so it was created`,
            'yellowbg'
        )
        utils.CreatePath(paths.peersPath)
        utils.WriteFile('[]', paths.peersFile)
    }

    if (!utils.FileExists(paths.selfInfoFile)) {
        const msg = `Self info file for p2p does not exist! (${paths.selfInfoFile}) P2P functionality will not be loaded`
        logger.Log(msg, 'redbg')
        return {}
    }

    let publicKey: string
    let privateKey: string

    if (
        !utils.FileExists(paths.keyFile + '.priv') ||
        !utils.FileExists(paths.keyFile + '.pub')
    ) {
        createKeyPair().then(({ publicKey: pubk, privateKey: privk }) => {
            // at first start there won't be a keypair available until this finishes
            utils.WriteFile(pubk, paths.keyFile + '.pub')
            utils.WriteFile(privk, paths.keyFile + '.priv')

            publicKey = pubk
            privateKey = privk
        })
        logger.Log(
            'There were no public / private keys for p2p functionality, created new ones',
            'yellowbg'
        )
    } else {
        publicKey = utils.ReadFile(paths.keyFile + '.pub')
        privateKey = utils.ReadFile(paths.keyFile + '.priv')
        // checking only here, because if it got generated in the other branch then it must be good
        if (!isKeypairValid(publicKey, privateKey)) {
            logger.Log('Loaded keypair is not valid!', 'redbg')
        }
    }

    let peers: PeerInfo[] = utils.ReadJSON(paths.peersFile)
    let selfInfo: PeerInfo = utils.ReadJSON(paths.selfInfoFile)

    // TODO: this should be checked by files!
    if (!isJsonValidAndLogError(peers, PeersInfoSchema, paths.peersFile)) {
        throw new Error('Invalid peers file')
    }
    if (!isJsonValidAndLogError(selfInfo, SelfInfoSchema, paths.selfInfoFile)) {
        throw new Error('Invalid peers file')
    }
    // self info file is not required to have the publicKey, as it is always added on init
    selfInfo.publicKey = publicKey

    const filesToWatch = [
        {
            fname: paths.peersFile,
            logMsg: 'Peers file updated',
            action: () => {
                try {
                    peers = utils.ReadJSON(paths.peersFile)
                } catch (e) {
                    logger.Log(
                        `Peers file contents are invalid! Check if syntax is correct for ${paths.peersFile}`,
                        'redbg'
                    )
                }
            },
        },
        {
            fname: paths.selfInfoFile,
            logMsg: 'P2P self info file changed',
            action: () => {
                try {
                    selfInfo = utils.ReadJSON(paths.selfInfoFile)
                    selfInfo.publicKey = publicKey
                } catch (e) {
                    logger.Log(
                        `Self info file contents are invalid! Check if syntax is correct for ${paths.selfInfoFile}`,
                        'redbg'
                    )
                }
            },
        },
    ]

    filesToWatch.forEach((ftw) => {
        if (utils.FileExists(ftw.fname)) {
            utils.WatchFile(ftw.fname, () => {
                logger.Log(ftw.logMsg)
                ftw.action()
            })
            ftw.action()
        } else {
            logger.Log(`File ${ftw.fname} does not exists to watch!`, 'redbg')
        }
    })

    if (peers.length === 0) {
        logger.Log(
            `Warning: peers file is empty. You probably want to fill it`,
            'yellowbg'
        )
    }

    // ---------------------------------------------------------------------------------------
    // FUNCTIONS
    // ---------------------------------------------------------------------------------------

    function getSelfInfo(includeVerboseInfo?: boolean) {
        const result: RemotePeerInfo = {
            selfInfo: selfInfo,
            myPeers: peers,
        }

        if (includeVerboseInfo) {
            result.serverRevision = utils.getGitRevision(__dirname)
            result.scriptRevision = utils.getGitRevision(
                paths.moodleTestUserscriptDir
            )
            result.qminingPageRevision = utils.getGitRevision(
                paths.qminingPageDir
            )
            result.dataEditorRevision = utils.getGitRevision(
                paths.dataEditorPageDir
            )
            result.qminingPageBuildTime = utils
                .statFile(paths.qminingIndexPath)
                ?.mtime.getTime()
            result.serverBuildTime = utils
                .statFile(paths.serverPath)
                ?.mtime.getTime()
            result.dataEditorBuildTime = utils
                .statFile(paths.dataEditorIndexPath)
                ?.mtime.getTime()
            result.scriptVersion = utils.getScriptVersion()
            result.userCount = dbtools.TableInfo(userDB, 'users').dataCount

            const questionDbCount = getQuestionDbs().length
            const { subjCount, questionCount } = countOfQdbs(getQuestionDbs())
            result.qdbInfo = {
                questionDbCount: questionDbCount,
                subjectCount: subjCount,
                questionCount: questionCount,
            }
        }

        return result
    }

    function getNewUsersSince(since?: number) {
        const users = dbtools.runStatement(
            userDB,
            `SELECT *
            FROM users
            WHERE created >= ${since};`
        )
        return users
    }

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

    async function getMergeResults(remoteQuestionDbs: 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(
                    doALongTask({
                        type: 'merge',
                        data: {
                            localQdbIndex: localQdb.index,
                            remoteQdb: remoteQdb,
                        },
                    })
                )
            }
        })

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

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

    async function syncData() {
        if (peers.length === 0) {
            logger.Log(
                `There are no peers specified in ${paths.peersFile}, aborting sync`,
                'yellowbg'
            )
            return {
                msg: 'No peers specified, aborting',
            }
        }
        // FIXME: this might be blocking the main thread, but not sure how much
        logger.Log(
            `\tStarting data sync, getting new data from ${logger.C('green')}${
                peers.length
            }${logger.C()} peers`
        )

        const lastSync = selfInfo.lastSync
        logger.Log(
            `\tLast sync date: ${logger.C('blue')}${new Date(
                lastSync
            ).toLocaleString()}${logger.C()}`
        )
        const syncStart = new Date().getTime()
        const lastSyncInfos = peers.map((peer) => {
            return [
                peerToString(peer),
                new Date(peer.lastSync).toLocaleString(),
            ]
        })
        logger.Log(`\tLast sync with peers:`)
        logger.logTable([['', 'Date'], ...lastSyncInfos], {
            colWidth: [15],
            rowPrefix: '\t',
        })

        const requests = peers.map((peer) => {
            const lastSyncWithPeer = peer.lastSync || 0

            return authAndGetNewData({
                peer: peer,
                selfInfo: selfInfo,
                lastSyncWithPeer: lastSyncWithPeer,
                lastSync: lastSync,
            })
        })

        const allResults = await Promise.all(requests)

        // -------------------------------------------------------------------------------------------------------
        // filtering, transforming, and counting data
        // -------------------------------------------------------------------------------------------------------
        allResults.forEach((res) => {
            if (res.error) {
                logger.Log(
                    `\tError syncing with ${peerToString(res.data.peer)}: ${
                        res.error.message
                    }`,
                    'red'
                )
            }
        })
        const resultDataWithoutErrors = allResults
            .filter((res) => !res.error)
            .map((res) => res.data)

        if (resultDataWithoutErrors.length === 0) {
            logger.Log(
                `No peers returned data without error, aborting sync`,
                'redbg'
            )
            return {
                msg: 'No peers returned data without error, aborting sync',
            }
        }

        const recievedDataCounts: (number | string)[][] = []
        const resultDataWithoutEmptyDbs = resultDataWithoutErrors.filter(
            (res) => {
                const qdbCount = res.questionDbs.length
                const { subjCount, questionCount } = countOfQdbs(
                    res.questionDbs
                )

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

                return questionCount > 0
            }
        )

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

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

        // -------------------------------------------------------------------------------------------------------
        // third party peers handling
        // -------------------------------------------------------------------------------------------------------
        const peersHosts = [...peers.map((peer) => peer.host), selfInfo.host]
        const thirdPartyPeers = resultData
            .map((res) => res.remoteInfo)
            .flatMap((x) => {
                return x.myPeers.filter(
                    (recievedPeer) => !peersHosts.includes(recievedPeer.host)
                )
            })
        if (thirdPartyPeers.length > 0) {
            updateThirdPartyPeers(thirdPartyPeers)
            logger.Log(
                `\tPeers reported ${logger.C('green')}${
                    thirdPartyPeers.length
                }${logger.C()} third party peer(s) not connected to this server. See ${logger.C(
                    'blue'
                )}${paths.thirdPartyPeersFile}${logger.C()} for details`
            )
        }

        // all results statistics
        const resultsCount: {
            [key: string]: {
                newUsers?: number
                newQuestionDbs?: number
                newSubjects?: number
                newQuestions?: number
            }
        } = {}

        // -------------------------------------------------------------------------------------------------------
        // new users handlin
        // -------------------------------------------------------------------------------------------------------
        const oldUserCount = dbtools.SelectAll(userDB, 'users').length
        try {
            resultData.forEach((res) => {
                if (res.encryptedUsers) {
                    const decryptedUsers: User[] = JSON.parse(
                        decrypt(privateKey, res.encryptedUsers)
                    )
                    decryptedUsers.forEach((remoteUser) => {
                        // eslint-disable-next-line @typescript-eslint/no-unused-vars
                        const { id, ...remoteUserWithoutId } = remoteUser
                        const localUser = dbtools.Select(userDB, 'users', {
                            pw: remoteUser.pw,
                        })
                        if (localUser.length === 0) {
                            // FIXME: users will not have consistend id across servers. This may be
                            // harmless, will see
                            dbtools.Insert(userDB, 'users', {
                                ...(remoteUserWithoutId as Omit<User, 'id'>),
                                sourceHost: peerToString(res.peer),
                            })
                        }
                    })
                    resultsCount[peerToString(res.peer)] = {
                        newUsers: decryptedUsers.length,
                    }
                }
            })
        } catch (e) {
            logger.Log(
                '\tError while trying to sync users: ' + e.message,
                'redbg'
            )
            console.error(e)
        }
        const newUserCount = dbtools.SelectAll(userDB, 'users').length

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

        // -------------------------------------------------------------------------------------------------------
        // 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
            )

            const newQuestionDbs = updateQdbForLocalUse(rawNewQuestionDbs)

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

            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
            const updatedPeersFile = peers.map((x) => {
                if (isPeerSameAs(peer, x)) {
                    return {
                        ...x,
                        lastSync: syncStart,
                    }
                } else {
                    return x
                }
            })

            utils.WriteFile(
                JSON.stringify(updatedPeersFile, null, 2),
                paths.peersFile
            )
        }

        // -------------------------------------------------------------------------------------------------------
        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.newUsers,
                    value.newQuestionDbs,
                    value.newSubjects,
                    value.newQuestions,
                ]
            }
        )

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

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

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

        logger.Log(
            `Question DB-s written! Sync successfully finished!`,
            'green'
        )

        return {
            old: {
                oldUserCount: oldUserCount,
                oldQuestionDbCount: oldQuestionDbCount,
                oldSubjCount: oldSubjCount,
                oldQuestionCount: oldQuestionCount,
            },
            added: {
                totalNewQdbs: totalNewQdbs,
                totalNewSubjects: totalNewSubjects,
                totalNewQuestions: totalNewQuestions,
            },
            final: {
                newUserCount: newUserCount,
                newQuestionDbCount: newQuestionDbCount,
                newSubjCount: newSubjCount,
                newQuestionCount: newQuestionCount,
            },
        }
    }

    // ---------------------------------------------------------------------------------------
    // APP SETUP
    // ---------------------------------------------------------------------------------------
    app.get('/selfInfo', (_req: Request, res: Response<PeerInfo>) => {
        res.json(selfInfo)
    })

    app.get('/p2pinfo', (_req: Request, res: Response<RemotePeerInfo>) => {
        res.json(getSelfInfo(true))
    })

    // TODO: get all user files

    app.get('/getnewdatasince', (req: Request, res: Response<SyncDataRes>) => {
        // FIXME: hash question db to see if different?
        // it could help in determining if it should be checked for new data, but it would only save
        // a getNewDataSince() call per question db
        logger.LogReq(req)
        const since = +req.query.since
        const remoteHost = req.query.host

        const questionDbsWithNewQuestions = Number.isNaN(since)
            ? getQuestionDbs()
            : getQuestionDbs()
                  .map((qdb) => {
                      return {
                          ...qdb,
                          data: getNewDataSince(qdb.data, since),
                      }
                  })
                  .filter((qdb) => {
                      const { questionCount: questionCount } = countOfQdb(qdb)
                      return questionCount > 0
                  })

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

        const result: SyncDataRes = {
            questionDbs: questionDbsWithNewQuestions,
            count: {
                qdbs: questionDbsWithNewQuestions.length,
                subjects: subjects,
                questions: questions,
            },
            remoteInfo: getSelfInfo(),
        }

        let hostToLog = req.hostname
        if (remoteHost) {
            const remotePeerInfo = peers.find((peer) => {
                return peerToString(peer) === remoteHost
            })
            if (remotePeerInfo) {
                hostToLog = peerToString(remotePeerInfo)
                const remotePublicKey = remotePeerInfo?.publicKey
                if (remotePublicKey) {
                    // FIXME: sign data?
                    const newUsers = getNewUsersSince(since)
                    result.encryptedUsers = encrypt(
                        remotePublicKey,
                        JSON.stringify(newUsers)
                    )
                    logger.Log(
                        `Sending new users to "${remoteHost}" (encrypted)`,
                        'green'
                    )
                } else if (remotePeerInfo) {
                    logger.Log(
                        `Warning: "${hostToLog}" has no public key saved!`,
                        'yellowbg'
                    )
                }
            } else {
                logger.Log(
                    'Couldn\'t find remote peer info based on remoteHost: "' +
                        remoteHost +
                        '". This could mean that the host uses this server as peer, but this server does not ' +
                        'use it as a peer.',
                    'yellowbg'
                )
                if (remoteHost.includes(':')) {
                    const [host, port] = remoteHost.split(':')
                    updateThirdPartyPeers([
                        {
                            host: host,
                            port: +port,
                        },
                    ])
                    logger.Log('Host info written to host info file')
                }
            }
        }

        const dateToLog = Number.isNaN(since)
            ? 'all time'
            : new Date(since).toLocaleString()

        logger.Log(
            `Sending new data to ${logger.C(
                'blue'
            )}${hostToLog}${logger.C()} since ${logger.C(
                'blue'
            )}${dateToLog}${logger.C()} `
        )

        res.json(result)
    })

    app.get('/syncp2pdata', (req: Request, res: Response) => {
        logger.LogReq(req)
        const user = req.session.user
        if (user.id !== 1) {
            res.json({
                status: 'error',
                msg: 'only user 1 can call this EP',
            })
            return
        }

        // FIXME: /syncResult EP if this EP times out, but we still need the result
        if (syncInProgress) {
            res.json({
                error: 'A sync is already in progress!',
            })
            return
        }

        syncInProgress = true
        setPendingJobsAlertCount(5000)
        syncData()
            .then((syncResult) => {
                res.json({
                    msg: 'sync successfull',
                    ...syncResult,
                })
                setPendingJobsAlertCount()
                syncInProgress = false
            })
            .catch((e) => {
                console.error(e)
                res.json({
                    error: e,
                    msg: e.message,
                })
                setPendingJobsAlertCount()
                syncInProgress = false
            })
    })

    logger.Log(
        'P2P functionality set up. Peers (' +
            peers.length +
            '): ' +
            peers.map((peer) => peerToString(peer)).join(', '),
        'blue'
    )

    return {}
}

export default {
    setup: setup,
}
