// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * Gates Utility Helpers
 *
 * WU-1647: Extracted utility functions used across gate runners,
 * plan resolvers, and the gates orchestrator.
 *
 * @module gates-utils
 */
import { execSync, spawnSync } from 'node:child_process';
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, writeSync, } from 'node:fs';
import { access } from 'node:fs/promises';
import path from 'node:path';
import { buildGatesLogPath } from '@lumenflow/core/gates-agent-mode';
import { createGitForPath } from '@lumenflow/core/git-adapter';
import { getCurrentWU } from '@lumenflow/core/telemetry';
import { createWuPaths } from '@lumenflow/core/wu-paths';
import { readWURaw } from '@lumenflow/core/wu-yaml';
import { PKG_MANAGER, SCRIPTS, GIT_REFS, FILE_SYSTEM, EXIT_CODES, PRETTIER_ARGS, PRETTIER_FLAGS, } from '@lumenflow/core/wu-constants';
import { LOG_TAIL_MAX_LINES, LOG_TAIL_MAX_BYTES } from './constants.js';
const TURBO_COMMAND = 'turbo';
const TURBO_LS_OUTPUT_FLAG = '--output';
const TURBO_LS_JSON_MODE = 'json';
const TURBO_JSON_PAYLOAD_START = '{';
const PACKAGE_JSON_FILE = 'package.json';
const TEST_SCRIPT_NAME = 'test';
// ── Shell command builders ─────────────────────────────────────────────
/**
 * Build a pnpm command string
 */
export function pnpmCmd(...parts) {
    return `${PKG_MANAGER} ${parts.join(' ')}`;
}
/**
 * Build a pnpm run command string
 */
export function pnpmRun(script, ...args) {
    const argsStr = args.length > 0 ? ` ${args.join(' ')}` : '';
    return `${PKG_MANAGER} ${SCRIPTS.RUN} ${script}${argsStr}`;
}
// ── Path/string helpers ────────────────────────────────────────────────
export function normalizePath(filePath) {
    return filePath.replace(/\\/g, '/');
}
export function getBasename(filePath) {
    const normalized = normalizePath(filePath);
    const parts = normalized.split('/');
    return parts[parts.length - 1] || normalized;
}
export function quoteShellArgs(files) {
    return files.map((file) => `"${file}"`).join(' ');
}
// ── Prettier helpers ───────────────────────────────────────────────────
const PRETTIER_NON_FILE_OUTPUT_MARKERS = [
    'code style issues found',
    'all matched files use prettier',
    'checking formatting',
    'is a symbolic link',
];
function isNonFilePrettierOutputLine(line) {
    const normalizedLine = line.toLowerCase();
    return PRETTIER_NON_FILE_OUTPUT_MARKERS.some((marker) => normalizedLine.includes(marker));
}
export function parsePrettierListOutput(output) {
    if (!output)
        return [];
    return output
        .split(/\r?\n/)
        .map((line) => line.trim())
        .filter(Boolean)
        .map((line) => line.replace(/^\[error\]\s*/i, '').trim())
        .filter((line) => !isNonFilePrettierOutputLine(line));
}
export function buildPrettierWriteCommand(files) {
    const quotedFiles = files.map((file) => `"${file}"`).join(' ');
    const base = pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_FLAGS.WRITE);
    return quotedFiles ? `${base} ${quotedFiles}` : base;
}
export function buildPrettierCheckCommand(files) {
    const filesArg = files.length > 0 ? quoteShellArgs(files) : '.';
    return pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_ARGS.CHECK, filesArg);
}
export function formatFormatCheckGuidance(files) {
    if (!files.length)
        return [];
    const command = buildPrettierWriteCommand(files);
    return [
        '',
        '\u274C format:check failed',
        'Fix with:',
        `  ${command}`,
        '',
        'Affected files:',
        ...files.map((file) => `  - ${file}`),
        '',
    ];
}
export function collectPrettierListDifferent(cwd, files = []) {
    const filesArg = files.length > 0 ? quoteShellArgs(files) : '.';
    const cmd = pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_ARGS.LIST_DIFFERENT, filesArg);
    const result = spawnSync(cmd, [], {
        shell: true,
        cwd,
        encoding: FILE_SYSTEM.ENCODING,
    });
    const output = `${result.stdout || ''}\n${result.stderr || ''}`;
    return parsePrettierListOutput(output);
}
export function emitFormatCheckGuidance({ agentLog, useAgentMode, files, cwd, }) {
    const formattedFiles = collectPrettierListDifferent(cwd, files ?? []);
    if (!formattedFiles.length)
        return;
    const lines = formatFormatCheckGuidance(formattedFiles);
    const logLine = useAgentMode && agentLog
        ? (line) => writeSync(agentLog.logFd, `${line}\n`)
        : (line) => console.log(line);
    for (const line of lines) {
        logLine(line);
    }
}
// ── Process execution ──────────────────────────────────────────────────
export function run(cmd, { agentLog, cwd = process.cwd(), } = {}) {
    const start = Date.now();
    if (!agentLog) {
        console.log(`\n> ${cmd}\n`);
        try {
            // Pre-existing: execSync is intentional here for synchronous gate execution
            execSync(cmd, {
                stdio: 'inherit',
                encoding: FILE_SYSTEM.ENCODING,
                cwd,
            });
            return { ok: true, duration: Date.now() - start };
        }
        catch {
            return { ok: false, duration: Date.now() - start };
        }
    }
    writeSync(agentLog.logFd, `\n> ${cmd}\n\n`);
    const result = spawnSync(cmd, [], {
        shell: true,
        stdio: ['ignore', agentLog.logFd, agentLog.logFd],
        cwd,
        encoding: FILE_SYSTEM.ENCODING,
    });
    return { ok: result.status === EXIT_CODES.SUCCESS, duration: Date.now() - start };
}
// ── Logging ────────────────────────────────────────────────────────────
export function makeGateLogger({ agentLog, useAgentMode }) {
    return (line) => {
        if (!useAgentMode) {
            console.log(line);
            return;
        }
        if (agentLog) {
            writeSync(agentLog.logFd, `${line}\n`);
        }
    };
}
export function readLogTail(logPath, { maxLines = LOG_TAIL_MAX_LINES, maxBytes = LOG_TAIL_MAX_BYTES } = {}) {
    try {
        const stats = statSync(logPath);
        const startPos = Math.max(0, stats.size - maxBytes);
        const bytesToRead = stats.size - startPos;
        const fd = openSync(logPath, 'r');
        try {
            const buffer = Buffer.alloc(bytesToRead);
            readSync(fd, buffer, 0, bytesToRead, startPos);
            const text = buffer.toString(FILE_SYSTEM.ENCODING);
            const lines = text.split(/\r?\n/).filter(Boolean);
            return lines.slice(-maxLines).join('\n');
        }
        finally {
            closeSync(fd);
        }
    }
    catch {
        return '';
    }
}
export function createAgentLogContext({ wuId, lane, cwd, }) {
    const logPath = buildGatesLogPath({
        cwd,
        env: process.env,
        wuId: wuId ?? undefined,
        lane: lane ?? undefined,
    });
    mkdirSync(path.dirname(logPath), { recursive: true });
    const logFd = openSync(logPath, 'a');
    const header = `# gates log\n# lane: ${lane || 'unknown'}\n# wu: ${wuId || 'unknown'}\n# started: ${new Date().toISOString()}\n\n`;
    writeSync(logFd, header);
    // Ensure we close the FD even if gates exit via die().
    process.on('exit', () => {
        try {
            closeSync(logFd);
        }
        catch {
            // ignore
        }
    });
    return { logPath, logFd };
}
// ── File system helpers ────────────────────────────────────────────────
export async function filterExistingFiles(files, cwd = process.cwd()) {
    const existingFiles = await Promise.all(files.map(async (file) => {
        const filePath = path.isAbsolute(file) ? file : path.resolve(cwd, file);
        try {
            await access(filePath);
            return file;
        }
        catch {
            return null;
        }
    }));
    return existingFiles.filter((file) => Boolean(file));
}
// ── Git helpers ────────────────────────────────────────────────────────
export async function getChangedFilesForIncremental({ git, baseBranch = GIT_REFS.ORIGIN_MAIN, }) {
    const mergeBase = await git.mergeBase('HEAD', baseBranch);
    const committedOutput = await git.raw(['diff', '--name-only', `${mergeBase}...HEAD`]);
    const committedFiles = committedOutput
        .split('\n')
        .map((f) => f.trim())
        .filter(Boolean);
    const unstagedOutput = await git.raw(['diff', '--name-only']);
    const unstagedFiles = unstagedOutput
        .split('\n')
        .map((f) => f.trim())
        .filter(Boolean);
    const untrackedOutput = await git.raw(['ls-files', '--others', '--exclude-standard']);
    const untrackedFiles = untrackedOutput
        .split('\n')
        .map((f) => f.trim())
        .filter(Boolean);
    return [...new Set([...committedFiles, ...unstagedFiles, ...untrackedFiles])];
}
export async function getAllChangedFiles(options = {}) {
    const { git = createGitForPath(options.cwd ?? process.cwd()) } = options;
    try {
        return await getChangedFilesForIncremental({ git });
    }
    catch (error) {
        console.error('\u26A0\uFE0F  Failed to get changed files:', error.message);
        return [];
    }
}
// ── WU helpers ─────────────────────────────────────────────────────────
/**
 * Parse a WU ID from a branch name.
 * Returns canonical upper-case ID (e.g., WU-123) or null when not present.
 */
export function parseWUFromBranchName(branchName) {
    if (!branchName) {
        return null;
    }
    const match = branchName.match(/wu-(\d+)/i);
    if (!match) {
        return null;
    }
    return `WU-${match[1]}`.toUpperCase();
}
export async function detectCurrentWUForCwd(cwd) {
    const workingDir = cwd ?? process.cwd();
    try {
        const branch = await createGitForPath(workingDir).getCurrentBranch();
        const parsed = parseWUFromBranchName(branch);
        if (parsed) {
            return parsed;
        }
    }
    catch {
        // Fall back to legacy process-cwd based resolver below.
    }
    return getCurrentWU();
}
// ── Package extraction (WU-1299) ──────────────────────────────────────
/**
 * WU-1299: Extract package name from a single code path
 */
function extractPackageFromPath(codePath) {
    if (!codePath || typeof codePath !== 'string') {
        return null;
    }
    const normalized = codePath.replace(/\\/g, '/');
    // Handle packages/@scope/name/... or packages/name/...
    if (normalized.startsWith('packages/')) {
        const parts = normalized.slice('packages/'.length).split('/');
        // Scoped package (@scope/name)
        if (parts[0]?.startsWith('@') && parts[1]) {
            return `${parts[0]}/${parts[1]}`;
        }
        // Unscoped package
        if (parts[0]) {
            return parts[0];
        }
    }
    // WU-1415: Skip apps/ paths - they aren't valid turbo packages for test filtering
    return null;
}
const DOCUMENTATION_FILE_EXTENSIONS = new Set(['.md', '.mdx', '.rst', '.txt']);
const GLOB_SEGMENT_PATTERN = /[*{[]/;
function isDocumentationPath(codePath) {
    const normalized = normalizePath(codePath);
    if (normalized.startsWith('docs/')) {
        return true;
    }
    const extension = path.extname(normalized).toLowerCase();
    return DOCUMENTATION_FILE_EXTENSIONS.has(extension);
}
function sanitizePackageLookupPath(codePath) {
    const normalized = normalizePath(codePath).replace(/\/+$/, '');
    const segments = normalized.split('/').filter(Boolean);
    const safeSegments = [];
    for (const segment of segments) {
        if (GLOB_SEGMENT_PATTERN.test(segment)) {
            break;
        }
        safeSegments.push(segment);
    }
    return safeSegments.join('/');
}
function resolvePackageNameFromManifest(packageJsonPath) {
    try {
        const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
        return typeof packageJson.name === 'string' && packageJson.name.length > 0
            ? packageJson.name
            : null;
    }
    catch {
        return null;
    }
}
function extractPackageFromRepoShape(codePath, cwd) {
    if (!cwd || isDocumentationPath(codePath)) {
        return null;
    }
    const sanitizedPath = sanitizePackageLookupPath(codePath);
    if (!sanitizedPath) {
        return null;
    }
    const absoluteRoot = path.resolve(cwd);
    const candidatePath = path.resolve(absoluteRoot, sanitizedPath);
    let currentPath = path.extname(candidatePath) ? path.dirname(candidatePath) : candidatePath;
    while (currentPath.startsWith(absoluteRoot)) {
        const packageJsonPath = path.join(currentPath, 'package.json');
        if (existsSync(packageJsonPath)) {
            return (resolvePackageNameFromManifest(packageJsonPath) ||
                path.basename(currentPath === absoluteRoot ? absoluteRoot : currentPath));
        }
        if (currentPath === absoluteRoot) {
            break;
        }
        const parentPath = path.dirname(currentPath);
        if (parentPath === currentPath) {
            break;
        }
        currentPath = parentPath;
    }
    return null;
}
/**
 * WU-1299: Extract package/app names from code_paths
 */
export function extractPackagesFromCodePaths(codePaths, options = {}) {
    if (!codePaths || !Array.isArray(codePaths) || codePaths.length === 0) {
        return [];
    }
    const packages = new Set();
    for (const codePath of codePaths) {
        const pkg = extractPackageFromPath(codePath) || extractPackageFromRepoShape(codePath, options.cwd);
        if (pkg) {
            packages.add(pkg);
        }
    }
    return Array.from(packages);
}
function parseTurboWorkspaceList(output) {
    const payloadStart = output.indexOf(TURBO_JSON_PAYLOAD_START);
    if (payloadStart === -1) {
        return null;
    }
    const payloadEnd = output.lastIndexOf('}');
    if (payloadEnd === -1 || payloadEnd < payloadStart) {
        return null;
    }
    try {
        return JSON.parse(output.slice(payloadStart, payloadEnd + 1));
    }
    catch {
        return null;
    }
}
function loadTurboWorkspacePackagePaths(cwd) {
    const result = spawnSync(PKG_MANAGER, [TURBO_COMMAND, 'ls', TURBO_LS_OUTPUT_FLAG, TURBO_LS_JSON_MODE], {
        cwd,
        encoding: FILE_SYSTEM.ENCODING,
    });
    if (result.status !== EXIT_CODES.SUCCESS) {
        return new Map();
    }
    const parsed = parseTurboWorkspaceList(result.stdout ?? '') ?? parseTurboWorkspaceList(result.stderr ?? '');
    const items = parsed?.packages?.items;
    if (!items) {
        return new Map();
    }
    const packagePaths = new Map();
    for (const item of items) {
        if (typeof item.name !== 'string' || typeof item.path !== 'string') {
            continue;
        }
        packagePaths.set(item.name, path.resolve(cwd, item.path));
    }
    return packagePaths;
}
function packageHasRunnableTestScript(packageRoot) {
    const packageJsonPath = path.join(packageRoot, PACKAGE_JSON_FILE);
    if (!existsSync(packageJsonPath)) {
        return false;
    }
    try {
        const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
        return typeof packageJson.scripts?.[TEST_SCRIPT_NAME] === 'string';
    }
    catch {
        return false;
    }
}
export function resolveFilterableDocsOnlyPackages(packages, options = {}) {
    if (!Array.isArray(packages) || packages.length === 0) {
        return [];
    }
    const cwd = options.cwd ?? process.cwd();
    const workspacePackagePaths = loadTurboWorkspacePackagePaths(cwd);
    return packages.filter((packageName) => {
        const packageRoot = workspacePackagePaths.get(packageName);
        if (!packageRoot) {
            return false;
        }
        return packageHasRunnableTestScript(packageRoot);
    });
}
/**
 * WU-1299: Load code_paths from current WU YAML
 */
export function loadCurrentWUCodePaths(options = {}) {
    const cwd = options.cwd ?? process.cwd();
    const wuId = getCurrentWU();
    if (!wuId) {
        return [];
    }
    try {
        const wuPaths = createWuPaths({ projectRoot: cwd });
        const wuYamlPath = wuPaths.WU(wuId);
        const wuDoc = readWURaw(wuYamlPath);
        if (wuDoc && Array.isArray(wuDoc.code_paths)) {
            return wuDoc.code_paths.filter((p) => typeof p === 'string');
        }
    }
    catch {
        // WU YAML not found or unreadable - return empty array
    }
    return [];
}
//# sourceMappingURL=gates-utils.js.map