// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
//
// SPDX-License-Identifier: GPL-3.0-or-later

import { accessSync, constants, realpathSync } from "node:fs";
import { dirname, isAbsolute, resolve as resolvePath, sep } from "node:path";
import { ToolInputError } from "../../util/errors.js";

const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;

function normalizeAtPrefix(filePath: string): string {
	return filePath.startsWith("@") ? filePath.slice(1) : filePath;
}

function fileExists(path: string): boolean {
	try {
		accessSync(path, constants.F_OK);
		return true;
	} catch {
		return false;
	}
}

function tryMacOSScreenshotPath(filePath: string): string {
	return filePath.replace(/ (AM|PM)\./g, "\u202F$1.");
}

function tryNFDVariant(filePath: string): string {
	return filePath.normalize("NFD");
}

function tryCurlyQuoteVariant(filePath: string): string {
	return filePath.replace(/'/g, "\u2019");
}

export function expandPath(filePath: string): string {
	let result = filePath.replace(UNICODE_SPACES, " ");
	result = normalizeAtPrefix(result);

	// NOTE: tilde expansion is intentionally omitted.
	// In a workspace-sandboxed context, expanding ~ to the user's home
	// directory would bypass workspace containment. Tildes are treated
	// as literal path characters.

	return result;
}

export function resolveToCwd(filePath: string, cwd: string): string {
	const expanded = expandPath(filePath);
	if (isAbsolute(expanded)) {
		return expanded;
	}
	return resolvePath(cwd, expanded);
}

export function resolveReadPath(filePath: string, cwd: string): string {
	const resolved = resolveToCwd(filePath, cwd);

	if (fileExists(resolved)) {
		return resolved;
	}

	const variants = [tryNFDVariant, tryMacOSScreenshotPath, tryCurlyQuoteVariant];
	for (const variant of variants) {
		const candidate = variant(resolved);
		if (candidate !== resolved && fileExists(candidate)) {
			return candidate;
		}
	}

	return resolved;
}

/**
 * Resolve the real path of `p`, following symlinks. If `p` does not exist,
 * walk up to the nearest existing ancestor, resolve *that*, and re-append
 * the remaining segments. This lets us validate write targets that don't
 * exist yet while still catching symlink escapes in any ancestor directory.
 */
function safeRealpath(p: string): string {
	try {
		return realpathSync(p);
	} catch (err: any) {
		if (err?.code === "ENOENT") {
			const parent = dirname(p);
			if (parent === p) {
				// filesystem root — nothing more to resolve
				return p;
			}
			const realParent = safeRealpath(parent);
			const tail = p.slice(parent.length);
			return realParent + tail;
		}
		throw err;
	}
}

export function ensureWorkspacePath(workspacePath: string, targetPath: string): string {
	const resolved = resolvePath(workspacePath, targetPath);

	// Quick textual check first (catches the common case cheaply)
	const root = workspacePath.endsWith(sep) ? workspacePath : `${workspacePath}${sep}`;
	if (resolved !== workspacePath && !resolved.startsWith(root)) {
		throw new ToolInputError(`Path escapes workspace: ${targetPath}`);
	}

	// Resolve symlinks to catch symlink-based escapes
	const realWorkspace = safeRealpath(workspacePath);
	const realTarget = safeRealpath(resolved);
	const realRoot = realWorkspace.endsWith(sep) ? realWorkspace : `${realWorkspace}${sep}`;

	if (realTarget !== realWorkspace && !realTarget.startsWith(realRoot)) {
		throw new ToolInputError(`Path escapes workspace via symlink: ${targetPath}`);
	}

	return resolved;
}
