// @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
}

const supportedPlaintextPreviewExtensions = new Set([
	"4d",
	"abnf",
	"accesslog",
	"actionscript",
	"ada",
	"adoc",
	"alan",
	"angelscript",
	"ansi",
	"apache",
	"apacheconf",
	"applescript",
	"arcade",
	"arduino",
	"arm",
	"armasm",
	"as",
	"asc",
	"asciidoc",
	"aspectj",
	"ass",
	"atom",
	"autohotkey",
	"autoit",
	"avrasm",
	"awk",
	"axapta",
	"bash",
	"basic",
	"bat",
	"bbcode",
	"bf",
	"bind",
	"blade",
	"bnf",
	"brainfuck",
	"c",
	"c++",
	"cal",
	"capnp",
	"capnproto",
	"cc",
	"chaos",
	"chapel",
	"chpl",
	"cisco",
	"clj",
	"clojure",
	"cls",
	"cmake.in",
	"cmake",
	"cmd",
	"coffee",
	"coffeescript",
	"console",
	"coq",
	"cos",
	"cpc",
	"cpp",
	"cr",
	"craftcms",
	"crm",
	"crmsh",
	"crystal",
	"cs",
	"csharp",
	"cshtml",
	"cson",
	"csp",
	"css",
	"csv",
	"cxx",
	"cypher",
	"d",
	"dart",
	"delphi",
	"dfm",
	"diff",
	"django",
	"dns",
	"docker",
	"dockerfile",
	"dos",
	"dpr",
	"dsconfig",
	"dst",
	"dts",
	"dust",
	"dylan",
	"ebnf",
	"elixir",
	"elm",
	"erl",
	"erlang",
	"ex",
	"extempore",
	"f90",
	"f95",
	"fix",
	"fortran",
	"freepascal",
	"fs",
	"fsharp",
	"gams",
	"gauss",
	"gawk",
	"gcode",
	"gdscript",
	"gemspec",
	"gf",
	"gherkin",
	"glsl",
	"gms",
	"gn",
	"gni",
	"go",
	"godot",
	"golang",
	"golo",
	"gololang",
	"gradle",
	"graph",
	"groovy",
	"gss",
	"gyp",
	"h",
	"h++",
	"haml",
	"handlebars",
	"haskell",
	"haxe",
	"hbs",
	"hcl",
	"hh",
	"hpp",
	"hs",
	"html.handlebars",
	"html.hbs",
	"html",
	"http",
	"https",
	"hx",
	"hxx",
	"hy",
	"hylang",
	"i",
	"i7",
	"iced",
	"iecst",
	"inform7",
	"ini",
	"ino",
	"instances",
	"iol",
	"irb",
	"irpf90",
	"java",
	"javascript",
	"jinja",
	"jolie",
	"js",
	"json",
	"jsp",
	"jsx",
	"julia-repl",
	"julia",
	"k",
	"kaos",
	"kdb",
	"kotlin",
	"kt",
	"lasso",
	"lassoscript",
	"lazarus",
	"ldif",
	"leaf",
	"lean",
	"less",
	"lfm",
	"lisp",
	"livecodeserver",
	"livescript",
	"ln",
	"lock",
	"log",
	"lpr",
	"ls",
	"ls",
	"lua",
	"mak",
	"make",
	"makefile",
	"markdown",
	"mathematica",
	"matlab",
	"mawk",
	"maxima",
	"md",
	"mel",
	"mercury",
	"mirc",
	"mizar",
	"mk",
	"mkd",
	"mkdown",
	"ml",
	"ml",
	"mm",
	"mma",
	"mojolicious",
	"monkey",
	"moon",
	"moonscript",
	"mrc",
	"n1ql",
	"nawk",
	"nc",
	"never",
	"nginx",
	"nginxconf",
	"nim",
	"nimrod",
	"nix",
	"nsis",
	"obj-c",
	"obj-c++",
	"objc",
	"objective-c++",
	"objectivec",
	"ocaml",
	"ocl",
	"ol",
	"openscad",
	"osascript",
	"oxygene",
	"p21",
	"parser3",
	"pas",
	"pascal",
	"patch",
	"pcmk",
	"perl",
	"pf.conf",
	"pf",
	"pgsql",
	"php",
	"php3",
	"php4",
	"php5",
	"php6",
	"php7",
	"pl",
	"plaintext",
	"plist",
	"pm",
	"podspec",
	"pony",
	"postgres",
	"postgresql",
	"powershell",
	"pp",
	"processing",
	"profile",
	"prolog",
	"properties",
	"proto",
	"protobuf",
	"ps",
	"ps1",
	"puppet",
	"py",
	"pycon",
	"python-repl",
	"python",
	"qml",
	"r",
	"razor-cshtml",
	"razor",
	"rb",
	"re",
	"reasonml",
	"rebol",
	"red-system",
	"red",
	"redbol",
	"rf",
	"rib",
	"robot",
	"rpm-spec",
	"rpm-specfile",
	"rpm",
	"rs",
	"rsl",
	"rss",
	"ruby",
	"ruleslanguage",
	"rust",
	"sas",
	"SAS",
	"sc",
	"scad",
	"scala",
	"scheme",
	"sci",
	"scilab",
	"scl",
	"scss",
	"sh",
	"shell",
	"shexc",
	"smali",
	"smalltalk",
	"sml",
	"sol",
	"solidity",
	"spec",
	"specfile",
	"sql",
	"srt",
	"ssa",
	"st",
	"stan",
	"stanfuncs",
	"stata",
	"step",
	"stp",
	"structured-text",
	"styl",
	"stylus",
	"subunit",
	"supercollider",
	"svelte",
	"svg",
	"swift",
	"tao",
	"tap",
	"tcl",
	"terraform",
	"tex",
	"text",
	"tf",
	"thor",
	"thrift",
	"tk",
	"toml",
	"tp",
	"ts",
	"tsql",
	"tsx",
	"ttml",
	"twig",
	"txt",
	"typescript",
	"unicorn-rails-log",
	"v",
	"vala",
	"vb",
	"vba",
	"vbnet",
	"vbs",
	"vbscript",
	"verilog",
	"vhdl",
	"vim",
	"vtt",
	"wl",
	"x++",
	"x86asm",
	"xhtml",
	"xjb",
	"xl",
	"xml",
	"xpath",
	"xq",
	"xquery",
	"xsd",
	"xsl",
	"xtlang",
	"xtm",
	"yaml",
	"yml",
	"zep",
	"zephir",
	"zone",
	"zsh"
])

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
module.exports.supportedPlaintextPreviewExtensions = supportedPlaintextPreviewExtensions
