/**
 * Naming convention validator for Acurity Architect.
 * Validates that variable names follow prefix conventions.
 */

import { getExpectedTypeFromName, getRecommendedPrefix, AcurityTypes } from "../types/acurityTypes.js";
import { createDiagnostic } from "../diagnostics/diagnosticFactory.js";
import { buildTokenRange } from "../parser.js";

const DEFAULT_CHAR_TO_TYPE_MAP  = { z: "STRING", s: "SHORT", l: "LONG", f: "DOUBLE", b: "BOOLEAN", d: "DATE", t: "TIME" };
const DEFAULT_SCOPE_TO_CHAR_MAP = { "global": "g", "parameter": "p", "function-scoped": "f" };

/**
 * Builds a mapping from prefix character to expected type.
 * Merges any overrides from namingConfig on top of the defaults.
 * @param {Object|null|undefined} namingConfig - optional naming config (e.g. from .acurity.json)
 * @returns {Object} Map of prefix char to expected type (e.g., { z: "STRING", s: "SHORT", ... })
 */
function buildCharToTypeMap(namingConfig) {
    if (namingConfig && namingConfig.charToTypeMap) {
        return Object.assign({}, DEFAULT_CHAR_TO_TYPE_MAP, namingConfig.charToTypeMap);
    }
    return DEFAULT_CHAR_TO_TYPE_MAP;
}

/**
 * Builds a mapping from scope name to prefix character.
 * Merges any overrides from namingConfig on top of the defaults.
 * @param {Object|null|undefined} namingConfig - optional naming config (e.g. from .acurity.json)
 * @returns {Object} Map of scope key to prefix char (e.g., { "global": "g", "parameter": "p", "function-scoped": "f" })
 */
function buildScopeToCharMap(namingConfig) {
    if (namingConfig && namingConfig.scopeToCharMap) {
        return Object.assign({}, DEFAULT_SCOPE_TO_CHAR_MAP, namingConfig.scopeToCharMap);
    }
    return DEFAULT_SCOPE_TO_CHAR_MAP;
}

/**
 * Gets the scope key (for map lookup) from variable info.
 * @param {Object} varInfo - Variable information
 * @returns {string} - Scope key: "global", "parameter", or "function-scoped"
 */
function getScopeKey(varInfo) {
    if (varInfo.isFunctionParameter) {
        return "parameter";
    }
    if (varInfo.isFunctionScoped) {
        return "function-scoped";
    }
    return "global";
}

/**
 * Gets the expected scope name representation for display.
 * @param {string} scopeKey - The scope key ("global", "parameter", or "function-scoped")
 * @returns {string} - Display name
 */
function getScopeLabel(scopeKey) {
    switch (scopeKey) {
        case "global":
            return "global";
        case "parameter":
            return "function parameter";
        case "function-scoped":
            return "function-scoped";
        default:
            return "unknown";
    }
}

function stripExistingVariablePrefix(variableName) {
    // Count how many lowercase letters are at the start of the variable name (up to 2 for global prefixes)
    let prefixLength = 0;
    let maxPrefixLength = 2;
    for (let i = 0; i < variableName.length && i < maxPrefixLength + 1; i++) {
        if (variableName[i] >= 'a' && variableName[i] <= 'z') {
            prefixLength++;
        } else {
            break;
        }
    }

    // If the prefix length is 0 or greater than max allowed, we won't strip anything
    if (prefixLength === 0 || prefixLength > maxPrefixLength) {
        return variableName;
    }

    variableName = variableName.substring(prefixLength);
    return variableName;
}

function getLeadingLowercasePrefix(variableName) {
    let i = 0;
    while (i < variableName.length && variableName[i] >= "a" && variableName[i] <= "z") {
        i += 1;
    }
    return variableName.substring(0, i);
}

/**
 * Validates naming conventions in variable declarations.
 * @param {vscode.TextDocument} document - The document to validate
 * @param {Function} getRuleSeverity - Function to resolve severity from rule id
 * @param {Object} [options={}]
 * @param {Map<string, Object>} [options.variableTypes] - Map of variable names to type info
 * @param {Object|null} [options.namingConfig] - Optional naming config with charToTypeMap / scopeToCharMap overrides
 * @returns {vscode.Diagnostic[]} - Array of naming convention diagnostics
 */
function validateNamingConventions(document, getRuleSeverity, options = {}) {
    const { variableTypes = new Map(), namingConfig } = options;
    const diagnostics = [];
    const charToTypeMap = buildCharToTypeMap(namingConfig);
    const scopeToCharMap = buildScopeToCharMap(namingConfig);

    for (const [_key, varInfo] of variableTypes.entries()) {
        const variableName = varInfo.name;
        const prefix = getLeadingLowercasePrefix(variableName);
        const prefixLength = prefix.length;

        // A single lowercase 'h' prefix is reserved for database fields and should not be used for declared vars.
        if (prefixLength === 1 && prefix === "h") {
            const databaseFieldPrefixDiag = createDiagnostic(
                buildTokenRange(document.lineAt(varInfo.line).text, varInfo.line, variableName, 0),
                { messageId: "ACU-NAM-001", params: {} },
                getRuleSeverity
            );
            if (databaseFieldPrefixDiag) {
                diagnostics.push(databaseFieldPrefixDiag);
            }
            continue;
        }

        // Only infer naming semantics when prefix length is exactly 1 (type only) or 2 (scope + type).
        if (prefixLength !== 1 && prefixLength !== 2) {
            continue;
        }

        const isFunctionParameter = varInfo.isFunctionParameter || false;
        const isFunctionScoped = (varInfo.isFunctionScoped || false) || isFunctionParameter;
        const isGlobal = !isFunctionScoped;
        
        // Build recommended prefix using current settings
        const scopeKey = getScopeKey(varInfo);
        const scopeChar = scopeToCharMap[scopeKey];
        const typeChar = Object.entries(charToTypeMap).find(([ch, type]) => type === varInfo.type)?.[0] || "x";
        const recommendedPrefix = scopeChar + typeChar;
        const recommendedName = recommendedPrefix + stripExistingVariablePrefix(variableName);

        // Scope validation only applies when prefix has two lowercase chars (scope + type).
        if (prefixLength === 2) {
            const actualScopeChar = prefix[0];
            const expectedScopeChar = scopeChar;

            if (actualScopeChar !== expectedScopeChar) {
                const scopeDiag = createDiagnostic(
                    buildTokenRange(document.lineAt(varInfo.line).text, varInfo.line, variableName, 0),
                    { messageId: "ACU-NAM-002", params: { variableName, actualPrefix: actualScopeChar, actualScope: getScopeLabel(Object.entries(scopeToCharMap).find(([k, v]) => v === actualScopeChar)?.[0] || "unknown"), expectedScope: getScopeLabel(scopeKey), recommendedName } },
                    getRuleSeverity
                );
                if (scopeDiag) {
                    diagnostics.push(scopeDiag);
                }
            }
        }

        // Type validation applies to prefix length 1 (type-only) and 2 (scope + type).
        const typeCharToCheck = prefixLength === 1 ? prefix[0] : prefix[1];
        const expectedType = charToTypeMap[typeCharToCheck.toLowerCase()];
        if (expectedType && expectedType !== varInfo.type) {
            const typeDiag = createDiagnostic(
                buildTokenRange(document.lineAt(varInfo.line).text, varInfo.line, variableName, 0),
                { messageId: "ACU-NAM-003", params: { variableName, expectedType, actualType: varInfo.type, recommendedName } },
                getRuleSeverity
            );
            if (typeDiag) {
                diagnostics.push(typeDiag);
            }
        }
    }

    return diagnostics;
}


export {
    validateNamingConventions
};
