#!/usr/bin/env node
// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * Flow Report Generator CLI (WU-1018)
 *
 * Generates DORA/SPACE flow reports from telemetry and WU data.
 *
 * Usage:
 *   pnpm flow:report                         # Default: last 7 days, JSON output
 *   pnpm flow:report --days 30               # Last 30 days
 *   pnpm flow:report --format table          # Table output
 *   pnpm flow:report --start 2026-01-01 --end 2026-01-15
 *
 * @module flow-report
 * @see {@link @lumenflow/metrics/flow/generate-flow-report}
 */
import { readFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import fg from 'fast-glob';
import { parse as parseYaml } from 'yaml';
import { Command } from 'commander';
import { generateFlowReport, TELEMETRY_PATHS, } from '@lumenflow/metrics';
import { WU_PATHS } from '@lumenflow/core/wu-paths';
import { runCLI } from './cli-entry-point.js';
import { FLOW_REPORT_WU_DISPLAY_LIMIT, FLOW_REPORT_TITLE_COLUMN_WIDTH, WU_ID_COLUMN_WIDTH, LANE_COLUMN_WIDTH, GATE_NAME_COLUMN_WIDTH, MS_PER_HOUR, ONE_DECIMAL_ROUNDING_FACTOR, JSON_INDENT, } from './constants.js';
/** Log prefix for console output */
const LOG_PREFIX = '[flow:report]';
/** Default report window in days */
const DEFAULT_DAYS = 7;
/** Output format options */
const OUTPUT_FORMATS = {
    JSON: 'json',
    TABLE: 'table',
};
/** WU directory relative to repo root (WU-1301: uses config-based paths) */
const WU_DIR = WU_PATHS.WU_DIR();
/**
 * Parse command line arguments
 */
function parseArgs() {
    const program = new Command()
        .name('flow-report')
        .description('Generate DORA/SPACE flow report from telemetry and WU data')
        .option('--start <date>', 'Start date (YYYY-MM-DD)')
        .option('--end <date>', 'End date (YYYY-MM-DD), defaults to today')
        .option('--days <number>', `Days to report (default: ${DEFAULT_DAYS})`, String(DEFAULT_DAYS))
        .option('--format <type>', `Output format: json, table (default: json)`, OUTPUT_FORMATS.JSON)
        .exitOverride();
    try {
        program.parse(process.argv);
        return program.opts();
    }
    catch (err) {
        const error = err;
        if (error.code === 'commander.helpDisplayed' || error.code === 'commander.version') {
            process.exit(0);
        }
        throw err;
    }
}
/**
 * Calculate date range from options
 */
function calculateDateRange(opts) {
    const end = opts.end ? new Date(opts.end) : new Date();
    end.setHours(23, 59, 59, 999);
    let start;
    if (opts.start) {
        start = new Date(opts.start);
        start.setHours(0, 0, 0, 0);
    }
    else {
        const days = parseInt(opts.days ?? String(DEFAULT_DAYS), 10);
        start = new Date(end);
        start.setDate(start.getDate() - days);
        start.setHours(0, 0, 0, 0);
    }
    return { start, end };
}
/**
 * Read NDJSON file and parse events
 */
async function readNDJSON(filePath) {
    if (!existsSync(filePath)) {
        return [];
    }
    const content = await readFile(filePath, { encoding: 'utf-8' });
    const lines = content.split('\n').filter((line) => line.trim());
    const events = [];
    for (const line of lines) {
        try {
            events.push(JSON.parse(line));
        }
        catch {
            // Skip invalid JSON lines
        }
    }
    return events;
}
/**
 * Transform raw gate event to GateTelemetryEvent type
 */
function transformGateEvent(raw) {
    if (typeof raw.timestamp !== 'string' ||
        typeof raw.gate_name !== 'string' ||
        typeof raw.passed !== 'boolean' ||
        typeof raw.duration_ms !== 'number') {
        return null;
    }
    return {
        timestamp: raw.timestamp,
        wuId: raw.wu_id ?? null,
        lane: raw.lane ?? null,
        gateName: raw.gate_name,
        passed: raw.passed,
        durationMs: raw.duration_ms,
    };
}
/**
 * Transform raw LLM event to LLMTelemetryEvent type
 */
function transformLLMEvent(raw) {
    if (typeof raw.timestamp !== 'string' ||
        typeof raw.event_type !== 'string' ||
        typeof raw.classification_type !== 'string') {
        return null;
    }
    const eventType = raw.event_type;
    if (eventType !== 'llm.classification.start' &&
        eventType !== 'llm.classification.complete' &&
        eventType !== 'llm.classification.error') {
        return null;
    }
    return {
        timestamp: raw.timestamp,
        eventType,
        classificationType: raw.classification_type,
        durationMs: raw.duration_ms,
        tokensUsed: raw.tokens_used,
        estimatedCostUsd: raw.estimated_cost_usd,
        confidence: raw.confidence,
        fallbackUsed: raw.fallback_used,
        fallbackReason: raw.fallback_reason,
        errorType: raw.error_type,
        errorMessage: raw.error_message,
    };
}
/**
 * Load gate telemetry events from file
 */
async function loadGateEvents(baseDir) {
    const gatesPath = join(baseDir, TELEMETRY_PATHS.GATES);
    const rawEvents = await readNDJSON(gatesPath);
    return rawEvents.map(transformGateEvent).filter((e) => e !== null);
}
/**
 * Load LLM telemetry events from file
 */
async function loadLLMEvents(baseDir) {
    const llmPath = join(baseDir, TELEMETRY_PATHS.LLM_CLASSIFICATION);
    const rawEvents = await readNDJSON(llmPath);
    return rawEvents.map(transformLLMEvent).filter((e) => e !== null);
}
/**
 * Load completed WUs from YAML files
 */
async function loadCompletedWUs(baseDir, start, end) {
    const wuDir = join(baseDir, WU_DIR);
    const wuFiles = await fg('WU-*.yaml', { cwd: wuDir, absolute: true });
    const completedWUs = [];
    for (const file of wuFiles) {
        try {
            const content = await readFile(file, { encoding: 'utf-8' });
            const wu = parseYaml(content);
            if (wu.status !== 'done' || !wu.completed_at) {
                continue;
            }
            const completedAt = new Date(wu.completed_at);
            if (completedAt < start || completedAt > end) {
                continue;
            }
            completedWUs.push({
                id: wu.id,
                title: wu.title,
                lane: wu.lane,
                status: 'done',
                claimedAt: wu.claimed_at ? new Date(wu.claimed_at) : undefined,
                completedAt,
                cycleTimeHours: calculateCycleTime(wu),
            });
        }
        catch {
            // Skip invalid WU files
        }
    }
    return completedWUs;
}
/**
 * Calculate cycle time in hours from WU data
 */
function calculateCycleTime(wu) {
    if (!wu.claimed_at || !wu.completed_at) {
        return undefined;
    }
    const claimed = new Date(wu.claimed_at);
    const completed = new Date(wu.completed_at);
    const diffMs = completed.getTime() - claimed.getTime();
    const diffHours = diffMs / MS_PER_HOUR;
    return Math.round(diffHours * ONE_DECIMAL_ROUNDING_FACTOR) / ONE_DECIMAL_ROUNDING_FACTOR;
}
/**
 * Filter events by date range
 */
function filterByDateRange(events, start, end) {
    return events.filter((event) => {
        const eventDate = new Date(event.timestamp);
        return eventDate >= start && eventDate <= end;
    });
}
/**
 * Format report as table output
 */
function formatAsTable(report) {
    const lines = [];
    lines.push('═══════════════════════════════════════════════════════════════');
    lines.push(`  FLOW REPORT: ${report.range.start} to ${report.range.end}`);
    lines.push('═══════════════════════════════════════════════════════════════');
    lines.push('');
    // Gates section
    lines.push('┌─────────────────────────────────────────────────────────────┐');
    lines.push('│ GATES                                                       │');
    lines.push('├─────────────────────────────────────────────────────────────┤');
    lines.push(`│ Pass Rate: ${report.gates.passRate}%  |  Total: ${report.gates.total}  |  P95: ${report.gates.p95}ms │`);
    lines.push(`│ Passed: ${report.gates.passed}  |  Failed: ${report.gates.failed}`);
    lines.push('├─────────────────────────────────────────────────────────────┤');
    lines.push('│ By Name:                                                    │');
    for (const [name, stats] of Object.entries(report.gates.byName)) {
        lines.push(`│   ${name.padEnd(GATE_NAME_COLUMN_WIDTH)} ${stats.passRate}% (${stats.passed}/${stats.total})`);
    }
    lines.push('└─────────────────────────────────────────────────────────────┘');
    lines.push('');
    // WUs section
    lines.push('┌─────────────────────────────────────────────────────────────┐');
    lines.push('│ WORK UNITS                                                  │');
    lines.push('├─────────────────────────────────────────────────────────────┤');
    lines.push(`│ Completed: ${report.wus.completed}`);
    lines.push('├─────────────────────────────────────────────────────────────┤');
    for (const wu of report.wus.list.slice(0, FLOW_REPORT_WU_DISPLAY_LIMIT)) {
        lines.push(`│ ${wu.wuId.padEnd(WU_ID_COLUMN_WIDTH)} ${wu.completedDate}  ${wu.lane.padEnd(LANE_COLUMN_WIDTH)} ${wu.title.slice(0, FLOW_REPORT_TITLE_COLUMN_WIDTH)}`);
    }
    if (report.wus.list.length > FLOW_REPORT_WU_DISPLAY_LIMIT) {
        lines.push(`│ ... and ${report.wus.list.length - FLOW_REPORT_WU_DISPLAY_LIMIT} more`);
    }
    lines.push('└─────────────────────────────────────────────────────────────┘');
    lines.push('');
    // LLM section
    lines.push('┌─────────────────────────────────────────────────────────────┐');
    lines.push('│ LLM CLASSIFICATION                                          │');
    lines.push('├─────────────────────────────────────────────────────────────┤');
    lines.push(`│ Total: ${report.llm.totalClassifications}  |  Error Rate: ${report.llm.errorRate}%`);
    lines.push(`│ Avg Latency: ${report.llm.avgLatencyMs}ms  |  P95: ${report.llm.p95LatencyMs}ms`);
    lines.push(`│ Tokens: ${report.llm.totalTokens}  |  Cost: $${report.llm.totalCostUsd.toFixed(4)}`);
    lines.push('└─────────────────────────────────────────────────────────────┘');
    return lines.join('\n');
}
/**
 * Main function
 */
export async function main() {
    const opts = parseArgs();
    const baseDir = process.cwd();
    console.log(`${LOG_PREFIX} Generating flow report...`);
    const { start, end } = calculateDateRange(opts);
    console.log(`${LOG_PREFIX} Date range: ${start.toISOString().split('T')[0]} to ${end.toISOString().split('T')[0]}`);
    // Load telemetry and WU data
    const [gateEvents, llmEvents, completedWUs] = await Promise.all([
        loadGateEvents(baseDir),
        loadLLMEvents(baseDir),
        loadCompletedWUs(baseDir, start, end),
    ]);
    // Filter events by date range
    const filteredGateEvents = filterByDateRange(gateEvents, start, end);
    const filteredLLMEvents = filterByDateRange(llmEvents, start, end);
    console.log(`${LOG_PREFIX} Found ${filteredGateEvents.length} gate events`);
    console.log(`${LOG_PREFIX} Found ${filteredLLMEvents.length} LLM events`);
    console.log(`${LOG_PREFIX} Found ${completedWUs.length} completed WUs`);
    // Generate report
    const input = {
        gateEvents: filteredGateEvents,
        llmEvents: filteredLLMEvents,
        completedWUs,
        dateRange: {
            start: start.toISOString().split('T')[0],
            end: end.toISOString().split('T')[0],
        },
    };
    const report = generateFlowReport(input);
    // Output report
    console.log('');
    if (opts.format === OUTPUT_FORMATS.TABLE) {
        console.log(formatAsTable(report));
    }
    else {
        console.log(JSON.stringify(report, null, JSON_INDENT));
    }
}
// WU-1181: Use import.meta.main instead of process.argv[1] comparison
// The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
// path but import.meta.url resolves to the real path - they never match
// WU-1537: Use import.meta.main + runCLI for consistent EPIPE and error handling
if (import.meta.main) {
    void runCLI(main);
}
//# sourceMappingURL=flow-report.js.map