// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// ═══════════════════════════════════════════════════════════════════════════
// orgPolicy.ts - Organization policy layer
//
// Externally-defined constraints that are NOT generated by the model.
// Enforced at both plan validation time and runtime.
// ═══════════════════════════════════════════════════════════════════════════

import { readFileSync } from "node:fs";
import { basename } from "node:path";
import type { AgentPlan, PlanLimits, Tool } from "./specSchema.js";

// ───────────────────────────────────────────────────────────────────────────
// POLICY TYPES
// ───────────────────────────────────────────────────────────────────────────

export interface OrgPolicy {
    /** Policy schema version */
    version: "1.0";

    /** Human-readable name for this policy */
    name: string;

    /** Tools the agent is NEVER allowed to use */
    deniedTools?: Tool[];

    /** If set, ONLY these tools are allowed (allowlist mode) */
    allowedTools?: Tool[];

    /** File path restrictions */
    paths?: PathPolicy;

    /** Bash command restrictions */
    bash?: BashPolicy;

    /** Caps on plan-declared limits — the plan cannot exceed these */
    limitCaps?: Partial<PlanLimits>;

    /** Container sandbox for bash execution */
    container?: ContainerPolicy;
}

export interface PathPolicy {
    /** Glob patterns for allowed read paths */
    allowedReadPatterns?: string[];

    /** Glob patterns for allowed write paths */
    allowedWritePatterns?: string[];

    /** Glob patterns that are NEVER accessible (deny overrides allow) */
    deniedPatterns?: string[];
}

export interface BashPolicy {
    /**
     * Execution mode:
     * - 'unrestricted': validated_bash runs with no policy checks
     * - 'policy-checked': validated_bash runs but commands are checked (default)
     * - 'capabilities-only': validated_bash is blocked; use validated_npm/git/node/tsc instead
     */
    mode?: "unrestricted" | "policy-checked" | "capabilities-only";

    /** If set, ONLY these base commands are permitted (allowlist) */
    allowedCommands?: string[];

    /** These base commands are never permitted (denylist) */
    deniedCommands?: string[];

    /** Regex patterns run against the full command string; any match blocks */
    denyPatterns?: string[];

    /** Network access restrictions */
    network?: NetworkPolicy;

    /** Maximum timeout in milliseconds for any bash command */
    maxTimeoutMs?: number;
}

export interface NetworkPolicy {
    /** If true, all network-accessing commands are denied */
    denyAll?: boolean;

    /** Allowed destination hosts (glob patterns) */
    allowedHosts?: string[];

    /** Allowed outbound ports (heuristic check on bash command strings) */
    allowedPorts?: number[];

    /**
     * Ports to publish from the container (docker -p).
     * Each entry is host:container or just a port number (same on both sides).
     * Only takes effect when container is enabled.
     * Derived from allowedPorts when derivePortsFromPolicy is true on ContainerPolicy.
     */
    publishPorts?: Array<number | string>;
}

export interface ContainerPolicy {
    /** Enable container sandbox for bash execution */
    enabled: boolean;

    /** Docker image to use (e.g., "node:20-slim", "python:3.12-slim") */
    image: string;

    /** Container network mode */
    networkMode: "none" | "host" | "bridge";

    /**
     * Auto-derive volume mounts from policy.paths.
     * When true:
     *   - allowedWritePatterns base dirs → mounted read-write
     *   - allowedReadPatterns base dirs → mounted read-only
     *   - deniedPatterns are NOT mounted
     * When false (default): only the working directory and additionalVolumes are mounted.
     */
    deriveVolumesFromPolicy?: boolean;

    /** Read-only root filesystem (tmpfs mounted at /tmp) */
    readOnly?: boolean;

    /** Memory limit (e.g., "512m") */
    memoryLimit?: string;

    /** CPU limit (e.g., "1.0") */
    cpuLimit?: string;

    /** Maximum number of processes */
    pidsLimit?: number;

    /** Execution timeout in milliseconds */
    timeoutMs?: number;

    /** Additional volume mounts in Docker format (e.g., ["/data:/data:ro"]) */
    additionalVolumes?: string[];

    /** Environment variables to pass into the container */
    env?: Record<string, string>;

    /** Working directory path inside the container (default: /workspace) */
    workDir?: string;

    /**
     * Auto-derive published ports from network.allowedPorts.
     * When true, each allowedPort becomes a -p host:container mapping.
     * Only meaningful when networkMode is 'bridge' (not 'none' or 'host').
     */
    derivePortsFromPolicy?: boolean;

    /** Device access policy */
    devices?: DevicePolicy;
}

export interface DevicePolicy {
    /**
     * GPU access.
     * - false / undefined: no GPU access (default, most secure)
     * - true: all GPUs (docker --gpus all)
     * - number: specific GPU count (docker --gpus N)
     * - string: device IDs (docker --gpus '"device=0,1"')
     */
    gpu?: boolean | number | string;

    /**
     * Explicit device mounts (docker --device).
     * e.g., ["/dev/video0"] for camera, ["/dev/snd"] for audio.
     * Each entry is a host device path.
     */
    allowedDevices?: string[];

    /**
     * Denied device patterns — if any --device in the container config
     * matches these patterns, reject the policy at load time.
     * Useful for blanket denials like ["/dev/video*", "/dev/snd*"].
     */
    deniedDevicePatterns?: string[];
}

// ───────────────────────────────────────────────────────────────────────────
// POLICY VIOLATION
// ───────────────────────────────────────────────────────────────────────────

export type PolicyViolationRule =
    | "denied_tool"
    | "tool_not_allowed"
    | "limit_cap_exceeded"
    | "denied_path"
    | "path_not_allowed"
    | "denied_command"
    | "command_not_allowed"
    | "deny_pattern_match"
    | "network_denied"
    | "bash_timeout_exceeded";

export interface PolicyViolation {
    rule: PolicyViolationRule;
    message: string;
    stepIndex?: number;
}

export interface PolicyValidationResult {
    valid: boolean;
    errors: PolicyViolation[];
    warnings: PolicyViolation[];
}

// ───────────────────────────────────────────────────────────────────────────
// POLICY LOADING
// ───────────────────────────────────────────────────────────────────────────

export function loadOrgPolicy(filePath: string): OrgPolicy {
    const raw = readFileSync(filePath, "utf-8");
    const parsed = JSON.parse(raw);

    if (parsed.version !== "1.0") {
        throw new Error(`Unsupported policy version: ${parsed.version}`);
    }
    if (!parsed.name || typeof parsed.name !== "string") {
        throw new Error("Policy must have a 'name' string field");
    }

    return parsed as OrgPolicy;
}

// ───────────────────────────────────────────────────────────────────────────
// PLAN-TIME VALIDATION
// ───────────────────────────────────────────────────────────────────────────

/**
 * Validates a plan against org policy at submission time.
 * Checks: tool restrictions, limit caps, best-effort literal path checks.
 */
export function validatePlanAgainstPolicy(
    plan: AgentPlan,
    policy: OrgPolicy,
): PolicyValidationResult {
    const errors: PolicyViolation[] = [];
    const warnings: PolicyViolation[] = [];

    // 1. Check tools used in the plan against policy
    const steps = plan.steps;
    for (const step of steps) {
        const toolViolation = checkToolAgainstPolicy(step.tool, policy);
        if (toolViolation) {
            errors.push({ ...toolViolation, stepIndex: step.index });
        }
    }

    // Also check metadata.allowedTools for denied tools
    if (policy.deniedTools) {
        for (const tool of plan.metadata.allowedTools) {
            if (policy.deniedTools.includes(tool)) {
                errors.push({
                    rule: "denied_tool",
                    message: `metadata.allowedTools includes '${tool}' which is denied by policy`,
                });
            }
        }
    }

    // 2. Check limit caps
    if (policy.limitCaps) {
        const caps = policy.limitCaps;
        const limits = plan.limits;

        if (
            caps.maxTotalSteps !== undefined &&
            limits.maxTotalSteps > caps.maxTotalSteps
        ) {
            errors.push({
                rule: "limit_cap_exceeded",
                message: `Plan maxTotalSteps (${limits.maxTotalSteps}) exceeds policy cap (${caps.maxTotalSteps})`,
            });
        }
        if (
            caps.maxDurationMs !== undefined &&
            limits.maxDurationMs > caps.maxDurationMs
        ) {
            errors.push({
                rule: "limit_cap_exceeded",
                message: `Plan maxDurationMs (${limits.maxDurationMs}) exceeds policy cap (${caps.maxDurationMs})`,
            });
        }
        if (
            caps.maxFileWrites !== undefined &&
            limits.maxFileWrites > caps.maxFileWrites
        ) {
            errors.push({
                rule: "limit_cap_exceeded",
                message: `Plan maxFileWrites (${limits.maxFileWrites}) exceeds policy cap (${caps.maxFileWrites})`,
            });
        }
        if (
            caps.maxBytesWritten !== undefined &&
            limits.maxBytesWritten > caps.maxBytesWritten
        ) {
            errors.push({
                rule: "limit_cap_exceeded",
                message: `Plan maxBytesWritten (${limits.maxBytesWritten}) exceeds policy cap (${caps.maxBytesWritten})`,
            });
        }
    }

    // 3. Best-effort bash command checks on literal inputSpec constraints
    if (policy.bash) {
        for (const step of steps) {
            if (step.tool === "Bash" && step.inputSpec.command) {
                const constraint = step.inputSpec.command;
                if (
                    constraint.type === "exact" &&
                    typeof constraint.value === "string"
                ) {
                    const violation = checkBashAgainstPolicy(
                        constraint.value,
                        policy.bash,
                    );
                    if (violation) {
                        errors.push({ ...violation, stepIndex: step.index });
                    }
                }
            }
        }
    }

    return { valid: errors.length === 0, errors, warnings };
}

// ───────────────────────────────────────────────────────────────────────────
// RUNTIME: TOOL CALL CHECK
// ───────────────────────────────────────────────────────────────────────────

/**
 * Checks a single tool call against policy before execution.
 * Returns null if allowed, or a PolicyViolation if blocked.
 */
export function checkToolCallAgainstPolicy(
    toolName: Tool,
    input: Record<string, unknown>,
    policy: OrgPolicy,
): PolicyViolation | null {
    // 1. Tool restriction
    const toolViolation = checkToolAgainstPolicy(toolName, policy);
    if (toolViolation) return toolViolation;

    // 2. Path restrictions for file operations
    if (policy.paths) {
        if (toolName === "Read" || toolName === "Grep" || toolName === "Glob") {
            const filePath = (input.file_path ?? input.path) as
                | string
                | undefined;
            if (filePath) {
                const pathViolation = checkPathAgainstPolicy(
                    filePath,
                    "read",
                    policy.paths,
                );
                if (pathViolation) return pathViolation;
            }
        }
        if (toolName === "Write" || toolName === "Edit") {
            const filePath = input.file_path as string | undefined;
            if (filePath) {
                const pathViolation = checkPathAgainstPolicy(
                    filePath,
                    "write",
                    policy.paths,
                );
                if (pathViolation) return pathViolation;
            }
        }
    }

    // 3. Bash restrictions
    if (toolName === "Bash" && policy.bash) {
        const command = input.command as string | undefined;
        if (command) {
            return checkBashAgainstPolicy(command, policy.bash);
        }
    }

    return null;
}

// ───────────────────────────────────────────────────────────────────────────
// TOOL RESTRICTION CHECK
// ───────────────────────────────────────────────────────────────────────────

function checkToolAgainstPolicy(
    tool: Tool,
    policy: OrgPolicy,
): PolicyViolation | null {
    if (policy.deniedTools?.includes(tool)) {
        return {
            rule: "denied_tool",
            message: `Tool '${tool}' is denied by organization policy`,
        };
    }
    if (policy.allowedTools && !policy.allowedTools.includes(tool)) {
        return {
            rule: "tool_not_allowed",
            message: `Tool '${tool}' is not in the organization's allowed tools list`,
        };
    }
    return null;
}

// ───────────────────────────────────────────────────────────────────────────
// PATH POLICY CHECK
// ───────────────────────────────────────────────────────────────────────────

/**
 * Checks if a concrete file path is allowed by the path policy.
 * Uses simple glob matching: patterns ending with /** match all descendants,
 * patterns with * match wildcards. Deny always overrides allow.
 */
export function checkPathAgainstPolicy(
    filePath: string,
    operation: "read" | "write",
    pathPolicy: PathPolicy,
): PolicyViolation | null {
    // Normalize path separators
    const normalized = filePath.replace(/\\/g, "/");

    // Deny patterns always win
    if (pathPolicy.deniedPatterns) {
        for (const pattern of pathPolicy.deniedPatterns) {
            if (globMatch(normalized, pattern)) {
                return {
                    rule: "denied_path",
                    message: `Path '${filePath}' matches denied pattern '${pattern}'`,
                };
            }
        }
    }

    // Check allowed patterns
    const allowedPatterns =
        operation === "read"
            ? pathPolicy.allowedReadPatterns
            : pathPolicy.allowedWritePatterns;

    if (allowedPatterns && allowedPatterns.length > 0) {
        const allowed = allowedPatterns.some((pattern) =>
            globMatch(normalized, pattern),
        );
        if (!allowed) {
            return {
                rule: "path_not_allowed",
                message: `Path '${filePath}' is not within allowed ${operation} paths`,
            };
        }
    }

    return null;
}

/**
 * Simple glob matching for path policy.
 * Supports: ** (any depth), * (single segment), literal prefixes.
 */
function globMatch(path: string, pattern: string): boolean {
    const normalizedPattern = pattern.replace(/\\/g, "/");

    // Convert glob to regex
    const regexStr = normalizedPattern
        .replace(/[\\.+^${}()|[\]]/g, "\\$&") // escape regex chars (incl. \) except * and ?
        .replace(/\*\*/g, "{{DOUBLESTAR}}") // temp placeholder
        .replace(/\*/g, "[^/]*") // * matches within a segment
        .replace(/{{DOUBLESTAR}}/g, ".*") // ** matches across segments
        .replace(/\?/g, "[^/]"); // ? matches single char

    return new RegExp(`^${regexStr}$`).test(path);
}

// ───────────────────────────────────────────────────────────────────────────
// BASH COMMAND PARSING
// ───────────────────────────────────────────────────────────────────────────

export interface CommandSegment {
    /** The full text of this segment */
    raw: string;

    /** The base command name (first word, after skipping prefixes) */
    commandName: string;

    /** The shell operator that preceded this segment */
    operator: "&&" | "||" | ";" | "|" | null;
}

export interface ParsedCommand {
    segments: CommandSegment[];
}

/** Prefixes to skip when extracting the base command name */
const COMMAND_PREFIXES = new Set([
    "sudo",
    "env",
    "nice",
    "nohup",
    "time",
    "strace",
    "exec",
    "ionice",
    "taskset",
    "chroot",
]);

/**
 * Parses a shell command string into pipeline/chain segments,
 * extracting the base command name from each.
 *
 * Quote-aware: respects single quotes, double quotes, and backticks.
 * Splits on: &&, ||, ;, | (single pipe)
 */
export function parseBashCommand(command: string): ParsedCommand {
    const segments: CommandSegment[] = [];
    const parts = splitShellOperators(command);

    for (const part of parts) {
        const trimmed = part.raw.trim();
        if (!trimmed) continue;

        const commandName = extractCommandName(trimmed);
        segments.push({
            raw: trimmed,
            commandName,
            operator: part.operator,
        });
    }

    return { segments };
}

interface RawPart {
    raw: string;
    operator: "&&" | "||" | ";" | "|" | null;
}

/**
 * Split a command string by shell operators, respecting quotes.
 */
function splitShellOperators(command: string): RawPart[] {
    const parts: RawPart[] = [];
    let current = "";
    let inSingleQuote = false;
    let inDoubleQuote = false;
    let currentOperator: RawPart["operator"] = null;
    let i = 0;

    while (i < command.length) {
        const ch = command[i];

        // Quote tracking
        if (ch === "'" && !inDoubleQuote) {
            inSingleQuote = !inSingleQuote;
            current += ch;
            i++;
            continue;
        }
        if (ch === '"' && !inSingleQuote) {
            inDoubleQuote = !inDoubleQuote;
            current += ch;
            i++;
            continue;
        }

        // Only split when outside quotes
        if (!inSingleQuote && !inDoubleQuote) {
            // Check for && and ||
            if (i + 1 < command.length) {
                const two = command.slice(i, i + 2);
                if (two === "&&" || two === "||") {
                    parts.push({ raw: current, operator: currentOperator });
                    currentOperator = two;
                    current = "";
                    i += 2;
                    continue;
                }
            }
            // Check for ; and |
            if (ch === ";") {
                parts.push({ raw: current, operator: currentOperator });
                currentOperator = ";";
                current = "";
                i++;
                continue;
            }
            if (ch === "|") {
                parts.push({ raw: current, operator: currentOperator });
                currentOperator = "|";
                current = "";
                i++;
                continue;
            }
        }

        current += ch;
        i++;
    }

    // Push the last segment
    if (current.trim()) {
        parts.push({ raw: current, operator: currentOperator });
    }

    return parts;
}

/**
 * Extract the base command name from a single command segment.
 * Skips env-var assignments (VAR=value) and common prefixes (sudo, env, etc.).
 */
function extractCommandName(segment: string): string {
    const words = segment.trim().split(/\s+/);

    let idx = 0;

    // Skip environment variable assignments (VAR=value or VAR="value")
    while (idx < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[idx])) {
        idx++;
    }

    // Skip command prefixes (sudo, env, etc.)
    while (idx < words.length && COMMAND_PREFIXES.has(words[idx])) {
        idx++;
        // Skip flags and their arguments after prefix (e.g., sudo -u root)
        while (idx < words.length && words[idx].startsWith("-")) {
            const flag = words[idx];
            idx++;
            // Short flags like -u often take an argument (the next word).
            // Skip it if present and not another flag.
            if (
                flag.length === 2 &&
                flag[1] !== "-" &&
                idx < words.length &&
                !words[idx].startsWith("-")
            ) {
                idx++;
            }
            // -- means end of flags
            if (flag === "--") break;
        }
    }

    if (idx >= words.length) {
        return words[words.length - 1] ?? "";
    }

    const cmd = words[idx];

    // If the command contains a path separator, take the basename
    if (cmd.includes("/")) {
        return basename(cmd);
    }

    return cmd;
}

// ───────────────────────────────────────────────────────────────────────────
// BASH POLICY CHECK
// ───────────────────────────────────────────────────────────────────────────

/** Commands commonly used for network access */
const NETWORK_COMMANDS = new Set([
    "curl",
    "wget",
    "ssh",
    "scp",
    "rsync",
    "nc",
    "ncat",
    "telnet",
    "ftp",
    "sftp",
    "nmap",
    "ping",
    "traceroute",
    "dig",
    "nslookup",
    "host",
]);

/**
 * Checks a bash command against BashPolicy.
 * Returns null if allowed, or a PolicyViolation if blocked.
 */
export function checkBashAgainstPolicy(
    command: string,
    policy: BashPolicy,
): PolicyViolation | null {
    // 1. Deny patterns (regex on full command string)
    if (policy.denyPatterns) {
        for (const pattern of policy.denyPatterns) {
            try {
                if (new RegExp(pattern).test(command)) {
                    return {
                        rule: "deny_pattern_match",
                        message: `Command matches denied pattern: /${pattern}/`,
                    };
                }
            } catch {
                // Invalid regex in policy — skip
            }
        }
    }

    // 2. Parse and check each command segment
    const parsed = parseBashCommand(command);

    for (const segment of parsed.segments) {
        const cmd = segment.commandName;

        // Denied commands
        if (policy.deniedCommands?.includes(cmd)) {
            return {
                rule: "denied_command",
                message: `Command '${cmd}' is denied by policy`,
            };
        }

        // Allowed commands (allowlist mode)
        if (policy.allowedCommands && !policy.allowedCommands.includes(cmd)) {
            return {
                rule: "command_not_allowed",
                message: `Command '${cmd}' is not in the allowed commands list`,
            };
        }

        // Network policy: block network-accessing commands
        if (policy.network?.denyAll && NETWORK_COMMANDS.has(cmd)) {
            return {
                rule: "network_denied",
                message: `Network command '${cmd}' is denied by policy (network.denyAll)`,
            };
        }
    }

    // 3. Network port checking (heuristic)
    if (
        policy.network?.allowedPorts &&
        policy.network.allowedPorts.length > 0
    ) {
        const portMatches = command.match(/:(\d{2,5})\b/g);
        if (portMatches) {
            for (const match of portMatches) {
                const port = parseInt(match.slice(1), 10);
                if (
                    !isNaN(port) &&
                    !policy.network.allowedPorts.includes(port)
                ) {
                    return {
                        rule: "network_denied",
                        message: `Port ${port} is not in the allowed ports list`,
                    };
                }
            }
        }
    }

    return null;
}

// ───────────────────────────────────────────────────────────────────────────
// CONTAINER: VOLUME DERIVATION FROM POLICY
// ───────────────────────────────────────────────────────────────────────────

export interface DerivedVolume {
    /** Host path */
    hostPath: string;
    /** Container path */
    containerPath: string;
    /** Read-only flag */
    readonly: boolean;
}

/**
 * Extracts the base directory from a glob pattern.
 * e.g., "/home/user/project/src/**" → "/home/user/project/src"
 *       "/home/user/project/**\/*.ts" → "/home/user/project"
 *       "**\/.env" → null (no concrete base)
 */
function globBaseDir(pattern: string): string | null {
    const normalized = pattern.replace(/\\/g, "/");
    // Find the first segment containing a wildcard
    const parts = normalized.split("/");
    const concreteParts: string[] = [];
    for (const part of parts) {
        if (part.includes("*") || part.includes("?") || part.includes("{"))
            break;
        concreteParts.push(part);
    }
    const result = concreteParts.join("/");
    // Must have at least a root (e.g., "/" or "C:/")
    return result.length > 0 ? result : null;
}

/**
 * Derives container volume mounts from the org policy's path restrictions.
 *
 * Write patterns → read-write mounts
 * Read patterns → read-only mounts (unless already covered by a write mount)
 * Denied patterns are not mounted (they're enforced at the MCP server level).
 *
 * Inside the container, host paths are mapped under /mnt/<basename>.
 */
export function deriveContainerVolumes(
    pathPolicy: PathPolicy | undefined,
    workDir: string,
): DerivedVolume[] {
    const volumes: DerivedVolume[] = [];
    const mountedPaths = new Set<string>();

    // Always mount the working directory as read-write
    const normalizedWorkDir = workDir.replace(/\\/g, "/");
    volumes.push({
        hostPath: normalizedWorkDir,
        containerPath: "/workspace",
        readonly: false,
    });
    mountedPaths.add(normalizedWorkDir);

    if (!pathPolicy) return volumes;

    // Write patterns → read-write mounts
    if (pathPolicy.allowedWritePatterns) {
        for (const pattern of pathPolicy.allowedWritePatterns) {
            const base = globBaseDir(pattern);
            if (!base || mountedPaths.has(base.replace(/\\/g, "/"))) continue;
            const normalized = base.replace(/\\/g, "/");
            // Skip if this is a parent/child of an existing mount
            if (isSubpathOfAny(normalized, mountedPaths)) continue;
            const containerBase = `/mnt/${safeName(normalized)}`;
            volumes.push({
                hostPath: normalized,
                containerPath: containerBase,
                readonly: false,
            });
            mountedPaths.add(normalized);
        }
    }

    // Read patterns → read-only mounts (skip if already mounted)
    if (pathPolicy.allowedReadPatterns) {
        for (const pattern of pathPolicy.allowedReadPatterns) {
            const base = globBaseDir(pattern);
            if (!base) continue;
            const normalized = base.replace(/\\/g, "/");
            if (mountedPaths.has(normalized)) continue;
            if (isSubpathOfAny(normalized, mountedPaths)) continue;
            const containerBase = `/mnt/${safeName(normalized)}`;
            volumes.push({
                hostPath: normalized,
                containerPath: containerBase,
                readonly: true,
            });
            mountedPaths.add(normalized);
        }
    }

    return volumes;
}

/** Checks if path is a subpath of any path in the set */
function isSubpathOfAny(path: string, paths: Set<string>): boolean {
    for (const existing of paths) {
        if (
            path.startsWith(existing + "/") ||
            existing.startsWith(path + "/")
        ) {
            return true;
        }
    }
    return false;
}

/** Creates a safe mount name from a path */
function safeName(path: string): string {
    const sanitized = path.replace(/[^a-zA-Z0-9]/g, "_");
    let start = 0;
    let end = sanitized.length;
    while (start < end && sanitized.charCodeAt(start) === 95 /* _ */) start++;
    while (end > start && sanitized.charCodeAt(end - 1) === 95) end--;
    return sanitized.slice(start, end).slice(-30) || "vol";
}

/**
 * Builds the docker run arguments from a ContainerPolicy and optional derived volumes.
 * Exported for testing — the actual execution uses executeBashInContainer.
 */
export function buildDockerArgs(
    command: string,
    cwd: string,
    policy: ContainerPolicy,
    pathPolicy?: PathPolicy,
    networkPolicy?: NetworkPolicy,
): string[] {
    const workDir = policy.workDir ?? "/workspace";

    // Derive or use explicit volumes
    const volumes: DerivedVolume[] = policy.deriveVolumesFromPolicy
        ? deriveContainerVolumes(pathPolicy, cwd)
        : [
              {
                  hostPath: cwd.replace(/\\/g, "/"),
                  containerPath: workDir,
                  readonly: false,
              },
          ];

    // Derive published ports from network policy
    const portArgs: string[] = [];
    if (
        policy.derivePortsFromPolicy &&
        networkPolicy?.allowedPorts &&
        policy.networkMode === "bridge"
    ) {
        for (const port of networkPolicy.allowedPorts) {
            portArgs.push("-p", `${port}:${port}`);
        }
    }
    // Explicit port publishing from network policy
    if (networkPolicy?.publishPorts) {
        for (const port of networkPolicy.publishPorts) {
            const portStr = typeof port === "number" ? `${port}:${port}` : port;
            portArgs.push("-p", portStr);
        }
    }

    // Device args
    const deviceArgs: string[] = [];
    if (policy.devices) {
        const dev = policy.devices;
        // GPU
        if (dev.gpu === true) {
            deviceArgs.push("--gpus", "all");
        } else if (typeof dev.gpu === "number") {
            deviceArgs.push("--gpus", dev.gpu.toString());
        } else if (typeof dev.gpu === "string") {
            deviceArgs.push("--gpus", `"device=${dev.gpu}"`);
        }
        // Explicit device mounts
        if (dev.allowedDevices) {
            for (const device of dev.allowedDevices) {
                deviceArgs.push("--device", device);
            }
        }
    }

    return [
        "run",
        "--rm",
        `--network=${policy.networkMode}`,
        ...(policy.readOnly ? ["--read-only", "--tmpfs", "/tmp"] : []),
        ...(policy.memoryLimit ? [`--memory=${policy.memoryLimit}`] : []),
        ...(policy.cpuLimit ? [`--cpus=${policy.cpuLimit}`] : []),
        ...(policy.pidsLimit
            ? [`--pids-limit=${policy.pidsLimit.toString()}`]
            : []),
        // Port publishing
        ...portArgs,
        // Device access
        ...deviceArgs,
        // Volume mounts
        ...volumes.flatMap((v) => [
            "-v",
            `${v.hostPath}:${v.containerPath}${v.readonly ? ":ro" : ""}`,
        ]),
        // Additional explicit volumes
        ...(policy.additionalVolumes?.flatMap((v) => ["-v", v]) ?? []),
        // Environment variables
        ...(policy.env
            ? Object.entries(policy.env).flatMap(([k, v]) => [
                  "-e",
                  `${k}=${v}`,
              ])
            : []),
        // Working directory
        "-w",
        workDir,
        // Image and command
        policy.image,
        "sh",
        "-c",
        command,
    ];
}

/**
 * Checks if Docker is available on the system.
 * Returns the Docker version string on success, or an error message on failure.
 */
export function checkDockerAvailability(): {
    available: boolean;
    version?: string;
    error?: string;
} {
    try {
        const { spawnSync } = require("node:child_process");
        const result = spawnSync(
            "docker",
            ["version", "--format", "{{.Server.Version}}"],
            {
                timeout: 5000,
                encoding: "utf-8",
                stdio: ["pipe", "pipe", "pipe"],
            },
        );
        if (result.status === 0 && result.stdout.trim()) {
            return { available: true, version: result.stdout.trim() };
        }
        return {
            available: false,
            error: result.stderr || "Docker not responding",
        };
    } catch (err: any) {
        return { available: false, error: err.message };
    }
}
