import { realpath } from 'node:fs/promises';
import { isAbsolute, resolve, sep } from 'node:path';

export class PathScopeError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'PathScopeError';
  }
}

export async function resolveProjectRoot(projectPath: string): Promise<string> {
  try {
    return await realpath(projectPath);
  } catch {
    throw new PathScopeError(`project path does not exist: ${projectPath}`);
  }
}

function isUnder(real: string, root: string): boolean {
  return real === root || real.startsWith(root + sep);
}

// v1.13.17-cross-repo-reads: pathGuard now accepts an optional extraRoots
// list (typically session.allowed_read_paths). The primary projectRoot is
// tried first; if the resolved path doesn't sit under it, each extraRoot is
// tried in turn. Throws PathScopeError if no root accepts. The error message
// includes a hint pointing the model at the request_read_access tool so it
// can self-correct on the next turn — extraRoots IS the persistence
// mechanism for those grants, so we only suggest it when there's a missing
// grant to ask for (i.e. the path isn't already under any allowed root).
export async function pathGuard(
  projectRoot: string,
  requested: string,
  extraRoots: readonly string[] = [],
): Promise<string> {
  if (typeof requested !== 'string' || requested.length === 0) {
    throw new PathScopeError('path is required');
  }
  const candidate = isAbsolute(requested) ? requested : resolve(projectRoot, requested);
  let real: string;
  try {
    real = await realpath(candidate);
  } catch {
    throw new PathScopeError(`path does not exist: ${requested}`);
  }
  if (isUnder(real, projectRoot)) return real;
  for (const extra of extraRoots) {
    if (extra.length === 0) continue;
    if (isUnder(real, extra)) return real;
  }
  throw new PathScopeError(
    `path escapes project root: ${requested} -> ${real}. ` +
      `Use request_read_access(path, reason) to ask the user for permission.`,
  );
}
