// [nexus] Zero-terminal helpers for user-provided filesystem paths.
// Extracted from server/src/routes/agents.ts so projects, execution-
// workspaces, and nexus-settings can share the same expansion + mkdir
// logic. The agent route endpoints still call these via the same
// wrapper names for source-diff minimalism.

import { mkdir, stat } from "node:fs/promises";
import { homedir } from "node:os";
import path from "node:path";
import { unprocessable } from "../errors.js";
import { logger } from "../middleware/logger.js";

/**
 * Expand a user-provided path: resolve `~` / `~/foo` to the server
 * process's home directory and convert to absolute form. Returns null
 * if the input is not a non-empty string.
 */
export function expandUserPath(candidate: unknown): string | null {
  if (typeof candidate !== "string") return null;
  const trimmed = candidate.trim();
  if (trimmed.length === 0) return null;
  const home = homedir();
  let expanded: string;
  if (trimmed === "~") {
    expanded = home;
  } else if (trimmed.startsWith("~/")) {
    expanded = path.join(home, trimmed.slice(2));
  } else {
    expanded = trimmed;
  }
  return path.resolve(expanded);
}

/**
 * Normalize a user-provided directory path and ensure it exists on
 * disk. Expands tildes, resolves to absolute, creates the directory
 * recursively if missing, and stats to confirm it's a directory.
 * Throws a 422 unprocessable error with the reason on failure.
 *
 * Returns the normalized absolute path. Use at any endpoint that
 * accepts a user-provided cwd / workspace / working-directory field
 * before persisting it to the DB or passing it to a child process.
 */
export async function normalizeWorkspaceDir(
  rawPath: string,
  context: { field: string; reason?: string } = { field: "cwd" },
): Promise<string> {
  const resolved = expandUserPath(rawPath);
  if (!resolved) {
    throw unprocessable(
      `${context.field} must be a non-empty string${context.reason ? ` (${context.reason})` : ""}`,
    );
  }
  if (!path.isAbsolute(resolved)) {
    throw unprocessable(
      `${context.field} must resolve to an absolute path after tilde expansion (got ${resolved})`,
    );
  }
  try {
    await mkdir(resolved, { recursive: true });
  } catch (err) {
    const reason = err instanceof Error ? err.message : String(err);
    throw unprocessable(`Could not create directory ${resolved}: ${reason}`);
  }
  try {
    const stats = await stat(resolved);
    if (!stats.isDirectory()) {
      throw unprocessable(`Path ${resolved} exists but is not a directory`);
    }
  } catch (err) {
    if (err && typeof err === "object" && "status" in err) throw err;
    const reason = err instanceof Error ? err.message : String(err);
    throw unprocessable(`Could not stat ${resolved}: ${reason}`);
  }
  if (resolved !== rawPath) {
    logger.info(
      { field: context.field, original: rawPath, resolved },
      "normalized user-supplied workspace path (tilde expansion / absolute path / mkdir)",
    );
  }
  return resolved;
}
