const SocketController = require('flitter-socket/Controller')
const uuid = require('uuid').v4

class NormEditorGroup {
    constructor(page, node) {
        this.id = `${page.UUID}-${node.UUID}`
        this.page = page
        this.node = node
        this.connections = {}
        this.connection_id_x_session = {}
    }

    join(connection_manager) {
        this.connections[connection_manager.id] = connection_manager
        this.connection_id_x_session[connection_manager.id] = {}

        connection_manager.on_close().then(() => {
            delete this.connection_id_x_session[connection_manager.id]
            delete this.connections[connection_manager.id]
            this.blast_editor_group_users()
        })
    }

    has(connection_manager) {
        return !!this.connections[connection_manager.id]
    }

    unique_users() {
        const users = {}
        const conns = Object.values(this.connections)

        for ( const conn of conns ) {
            const session = this.session(conn)
            const user = conn.request.user
            if ( user && !users[user.uuid] ) {
                if ( !session.user_color ) {
                    session.user_color = '#' + Math.floor(Math.random() * 16777215).toString(16)
                }

                users[user.uuid] = { user, color: session.user_color }
            }
        }

        return Object.values(users)
    }

    session(conn) {
        return this.connection_id_x_session[conn.id]
    }

    blast_editor_group_users() {
        const conns = Object.values(this.connections)
        const unique_users = this.unique_users()

        for ( const conn of conns ) {
            const users = unique_users.filter(data => data.user.uuid !== conn.request.user.uuid)
                .map(data => {
                    const { user, color } = data
                    return {
                        uuid: user.uuid,
                        uid: user.uid,
                        display: user.uid,
                        color,
                    }
                })

            conn._request('setEditorGroupUsers', { users })
        }
    }

    blast_selections() {
        const conns = Object.values(this.connections)

        for ( const conn of conns ) {
            const selections = conns.filter(maybeConn => conn.id !== maybeConn.id)
                .map(otherConn => {
                    const session = this.session(otherConn)
                    const user_data = {
                        uuid: otherConn.request.user.uuid,
                        uid: otherConn.request.user.uid,
                        display: otherConn.request.user.uid,
                    }

                    if ( session.user_color ) {
                        user_data.color = session.user_color
                    }

                    return {...(this.session(otherConn).selection || {}), user_data}
                })
                .filter(Boolean)

            conn._request('setEditorGroupSelections', { selections })
        }
    }

    blast_content_mutation(mutation, exclude_conn_id) {
        const conns = Object.values(this.connections)

        for ( const conn of conns ) {
            if ( conn.id === exclude_conn_id ) continue;
            conn._request('applyRemoteContentMutation', { mutation })
        }
    }

    set_member_selection(conn, selection) {
        const session = this.session(conn)
        if ( session ) {
            session.selection = selection
            this.blast_selections()
        }
    }
}

class NormEditorController extends SocketController {
    editor_groups = {}

    static get services() {
        return [...super.services, 'output', 'models']
    }

    ping(transaction, socket) {
        this.output.info('Got norm editor socket ping.')

        transaction.status(200)
            .message('Pong!')
            .send(transaction.incoming)
    }

    _get_editor_group_or_fail(transaction) {
        if ( !transaction.incoming.editor_group_id ) {
            transaction.status(400)
                .message('Missing editor_group_id.')
                .send()

            return
        }

        const editor_group = this.editor_groups[transaction.incoming.editor_group_id]
        if ( !editor_group || !editor_group.has(transaction.cm) ) {
            transaction.status(400)
                .message('Invalid editor_group_id.')
                .send()

            return
        }

        return editor_group
    }

    set_member_selection(transaction, socket) {
        const editor_group = this._get_editor_group_or_fail(transaction)
        if ( !editor_group ) return;

        if ( !transaction.incoming.selection ) {
            return transaction.status(400)
                .message('Missing selection.')
                .send()
        }

        editor_group.set_member_selection(transaction.cm, transaction.incoming.selection)
        transaction.status(200).send()
    }

    get_selections(transaction, socket) {
        const editor_group = this._get_editor_group_or_fail(transaction)
        if ( !editor_group ) return;

        const selections = Object.values(editor_group.connections)
            .filter(maybeConn => transaction.cm.id !== maybeConn.id)
            .map(otherConn => {
                const session = editor_group.session(otherConn)
                const user_data = {
                    uuid: otherConn.request.user.uuid,
                    uid: otherConn.request.user.uid,
                    display: otherConn.request.user.uid,
                }

                if ( session.user_color ) {
                    user_data.color = session.user_color
                }

                return {...(editor_group.session(otherConn).selection || {}), user_data}
            })
            .filter(Boolean)

        transaction.status(200)
            .send({ selections })
    }

    get_editor_group_users(transaction, socket) {
        const editor_group = this._get_editor_group_or_fail(transaction)
        if ( !editor_group ) return;

        transaction.status(200)
            .send(
                editor_group.unique_users()
                    .filter(data => data.user.uuid !== transaction.cm.request.user.uuid)
                    .map(data => {
                        const { user, color } = data
                        return {
                            uuid: user.uuid,
                            uid: user.uid,
                            display: user.uid,
                            color,
                        }
                    })
            )
    }

    broadcast_content_mutation(transaction, socket) {
        const editor_group = this._get_editor_group_or_fail(transaction)
        if ( !editor_group ) return;

        if ( transaction.incoming.data.path.toLowerCase().startsWith('body@') ) {
            return transaction.status(200).send()
        }

        console.log('mutation', transaction.incoming.data)
        editor_group.blast_content_mutation(transaction.incoming.data, transaction.cm.id)

        transaction.status(200).send()
    }

    async join_editor_group(transaction, socket) {
        // FIXME support versioning
        const Page = this.models.get('api:Page')
        const Node = this.models.get('api:Node')

        if ( !transaction.incoming.PageId ) {
            return transaction.status(400)
                .message('Missing pageId.')
                .send()
        }

        const page = await Page.findOne({ UUID: transaction.incoming.PageId, Active: true })
        if ( !page || !(await page.is_accessible_by(transaction.cm.request.user, 'update')) ) {
            return transaction.status(401)
                .message('Invalid PageId.')
                .send()
        }

        if ( !transaction.incoming.NodeId ) {
            transaction.status(400)
                .message('Missing NodeId.')
                .send()
        }

        const node = await Node.findOne({ UUID: transaction.incoming.NodeId, PageId: page.UUID })
        if ( !node ) {
            transaction.status(400)
                .message('Invalid NodeId.')
                .send()
        }

        // Try to look up an existing editor group
        const editor_group_id = `${page.UUID}-${node.UUID}`
        let editor_group = this.editor_groups[editor_group_id]

        if ( !editor_group ) {
            // create a new editor group
            editor_group = new NormEditorGroup(page, node)
            this.editor_groups[editor_group_id] = editor_group
        }

        if ( !editor_group.has(transaction.cm) )
            editor_group.join(transaction.cm)

        transaction.status(200)
            .message('Joined editor group.')
            .send({ editor_group_id })

        editor_group.blast_editor_group_users()
    }

    _client_connected(connection_manager) {
        super._client_connected(connection_manager)

        this.output.debug('New client connected to norm editor socket.')

        if ( !connection_manager.request?.session?.auth?.user_id ) {
            delete this.connections[connection_manager.id]
            connection_manager.socket.close()
        }
    }
}

module.exports = exports = NormEditorController
