import { createLogger } from "../../../utils/logger/index.js";
import { init_logger } from "../../../utils/logger.js";
import { init_agent_scope, listAgentEntries, normalizeAgentId, resolveAgentHomeDir, resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../../agent/agent-scope.js";
import { extractProfileAgentId } from "../../../config/agent-profile.js";
import { validateWritePath } from "../../../agent/sandbox/path-policy.js";
import { isPathUnderWorkspace, resolveWorkspaceSafePath, toWorkspaceRelativePosix } from "../../workspace-editor-path.js";
import { resolveSafeInboundFilePath } from "../../../channels/attachments/inbound-persist.js";
import { getWorkspacePath } from "../../../config/workspace-path-helpers.js";
import { resolveSafeTtsFilePath } from "../../../channels/attachments/outbound-tts-persist.js";
import { buildFilePathClassifierContext, classifyFileLocation, displayNameForPath, fileRefSessionKeysMatch, resolveFileReferenceCandidate } from "../../file-path-classifier.js";
import { fileReferenceRegistry } from "../../file-reference-registry.js";
import { resolveHeartbeatMdPath } from "../../workspace-heartbeat-path.js";
import { listWorkspaceRelativeFilesFsFallback } from "../../workspace-fs-file-list.js";
import { runRipgrepInDirectory, runRipgrepListFiles } from "../../workspace-ripgrep.js";
import { randomUUID } from "node:crypto";
import { constants } from "node:fs";
import { copyFile, link, mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
import { basename, dirname, join, resolve } from "node:path";
//#region src/gateway/hono/routes/workspace.ts
init_agent_scope();
init_logger();
const log = createLogger("HonoApp");
/** Agent home for persisted `inbound/` and `tts/` attachments (matches `persistOutboundTtsAudio` / `prepareInboundAttachments`). */
function resolvePersistedAttachmentAgentHome(cfg, sessionKeyRaw) {
	const sk = typeof sessionKeyRaw === "string" ? sessionKeyRaw.trim() : "";
	return resolveAgentHomeDir(cfg, sk ? extractProfileAgentId(sk, cfg) : resolveDefaultAgentId(cfg));
}
const FILE_SEARCH_MAX_LIMIT = 50;
/** Subsequence fuzzy match: all query chars appear in order in `candidate` (case-insensitive). */
function fuzzySubsequenceScore(query, candidate) {
	const q = query.toLowerCase();
	const c = candidate.toLowerCase();
	if (q.length === 0) return 0;
	let qi = 0;
	for (let ci = 0; ci < c.length && qi < q.length; ci++) if (c[ci] === q[qi]) qi++;
	if (qi < q.length) return null;
	const base = c.split("/").pop() ?? c;
	let score = 10;
	if (c.startsWith(q)) score += 40;
	if (base.startsWith(q)) score += 35;
	else if (base.includes(q)) score += 20;
	else if (c.includes(q)) score += 10;
	score -= c.length * 1e-4;
	return score;
}
async function fuzzySearchWorkspaceFiles(workspaceRoot, query, limit) {
	let files = await runRipgrepListFiles(workspaceRoot);
	if (files.length === 0) {
		files = await listWorkspaceRelativeFilesFsFallback(workspaceRoot, 12e4);
		if (files.length > 0) log.debug({
			workspaceRoot,
			fileCount: files.length
		}, "workspace files/search: file list from fs walk (ripgrep unavailable or returned empty)");
	}
	const q = query.trim();
	const capped = Math.min(Math.max(limit, 1), FILE_SEARCH_MAX_LIMIT);
	const rows = [];
	if (!q) {
		const sorted = [...files].sort((a, b) => a.localeCompare(b));
		for (const rel of sorted.slice(0, capped)) {
			const name = rel.split("/").pop() ?? rel;
			rows.push({
				name,
				path: rel,
				isDirectory: false,
				score: 0
			});
		}
		return rows;
	}
	for (const rel of files) {
		const name = rel.split("/").pop() ?? rel;
		const scorePath = fuzzySubsequenceScore(q, rel);
		const scoreName = fuzzySubsequenceScore(q, name);
		const score = Math.max(scorePath ?? -Infinity, scoreName ?? -Infinity);
		if (score === -Infinity) continue;
		rows.push({
			name,
			path: rel,
			isDirectory: false,
			score
		});
	}
	rows.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
	return rows.slice(0, capped).map(({ name, path, isDirectory }) => ({
		name,
		path,
		isDirectory
	}));
}
function isKnownEditorAgentId(cfg, id) {
	const n = normalizeAgentId(id);
	if (n === resolveDefaultAgentId(cfg)) return true;
	return listAgentEntries(cfg).some((e) => normalizeAgentId(e.id) === n);
}
function resolveEditorWorkspaceRoot(cfg, agentIdRaw) {
	const trimmed = typeof agentIdRaw === "string" ? agentIdRaw.trim() : "";
	if (!trimmed) {
		const root = getWorkspacePath(cfg);
		if (!root) return {
			ok: false,
			message: "Workspace not configured"
		};
		return {
			ok: true,
			root
		};
	}
	const id = normalizeAgentId(trimmed);
	if (!isKnownEditorAgentId(cfg, id)) return {
		ok: false,
		message: "Unknown agent"
	};
	return {
		ok: true,
		root: resolveAgentWorkspaceDir(cfg, id)
	};
}
/** Prefer `sessionKey` (per-session workspace override) over `agentId`. */
async function resolveEditorWorkspaceRootAsync(service, cfg, sessionKeyRaw, agentIdRaw) {
	const sk = typeof sessionKeyRaw === "string" ? sessionKeyRaw.trim() : "";
	if (sk) try {
		return {
			ok: true,
			root: await service.sessions.getEffectiveWorkspacePath(sk)
		};
	} catch (err) {
		const em = err instanceof Error ? err.message : String(err);
		log.warn({
			err,
			sessionKey: sk
		}, "Session workspace root resolution failed");
		return {
			ok: false,
			message: em || "Session workspace resolution failed"
		};
	}
	return resolveEditorWorkspaceRoot(cfg, agentIdRaw);
}
function resolveWorkspaceImportConfig(cfg) {
	const raw = cfg.workspace?.import;
	return {
		targetDir: raw?.targetDir?.trim() || "imports",
		maxBytes: raw?.maxBytes ?? 104857600,
		allowOverwrite: raw?.allowOverwrite ?? true
	};
}
/** Strip path separators, NULs and control chars from a basename so it stays in the destination dir. */
function sanitizeImportBasename(name) {
	return name.replace(/[\\/]/g, "").replace(/[\x00-\x1f\x7f]/g, "").trim();
}
/**
* Race-safe target picker for the `rename`-on-conflict strategy. Uses `link(tmp, target)`
* which atomically fails with EEXIST when the candidate is taken; on success the tmp
* file is left in place for the caller to unlink. Returns the linked target path.
*/
async function pickAvailableTargetWithLink(tmpAbs, initialDestAbs, maxAttempts = 1e3) {
	const dir = dirname(initialDestAbs);
	const original = basename(initialDestAbs);
	const dotIdx = original.lastIndexOf(".");
	const stem = dotIdx > 0 ? original.slice(0, dotIdx) : original;
	const ext = dotIdx > 0 ? original.slice(dotIdx) : "";
	for (let attempt = 1; attempt <= maxAttempts; attempt++) {
		const candidate = attempt === 1 ? initialDestAbs : join(dir, `${stem}-${attempt}${ext}`);
		try {
			await link(tmpAbs, candidate);
			return {
				ok: true,
				path: candidate,
				attempts: attempt
			};
		} catch (err) {
			if (err?.code !== "EEXIST") throw err;
		}
	}
	return {
		ok: false,
		attempts: maxAttempts
	};
}
function fileReferenceCapabilities(scope, isDirectory, locationKind) {
	if (scope === "workspace") return isDirectory ? [
		"openExternal",
		"revealInFolder",
		"copyPath"
	] : [
		"preview",
		"edit",
		"openExternal",
		"revealInFolder",
		"copyPath"
	];
	if (scope === "external" || scope === "agent-profile" || scope === "session-artifact") {
		const base = [
			"openExternal",
			"revealInFolder",
			"copyPath"
		];
		if (!isDirectory && locationKind !== "xopc-config") base.push("importToWorkspace");
		return base;
	}
	if (scope === "missing") return ["copyPath"];
	return [];
}
function isFileReferenceAction(action) {
	return action === "openExternal" || action === "revealInFolder";
}
function registerWorkspaceRoutes(authenticated, deps) {
	const { service } = deps;
	authenticated.get("/api/workspace/inbound-file", async (c) => {
		const rel = c.req.query("rel");
		if (!rel || typeof rel !== "string") return c.json({
			ok: false,
			error: { message: "Missing rel" }
		}, 400);
		const cfg = service.currentConfig;
		const abs = resolveSafeInboundFilePath({ agentHome: resolvePersistedAttachmentAgentHome(cfg, c.req.query("sessionKey")) }, rel);
		if (!abs) return c.json({
			ok: false,
			error: { message: "Forbidden" }
		}, 403);
		try {
			const buf = await readFile(abs);
			const contentType = {
				pdf: "application/pdf",
				png: "image/png",
				jpg: "image/jpeg",
				jpeg: "image/jpeg",
				webp: "image/webp",
				gif: "image/gif",
				md: "text/markdown",
				txt: "text/plain",
				json: "application/json",
				html: "text/html",
				css: "text/css",
				js: "text/javascript",
				ts: "text/typescript",
				webm: "audio/webm",
				ogg: "audio/ogg",
				opus: "audio/ogg",
				mp3: "audio/mpeg",
				wav: "audio/wav",
				m4a: "audio/mp4"
			}[rel.split(".").pop()?.toLowerCase() ?? ""] || "application/octet-stream";
			return new Response(buf, { headers: {
				"Content-Type": contentType,
				"Cache-Control": "private, max-age=3600"
			} });
		} catch {
			return c.json({
				ok: false,
				error: { message: "Not found" }
			}, 404);
		}
	});
	authenticated.get("/api/workspace/tts-file", async (c) => {
		const rel = c.req.query("rel");
		if (!rel || typeof rel !== "string") return c.json({
			ok: false,
			error: { message: "Missing rel" }
		}, 400);
		const cfg = service.currentConfig;
		const abs = resolveSafeTtsFilePath({ agentHome: resolvePersistedAttachmentAgentHome(cfg, c.req.query("sessionKey")) }, rel);
		if (!abs) return c.json({
			ok: false,
			error: { message: "Forbidden" }
		}, 403);
		try {
			const buf = await readFile(abs);
			const contentType = {
				ogg: "audio/ogg",
				opus: "audio/ogg",
				mp3: "audio/mpeg",
				wav: "audio/wav",
				m4a: "audio/mp4"
			}[rel.split(".").pop()?.toLowerCase() ?? ""] || "application/octet-stream";
			return new Response(buf, { headers: {
				"Content-Type": contentType,
				"Cache-Control": "private, max-age=3600"
			} });
		} catch {
			return c.json({
				ok: false,
				error: { message: "Not found" }
			}, 404);
		}
	});
	authenticated.get("/api/workspace/heartbeat-md", async (c) => {
		const abs = resolveHeartbeatMdPath(service.currentConfig);
		if (!abs) return c.json({
			ok: false,
			error: { message: "Workspace not configured" }
		}, 400);
		try {
			const content = await readFile(abs, "utf-8");
			return c.json({
				ok: true,
				payload: {
					content,
					file: "HEARTBEAT.md"
				}
			});
		} catch {
			return c.json({
				ok: true,
				payload: {
					content: "",
					file: "HEARTBEAT.md"
				}
			});
		}
	});
	authenticated.put("/api/workspace/heartbeat-md", async (c) => {
		const abs = resolveHeartbeatMdPath(service.currentConfig);
		if (!abs) return c.json({
			ok: false,
			error: { message: "Workspace not configured" }
		}, 400);
		let body;
		try {
			body = await c.req.json();
		} catch {
			return c.json({
				ok: false,
				error: { message: "Invalid JSON" }
			}, 400);
		}
		const content = typeof body === "object" && body !== null && "content" in body && typeof body.content === "string" ? body.content : "";
		try {
			await writeFile(abs, content, "utf-8");
			return c.json({
				ok: true,
				payload: { file: "HEARTBEAT.md" }
			});
		} catch (err) {
			log.error({
				err,
				path: abs
			}, "Failed to write HEARTBEAT.md");
			return c.json({
				ok: false,
				error: { message: "Write failed" }
			}, 500);
		}
	});
	authenticated.get("/api/workspace/editor/list", async (c) => {
		const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, c.req.query("sessionKey"), c.req.query("agentId"));
		if (ws.ok === false) return c.json({
			ok: false,
			error: { message: ws.message }
		}, 400);
		const workspaceRoot = ws.root;
		const absDir = resolveWorkspaceSafePath(workspaceRoot, typeof c.req.query("dir") === "string" ? c.req.query("dir") : "");
		if (!absDir) return c.json({
			ok: false,
			error: { message: "Invalid path" }
		}, 400);
		let st;
		try {
			st = await stat(absDir);
		} catch {
			return c.json({
				ok: false,
				error: { message: "Not found" }
			}, 404);
		}
		if (!st.isDirectory()) return c.json({
			ok: false,
			error: { message: "Not a directory" }
		}, 400);
		const dirents = await readdir(absDir, { withFileTypes: true });
		const entries = [];
		for (const entry of dirents) {
			if (entry.name.startsWith(".")) continue;
			const fullPath = join(absDir, entry.name);
			if (entry.isDirectory()) entries.push({
				name: entry.name,
				path: toWorkspaceRelativePosix(workspaceRoot, fullPath),
				absolutePath: fullPath,
				isDirectory: true
			});
			else entries.push({
				name: entry.name,
				path: toWorkspaceRelativePosix(workspaceRoot, fullPath),
				absolutePath: fullPath,
				isDirectory: false
			});
		}
		entries.sort((a, b) => {
			if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
			return a.name.localeCompare(b.name);
		});
		return c.json({
			ok: true,
			payload: { entries }
		});
	});
	authenticated.get("/api/workspace/editor/read", async (c) => {
		const pathRel = typeof c.req.query("path") === "string" ? c.req.query("path") : "";
		if (!pathRel.trim()) return c.json({
			ok: false,
			error: { message: "Missing path" }
		}, 400);
		const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, c.req.query("sessionKey"), c.req.query("agentId"));
		if (ws.ok === false) return c.json({
			ok: false,
			error: { message: ws.message }
		}, 400);
		const workspaceRoot = ws.root;
		const abs = resolveWorkspaceSafePath(workspaceRoot, pathRel);
		if (!abs) return c.json({
			ok: false,
			error: { message: "Invalid path" }
		}, 400);
		let st;
		try {
			st = await stat(abs);
		} catch {
			return c.json({
				ok: false,
				error: { message: "Not found" }
			}, 404);
		}
		if (!st.isFile()) return c.json({
			ok: false,
			error: { message: "Not a file" }
		}, 400);
		try {
			const content = await readFile(abs, "utf-8");
			return c.json({
				ok: true,
				payload: {
					content,
					path: toWorkspaceRelativePosix(workspaceRoot, abs),
					absolutePath: abs,
					mtimeMs: st.mtimeMs
				}
			});
		} catch {
			return c.json({
				ok: false,
				error: { message: "Read failed" }
			}, 500);
		}
	});
	/** Read file as raw bytes and return base64 (for PDF/images in workspace preview — avoids UTF-8 corruption). */
	authenticated.get("/api/workspace/editor/read-base64", async (c) => {
		const pathRel = typeof c.req.query("path") === "string" ? c.req.query("path") : "";
		if (!pathRel.trim()) return c.json({
			ok: false,
			error: { message: "Missing path" }
		}, 400);
		const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, c.req.query("sessionKey"), c.req.query("agentId"));
		if (ws.ok === false) return c.json({
			ok: false,
			error: { message: ws.message }
		}, 400);
		const workspaceRoot = ws.root;
		const abs = resolveWorkspaceSafePath(workspaceRoot, pathRel);
		if (!abs) return c.json({
			ok: false,
			error: { message: "Invalid path" }
		}, 400);
		let st;
		try {
			st = await stat(abs);
		} catch {
			return c.json({
				ok: false,
				error: { message: "Not found" }
			}, 404);
		}
		if (!st.isFile()) return c.json({
			ok: false,
			error: { message: "Not a file" }
		}, 400);
		try {
			const buf = await readFile(abs);
			return c.json({
				ok: true,
				payload: {
					contentBase64: buf.toString("base64"),
					path: toWorkspaceRelativePosix(workspaceRoot, abs),
					/** Host absolute path — Electron can open with the default app (shell.openPath). */
					absolutePath: abs,
					mtimeMs: st.mtimeMs
				}
			});
		} catch {
			return c.json({
				ok: false,
				error: { message: "Read failed" }
			}, 500);
		}
	});
	/** Map an absolute host path to a workspace-relative path (if under this session’s workspace). */
	authenticated.get("/api/workspace/editor/resolve-path", async (c) => {
		const raw = c.req.query("absolutePath");
		if (!raw || typeof raw !== "string" || !raw.trim()) return c.json({
			ok: false,
			error: { message: "Missing absolutePath" }
		}, 400);
		const absolutePath = raw.trim();
		const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, c.req.query("sessionKey"), c.req.query("agentId"));
		if (ws.ok === false) return c.json({
			ok: false,
			error: { message: ws.message }
		}, 400);
		const workspaceRoot = ws.root;
		const normalized = resolve(absolutePath);
		if (!isPathUnderWorkspace(workspaceRoot, normalized)) return c.json({
			ok: false,
			error: { message: "Path not under workspace" }
		}, 403);
		const rel = toWorkspaceRelativePosix(workspaceRoot, normalized);
		return c.json({
			ok: true,
			payload: { workspaceRelativePath: rel }
		});
	});
	authenticated.get("/api/workspace/editor/resolve-reference", async (c) => {
		const rawPath = typeof c.req.query("path") === "string" ? c.req.query("path").trim() : "";
		if (!rawPath) return c.json({
			ok: false,
			error: {
				code: "INVALID_PATH",
				message: "Missing path"
			}
		}, 400);
		const sessionKey = typeof c.req.query("sessionKey") === "string" ? c.req.query("sessionKey").trim() : "";
		const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, sessionKey, c.req.query("agentId"));
		if (ws.ok === false) return c.json({
			ok: false,
			error: {
				code: "WORKSPACE_RESOLUTION_FAILED",
				message: ws.message
			}
		}, 400);
		const workspaceRoot = ws.root;
		const classifierCtx = {
			...buildFilePathClassifierContext(service.currentConfig, sessionKey),
			workspaceRoot
		};
		const displayName = displayNameForPath(rawPath);
		const { candidate, invalid } = await resolveFileReferenceCandidate(rawPath, workspaceRoot, classifierCtx);
		if (!candidate || invalid) return c.json({
			ok: true,
			payload: {
				inputPath: rawPath,
				displayName,
				scope: "invalid",
				exists: false,
				capabilities: [],
				errorCode: "INVALID_PATH"
			}
		});
		let st = null;
		try {
			st = await stat(candidate);
		} catch {
			st = null;
		}
		if (!st) return c.json({
			ok: true,
			payload: {
				inputPath: rawPath,
				displayName,
				scope: "missing",
				exists: false,
				absolutePath: candidate,
				capabilities: fileReferenceCapabilities("missing", false),
				errorCode: "FILE_NOT_FOUND"
			}
		});
		const { scope, locationKind, manageRoute } = classifyFileLocation(candidate, classifierCtx);
		const inWorkspace = scope === "workspace";
		const isDirectory = st.isDirectory();
		const capabilities = fileReferenceCapabilities(scope, isDirectory, locationKind);
		const ref = fileReferenceRegistry.register({
			absolutePath: candidate,
			sessionKey: sessionKey || void 0,
			scope,
			locationKind,
			capabilities
		});
		return c.json({
			ok: true,
			payload: {
				fileRefId: ref.id,
				inputPath: rawPath,
				displayName,
				scope,
				locationKind,
				manageRoute,
				exists: true,
				isDirectory,
				absolutePath: candidate,
				workspaceRelativePath: inWorkspace ? toWorkspaceRelativePosix(workspaceRoot, candidate) : void 0,
				capabilities,
				mtimeMs: st.mtimeMs
			}
		});
	});
	authenticated.post("/api/workspace/file-ref/:id/resolve-action", async (c) => {
		const id = c.req.param("id")?.trim() ?? "";
		if (!id) return c.json({
			ok: false,
			error: {
				code: "INVALID_FILE_REF",
				message: "Missing file reference"
			}
		}, 400);
		const ref = fileReferenceRegistry.resolve(id);
		if (!ref) return c.json({
			ok: false,
			error: {
				code: "FILE_REF_EXPIRED",
				message: "File reference expired"
			}
		}, 404);
		const sessionKey = typeof c.req.query("sessionKey") === "string" ? c.req.query("sessionKey").trim() : "";
		if (!fileRefSessionKeysMatch(ref.sessionKey, sessionKey)) return c.json({
			ok: false,
			error: {
				code: "FILE_REF_FORBIDDEN",
				message: "File reference forbidden"
			}
		}, 403);
		const action = (await c.req.json().catch(() => ({}))).action;
		if (!isFileReferenceAction(action)) return c.json({
			ok: false,
			error: {
				code: "INVALID_ACTION",
				message: "Invalid action"
			}
		}, 400);
		if (!ref.capabilities.includes(action)) return c.json({
			ok: false,
			error: {
				code: "ACTION_NOT_ALLOWED",
				message: "Action not allowed"
			}
		}, 403);
		let st;
		try {
			st = await stat(ref.absolutePath);
		} catch {
			return c.json({
				ok: false,
				error: {
					code: "FILE_NOT_FOUND",
					message: "File not found"
				}
			}, 404);
		}
		return c.json({
			ok: true,
			payload: {
				absolutePath: ref.absolutePath,
				isDirectory: st.isDirectory()
			}
		});
	});
	authenticated.post("/api/workspace/import-file-ref/:id", async (c) => {
		const id = c.req.param("id")?.trim() ?? "";
		if (!id) return c.json({
			ok: false,
			error: {
				code: "INVALID_FILE_REF",
				message: "Missing file reference"
			}
		}, 400);
		const ref = fileReferenceRegistry.resolve(id);
		if (!ref) return c.json({
			ok: false,
			error: {
				code: "FILE_REF_EXPIRED",
				message: "File reference expired"
			}
		}, 404);
		const sessionKey = typeof c.req.query("sessionKey") === "string" ? c.req.query("sessionKey").trim() : "";
		if (!fileRefSessionKeysMatch(ref.sessionKey, sessionKey)) return c.json({
			ok: false,
			error: {
				code: "FILE_REF_FORBIDDEN",
				message: "File reference forbidden"
			}
		}, 403);
		if (!ref.capabilities.includes("importToWorkspace")) return c.json({
			ok: false,
			error: {
				code: "IMPORT_NOT_ALLOWED",
				message: "Import not allowed for this file"
			}
		}, 403);
		let sourceStat;
		try {
			sourceStat = await stat(ref.absolutePath);
		} catch {
			fileReferenceRegistry.expireById(id);
			return c.json({
				ok: false,
				error: {
					code: "SOURCE_NOT_FOUND",
					message: "Source file no longer exists"
				}
			}, 404);
		}
		if (!sourceStat.isFile()) return c.json({
			ok: false,
			error: {
				code: "SOURCE_NOT_FILE",
				message: "Source is not a regular file"
			}
		}, 400);
		const importCfg = resolveWorkspaceImportConfig(service.currentConfig);
		if (sourceStat.size > importCfg.maxBytes) return c.json({
			ok: false,
			error: {
				code: "SOURCE_TOO_LARGE",
				message: `Source exceeds maximum import size (${importCfg.maxBytes} bytes)`
			}
		}, 413);
		const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, sessionKey, void 0);
		if (ws.ok === false) return c.json({
			ok: false,
			error: {
				code: "WORKSPACE_RESOLUTION_FAILED",
				message: ws.message
			}
		}, 400);
		const workspaceRoot = ws.root;
		let body;
		try {
			body = await c.req.json().catch(() => ({}));
		} catch {
			body = {};
		}
		const requestedDestRaw = typeof body.destination === "string" ? body.destination.trim() : "";
		const onConflictRaw = typeof body.onConflict === "string" ? body.onConflict : "rename";
		if (onConflictRaw !== "rename" && onConflictRaw !== "overwrite" && onConflictRaw !== "error") return c.json({
			ok: false,
			error: {
				code: "INVALID_CONFLICT_MODE",
				message: "Invalid onConflict value"
			}
		}, 400);
		const onConflict = onConflictRaw;
		if (onConflict === "overwrite" && !importCfg.allowOverwrite) return c.json({
			ok: false,
			error: {
				code: "OVERWRITE_DISABLED",
				message: "Overwrite is disabled by config"
			}
		}, 403);
		const sourceBasename = sanitizeImportBasename(basename(ref.absolutePath)) || "imported-file";
		let requestedRel;
		if (!requestedDestRaw) requestedRel = `${importCfg.targetDir}/${sourceBasename}`;
		else {
			const trimmedDest = requestedDestRaw.replace(/\\/g, "/");
			requestedRel = trimmedDest.endsWith("/") ? `${trimmedDest}${sourceBasename}` : trimmedDest;
		}
		let initialDestAbs = resolveWorkspaceSafePath(workspaceRoot, requestedRel);
		if (!initialDestAbs) return c.json({
			ok: false,
			error: {
				code: "INVALID_DESTINATION",
				message: "Invalid destination path"
			}
		}, 400);
		const writePolicy = validateWritePath(initialDestAbs, workspaceRoot);
		if (!writePolicy.allowed) return c.json({
			ok: false,
			error: {
				code: "DESTINATION_BLOCKED",
				message: writePolicy.reason ?? "Destination blocked"
			}
		}, 403);
		if (resolve(ref.absolutePath) === resolve(initialDestAbs)) return c.json({
			ok: false,
			error: {
				code: "SAME_LOCATION",
				message: "Destination is the same as source"
			}
		}, 400);
		const destDir = dirname(initialDestAbs);
		try {
			await mkdir(destDir, { recursive: true });
		} catch (err) {
			log.warn({
				err,
				destDir
			}, "Failed to create import destination directory");
			return c.json({
				ok: false,
				error: {
					code: "IMPORT_FAILED",
					message: "Failed to prepare destination"
				}
			}, 500);
		}
		const tmpAbs = join(destDir, `.${basename(initialDestAbs)}.import-${randomUUID()}.tmp`);
		const started = Date.now();
		let renamed = false;
		let overwrote = false;
		let finalDestAbs = initialDestAbs;
		try {
			try {
				await copyFile(ref.absolutePath, tmpAbs, constants.COPYFILE_FICLONE);
			} catch (err) {
				await unlink(tmpAbs).catch(() => {});
				log.warn({
					err,
					source: ref.absolutePath,
					tmpAbs
				}, "Failed to copy source into staging tmp");
				return c.json({
					ok: false,
					error: {
						code: "IMPORT_FAILED",
						message: "Failed to copy source file"
					}
				}, 500);
			}
			if (onConflict === "overwrite") {
				overwrote = await stat(initialDestAbs).then(() => true).catch(() => false);
				try {
					await rename(tmpAbs, initialDestAbs);
				} catch (err) {
					await unlink(tmpAbs).catch(() => {});
					log.warn({
						err,
						target: initialDestAbs
					}, "Atomic rename failed");
					return c.json({
						ok: false,
						error: {
							code: "IMPORT_FAILED",
							message: "Failed to finalize import"
						}
					}, 500);
				}
			} else if (onConflict === "error") {
				if (await stat(initialDestAbs).then(() => true).catch(() => false)) {
					await unlink(tmpAbs).catch(() => {});
					return c.json({
						ok: false,
						error: {
							code: "DESTINATION_EXISTS",
							message: "Destination already exists"
						}
					}, 409);
				}
				try {
					await link(tmpAbs, initialDestAbs);
					await unlink(tmpAbs).catch(() => {});
				} catch (err) {
					await unlink(tmpAbs).catch(() => {});
					log.warn({
						err,
						target: initialDestAbs
					}, "Hard link to destination failed");
					return c.json({
						ok: false,
						error: {
							code: "IMPORT_FAILED",
							message: "Failed to finalize import"
						}
					}, 500);
				}
			} else {
				const picked = await pickAvailableTargetWithLink(tmpAbs, initialDestAbs);
				if (!picked.ok) {
					await unlink(tmpAbs).catch(() => {});
					log.warn({
						target: initialDestAbs,
						attempts: picked.attempts
					}, "Failed to find free import target name");
					return c.json({
						ok: false,
						error: {
							code: "IMPORT_FAILED",
							message: "No free destination name available"
						}
					}, 500);
				}
				await unlink(tmpAbs).catch(() => {});
				finalDestAbs = picked.path;
				renamed = picked.path !== initialDestAbs;
			}
		} catch (err) {
			await unlink(tmpAbs).catch(() => {});
			log.error({ err }, "Import file unexpected failure");
			return c.json({
				ok: false,
				error: {
					code: "IMPORT_FAILED",
					message: "Import failed"
				}
			}, 500);
		}
		let finalMtime;
		try {
			finalMtime = (await stat(finalDestAbs)).mtimeMs;
		} catch {
			finalMtime = Date.now();
		}
		const workspaceRel = toWorkspaceRelativePosix(workspaceRoot, finalDestAbs);
		fileReferenceRegistry.expireById(id);
		const newRef = fileReferenceRegistry.register({
			absolutePath: finalDestAbs,
			sessionKey: sessionKey || void 0,
			scope: "workspace",
			capabilities: fileReferenceCapabilities("workspace", false)
		});
		const sourceScope = ref.scope;
		const sourceLocationKind = ref.locationKind;
		service.emit("workspace.file-imported", {
			sessionKey: sessionKey || void 0,
			workspaceRelativePath: workspaceRel,
			absolutePath: finalDestAbs,
			bytes: sourceStat.size,
			sourceScope,
			sourceLocationKind
		});
		log.info({
			sessionKey,
			fileRefId: id,
			sourceAbsolutePath: ref.absolutePath,
			sourceScope,
			sourceLocationKind,
			destWorkspaceRelativePath: workspaceRel,
			bytes: sourceStat.size,
			renamed,
			overwrote,
			durationMs: Date.now() - started
		}, "Workspace file import succeeded");
		return c.json({
			ok: true,
			payload: {
				workspaceRelativePath: workspaceRel,
				absolutePath: finalDestAbs,
				bytesCopied: sourceStat.size,
				sourceAbsolutePath: ref.absolutePath,
				sourceScope,
				sourceLocationKind,
				renamed,
				overwrote,
				mtimeMs: finalMtime,
				newFileRefId: newRef.id
			}
		});
	});
	/**
	* Serve a workspace file as raw bytes (e.g. <img> after auth fetch + blob URL).
	* Path is workspace-relative; scope via sessionKey / agentId like other editor routes.
	*/
	authenticated.get("/api/workspace/editor/raw", async (c) => {
		const pathRel = typeof c.req.query("path") === "string" ? c.req.query("path") : "";
		if (!pathRel.trim()) return c.json({
			ok: false,
			error: { message: "Missing path" }
		}, 400);
		const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, c.req.query("sessionKey"), c.req.query("agentId"));
		if (ws.ok === false) return c.json({
			ok: false,
			error: { message: ws.message }
		}, 400);
		const workspaceRoot = ws.root;
		const abs = resolveWorkspaceSafePath(workspaceRoot, pathRel);
		if (!abs) return c.json({
			ok: false,
			error: { message: "Invalid path" }
		}, 400);
		let st;
		try {
			st = await stat(abs);
		} catch {
			return c.json({
				ok: false,
				error: { message: "Not found" }
			}, 404);
		}
		if (!st.isFile()) return c.json({
			ok: false,
			error: { message: "Not a file" }
		}, 400);
		const contentType = {
			png: "image/png",
			jpg: "image/jpeg",
			jpeg: "image/jpeg",
			gif: "image/gif",
			webp: "image/webp",
			bmp: "image/bmp",
			svg: "image/svg+xml",
			pdf: "application/pdf",
			docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
			xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
			pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
			txt: "text/plain",
			md: "text/markdown",
			json: "application/json",
			html: "text/html",
			css: "text/css",
			js: "text/javascript",
			ts: "text/typescript",
			mp3: "audio/mpeg",
			wav: "audio/wav",
			ogg: "audio/ogg",
			webm: "video/webm",
			mp4: "video/mp4",
			mov: "video/quicktime"
		}[pathRel.split(".").pop()?.toLowerCase() ?? ""] || "application/octet-stream";
		try {
			const buf = await readFile(abs);
			return new Response(buf, { headers: {
				"Content-Type": contentType,
				"Cache-Control": "private, max-age=3600"
			} });
		} catch {
			return c.json({
				ok: false,
				error: { message: "Read failed" }
			}, 500);
		}
	});
	authenticated.put("/api/workspace/editor/write", async (c) => {
		const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, c.req.query("sessionKey"), c.req.query("agentId"));
		if (ws.ok === false) return c.json({
			ok: false,
			error: { message: ws.message }
		}, 400);
		const workspaceRoot = ws.root;
		let body;
		try {
			body = await c.req.json();
		} catch {
			return c.json({
				ok: false,
				error: { message: "Invalid JSON" }
			}, 400);
		}
		const pathRel = typeof body === "object" && body !== null && "path" in body && typeof body.path === "string" ? body.path : "";
		const content = typeof body === "object" && body !== null && "content" in body && typeof body.content === "string" ? body.content : "";
		if (!pathRel.trim()) return c.json({
			ok: false,
			error: { message: "Missing path" }
		}, 400);
		const abs = resolveWorkspaceSafePath(workspaceRoot, pathRel);
		if (!abs) return c.json({
			ok: false,
			error: { message: "Invalid path" }
		}, 400);
		let st;
		try {
			st = await stat(abs);
		} catch {
			st = void 0;
		}
		if (st && !st.isFile()) return c.json({
			ok: false,
			error: { message: "Not a file" }
		}, 400);
		try {
			await writeFile(abs, content, "utf-8");
			let mtimeMs;
			try {
				mtimeMs = (await stat(abs)).mtimeMs;
			} catch {
				mtimeMs = Date.now();
			}
			return c.json({
				ok: true,
				payload: {
					path: toWorkspaceRelativePosix(workspaceRoot, abs),
					mtimeMs
				}
			});
		} catch (err) {
			log.error({
				err,
				path: abs
			}, "workspace editor write failed");
			return c.json({
				ok: false,
				error: { message: "Write failed" }
			}, 500);
		}
	});
	authenticated.get("/api/workspace/editor/search", async (c) => {
		const q = typeof c.req.query("q") === "string" ? c.req.query("q").trim() : "";
		const dirRel = typeof c.req.query("dir") === "string" ? c.req.query("dir") : "";
		if (!q) return c.json({
			ok: true,
			payload: { results: [] }
		});
		const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, c.req.query("sessionKey"), c.req.query("agentId"));
		if (ws.ok === false) return c.json({
			ok: false,
			error: { message: ws.message }
		}, 400);
		const workspaceRoot = ws.root;
		const absDir = resolveWorkspaceSafePath(workspaceRoot, dirRel);
		if (!absDir) return c.json({
			ok: false,
			error: { message: "Invalid path" }
		}, 400);
		let st;
		try {
			st = await stat(absDir);
		} catch {
			return c.json({
				ok: false,
				error: { message: "Not found" }
			}, 404);
		}
		if (!st.isDirectory()) return c.json({
			ok: false,
			error: { message: "Not a directory" }
		}, 400);
		const results = (await runRipgrepInDirectory(q, absDir)).filter((r) => isPathUnderWorkspace(workspaceRoot, r.filePath)).map((r) => ({
			...r,
			filePath: toWorkspaceRelativePosix(workspaceRoot, resolve(r.filePath))
		}));
		return c.json({
			ok: true,
			payload: { results }
		});
	});
	/** Fuzzy filename / path search over the session workspace (ripgrep `--files` + subsequence scoring). */
	authenticated.get("/api/workspace/editor/files/search", async (c) => {
		const q = typeof c.req.query("q") === "string" ? c.req.query("q").trim() : "";
		const limitRaw = c.req.query("limit");
		const limit = Math.min(Math.max(parseInt(typeof limitRaw === "string" ? limitRaw : "15", 10) || 15, 1), FILE_SEARCH_MAX_LIMIT);
		const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, c.req.query("sessionKey"), c.req.query("agentId"));
		if (ws.ok === false) return c.json({
			ok: false,
			error: { message: ws.message }
		}, 400);
		const entries = await fuzzySearchWorkspaceFiles(ws.root, q, limit);
		return c.json({
			ok: true,
			payload: { entries }
		});
	});
}
//#endregion
export { registerWorkspaceRoutes };

//# sourceMappingURL=workspace.js.map