import path from 'node:path';
import fs from 'node:fs';

/**
 * Resolves a user-provided path and verifies it is inside the allowed sandbox directory.
 * Throws SandboxEscapeError if the resolved path is outside the sandbox.
 *
 * Uses realpathSync to resolve symlinks in the sandbox root. The user-supplied path
 * is checked for containment AFTER lexical resolution but BEFORE resolving any symlinks
 * within the user path — so symlink escape attempts are caught too.
 *
 * @param userPath - The path provided by the agent (may be relative or absolute)
 * @param sandboxDir - The allowed root directory (already validated on session creation)
 * @returns The resolved absolute path, guaranteed to be within sandboxDir
 */
export function guardPath(userPath: string, sandboxDir: string): string {
  const resolved = path.resolve(sandboxDir, userPath);
  const sandboxResolved = fs.realpathSync.native(sandboxDir);

  // Normalize both paths to resolve any symlinks in the sandbox root itself.
  // For the user path, we check containment BEFORE resolving symlinks in the path
  // (so we catch symlink escape attempts too — the resolved path must still be under sandbox)
  if (!resolved.startsWith(sandboxResolved + path.sep) && resolved !== sandboxResolved) {
    throw new SandboxEscapeError(userPath, sandboxDir, resolved);
  }

  return resolved;
}

/**
 * Validates a path without resolving symlinks in the user-provided portion.
 * Use for paths that may not exist yet (creates, writes).
 *
 * Performs a lexical containment check only using path.resolve.
 */
export function guardPathUnsafe(userPath: string, sandboxDir: string): string {
  const resolved = path.resolve(sandboxDir, userPath);
  const sandboxAbs = path.resolve(sandboxDir);

  if (!resolved.startsWith(sandboxAbs + path.sep) && resolved !== sandboxAbs) {
    throw new SandboxEscapeError(userPath, sandboxDir, resolved);
  }

  return resolved;
}

export class SandboxEscapeError extends Error {
  constructor(
    public readonly userPath: string,
    public readonly sandboxDir: string,
    public readonly resolvedPath: string,
  ) {
    super(
      `Path escape attempt blocked: "${userPath}" resolves to "${resolvedPath}" which is outside sandbox "${sandboxDir}"`,
    );
    this.name = 'SandboxEscapeError';
  }
}
