#!/usr/bin/env node
/**
 * acurity-validate — headless CLI for the Acurity validator engine.
 *
 * Usage:
 *   node cli/index.js --folder <path> [--output <file>] [--fail-on error|warning|none]
 *                     [--surface ide|cicd] [--config <path>]
 *                     [--diff-only [--base-branch <ref>]]
 *
 * Exit codes:
 *   0  No diagnostics at or above the fail-on threshold (default: error)
 *   1  One or more diagnostics found at or above the threshold
 *   2  Runtime error (bad arguments, folder not found, etc.)
 */

import * as fs from "fs";
import * as path from "path";

import { collectDiagnosticsAsync } from "../core/src/collectDiagnostics.js";
import { PlainTextDocument } from "../core/src/plainTextDocument.js";
import { isOnSurface, getProfileRuleSeverities } from "../core/src/diagnostics/diagnosticCatalog.js";
import { loadConfig, resolveFailOn } from "./configLoader.js";
import { buildChangedLinesMap } from "./diffFilter.js";

// ---------------------------------------------------------------------------
// Rule severity — use catalog defaults for the "default" profile.
// Maps severity strings to the numeric constants defined by the VS Code API
// (Error=0, Warning=1, Information=2, Hint=3) so plain diagnostics are
// consistent with what the extension produces.
// ---------------------------------------------------------------------------

const SEVERITY_NUMBER = { error: 0, warning: 1, information: 2, info: 2, hint: 3 };
const _profileSeverities = getProfileRuleSeverities("default");

function getRuleSeverity(ruleId) {
    const raw = _profileSeverities[ruleId] ?? "warning";
    if (raw === "off") return null;
    return SEVERITY_NUMBER[raw] ?? 1;
}

// ---------------------------------------------------------------------------
// Argument parsing
// ---------------------------------------------------------------------------

function parseArgs(argv) {
    const args = { folder: null, output: null, failOn: "error", surface: "cicd", config: null, diffOnly: false, baseBranch: "main" };

    for (let i = 2; i < argv.length; i += 1) {
        switch (argv[i]) {
            case "--folder":   args.folder  = argv[++i]; break;
            case "--output":   args.output  = argv[++i]; break;
            case "--fail-on":  args.failOn  = argv[++i]; break;
            case "--surface":  args.surface = argv[++i]; break;
            case "--config":       args.config     = argv[++i]; break;
            case "--diff-only":    args.diffOnly   = true;       break;
            case "--base-branch":  args.baseBranch = argv[++i];  break;
            default:
                console.error(`Unknown argument: ${argv[i]}`);
                process.exit(2);
        }
    }

    return args;
}

// ---------------------------------------------------------------------------
// File discovery
// ---------------------------------------------------------------------------

function findTxtFiles(dir, results = []) {
    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
        if (entry.name === "node_modules" || entry.name === ".git") {
            continue;
        }
        const full = path.join(dir, entry.name);
        if (entry.isDirectory()) {
            findTxtFiles(full, results);
        } else if (/\.txt$/i.test(entry.name)) {
            results.push(full);
        }
    }
    return results;
}

// ---------------------------------------------------------------------------
// Severity helpers
// ---------------------------------------------------------------------------

const SEVERITY_NAMES = { 0: "error", 1: "warning", 2: "information", 3: "hint" };

function severityName(severity) {
    return SEVERITY_NAMES[severity] ?? String(severity);
}

const SEVERITY_RANK = { error: 0, warning: 1, information: 2, hint: 3, none: 99 };

function exceedsThreshold(severityValue, failOn) {
    const rank = SEVERITY_RANK[severityName(severityValue)] ?? 99;
    return rank <= (SEVERITY_RANK[failOn] ?? 0);
}

// ---------------------------------------------------------------------------
// Report helpers  (mirrors batchValidateTxtFolder.js report format)
// ---------------------------------------------------------------------------

function buildDefaultReportFileName() {
    const now = new Date();
    const pad = (v) => String(v).padStart(2, "0");
    const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-` +
                  `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
    return `acurity-validation-report-${stamp}.json`;
}

function summarize(fileResults) {
    const totals = {
        filesScanned: fileResults.length,
        filesWithDiagnostics: 0,
        diagnosticCount: 0,
        severity: { error: 0, warning: 0, information: 0, hint: 0 },
        byMessageId: {}
    };

    for (const fr of fileResults) {
        if (fr.diagnosticCount > 0) {
            totals.filesWithDiagnostics += 1;
        }
        totals.diagnosticCount += fr.diagnosticCount;

        for (const d of fr.diagnostics) {
            const sev = d.severity;
            totals.severity[sev] = (totals.severity[sev] || 0) + 1;
            if (d.messageId) {
                totals.byMessageId[d.messageId] = (totals.byMessageId[d.messageId] || 0) + 1;
            }
        }
    }

    return totals;
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

async function main() {
    const args = parseArgs(process.argv);

    if (!args.folder) {
        console.error("Error: --folder <path> is required.");
        process.exit(2);
    }

    if (!["error", "warning", "information", "hint", "none"].includes(args.failOn)) {
        console.error(`Error: --fail-on must be one of: error, warning, information, hint, none`);
        process.exit(2);
    }

    if (args.surface === "all") {
        args.surface = null;
    } else if (!["ide", "cicd"].includes(args.surface)) {
        console.error(`Error: --surface must be one of: ide, cicd, all`);
        process.exit(2);
    }

    const folderPath = path.resolve(args.folder);

    let folderStat;
    try {
        folderStat = fs.statSync(folderPath);
    } catch (_) {
        console.error(`Error: folder not found: ${folderPath}`);
        process.exit(2);
    }
    if (!folderStat.isDirectory()) {
        console.error(`Error: not a directory: ${folderPath}`);
        process.exit(2);
    }

    // Load .acurity.json if --config was supplied.
    const acurityConfig = args.config ? loadConfig(path.resolve(args.config)) : null;
    const namingConfig  = acurityConfig && acurityConfig.naming ? acurityConfig.naming : null;

    let txtFiles;
    try {
        txtFiles = findTxtFiles(folderPath);
    } catch (err) {
        console.error(`Error: failed to scan folder: ${err.message}`);
        process.exit(2);
    }

    if (txtFiles.length === 0) {
        console.log("No .txt / .TXT files found. Nothing to validate.");
        process.exit(0);
    }

    // Build the diff filter map before scanning (requires git; exits on failure).
    let changedLines = null;
    if (args.diffOnly) {
        try {
            changedLines = buildChangedLinesMap(folderPath, args.baseBranch);
        } catch (err) {
            console.error(`Error: ${err.message}`);
            process.exit(2);
        }
    }

    console.log(`Acurity: scanning ${txtFiles.length} file(s) in ${folderPath}`);

    const fileResults = [];
    let thresholdBreached = false;

    for (const filePath of txtFiles) {
        let content;
        try {
            content = fs.readFileSync(filePath, "utf8");
        } catch (err) {
            console.error(`Error: failed to read file ${filePath}: ${err.message}`);
            process.exit(2);
        }
        const document   = new PlainTextDocument(filePath, content);
        const relPath    = path.relative(folderPath, filePath).replace(/\\/g, "/");

        const allDiagnostics = await collectDiagnosticsAsync(document, getRuleSeverity, {
            workspaceRoot: folderPath,
            namingConfig
        });
        let diagnostics = args.surface
            ? allDiagnostics.filter((d) => isOnSurface(d.data?.messageId ?? null, args.surface))
            : allDiagnostics;

        if (changedLines !== null) {
            const fileChanges = changedLines.get(filePath);
            diagnostics = fileChanges
                ? diagnostics.filter((d) => fileChanges.has(d.range.start.line))
                : [];
        }

        const normalized = diagnostics.map((d) => ({
            severity:  severityName(d.severity),
            messageId: d.data?.messageId ?? null,
            ruleId:    d.data?.ruleId    ?? null,
            message:   d.message,
            line:      d.range.start.line      + 1,
            column:    d.range.start.character + 1,
            endLine:   d.range.end.line        + 1,
            endColumn: d.range.end.character   + 1,
            params:    d.data?.params          ?? null
        }));

        const effectiveFailOn = resolveFailOn(acurityConfig, relPath, args.failOn);
        for (const d of diagnostics) {
            if (exceedsThreshold(d.severity, effectiveFailOn)) {
                thresholdBreached = true;
            }
        }

        fileResults.push({
            file:            relPath,
            diagnosticCount: normalized.length,
            diagnostics:     normalized
        });
    }

    const summary = summarize(fileResults);
    const report  = {
        generatedAt:   new Date().toISOString(),
        folder:        folderPath,
        reportVersion: "1.0",
        summary,
        files:         fileResults
    };

    const reportJson = JSON.stringify(report, null, 2);

    const outputPath = args.output
        ? path.resolve(args.output)
        : path.join(folderPath, buildDefaultReportFileName());

    fs.writeFileSync(outputPath, reportJson, "utf8");

    console.log(
        `Done: ${summary.filesScanned} file(s) scanned, ` +
        `${summary.diagnosticCount} diagnostic(s) found. ` +
        `Report: ${outputPath}`
    );

    if (summary.diagnosticCount > 0) {
        const { error, warning } = summary.severity;
        if (error)   console.log(`  errors:   ${error}`);
        if (warning) console.log(`  warnings: ${warning}`);
    }

    process.exit(thresholdBreached ? 1 : 0);
}

main();
