import * as fs from "fs"
import * as path from "path"
import { execSync } from "child_process"

import { ViewsContainer, Views, Menus, Configuration, Keybindings, contributesSchema } from "./types.js"

const INCLUDES_LOCALES = ["zh-CN", "zh-TW", "en"]

function copyDir(srcDir: string, dstDir: string, count: number): number {
	const entries = fs.readdirSync(srcDir, { withFileTypes: true })

	for (const entry of entries) {
		const srcPath = path.join(srcDir, entry.name)
		const dstPath = path.join(dstDir, entry.name)

		if (entry.isDirectory()) {
			if (!INCLUDES_LOCALES.includes(entry.name) && entry.parentPath.includes("locales")) {
				console.log(`Skipping excluded locale: ${entry.name}`)
				continue
			}
			fs.mkdirSync(dstPath, { recursive: true })
			count = copyDir(srcPath, dstPath, count)
		} else {
			count = count + 1
			fs.copyFileSync(srcPath, dstPath)
		}
	}

	return count
}

function rmDir(dirPath: string, maxRetries: number = 5): void {
	for (let attempt = 1; attempt <= maxRetries; attempt++) {
		try {
			fs.rmSync(dirPath, { recursive: true, force: true })
			return
		} catch (error) {
			const isLastAttempt = attempt === maxRetries

			const isRetryableError =
				error instanceof Error &&
				"code" in error &&
				(error.code === "ENOTEMPTY" ||
					error.code === "EBUSY" ||
					error.code === "EPERM" ||
					error.code === "EACCES")

			if (isLastAttempt) {
				// On the last attempt, try alternative cleanup methods.
				try {
					console.warn(`[rmDir] Final attempt using alternative cleanup for ${dirPath}`)

					// Try to clear readonly flags on Windows.
					if (process.platform === "win32") {
						try {
							execSync(`attrib -R "${dirPath}\\*.*" /S /D`, { stdio: "ignore" })
						} catch {
							// Ignore attrib errors.
						}
					}
					fs.rmSync(dirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 })
					return
				} catch (finalError) {
					console.error(`[rmDir] Failed to remove ${dirPath} after ${maxRetries} attempts:`, finalError)
					throw finalError
				}
			}

			if (!isRetryableError) {
				throw error // Re-throw if it's not a retryable error.
			}

			// Wait with exponential backoff before retrying, with longer delays for Windows.
			const baseDelay = process.platform === "win32" ? 200 : 100
			const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), 2000) // Cap at 2s
			console.warn(`[rmDir] Attempt ${attempt} failed for ${dirPath}, retrying in ${delay}ms...`)

			// Synchronous sleep for simplicity in build scripts.
			const start = Date.now()

			while (Date.now() - start < delay) {
				/* Busy wait */
			}
		}
	}
}

type CopyPathOptions = {
	optional?: boolean
}

export function copyPaths(copyPaths: [string, string, CopyPathOptions?][], srcDir: string, dstDir: string) {
	copyPaths.forEach(([srcRelPath, dstRelPath, options = {}]) => {
		try {
			const stats = fs.lstatSync(path.join(srcDir, srcRelPath))

			if (stats.isDirectory()) {
				if (fs.existsSync(path.join(dstDir, dstRelPath))) {
					rmDir(path.join(dstDir, dstRelPath))
				}

				fs.mkdirSync(path.join(dstDir, dstRelPath), { recursive: true })

				const count = copyDir(path.join(srcDir, srcRelPath), path.join(dstDir, dstRelPath), 0)
				console.log(`[copyPaths] Copied ${count} files from ${srcRelPath} to ${dstRelPath}`)
			} else {
				fs.copyFileSync(path.join(srcDir, srcRelPath), path.join(dstDir, dstRelPath))
				console.log(`[copyPaths] Copied ${srcRelPath} to ${dstRelPath}`)
			}
		} catch (error) {
			if (options.optional) {
				console.warn(`[copyPaths] Optional file not found: ${srcRelPath}`)
			} else {
				throw error
			}
		}
	})
}

export function copyWasms(srcDir: string, distDir: string): void {
	const nodeModulesDir = path.join(srcDir, "node_modules")

	fs.mkdirSync(distDir, { recursive: true })

	// Tiktoken WASM file.
	fs.copyFileSync(
		path.join(nodeModulesDir, "tiktoken", "lite", "tiktoken_bg.wasm"),
		path.join(distDir, "tiktoken_bg.wasm"),
	)

	console.log(`[copyWasms] Copied tiktoken WASMs to ${distDir}`)

	// Also copy Tiktoken WASMs to the workers directory.
	const workersDir = path.join(distDir, "workers")
	fs.mkdirSync(workersDir, { recursive: true })

	fs.copyFileSync(
		path.join(nodeModulesDir, "tiktoken", "lite", "tiktoken_bg.wasm"),
		path.join(workersDir, "tiktoken_bg.wasm"),
	)

	console.log(`[copyWasms] Copied tiktoken WASMs to ${workersDir}`)

	// Main tree-sitter WASM file.
	fs.copyFileSync(
		path.join(nodeModulesDir, "web-tree-sitter", "tree-sitter.wasm"),
		path.join(distDir, "tree-sitter.wasm"),
	)

	console.log(`[copyWasms] Copied tree-sitter.wasm to ${distDir}`)

	// Copy language-specific WASM files.
	const languageWasmDir = path.join(nodeModulesDir, "tree-sitter-wasms", "out")

	if (!fs.existsSync(languageWasmDir)) {
		throw new Error(`Directory does not exist: ${languageWasmDir}`)
	}

	// Dynamically read all WASM files from the directory instead of using a hardcoded list.
	const wasmFiles = fs.readdirSync(languageWasmDir).filter((file) => file.endsWith(".wasm"))

	wasmFiles.forEach((filename) => {
		fs.copyFileSync(path.join(languageWasmDir, filename), path.join(distDir, filename))
	})

	console.log(`[copyWasms] Copied ${wasmFiles.length} tree-sitter language wasms to ${distDir}`)

	// Copy esbuild-wasm files for custom tool transpilation (cross-platform).
	copyEsbuildWasmFiles(nodeModulesDir, distDir)

	// Copy @roo-code/types package for custom tool compilation.
	// Note: @roo-code/types is built with zod bundled in, so we don't need to copy zod separately.
	copyTypesPackage(srcDir, distDir)
}

/**
 * Copy esbuild-wasm files to the dist/bin directory.
 *
 * This function copies the esbuild-wasm CLI and WASM binary, which provides
 * a cross-platform esbuild implementation that works on all platforms.
 *
 * Files copied:
 * - bin/esbuild (Node.js CLI script)
 * - esbuild.wasm (WASM binary)
 * - wasm_exec_node.js (Go WASM runtime for Node.js)
 * - wasm_exec.js (Go WASM runtime dependency)
 */
function copyEsbuildWasmFiles(nodeModulesDir: string, distDir: string): void {
	const esbuildWasmDir = path.join(nodeModulesDir, "esbuild-wasm")

	if (!fs.existsSync(esbuildWasmDir)) {
		throw new Error(`Directory does not exist: ${esbuildWasmDir}`)
	}

	// Create bin directory in dist.
	const binDir = path.join(distDir, "bin")
	fs.mkdirSync(binDir, { recursive: true })

	// Files to copy - the esbuild CLI script expects wasm_exec_node.js and esbuild.wasm
	// to be one directory level up from the bin directory (i.e., in distDir directly).
	// wasm_exec_node.js requires wasm_exec.js, so we need to copy that too.
	const filesToCopy = [
		{ src: path.join(esbuildWasmDir, "bin", "esbuild"), dest: path.join(binDir, "esbuild") },
		{ src: path.join(esbuildWasmDir, "esbuild.wasm"), dest: path.join(distDir, "esbuild.wasm") },
		{ src: path.join(esbuildWasmDir, "wasm_exec_node.js"), dest: path.join(distDir, "wasm_exec_node.js") },
		{ src: path.join(esbuildWasmDir, "wasm_exec.js"), dest: path.join(distDir, "wasm_exec.js") },
	]

	for (const { src, dest } of filesToCopy) {
		fs.copyFileSync(src, dest)

		// Make CLI executable.
		if (src.endsWith("esbuild")) {
			try {
				fs.chmodSync(dest, 0o755)
			} catch {
				// Ignore chmod errors on Windows.
			}
		}
	}

	console.log(`[copyWasms] Copied ${filesToCopy.length} esbuild-wasm files to ${distDir}`)
}

/**
 * Copy @roo-code/types package files for custom tool compilation.
 *
 * This function copies the compiled @roo-code/types package so that custom tools
 * can import it during runtime compilation. This enables users to write custom
 * tools that use TypeScript and Zod schemas even after the extension is packaged.
 *
 * Files copied:
 * - dist/index.js (ESM version)
 * - dist/index.js.map (source map)
 * - dist/index.d.ts (TypeScript definitions)
 * - dist/index.cjs (CommonJS version)
 * - dist/index.d.cts (CommonJS type definitions)
 */
function copyTypesPackage(srcDir: string, distDir: string): void {
	// Find the types package - try multiple locations
	const possiblePaths = [
		// Monorepo packages directory (relative to src)
		path.join(srcDir, "..", "packages", "types"),
		// Parent's packages directory
		path.join(srcDir, "..", "..", "packages", "types"),
	]

	let typesPackageDir: string | null = null
	for (const possiblePath of possiblePaths) {
		if (fs.existsSync(possiblePath)) {
			typesPackageDir = possiblePath
			break
		}
	}

	if (!typesPackageDir) {
		throw new Error("Could not find @roo-code/types package directory")
	}

	const typesDistDir = path.join(typesPackageDir, "dist")
	if (!fs.existsSync(typesDistDir)) {
		throw new Error(`@roo-code/types dist directory does not exist: ${typesDistDir}. Run 'pnpm build' first.`)
	}

	// Create packages/types/dist directory structure in dist
	const targetDir = path.join(distDir, "packages", "types", "dist")
	fs.mkdirSync(targetDir, { recursive: true })

	// Files to copy (exclude sourcemap files in production to reduce bundle size)
	const filesToCopy = ["index.js", "index.d.ts", "index.cjs", "index.d.cts"]

	let copiedCount = 0
	for (const file of filesToCopy) {
		const srcFile = path.join(typesDistDir, file)
		const destFile = path.join(targetDir, file)

		if (fs.existsSync(srcFile)) {
			fs.copyFileSync(srcFile, destFile)
			copiedCount++
		}
	}

	if (copiedCount === 0) {
		throw new Error(`No files copied from @roo-code/types dist directory: ${typesDistDir}`)
	}

	console.log(`[copyWasms] Copied ${copiedCount} @roo-code/types files to ${targetDir}`)
}

export function copyLocales(srcDir: string, distDir: string): void {
	const destDir = path.join(distDir, "i18n", "locales")
	const costrictDestDir = path.join(distDir, "i18n", "costrict-i18n", "locales")
	const dirs = [
		[destDir, path.join(srcDir, "i18n", "locales")],
		[costrictDestDir, path.join(srcDir, "i18n", "costrict-i18n", "locales")],
	] as Array<[string, string]>

	for (const dirInfo of dirs) {
		fs.mkdirSync(dirInfo[0], { recursive: true })
		const count = copyDir(dirInfo[1], dirInfo[0], 0)
		console.log(`[copyLocales] Copied ${count} locale files to ${dirInfo[0]}`)
	}
}

export function setupLocaleWatcher(srcDir: string, distDir: string) {
	const localesDir = path.join(srcDir, "i18n", "locales")

	if (!fs.existsSync(localesDir)) {
		console.warn(`Cannot set up watcher: Source locales directory does not exist: ${localesDir}`)
		return
	}

	console.log(`Setting up watcher for locale files in ${localesDir}`)

	let debounceTimer: NodeJS.Timeout | null = null

	const debouncedCopy = () => {
		if (debounceTimer) {
			clearTimeout(debounceTimer)
		}

		// Wait 300ms after last change before copying.
		debounceTimer = setTimeout(() => {
			console.log("Locale files changed, copying...")
			copyLocales(srcDir, distDir)
		}, 300)
	}

	try {
		fs.watch(localesDir, { recursive: true }, (_eventType, filename) => {
			if (filename && filename.endsWith(".json")) {
				console.log(`Locale file ${filename} changed, triggering copy...`)
				debouncedCopy()
			}
		})
		console.log("Watcher for locale files is set up")
	} catch (error) {
		console.error(
			`Error setting up watcher for ${localesDir}:`,
			error instanceof Error ? error.message : "Unknown error",
		)
	}
}

export function generatePackageJson({
	packageJson: { contributes, ...packageJson },
	overrideJson,
	substitution,
}: {
	packageJson: Record<string, any> // eslint-disable-line @typescript-eslint/no-explicit-any
	overrideJson: Record<string, any> // eslint-disable-line @typescript-eslint/no-explicit-any
	substitution: [string, string]
}) {
	const { viewsContainers, views, commands, menus, submenus, keybindings, configuration } =
		contributesSchema.parse(contributes)
	const [from, to] = substitution

	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	const result: Record<string, any> = {
		...packageJson,
		...overrideJson,
		contributes: {
			viewsContainers: transformArrayRecord<ViewsContainer>(viewsContainers, from, to, ["id"]),
			views: transformArrayRecord<Views>(views, from, to, ["id"]),
			commands: transformArray(commands, from, to, "command"),
			menus: transformArrayRecord<Menus>(menus, from, to, ["command", "submenu", "when"]),
			submenus: transformArray(submenus, from, to, "id"),
			configuration: {
				title: configuration.title,
				properties: transformRecord<Configuration["properties"]>(configuration.properties, from, to),
			},
		},
	}

	// Only add keybindings if they exist
	if (keybindings) {
		result.contributes.keybindings = transformArray<Keybindings>(keybindings, from, to, "command")
	}

	return result
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function transformArrayRecord<T>(obj: Record<string, any[]>, from: string, to: string, props: string[]): T {
	return Object.entries(obj).reduce(
		(acc, [key, ary]) => ({
			...acc,
			[key.replaceAll(from, to)]: ary.map((item) => {
				const transformedItem = { ...item }

				for (const prop of props) {
					if (prop in item && typeof item[prop] === "string") {
						transformedItem[prop] = item[prop].replaceAll(from, to)
					}
				}

				return transformedItem
			}),
		}),
		{} as T,
	)
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function transformArray<T>(arr: any[], from: string, to: string, idProp: string): T[] {
	return arr.map(({ [idProp]: id, ...rest }) => ({
		[idProp]: id.replaceAll(from, to),
		...rest,
	}))
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function transformRecord<T>(obj: Record<string, any>, from: string, to: string): T {
	return Object.entries(obj).reduce(
		(acc, [key, value]) => ({
			...acc,
			[key.replaceAll(from, to)]: value,
		}),
		{} as T,
	)
}
