/**
 * String-level parsing utilities for Acurity Architect source code.
 *
 * This module provides lightweight regex-based helpers for extracting specific
 * syntactic elements from individual lines (variable declarations, assignments,
 * include directives, function signatures, etc.).
 *
 * It is intentionally separate from the full structural AST parser at
 * `core/src/ast/parser.js`, which converts a token stream into a complete
 * Program syntax tree. Use the AST when you need structural relationships
 * (block nesting, expression trees, scope information). Use these helpers when
 * you only need to extract a single value from a known line pattern.
 */

import { ACURITY_KEYWORDS } from "./constants.js";

/**
 * Parses a VAR declaration line.
 * Supports two formats:
 * - Standard: VAR varName : TYPE
 * - Alternate: TYPE varName [, varName, ...]
 * @param {string} lineText - The line of text to parse
 * @returns {Object|null} - { variables: string[], type: string } or null if not a VAR declaration
 */
function parseVarDeclaration(lineText) {
    const text = stripTrailingComment(lineText);
    
    // Try standard format first: VAR varName : TYPE
    let match = text.match(/^\s*#?\s*VAR\s+(.+?)\s*:\s*([A-Z0-9_]+)\s*$/i);
    if (match) {
        const variables = match[1]
            .split(",")
            .map((name) => name.trim())
            .filter(Boolean)
            .map((name) => name.match(/[A-Za-z_][A-Za-z0-9_]*/))  
            .filter(Boolean)
            .map((result) => result[0]);

        const type = match[2].toUpperCase();

        return { variables, type };
    }

    // Try alternate format: TYPE varName [, varName, ...]
    // Recognizes: STRING, SHORT, LONG, DATE, DOUBLE, BOOLEAN, TIME, INTEGER, BIGINT
    match = text.match(/^\s*#?\s*(STRING|SHORT|LONG|DATE|DOUBLE|BOOLEAN|TIME|INTEGER|BIGINT)\s+(.+?)\s*$/i);
    if (match) {
        const type = match[1].toUpperCase();
        const variables = match[2]
            .split(",")
            .map((name) => name.trim())
            .filter(Boolean)
            .map((name) => name.match(/[A-Za-z_][A-Za-z0-9_]*/))  
            .filter(Boolean)
            .map((result) => result[0]);

        if (variables.length > 0) {
            return { variables, type };
        }
    }

    return null;
}

/**
 * Parses an assignment statement.
 * @param {string} lineText - The line of text to parse
 * @returns {Object|null} - { target, expression, expressionStart } or null
 */
function parseAssignment(lineText) {
    const text = stripTrailingComment(lineText);
    // Match both "SET variable = expression" and bare "variable = expression"
    const match = text.match(/^\s*#?\s*(?:SET\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$/i);
    if (!match) {
        return null;
    }

    const target = match[1];
    const expression = match[2];
    const prefixMatch = text.match(/^\s*#?\s*(?:SET\s+)?/);
    const searchFrom = prefixMatch ? prefixMatch[0].length : 0;
    const targetIndex = lineText.indexOf(target, searchFrom);
    const equalsIndex = lineText.indexOf("=", targetIndex + target.length);

    return {
        target,
        expression,
        expressionStart: equalsIndex + 1
    };
}

/**
 * Strips trailing comments from a line, respecting quoted strings.
 * @param {string} lineText - The line of text
 * @returns {string} - Text without trailing comment
 */
function stripTrailingComment(lineText) {
    let inSingleQuote = false;
    let inDoubleQuote = false;

    for (let i = 0; i < lineText.length - 1; i += 1) {
        const current = lineText[i];
        const next = lineText[i + 1];

        if (current === "'" && !inDoubleQuote) {
            inSingleQuote = !inSingleQuote;
        } else if (current === '"' && !inSingleQuote) {
            inDoubleQuote = !inDoubleQuote;
        }

        if (!inSingleQuote && !inDoubleQuote && current === "/" && next === "/") {
            return lineText.substring(0, i);
        }
    }

    return lineText;
}

/**
 * Finds an unclosed string in the given text.
 * @param {string} text - The text to search
 * @returns {Object|null} - { start } or null if all strings are closed
 */
function findUnclosedString(text) {
    let inSingleQuote = false;
    let inDoubleQuote = false;
    let start = -1;

    for (let i = 0; i < text.length; i += 1) {
        const char = text[i];
        if (char === "'" && !inDoubleQuote) {
            if (!inSingleQuote) {
                inSingleQuote = true;
                start = i;
            } else {
                inSingleQuote = false;
                start = -1;
            }
        } else if (char === '"' && !inSingleQuote) {
            if (!inDoubleQuote) {
                inDoubleQuote = true;
                start = i;
            } else {
                inDoubleQuote = false;
                start = -1;
            }
        }
    }

    if (inSingleQuote || inDoubleQuote) {
        return { start: Math.max(0, start) };
    }

    return null;
}

/**
 * Replaces quoted strings with spaces to simplify token extraction.
 * @param {string} text - The text to process
 * @returns {string} - Text with quoted strings replaced by spaces
 */
function stripQuotedStrings(text) {
    let result = "";
    let inSingleQuote = false;
    let inDoubleQuote = false;

    for (let i = 0; i < text.length; i += 1) {
        const char = text[i];

        if (char === "'" && !inDoubleQuote) {
            inSingleQuote = !inSingleQuote;
            result += " ";
            continue;
        }

        if (char === '"' && !inSingleQuote) {
            inDoubleQuote = !inDoubleQuote;
            result += " ";
            continue;
        }

        if (inSingleQuote || inDoubleQuote) {
            result += " ";
        } else {
            result += char;
        }
    }

    return result;
}

/**
 * Extracts variable identifiers from an expression, excluding keywords and function calls.
 * @param {string} expression - The expression text
 * @returns {string[]} - Array of variable names
 */
function extractVariables(expression) {
    // Find all identifiers
    const identifierPattern = /[A-Za-z_][A-Za-z0-9_]*/g;
    const matches = [];
    let match;
    
    while ((match = identifierPattern.exec(expression)) !== null) {
        const identifier = match[0];
        const matchEnd = match.index + identifier.length;
        
        // Skip if it's a keyword
        if (ACURITY_KEYWORDS.has(identifier.toUpperCase())) {
            continue;
        }
        
        // Skip if followed by '(' (it's a function call, not a variable reference)
        const nextChar = expression.substring(matchEnd).trim()[0];
        if (nextChar === '(') {
            continue;
        }
        
        matches.push(identifier);
    }
    
    return matches;
}

/**
 * Extracts all function call names from document text (identifiers followed by '(').
 * Strips comments and quoted strings before scanning.
 * @param {string} text - Full document text
 * @returns {string[]} - Array of function names (may contain duplicates)
 */
function parseFunctionCalls(text) {
    const results = [];
    const identifierPattern = /[A-Za-z_][A-Za-z0-9_]*/g;

    for (const rawLine of text.split('\n')) {
        const line = stripQuotedStrings(stripTrailingComment(rawLine));
        let match;
        identifierPattern.lastIndex = 0;
        while ((match = identifierPattern.exec(line)) !== null) {
            const end = match.index + match[0].length;
            const nextChar = line.substring(end).trimStart()[0];
            if (nextChar === '(' && !ACURITY_KEYWORDS.has(match[0].toUpperCase())) {
                results.push(match[0]);
            }
        }
    }

    return results;
}

/**
 * Builds a range for a token within a line.
 * @param {string} lineText - The full line text
 * @param {number} lineIndex - The line index
 * @param {string} token - The token to find
 * @param {number} searchStart - Where to start searching
 * @returns {{ start: { line: number, character: number }, end: { line: number, character: number } }} - The token range
 */
function buildTokenRange(lineText, lineIndex, token, searchStart) {
    const relativeIndex = lineText.substring(searchStart).indexOf(token);
    const tokenIndex = relativeIndex >= 0 ? searchStart + relativeIndex : lineText.indexOf(token);
    const start = Math.max(0, tokenIndex);

    return { start: { line: lineIndex, character: start }, end: { line: lineIndex, character: start + token.length } };
}

/**
 * Parses a FUNCTION declaration line.
 * @param {string} lineText - The line of text to parse
 * @returns {{name: string, upperName: string, params: Array<{name: string, type: string}>, returnType: string|null}|null}
 */
function parseFunctionDeclaration(lineText) {
    const effectiveText = stripTrailingComment(lineText);
    const match = effectiveText.match(
        /^\s*#?\s*FUNCTION\s+([A-Za-z_][A-Za-z0-9_]*)\s*\((.*?)\)(?:\s*:\s*([A-Za-z0-9_]+))?\s*$/i
    );
    if (!match) {
        return null;
    }

    const name = match[1];
    const paramBlock = (match[2] || "").trim();
    const params = [];
    const malformedParams = [];

    if (paramBlock.length > 0) {
        paramBlock
            .split(",")
            .map((p) => p.trim())
            .filter(Boolean)
            .forEach((param) => {
                const paramMatch = param.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([A-Z0-9_]+)$/i);
                if (paramMatch) {
                    params.push({
                        name: paramMatch[1],
                        type: paramMatch[2].toUpperCase()
                    });
                } else {
                    const nameMatch = param.match(/^([A-Za-z_][A-Za-z0-9_]*)/);
                    malformedParams.push({
                        raw: param,
                        name: nameMatch ? nameMatch[1] : param
                    });
                }
            });
    }

    return {
        name,
        upperName: name.toUpperCase(),
        params,
        malformedParams,
        returnType: match[3] ? match[3].toUpperCase() : null
    };
}

/**
 * Parses a MACRO declaration line.
 * @param {string} lineText - The line of text to parse
 * @returns {Object|null} - { name, params: string[], body: string } or null
 */
function parseMacroDeclaration(lineText) {
    const text = stripTrailingComment(lineText);
    
    // Match MACRO name(params) body or MACRO name body
    const withParamsMatch = text.match(/^\s*#?\s*MACRO\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)\s+(.+)$/i);
    if (withParamsMatch) {
        const params = withParamsMatch[2]
            .split(",")
            .map((p) => p.trim())
            .filter(Boolean);
        
        return {
            name: withParamsMatch[1],
            params,
            body: withParamsMatch[3]
        };
    }

    // Match simple MACRO name body (no parameters)
    const simpleMatch = text.match(/^\s*#?\s*MACRO\s+([A-Za-z_][A-Za-z0-9_]*)\s+(.+)$/i);
    if (simpleMatch) {
        return {
            name: simpleMatch[1],
            params: [],
            body: simpleMatch[2]
        };
    }

    return null;
}

/**
 * Parses an INCLUDE/INCLUDEONCE/INCLUDETEST statement and extracts the filename.
 * This is the canonical implementation — definitionProvider and includeResolver/graph both use this.
 * Supports bare filenames and quoted filenames (single or double quotes).
 * @param {string} lineText - The line of text to parse
 * @returns {{ filename: string }|null}
 */
function parseInclude(lineText) {
    const effectiveText = stripTrailingComment(lineText);
    const includeMatch = effectiveText.match(
        /^\s*#?\s*include(?:once|test)?\s+(?:"([^"]+)"|'([^']+)'|([a-zA-Z0-9_.\/-]+))\s*$/i
    );
    if (!includeMatch) return null;
    const filename = includeMatch[1] || includeMatch[2] || includeMatch[3];
    return filename ? { filename } : null;
}

/**
 * Parses a REQUIRE statement line.
 * @param {string} lineText - The line of text to parse
 * @returns {Object|null} - { filename: string, lineIndex from caller } or null if not a REQUIRE statement
 */
function parseRequire(lineText) {
    const text = stripTrailingComment(lineText);
    // Match: REQUIRE "filename" or REQUIRE filename
    const requireMatch = text.match(/^\s*#?\s*REQUIRE\s+(?:"([^"]+)"|'([^']+)'|([a-zA-Z0-9_.\/-]+))\s*$/i);
    
    if (!requireMatch) {
        return null;
    }
    
    const filename = requireMatch[1] || requireMatch[2] || requireMatch[3];
    if (!filename) {
        return null;
    }
    
    return { filename };
}

/**
 * Parses any INCLUDE-family directive (INCLUDE, INCLUDEONCE, INCLUDETEST, REQUIRE)
 * and returns the keyword and the character range of the filepath argument.
 *
 * Returns null if the line is not an INCLUDE-family directive.
 *
 * The filepath range covers only the filename token — any trailing // comment
 * is excluded. This lets callers suppress validations within the filepath only.
 *
 * @param {string} lineText
 * @returns {{ keyword: string, filepath: string, filepathStart: number, filepathEnd: number }|null}
 */
function parseIncludeDirective(lineText) {
    // Strip optional leading # and whitespace, then match keyword + filepath.
    // filepath may be quoted (single or double) or bare (path chars including / and -).
    const match = lineText.match(
        /^(\s*#?\s*)(INCLUDE(?:ONCE|TEST)?|REQUIRE)\s+(?:"([^"]+)"|'([^']+)'|([a-zA-Z0-9_.\/\\-]+))/i
    );
    if (!match) return null;

    const keyword = match[2];
    const filepath = match[3] || match[4] || match[5];
    if (!filepath) return null;

    const filepathStart = lineText.indexOf(filepath, match[1].length + keyword.length);
    const filepathEnd = filepathStart + filepath.length;

    return { keyword, filepath, filepathStart, filepathEnd };
}

/**
 * Returns the raw text of the Nth argument (0-indexed) in a function call expression.
 * Pass the string starting at the function name (e.g. "INDEX_CLOSE(sHandle)").
 * Handles nested parentheses and quoted strings. Returns null if not found.
 */
function extractArgAt(expression, argIndex) {
    const openIdx = expression.indexOf('(');
    if (openIdx === -1) return null;

    let depth = 0;
    let inString = false;
    let stringChar = null;
    let currentArg = 0;
    let argStart = openIdx + 1;

    for (let i = openIdx; i < expression.length; i++) {
        const char = expression[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 expression.substring(argStart, i);
                }
                return null;
            }
        } else if (char === ',' && depth === 1) {
            if (currentArg === argIndex) {
                return expression.substring(argStart, i);
            }
            currentArg++;
            argStart = i + 1;
        }
    }

    return null;
}

export {
    parseVarDeclaration,
    parseAssignment,
    stripTrailingComment,
    findUnclosedString,
    stripQuotedStrings,
    extractVariables,
    parseFunctionCalls,
    buildTokenRange,
    parseFunctionDeclaration,
    parseMacroDeclaration,
    parseInclude,
    parseRequire,
    parseIncludeDirective,
    extractArgAt
};
