import { execFile } from 'child_process';
import { existsSync, realpathSync, statSync } from 'fs';

const BWRAP_PATH = '/usr/bin/bwrap';

const RO_BIND_DIRS = ['/usr', '/bin', '/sbin', '/lib', '/etc'];
const RO_BIND_TRY_DIRS = ['/lib64'];

export interface ExtraReadOnlyBind {
  src: string;
  dest: string;
}

/**
 * Build the minimal, secret-free environment handed to sandboxed bash.
 * Allowlist only — any var not listed (incl. all secrets) is dropped.
 */
export function buildSandboxEnv(
  parentEnv: NodeJS.ProcessEnv,
  workspacePath: string,
): Record<string, string> {
  const env: Record<string, string> = {
    PATH: parentEnv.PATH ?? '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
    HOME: workspacePath,
    LANG: parentEnv.LANG ?? 'C.UTF-8',
    TERM: 'dumb',
    TMPDIR: '/tmp',
  };
  if (parentEnv.LC_ALL) env.LC_ALL = parentEnv.LC_ALL;
  if (parentEnv.TZ) env.TZ = parentEnv.TZ;
  return env;
}

export function buildBwrapArgs(
  command: string,
  workspacePath: string,
  extraReadOnlyBinds?: ExtraReadOnlyBind[],
  parentEnv: NodeJS.ProcessEnv = process.env,
  allowNetwork: boolean = false,
): string[] {
  const args: string[] = [];

  for (const dir of RO_BIND_DIRS) {
    args.push('--ro-bind', dir, dir);
  }
  for (const dir of RO_BIND_TRY_DIRS) {
    if (existsSync(dir)) {
      args.push('--ro-bind', dir, dir);
    }
  }

  if (extraReadOnlyBinds) {
    for (const bind of extraReadOnlyBinds) {
      try {
        const realSrc = realpathSync(bind.src);
        const stat = statSync(realSrc);
        if (stat.isDirectory()) {
          args.push('--ro-bind', realSrc, bind.dest);
        }
      } catch {
        // skip entries where realpath fails or stat fails (non-existent src)
      }
    }
  }

  args.push('--proc', '/proc');
  args.push('--dev', '/dev');
  args.push('--tmpfs', '/tmp');

  args.push('--bind', workspacePath, workspacePath);

  args.push('--chdir', workspacePath);
  args.push('--die-with-parent');
  // Network isolation is the one --unshare-* that's optional. When
  // allowNetwork is true (config.safety.bashAllowNetwork) the sandbox keeps host
  // network so pip/npm/curl work; all other namespaces stay unshared.
  args.push('--unshare-user', '--unshare-ipc', '--unshare-pid', '--unshare-uts', '--unshare-cgroup');
  if (!allowNetwork) {
    args.push('--unshare-net');
  }

  args.push('--clearenv');
  const sandboxEnv = buildSandboxEnv(parentEnv, workspacePath);
  for (const [k, v] of Object.entries(sandboxEnv)) {
    args.push('--setenv', k, v);
  }

  args.push('/bin/bash', '-c', command);

  return args;
}

export interface BwrapCheckResult {
  ok: boolean;
  reason?: string;
}

export async function checkBwrapAvailable(): Promise<BwrapCheckResult> {
  if (!existsSync(BWRAP_PATH)) {
    return { ok: false, reason: `bwrap not found at ${BWRAP_PATH}` };
  }
  return new Promise((resolve) => {
    execFile(BWRAP_PATH, ['--ro-bind', '/', '/', 'true'], { timeout: 5000 }, (err) => {
      if (err) {
        resolve({ ok: false, reason: `bwrap test failed: ${err.message}` });
      } else {
        resolve({ ok: true });
      }
    });
  });
}

let _bwrapAvailable: Promise<boolean> | null = null;
/** Memoized bwrap availability — probed once per process. */
export function isBwrapAvailable(): Promise<boolean> {
  if (_bwrapAvailable === null) {
    _bwrapAvailable = checkBwrapAvailable().then((r) => r.ok).catch(() => false);
  }
  return _bwrapAvailable;
}

export interface SandboxedBashResult {
  output: string;
  isError: boolean;
}

export async function executeSandboxedBash(
  command: string,
  workspacePath: string,
  timeoutSec: number,
  maxBuffer: number,
  abortSignal?: AbortSignal,
  extraReadOnlyBinds?: ExtraReadOnlyBind[],
  allowNetwork: boolean = false,
): Promise<SandboxedBashResult> {
  const args = buildBwrapArgs(command, workspacePath, extraReadOnlyBinds, process.env, allowNetwork);

  if (abortSignal?.aborted) {
    return { output: 'Cancelled before sandbox bash launch', isError: true };
  }

  return new Promise((resolve) => {
    execFile(
      BWRAP_PATH,
      args,
      {
        timeout: timeoutSec * 1000,
        encoding: 'utf-8',
        maxBuffer,
        signal: abortSignal,
      },
      (error, stdout, stderr) => {
        if (!error) {
          resolve({ output: stdout, isError: false });
          return;
        }

        const msg = error.message ?? String(error);
        const execError = error as Error & { signal?: NodeJS.Signals | null; killed?: boolean; code?: string };
        const details: string[] = [stdout || '', stderr || ''];
        if (execError.code === 'ABORT_ERR' || abortSignal?.aborted) {
          details.push('Cancelled by user request');
        } else if (execError.killed) {
          details.push(`Command timed out after ${timeoutSec}s`);
        }
        if (execError.signal) {
          details.push(`Signal: ${execError.signal}`);
        }
        resolve({
          output: [...details, msg].filter(Boolean).join('\n'),
          isError: true,
        });
      },
    );
  });
}
