const { Controller } = require('libflitter')
const FakeRequest = require('../../../FakeRequest')
const { ObjectId } = require('mongodb')

class OfflineController extends Controller {
    static get services() {
        return [...super.services, 'models', 'controllers', 'app']
    }

    async do_prefetch(req, res, next) {
        const PageModel = this.models.get('api:Page')
        const PageNode = this.models.get('api:Node')
        const Codium = this.models.get('api:Codium')
        const Database = this.models.get('api:db:Database')
        const ColumnDef = this.models.get('api:db:ColumnDef')
        const DBEntry = this.models.get('api:db:DBEntry')
        const FileGroup = this.models.get('api:FileGroup')
        const File = this.models.get('upload::File')

        const pages = (await PageModel.visible_by_user(req.user)).filter(x => x.Active)
        const page_uuids = pages.map(x => x.UUID)

        const pageNodes = await PageNode.find({ PageId: { $in: page_uuids }})

        const codiums = await Codium.find({ PageId: { $in: page_uuids }, Active: true })

        const databases = await Database.find({ PageId: { $in: page_uuids }, Active: true })
        const database_uuids = databases.map(x => x.UUID)

        const databaseColumns = await ColumnDef.find({ DatabaseId: { $in: database_uuids }})

        const databaseEntries = await DBEntry.find({ DatabaseId: { $in: database_uuids }})

        const fileGroups = (await FileGroup.find({ PageId: { $in: page_uuids }}))
        for ( const grp of fileGroups ) {
            grp.files = await File.find({_id: {$in: grp.FileIds.map(x => ObjectId(x))}})
        }

        return res.api({
            pages,
            pageNodes,
            codiums,
            databases,
            databaseColumns,
            databaseEntries,
            fileGroups,
        })
    }

    async do_sync(req, res, next) {
        // TODO account for modify date to not overwrite more recent data!!

        const OfflineDataSync = this.models.get('api:OfflineDataSync')
        const record = await OfflineDataSync.from_request(req)
        const return_maps = {}

        // pages
        if ( Array.isArray(record.pages) ) {
            return_maps.pages = await this.do_sync_pages(req, record.pages)
        }

        // pageNodes
        if ( Array.isArray(record.pageNodes) ) {
            return_maps.pageNodes = await this.do_sync_page_nodes(req, record.pageNodes, record.pages)
        }

        // codiums
        if ( Array.isArray(record.codiums) ) {
            return_maps.codiums = await this.do_sync_codiums(req, record.codiums)
        }

        // databases
        if ( Array.isArray(record.databases) ) {
            return_maps.databases = await this.do_sync_databases(req, record.databases)
        }

        // databaseColumns
        if ( Array.isArray(record.databaseColumns) ) {
            return_maps.databaseColumns = await this.do_sync_database_columns(req, record.databases, record.databaseColumns)
        }

        // databaseEntries
        if ( Array.isArray(record.databaseEntries) ) {
            return_maps.databaseEntries = await this.do_sync_database_entries(req, record.databases, record.databaseEntries)
        }

        // fileGroups
        if ( Array.isArray(record.fileGroups) ) {
            return_maps.fileGroups = await this.do_sync_file_groups(req, record.fileGroups)
        }

        return res.api(return_maps)
    }

    async do_sync_file_groups(req, file_recs) {
        const FileController = this.controllers.get('api:v1:File')
        const FileGroup = this.models.get('api:FileGroup')

        const uuid_mapping = {}

        for ( const rec of file_recs ) {
            const existing_rec = await FileGroup.findOne({ UUID: rec.UUID })

            const fake_req = this.app.di().make(FakeRequest)
            await fake_req.inflate(req)
            fake_req.params = { PageId: rec.PageId, NodeId: rec.NodeId, FilesId: rec.UUID }
            fake_req.body = rec

            if ( existing_rec && rec.deleted ) {
                await FileController.delete_group(fake_req, fake_req.response)
                uuid_mapping[rec.UUID] = false
            } else if ( !existing_rec ) {
                await FileController.create_config(fake_req, fake_req.response)
                uuid_mapping[rec.UUID] = fake_req.response?._api_data?.UUID
            }

            // Currently, there's no reason to update a file group, since they have no internal metadata
        }

        return uuid_mapping
    }

    async do_sync_database_entries(req, database_recs, database_row_recs) {
        const FormDatabaseController = this.controllers.get('api:v1:FormDatabase')

        const uuid_mapping = {}

        for ( const rec of database_recs ) {
            const entries = database_row_recs.filter(x => !x.deleted && x.DatabaseId === rec.UUID)

            const fake_req = this.app.di().make(FakeRequest)
            await fake_req.inflate(req)
            fake_req.params = { PageId: rec.PageId, NodeId: rec.NodeId, DatabaseId: rec.UUID }
            fake_req.body = entries.map(x => JSON.parse(x.RowDataJSON || '{}'))

            await FormDatabaseController.set_data(fake_req, fake_req.response)

            entries.forEach((rec, i) => {
                uuid_mapping[rec.UUID] = fake_req.response?._api_data?.[i]?.UUID
            })
        }

        return uuid_mapping
    }

    async do_sync_database_columns(req, database_recs, database_col_recs) {
        const FormDatabaseController = this.controllers.get('api:v1:FormDatabase')

        const uuid_mapping = {}

        for ( const rec of database_recs ) {
            const col_recs = database_col_recs.filter(x => rec.ColumnIds.includes(x.UUID) && !x.deleted)

            const fake_req = this.app.di().make(FakeRequest)
            await fake_req.inflate(req)
            fake_req.params = { PageId: rec.PageId, NodeId: rec.NodeId, DatabaseId: rec.UUID }
            fake_req.body = { columns: col_recs }

            await FormDatabaseController.set_columns(fake_req, fake_req.response)

            col_recs.forEach((rec, i) => {
                uuid_mapping[rec.UUID] = fake_req.response?._api_data?.[i]?.UUID
            })
        }

        return uuid_mapping
    }

    async do_sync_databases(req, database_recs) {
        const FormDatabaseController = this.controllers.get('api:v1:FormDatabase')
        const Database = this.models.get('api:db:Database')

        const uuid_mapping = {}

        for ( const rec of database_recs ) {
            const existing_db = await Database.findOne({ UUID: rec.UUID })

            const fake_req = this.app.di().make(FakeRequest)
            await fake_req.inflate(req)
            fake_req.params = { PageId: rec.PageId, NodeId: rec.NodeId, DatabaseId: rec.UUID }
            fake_req.body = rec

            if ( !existing_db && !rec.deleted ) {
                // this was created on the client side
                await FormDatabaseController.create_new(fake_req, fake_req.response)
                uuid_mapping[rec.UUID] = fake_req.response?._api_data?.UUID
            } else {
                if ( rec.deleted ) {
                    // the database was deleted
                    await FormDatabaseController.drop_database(fake_req, fake_req.response)
                    uuid_mapping[rec.UUID] = false
                } else {
                    // the database was updated
                    await FormDatabaseController.set_name(fake_req, fake_req.response)
                    uuid_mapping[rec.UUID] = fake_req.response?._api_data?.UUID
                }
            }
        }

        return uuid_mapping
    }

    async do_sync_codiums(req, codium_recs) {
        const FormCodeController = this.controllers.get('api:v1:FormCode')
        const Codium = this.models.get('api:Codium')

        const uuid_mapping = {}

        for ( const rec of codium_recs ) {
            const existing_code = await Codium.findOne({ UUID: rec.UUID })

            const fake_req = this.app.di().make(FakeRequest)
            await fake_req.inflate(req)
            fake_req.params = { PageId: rec.PageId, NodeId: rec.NodeId, CodiumId: rec.UUID }
            fake_req.body = rec

            if ( !existing_code && !rec.deleted ) {
                // This was created on the client side
                await FormCodeController.create_new(fake_req, fake_req.response)
                uuid_mapping[rec.UUID] = fake_req.response?._api_data?.UUID
            } else {
                if ( rec.deleted ) {
                    // The code was deleted
                    await FormCodeController.drop_code(fake_req, fake_req.response)
                    uuid_mapping[rec.UUID] = false
                } else {
                    // The code was updated
                    await FormCodeController.set_values(fake_req, fake_req.response)
                    uuid_mapping[rec.UUID] = fake_req.response?._api_data?.UUID
                }
            }
        }

        return uuid_mapping
    }

    async do_sync_page_nodes(req, page_node_recs, page_recs) {
        const PageController = this.controllers.get('api:v1:Page')
        const PageModel = this.models.get('api:Page')
        const NodeModel = this.models.get('api:Node')

        const page_id_x_page = {}
        for ( const page of page_recs ) {
            page_id_x_page[page.UUID] = page
        }

        const uuid_mapping = {}

        for ( const rec of page_node_recs ) {
            rec.Value = rec.ValueJSON ? JSON.parse(rec.ValueJSON) : {}

            const existing_node = await NodeModel.findOne({ UUID: rec.UUID })
            const offline_page = page_id_x_page[rec.PageId]
            let online_page = await PageModel.findOne({ UUID: rec.PageId })

            if ( existing_node && rec.deleted ) {
                // node that exists on the server was deleted
                if ( online_page ) {
                    // if it existed in the online page, delete it
                    online_page.NodeIds = online_page.NodeIds.filter(x => x !== rec.UUID)
                    await online_page.version_save('Updated from offline sync', req.user.id)
                }

                await existing_node.delete()
                uuid_mapping[rec.UUID] = false
            } else if ( existing_node ) {
                // if the node exists, we assume it's already in a page structure
                // update the server-side record if the user can access it
                if ( await online_page.is_accessible_by(req.user, 'edit') ) {
                    existing_node.Type = rec.Type
                    existing_node.Value = rec.Value
                    existing_node.UpdatedAt = new Date(rec.UpdatedAt)
                    existing_node.UpdateUserId = req.user.id

                    await existing_node.version_save('Updated from offline sync', req.user)
                    uuid_mapping[rec.UUID] = existing_node.UUID
                }
            } else if ( !existing_node && online_page && !rec.deleted ) {
                // the node was created offline
                // first, save the node to the page
                const fake_req = this.app.di().make(FakeRequest)
                await fake_req.inflate(req)
                fake_req.params = { PageId: online_page.UUID }
                fake_req.body = { nodeData: {...rec} }

                await PageController.save_node_to_page(fake_req, fake_req.response)
                const new_uuid = uuid_mapping[rec.UUID] = fake_req.response?._api_data?.UUID
                online_page = await PageModel.findOne({ UUID: rec.PageId })  // to refresh the model's data

                // now, try to place the node in the correct spot
                // if first in offline page, make first in online page
                if ( Array.isArray(offline_page?.NodeIds) && offline_page.NodeIds[0] === rec.UUID ) {
                    online_page.NodeIds = [new_uuid, ...online_page.NodeIds.filter(x => x !== new_uuid && x !== rec.UUID)]
                } else if ( Array.isArray(offline_page?.NodeIds) && offline_page.NodeIds.includes(rec.UUID) ) {
                    if ( offline_page.NodeIds.slice(-1)[0] !== rec.UUID ) {
                        // We're not first, and we're not last, so try to place in the correct spot
                        const index = offline_page.NodeIds.findIndex(x => x.UUID === rec.UUID)
                        let predecessor = undefined
                        let index_diff = 1

                        while ( !predecessor && (index - index_diff) >= 0 ) {
                            const maybe_predecessor = offline_page.NodeIds[index - index_diff]
                            if ( online_page.NodeIds.includes(maybe_predecessor) ) {
                                predecessor = maybe_predecessor
                            } else {
                                index_diff += 1
                            }
                        }

                        if ( predecessor ) {
                            // We found the predecessor to insert the child in the existing node
                            const newNodeIds = []
                            online_page.NodeIds.forEach(uuid => {
                                newNodeIds.push(uuid)
                                if ( uuid === predecessor ) {
                                    newNodeIds.push(new_uuid)
                                }
                            })

                            online_page.NodeIds = newNodeIds
                        }
                    }
                }

                await online_page.version_save('Updated page from online save', req.user.id)
            }

            // assuming the pages were created first, we should never have a case
            // where we have !existing_node && !online_page
        }

        return uuid_mapping
    }

    async do_sync_pages(req, page_recs) {
        // TODO order pages by create date to prevent child-before-parent errors
        const PageController = this.controllers.get('api:v1:Page')
        const PageModel = this.models.get('api:Page')

        const uuid_mapping = {}

        for ( const rec of page_recs ) {
            const UUID = rec.UUID
            if ( !UUID ) continue;

            const existing_page = await PageModel.findOne({ UUID })
            const fake_req = this.app.di().make(FakeRequest)
            await fake_req.inflate(req)
            fake_req.body = {...rec}

            if ( !existing_page && !rec.deleted ) {
                // create the new page
                if ( parseInt(rec.ParentId) === 0 ) {
                    // Create a new top-level page
                    fake_req.body.omit_starter = true
                    fake_req.body.name = rec.Name
                    await PageController.create_top_level(fake_req, fake_req.response)
                } else {
                    // Create a new child page
                    fake_req.body.omit_starter = true
                    fake_req.body.name = rec.Name
                    fake_req.body.parentId = rec.ParentId
                    await PageController.create_child(fake_req, fake_req.response)
                }
            } else if ( existing_page ) {
                fake_req.params = { PageId: rec.UUID }

                if ( rec.deleted ) {
                    // The page was deleted on the client
                    await PageController.delete_page(fake_req, fake_req.response)
                } else {
                    // update an existing page
                    await PageController.save_page(fake_req, fake_req.response)
                }
            }

            uuid_mapping[rec.UUID] = fake_req.response?._api_data?.UUID || false
        }

        return uuid_mapping
    }
}

module.exports = exports = OfflineController
