const log = require("./logging").get("RoomUpgradeHandler");
const MatrixRoom = require("../models/rooms/matrix");
const MatrixUser = require("../models/users/matrix");

/**
 * Handles migration of rooms when a room upgrade is performed.
 */
class RoomUpgradeHandler {
    /**
     * @param {RoomUpgradeHandler~Options} opts
     * @param {Bridge} bridge The parent bridge.
     */
    constructor(opts, bridge) {
        if (opts.migrateGhosts !== false) {
            opts.migrateGhosts = opts.migrateGhosts !== false;
        }
        this._opts = opts;
        this._bridge = bridge;
        this._waitingForInvite = new Map(); //newRoomId: oldRoomId
    }

    onTombstone(ev) {
        const movingTo = ev.content.replacement_room;
        log.info(`Got tombstone event for ${ev.room_id} -> ${movingTo}`);
        const joinVia = new MatrixUser(ev.sender).domain;
        // Try to join the new room.
        return this._joinNewRoom(movingTo, [joinVia]).then((couldJoin) => {
            if (couldJoin) {
                return this._onJoinedNewRoom(ev.room_id, movingTo);
            }
            this._waitingForInvite.set(movingTo, ev.room_id);
            return true;
        }).catch((err) => {
            log.error("Couldn't handle room upgrade: ", err);
            return false;
        });
    }

    _joinNewRoom(newRoomId, joinVia=[]) {
        const intent = this._bridge.getIntent();
        return intent.join(newRoomId, [joinVia]).then(() => {
            return true;
        }).catch((ex) => {
            if (ex.errcode === "M_FORBIDDEN") {
                return false;
            }
            throw Error("Failed to handle upgrade");
        })
    }

    onInvite(ev) {
        if (!this._waitingForInvite.has(ev.room_id)) {
            return false;
        }
        const oldRoomId = this._waitingForInvite.get(ev.room_id);
        this._waitingForInvite.delete(ev.room_id);
        log.debug(`Got invite to upgraded room ${ev.room_id}`);
        this._joinNewRoom(ev.room_id).then(() => {
            return this._onJoinedNewRoom(oldRoomId, ev.room_id);
        }).catch((err) => {
            log.error("Couldn't handle room upgrade: ", err);
        });
        return true;
    }

    async _onJoinedNewRoom(oldRoomId, newRoomId) {
        log.debug(`Joined ${newRoomId}`);
        const intent = this._bridge.getIntent();
        const asBot = this._bridge.getBot();
        const roomStore = this._bridge.getRoomStore();
        const entries = await roomStore.getEntriesByMatrixId(oldRoomId);
        let success = false;
        for (const entry of entries) {
            log.debug(`Migrating room entry ${entry.id}`);
            const existingId = entry.id;
            try {
                const newEntry = (
                    this._opts.migrateEntry || this._migrateEntry)(entry, newRoomId);

                if (!newEntry) {
                    continue;
                }

                // If migrateEntry changed the id of the room, then ensure
                // that we remove the old one.
                if (existingId !== newEntry.id) {
                    await roomStore.removeEntryById(existingId);
                }
                await roomStore.upsertEntry(newEntry);
                success = true;
            }
            catch (ex) {
                log.error(`Failed to migrate room entry ${entry.id}.`);
            }
        }
        // Upgrades are critical to get right, or a room will be stuck
        // until someone manaually intervenes. It's important to continue
        // migrating if at least one entry is successfully migrated.
        if (!success) {
            log.error("Failed to migrate room entries. Not continuing with migration.");
            return false;
        }

        log.debug(`Migrated entries from ${oldRoomId} to ${newRoomId} successfully.`);
        if (this._opts.onRoomMigrated) {
            // This may or may not be a promise, so await it.
            await this._opts.onRoomMigrated(oldRoomId, newRoomId);
        }

        if (!this._opts.migrateGhosts) {
            return false;
        }
        try {
            const members = await asBot.getJoinedMembers(oldRoomId);
            const userIds = Object.keys(members).filter((u) => asBot.isRemoteUser(u));
            log.debug(`Migrating ${userIds.length} ghosts`);
            for (const userId of userIds) {
                const i = this._bridge.getIntent(userId);
                await i.leave(oldRoomId);
                await i.join(newRoomId);
            }
            intent.leave(oldRoomId);
        }
        catch (ex) {
            log.warn("Failed to migrate ghosts", ex);
            return false;
        }
        return true;
    }

    _migrateEntry(entry, newRoomId) {
        entry.matrix = new MatrixRoom(newRoomId, {
            name: entry.name,
            topic: entry.topic,
            extras: entry._extras,
        });
        return entry;
    }
}

module.exports = RoomUpgradeHandler;

 /**
  * Options to supply to the {@link RoomUpgradeHandler}.
  * @typedef RoomUpgradeHandler~Options
  * @type {Object}
  * @property {RoomUpgradeHandler~MigrateEntry} migrateEntry Called when
  * the handler wishes to migrate a MatrixRoom entry to a new room_id. If omitted,
  * {@link RoomUpgradeHandler~_migrateEntry} will be used instead.
  * @property {RoomUpgradeHandler~onRoomMigrated} onRoomMigrated This is called
  * when the entries of the room have been migrated, the bridge should do any cleanup it
  * needs of the old room and setup the new room (ex: Joining ghosts to the new room).
  * @property {bool} [consumeEvent=true] Consume tombstone or invite events that
  * are acted on by this handler.
  * @property {bool} [migrateGhosts=true] If given, migrate all ghost users across to
  * the new room.
  */


 /**
 * Invoked when iterating around a rooms entries. Should be used to update entries
 * with a new room id.
 *
 * @callback RoomUpgradeHandler~MigrateEntry
 * @param {RoomBridgeStore~Entry} entry The existing entry.
 * @param {string} newRoomId The new roomId.
 * @return {RoomBridgeStore~Entry} Return the entry to upsert it,
 * or null to ignore it.
 */

 /**
  * Invoked after a room has been upgraded and it's entries updated.
  *
  * @callback RoomUpgradeHandler~onRoomMigrated
  * @param {string} oldRoomId The old roomId.
  * @param {string} newRoomId The new roomId.
  */

