/*
 * Copyright 2026, Jaime Terrats.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { execFileSync } from 'node:child_process';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { join, relative } from 'node:path';
/**
 * Scan a project directory for `.env*` files and environment variable references in code.
 * Returns a markdown report suitable for security review.
 */
export function generateEnvAudit(cwd) {
    const envFiles = findEnvFiles(cwd);
    const codeRefs = findEnvRefsInCode(cwd);
    const exampleVars = parseEnvExample(cwd);
    const missingFromExample = findMissingFromExample(codeRefs, exampleVars);
    const lines = [
        '# Environment Variable Audit',
        '',
        `> Generated by \`sf setup-agents local\` — ${new Date().toISOString().slice(0, 10)}`,
        '',
        '## .env Files Found',
        '',
        ...formatEnvFilesSection(envFiles),
        '',
        '## Environment Variables Referenced in Code',
        '',
        ...formatCodeRefsSection(codeRefs),
        '',
        '## Variables Missing from .env.example',
        '',
        ...formatMissingSection(exampleVars, missingFromExample),
        '',
    ];
    return lines.join('\n');
}
function formatEnvFilesSection(envFiles) {
    if (envFiles.length === 0) {
        return ['No `.env*` files found in this project.'];
    }
    return [
        '| File | Tracked by Git |',
        '|------|----------------|',
        ...envFiles.map((f) => `| \`${f.relativePath}\` | ${f.gitTracked ? 'YES (risk)' : 'no'} |`),
    ];
}
function formatCodeRefsSection(codeRefs) {
    if (codeRefs.length === 0) {
        return ['No environment variable references found in source code.'];
    }
    return [
        '| Variable | Source | File |',
        '|----------|--------|------|',
        ...codeRefs.map((ref) => `| \`${ref.name}\` | ${ref.source} | \`${ref.file}\` |`),
    ];
}
function formatMissingSection(exampleVars, missingFromExample) {
    if (!exampleVars) {
        return ['No `.env.example` file found. Consider creating one to document required variables.'];
    }
    if (missingFromExample.length === 0) {
        return ['All referenced variables have a corresponding entry in `.env.example`.'];
    }
    return [
        'The following variables are referenced in code but have no entry in `.env.example`:',
        '',
        ...missingFromExample.map((name) => `- \`${name}\``),
    ];
}
function findEnvFiles(cwd) {
    const gitTrackedFiles = getGitTrackedFiles(cwd);
    const rootFiles = collectEnvFilesInDir(cwd, '', gitTrackedFiles);
    const subdirFiles = ['config', 'env', 'environments', 'deploy'].flatMap((subdir) => {
        if (!existsSync(join(cwd, subdir)))
            return [];
        return collectEnvFilesInDir(join(cwd, subdir), subdir, gitTrackedFiles);
    });
    return [...rootFiles, ...subdirFiles];
}
function collectEnvFilesInDir(dir, prefix, gitTrackedFiles) {
    try {
        return readdirSync(dir, { withFileTypes: true })
            .filter((entry) => entry.isFile() && entry.name.startsWith('.env'))
            .map((entry) => {
            const rel = prefix ? join(prefix, entry.name) : entry.name;
            return { relativePath: rel, gitTracked: gitTrackedFiles.has(rel) };
        });
    }
    catch {
        return [];
    }
}
function getGitTrackedFiles(cwd) {
    try {
        const output = execFileSync('git', ['ls-files'], { cwd, encoding: 'utf8', timeout: 5000 });
        return new Set(output.split('\n').filter(Boolean));
    }
    catch {
        return new Set();
    }
}
function findEnvRefsInCode(cwd) {
    const refs = [];
    const seen = new Set();
    const scanDirs = ['src', 'lib', 'scripts', 'bin', 'app', 'server', 'config'];
    for (const dir of scanDirs) {
        const fullDir = join(cwd, dir);
        if (!existsSync(fullDir))
            continue;
        scanDirectory(fullDir, cwd, refs, seen);
    }
    // Also scan shell scripts at root
    try {
        const rootEntries = readdirSync(cwd, { withFileTypes: true });
        for (const entry of rootEntries) {
            if (entry.isFile() && (entry.name.endsWith('.sh') || entry.name === 'Makefile' || entry.name === 'Dockerfile')) {
                scanFile(join(cwd, entry.name), cwd, refs, seen);
            }
        }
    }
    catch {
        // Skip
    }
    return refs;
}
function scanDirectory(dir, cwd, refs, seen) {
    try {
        const entries = readdirSync(dir, { withFileTypes: true });
        for (const entry of entries) {
            const fullPath = join(dir, entry.name);
            if (entry.isDirectory()) {
                if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist')
                    continue;
                scanDirectory(fullPath, cwd, refs, seen);
            }
            else if (entry.isFile()) {
                scanFile(fullPath, cwd, refs, seen);
            }
        }
    }
    catch {
        // Skip unreadable dirs
    }
}
const CODE_EXTENSIONS = new Set(['.ts', '.js', '.mts', '.mjs', '.cjs', '.sh', '.bash', '.yml', '.yaml', '.py']);
function scanFile(filePath, cwd, refs, seen) {
    const ext = filePath.slice(filePath.lastIndexOf('.'));
    const basename = filePath.slice(filePath.lastIndexOf('/') + 1);
    if (!CODE_EXTENSIONS.has(ext) && basename !== 'Makefile' && basename !== 'Dockerfile')
        return;
    try {
        const content = readFileSync(filePath, 'utf8');
        const rel = relative(cwd, filePath);
        collectRegexMatches(content, /process\.env\.([A-Z_][A-Z0-9_]*)/g, 'process.env', rel, refs, seen);
        collectRegexMatches(content, /process\.env\[['"]([A-Z_][A-Z0-9_]*)['"]\]/g, 'process.env[]', rel, refs, seen);
        const isShellLike = ext === '.sh' || ext === '.bash' || basename === 'Makefile' || basename === 'Dockerfile';
        if (isShellLike) {
            collectShellVarMatches(content, rel, refs, seen);
        }
    }
    catch {
        // Skip unreadable files
    }
}
function collectRegexMatches(content, regex, source, rel, refs, seen) {
    let match;
    while ((match = regex.exec(content)) !== null) {
        const key = `${match[1]}:${rel}`;
        if (seen.has(key))
            continue;
        seen.add(key);
        refs.push({ name: match[1], source, file: rel });
    }
}
function collectShellVarMatches(content, rel, refs, seen) {
    const shellVarRegex = /\$\{?([A-Z_][A-Z0-9_]*)\}?/g;
    let match;
    while ((match = shellVarRegex.exec(content)) !== null) {
        if (SHELL_BUILTINS.has(match[1]))
            continue;
        const key = `${match[1]}:${rel}`;
        if (seen.has(key))
            continue;
        seen.add(key);
        refs.push({ name: match[1], source: 'shell $VAR', file: rel });
    }
}
const SHELL_BUILTINS = new Set([
    'HOME',
    'USER',
    'PATH',
    'PWD',
    'SHELL',
    'TERM',
    'LANG',
    'HOSTNAME',
    'OLDPWD',
    'IFS',
    'RANDOM',
    'LINENO',
    'SECONDS',
    'BASH',
    'BASH_VERSION',
    'BASH_SOURCE',
    'FUNCNAME',
    'PIPESTATUS',
    'OSTYPE',
    'MACHTYPE',
    'HOSTTYPE',
    'UID',
    'EUID',
    'GROUPS',
    'PPID',
    'SHLVL',
]);
function parseEnvExample(cwd) {
    const examplePath = join(cwd, '.env.example');
    if (!existsSync(examplePath))
        return null;
    try {
        const content = readFileSync(examplePath, 'utf8');
        const vars = new Set();
        for (const line of content.split('\n')) {
            const trimmed = line.trim();
            if (!trimmed || trimmed.startsWith('#'))
                continue;
            const eqIdx = trimmed.indexOf('=');
            if (eqIdx > 0) {
                vars.add(trimmed.slice(0, eqIdx).trim());
            }
        }
        return vars;
    }
    catch {
        return null;
    }
}
function findMissingFromExample(refs, exampleVars) {
    if (!exampleVars)
        return [];
    const referencedNames = new Set(refs.map((r) => r.name));
    const missing = [];
    for (const name of referencedNames) {
        if (!exampleVars.has(name)) {
            missing.push(name);
        }
    }
    return missing.sort();
}
//# sourceMappingURL=env-audit-content.js.map