// @ts-check

const assert = require("assert").strict
const DiscordTypes = require("discord-api-types/v10")
const Ty = require("../../types")
const {reg} = require("../../matrix/read-registration")

const passthrough = require("../../passthrough")
const {discord, sync, db, select, from} = passthrough
/** @type {import("../../matrix/file")} */
const file = sync.require("../../matrix/file")
/** @type {import("../../matrix/api")} */
const api = sync.require("../../matrix/api")
/** @type {import("../../matrix/mreq")} */
const mreq = sync.require("../../matrix/mreq")
/** @type {import("../../matrix/kstate")} */
const ks = sync.require("../../matrix/kstate")
/** @type {import("../../discord/utils")} */
const dUtils = sync.require("../../discord/utils")
/** @type {import("../../matrix/utils")} */
const mUtils = sync.require("../../matrix/utils")
/** @type {import("./create-space")} */
const createSpace = sync.require("./create-space")

/**
 * There are 3 levels of room privacy:
 * 0: Room is invite-only.
 * 1: Anybody can use a link to join.
 * 2: Room is published in room directory.
 */
const PRIVACY_ENUMS = {
	PRESET: ["private_chat", "public_chat", "public_chat"],
	VISIBILITY: ["private", "private", "private"],
	SPACE_HISTORY_VISIBILITY: ["invited", "world_readable", "world_readable"], // copying from element client
	ROOM_HISTORY_VISIBILITY: ["shared", "shared", "world_readable"], // any events sent after <value> are visible, but for world_readable anybody can read without even joining
	GUEST_ACCESS: ["can_join", "forbidden", "forbidden"], // whether guests can join space if other conditions are met
	SPACE_JOIN_RULES: ["invite", "public", "public"],
	ROOM_JOIN_RULES: ["restricted", "public", "public"]
}

const DEFAULT_PRIVACY_LEVEL = 0

const READ_ONLY_ROOM_EVENTS_DEFAULT_POWER = 50

/** @type {Map<string, Promise<string>>} channel ID -> Promise<room ID> */
const inflightRoomCreate = new Map()

/**
 * @param {{id: string, name: string, topic?: string?, type: number, parent_id?: string?}} channel
 * @param {{id: string}} guild
 * @param {string | null | undefined} customName
 */
function convertNameAndTopic(channel, guild, customName) {
	// @ts-ignore
	const parentChannel = discord.channels.get(channel.parent_id)
	let channelPrefix =
		( parentChannel?.type === DiscordTypes.ChannelType.GuildForum ? ""
		: channel.type === DiscordTypes.ChannelType.PublicThread ? "[⛓️] "
		: channel.type === DiscordTypes.ChannelType.AnnouncementThread ? "[⛓️] "
		: channel.type === DiscordTypes.ChannelType.PrivateThread ? "[🔒⛓️] "
		: channel.type === DiscordTypes.ChannelType.GuildVoice ? "[🔊] "
		: "")
	const chosenName = customName || (channelPrefix + channel.name);
	const maybeTopicWithPipe = channel.topic ? ` | ${channel.topic}` : '';
	const maybeTopicWithNewlines = channel.topic ? `${channel.topic}\n\n` : '';
	const channelIDPart = `Channel ID: ${channel.id}`;
	const guildIDPart = `Guild ID: ${guild.id}`;

	const convertedTopic = customName
		 ? `#${channel.name}${maybeTopicWithPipe}\n\n${channelIDPart}\n${guildIDPart}`
		 : `${maybeTopicWithNewlines}${channelIDPart}\n${guildIDPart}`;

	return [chosenName, convertedTopic];
}

/**
 * Async because it may create the guild and/or upload the guild icon to mxc.
 * @param {DiscordTypes.APIGuildTextChannel | DiscordTypes.APIThreadChannel} channel
 * @param {DiscordTypes.APIGuild} guild
 * @param {{api: {getStateEvent: typeof api.getStateEvent, getStateEventOuter: typeof api.getStateEventOuter}}} di simple-as-nails dependency injection for the matrix API
 */
async function channelToKState(channel, guild, di) {
	// @ts-ignore
	const parentChannel = discord.channels.get(channel.parent_id)

	/** Used for membership/permission checks. */
	const guildSpaceID = await createSpace.ensureSpace(guild)
	/** Used as the literal parent on Matrix, for categorisation. Will be the same as `guildSpaceID` unless it's a forum channel's thread, in which case a different space is used to group those threads. */
	let parentSpaceID = guildSpaceID
	if (parentChannel?.type === DiscordTypes.ChannelType.GuildForum) {
		parentSpaceID = await ensureRoom(channel.parent_id)
		assert(typeof parentSpaceID === "string")
	}

	const channelRow = select("channel_room", ["nick", "custom_avatar", "custom_topic"], {channel_id: channel.id}).get()
	const customName = channelRow?.nick
	const customAvatar = channelRow?.custom_avatar
	const hasCustomTopic = channelRow?.custom_topic
	const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, customName)

	const avatarEventContent = {}
	if (customAvatar) {
		avatarEventContent.url = customAvatar
	} else if (guild.icon) {
		avatarEventContent.url = {$url: file.guildIcon(guild)}
	}

	const privacyLevel = select("guild_space", "privacy_level", {guild_id: guild.id}).pluck().get()
	assert(privacyLevel != null) // already ensured the space exists
	let history_visibility = PRIVACY_ENUMS.ROOM_HISTORY_VISIBILITY[privacyLevel]
	if (channel["thread_metadata"]) history_visibility = "world_readable"

	/** @type {{join_rule: string, allow?: any}} */
	let join_rules = {
		join_rule: "restricted",
		allow: [{
			type: "m.room_membership",
			room_id: guildSpaceID
		}]
	}
	if (PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel] !== "restricted") {
		join_rules = {join_rule: PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel]}
	}

	const everyonePermissions = dUtils.getDefaultPermissions(guild, channel.permission_overwrites)
	const everyoneCanSend = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.SendMessages)
	const everyoneCanMentionEveryone = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.MentionEveryone)

	const pollStartPowerLevel = {}
	const everyoneCanCreatePolls = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.SendPolls)
	if (everyoneCanSend && !everyoneCanCreatePolls) pollStartPowerLevel["org.matrix.msc3381.poll.start"] = 10

	const spacePowerDetails = await mUtils.getEffectivePower(guildSpaceID, [], di.api)
	spacePowerDetails.powerLevels.users ??= {}
	const spaceCreatorsAndFounders = spacePowerDetails.allCreators
		.concat(Object.entries(spacePowerDetails.powerLevels.users).filter(([, power]) => power >= spacePowerDetails.tombstone).map(([mxid]) => mxid))

	const globalAdmins = select("member_power", ["mxid", "power_level"], {room_id: "*"}).all()
	const globalAdminPower = globalAdmins.reduce((a, c) => (a[c.mxid] = c.power_level, a), {})

	const additionalCreators = select("member_power", "mxid", {room_id: "*"}, "AND power_level > 100").pluck().all().concat(spaceCreatorsAndFounders)
	const creationContent = {}
	creationContent.additional_creators = additionalCreators

	if (channel.type === DiscordTypes.ChannelType.GuildForum) creationContent.type = "m.space"

	/** @type {any} */
	const channelKState = {
		"m.room.create/": creationContent,
		"m.room.name/": {name: convertedName},
		"m.room.topic/": {topic: convertedTopic},
		"m.room.avatar/": avatarEventContent,
		"m.room.guest_access/": {guest_access: PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]},
		"m.room.history_visibility/": {history_visibility},
		[`m.space.parent/${parentSpaceID}`]: {
			via: [reg.ooye.server_name],
			canonical: true
		},
		/** @type {{join_rule: string, [x: string]: any}} */
		"m.room.join_rules/": join_rules,
		/** @type {Ty.Event.M_Power_Levels} */
		"m.room.power_levels/": {
			events_default: everyoneCanSend ? 0 : READ_ONLY_ROOM_EVENTS_DEFAULT_POWER,
			events: {
				"m.reaction": 0,
				"m.room.redaction": 0, // only affects redactions of own events, required to be able to un-react
				...pollStartPowerLevel
			},
			notifications: {
				room: everyoneCanMentionEveryone ? 0 : 20
			},
			users: {...spacePowerDetails.powerLevels.users, ...globalAdminPower}
		},
		[`uk.half-shot.bridge/moe.cadence.ooye://discord/${guild.id}/${channel.id}`]: {
			bridgebot: mUtils.bot,
			protocol: {
				id: "discord",
				displayname: "Discord"
			},
			network: {
				id: guild.id,
				displayname: guild.name,
				avatar_url: {$url: file.guildIcon(guild)}
			},
			channel: {
				id: channel.id,
				displayname: channel.name,
				external_url: `https://discord.com/channels/${guild.id}/${channel.id}`
			}
		}
	}

	// Don't overwrite room topic if the topic has been customised
	if (hasCustomTopic) delete channelKState["m.room.topic/"]

	// Don't add a space parent if it's self service
	// (The person setting up self-service has already put it in their preferred space to be able to get this far.)
	const autocreate = select("guild_active", "autocreate", {guild_id: guild.id}).pluck().get()
	if (autocreate === 0 && ![DiscordTypes.ChannelType.PrivateThread, DiscordTypes.ChannelType.PublicThread, DiscordTypes.ChannelType.AnnouncementThread].includes(channel.type)) {
		delete channelKState[`m.space.parent/${parentSpaceID}`]
	}

	return {spaceID: parentSpaceID, privacyLevel, channelKState}
}

/**
 * Create a bridge room, store the relationship in the database, and add it to the guild's space.
 * @param {DiscordTypes.APIGuildTextChannel} channel
 * @param {DiscordTypes.APIGuild} guild
 * @param {string} spaceID
 * @param {any} kstate
 * @param {number} privacyLevel
 * @returns {Promise<string>} room ID
 */
async function createRoom(channel, guild, spaceID, kstate, privacyLevel) {
	let threadParent = null
	if (channel.type === DiscordTypes.ChannelType.PublicThread) threadParent = channel.parent_id

	// Name and topic can be done earlier in room creation rather than in initial_state
	// https://spec.matrix.org/latest/client-server-api/#creation
	const name = kstate["m.room.name/"].name
	delete kstate["m.room.name/"]
	assert(name)
	const topic = kstate["m.room.topic/"].topic
	delete kstate["m.room.topic/"]
	assert(topic)

	const roomCreate = await postApplyPowerLevels(kstate, async kstate => {
		const roomID = await api.createRoom({
			name,
			topic,
			preset: PRIVACY_ENUMS.PRESET[privacyLevel], // This is closest to what we want, but properties from kstate override it anyway
			visibility: PRIVACY_ENUMS.VISIBILITY[privacyLevel],
			invite: [],
			initial_state: await ks.kstateToState(kstate),
			creation_content: ks.kstateToCreationContent(kstate)
		})

		/** @type {Ty.Event.StateOuter<Ty.Event.M_Room_Create>} */
		const roomCreate = await api.getStateEventOuter(roomID, "m.room.create", "")

		db.transaction(() => {
			db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, guild_id) VALUES (?, ?, ?, NULL, ?, ?)").run(channel.id, roomID, channel.name, threadParent, guild.id)
			db.prepare("INSERT INTO historical_channel_room (reference_channel_id, room_id, upgraded_timestamp) VALUES (?, ?, 0)").run(channel.id, roomID)
		})()

		return roomCreate
	})
	const roomID = roomCreate.room_id

	// Put the newly created child into the space
	await _syncSpaceMember(channel, spaceID, roomID, guild.id)

	return roomID
}

/**
 * Handling power levels separately. The spec doesn't specify what happens, Dendrite differs,
 * and Synapse does a very poorly thought out *shallow merge* of what I provide on top of what it creates.
 * We don't want the `events` key to be overridden completely.
 * https://github.com/matrix-org/synapse/blob/develop/synapse/handlers/room.py#L1170-L1210
 * https://github.com/matrix-org/matrix-spec/issues/492
 * @param {any} kstate
 * @param {(_: any) => Promise<Ty.Event.StateOuter<Ty.Event.M_Room_Create>>} callback must return room ID and room version
 * @returns {Promise<Ty.Event.StateOuter<Ty.Event.M_Room_Create>>} room ID
 */
async function postApplyPowerLevels(kstate, callback) {
	const powerLevelContent = kstate["m.room.power_levels/"]
	const kstateWithoutPowerLevels = {...kstate}
	delete kstateWithoutPowerLevels["m.room.power_levels/"]

	const roomCreate = await callback(kstateWithoutPowerLevels)
	const roomID = roomCreate.room_id

	// Now *really* apply the power level overrides on top of what Synapse *really* set
	if (powerLevelContent) {
		mUtils.removeCreatorsFromPowerLevels(roomCreate, powerLevelContent)

		const originalPowerLevels = await api.getStateEvent(roomID, "m.room.power_levels", "")
		const powerLevelsDiff = ks.diffKState(
			{"m.room.power_levels/": originalPowerLevels, "m.room.create/": roomCreate.content, "m.room.create/outer": roomCreate},
			{"m.room.power_levels/": powerLevelContent}
		)
		await ks.applyKStateDiffToRoom(roomID, powerLevelsDiff)
	}

	return roomCreate
}

/**
 * @param {DiscordTypes.APIGuildChannel} channel
 */
function channelToGuild(channel) {
	const guildID = channel.guild_id
	assert(guildID)
	const guild = discord.guilds.get(guildID)
	assert(guild)
	return guild
}

/**
 * This function handles whether it's allowed to bridge messages in this channel, and if so, where to.
 * This has to account for whether self-service is enabled for the guild or not.
 * This also has to account for different channel types, like forum channels (which need the
 * parent forum to already exist, and ignore the self-service setting), or thread channels (which
 * need the parent channel to already exist, and ignore the self-service setting).
 * @param {DiscordTypes.APIGuildTextChannel | DiscordTypes.APIThreadChannel} channel text channel or thread
 * @param {string} guildID
 * @returns obj if bridged; 1 if autocreatable; null/undefined if guild is not bridged; 0 if self-service and not autocreatable thread
 */
function existsOrAutocreatable(channel, guildID) {
	// 1. If the channel is already linked somewhere, it's always okay to bridge to that destination, no matter what. Yippee!
	const existing = select("channel_room", ["room_id", "thread_parent"], {channel_id: channel.id}).get()
	if (existing) return existing

	// 2. If the guild is an autocreate guild, it's always okay to bridge to that destination, and
	// we'll need to create any dependent resources recursively.
	const autocreate = select("guild_active", "autocreate", {guild_id: guildID}).pluck().get()
	if (autocreate === 1) return autocreate

	// 3. If the guild is not approved for bridging yet, we can't bridge there.
	// They need to decide one way or another whether it's self-service before we can continue.
	if (autocreate == null) return autocreate

	// 4. If we got here, the guild is in self-service mode.
	// New channels won't be able to create new rooms. But forum threads or channel threads could be fine.
	if ([DiscordTypes.ChannelType.PublicThread, DiscordTypes.ChannelType.PrivateThread, DiscordTypes.ChannelType.AnnouncementThread].includes(channel.type)) {
		// In self-service mode, threads rely on the parent resource already existing.
		/** @type {DiscordTypes.APIGuildTextChannel} */ // @ts-ignore
		const parent = discord.channels.get(channel.parent_id)
		assert(parent)
		const parentExisting = existsOrAutocreatable(parent, guildID)
		if (parentExisting) return 1 // Autocreatable
	}

	// 5. If we got here, the guild is in self-service mode and the channel is truly not bridged.
	return autocreate
}

/**
 * @param {DiscordTypes.APIGuildTextChannel | DiscordTypes.APIThreadChannel} channel text channel or thread
 * @param {string} guildID
 * @returns obj if bridged; 1 if autocreatable. (throws if not autocreatable)
 */
function assertExistsOrAutocreatable(channel, guildID) {
	const existing = existsOrAutocreatable(channel, guildID)
	if (existing === 0) {
		throw new Error(`Guild ${guildID} is self-service, so won't create a Matrix room for channel ${channel.id}`)
	}
	if (!existing) {
		throw new Error(`Guild ${guildID} is not bridged, so won't create a Matrix room for channel ${channel.id}`)
	}
	return existing
}

/*
	Ensure flow:
	1. Get IDs
	2. Does room exist? If so great!
	(it doesn't, so it needs to be created)
	3. Get kstate for channel
	4. Create room, return new ID

	Ensure + sync flow:
	1. Get IDs
	2. Does room exist?
	2.5: If room does exist AND wasn't asked to sync: return here
	3. Get kstate for channel
	4. Create room with kstate if room doesn't exist
	5. Get and update room state with kstate if room does exist
*/

/**
 * Create room and/or sync room data. Please check that a channel_room entry exists or autocreate = 1 before calling this.
 * @param {string} channelID
 * @param {boolean} shouldActuallySync false if just need to ensure room exists (which is a quick database check), true if also want to sync room data when it does exist (slow)
 * @returns {Promise<string>} room ID
 */
async function _syncRoom(channelID, shouldActuallySync) {
	/** @ts-ignore @type {DiscordTypes.APIGuildTextChannel} */
	const channel = discord.channels.get(channelID)
	assert.ok(channel)
	const guild = channelToGuild(channel)

	if (inflightRoomCreate.has(channelID)) {
		await inflightRoomCreate.get(channelID) // just waiting, and then doing a new db query afterwards, is the simplest way of doing it
	}

	const existing = assertExistsOrAutocreatable(channel, guild.id)

	if (existing === 1) {
		const creation = (async () => {
			const {spaceID, privacyLevel, channelKState} = await channelToKState(channel, guild, {api})
			const roomID = await createRoom(channel, guild, spaceID, channelKState, privacyLevel)
			inflightRoomCreate.delete(channelID) // OK to release inflight waiters now. they will read the correct `existing` row
			return roomID
		})()
		inflightRoomCreate.set(channelID, creation)
		return creation // Naturally, the newly created room is already up to date, so we can always skip syncing here.
	}

	const roomID = existing.room_id

	if (!shouldActuallySync) {
		return existing.room_id // only need to ensure room exists, and it does. return the room ID
	}

	console.log(`[room sync] to matrix: ${channel.name}`)

	const {spaceID, channelKState} = await channelToKState(channel, guild, {api}) // calling this in both branches because we don't want to calculate this if not syncing
	await ks.kstateUploadMxc(channelKState) // pre-upload icons before diffing

	// sync channel state to room
	const roomKState = await ks.roomToKState(roomID)
	if (!mUtils.roomHasAtLeastVersion(roomKState["m.room.create/"].room_version, 9)) {
		// join_rule `restricted` is not available in room version < 8 and not working properly in version == 8, so require version 9
		// read more: https://spec.matrix.org/v1.8/rooms/v9/
		// we have to use `public` instead, otherwise the room will be unjoinable.
		channelKState["m.room.join_rules/"] = {join_rule: "public"}
	}
	const roomDiff = ks.diffKState(roomKState, channelKState)
	const roomApply = ks.applyKStateDiffToRoom(roomID, roomDiff)
	db.prepare("UPDATE channel_room SET name = ? WHERE channel_id = ?").run(channel.name, channel.id)

	// sync room as space member
	const spaceApply = _syncSpaceMember(channel, spaceID, roomID, guild.id)
	await Promise.all([roomApply, spaceApply])

	return roomID
}

/** Ensures the room exists. If it doesn't, creates the room with an accurate initial state. Please check that a channel_room entry exists or guild autocreate = 1 before calling this. */
function ensureRoom(channelID) {
	return _syncRoom(channelID, false)
}

/** Actually syncs. Gets all room state from the homeserver in order to diff, and uploads the icon to mxc if it has changed. Please check that a channel_room entry exists or guild autocreate = 1 before calling this. */
function syncRoom(channelID) {
	return _syncRoom(channelID, true)
}

/**
 * @param {{id: string, topic?: string?}} channel channel-ish (just needs an id, topic is optional)
 * @param {string} guildID
 * @param {string} messageBeforeLeave
 */
async function unbridgeChannel(channel, guildID, messageBeforeLeave = "This room was removed from the bridge.") {
	const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
	assert.ok(roomID)
	const row = from("guild_space").join("guild_active", "guild_id").select("space_id", "autocreate").where({guild_id: guildID}).get()
	assert.ok(row)

	let botInRoom = true

	// remove declaration that the room is bridged
	try {
		await api.sendState(roomID, "uk.half-shot.bridge", `moe.cadence.ooye://discord/${guildID}/${channel.id}`, {})
	} catch (e) {
		if (String(e).includes("not in room")) {
			botInRoom = false
		} else {
			throw e
		}
	}

	if (botInRoom && "topic" in channel) {
		// previously the Matrix topic would say the channel ID. we should remove that
		await api.sendState(roomID, "m.room.topic", "", {topic: channel.topic || ""})
	}

	// delete webhook on discord
	const webhook = select("webhook", ["webhook_id", "webhook_token"], {channel_id: channel.id}).get()
	if (webhook) {
		await discord.snow.webhook.deleteWebhook(webhook.webhook_id, webhook.webhook_token).catch(() => {})
		db.prepare("DELETE FROM webhook WHERE channel_id = ?").run(channel.id)
	}

	// delete room from database
	db.prepare("DELETE FROM member_cache WHERE room_id = ?").run(roomID)
	db.prepare("DELETE FROM channel_room WHERE room_id = ? AND channel_id = ?").run(roomID, channel.id) // cascades to most other tables, like messages and historical rooms

	if (!botInRoom) return

	// demote discord sim admins in room
	const {powerLevels, allCreators} = await mUtils.getEffectivePower(roomID, [], api)
	const powerLevelsUsers = (powerLevels.users ||= {})
	for (const mxid of Object.keys(powerLevelsUsers)) {
		if (powerLevelsUsers[mxid] >= (powerLevels.state_default ?? 50) && !allCreators.includes(mxid) && mUtils.eventSenderIsFromDiscord(mxid) && mxid !== mUtils.bot) {
			delete powerLevelsUsers[mxid]
			await api.sendState(roomID, "m.room.power_levels", "", powerLevels, mxid) // done individually because each user must demote themselves
		}
	}

	// send a notification in the room
	await api.sendEvent(roomID, "m.room.message", {
		msgtype: "m.notice",
		body: `⚠️ ${messageBeforeLeave}`
	})

	// if it is an easy mode room, clean up the room from the managed space and make it clear it's not being bridged
	// (don't do this for self-service rooms, because they might continue to be used on Matrix or linked somewhere else later)
	if (row.autocreate === 1) {
		// remove room from being a space member
		await api.sendState(roomID, "m.space.parent", row.space_id, {})
		await api.sendState(row.space_id, "m.space.child", roomID, {})
	}

	// if it is a self-service room, remove sim members
	// (the room can be used with less clutter and the member list makes sense if it's bridged somewhere else)
	if (row.autocreate === 0) {
		// remove sim members
		const members = db.prepare("SELECT mxid FROM sim_member WHERE room_id = ? AND mxid <> ?").pluck().all(roomID, mUtils.bot)
		const preparedDelete = db.prepare("DELETE FROM sim_member WHERE room_id = ? AND mxid = ?")
		for (const mxid of members) {
			await api.leaveRoom(roomID, mxid)
			preparedDelete.run(roomID, mxid)
		}
	}

	// leave room
	await mUtils.setUserPower(roomID, mUtils.bot, 0, api)
	await api.leaveRoom(roomID)
}

/**
 * Async because it gets all space state from the homeserver, then if necessary sends one state event back.
 * @param {DiscordTypes.APIGuildTextChannel} channel
 * @param {string} spaceID
 * @param {string} roomID
 * @param {string} guild_id
 * @returns {Promise<string[]>}
 */
async function _syncSpaceMember(channel, spaceID, roomID, guild_id) {
	// If space is self-service then only permit changes to space parenting for threads
	// (The person setting up self-service has already put it in their preferred space to be able to get this far.)
	const autocreate = select("guild_active", "autocreate", {guild_id}).pluck().get()
	if (autocreate === 0 && ![DiscordTypes.ChannelType.PrivateThread, DiscordTypes.ChannelType.PublicThread, DiscordTypes.ChannelType.AnnouncementThread].includes(channel.type)) {
		return []
	}

	const spaceKState = await ks.roomToKState(spaceID)
	let spaceEventContent = {}
	if (
		channel.type !== DiscordTypes.ChannelType.PrivateThread // private threads do not belong in the space (don't offer people something they can't join)
		&& (
			!channel["thread_metadata"]?.archived // archived threads do not belong in the space (don't offer people conversations that are no longer relevant)
			|| discord.channels.get(channel.parent_id || "")?.type === DiscordTypes.ChannelType.GuildForum
		)
	) {
		spaceEventContent = {
			via: [reg.ooye.server_name]
		}
	}
	const spaceDiff = ks.diffKState(spaceKState, {
		[`m.space.child/${roomID}`]: spaceEventContent
	})
	return ks.applyKStateDiffToRoom(spaceID, spaceDiff)
}

async function createAllForGuild(guildID) {
	const channelIDs = discord.guildChannelMap.get(guildID)
	assert.ok(channelIDs)
	for (const channelID of channelIDs) {
		const allowedTypes = [DiscordTypes.ChannelType.GuildText, DiscordTypes.ChannelType.PublicThread]
		// @ts-ignore
		if (allowedTypes.includes(discord.channels.get(channelID)?.type)) {
			const roomID = await syncRoom(channelID)
			console.log(`synced ${channelID} <-> ${roomID}`)
		}
	}
}

module.exports.DEFAULT_PRIVACY_LEVEL = DEFAULT_PRIVACY_LEVEL
module.exports.READ_ONLY_ROOM_EVENTS_DEFAULT_POWER = READ_ONLY_ROOM_EVENTS_DEFAULT_POWER
module.exports.PRIVACY_ENUMS = PRIVACY_ENUMS
module.exports.createRoom = createRoom
module.exports.ensureRoom = ensureRoom
module.exports.syncRoom = syncRoom
module.exports.createAllForGuild = createAllForGuild
module.exports.channelToKState = channelToKState
module.exports.postApplyPowerLevels = postApplyPowerLevels
module.exports._convertNameAndTopic = convertNameAndTopic
module.exports._syncSpaceMember = _syncSpaceMember
module.exports.unbridgeChannel = unbridgeChannel
module.exports.existsOrAutocreatable = existsOrAutocreatable
module.exports.assertExistsOrAutocreatable = assertExistsOrAutocreatable
