/**
 * Argument string validator for Acurity Architect.
 * Validates string literal arguments passed to specific built-in functions.
 * Only string literals are checked — variable arguments are skipped.
 */

import { createDiagnostic } from "../diagnostics/diagnosticFactory.js";
import { stripTrailingComment } from "../parser.js";
import { isValidTableSynonym, getHPrefixSuggestion } from "../builtins/tableSynonymsRegistry.js";
import { BuiltInFunctions, isBuiltInGlobalVariable } from "../builtins/functions.js";

const VALID_CURSOR_DIRECTIONS = new Set([">=", ">", "+", "=", "-", "<", "<="]);
const CURSOR_DIRECTION_SUGGESTIONS = { "=>": ">=", "=<": "<=" };

const DATE_PATTERN = /^(\d{2})\/(\d{2})\/(\d{4})$/;
const MAX_DATE_YEAR = 9998;

/**
 * Finds all positions of a named function call in a line.
 * Returns an array of { matchStart, openParen } where openParen is the
 * absolute column index of the opening parenthesis.
 */
function findFunctionCallPositions(lineText, functionName) {
    const positions = [];
    const pattern = new RegExp(`\\b${functionName}\\s*\\(`, "gi");
    let match;
    while ((match = pattern.exec(lineText)) !== null) {
        const openParen = match.index + match[0].length - 1;
        positions.push({ matchStart: match.index, openParen });
    }
    return positions;
}

/**
 * Returns the absolute { start, end } column bounds for the Nth argument
 * (0-indexed) of the function call whose opening parenthesis is at openParen.
 * start is the first character after the opening paren or preceding comma.
 * end is the position of the trailing comma or closing paren (exclusive bound
 * for the argument content, inclusive for the delimiter).
 * Returns null if the argument does not exist.
 */
function findArgBounds(lineText, openParen, argIndex) {
    let depth = 0;
    let inString = false;
    let stringChar = null;
    let currentArg = 0;
    let argStart = openParen + 1;

    for (let i = openParen; i < lineText.length; i++) {
        const char = lineText[i];

        if (inString) {
            if (char === stringChar) {
                inString = false;
            }
            continue;
        }

        if (char === '"' || char === "'") {
            inString = true;
            stringChar = char;
        } else if (char === "(") {
            depth++;
        } else if (char === ")") {
            depth--;
            if (depth === 0) {
                if (currentArg === argIndex) {
                    return { start: argStart, end: i };
                }
                return null;
            }
        } else if (char === "," && depth === 1) {
            if (currentArg === argIndex) {
                return { start: argStart, end: i };
            }
            currentArg++;
            argStart = i + 1;
        }
    }

    return null;
}

/**
 * Extracts a quoted string literal from argument bounds.
 * Accepts both double-quoted ("...") and single-quoted ('...') strings.
 * Returns { value, start, end } where start is the column of the opening quote
 * and end is one past the closing quote (exclusive).
 * Returns null if the argument is not a plain quoted string literal.
 */
function extractStringLiteralArg(lineText, argStart, argEnd) {
    let start = argStart;
    let end = argEnd;

    while (start < end && lineText[start] === " ") {
        start++;
    }
    while (end > start && lineText[end - 1] === " ") {
        end--;
    }

    if (end - start < 2) {
        return null;
    }
    const quoteChar = lineText[start];
    if ((quoteChar !== '"' && quoteChar !== "'") || lineText[end - 1] !== quoteChar) {
        return null;
    }

    const value = lineText.substring(start + 1, end - 1);
    return { value, start, end };
}

function checkStrToDate(lineText, lineIndex, diagnostics, getRuleSeverity) {
    const positions = findFunctionCallPositions(lineText, "STR_TO_DATE");
    for (const { openParen } of positions) {
        const argBounds = findArgBounds(lineText, openParen, 0);
        if (!argBounds) {
            continue;
        }

        const literal = extractStringLiteralArg(lineText, argBounds.start, argBounds.end);
        if (!literal) {
            continue;
        }

        const dateMatch = literal.value.match(DATE_PATTERN);
        if (dateMatch) {
            const day = parseInt(dateMatch[1], 10);
            const month = parseInt(dateMatch[2], 10);
            const year = parseInt(dateMatch[3], 10);

            if (day > 31 || month > 12) {
                const diag = createDiagnostic(
                    { start: { line: lineIndex, character: literal.start }, end: { line: lineIndex, character: literal.end } },
                    { messageId: "ACU-ARG-001", params: { value: literal.value } },
                    getRuleSeverity
                );
                if (diag) {
                    diagnostics.push(diag);
                }
                continue;
            }

            if (year > MAX_DATE_YEAR) {
                const diag = createDiagnostic(
                    { start: { line: lineIndex, character: literal.start }, end: { line: lineIndex, character: literal.end } },
                    { messageId: "ACU-ARG-002", params: { value: literal.value } },
                    getRuleSeverity
                );
                if (diag) {
                    diagnostics.push(diag);
                }
            }
            continue;
        }

        const diag = createDiagnostic(
            { start: { line: lineIndex, character: literal.start }, end: { line: lineIndex, character: literal.end } },
            { messageId: "ACU-ARG-001", params: { value: literal.value } },
            getRuleSeverity
        );
        if (diag) {
            diagnostics.push(diag);
        }
    }
}

function checkIndexReadByKey(lineText, lineIndex, diagnostics, getRuleSeverity) {
    const positions = [
        ...findFunctionCallPositions(lineText, "INDEX_READ_BY_KEY"),
        ...findFunctionCallPositions(lineText, "SQL_INDEX_READ_BY_KEY")
    ];
    for (const { openParen } of positions) {
        const argBounds = findArgBounds(lineText, openParen, 1);
        if (!argBounds) {
            continue;
        }

        const literal = extractStringLiteralArg(lineText, argBounds.start, argBounds.end);
        if (!literal) {
            continue;
        }

        if (VALID_CURSOR_DIRECTIONS.has(literal.value)) {
            continue;
        }

        const suggestion = CURSOR_DIRECTION_SUGGESTIONS[literal.value];
        if (suggestion) {
            const diag = createDiagnostic(
                { start: { line: lineIndex, character: literal.start }, end: { line: lineIndex, character: literal.end } },
                { messageId: "ACU-ARG-004", params: { value: literal.value, suggestion } },
                getRuleSeverity
            );
            if (diag) {
                diagnostics.push(diag);
            }
        } else {
            const diag = createDiagnostic(
                { start: { line: lineIndex, character: literal.start }, end: { line: lineIndex, character: literal.end } },
                { messageId: "ACU-ARG-003", params: { value: literal.value } },
                getRuleSeverity
            );
            if (diag) {
                diagnostics.push(diag);
            }
        }
    }
}

/**
 * Static list of { functionName, argIndex, paramLabel } for every VAR-mode
 * parameter defined in the built-in function registry.
 * Built once at module load time so iteration in the hot path is O(n checks).
 */
const VAR_PARAM_CHECKS = [];
for (const [funcName, entry] of Object.entries(BuiltInFunctions)) {
    if (!entry.paramModes) {
        continue;
    }
    for (let i = 0; i < entry.paramModes.length; i++) {
        if (entry.paramModes[i] === "VAR") {
            const paramLabel = entry.paramLabels ? entry.paramLabels[i] : `param${i + 1}`;
            VAR_PARAM_CHECKS.push({ functionName: funcName, argIndex: i, paramLabel });
        }
    }
}

/**
 * Returns true when argText (trimmed) is a literal value — a quoted string,
 * a numeric literal, or a built-in pseudo-constant such as MIN_STR.
 */
function isLiteralValue(argText) {
    const t = argText.trim();
    if (t.length === 0) {
        return false;
    }
    if ((t[0] === '"' && t[t.length - 1] === '"') || (t[0] === "'" && t[t.length - 1] === "'")) {
        return true;
    }
    if (/^-?\d/.test(t)) {
        return true;
    }
    return isBuiltInGlobalVariable(t);
}

function checkVarParamLiterals(lineText, lineIndex, diagnostics, getRuleSeverity) {
    for (const { functionName, argIndex, paramLabel } of VAR_PARAM_CHECKS) {
        const positions = findFunctionCallPositions(lineText, functionName);
        for (const { openParen } of positions) {
            const argBounds = findArgBounds(lineText, openParen, argIndex);
            if (!argBounds) {
                continue;
            }

            const rawArg = lineText.substring(argBounds.start, argBounds.end);
            if (!isLiteralValue(rawArg)) {
                continue;
            }

            let tokenStart = argBounds.start;
            let tokenEnd = argBounds.end;
            while (tokenStart < tokenEnd && lineText[tokenStart] === " ") {
                tokenStart++;
            }
            while (tokenEnd > tokenStart && lineText[tokenEnd - 1] === " ") {
                tokenEnd--;
            }

            const diag = createDiagnostic(
                { start: { line: lineIndex, character: tokenStart }, end: { line: lineIndex, character: tokenEnd } },
                { messageId: "ACU-ARG-008", params: { paramLabel, functionName } },
                getRuleSeverity
            );
            if (diag) {
                diagnostics.push(diag);
            }
        }
    }
}

const TABLE_SYNONYM_FUNCTIONS = [
    "INDEX_SAVE_POSITION",
    "INDEX_RESTORE_POSITION",
    "INDEX_OPEN_STANDARD",
    "INDEX_OPEN_CUSTOM",
    "SQL_INDEX_OPEN_STANDARD"
];

function checkTableSynonym(lineText, lineIndex, diagnostics, getRuleSeverity) {
    for (const fnName of TABLE_SYNONYM_FUNCTIONS) {
        const positions = findFunctionCallPositions(lineText, fnName);
        for (const { openParen } of positions) {
            const argBounds = findArgBounds(lineText, openParen, 0);
            if (!argBounds) {
                continue;
            }

            const literal = extractStringLiteralArg(lineText, argBounds.start, argBounds.end);
            if (!literal) {
                continue;
            }

            if (isValidTableSynonym(literal.value)) {
                continue;
            }

            const suggestion = getHPrefixSuggestion(literal.value);
            const messageId = "ACU-ARG-005";
            const params = suggestion
                ? { value: literal.value, suggestion }
                : { value: literal.value };

            const diag = createDiagnostic(
                { start: { line: lineIndex, character: literal.start }, end: { line: lineIndex, character: literal.end } },
                { messageId, params },
                getRuleSeverity
            );
            if (diag) {
                diagnostics.push(diag);
            }
        }
    }
}

/**
 * Validates string literal arguments for specific built-in functions.
 * Currently checks STR_TO_DATE (date format DD/MM/YYYY),
 * INDEX_READ_BY_KEY (cursor direction), and
 * INDEX_SAVE/RESTORE_POSITION / INDEX_OPEN_* (table synonyms).
 * @param {vscode.TextDocument} document
 * @param {Function} getRuleSeverity
 * @param {Object} options
 * @returns {vscode.Diagnostic[]}
 */
function validateArgumentStrings(document, getRuleSeverity, options = {}) {
    const diagnostics = [];

    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex++) {
        if (options.executionClassificationMap) {
            const cls = options.executionClassificationMap[lineIndex];
            if (cls.isOutput || !cls.isExecutable) {
                continue;
            }
        }

        const rawLineText = document.lineAt(lineIndex).text;
        const lineText = stripTrailingComment(rawLineText);

        checkStrToDate(lineText, lineIndex, diagnostics, getRuleSeverity);
        checkIndexReadByKey(lineText, lineIndex, diagnostics, getRuleSeverity);
        checkTableSynonym(lineText, lineIndex, diagnostics, getRuleSeverity);
        checkVarParamLiterals(lineText, lineIndex, diagnostics, getRuleSeverity);
    }

    return diagnostics;
}

export { validateArgumentStrings };
