import { realpath } from 'node:fs/promises';
import { dirname, isAbsolute, relative, resolve as resolvePath } from 'node:path';
import { expandPath, fileExists } from '../../shared/files/path-utils.js';
async function resolveNearestExistingPath(filePath) {
    let current = filePath;
    const segments = [];
    while (!fileExists(current)) {
        const parent = dirname(current);
        if (parent === current) {
            return resolvePath(filePath);
        }
        segments.unshift(current.slice(parent.length + (parent.endsWith('/') ? 0 : 1)));
        current = parent;
    }
    const canonical = await realpath(current);
    return segments.length > 0 ? resolvePath(canonical, ...segments) : canonical;
}
function isWithinRoot(rootPath, targetPath) {
    const rel = relative(rootPath, targetPath);
    return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
}
async function resolveWorkspaceRoots(workspaceRoots) {
    const canonicalRoots = await Promise.all(workspaceRoots.map(async (root) => resolveNearestExistingPath(resolvePath(expandPath(root)))));
    return [...new Set(canonicalRoots)];
}
function allowsRead(scope) {
    return scope === 'workspace' || scope === 'unrestricted';
}
function allowsWrite(scope) {
    return scope === 'workspace' || scope === 'unrestricted';
}
const SENSITIVE_DIRECTORY_SEGMENTS = new Set([
    '.git',
    '.vscode',
    '.idea',
    '.ssh',
    '.aws',
    '.config/gcloud'
]);
const SENSITIVE_FILE_BASENAMES = new Set([
    '.gitconfig',
    '.gitmodules',
    '.bashrc',
    '.bash_profile',
    '.zshrc',
    '.zprofile',
    '.profile'
]);
function normalizePathForMatching(filePath) {
    return filePath.replace(/\\/g, '/').replace(/\/+/g, '/').toLowerCase();
}
function getSensitivePathReason(canonicalPath, access) {
    if (access !== 'write') {
        return undefined;
    }
    const normalizedPath = normalizePathForMatching(canonicalPath);
    const segments = normalizedPath.split('/').filter(Boolean);
    const fileName = segments.at(-1) ?? '';
    if (fileName === '.env' ||
        fileName.startsWith('.env.') ||
        SENSITIVE_FILE_BASENAMES.has(fileName)) {
        return `Writing to a sensitive file requires explicit approval: ${canonicalPath}`;
    }
    if (normalizedPath.includes('/.ssh/') ||
        normalizedPath.includes('/.aws/credentials') ||
        normalizedPath.includes('/.aws/config')) {
        return `Writing to a sensitive credentials path requires explicit approval: ${canonicalPath}`;
    }
    for (const segment of SENSITIVE_DIRECTORY_SEGMENTS) {
        if (normalizedPath === `/${segment}` ||
            normalizedPath.endsWith(`/${segment}`) ||
            normalizedPath.includes(`/${segment}/`)) {
            return `Writing to a sensitive path requires explicit approval: ${canonicalPath}`;
        }
    }
    return undefined;
}
export async function getFilesystemAccessDecision(options) {
    const { path, policy, access } = options;
    if (access === 'read') {
        if (!allowsRead(policy.defaultReadScope)) {
            return {
                outcome: 'deny',
                canonicalPath: await resolveNearestExistingPath(path),
                canonicalWorkspaceRoots: await resolveWorkspaceRoots(policy.workspaceRoots),
                reason: `Reading files is denied by the current permission policy: ${path}`
            };
        }
    }
    else if (!allowsWrite(policy.defaultWriteScope)) {
        return {
            outcome: 'deny',
            canonicalPath: await resolveNearestExistingPath(path),
            canonicalWorkspaceRoots: await resolveWorkspaceRoots(policy.workspaceRoots),
            reason: `Writing files is denied by the current permission policy: ${path}`
        };
    }
    const workspaceScoped = access === 'read'
        ? policy.defaultReadScope === 'workspace'
        : policy.defaultWriteScope === 'workspace';
    const [canonicalPath, canonicalWorkspaceRoots] = await Promise.all([
        resolveNearestExistingPath(path),
        resolveWorkspaceRoots(policy.workspaceRoots)
    ]);
    if (!workspaceScoped) {
        const sensitiveReason = getSensitivePathReason(canonicalPath, access);
        if (sensitiveReason) {
            return {
                outcome: 'require_approval',
                canonicalPath,
                canonicalWorkspaceRoots,
                allowPersistentGrant: false,
                reason: sensitiveReason
            };
        }
        return { outcome: 'allow', canonicalPath, canonicalWorkspaceRoots };
    }
    const withinWorkspace = canonicalWorkspaceRoots.some((root) => isWithinRoot(root, canonicalPath));
    const sensitiveReason = getSensitivePathReason(canonicalPath, access);
    if (sensitiveReason) {
        return {
            outcome: 'require_approval',
            canonicalPath,
            canonicalWorkspaceRoots,
            allowPersistentGrant: false,
            reason: sensitiveReason
        };
    }
    if (withinWorkspace) {
        return { outcome: 'allow', canonicalPath, canonicalWorkspaceRoots };
    }
    if (policy.requireApprovalOutsideWorkspace) {
        return {
            outcome: 'require_approval',
            canonicalPath,
            canonicalWorkspaceRoots,
            allowPersistentGrant: true,
            fileGrantPath: canonicalPath,
            folderGrantPath: dirname(canonicalPath),
            reason: `Path is outside the approved workspaces and requires explicit approval: ${path}`
        };
    }
    return { outcome: 'allow', canonicalPath, canonicalWorkspaceRoots };
}
async function hasFilesystemApprovalGrant(grants, canonicalPath, access) {
    if (!grants || grants.length === 0) {
        return false;
    }
    const canonicalGrantPaths = await Promise.all(grants.map(async (grant) => ({
        access: grant.access,
        scope: grant.scope,
        canonicalPath: await resolveNearestExistingPath(resolvePath(expandPath(grant.path)))
    })));
    return canonicalGrantPaths.some((grant) => {
        if (grant.access !== access && !(grant.access === 'write' && access === 'read')) {
            return false;
        }
        if (grant.scope === 'file') {
            return grant.canonicalPath === canonicalPath;
        }
        return isWithinRoot(grant.canonicalPath, canonicalPath);
    });
}
export async function enforceFilesystemPolicy(options) {
    const { path, policy, access, approvalGrants, sessionState } = options;
    const decision = await getFilesystemAccessDecision({ path, policy, access });
    if (decision.outcome === 'allow') {
        return;
    }
    if (decision.outcome === 'require_approval') {
        const sessionApprovalGrants = sessionState?.getFilesystemApprovalGrants();
        if ((await hasFilesystemApprovalGrant(policy.persistentApprovals, decision.canonicalPath, access)) ||
            (await hasFilesystemApprovalGrant(sessionApprovalGrants, decision.canonicalPath, access)) ||
            (await hasFilesystemApprovalGrant(approvalGrants, decision.canonicalPath, access))) {
            return;
        }
        throw new Error(decision.reason);
    }
    throw new Error(decision.reason);
}
//# sourceMappingURL=filesystem-policy.js.map