"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveSuffix = exports.doCompletionResolve = exports.doCompletion = void 0;
const os_1 = require("os");
const vscode_languageserver_1 = require("vscode-languageserver");
const yaml_1 = require("yaml");
const ansible_1 = require("../utils/ansible");
const docsFormatter_1 = require("../utils/docsFormatter");
const misc_1 = require("../utils/misc");
const yaml_2 = require("../utils/yaml");
const completionProviderUtils_1 = require("./completionProviderUtils");
const priorityMap = {
    nameKeyword: 1,
    moduleName: 2,
    redirectedModuleName: 3,
    keyword: 4,
    // options
    requiredOption: 1,
    option: 2,
    aliasOption: 3,
    // choices
    defaultChoice: 1,
    choice: 2,
};
let dummyMappingCharacter;
let isAnsiblePlaybook;
async function doCompletion(document, position, context) {
    isAnsiblePlaybook = (0, yaml_2.isPlaybook)(document);
    let preparedText = document.getText();
    const offset = document.offsetAt(position);
    // HACK: We need to insert a dummy mapping, so that the YAML parser can properly recognize the scope.
    // This is particularly important when parser has nothing more than indentation to
    // determine the scope of the current line.
    // This is handled w.r.t two scenarios:
    // 1. When we are at the key level, we use `_:` since we expect to work on a pair level.
    // 2. When we are at the value level, we use `__`. We do this because based on the above hack, the
    // use of `_:` at the value level creates invalid YAML as `: ` is an incorrect token in yaml string scalar
    dummyMappingCharacter = "_:";
    const previousCharactersOfCurrentLine = document.getText({
        start: { line: position.line, character: 0 },
        end: { line: position.line, character: position.character },
    });
    if (previousCharactersOfCurrentLine.includes(": ")) {
        // this means we have encountered ": " previously in the same line and thus we are
        // at the value level
        dummyMappingCharacter = "__";
    }
    preparedText = (0, misc_1.insert)(preparedText, offset, dummyMappingCharacter);
    const yamlDocs = (0, yaml_2.parseAllDocuments)(preparedText);
    const extensionSettings = await context.documentSettings.get(document.uri);
    const useFqcn = extensionSettings.ansible.useFullyQualifiedCollectionNames;
    const provideRedirectModulesCompletion = extensionSettings.completion.provideRedirectModules;
    const provideModuleOptionAliasesCompletion = extensionSettings.completion.provideModuleOptionAliases;
    // We need inclusive matching, since cursor position is the position of the character right after it
    // NOTE: Might no longer be required due to the hack above
    const path = (0, yaml_2.getPathAt)(document, position, yamlDocs, true);
    if (path) {
        const node = path[path.length - 1];
        if (node) {
            const docsLibrary = await context.docsLibrary;
            const isPlay = (0, yaml_2.isPlayParam)(path);
            if (isPlay) {
                return getKeywordCompletion(document, position, path, ansible_1.playKeywords);
            }
            if ((0, yaml_2.isBlockParam)(path)) {
                return getKeywordCompletion(document, position, path, ansible_1.blockKeywords);
            }
            if ((0, yaml_2.isRoleParam)(path)) {
                return getKeywordCompletion(document, position, path, ansible_1.roleKeywords);
            }
            if ((0, yaml_2.isTaskParam)(path)) {
                // offer basic task keywords
                const completionItems = getKeywordCompletion(document, position, path, ansible_1.taskKeywords);
                if (isPlay === undefined) {
                    // this can still turn into a play, so we should offer those keywords too
                    completionItems.push(...getKeywordCompletion(document, position, path, ansible_1.playWithoutTaskKeywords));
                }
                // incidentally, the hack mentioned above prevents finding a module in
                // case the cursor is on it
                const module = await (0, yaml_2.findProvidedModule)(path, document, docsLibrary);
                if (!module) {
                    // offer the 'block' keyword (as it is not one of taskKeywords)
                    completionItems.push(...getKeywordCompletion(document, position, path, new Map([["block", ansible_1.blockKeywords.get("block")]])));
                    const inlineCollections = (0, yaml_2.getDeclaredCollections)(path);
                    const cursorAtEndOfLine = atEndOfLine(document, position);
                    let textEdit;
                    const nodeRange = getNodeRange(node, document);
                    if (nodeRange) {
                        textEdit = {
                            range: nodeRange,
                            newText: "", // placeholder
                        };
                    }
                    const cursorAtFirstElementOfList = firstElementOfList(document, nodeRange);
                    // offer modules
                    const moduleCompletionItems = [
                        ...(await docsLibrary.getModuleFqcns(document.uri)),
                    ]
                        .filter((moduleFqcn) => provideRedirectModulesCompletion ||
                        !docsLibrary.getModuleRoute(moduleFqcn)?.redirect)
                        .map((moduleFqcn) => {
                        let priority, kind;
                        if (docsLibrary.getModuleRoute(moduleFqcn)?.redirect) {
                            priority = priorityMap.redirectedModuleName;
                            kind = vscode_languageserver_1.CompletionItemKind.Reference;
                        }
                        else {
                            priority = priorityMap.moduleName;
                            kind = vscode_languageserver_1.CompletionItemKind.Class;
                        }
                        const [namespace, collection, name] = moduleFqcn.split(".");
                        const insertName = useFqcn ? moduleFqcn : name;
                        const insertText = cursorAtEndOfLine
                            ? `${insertName}:${resolveSuffix("dict", // since a module is always a dictionary
                            cursorAtFirstElementOfList, isAnsiblePlaybook)}`
                            : insertName;
                        return {
                            label: useFqcn ? moduleFqcn : name,
                            kind: kind,
                            detail: `${namespace}.${collection}`,
                            sortText: useFqcn
                                ? `${priority}_${moduleFqcn}`
                                : `${priority}_${name}`,
                            filterText: useFqcn
                                ? `${name} ${moduleFqcn} ${collection} ${namespace}` // name should have highest priority (in case of FQCN)
                                : `${name} ${moduleFqcn}`, // name should have priority (in case of no FQCN)
                            data: {
                                documentUri: document.uri, // preserve document URI for completion request
                                moduleFqcn: moduleFqcn,
                                inlineCollections: inlineCollections,
                                atEndOfLine: cursorAtEndOfLine,
                                firstElementOfList: cursorAtFirstElementOfList,
                            },
                            textEdit: {
                                ...textEdit,
                                newText: insertText,
                            },
                        };
                    });
                    completionItems.push(...moduleCompletionItems);
                }
                return completionItems;
            }
            // Provide variable auto-completion if the cursor is inside valid jinja inline brackets in a playbook
            if (isAnsiblePlaybook &&
                (0, yaml_2.isCursorInsideJinjaBrackets)(document, position, path)) {
                const varCompletion = (0, completionProviderUtils_1.getVarsCompletion)(document.uri, path);
                return varCompletion;
            }
            // Check if we're looking for module options or sub-options
            const options = await (0, yaml_2.getPossibleOptionsForPath)(path, document, docsLibrary);
            if (options) {
                const optionMap = new yaml_2.AncestryBuilder(path)
                    .parentOfKey()
                    .get();
                // find options that have been already provided by the user
                const providedOptions = new Set((0, yaml_2.getYamlMapKeys)(optionMap));
                const remainingOptions = [...options.entries()].filter(([, specs]) => !providedOptions.has(specs.name));
                const nodeRange = getNodeRange(node, document);
                const cursorAtFirstElementOfList = firstElementOfList(document, nodeRange);
                const cursorAtEndOfLine = atEndOfLine(document, position);
                return remainingOptions
                    .map(([option, specs]) => {
                    return {
                        name: option,
                        specs: specs,
                    };
                })
                    .filter((option) => provideModuleOptionAliasesCompletion || !isAlias(option))
                    .map((option, index) => {
                    // translate option documentation to CompletionItem
                    const details = (0, docsFormatter_1.getDetails)(option.specs);
                    let priority;
                    if (isAlias(option)) {
                        priority = priorityMap.aliasOption;
                    }
                    else if (option.specs.required) {
                        priority = priorityMap.requiredOption;
                    }
                    else {
                        priority = priorityMap.option;
                    }
                    const completionItem = {
                        label: option.name,
                        detail: details,
                        // using index preserves order from the specification
                        // except when overridden by the priority
                        sortText: priority.toString() + index.toString().padStart(3),
                        kind: isAlias(option)
                            ? vscode_languageserver_1.CompletionItemKind.Reference
                            : vscode_languageserver_1.CompletionItemKind.Property,
                        documentation: (0, docsFormatter_1.formatOption)(option.specs),
                        data: {
                            documentUri: document.uri, // preserve document URI for completion request
                            type: option.specs.type,
                            range: nodeRange,
                            atEndOfLine: cursorAtEndOfLine,
                            firstElementOfList: cursorAtFirstElementOfList,
                        },
                    };
                    const insertText = atEndOfLine(document, position)
                        ? `${option.name}:`
                        : option.name;
                    if (nodeRange) {
                        completionItem.textEdit = {
                            range: nodeRange,
                            newText: insertText,
                        };
                    }
                    else {
                        completionItem.insertText = insertText;
                    }
                    return completionItem;
                });
            }
            // Now check if we're looking for option/sub-option values
            let keyPath;
            // establish path for the key (option/sub-option name)
            if (new yaml_2.AncestryBuilder(path).parent(yaml_1.YAMLMap).getValue() === null) {
                keyPath = new yaml_2.AncestryBuilder(path)
                    .parent(yaml_1.YAMLMap) // compensates for DUMMY MAPPING
                    .parent(yaml_1.YAMLMap)
                    .getKeyPath();
            }
            else {
                // in this case there is a character immediately after DUMMY MAPPING, which
                // prevents formation of nested map
                keyPath = new yaml_2.AncestryBuilder(path).parent(yaml_1.YAMLMap).getKeyPath();
            }
            if (keyPath) {
                const keyNode = keyPath[keyPath.length - 1];
                const keyOptions = await (0, yaml_2.getPossibleOptionsForPath)(keyPath, document, docsLibrary);
                if (keyOptions &&
                    (0, yaml_1.isScalar)(keyNode) &&
                    keyOptions.has(keyNode.value)) {
                    const nodeRange = getNodeRange(node, document);
                    const option = keyOptions.get(keyNode.value);
                    if (option) {
                        const choices = [];
                        let defaultChoice = option.default;
                        if (option.type === "bool" && typeof option.default === "string") {
                            // the YAML parser does not recognize values such as 'Yes'/'no' as booleans
                            defaultChoice =
                                option.default.toLowerCase() === "yes" ? true : false;
                        }
                        if (option.choices) {
                            choices.push(...option.choices);
                        }
                        else if (option.type === "bool") {
                            choices.push(true);
                            choices.push(false);
                        }
                        else if (defaultChoice !== undefined) {
                            choices.push(defaultChoice);
                        }
                        return choices.map((choice, index) => {
                            let priority;
                            if (choice === defaultChoice) {
                                priority = priorityMap.defaultChoice;
                            }
                            else {
                                priority = priorityMap.choice;
                            }
                            const insertValue = new String(choice).toString();
                            const completionItem = {
                                label: insertValue,
                                detail: choice === defaultChoice ? "default" : undefined,
                                // using index preserves order from the specification
                                // except when overridden by the priority
                                sortText: priority.toString() + index.toString().padStart(3),
                                kind: vscode_languageserver_1.CompletionItemKind.Value,
                            };
                            if (nodeRange) {
                                completionItem.textEdit = {
                                    range: nodeRange,
                                    newText: insertValue,
                                };
                            }
                            else {
                                completionItem.insertText = insertValue;
                            }
                            return completionItem;
                        });
                    }
                }
            }
            // check for 'hosts' keyword and 'ansible_host keyword under vars' to provide inventory auto-completion
            let keyPathForHosts;
            const element = new yaml_2.AncestryBuilder(path).parent(yaml_1.YAMLMap).getValue();
            if ((0, yaml_1.isNode)(element) && (0, yaml_1.isScalar)(element) && element["value"] === null) {
                keyPathForHosts = new yaml_2.AncestryBuilder(path)
                    .parent(yaml_1.YAMLMap) // compensates for DUMMY MAPPING
                    .parent(yaml_1.YAMLMap)
                    .getKeyPath();
            }
            else {
                keyPathForHosts = new yaml_2.AncestryBuilder(path)
                    .parent(yaml_1.YAMLMap) // compensates for DUMMY MAPPING
                    .getKeyPath();
            }
            if (keyPathForHosts) {
                const keyNodeForHosts = keyPathForHosts[keyPathForHosts.length - 1];
                const conditionForHostsKeyword = (0, yaml_2.isPlayParam)(keyPathForHosts) &&
                    (0, yaml_1.isScalar)(keyNodeForHosts) &&
                    keyNodeForHosts["value"] === "hosts";
                const conditionForAnsibleHostKeyword = (0, yaml_1.isScalar)(keyNodeForHosts) &&
                    keyNodeForHosts["value"] === "ansible_host" &&
                    new yaml_2.AncestryBuilder(keyPathForHosts)
                        .parent()
                        .parent(yaml_1.YAMLMap)
                        .getStringKey() === "vars";
                if (conditionForHostsKeyword || conditionForAnsibleHostKeyword) {
                    // const nodeRange = getNodeRange(node, document);
                    // nodeRange is not being passed to getHostCompletion because this will prevent
                    // completion for items beyond ',', ':', '!', and we know that 'hosts' keyword supports regex
                    const hostsList = (await context.ansibleInventory).hostList;
                    const testHostCompletion = getHostCompletion(hostsList);
                    return testHostCompletion;
                }
            }
        }
    }
    return [];
}
exports.doCompletion = doCompletion;
function getKeywordCompletion(document, position, path, keywords) {
    const parameterMap = new yaml_2.AncestryBuilder(path)
        .parent(yaml_1.YAMLMap)
        .get();
    // find options that have been already provided by the user
    const providedParams = new Set((0, yaml_2.getYamlMapKeys)(parameterMap));
    const remainingParams = [...keywords.entries()].filter(([keyword]) => !providedParams.has(keyword));
    const nodeRange = getNodeRange(path[path.length - 1], document);
    return remainingParams.map(([keyword, description]) => {
        const priority = keyword === "name" ? priorityMap.nameKeyword : priorityMap.keyword;
        const completionItem = {
            label: keyword,
            kind: vscode_languageserver_1.CompletionItemKind.Property,
            sortText: `${priority}_${keyword}`,
            documentation: description,
        };
        const insertText = atEndOfLine(document, position)
            ? `${keyword}:`
            : keyword;
        if (nodeRange) {
            completionItem.textEdit = {
                range: nodeRange,
                newText: insertText,
            };
        }
        else {
            completionItem.insertText = insertText;
        }
        return completionItem;
    });
}
function getHostCompletion(hostObjectList) {
    return hostObjectList.map(({ host, priority }) => {
        const completionItem = {
            label: host,
            sortText: `${priority}_${host}`,
            kind: [1, 2].includes(priority)
                ? vscode_languageserver_1.CompletionItemKind.Variable
                : vscode_languageserver_1.CompletionItemKind.Value,
        };
        return completionItem;
    });
}
/**
 * Returns an LSP formatted range compensating for the DUMMY MAPPING hack, provided that
 * the node has range information and is a string scalar.
 */
function getNodeRange(node, document) {
    const range = (0, yaml_2.getOrigRange)(node);
    if (range && (0, yaml_1.isScalar)(node) && typeof node.value === "string") {
        const start = range[0];
        let end = range[1];
        // compensate for DUMMY MAPPING
        if (node.value.includes(dummyMappingCharacter)) {
            end -= 2;
        }
        else {
            // colon, being at the end of the line, was excluded from the node
            end -= 1;
        }
        return (0, misc_1.toLspRange)([start, end], document);
    }
}
async function doCompletionResolve(completionItem, context) {
    if (completionItem.data?.moduleFqcn && completionItem.data?.documentUri) {
        // resolve completion for a module
        const docsLibrary = await context.docsLibrary;
        const [module] = await docsLibrary.findModule(completionItem.data.moduleFqcn);
        if (module && module.documentation) {
            const [namespace, collection, name] = completionItem.data.moduleFqcn.split(".");
            let useFqcn = (await context.documentSettings.get(completionItem.data.documentUri)).ansible.useFullyQualifiedCollectionNames;
            if (!useFqcn) {
                // determine if the short name can really be used
                const declaredCollections = completionItem.data?.inlineCollections || [];
                declaredCollections.push("ansible.builtin");
                const metadata = await context.documentMetadata.get(completionItem.data.documentUri);
                if (metadata) {
                    declaredCollections.push(...metadata.collections);
                }
                const canUseShortName = declaredCollections.some((c) => c === `${namespace}.${collection}`);
                if (!canUseShortName) {
                    // not an Ansible built-in module, and not part of the declared
                    // collections
                    useFqcn = true;
                }
            }
            const insertName = useFqcn ? completionItem.data.moduleFqcn : name;
            const insertText = completionItem.data.atEndOfLine
                ? `${insertName}:${resolveSuffix("dict", // since a module is always a dictionary
                completionItem.data.firstElementOfList, isAnsiblePlaybook)}`
                : insertName;
            if (completionItem.textEdit) {
                completionItem.textEdit.newText = insertText;
                completionItem.insertTextFormat = vscode_languageserver_1.InsertTextFormat.Snippet;
            }
            else {
                completionItem.insertText = insertText;
                completionItem.insertTextFormat = vscode_languageserver_1.InsertTextFormat.PlainText;
            }
            completionItem.documentation = (0, docsFormatter_1.formatModule)(module.documentation, docsLibrary.getModuleRoute(completionItem.data.moduleFqcn));
        }
    }
    if (completionItem.data?.type) {
        // resolve completion for a module option or sub-option
        const insertText = completionItem.data.atEndOfLine
            ? `${completionItem.label}:${resolveSuffix(completionItem.data.type, completionItem.data.firstElementOfList, isAnsiblePlaybook)}`
            : `${completionItem.label}`;
        if (completionItem.textEdit) {
            completionItem.textEdit.newText = insertText;
        }
        else {
            completionItem.insertText = insertText;
        }
    }
    return completionItem;
}
exports.doCompletionResolve = doCompletionResolve;
function isAlias(option) {
    return option.name !== option.specs.name;
}
function atEndOfLine(document, position) {
    const charAfterCursor = `${document.getText()}\n`[document.offsetAt(position)];
    return charAfterCursor === "\n" || charAfterCursor === "\r";
}
/**
 * A utility function to check if the item is the first element of a list or not
 * @param document - current document
 * @param nodeRange - range of the keyword in the document
 * @returns boolean true if the key is the first element of the list, else false
 */
function firstElementOfList(document, nodeRange) {
    if (!nodeRange) {
        return false;
    }
    const checkNodeRange = {
        start: { line: nodeRange.start.line, character: 0 },
        end: nodeRange.start,
    };
    const elementsBeforeKey = document.getText(checkNodeRange).trim();
    return elementsBeforeKey === "-";
}
function resolveSuffix(optionType, firstElementOfList, isDocPlaybook) {
    let returnSuffix;
    if (isDocPlaybook) {
        // if doc is a playbook, indentation will shift one tab since a play is a list
        switch (optionType) {
            case "list":
                returnSuffix = firstElementOfList ? `${os_1.EOL}\t\t- ` : `${os_1.EOL}\t- `;
                break;
            case "dict":
                returnSuffix = firstElementOfList ? `${os_1.EOL}\t\t` : `${os_1.EOL}\t`;
                break;
            default:
                returnSuffix = " ";
                break;
        }
    }
    else {
        // if doc is not a playbook (any other ansible file like task file, etc.) indentation will not
        // include that extra tab
        switch (optionType) {
            case "list":
                returnSuffix = `${os_1.EOL}\t- `;
                break;
            case "dict":
                returnSuffix = `${os_1.EOL}\t`;
                break;
            default:
                returnSuffix = " ";
                break;
        }
    }
    return returnSuffix;
}
exports.resolveSuffix = resolveSuffix;
