// @ts-check

const DiscordTypes = require("discord-api-types/v10")
const assert = require("assert").strict

const {reg} = require("../matrix/read-registration")

const {db, select} = require("../passthrough")

/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
let hasher = null
// @ts-ignore
require("xxhash-wasm")().then(h => hasher = h)

const EPOCH = 1420070400000

/**
 * @param {string} guildID
 * @param {string[]} userRoles
 * @param {DiscordTypes.APIGuild["roles"]} guildRoles
 * @param {string} [userID]
 * @param {DiscordTypes.APIGuildChannel["permission_overwrites"]} [channelOverwrites]
 */
function getPermissions(guildID, userRoles, guildRoles, userID, channelOverwrites) {
	let allowed = BigInt(0)
	// Guild allows
	for (const role of guildRoles) {
		if (role.id === guildID) {
			allowed |= BigInt(role.permissions)
		}
		if (userRoles.includes(role.id)) {
			allowed |= BigInt(role.permissions)
		}
	}

	if (channelOverwrites) {
		/** @type {((overwrite: Required<DiscordTypes.APIOverwrite>) => any)[]} */
		const actions = [
			// Channel @everyone deny
			overwrite => overwrite.id === guildID && (allowed &= ~BigInt(overwrite.deny)),
			// Channel @everyone allow
			overwrite => overwrite.id === guildID && (allowed |= BigInt(overwrite.allow)),
			// Role deny
			overwrite => userRoles.includes(overwrite.id) && (allowed &= ~BigInt(overwrite.deny)),
			// Role allow
			overwrite => userRoles.includes(overwrite.id) && (allowed |= BigInt(overwrite.allow)),
			// User deny
			overwrite => overwrite.id === userID && (allowed &= ~BigInt(overwrite.deny)),
			// User allow
			overwrite => overwrite.id === userID && (allowed |= BigInt(overwrite.allow))
		]
		for (let i = 0; i < actions.length; i++) {
			for (const overwrite of channelOverwrites) {
				actions[i](overwrite)
			}
		}
	}
	return allowed
}

/**
 * @param {{id: string, roles: DiscordTypes.APIGuild["roles"]}} guild
 * @param {DiscordTypes.APIGuildChannel["permission_overwrites"]} [channel]
 */
function getDefaultPermissions(guild, channel) {
	const defaultRoles = select("role_default", "role_id", {guild_id: guild.id}).pluck().all()
	return getPermissions(guild.id, defaultRoles, guild.roles, undefined, channel)
}

/**
 * Note: You can only provide one permission bit to permissionToCheckFor. To check multiple permissions, call `hasAllPermissions` or `hasSomePermissions`.
 * It is designed like this to avoid developer error with bit manipulations.
 *
 * @param {bigint} resolvedPermissions
 * @param {bigint} permissionToCheckFor
 * @returns {boolean} whether the user has the requested permission
 * @example
 * const permissions = getPermissions(userRoles, guildRoles, userID, channelOverwrites)
 * hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel)
 */
function hasPermission(resolvedPermissions, permissionToCheckFor) {
	// Make sure permissionToCheckFor has exactly one permission in it
	assert.equal(permissionToCheckFor.toString(2).match(/1/g)?.length, 1)
	// Do the actual calculation
	return (resolvedPermissions & permissionToCheckFor) === permissionToCheckFor
}

/**
 * @param {bigint} resolvedPermissions
 * @param {(keyof DiscordTypes.PermissionFlagsBits)[]} permissionsToCheckFor
 * @returns {boolean} whether the user has any of the requested permissions
 * @example
 * const permissions = getPermissions(userRoles, guildRoles, userID, channelOverwrites)
 * hasSomePermissions(permissions, ["ViewChannel", "ReadMessageHistory"])
 */
function hasSomePermissions(resolvedPermissions, permissionsToCheckFor) {
	return permissionsToCheckFor.some(x => hasPermission(resolvedPermissions, DiscordTypes.PermissionFlagsBits[x]))
}

/**
 * @param {bigint} resolvedPermissions
 * @param {(keyof DiscordTypes.PermissionFlagsBits)[]} permissionsToCheckFor
 * @returns {boolean} whether the user has all of the requested permissions
 * @example
 * const permissions = getPermissions(userRoles, guildRoles, userID, channelOverwrites)
 * hasAllPermissions(permissions, ["ViewChannel", "ReadMessageHistory"])
 */
function hasAllPermissions(resolvedPermissions, permissionsToCheckFor) {
	return permissionsToCheckFor.every(x => hasPermission(resolvedPermissions, DiscordTypes.PermissionFlagsBits[x]))
}

/**
 * Command interaction responses have a webhook_id for some reason, but still have real author info of a real bot user in the server.
 * @param {DiscordTypes.APIMessage} message
 */
function isWebhookMessage(message) {
	return message.webhook_id && message.type !== DiscordTypes.MessageType.ChatInputCommand && message.type !== DiscordTypes.MessageType.ContextMenuCommand
}

/**
 * @param {Pick<DiscordTypes.APIMessage, "flags">} message
 */
function isEphemeralMessage(message) {
	return Boolean(message.flags && (message.flags & DiscordTypes.MessageFlags.Ephemeral))
}

/** @param {string} snowflake */
function snowflakeToTimestampExact(snowflake) {
	return Number(BigInt(snowflake) >> 22n) + EPOCH
}

/** @param {number} timestamp */
function timestampToSnowflakeInexact(timestamp) {
	return String((timestamp - EPOCH) * 2**22)
}

/** @param {string} url */
function getPublicUrlForCdn(url) {
	const match = url.match(/https:\/\/(cdn|media)\.discordapp\.(?:com|net)\/attachments\/([0-9]+)\/([0-9]+)\/([-A-Za-z0-9_.,]+)/)
	if (!match) return url
	const unsignedHash = hasher.h64(match[3]) // attachment ID
	const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range
	db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash)
	return `${reg.ooye.bridge_origin}/download/discord${match[1]}/${match[2]}/${match[3]}/${match[4]}`
}

/**
 * @param {string} oldTimestamp
 * @param {string} newTimestamp
 * @returns {string} "a x-day-old unbridged message"
 */
function howOldUnbridgedMessage(oldTimestamp, newTimestamp) {
	const dateDifference = new Date(newTimestamp).getTime() - new Date(oldTimestamp).getTime()
	const oneHour = 60 * 60 * 1000
	if (dateDifference < oneHour) {
		return "an unbridged message"
	} else if (dateDifference < 25 * oneHour) {
		var dateDisplay = `a ${Math.floor(dateDifference / oneHour)}-hour-old unbridged message`
	} else {
		var dateDisplay = `a ${Math.round(dateDifference / (24 * oneHour))}-day-old unbridged message`
	}
	return dateDisplay
}

/**
 * Modifies the input, removing items that don't pass the filter. Returns the items that didn't pass.
 * @param {T[]} xs
 * @param {(x: T, i?: number) => any} fn
 * @template T
 * @returns T[]
 */
function filterTo(xs, fn) {
	/** @type {T[]} */
	const filtered = []
	for (let i = xs.length-1; i >= 0; i--) {
		const x = xs[i]
		if (!fn(x, i)) {
			filtered.unshift(x)
			xs.splice(i, 1)
		}
	}
	return filtered
}

module.exports.getPermissions = getPermissions
module.exports.getDefaultPermissions = getDefaultPermissions
module.exports.hasPermission = hasPermission
module.exports.hasSomePermissions = hasSomePermissions
module.exports.hasAllPermissions = hasAllPermissions
module.exports.isWebhookMessage = isWebhookMessage
module.exports.isEphemeralMessage = isEphemeralMessage
module.exports.snowflakeToTimestampExact = snowflakeToTimestampExact
module.exports.timestampToSnowflakeInexact = timestampToSnowflakeInexact
module.exports.getPublicUrlForCdn = getPublicUrlForCdn
module.exports.howOldUnbridgedMessage = howOldUnbridgedMessage
module.exports.filterTo = filterTo
