/**
 * Diagnostic provider for Acurity Architect documents.
 */

import * as path from "path";
import * as vscode from "vscode";
import { LANGUAGE_ID } from "../../core/src/constants.js";
import * as logger from "../../core/src/logger.js";
import { invalidateDocumentFacts } from "../../core/src/analysis/documentFacts.js";
import {
    collectIncludeDependenciesForDocument,
    invalidateIncludeCacheForPath,
    warmIncludeCacheForDocument
} from "../../core/src/includeResolver.js";
import { buildGetRuleSeverity } from "./config/standardsConfig.js";
import { detectMode } from "../../core/src/analysis/documentFacts.js";
import { collectDiagnosticsAsync as coreCollectDiagnosticsAsync } from "../../core/src/collectDiagnostics.js";
import {
    isAcurityDocument,
    isSmallEdit,
    scheduleValidation,
    clearPendingValidation,
    clearAllPendingValidations
} from "./diagnostics/validationScheduler.js";

let diagnosticCollection;
const includePathToDependentDocuments = new Map();
const documentToIncludeDependencies = new Map();
const includeWarmupInFlightByDocument = new Map();


/**
 * Initializes the diagnostic provider.
 * @param {vscode.ExtensionContext} context - The extension context
 */
function initializeDiagnostics(context) {
    diagnosticCollection = vscode.languages.createDiagnosticCollection(LANGUAGE_ID);
    context.subscriptions.push(diagnosticCollection);

    context.subscriptions.push(
        vscode.workspace.onDidOpenTextDocument(async (document) => {
            await scheduleIncludeCacheWarmup(document);
            validateDocument(document);
        }),
        vscode.workspace.onDidChangeTextDocument((event) => {
            handleDocumentChanged(event.document, event.contentChanges);
        }),
        vscode.workspace.onDidCloseTextDocument((document) => {
            clearPendingValidation(document.uri);
            clearDocumentIncludeDependencies(document);
            if (isAcurityDocument(document)) {
                diagnosticCollection.delete(document.uri);
            }
        })
    );

    // Track include file system changes so dependent open documents are refreshed
    // even when include files are modified outside an open text editor.
    const includeWatcher = vscode.workspace.createFileSystemWatcher("**/*.{txt,TXT}");
    context.subscriptions.push(
        includeWatcher,
        includeWatcher.onDidCreate((uri) => handleIncludeFileSystemEvent(uri)),
        includeWatcher.onDidChange((uri) => handleIncludeFileSystemEvent(uri)),
        includeWatcher.onDidDelete((uri) => handleIncludeFileSystemEvent(uri))
    );

    // Validate all open documents (warm cache first so sync validation path avoids readFileSync)
    vscode.workspace.textDocuments.forEach(async (document) => {
        await scheduleIncludeCacheWarmup(document);
        validateDocument(document);
    });
}

/**
 * Validates an Acurity document and reports diagnostics.
 * @param {vscode.TextDocument} document - The document to validate
 */
async function validateDocument(document) {
    if (!isAcurityDocument(document)) {
        return;
    }

    clearPendingValidation(document.uri);

    const diagnostics = await collectDiagnostics(document);
    updateDocumentIncludeDependencies(document);

    if (diagnosticCollection) {
        diagnosticCollection.set(document.uri, diagnostics);
    }
}

function normalizeFilePath(filePath) {
    return path.normalize(filePath).toLowerCase();
}

function clearDocumentIncludeDependencies(document) {
    const documentKey = document.uri.toString();
    const previousDependencies = documentToIncludeDependencies.get(documentKey);
    if (!previousDependencies) {
        return;
    }

    for (const includePath of previousDependencies) {
        const dependents = includePathToDependentDocuments.get(includePath);
        if (!dependents) {
            continue;
        }

        dependents.delete(documentKey);
        if (dependents.size === 0) {
            includePathToDependentDocuments.delete(includePath);
        }
    }

    documentToIncludeDependencies.delete(documentKey);
}

function updateDocumentIncludeDependencies(document) {
    if (!isAcurityDocument(document)) {
        return;
    }

    clearDocumentIncludeDependencies(document);

    const dependencies = collectIncludeDependenciesForDocument(document);
    const documentKey = document.uri.toString();
    documentToIncludeDependencies.set(documentKey, dependencies);

    for (const includePath of dependencies) {
        if (!includePathToDependentDocuments.has(includePath)) {
            includePathToDependentDocuments.set(includePath, new Set());
        }

        includePathToDependentDocuments.get(includePath).add(documentKey);
    }
}

function scheduleDependentRevalidations(changedDocument, changedFilePath) {
    if (!changedFilePath) {
        return;
    }

    const normalizedChangedPath = normalizeFilePath(changedFilePath);
    const dependentDocumentKeys = includePathToDependentDocuments.get(normalizedChangedPath);
    if (!dependentDocumentKeys || dependentDocumentKeys.size === 0) {
        return;
    }

    for (const documentKey of dependentDocumentKeys) {
        if (changedDocument && documentKey === changedDocument.uri.toString()) {
            continue;
        }

        // Evict stale document facts — the document version hasn't changed but
        // its included content has, so the memoized facts are now stale.
        invalidateDocumentFacts(documentKey);

        const dependentDocument = vscode.workspace.textDocuments.find((doc) => doc.uri.toString() === documentKey);
        if (!dependentDocument || !isAcurityDocument(dependentDocument)) {
            continue;
        }

        scheduleIncludeCacheWarmup(dependentDocument);
        scheduleValidation(dependentDocument, async (doc) => {
            await scheduleIncludeCacheWarmup(doc);
            validateDocument(doc);
        }, false);
    }
}

function handleIncludeFileSystemEvent(uri) {
    if (!uri || uri.scheme !== "file") {
        return;
    }

    invalidateIncludeCacheForPath(uri.fsPath);
    scheduleDependentRevalidations(null, uri.fsPath);
}

function handleDocumentChanged(document, contentChanges) {
    if (!document) {
        return;
    }

    const changedFilePath = document.uri?.scheme === "file" ? document.uri.fsPath : null;
    if (changedFilePath) {
        invalidateIncludeCacheForPath(changedFilePath);
        scheduleDependentRevalidations(document, changedFilePath);
    }

    scheduleIncludeCacheWarmup(document);
    
    // Use immediate validation for small edits (like quick fixes) to make diagnostics feel responsive
    const useImmediateValidation = isSmallEdit(contentChanges);
    scheduleValidation(document, async (doc) => {
        await scheduleIncludeCacheWarmup(doc);
        validateDocument(doc);
    }, useImmediateValidation);
}

async function scheduleIncludeCacheWarmup(document) {
    if (!isAcurityDocument(document)) {
        return;
    }

    const documentKey = document.uri.toString();
    if (includeWarmupInFlightByDocument.has(documentKey)) {
        return includeWarmupInFlightByDocument.get(documentKey);
    }

    const pending = (async () => {
        try {
            await warmIncludeCacheForDocument(document);
        } catch (error) {
            logger.debug("Include cache warmup skipped due to error", {
                file: document.fileName || document.uri?.fsPath,
                message: error?.message
            });
        } finally {
            includeWarmupInFlightByDocument.delete(documentKey);
        }
    })();

    includeWarmupInFlightByDocument.set(documentKey, pending);
    return pending;
}



/**
 * Collects diagnostics for an Acurity document without mutating VS Code collections.
 * @param {vscode.TextDocument} document - The document to analyze
 * @returns {Promise<vscode.Diagnostic[]>} - Diagnostics found in the document
 */
async function collectDiagnostics(document, options = {}) {
    if (!isAcurityDocument(document)) {
        return [];
    }

    const workspaceRoot = options.workspaceRoot
        ?? vscode.workspace.getWorkspaceFolder(document.uri)?.uri.fsPath
        ?? null;

    const robodocCfg = vscode.workspace.getConfiguration("acurity.robodoc");
    const robodocConfig = {
        enabled:               robodocCfg.get("enabled",               true),
        requireDocBlock:       robodocCfg.get("requireDocBlock",       true),
        requiredSections:      robodocCfg.get("requiredSections",      ["DESCRIPTION", "CHANGE HISTORY", "USES"]),
        reportFilenamePattern: robodocCfg.get("reportFilenamePattern", "^[A-Za-z0-9]{4}\\.TXT$"),
        reportHeaderPrefix:    robodocCfg.get("reportHeaderPrefix",    "REPORT/"),
        includeHeaderPrefix:   robodocCfg.get("includeHeaderPrefix",   "INCLUDE/"),
        validateHeaderTarget:  robodocCfg.get("validateHeaderTarget",  true),
        authorName:            robodocCfg.get("authorName",            "")
    };
    const getRuleSeverity = buildGetRuleSeverity(detectMode(document));
    return coreCollectDiagnosticsAsync(document, getRuleSeverity, { workspaceRoot, robodocConfig });
}

/**
 * Disposes the diagnostic provider.
 */
function disposeDiagnostics() {
    clearAllPendingValidations();
    includeWarmupInFlightByDocument.clear();
    includePathToDependentDocuments.clear();
    documentToIncludeDependencies.clear();

    if (diagnosticCollection) {
        diagnosticCollection.dispose();
    }
}

export {
    initializeDiagnostics,
    disposeDiagnostics,
    collectDiagnostics
};
