import fs from "node:fs";
import path from "node:path";
import { DEFAULT_SAFE_BINS, analyzeShellCommand, isWindowsPlatform, matchAllowlist, resolveAllowlistCandidatePath, splitCommandChain, } from "./exec-approvals-analysis.js";
function isPathLikeToken(value) {
    const trimmed = value.trim();
    if (!trimmed) {
        return false;
    }
    if (trimmed === "-") {
        return false;
    }
    if (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("~")) {
        return true;
    }
    if (trimmed.startsWith("/")) {
        return true;
    }
    return /^[A-Za-z]:[\\/]/.test(trimmed);
}
function defaultFileExists(filePath) {
    try {
        return fs.existsSync(filePath);
    }
    catch {
        return false;
    }
}
export function normalizeSafeBins(entries) {
    if (!Array.isArray(entries)) {
        return new Set();
    }
    const normalized = entries
        .map((entry) => entry.trim().toLowerCase())
        .filter((entry) => entry.length > 0);
    return new Set(normalized);
}
export function resolveSafeBins(entries) {
    if (entries === undefined) {
        return normalizeSafeBins(DEFAULT_SAFE_BINS);
    }
    return normalizeSafeBins(entries ?? []);
}
function hasGlobToken(value) {
    // Safe bins are stdin-only; globbing is both surprising and a historical bypass vector.
    // Note: we still harden execution-time expansion separately.
    return /[*?[\]]/.test(value);
}
export function isSafeBinUsage(params) {
    // Windows host exec uses PowerShell, which has different parsing/expansion rules.
    // Keep safeBins conservative there (require explicit allowlist entries).
    if (isWindowsPlatform(process.platform)) {
        return false;
    }
    if (params.safeBins.size === 0) {
        return false;
    }
    const resolution = params.resolution;
    const execName = resolution?.executableName?.toLowerCase();
    if (!execName) {
        return false;
    }
    const matchesSafeBin = params.safeBins.has(execName) ||
        (process.platform === "win32" && params.safeBins.has(path.parse(execName).name));
    if (!matchesSafeBin) {
        return false;
    }
    if (!resolution?.resolvedPath) {
        return false;
    }
    const cwd = params.cwd ?? process.cwd();
    const exists = params.fileExists ?? defaultFileExists;
    const argv = params.argv.slice(1);
    for (let i = 0; i < argv.length; i += 1) {
        const token = argv[i];
        if (!token) {
            continue;
        }
        if (token === "-") {
            continue;
        }
        if (token.startsWith("-")) {
            const eqIndex = token.indexOf("=");
            if (eqIndex > 0) {
                const value = token.slice(eqIndex + 1);
                if (value && hasGlobToken(value)) {
                    return false;
                }
                if (value && (isPathLikeToken(value) || exists(path.resolve(cwd, value)))) {
                    return false;
                }
            }
            continue;
        }
        if (hasGlobToken(token)) {
            return false;
        }
        if (isPathLikeToken(token)) {
            return false;
        }
        if (exists(path.resolve(cwd, token))) {
            return false;
        }
    }
    return true;
}
function evaluateSegments(segments, params) {
    const matches = [];
    const allowSkills = params.autoAllowSkills === true && (params.skillBins?.size ?? 0) > 0;
    const segmentSatisfiedBy = [];
    const satisfied = segments.every((segment) => {
        const candidatePath = resolveAllowlistCandidatePath(segment.resolution, params.cwd);
        const candidateResolution = candidatePath && segment.resolution
            ? { ...segment.resolution, resolvedPath: candidatePath }
            : segment.resolution;
        const match = matchAllowlist(params.allowlist, candidateResolution);
        if (match) {
            matches.push(match);
        }
        const safe = isSafeBinUsage({
            argv: segment.argv,
            resolution: segment.resolution,
            safeBins: params.safeBins,
            cwd: params.cwd,
        });
        const skillAllow = allowSkills && segment.resolution?.executableName
            ? params.skillBins?.has(segment.resolution.executableName)
            : false;
        const by = match
            ? "allowlist"
            : safe
                ? "safeBins"
                : skillAllow
                    ? "skills"
                    : null;
        segmentSatisfiedBy.push(by);
        return Boolean(by);
    });
    return { satisfied, matches, segmentSatisfiedBy };
}
export function evaluateExecAllowlist(params) {
    const allowlistMatches = [];
    const segmentSatisfiedBy = [];
    if (!params.analysis.ok || params.analysis.segments.length === 0) {
        return { allowlistSatisfied: false, allowlistMatches, segmentSatisfiedBy };
    }
    // If the analysis contains chains, evaluate each chain part separately
    if (params.analysis.chains) {
        for (const chainSegments of params.analysis.chains) {
            const result = evaluateSegments(chainSegments, {
                allowlist: params.allowlist,
                safeBins: params.safeBins,
                cwd: params.cwd,
                skillBins: params.skillBins,
                autoAllowSkills: params.autoAllowSkills,
            });
            if (!result.satisfied) {
                return { allowlistSatisfied: false, allowlistMatches: [], segmentSatisfiedBy: [] };
            }
            allowlistMatches.push(...result.matches);
            segmentSatisfiedBy.push(...result.segmentSatisfiedBy);
        }
        return { allowlistSatisfied: true, allowlistMatches, segmentSatisfiedBy };
    }
    // No chains, evaluate all segments together
    const result = evaluateSegments(params.analysis.segments, {
        allowlist: params.allowlist,
        safeBins: params.safeBins,
        cwd: params.cwd,
        skillBins: params.skillBins,
        autoAllowSkills: params.autoAllowSkills,
    });
    return {
        allowlistSatisfied: result.satisfied,
        allowlistMatches: result.matches,
        segmentSatisfiedBy: result.segmentSatisfiedBy,
    };
}
const SHELL_WRAPPER_EXECUTABLES = new Set([
    "ash",
    "bash",
    "cmd",
    "cmd.exe",
    "dash",
    "fish",
    "ksh",
    "powershell",
    "powershell.exe",
    "pwsh",
    "pwsh.exe",
    "sh",
    "zsh",
]);
function normalizeExecutableName(name) {
    return (name ?? "").trim().toLowerCase();
}
function isShellWrapperSegment(segment) {
    const candidates = [
        normalizeExecutableName(segment.resolution?.executableName),
        normalizeExecutableName(segment.resolution?.rawExecutable),
    ];
    for (const candidate of candidates) {
        if (!candidate) {
            continue;
        }
        if (SHELL_WRAPPER_EXECUTABLES.has(candidate)) {
            return true;
        }
        const base = candidate.split(/[\\/]/).pop();
        if (base && SHELL_WRAPPER_EXECUTABLES.has(base)) {
            return true;
        }
    }
    return false;
}
function extractShellInlineCommand(argv) {
    for (let i = 1; i < argv.length; i += 1) {
        const token = argv[i];
        if (!token) {
            continue;
        }
        const lower = token.toLowerCase();
        if (lower === "--") {
            break;
        }
        if (lower === "-c" ||
            lower === "--command" ||
            lower === "-command" ||
            lower === "/c" ||
            lower === "/k") {
            const next = argv[i + 1]?.trim();
            return next ? next : null;
        }
        if (/^-[^-]*c[^-]*$/i.test(token)) {
            const commandIndex = lower.indexOf("c");
            const inline = token.slice(commandIndex + 1).trim();
            if (inline) {
                return inline;
            }
            const next = argv[i + 1]?.trim();
            return next ? next : null;
        }
    }
    return null;
}
function collectAllowAlwaysPatterns(params) {
    const candidatePath = resolveAllowlistCandidatePath(params.segment.resolution, params.cwd);
    if (!candidatePath) {
        return;
    }
    if (!isShellWrapperSegment(params.segment)) {
        params.out.add(candidatePath);
        return;
    }
    if (params.depth >= 3) {
        return;
    }
    const inlineCommand = extractShellInlineCommand(params.segment.argv);
    if (!inlineCommand) {
        return;
    }
    const nested = analyzeShellCommand({
        command: inlineCommand,
        cwd: params.cwd,
        env: params.env,
        platform: params.platform,
    });
    if (!nested.ok) {
        return;
    }
    for (const nestedSegment of nested.segments) {
        collectAllowAlwaysPatterns({
            segment: nestedSegment,
            cwd: params.cwd,
            env: params.env,
            platform: params.platform,
            depth: params.depth + 1,
            out: params.out,
        });
    }
}
/**
 * Resolves allowlist patterns to persist for "allow-always" decisions.
 * When commands are shell-wrapped (e.g., `zsh -lc "<cmd>"`),
 * persists the inner executable(s) rather than the shell binary itself.
 */
export function resolveAllowAlwaysPatterns(params) {
    const patterns = new Set();
    for (const segment of params.segments) {
        collectAllowAlwaysPatterns({
            segment,
            cwd: params.cwd,
            env: params.env,
            platform: params.platform,
            depth: 0,
            out: patterns,
        });
    }
    return Array.from(patterns);
}
/**
 * Evaluates allowlist for shell commands (including &&, ||, ;) and returns analysis metadata.
 */
export function evaluateShellAllowlist(params) {
    const chainParts = isWindowsPlatform(params.platform) ? null : splitCommandChain(params.command);
    if (!chainParts) {
        const analysis = analyzeShellCommand({
            command: params.command,
            cwd: params.cwd,
            env: params.env,
            platform: params.platform,
        });
        if (!analysis.ok) {
            return {
                analysisOk: false,
                allowlistSatisfied: false,
                allowlistMatches: [],
                segments: [],
                segmentSatisfiedBy: [],
            };
        }
        const evaluation = evaluateExecAllowlist({
            analysis,
            allowlist: params.allowlist,
            safeBins: params.safeBins,
            cwd: params.cwd,
            skillBins: params.skillBins,
            autoAllowSkills: params.autoAllowSkills,
        });
        return {
            analysisOk: true,
            allowlistSatisfied: evaluation.allowlistSatisfied,
            allowlistMatches: evaluation.allowlistMatches,
            segments: analysis.segments,
            segmentSatisfiedBy: evaluation.segmentSatisfiedBy,
        };
    }
    const allowlistMatches = [];
    const segments = [];
    const segmentSatisfiedBy = [];
    for (const part of chainParts) {
        const analysis = analyzeShellCommand({
            command: part,
            cwd: params.cwd,
            env: params.env,
            platform: params.platform,
        });
        if (!analysis.ok) {
            return {
                analysisOk: false,
                allowlistSatisfied: false,
                allowlistMatches: [],
                segments: [],
                segmentSatisfiedBy: [],
            };
        }
        segments.push(...analysis.segments);
        const evaluation = evaluateExecAllowlist({
            analysis,
            allowlist: params.allowlist,
            safeBins: params.safeBins,
            cwd: params.cwd,
            skillBins: params.skillBins,
            autoAllowSkills: params.autoAllowSkills,
        });
        allowlistMatches.push(...evaluation.allowlistMatches);
        segmentSatisfiedBy.push(...evaluation.segmentSatisfiedBy);
        if (!evaluation.allowlistSatisfied) {
            return {
                analysisOk: true,
                allowlistSatisfied: false,
                allowlistMatches,
                segments,
                segmentSatisfiedBy,
            };
        }
    }
    return {
        analysisOk: true,
        allowlistSatisfied: true,
        allowlistMatches,
        segments,
        segmentSatisfiedBy,
    };
}
