import {
  accessSync,
  closeSync,
  constants,
  fstatSync,
  lstatSync,
  mkdirSync,
  openSync,
  readFileSync,
  readlinkSync,
  statSync,
} from 'node:fs';
import { platform } from 'node:os';
import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path';

export interface SecureFileReadOptions {
  root: string;
  maxBytes?: number;
  executable?: boolean;
  /**
   * Additional roots a symlink component may resolve into (stack#1380).
   * Default: only the managed root itself. Every symlink hop is validated —
   * containment under the root or one of these roots, current-user ownership,
   * no group/world-writable mode — and refusal stays the default for anything
   * else. Callers that operate the split-home layout pass the brain home so
   * the framework-created roster symlink resolves.
   */
  symlinkTargetRoots?: string[];
}

export interface SecureFileSnapshot {
  content: Buffer;
  mode: number;
  dev: number | bigint;
  ino: number | bigint;
}

function sameIdentity(
  left: { dev: number | bigint; ino: number | bigint },
  right: { dev: number | bigint; ino: number | bigint },
): boolean {
  return left.dev === right.dev && left.ino === right.ino;
}

function secureFilesystemError(message: string, cause: unknown): Error {
  const error = new Error(message);
  if (cause instanceof Error && 'code' in cause && typeof cause.code === 'string') {
    Object.defineProperty(error, 'code', { value: cause.code, enumerable: true });
  }
  return error;
}

function closeDescriptors(descriptors: number[]): void {
  for (const fd of descriptors.reverse()) {
    try {
      closeSync(fd);
    } catch {
      // Best-effort cleanup must not replace the security decision already made.
    }
  }
}

function procDescriptorPath(fd: number, component?: string): string {
  const descriptor = `/proc/self/fd/${fd}`;
  return component === undefined ? descriptor : `${descriptor}/${component}`;
}

/**
 * Hold each directory while opening its child through Linux proc-fd. The only
 * symlink followed is the kernel-owned descriptor link; O_NOFOLLOW protects
 * every appended filesystem component from substitution.
 */
function openDirectoryChain(absoluteDirectory: string): { fd: number; descriptors: number[] } {
  if (platform() !== 'linux') {
    throw new Error('secure descriptor traversal is unsupported on this platform');
  }

  const descriptors: number[] = [];
  try {
    let fd = openSync(sep, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
    descriptors.push(fd);
    for (const component of absoluteDirectory.split(sep).filter(Boolean)) {
      fd = openSync(
        procDescriptorPath(fd, component),
        constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
      );
      descriptors.push(fd);
      if (!fstatSync(fd).isDirectory()) {
        throw new Error('secure descriptor traversal encountered a non-directory component');
      }
    }
    return { fd, descriptors };
  } catch (error) {
    closeDescriptors(descriptors);
    throw secureFilesystemError(
      'secure descriptor traversal failed: symbolic link, unavailable, or not a directory',
      error,
    );
  }
}

function containedUnder(root: string, target: string): boolean {
  const rel = relative(resolve(root), resolve(target));
  return rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel) && rel !== '';
}

const MAX_SYMLINK_HOPS = 40;

/**
 * Resolve every symlink on `lexical` component-wise, validating each hop
 * (stack#1380 resolve-then-validate): the hop target must stay under one of
 * the sanctioned roots, must be owned by the current user (or root), and must
 * not be group- or world-writable. Returns a symlink-free absolute path.
 */
function resolveRealPath(lexical: string, sanctionedRoots: string[]): string {
  const hopTargets: string[] = [];
  let current: string = sep;
  for (const piece of resolve(lexical).split(sep).filter(Boolean)) {
    current = resolve(current, piece);
    for (let hops = 0; lstatSync(current).isSymbolicLink(); ) {
      if (++hops > MAX_SYMLINK_HOPS) {
        throw new Error(`symlink chain exceeds ${MAX_SYMLINK_HOPS} hops: ${lexical}`);
      }
      const linkTarget = readlinkSync(current);
      const absolute = resolve(dirname(current), linkTarget);
      if (!sanctionedRoots.some((root) => containedUnder(root, absolute))) {
        throw new Error(
          `symlink target escapes managed roots [${sanctionedRoots.join(', ')}]: ${absolute}`,
        );
      }
      hopTargets.push(absolute);
      current = absolute;
    }
  }
  const uid = typeof process.getuid === 'function' ? process.getuid() : 0;
  for (const hop of hopTargets) {
    const stat = statSync(hop);
    if (stat.uid !== uid && stat.uid !== 0) {
      throw new Error(`symlink target is not owned by the current user: ${hop}`);
    }
    if (stat.mode & 0o022) {
      throw new Error(`symlink target is group- or world-writable: ${hop}`);
    }
  }
  return current;
}

function openFileBeneathRoot(
  root: string,
  target: string,
  symlinkTargetRoots: string[] = [],
): { fd: number; descriptors: number[] } {
  const canonicalRoot = resolve(root);
  const canonicalTarget = resolve(target);
  assertCanonicalContainment(canonicalRoot, canonicalTarget);
  const components = relative(canonicalRoot, canonicalTarget).split(sep).filter(Boolean);
  const fileName = components.pop();
  if (fileName === undefined) throw new Error('managed file path names the managed root');

  // stack#1380: resolve-then-validate. The lexical path must name the managed
  // root (above); symlink components are then resolved hop-by-hop under the
  // sanctioned roots (validated per hop), and the descriptor traversal walks
  // the symlink-free real path — keeping the O_NOFOLLOW chain as the race
  // guard for anything substituted after resolution.
  let realRoot: string;
  try {
    realRoot = resolveRealPath(canonicalRoot, [canonicalRoot]);
  } catch (error) {
    throw secureFilesystemError(
      'secure descriptor traversal failed: symbolic link, unavailable, or not a directory',
      error,
    );
  }
  const sanctioned = [realRoot, ...symlinkTargetRoots.map((extra) => resolve(extra))];
  let realTarget: string;
  try {
    realTarget = resolveRealPath(canonicalTarget, sanctioned);
  } catch (error) {
    if (error instanceof Error && !('code' in error)) throw error;
    throw secureFilesystemError(
      'secure descriptor traversal failed: symbolic link, unavailable, or not a directory',
      error,
    );
  }
  if (!sanctioned.some((sr) => containedUnder(sr, realTarget) || resolve(sr) === realTarget)) {
    throw new Error(
      `resolved path escapes managed roots [${sanctioned.join(', ')}]: ${realTarget}`,
    );
  }

  const chain = openDirectoryChain(dirname(realTarget));
  try {
    let fd: number;
    try {
      fd = openSync(
        procDescriptorPath(chain.fd, basename(realTarget)),
        constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW,
      );
    } catch (error) {
      throw secureFilesystemError('file is a symbolic link or unavailable', error);
    }
    chain.descriptors.push(fd);
    return { fd, descriptors: chain.descriptors };
  } catch (error) {
    closeDescriptors(chain.descriptors);
    if (error instanceof Error) throw error;
    throw new Error('secure managed file open failed');
  }
}

export function assertCanonicalContainment(root: string, target: string): void {
  const canonicalRoot = resolve(root);
  const canonicalTarget = resolve(target);
  const rel = relative(canonicalRoot, canonicalTarget);
  if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
    throw new Error(`path escapes managed root ${canonicalRoot}: ${canonicalTarget}`);
  }
}

/** Reject every symlink from the filesystem root through the target's parent. */
export function assertNoSymlinkAncestors(target: string): void {
  const absolute = resolve(target);
  const parent = dirname(absolute);
  const pieces = parent.split(sep).filter(Boolean);
  let cursor: string = sep;
  for (const piece of pieces) {
    cursor = resolve(cursor, piece);
    const stat = lstatSync(cursor);
    if (stat.isSymbolicLink()) throw new Error(`path ancestor is a symbolic link: ${cursor}`);
    if (!stat.isDirectory()) throw new Error(`path ancestor is not a directory: ${cursor}`);
  }
}

export function ensureManagedDirectory(root: string, directory: string): void {
  assertCanonicalContainment(root, directory);
  const canonicalRoot = resolve(root);
  const canonicalDirectory = resolve(directory);
  assertNoSymlinkAncestors(canonicalRoot);
  try {
    const rootStat = lstatSync(canonicalRoot);
    if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
      throw new Error(`managed root is not a real directory: ${canonicalRoot}`);
    }
  } catch (error) {
    if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error;
    mkdirSync(canonicalRoot, { mode: 0o700 });
    const rootStat = lstatSync(canonicalRoot);
    if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
      throw new Error(`managed root creation was redirected: ${canonicalRoot}`);
    }
  }
  const rel = relative(canonicalRoot, canonicalDirectory);
  let cursor = canonicalRoot;
  for (const piece of rel.split(sep).filter(Boolean)) {
    cursor = resolve(cursor, piece);
    try {
      const stat = lstatSync(cursor);
      if (stat.isSymbolicLink()) throw new Error(`path ancestor is a symbolic link: ${cursor}`);
      if (!stat.isDirectory()) throw new Error(`path ancestor is not a directory: ${cursor}`);
    } catch (error) {
      if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error;
      mkdirSync(cursor, { mode: 0o700 });
      const created = lstatSync(cursor);
      if (!created.isDirectory() || created.isSymbolicLink()) {
        throw new Error(`managed directory creation was redirected: ${cursor}`);
      }
    }
  }
}

/**
 * Read a regular file through an O_NOFOLLOW descriptor. The inode is checked
 * before and after access/read, and executable access is tested against the
 * already-open descriptor so path replacement cannot redirect the check.
 */
export function readRegularFileSecure(
  path: string,
  options: SecureFileReadOptions,
): SecureFileSnapshot {
  const openedFile = openFileBeneathRoot(options.root, path, options.symlinkTargetRoots ?? []);
  try {
    const opened = fstatSync(openedFile.fd);
    if (!opened.isFile()) throw new Error('managed file is not a regular file');
    if (options.maxBytes !== undefined && opened.size > options.maxBytes) {
      throw new Error(`managed file exceeds ${options.maxBytes} bytes`);
    }
    if (options.executable) {
      try {
        accessSync(procDescriptorPath(openedFile.fd), constants.X_OK);
      } catch (error) {
        throw secureFilesystemError('managed file is not executable', error);
      }
      const afterAccess = fstatSync(openedFile.fd);
      if (!afterAccess.isFile() || !sameIdentity(opened, afterAccess)) {
        throw new Error('managed file changed during executable access check');
      }
    }

    const content = readFileSync(openedFile.fd);
    const after = fstatSync(openedFile.fd);
    if (!after.isFile() || !sameIdentity(opened, after)) {
      throw new Error('managed file changed during secure read');
    }
    if (options.maxBytes !== undefined && content.byteLength > options.maxBytes) {
      throw new Error(`managed file exceeds ${options.maxBytes} bytes`);
    }
    return {
      content,
      mode: Number(opened.mode),
      dev: opened.dev,
      ino: opened.ino,
    };
  } finally {
    closeDescriptors(openedFile.descriptors);
  }
}
