#!/usr/bin/env node
// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * Trace Generator CLI Command
 *
 * Creates traceability reports linking WUs to code changes.
 * Useful for audit trails, compliance documentation, and understanding
 * what code was changed as part of each WU.
 *
 * WU-1112: INIT-003 Phase 6 - Migrate remaining Tier 1 tools
 * WU-1534: Harden CLI command execution surfaces - argv-based git invocations
 *
 * Usage:
 *   pnpm trace:gen --wu WU-1112
 *   pnpm trace:gen --since 2024-01-01 --format json
 *   pnpm trace:gen --format markdown --output trace.md
 */
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
import { parse as parseYaml } from 'yaml';
import { EXIT_CODES, FILE_SYSTEM } from '@lumenflow/core/wu-constants';
import { WU_PATHS } from '@lumenflow/core/wu-paths';
import { runCLI } from './cli-entry-point.js';
import { MAX_WU_ID_LENGTH, COMMIT_SHA_DISPLAY_LENGTH, MARKDOWN_TABLE_TITLE_LENGTH, TRACE_FILES_DISPLAY_LIMIT, JSON_INDENT, } from './constants.js';
/** Log prefix for console output */
const LOG_PREFIX = '[trace:gen]';
/**
 * Regex for validating WU IDs.
 * Valid format: WU-<digits> (e.g., WU-1112, WU-1, WU-99999)
 */
const WU_ID_PATTERN = /^WU-\d+$/;
/**
 * Output formats for trace report
 */
export var TraceFormat;
(function (TraceFormat) {
    TraceFormat["JSON"] = "json";
    TraceFormat["MARKDOWN"] = "markdown";
    TraceFormat["CSV"] = "csv";
})(TraceFormat || (TraceFormat = {}));
/**
 * Validate a WU ID to ensure it matches the expected format.
 *
 * WU-1534: Input validation layer (defense-in-depth alongside argv-based execution).
 *
 * @param wuId - WU ID to validate
 * @returns true if the WU ID matches the WU-<digits> pattern
 */
export function validateWuId(wuId) {
    if (!wuId || wuId.length > MAX_WU_ID_LENGTH) {
        return false;
    }
    return WU_ID_PATTERN.test(wuId);
}
/**
 * Build argv array for git log commit lookup.
 *
 * WU-1534: Returns an argv array for use with execFileSync (no shell).
 * The WU ID is passed as a literal --grep= value, not interpolated into a shell string.
 *
 * @param wuId - WU ID to search for in commit messages
 * @returns Argv array for execFileSync('git', args)
 */
export function buildGitLogArgs(wuId) {
    return ['log', '--all', '--oneline', '--date=iso-strict', '--format=%H|%ad|%s', `--grep=${wuId}`];
}
/**
 * Build argv array for git log file listing.
 *
 * WU-1534: Returns an argv array for use with execFileSync (no shell).
 * Replaces the previous shell pipe `| sort -u` with in-process dedup.
 *
 * @param wuId - WU ID to search for in commit messages
 * @returns Argv array for execFileSync('git', args)
 */
export function buildGitFilesArgs(wuId) {
    return ['log', '--all', '--name-only', '--format=', `--grep=${wuId}`];
}
/**
 * Parse command line arguments for trace-gen
 *
 * @param argv - Process argv array
 * @returns Parsed arguments
 */
export function parseTraceArgs(argv) {
    const args = {};
    // Skip node and script name
    const cliArgs = argv.slice(2);
    for (let i = 0; i < cliArgs.length; i++) {
        const arg = cliArgs[i];
        if (arg === '--help' || arg === '-h') {
            args.help = true;
        }
        else if (arg === '--wu' || arg === '-w') {
            args.wuId = cliArgs[++i];
        }
        else if (arg === '--format' || arg === '-f') {
            args.format = cliArgs[++i];
        }
        else if (arg === '--output' || arg === '-o') {
            args.output = cliArgs[++i];
        }
        else if (arg === '--since' || arg === '-s') {
            args.since = cliArgs[++i];
        }
    }
    return args;
}
/**
 * Build a trace entry from WU and commit data
 *
 * @param input - Input data containing WU info, commits, and files
 * @returns Trace entry with summary statistics
 */
export function buildTraceEntry(input) {
    const { wuId, title, status, commits, files } = input;
    const entry = {
        wuId,
        title,
        status,
        commitCount: commits.length,
        fileCount: files.length,
    };
    if (commits.length > 0) {
        // Sort commits by date
        const sorted = [...commits].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
        entry.firstCommit = sorted[0].date;
        entry.lastCommit = sorted[sorted.length - 1].date;
        entry.commits = sorted;
    }
    if (files.length > 0) {
        entry.files = files;
    }
    return entry;
}
/**
 * Get commits for a WU by searching git log.
 *
 * WU-1534: Uses execFileSync with argv array (no shell interpolation).
 */
function getWuCommits(wuId) {
    try {
        const gitArgs = buildGitLogArgs(wuId);
        // eslint-disable-next-line sonarjs/no-os-command-from-path -- git resolved from PATH; CLI tool requires git
        const output = execFileSync('git', gitArgs, {
            encoding: FILE_SYSTEM.ENCODING,
        });
        const commits = [];
        for (const line of output.trim().split('\n')) {
            if (!line)
                continue;
            const [sha, date, ...messageParts] = line.split('|');
            commits.push({
                sha: sha.slice(0, COMMIT_SHA_DISPLAY_LENGTH),
                date,
                message: messageParts.join('|'),
            });
        }
        return commits;
    }
    catch {
        return [];
    }
}
/**
 * Get files changed by a WU.
 *
 * WU-1534: Uses execFileSync with argv array (no shell interpolation).
 * Dedup/sort done in-process instead of shell pipe `| sort -u`.
 */
function getWuFiles(wuId) {
    try {
        const gitArgs = buildGitFilesArgs(wuId);
        // eslint-disable-next-line sonarjs/no-os-command-from-path -- git resolved from PATH; CLI tool requires git
        const output = execFileSync('git', gitArgs, {
            encoding: FILE_SYSTEM.ENCODING,
        });
        // In-process dedup and sort (replaces shell `| sort -u`)
        const files = output
            .trim()
            .split('\n')
            .filter((f) => f.length > 0);
        return [...new Set(files)].sort();
    }
    catch {
        return [];
    }
}
/**
 * Get WU info from YAML file
 */
function getWuInfo(wuId) {
    // WU-1301: Use config-based paths
    const yamlPath = join(process.cwd(), WU_PATHS.WU(wuId));
    if (!existsSync(yamlPath)) {
        return null;
    }
    try {
        const content = readFileSync(yamlPath, { encoding: FILE_SYSTEM.ENCODING });
        const yaml = parseYaml(content);
        return {
            title: yaml?.title || wuId,
            status: yaml?.status || 'unknown',
        };
    }
    catch {
        return null;
    }
}
/**
 * Format trace report as JSON
 */
function formatJson(entries) {
    return JSON.stringify(entries, null, JSON_INDENT);
}
/**
 * Format trace report as Markdown
 */
function formatMarkdown(entries) {
    const lines = [
        '# Traceability Report',
        '',
        `Generated: ${new Date().toISOString()}`,
        '',
        '## WU Summary',
        '',
        '| WU ID | Title | Status | Commits | Files |',
        '|-------|-------|--------|---------|-------|',
    ];
    for (const entry of entries) {
        lines.push(`| ${entry.wuId} | ${entry.title.slice(0, MARKDOWN_TABLE_TITLE_LENGTH)} | ${entry.status} | ${entry.commitCount} | ${entry.fileCount} |`);
    }
    lines.push('', '## Details', '');
    for (const entry of entries) {
        lines.push(`### ${entry.wuId}: ${entry.title}`, '');
        lines.push(`- **Status:** ${entry.status}`);
        lines.push(`- **Commits:** ${entry.commitCount}`);
        lines.push(`- **Files:** ${entry.fileCount}`);
        if (entry.firstCommit) {
            lines.push(`- **First commit:** ${entry.firstCommit}`);
            lines.push(`- **Last commit:** ${entry.lastCommit}`);
        }
        if (entry.files && entry.files.length > 0) {
            lines.push('', '**Files changed:**');
            for (const file of entry.files.slice(0, TRACE_FILES_DISPLAY_LIMIT)) {
                lines.push(`- ${file}`);
            }
            if (entry.files.length > TRACE_FILES_DISPLAY_LIMIT) {
                lines.push(`- ... and ${entry.files.length - TRACE_FILES_DISPLAY_LIMIT} more`);
            }
        }
        lines.push('');
    }
    return lines.join('\n');
}
/**
 * Format trace report as CSV
 */
function formatCsv(entries) {
    const lines = ['WU ID,Title,Status,Commits,Files,First Commit,Last Commit'];
    for (const entry of entries) {
        const title = entry.title.replace(/,/g, ';');
        lines.push(`${entry.wuId},"${title}",${entry.status},${entry.commitCount},${entry.fileCount},${entry.firstCommit || ''},${entry.lastCommit || ''}`);
    }
    return lines.join('\n');
}
/**
 * Print help message for trace-gen
 */
/* istanbul ignore next -- CLI entry point */
function printHelp() {
    console.log(`
Usage: trace-gen [options]

Generate traceability reports linking WUs to code changes.

Options:
  -w, --wu <id>       Trace specific WU (otherwise traces all)
  -f, --format <fmt>  Output format: json, markdown, csv (default: json)
  -o, --output <file> Write output to file (default: stdout)
  -s, --since <date>  Only trace WUs modified since date (ISO format)
  -h, --help          Show this help message

Examples:
  trace:gen --wu WU-1112                    # Trace single WU
  trace:gen --format markdown --output report.md  # Markdown report
  trace:gen --since 2024-01-01 --format csv       # CSV report since date

Output includes:
  - WU ID and title
  - Status
  - Number of commits
  - Number of files changed
  - First and last commit dates
  - List of changed files
`);
}
/**
 * Main entry point for trace-gen command
 */
/* istanbul ignore next -- CLI entry point */
async function main() {
    const args = parseTraceArgs(process.argv);
    if (args.help) {
        printHelp();
        process.exit(EXIT_CODES.SUCCESS);
    }
    // WU-1534: Validate WU ID if provided
    if (args.wuId && !validateWuId(args.wuId)) {
        console.error(`${LOG_PREFIX} Error: Invalid WU ID format: ${args.wuId}`);
        console.error(`${LOG_PREFIX} Expected format: WU-<number> (e.g., WU-1112)`);
        process.exit(EXIT_CODES.ERROR);
    }
    const format = args.format || TraceFormat.JSON;
    const entries = [];
    if (args.wuId) {
        // Trace single WU
        console.error(`${LOG_PREFIX} Tracing ${args.wuId}...`);
        const info = getWuInfo(args.wuId);
        if (!info) {
            console.error(`${LOG_PREFIX} Error: WU ${args.wuId} not found`);
            process.exit(EXIT_CODES.ERROR);
        }
        const commits = getWuCommits(args.wuId);
        const files = getWuFiles(args.wuId);
        entries.push(buildTraceEntry({
            wuId: args.wuId,
            title: info.title,
            status: info.status,
            commits,
            files,
        }));
    }
    else {
        // Trace all WUs
        console.error(`${LOG_PREFIX} Scanning all WUs...`);
        // WU-1301: Use config-based paths
        const wuDir = join(process.cwd(), WU_PATHS.WU_DIR());
        if (!existsSync(wuDir)) {
            console.error(`${LOG_PREFIX} Error: WU directory not found`);
            process.exit(EXIT_CODES.ERROR);
        }
        const files = readdirSync(wuDir);
        for (const file of files) {
            if (!file.endsWith('.yaml') && !file.endsWith('.yml'))
                continue;
            const wuId = file.replace(/\.ya?ml$/, '');
            const info = getWuInfo(wuId);
            if (!info)
                continue;
            // Filter by since date if specified
            if (args.since) {
                const commits = getWuCommits(wuId);
                if (commits.length === 0)
                    continue;
                const lastCommitDate = new Date(commits[commits.length - 1]?.date || 0);
                if (lastCommitDate < new Date(args.since))
                    continue;
            }
            const commits = getWuCommits(wuId);
            const wuFiles = getWuFiles(wuId);
            entries.push(buildTraceEntry({
                wuId,
                title: info.title,
                status: info.status,
                commits,
                files: wuFiles,
            }));
        }
        console.error(`${LOG_PREFIX} Found ${entries.length} WU(s)`);
    }
    // Format output
    let output;
    switch (format) {
        case TraceFormat.MARKDOWN:
            output = formatMarkdown(entries);
            break;
        case TraceFormat.CSV:
            output = formatCsv(entries);
            break;
        case TraceFormat.JSON:
        default:
            output = formatJson(entries);
            break;
    }
    // Write output
    if (args.output) {
        writeFileSync(args.output, output, { encoding: FILE_SYSTEM.ENCODING });
        console.error(`${LOG_PREFIX} Report written to ${args.output}`);
    }
    else {
        console.log(output);
    }
}
// Run main if executed directly
if (import.meta.main) {
    void runCLI(main);
}
//# sourceMappingURL=trace-gen.js.map