/**
 * Shared execution-context helpers for validators.
 */

import { stripTrailingComment, parseIncludeDirective } from "../parser.js";

/**
 * Extracts output interpretation delimiters (OICC/CICC) from #OPTION lines.
 * Defaults to "@" when not specified.
 *
 * @param {import("vscode").TextDocument} document - The document to analyze
 * @returns {{oicc: string, cicc: string}}
 */
function extractOutputDelimiters(document) {
    let oicc = "@";
    let cicc = "@";

    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
        const lineText = document.lineAt(lineIndex).text;
        const optionMatch = lineText.match(/^\s*#?\s*OPTION\s+OICC\s*=\s*["'](.?)["']\s*,\s*CICC\s*=\s*["'](.?)["']/i);
        if (optionMatch) {
            oicc = optionMatch[1];
            cicc = optionMatch[2];
            break;
        }
    }

    return { oicc, cicc };
}

function escapeRegexCharacter(character) {
    return character.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

/**
 * Extracts code content from an output line with position tracking.
 *
 * @param {string} lineText - The output line text
 * @param {string} oicc - Output interpretation open character
 * @param {string} cicc - Output interpretation close character
 * @returns {Array<{snippet: string, position: number}>}
 */
function extractCodeFromOutputLineWithPositions(lineText, oicc, cicc) {
    const codeSnippets = [];
    const escapedOicc = escapeRegexCharacter(oicc);
    const escapedCicc = escapeRegexCharacter(cicc);
    const regex = new RegExp(`${escapedOicc}([^${escapedOicc}${escapedCicc}]*)${escapedCicc}`, "g");
    let match;

    while ((match = regex.exec(lineText)) !== null) {
        if (match[1].trim()) {
            codeSnippets.push({
                snippet: match[1],
                position: match.index + 1
            });
        }
    }

    return codeSnippets;
}

/**
 * Determines whether a non-# line should be treated as executable code.
 *
 * @param {string} lineText - Line text with trailing comments removed
 * @returns {boolean}
 */
function isLikelyExecutableLine(lineText) {
    const trimmed = lineText.trim();
    if (!trimmed) {
        return false;
    }

    // Comments are executable (for classification purposes)
    if (trimmed.startsWith("//")) {
        return true;
    }

    if (/^(option|newinterpreter|var|set|if|else|endif|while|do|forever|until|enddo|case|value|otherwise|of|endcase|break|continue|return|macro|function|endfunction|include|includeonce|includetest|require|exit|end-of-code|header|body|footer|hash-on|hash-off)\b/i.test(trimmed)) {
        return true;
    }

    // Alternate variable declaration syntax: TYPE varName [, varName...]
    // Recognizes: STRING, SHORT, LONG, DATE, DOUBLE, BOOLEAN, TIME, INTEGER, BIGINT
    if (/^(string|short|long|date|double|boolean|time|integer|bigint)\s+[A-Za-z_][A-Za-z0-9_]*(\s*,\s*[A-Za-z_][A-Za-z0-9_]*)*\s*$/i.test(trimmed)) {
        return true;
    }

    if (/^[A-Za-z_][A-Za-z0-9_]*\s*(\+|-|\*|\/)?=/.test(trimmed)) {
        return true;
    }

    if (/\b[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(trimmed)) {
        return true;
    }

    return false;
}

/**
 * Determines which code targets from a line should be function-validated.
 *
 * @param {string} lineText - Line text with trailing comments removed
 * @param {{oicc: string, cicc: string}} outputDelimiters - Active output delimiters
 * @returns {Array<{snippet:string, position:number}>}
 */
function getFunctionValidationTargetsForLine(lineText, outputDelimiters, options = {}) {
    const { oicc, cicc } = outputDelimiters;
    const { isOutputLine = false } = options;

    if (isOutputLine) {
        return extractCodeFromOutputLineWithPositions(lineText, oicc, cicc);
    }

    if (/^\s*#/.test(lineText)) {
        // NOTE-style comments (#NOTE / # NOTE) are comment text, not executable code.
        if (/^\s*#\s*NOTE\b/i.test(lineText)) {
            return [];
        }
        // INCLUDE-family lines: the argument is a filepath, not an expression.
        if (parseIncludeDirective(lineText)) {
            return [];
        }
        return [{ snippet: lineText, position: 0 }];
    }

    // INCLUDE-family lines in HASH-OFF mode.
    if (parseIncludeDirective(lineText)) {
        return [];
    }

    const codeSnippetsWithPos = extractCodeFromOutputLineWithPositions(lineText, oicc, cicc);
    if (codeSnippetsWithPos.length > 0) {
        return codeSnippetsWithPos;
    }

    if (isLikelyExecutableLine(lineText)) {
        return [{ snippet: lineText, position: 0 }];
    }

    return [];
}

/**
 * Pre-computes function-validation targets per line.
 *
 * @param {import("vscode").TextDocument} document - The document to analyze
 * @param {{oicc: string, cicc: string}} outputDelimiters - Active output delimiters
 * @returns {Array<Array<{snippet:string, position:number}>>}
 */
function buildFunctionValidationTargetsByLine(document, outputDelimiters, outputLineMap = null) {
    const targetsByLine = new Array(document.lineCount);
    const effectiveOutputLineMap = outputLineMap || buildOutputLineMap(document);

    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
        const rawLineText = document.lineAt(lineIndex).text;
        const lineText = stripTrailingComment(rawLineText);
        targetsByLine[lineIndex] = getFunctionValidationTargetsForLine(lineText, outputDelimiters, {
            isOutputLine: effectiveOutputLineMap[lineIndex]
        });
    }

    return targetsByLine;
}

/**
 * Extracts interpolation targets from an output line.
 *
 * @param {string} lineText - Raw line text
 * @returns {Array<{variableName:string, startPos:number, endPos:number, hasClosing:boolean, isFunctionLike:boolean}>}
 */
function getInterpolationTargetsForLine(lineText, outputDelimiters = { oicc: "^", cicc: "^" }) {
    const targets = [];
    const seen = new Set();
    const delimiterPairs = [{ oicc: outputDelimiters.oicc, cicc: outputDelimiters.cicc }];

    // Backward compatibility: many existing scripts/tests use caret interpolation.
    if (outputDelimiters.oicc !== "^" || outputDelimiters.cicc !== "^") {
        delimiterPairs.push({ oicc: "^", cicc: "^" });
    }

    delimiterPairs.forEach(({ oicc, cicc }) => {
        const escapedOicc = escapeRegexCharacter(oicc);
        const escapedCicc = escapeRegexCharacter(cicc);
        const interpolationPattern = new RegExp(`${escapedOicc}([A-Za-z_][A-Za-z0-9_]*)${escapedCicc}?`, "g");
        let match;

        while ((match = interpolationPattern.exec(lineText)) !== null) {
            const variableName = match[1];
            const startPos = match.index;
            const endPos = match.index + match[0].length;
            const isFunctionLike = lineText[endPos] === "(";
            const hasClosing = match[0].endsWith(cicc);
            const key = `${startPos}:${endPos}:${variableName}`;

            if (seen.has(key)) {
                continue;
            }
            seen.add(key);

            targets.push({
                variableName,
                startPos,
                endPos,
                hasClosing,
                isFunctionLike
            });
        }
    });

    return targets;
}

/**
 * Pre-computes interpolation targets for output lines.
 * Non-output lines are returned as empty arrays for consistent indexing.
 *
 * @param {import("vscode").TextDocument} document - The document to analyze
 * @param {boolean[]} outputLineMap - Output-line flags by line index
 * @returns {Array<Array<{variableName:string, startPos:number, endPos:number, hasClosing:boolean, isFunctionLike:boolean}>>}
 */
function buildInterpolationTargetsByLine(document, outputLineMap, outputDelimiters = { oicc: "^", cicc: "^" }) {
    const targetsByLine = new Array(document.lineCount);

    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
        if (!outputLineMap[lineIndex]) {
            targetsByLine[lineIndex] = [];
            continue;
        }

        const lineText = document.lineAt(lineIndex).text;
        targetsByLine[lineIndex] = getInterpolationTargetsForLine(lineText, outputDelimiters);
    }

    return targetsByLine;
}

/**
 * Checks if a line ends with `...` (multi-line continuation marker).
 * The `...` must appear before any comment.
 *
 * @param {string} lineText - The raw line text
 * @returns {boolean}
 */
function isLineWithContinuation(lineText) {
    const trimmed = lineText.trim();
    if (!trimmed) {
        return false;
    }
    
    // Strip trailing comment first to check for ... before comment
    let commentStart = lineText.indexOf("//");
    let inString = false;
    let stringChar = null;
    
    for (let i = 0; i < lineText.length; i++) {
        const char = lineText[i];
        if (char === '"' || char === "'") {
            if (!inString) {
                inString = true;
                stringChar = char;
            } else if (char === stringChar) {
                inString = false;
            }
        }
        if (!inString && char === "/" && i + 1 < lineText.length && lineText[i + 1] === "/") {
            commentStart = i;
            break;
        }
    }
    
    const beforeComment = commentStart >= 0 ? lineText.substring(0, commentStart) : lineText;
    return /\.\.\.\s*$/.test(beforeComment);
}

/**
 * Returns the language-default hash mode for reports.
 * Acurity semantics: reports begin in HASH-ON until HASH-OFF is encountered.
 *
 * @returns {'HASH-ON'}
 */
function getDefaultHashMode() {
    return 'HASH-ON';
}

/**
 * Builds a map of lines that should be treated as output text.
 *
 * @param {import("vscode").TextDocument} document - The document to analyze
 * @returns {boolean[]}
 */
function buildOutputLineMap(document) {
    const outputLineMap = new Array(document.lineCount).fill(false);
    let isHashOn = getDefaultHashMode() === 'HASH-ON';

    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
        const lineText = document.lineAt(lineIndex).text;
        const trimmed = lineText.trim();

        if (/^\s*#?\s*HASH-ON\b/i.test(lineText)) {
            isHashOn = true;
        } else if (/^\s*#?\s*HASH-OFF\b/i.test(lineText)) {
            isHashOn = false;
        }

        if (trimmed.length === 0 || trimmed.startsWith("//")) {
            outputLineMap[lineIndex] = false;
            continue;
        }

        if (trimmed.startsWith(">>")) {
            outputLineMap[lineIndex] = true;
            continue;
        }

        // DEBUG-mode output conventions in HASH-ON and HASH-OFF styles.
        if (trimmed.startsWith("??") || trimmed.startsWith("#??")) {
            outputLineMap[lineIndex] = true;
            continue;
        }

        outputLineMap[lineIndex] = isHashOn && !trimmed.startsWith("#");
    }

    return outputLineMap;
}

/**
 * Builds a comprehensive execution classification map for each line.
 * This pre-computes line classification to avoid repeated ad-hoc checks.
 *
 * @param {import("vscode").TextDocument} document - The document to analyze
 * @returns {Array<{
 *   isOutput: boolean,
 *   outputKind: 'explicit'|'debug'|'implicit'|null,
 *   isExecutable: boolean,
 *   isInvalidExecutable: boolean,
 *   hashMode: 'HASH-ON'|'HASH-OFF',
 *   effectiveContent: string,
 *   hasHashPrefix: boolean,
 *   rawLine: string
 * }>}
 */
function buildExecutionClassificationMap(document) {
    const classificationMap = new Array(document.lineCount);
    let currentHashMode = getDefaultHashMode();

    let prevLineEndedWithContinuation = false;
    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
        const isFollower = prevLineEndedWithContinuation;
        const rawLine = document.lineAt(lineIndex).text;
        const rawTrimmed = rawLine.trim();
        const lineText = stripTrailingComment(rawLine);
        const trimmed = lineText.trim();

        // Update hash mode state
        if (/^\s*#?\s*HASH-ON\b/i.test(lineText)) {
            currentHashMode = 'HASH-ON';
        } else if (/^\s*#?\s*HASH-OFF\b/i.test(lineText)) {
            currentHashMode = 'HASH-OFF';
        }

        // Determine whether this line can carry continuation to the next.
        // Only code lines carry '...' continuation — output and comment lines do not.
        const lineIsOutput =
            rawTrimmed.startsWith("//") ||
            rawTrimmed.startsWith(">>") ||
            rawTrimmed.startsWith("??") ||
            rawTrimmed.startsWith("#??") ||
            (currentHashMode === 'HASH-ON' && !rawTrimmed.startsWith("#"));
        prevLineEndedWithContinuation = !lineIsOutput && isLineWithContinuation(rawLine);

        const classification = {
            isOutput: false,
            outputKind: null,
            isExecutable: false,
            isInvalidExecutable: false,
            isContinuationFollower: false,
            hashMode: currentHashMode,
            effectiveContent: lineText,
            hasHashPrefix: /^\s*#/.test(lineText),
            rawLine: rawLine
        };

        const hashStripped = trimmed.startsWith("#")
            ? trimmed.substring(1).trimStart()
            : lineText;

        // HASH mode directives are always executable control statements.
        if (/^#?\s*HASH-(ON|OFF)\b/i.test(trimmed)) {
            classification.isExecutable = true;
            classification.effectiveContent = hashStripped;
            classificationMap[lineIndex] = classification;
            continue;
        }

        // Empty or comment-only lines
        if (trimmed.length === 0 || rawTrimmed.startsWith("//")) {
            classificationMap[lineIndex] = classification;
            continue;
        }

        // Explicit output (>>)
        if (trimmed.startsWith(">>")) {
            classification.isOutput = true;
            classification.outputKind = 'explicit';
            classification.effectiveContent = trimmed.substring(2).trimStart();
            classificationMap[lineIndex] = classification;
            continue;
        }

        // Debug output (?? or #??)
        if (trimmed.startsWith("??")) {
            classification.isOutput = true;
            classification.outputKind = 'debug';
            classification.effectiveContent = trimmed.substring(2).trimStart();
            classificationMap[lineIndex] = classification;
            continue;
        }
        if (trimmed.startsWith("#??")) {
            classification.isOutput = true;
            classification.outputKind = 'debug';
            classification.effectiveContent = trimmed.substring(3).trimStart();
            classificationMap[lineIndex] = classification;
            continue;
        }

        // HASH-ON comment directives.
        if (currentHashMode === 'HASH-ON' && /^#\s*NOTE\b/i.test(trimmed)) {
            classificationMap[lineIndex] = classification;
            continue;
        }

        // HASH-ON hash-prefixed comments (e.g., #// comment).
        if (currentHashMode === 'HASH-ON' && /^#\s*\/\//.test(rawTrimmed)) {
            classificationMap[lineIndex] = classification;
            continue;
        }

        // HASH-ON mode: lines without # are output (implicit output)
        if (currentHashMode === 'HASH-ON' && !trimmed.startsWith("#")) {
            classification.isOutput = true;
            classification.outputKind = 'implicit';
            // In HASH-ON mode, check if this looks like executable code
            // (some scripts mix output and executable in HASH-ON)
            classification.isExecutable = isLikelyExecutableLine(lineText);
            classificationMap[lineIndex] = classification;
            continue;
        }

        // Lines with # prefix in HASH-ON mode are executable
        if (currentHashMode === 'HASH-ON' && trimmed.startsWith("#")) {
            classification.effectiveContent = hashStripped;
            // A lone '#' (nothing after the prefix) is treated as a blank line — not an error.
            if (!hashStripped) {
                classificationMap[lineIndex] = classification;
                continue;
            }
            classification.isContinuationFollower = isFollower;
            classification.isExecutable = isFollower || isLikelyExecutableLine(hashStripped);
            if (!classification.isExecutable) {
                classification.isInvalidExecutable = true;
            }
            classificationMap[lineIndex] = classification;
            continue;
        }

        // HASH-OFF mode: non-output lines are treated as executable source.
        // Normalize optional legacy '#' prefix once here so downstream validators
        // do not need to duplicate HASH mode handling.
        if (currentHashMode === 'HASH-OFF') {
            const normalizedLine = hashStripped;
            classification.effectiveContent = normalizedLine;
            classification.isContinuationFollower = isFollower;
            classification.isExecutable = isFollower || isLikelyExecutableLine(normalizedLine);
            if (!classification.isExecutable) {
                classification.isInvalidExecutable = true;
            }
            classificationMap[lineIndex] = classification;
            continue;
        }

        classificationMap[lineIndex] = classification;
    }

    return classificationMap;
}

export {
    extractOutputDelimiters,
    buildOutputLineMap,
    buildExecutionClassificationMap,
    getFunctionValidationTargetsForLine,
    buildFunctionValidationTargetsByLine,
    getInterpolationTargetsForLine,
    buildInterpolationTargetsByLine,
    getDefaultHashMode,
    isLineWithContinuation
};
