/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import fs from 'node:fs';
import { join, dirname } from 'node:path';
import os from 'node:os';
import {
  type SandboxManager,
  type GlobalSandboxOptions,
  type SandboxRequest,
  type SandboxedCommand,
  type SandboxPermissions,
  GOVERNANCE_FILES,
  type ParsedSandboxDenial,
  resolveSandboxPaths,
} from '../../services/sandboxManager.js';
import type { ShellExecutionResult } from '../../services/shellExecutionService.js';
import {
  sanitizeEnvironment,
  getSecureSanitizationConfig,
} from '../../services/environmentSanitization.js';
import {
  isStrictlyApproved,
  verifySandboxOverrides,
} from '../utils/commandUtils.js';
import { assertValidPathString } from '../../utils/paths.js';
import {
  isKnownSafeCommand,
  isDangerousCommand,
} from '../utils/commandSafety.js';
import {
  parsePosixSandboxDenials,
  createSandboxDenialCache,
  type SandboxDenialCache,
} from '../utils/sandboxDenialUtils.js';
import { isErrnoException } from '../utils/fsUtils.js';
import { handleReadWriteCommands } from '../utils/sandboxReadWriteUtils.js';
import { buildBwrapArgs } from './bwrapArgsBuilder.js';
import {
  getCommandRoots,
  initializeShellParsers,
  stripShellWrapper,
} from '../../utils/shell-utils.js';

let cachedBpfPath: string | undefined;

function getSeccompBpfPath(): string {
  if (cachedBpfPath) return cachedBpfPath;

  const arch = os.arch();
  let AUDIT_ARCH: number;
  let SYS_ptrace: number;

  if (arch === 'x64') {
    AUDIT_ARCH = 0xc000003e; // AUDIT_ARCH_X86_64
    SYS_ptrace = 101;
  } else if (arch === 'arm64') {
    AUDIT_ARCH = 0xc00000b7; // AUDIT_ARCH_AARCH64
    SYS_ptrace = 117;
  } else if (arch === 'arm') {
    AUDIT_ARCH = 0x40000028; // AUDIT_ARCH_ARM
    SYS_ptrace = 26;
  } else if (arch === 'ia32') {
    AUDIT_ARCH = 0x40000003; // AUDIT_ARCH_I386
    SYS_ptrace = 26;
  } else {
    throw new Error(`Unsupported architecture for seccomp filter: ${arch}`);
  }

  const EPERM = 1;
  const SECCOMP_RET_KILL_PROCESS = 0x80000000;
  const SECCOMP_RET_ERRNO = 0x00050000;
  const SECCOMP_RET_ALLOW = 0x7fff0000;

  const instructions = [
    { code: 0x20, jt: 0, jf: 0, k: 4 }, // Load arch
    { code: 0x15, jt: 1, jf: 0, k: AUDIT_ARCH }, // Jump to kill if arch != native arch
    { code: 0x06, jt: 0, jf: 0, k: SECCOMP_RET_KILL_PROCESS }, // Kill

    { code: 0x20, jt: 0, jf: 0, k: 0 }, // Load nr
    { code: 0x15, jt: 0, jf: 1, k: SYS_ptrace }, // If ptrace, jump to ERRNO
    { code: 0x06, jt: 0, jf: 0, k: SECCOMP_RET_ERRNO | EPERM }, // ERRNO

    { code: 0x06, jt: 0, jf: 0, k: SECCOMP_RET_ALLOW }, // Allow
  ];

  const buf = Buffer.alloc(8 * instructions.length);
  for (let i = 0; i < instructions.length; i++) {
    const inst = instructions[i];
    const offset = i * 8;
    buf.writeUInt16LE(inst.code, offset);
    buf.writeUInt8(inst.jt, offset + 2);
    buf.writeUInt8(inst.jf, offset + 3);
    buf.writeUInt32LE(inst.k, offset + 4);
  }

  const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'gemini-cli-seccomp-'));
  const bpfPath = join(tempDir, 'seccomp.bpf');
  fs.writeFileSync(bpfPath, buf);
  cachedBpfPath = bpfPath;

  // Cleanup on exit
  process.on('exit', () => {
    try {
      fs.rmSync(tempDir, { recursive: true, force: true });
    } catch {
      // Ignore errors
    }
  });

  return bpfPath;
}

/**
 * Ensures a file or directory exists.
 */
function touch(filePath: string, isDirectory: boolean) {
  assertValidPathString(filePath);
  try {
    // If it exists (even as a broken symlink), do nothing
    fs.lstatSync(filePath);
    return;
  } catch (e: unknown) {
    if (isErrnoException(e) && e.code !== 'ENOENT') {
      throw e;
    }
  }

  if (isDirectory) {
    fs.mkdirSync(filePath, { recursive: true });
  } else {
    fs.mkdirSync(dirname(filePath), { recursive: true });
    fs.closeSync(fs.openSync(filePath, 'a'));
  }
}

/**
 * A SandboxManager implementation for Linux that uses Bubblewrap (bwrap).
 */

export class LinuxSandboxManager implements SandboxManager {
  private static maskFilePath: string | undefined;
  private readonly denialCache: SandboxDenialCache = createSandboxDenialCache();
  private governanceFilesInitialized = false;

  constructor(private readonly options: GlobalSandboxOptions) {}

  private ensureGovernanceFilesExist(workspace: string): void {
    if (this.governanceFilesInitialized) return;

    // These must exist on the host before running the sandbox to ensure they are protected.
    for (const file of GOVERNANCE_FILES) {
      const filePath = join(workspace, file.path);
      touch(filePath, file.isDirectory);
    }

    this.governanceFilesInitialized = true;
  }

  isKnownSafeCommand(args: string[]): boolean {
    return isKnownSafeCommand(args);
  }

  isDangerousCommand(args: string[]): boolean {
    return isDangerousCommand(args);
  }

  parseDenials(result: ShellExecutionResult): ParsedSandboxDenial | undefined {
    return parsePosixSandboxDenials(result, this.denialCache);
  }

  getWorkspace(): string {
    return this.options.workspace;
  }

  getOptions(): GlobalSandboxOptions {
    return this.options;
  }

  private getMaskFilePath(): string {
    if (
      LinuxSandboxManager.maskFilePath &&
      fs.existsSync(LinuxSandboxManager.maskFilePath)
    ) {
      return LinuxSandboxManager.maskFilePath;
    }
    const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'gemini-cli-mask-file-'));
    const maskPath = join(tempDir, 'mask');
    fs.writeFileSync(maskPath, '');
    fs.chmodSync(maskPath, 0);
    LinuxSandboxManager.maskFilePath = maskPath;

    // Cleanup on exit
    process.on('exit', () => {
      try {
        fs.rmSync(tempDir, { recursive: true, force: true });
      } catch {
        // Ignore errors
      }
    });

    return maskPath;
  }

  async prepareCommand(req: SandboxRequest): Promise<SandboxedCommand> {
    const isReadonlyMode = this.options.modeConfig?.readonly ?? true;
    const allowOverrides = this.options.modeConfig?.allowOverrides ?? true;

    verifySandboxOverrides(allowOverrides, req.policy);

    let command = req.command;
    let args = req.args;

    // Translate virtual commands for sandboxed file system access
    if (command === '__read') {
      command = 'cat';
    } else if (command === '__write') {
      command = 'sh';
      args = ['-c', 'cat > "$1"', '_', ...args];
    }

    await initializeShellParsers();
    const fullCmd = [command, ...args].join(' ');
    const stripped = stripShellWrapper(fullCmd);
    const roots = getCommandRoots(stripped).filter(
      (r) => r !== 'shopt' && r !== 'set',
    );
    const commandName = roots.length > 0 ? roots[0] : join(command);
    const isGitCommand = roots.includes('git');

    const isApproved = allowOverrides
      ? await isStrictlyApproved(
          { ...req, command, args },
          this.options.modeConfig?.approvedTools,
        )
      : false;
    const isYolo = this.options.modeConfig?.yolo ?? false;
    const workspaceWrite = !isReadonlyMode || isApproved || isYolo;

    const networkAccess =
      this.options.modeConfig?.network || req.policy?.networkAccess || isYolo;

    const persistentPermissions = allowOverrides
      ? this.options.policyManager?.getCommandPermissions(commandName)
      : undefined;

    const mergedAdditional: SandboxPermissions = {
      fileSystem: {
        read: [
          ...(persistentPermissions?.fileSystem?.read ?? []),
          ...(req.policy?.additionalPermissions?.fileSystem?.read ?? []),
        ],
        write: [
          ...(persistentPermissions?.fileSystem?.write ?? []),
          ...(req.policy?.additionalPermissions?.fileSystem?.write ?? []),
        ],
      },
      network:
        networkAccess ||
        persistentPermissions?.network ||
        req.policy?.additionalPermissions?.network ||
        false,
    };

    // If the workspace is writable and we're running a git command,
    // automatically allow write access to the .git directory.
    if (workspaceWrite && isGitCommand) {
      const gitDir = join(this.options.workspace, '.git');
      if (!mergedAdditional.fileSystem!.write!.includes(gitDir)) {
        mergedAdditional.fileSystem!.write!.push(gitDir);
      }
    }

    const { command: finalCommand, args: finalArgs } = handleReadWriteCommands(
      req,
      mergedAdditional,
      this.options.workspace,
      [
        ...(req.policy?.allowedPaths || []),
        ...(this.options.includeDirectories || []),
      ],
    );

    const sanitizationConfig = getSecureSanitizationConfig(
      req.policy?.sanitizationConfig,
    );

    const sanitizedEnv = sanitizeEnvironment(req.env, sanitizationConfig);

    const resolvedPaths = await resolveSandboxPaths(
      this.options,
      req,
      mergedAdditional,
    );

    this.ensureGovernanceFilesExist(resolvedPaths.workspace.resolved);

    const bwrapArgs = await buildBwrapArgs({
      resolvedPaths,
      workspaceWrite,
      networkAccess: mergedAdditional.network ?? false,
      maskFilePath: this.getMaskFilePath(),
      isReadOnlyCommand: req.command === '__read',
    });

    const bpfPath = getSeccompBpfPath();
    bwrapArgs.push('--seccomp', '9');

    const argsPath = this.writeArgsToTempFile(bwrapArgs);

    const shArgs = [
      '-c',
      'bpf_path="$1"; args_path="$2"; shift 2; exec bwrap --args 8 "$@" 8< "$args_path" 9< "$bpf_path"',
      '_',
      bpfPath,
      argsPath,
      '--',
      finalCommand,
      ...finalArgs,
    ];

    return {
      program: 'sh',
      args: shArgs,
      env: sanitizedEnv,
      cwd: req.cwd,
      cleanup: () => {
        try {
          fs.unlinkSync(argsPath);
        } catch {
          // Ignore cleanup errors
        }
      },
    };
  }

  private writeArgsToTempFile(args: string[]): string {
    const tempFile = join(
      os.tmpdir(),
      `gemini-cli-bwrap-args-${Date.now()}-${Math.random().toString(36).slice(2)}.args`,
    );
    const content = Buffer.from(args.join('\0') + '\0');
    fs.writeFileSync(tempFile, content, { mode: 0o600 });
    return tempFile;
  }
}
