import { join } from "@tauri-apps/api/path"
import { exists } from "@tauri-apps/plugin-fs"
import type { InternalSpawnOptions, SpawnOptions } from "tauri-plugin-shellx-api"
import { safeParse } from "valibot"
import { type DenoRunConfig } from "../../api/client"
import {
	PermissionScopeSchema,
	ShellPermissionScopedSchema,
	type ShellPermission,
	type ShellPermissionScoped
} from "../../permissions/schema"
import {
	AllPathAliases,
	matchPathAndScope,
	pathStartsWithAlias,
	translateScopeToPath
} from "../../utils/path"

/**
 *
 * @param userPermissionScopes
 * @param program should be deno for now, may be I will support custom deno path in the future
 * @param scriptPath absolute path to deno script
 * @param config Deno config from extension API call
 */
export async function verifyDenoCmdPermission(
	userPermissionScopes: ShellPermissionScoped[],
	program: string,
	scriptPath: string,
	config: Partial<DenoRunConfig> & SpawnOptions,
	extensionDir: string
) {
	scriptPath = await translateScopeToPath(scriptPath, extensionDir)
	if (!(await exists(scriptPath)) && config.cwd) {
		scriptPath = await join(config.cwd, scriptPath)
	}
	const userDenoPerms = userPermissionScopes.filter(
		(p) => p.permission && p.permission.startsWith("shell:deno:")
	)
	const pathMatchedPerms: ShellPermissionScoped[] = []
	for (const perm of userDenoPerms) {
		const safeParsed = safeParse(ShellPermissionScopedSchema, perm)
		if (!safeParsed.success) {
			throw new Error(`Invalid permission: ${perm}`)
		}
		const parsedPerm = safeParsed.output
		// ignore the ones whose path doesn't match
		let pathMatched = false
		if (parsedPerm.allow) {
			for (const allow of parsedPerm.allow) {
				if (allow.path && (await matchPathAndScope(scriptPath, allow.path, extensionDir))) {
					pathMatched = true
				}
			}
		}
		if (parsedPerm.deny) {
			for (const deny of parsedPerm.deny) {
				if (deny.path && (await matchPathAndScope(scriptPath, deny.path, extensionDir))) {
					pathMatched = true
				}
			}
		}
		if (pathMatched) {
			pathMatchedPerms.push(parsedPerm)
		}
	}

	/* -------------------- Start Verifying Deno Permissions -------------------- */
	const allowEnv: string[] = []
	let allowAllEnv = false
	const denyEnv: string[] = []
	let denyAllEnv = false
	const allowNet: string[] = []
	let allowAllNet = false
	const denyNet: string[] = []
	let denyAllNet = false
	let allowRead: string[] = []
	let allowAllRead = false
	let denyRead: string[] = []
	let denyAllRead = false
	let allowWrite: string[] = []
	let allowAllWrite = false
	let denyWrite: string[] = []
	let denyAllWrite = false
	const allowRun: string[] = []
	let allowAllRun = false
	const denyRun: string[] = []
	let denyAllRun = false
	const allowFfi: string[] = []
	let allowAllFfi = false
	const denyFfi: string[] = []
	let denyAllFfi = false
	const allowSys: string[] = []
	let allowAllSys = false
	const denySys: string[] = []
	let denyAllSys = false

	for (const perm of pathMatchedPerms) {
		if (perm.allow) {
			for (const allow of perm.allow) {
				if (allow.env) {
					if (allow.env === "*") {
						allowAllEnv = true
					} else {
						allowEnv.push(...allow.env)
					}
				}
				if (allow.net) {
					if (allow.net === "*") {
						allowAllNet = true
					} else {
						allowNet.push(...allow.net)
					}
				}
				if (allow.read) {
					if (allow.read === "*") {
						allowAllRead = true
					} else {
						allowRead.push(...allow.read)
					}
				}
				if (allow.write) {
					if (allow.write === "*") {
						allowAllWrite = true
					} else {
						allowWrite.push(...allow.write)
					}
				}
				if (allow.run) {
					if (allow.run === "*") {
						allowAllRun = true
					} else {
						allowRun.push(...allow.run)
					}
				}
				if (allow.ffi) {
					if (allow.ffi === "*") {
						allowAllFfi = true
					} else {
						allowFfi.push(...allow.ffi)
					}
				}
				if (allow.sys) {
					if (allow.sys === "*") {
						allowAllSys = true
					} else {
						allowSys.push(...allow.sys)
					}
				}
			}
		}
		if (perm.deny) {
			for (const deny of perm.deny) {
				if (deny.env) {
					if (deny.env === "*") {
						denyAllEnv = true
					} else {
						denyEnv.push(...deny.env)
					}
				}
				if (deny.net) {
					if (deny.net === "*") {
						denyAllNet = true
					} else {
						denyNet.push(...deny.net)
					}
				}
				if (deny.read) {
					if (deny.read === "*") {
						denyAllRead = true
					} else {
						denyRead.push(...deny.read)
					}
				}
				if (deny.write) {
					if (deny.write === "*") {
						denyAllWrite = true
					} else {
						denyWrite.push(...deny.write)
					}
				}
				if (deny.run) {
					if (deny.run === "*") {
						denyAllRun = true
					} else {
						denyRun.push(...deny.run)
					}
				}
				if (deny.ffi) {
					if (deny.ffi === "*") {
						denyAllFfi = true
					} else {
						denyFfi.push(...deny.ffi)
					}
				}
				if (deny.sys) {
					if (deny.sys === "*") {
						denyAllSys = true
					} else {
						denySys.push(...deny.sys)
					}
				}
			}
		}
	}
	/* ------------- Translate Paths for read and write Permissions ------------- */
	allowRead = await Promise.all(
		allowRead.map(async (p) =>
			pathStartsWithAlias(p) ? await translateScopeToPath(p, extensionDir) : p
		)
	)

	if (config.allowRead) {
		config.allowRead = await Promise.all(
			config.allowRead.map(async (p) => await translateScopeToPath(p, extensionDir))
		)
	}

	if (config.denyRead) {
		config.denyRead = await Promise.all(
			config.denyRead.map(async (p) => await translateScopeToPath(p, extensionDir))
		)
	}

	if (config.allowWrite) {
		config.allowWrite = await Promise.all(
			config.allowWrite.map(async (p) => await translateScopeToPath(p, extensionDir))
		)
	}

	if (config.denyWrite) {
		config.denyWrite = await Promise.all(
			config.denyWrite.map(async (p) => await translateScopeToPath(p, extensionDir))
		)
	}

	denyRead = await Promise.all(
		denyRead.map(async (p) =>
			pathStartsWithAlias(p) ? await translateScopeToPath(p, extensionDir) : p
		)
	)

	allowWrite = await Promise.all(
		allowWrite.map(async (p) =>
			pathStartsWithAlias(p) ? await translateScopeToPath(p, extensionDir) : p
		)
	)

	denyWrite = await Promise.all(
		denyWrite.map(async (p) =>
			pathStartsWithAlias(p) ? await translateScopeToPath(p, extensionDir) : p
		)
	)

	// now we have command requested permissions, we need to compare with permissions defined in manifest
	/* ----------------------- Check Allow All Permissions ---------------------- */

	if (config.allowAllEnv && !allowAllEnv) {
		throw new Error("allowAllEnv is not allowed")
	}
	if (config.allowAllNet && !allowAllNet) {
		throw new Error("allowAllNet is not allowed")
	}
	if (config.allowAllRead && !allowAllRead) {
		throw new Error("allowAllRead is not allowed")
	}
	if (config.allowAllWrite && !allowAllWrite) {
		throw new Error("allowAllWrite is not allowed")
	}
	if (config.allowAllRun && !allowAllRun) {
		throw new Error("allowAllRun is not allowed")
	}
	if (config.allowAllFfi && !allowAllFfi) {
		throw new Error("allowAllFfi is not allowed")
	}
	if (config.allowAllSys && !allowAllSys) {
		throw new Error("allowAllSys is not allowed")
	}

	function isSubsetOf<T>(subset: T[] | undefined, superset: T[]): boolean {
		if (!subset) {
			// if subset is undefined, this means extension didn't request this permission
			return true
		}
		return subset.every((item) => superset.includes(item))
	}

	function getDisallowed(allowEnv: string[], userAllow: string[]): string[] {
		return userAllow.filter((env) => !allowEnv?.includes(env))
	}

	if (!allowAllEnv && !isSubsetOf(config.allowEnv, allowEnv)) {
		throw new Error(`allowEnv is not allowed: ${getDisallowed(allowEnv, config.allowEnv ?? [])}`)
	}
	if (!allowAllNet && !isSubsetOf(config.allowNet, allowNet)) {
		throw new Error(`allowNet is not allowed: ${getDisallowed(allowNet, config.allowNet ?? [])}`)
	}
	if (!allowAllRead && !isSubsetOf(config.allowRead, allowRead)) {
		throw new Error(`allowRead is not allowed: ${getDisallowed(allowRead, config.allowRead ?? [])}`)
	}
	if (!allowAllWrite && !isSubsetOf(config.allowWrite, allowWrite)) {
		throw new Error(
			`allowWrite is not allowed: ${getDisallowed(allowWrite, config.allowWrite ?? [])}`
		)
	}
	if (!allowAllRun && !isSubsetOf(config.allowRun, allowRun)) {
		throw new Error(`allowRun is not allowed: ${getDisallowed(allowRun, config.allowRun ?? [])}`)
	}
	if (!allowAllFfi && !isSubsetOf(config.allowFfi, allowFfi)) {
		throw new Error(`allowFfi is not allowed: ${getDisallowed(allowFfi, config.allowFfi ?? [])}`)
	}
	if (!allowAllSys && !isSubsetOf(config.allowSys, allowSys)) {
		throw new Error(`allowSys is not allowed: ${getDisallowed(allowSys, config.allowSys ?? [])}`)
	}
}

/* -------------------------------------------------------------------------- */
/*                 Translate Deno Command to a regular command                */
/* -------------------------------------------------------------------------- */

/**
 * Translate deno script command to a regular shell command
 * @param scriptPath
 * @param config
 * @returns
 */
export async function translateDenoCommand(
	scriptPath: string,
	config: Partial<DenoRunConfig> & SpawnOptions,
	args: string[],
	extensionDir: string
): Promise<{
	program: string
	args: string[]
	options: InternalSpawnOptions
}> {
	const program = "deno" // TODO: support custom deno path
	const shellArgs: string[] = ["run"]
	/* ----------------------------------- Env ---------------------------------- */
	if (config.allowAllEnv) {
		shellArgs.push("--allow-env")
	} else if (config.allowEnv) {
		const allowEnvStr = config.allowEnv.join(",")
		shellArgs.push(`--allow-env=${allowEnvStr}`)
	}
	if (config.denyAllEnv) {
		shellArgs.push("--deny-env")
	} else if (config.denyEnv) {
		const denyEnvStr = config.denyEnv.join(",")
		shellArgs.push(`--deny-env=${denyEnvStr}`)
	}
	/* ----------------------------------- Net ---------------------------------- */
	if (config.allowAllNet) {
		shellArgs.push("--allow-net")
	} else if (config.allowNet) {
		const allowNetStr = config.allowNet.join(",")
		shellArgs.push(`--allow-net=${allowNetStr}`)
	}
	if (config.denyAllNet) {
		shellArgs.push("--deny-net")
	} else if (config.denyNet) {
		const denyNetStr = config.denyNet.join(",")
		shellArgs.push(`--deny-net=${denyNetStr}`)
	}
	/* ----------------------------------- Read ---------------------------------- */
	if (config.allowAllRead) {
		shellArgs.push("--allow-read")
	} else if (config.allowRead) {
		const allowReadStr = config.allowRead.join(",")
		shellArgs.push(`--allow-read=${allowReadStr}`)
	}
	if (config.denyAllRead) {
		shellArgs.push("--deny-read")
	} else if (config.denyRead) {
		const denyReadStr = config.denyRead.join(",")
		shellArgs.push(`--deny-read=${denyReadStr}`)
	}
	/* ----------------------------------- Write ---------------------------------- */
	if (config.allowAllWrite) {
		shellArgs.push("--allow-write")
	} else if (config.allowWrite) {
		const allowWriteStr = config.allowWrite.join(",")
		shellArgs.push(`--allow-write=${allowWriteStr}`)
	}
	if (config.denyAllWrite) {
		shellArgs.push("--deny-write")
	} else if (config.denyWrite) {
		const denyWriteStr = config.denyWrite.join(",")
		shellArgs.push(`--deny-write=${denyWriteStr}`)
	}
	/* ----------------------------------- Run ---------------------------------- */
	if (config.allowAllRun) {
		shellArgs.push("--allow-run")
	} else if (config.allowRun) {
		const allowRunStr = config.allowRun.join(",")
		shellArgs.push(`--allow-run=${allowRunStr}`)
	}
	if (config.denyAllRun) {
		shellArgs.push("--deny-run")
	} else if (config.denyRun) {
		const denyRunStr = config.denyRun.join(",")
		shellArgs.push(`--deny-run=${denyRunStr}`)
	}
	/* ----------------------------------- Ffi ---------------------------------- */
	if (config.allowAllFfi) {
		shellArgs.push("--allow-ffi")
	} else if (config.allowFfi) {
		const allowFfiStr = config.allowFfi.join(",")
		shellArgs.push(`--allow-ffi=${allowFfiStr}`)
	}
	if (config.denyAllFfi) {
		shellArgs.push("--deny-ffi")
	} else if (config.denyFfi) {
		const denyFfiStr = config.denyFfi.join(",")
		shellArgs.push(`--deny-ffi=${denyFfiStr}`)
	}
	/* ----------------------------------- Sys ---------------------------------- */
	if (config.allowAllSys) {
		shellArgs.push("--allow-sys")
	} else if (config.allowSys) {
		const allowSysStr = config.allowSys.join(",")
		shellArgs.push(`--allow-sys=${allowSysStr}`)
	}
	if (config.denyAllSys) {
		shellArgs.push("--deny-sys")
	} else if (config.denySys) {
		const denySysStr = config.denySys.join(",")
		shellArgs.push(`--deny-sys=${denySysStr}`)
	}
	/* ----------------------------------- Script ---------------------------------- */
	scriptPath = await translateScopeToPath(scriptPath, extensionDir)
	shellArgs.push(scriptPath)
	if (args) {
		shellArgs.push(...args)
	}

	return {
		program,
		args: shellArgs,
		options: {
			cwd: config.cwd,
			env: config.env,
			encoding: config.encoding
		}
	}
}
