import * as fs from "fs";
import * as path from "path";
import {
    DIAGNOSTIC_CATALOG,
    getProfileRuleSeverities
} from "../core/src/diagnostics/diagnosticCatalog.js";
import {
    getQuickFixActionCount,
    getQuickFixCatalogEntries
} from "../core/src/diagnostics/quickFixCatalog.js";
import {
    getConfigurationSections,
    getRuleSettingKeyByRuleId
} from "./shared/settings-metadata.js";
import { COLUMNS } from "./shared/diagnostic-report-columns.js";

import { fileURLToPath } from "url";
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const ROOT_DIR = path.join(__dirname, "..");
const SYNTAX_TESTS_DIR = path.join(ROOT_DIR, "test", "syntax-tests");
const VALIDATORS_DIR = path.join(ROOT_DIR, "core", "src", "validators");
const CATALOG_PATH = path.join(ROOT_DIR, "core", "src", "diagnostics", "diagnosticCatalog.js");
const OUTPUT_PATH = path.join(ROOT_DIR, "dev-docs", "DIAGNOSTICS_COMPREHENSIVE_REFERENCE.md");

/**
 * Scans diagnosticCatalog.js and returns a map from messageId → 1-based line number.
 * Used to generate direct editor links into the catalog.
 * @returns {Map<string, number>}
 */
function buildCatalogLineMap() {
    const lines = fs.readFileSync(CATALOG_PATH, "utf8").split(/\r?\n/);
    const map = new Map();
    lines.forEach((line, idx) => {
        const match = line.match(/^\s*"(ACU-[A-Z]+-\d+)"\s*:/);
        if (match) {
            map.set(match[1], idx + 1);
        }
    });
    return map;
}

function escapeCell(text) {
    return String(text).replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
}

function formatSurface(surfaces) {
    if (!Array.isArray(surfaces) || surfaces.length === 0) {
        return "IDE + CLI";
    }
    const hasIde = surfaces.includes("ide");
    const hasCicd = surfaces.includes("cicd");
    if (hasIde && hasCicd) return "IDE + CLI";
    if (hasIde) return "IDE";
    if (hasCicd) return "CLI";
    return "IDE + CLI";
}

function categoryForDiagnosticId(messageId) {
    const match = String(messageId).match(/^ACU-([A-Z]+)-/);
    return match ? match[1] : "OTHER";
}

function severityForProfile(profileName, ruleId) {
    return getProfileRuleSeverities(profileName)[ruleId] || "warning";
}

function messageTemplateToRegex(template) {
    const escaped = template.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    const withParams = escaped.replace(/\\\{[A-Za-z0-9_]+\\\}/g, ".+?");
    return new RegExp(`^${withParams}$`);
}

function inferDiagnosticIdFromMessage(message) {
    const matches = Object.entries(DIAGNOSTIC_CATALOG)
        .filter(([, entry]) => messageTemplateToRegex(entry.message).test(message))
        .map(([messageId]) => messageId);

    return matches.length === 1 ? matches[0] : null;
}

function findSpecFiles(dirPath) {
    const results = [];
    const entries = fs.readdirSync(dirPath, { withFileTypes: true });

    for (const entry of entries) {
        if (entry.name === "test-spec.schema.json") {
            continue;
        }

        const fullPath = path.join(dirPath, entry.name);
        if (entry.isDirectory()) {
            results.push(...findSpecFiles(fullPath));
        } else if (entry.name.endsWith(".test-spec.json")) {
            results.push(fullPath);
        }
    }

    return results.sort((a, b) => a.localeCompare(b));
}

function normalizeLegacyExpectation(raw, severity) {
    if (typeof raw === "string") {
        return { id: inferDiagnosticIdFromMessage(raw), line: null, severity };
    }

    if (!raw || typeof raw !== "object") {
        return null;
    }

    if (raw.id) {
        return {
            id: raw.id,
            line: raw.line || null,
            severity: raw.severity || severity
        };
    }

    if (raw.message) {
        return {
            id: inferDiagnosticIdFromMessage(raw.message),
            line: raw.line || null,
            severity: raw.severity || severity
        };
    }

    return null;
}

function extractExpectedDiagnostics(spec) {
    // Schema v2.0: assert-message-exists
    if (Array.isArray(spec["assert-message-exists"])) {
        return spec["assert-message-exists"]
            .map((raw) => {
                if (!raw || typeof raw !== "object" || !raw.id) return null;
                return { id: raw.id, line: raw.line || null, severity: null };
            })
            .filter((entry) => entry && entry.id);
    }

    if (Array.isArray(spec["expected-messages"])) {
        return spec["expected-messages"]
            .map((raw) => {
                if (typeof raw === "string") {
                    return { id: inferDiagnosticIdFromMessage(raw), line: null, severity: null };
                }

                if (!raw || typeof raw !== "object") {
                    return null;
                }

                return {
                    id: raw.id || (raw.message ? inferDiagnosticIdFromMessage(raw.message) : null),
                    line: raw.line || null,
                    severity: raw.severity || null
                };
            })
            .filter((entry) => entry && entry.id);
    }

    return [
        ...(spec["expected-errors"] || []).map((raw) => normalizeLegacyExpectation(raw, "error")),
        ...(spec["expected-warnings"] || []).map((raw) => normalizeLegacyExpectation(raw, "warning"))
    ].filter((entry) => entry && entry.id);
}

function buildCoverageMap() {
    const coverage = new Map();
    const specPaths = findSpecFiles(SYNTAX_TESTS_DIR);

    for (const specPath of specPaths) {
        const spec = JSON.parse(fs.readFileSync(specPath, "utf8"));
        const relativeDir = path.relative(SYNTAX_TESTS_DIR, path.dirname(specPath)).split(path.sep).join("/");
        const fixtureRelativePath = relativeDir
            ? `${relativeDir}/${spec["test-entry"]}`
            : spec["test-entry"];

        for (const expectation of extractExpectedDiagnostics(spec)) {
            if (!coverage.has(expectation.id)) {
                coverage.set(expectation.id, new Map());
            }

            const byFixture = coverage.get(expectation.id);
            if (!byFixture.has(fixtureRelativePath)) {
                byFixture.set(fixtureRelativePath, new Set());
            }

            if (expectation.line) {
                byFixture.get(fixtureRelativePath).add(expectation.line);
            }
        }
    }

    return coverage;
}

function formatFixtureCoverage(messageId, coverageMap) {
    const byFixture = coverageMap.get(messageId);
    if (!byFixture || byFixture.size === 0) {
        return "-";
    }

    return Array.from(byFixture.entries())
        .sort(([a], [b]) => a.localeCompare(b))
        .map(([fixturePath, lines]) => {
            const link = `[${fixturePath.replace(/\.TXT$/i, "")}](../test/syntax-tests/${fixturePath})`;
            const sortedLines = Array.from(lines).sort((a, b) => a - b);

            if (sortedLines.length === 0) {
                return link;
            }

            if (sortedLines.length === 1) {
                return `${link} LINE ${sortedLines[0]}`;
            }

            return `${link} LINES ${sortedLines.join(",")}`;
        })
        .join(", ");
}

/**
 * Scans every validator source file and returns a map from messageId to the
 * sorted, comma-separated list of validator names that can emit it.
 *
 * Validator names are derived from filenames by stripping the "Validator.js"
 * suffix (e.g. "interpolationValidator.js" → "interpolation").
 *
 * @returns {Map<string, string>}  messageId → "name1, name2, …"
 */
function buildValidatorMap() {
    const map = new Map();

    // Primary: all files in core/src/validators/
    const files = fs.readdirSync(VALIDATORS_DIR).filter(f => f.endsWith(".js"));
    const sources = files.map(f => ({ file: f, dir: VALIDATORS_DIR }));

    // Additional: validator-adjacent files that live outside the validators/ directory.
    // structureValidator.js sits in core/src/ because block-structure validation is a
    // prerequisite for other validators (not a peer of them), but it still emits ACU-* IDs.
    const EXTRA_SOURCES = [
        path.join(ROOT_DIR, "core", "src", "structureValidator.js")
    ];
    for (const fullPath of EXTRA_SOURCES) {
        if (fs.existsSync(fullPath)) {
            sources.push({ file: path.basename(fullPath), dir: path.dirname(fullPath) });
        }
    }

    for (const { file, dir } of sources) {
        const name = file.replace(/Validator\.js$/, "").replace(/\.js$/, "");
        const content = fs.readFileSync(path.join(dir, file), "utf8");
        const ids = [...new Set((content.match(/ACU-[A-Z]+-\d+/g) || []))];
        for (const id of ids) {
            if (!map.has(id)) map.set(id, new Set());
            map.get(id).add(name);
        }
    }

    // Collapse each Set to a sorted, comma-separated string.
    const result = new Map();
    for (const [id, names] of map) {
        result.set(id, [...names].sort().join(", "));
    }
    return result;
}

/**
 * Builds structured row objects for every catalog entry.
 * This is the single source of truth for which fields are extracted and how.
 * Both the markdown and HTML renderers consume this; column changes live here.
 *
 * @param {Map} coverageMap
 * @returns {Array<{messageId, category, surface, message, ruleId, validators, group, defSev, legSev, strSev, settingKey, quickFixCount, syntaxTestSuite, coverageText}>}
 */
function buildDiagnosticRows(coverageMap) {
    const ruleSettingByRuleId = getRuleSettingKeyByRuleId();
    const validatorMap = buildValidatorMap();
    const catalogLineMap = buildCatalogLineMap();

    return Object.entries(DIAGNOSTIC_CATALOG)
        .sort(([a], [b]) => a.localeCompare(b))
        .map(([messageId, entry]) => {
            const ruleId = entry.ruleId;
            const settingKey = ruleSettingByRuleId[ruleId] || "-";
            const group = settingKey.startsWith("acurity.rules.core.")
                ? "core"
                : (settingKey.startsWith("acurity.rules.standards.") ? "standards" : "-");

            // Derive top-level test suite folder(s) from coverage fixture paths.
            // Fixture paths look like "core/line-types/foo.TXT" or "coding-standards/bar.TXT".
            const byFixture = coverageMap.get(messageId);
            let syntaxTestSuite = "-";
            if (byFixture && byFixture.size > 0) {
                const suites = [...new Set(
                    Array.from(byFixture.keys()).map(p => p.split("/")[0])
                )].sort();
                syntaxTestSuite = suites.join(" + ");
            }

            return {
                messageId,
                catalogLine: catalogLineMap.get(messageId) || null,
                category: entry.category || "-",
                surface: formatSurface(entry.surfaces),
                message: entry.message,
                ruleId,
                validators: validatorMap.get(messageId) || "-",
                group,
                defSev: severityForProfile("default", ruleId),
                legSev: severityForProfile("legacy", ruleId),
                strSev: severityForProfile("strict", ruleId),
                settingKey,
                quickFixCount: getQuickFixActionCount(ruleId),
                syntaxTestSuite,
                coverageText: formatFixtureCoverage(messageId, coverageMap)
            };
        });
}

function buildReferenceRows(coverageMap) {
    // Cell order must match the column order declared in diagnostic-report-columns.js.
    return buildDiagnosticRows(coverageMap).map(r => {
        const catalogCell = r.catalogLine
            ? `[📖](../core/src/diagnostics/diagnosticCatalog.js#L${r.catalogLine})`
            : "-";
        return `| **${r.messageId}** | ${catalogCell} | ${r.category} | ${r.surface} | \`${escapeCell(r.message)}\` | ${r.ruleId} | ${r.validators} | ${r.strSev} | ${r.defSev} | ${r.legSev} | ${r.quickFixCount} | ${r.syntaxTestSuite} | ${r.coverageText} | ${r.group} | \`${r.settingKey}\` |`;
    });
}

function buildSettingsRows() {
    const sections = getConfigurationSections();
    const rows = [];

    for (const section of sections) {
        const title = section.title || "";
        for (const [key, schema] of Object.entries(section.properties || {})) {
            const type = Array.isArray(schema.type) ? schema.type.join("|") : (schema.type || "-");
            rows.push(`| \`${key}\` | ${type} | ${escapeCell(title)} |`);
        }
    }

    rows.sort((a, b) => a.localeCompare(b));
    return rows.join("\n");
}

function buildCategoryRows() {
    const categoryCounts = new Map();

    Object.keys(DIAGNOSTIC_CATALOG).forEach((messageId) => {
        const category = categoryForDiagnosticId(messageId);
        categoryCounts.set(category, (categoryCounts.get(category) || 0) + 1);
    });

    return Array.from(categoryCounts.entries())
        .sort(([a], [b]) => a.localeCompare(b))
        .map(([category, count]) => `| ${category} | ${count} |`)
        .join("\n");
}

function buildQuickFixRows() {
    return getQuickFixCatalogEntries()
        .sort((a, b) => a.messageId.localeCompare(b.messageId))
        .map((entry) => `| **${entry.messageId}** | ${entry.ruleId} | ${entry.actionCount} | ${entry.kinds.join(", ")} |`)
        .join("\n");
}

function getDiagnosticsWithoutPositiveCoverage(coverageMap = buildCoverageMap()) {
    return Object.keys(DIAGNOSTIC_CATALOG)
        .filter((messageId) => {
            const byFixture = coverageMap.get(messageId);
            return !byFixture || byFixture.size === 0;
        })
        .sort((a, b) => a.localeCompare(b));
}

function buildDocument(coverageMap) {
    const rows = buildReferenceRows(coverageMap).join("\n");
    const quickFixRows = buildQuickFixRows();
    const categoryRows = buildCategoryRows();

    return [
        "# Comprehensive Diagnostics Reference",
        "",
        "This file is auto-generated. Run `npm run docs:diagnostics` to refresh it.",
        "",
        "## Concepts",
        "",
        "Two distinct entities appear throughout this reference:",
        "",
        "- **Message** — a specific diagnostic instance identified by a stable `ACU-XXX-NNN` ID.",
        "  The Message ID appears in VS Code hover tooltips, the Problems panel, and in suppression",
        "  comments (`// @rule-ignore ACU-INT-001`). A Rule may produce several distinct Message types.",
        "- **Rule** — a named validation behaviour identified by a camelCase key (e.g. `interpolationUndefined`).",
        "  Each Rule maps to a single VS Code setting (`acurity.rules.<group>.<rule>.severity`).",
        "  Changing the setting adjusts the severity of **all** Messages that belong to that Rule.",
        "- **Surface** — where a diagnostic is raised. `IDE` means VS Code only; `CLI` means the headless",
        "  validator only; `IDE + CLI` means both. Surfaces are declared on each catalog entry's `surfaces`",
        "  field in [`core/src/diagnostics/diagnosticCatalog.js`](../core/src/diagnostics/diagnosticCatalog.js).",
        "",
        "**Where rule severities are set:**",
        "",
        "- **Default profiles** (default / legacy / strict) — `severity` block on each entry in",
        "  [`core/src/diagnostics/diagnosticCatalog.js`](../core/src/diagnostics/diagnosticCatalog.js).",
        "- **VS Code** — profile selection and per-rule overrides resolved in",
        "  [`vscode-extension/src/config/standardsConfig.js`](../vscode-extension/src/config/standardsConfig.js).",
        "  The active profile can be set via `acurity.rules.profile`; individual rules can be overridden",
        "  via `acurity.rules.<group>.<rule>.severity`.",
        "- **CLI** — always uses the `default` profile; severity mapping defined in",
        "  [`cli/index.js`](../cli/index.js).",
        "",
        "## Complete Diagnostic Reference Table",
        "",
        "Severity columns (Default / Legacy / Strict) and VS Code Settings Group / VS Code Setting are **Rule-level** properties.",
        "Message ID, Category, Surface, and Message are **Message-level** properties.",
        "",
        "| " + COLUMNS.map(c => c.label).join(" | ") + " |",
        "| " + COLUMNS.map(() => "---").join(" | ") + " |",
        rows,
        "",
        "## Quick Fix Coverage",
        "",
        "| Message ID | Rule | Actions | Kinds |",
        "| --- | --- | --- | --- |",
        quickFixRows || "| - | - | 0 | - |",
        "",
        "## Category Counts",
        "",
        "| Category | Diagnostics |",
        "| --- | --- |",
        categoryRows,
        "",
        "## Contributed Settings (from package.json)",
        "",
        "| Setting Key | Type | Settings Section |",
        "| --- | --- | --- |",
        buildSettingsRows(),
        ""
    ].join("\n");
}

function generateComprehensiveDiagnosticReference(outputPath = OUTPUT_PATH) {
    const coverageMap = buildCoverageMap();
    const documentText = buildDocument(coverageMap);
    fs.writeFileSync(outputPath, documentText, "utf8");
    return outputPath;
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
    const outPath = generateComprehensiveDiagnosticReference();
    console.log(`Generated comprehensive diagnostics reference in ${outPath}`);
}

export {
    generateComprehensiveDiagnosticReference,
    buildCoverageMap,
    buildDiagnosticRows,
    formatFixtureCoverage,
    getDiagnosticsWithoutPositiveCoverage,
    inferDiagnosticIdFromMessage
};