"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isCursorInsideJinjaBrackets = exports.isPlaybook = exports.parseAllDocuments = exports.getOrigRange = exports.getYamlMapKeys = exports.findProvidedModule = exports.getTaskParamPathWithTrace = exports.getPossibleOptionsForPath = exports.isRoleParam = exports.isBlockParam = exports.isPlayParam = exports.getDeclaredCollections = exports.isTaskParam = exports.tasksKey = exports.getPathAtOffset = exports.contains = exports.getPathAt = exports.AncestryBuilder = void 0;
const _ = __importStar(require("lodash"));
const yaml_1 = require("yaml");
const ansible_1 = require("./ansible");
const ansible_2 = require("../utils/ansible");
const vscode_languageserver_1 = require("vscode-languageserver");
/**
 * A helper class used for building YAML path assertions and retrieving parent
 * nodes. The assertions are built up from the most nested (last in array)
 * element.
 */
class AncestryBuilder {
    _path;
    _index;
    constructor(path, index) {
        this._path = path || [];
        this._index = index || this._path.length - 1;
    }
    /**
     * Move up the path, optionally asserting the type of the parent.
     *
     * Unless Pair is explicitly asserted, it is ignored/skipped over when moving
     * up.
     */
    parent(type) {
        this._index--;
        if ((0, yaml_1.isPair)(this.get())) {
            if (!type || !(type === yaml_1.Pair.prototype.constructor)) {
                this._index--;
            }
        }
        if (type) {
            if (!(this.get() instanceof type)) {
                this._index = Number.MIN_SAFE_INTEGER;
            }
        }
        return this;
    }
    /**
     * Move up the path, asserting that the current node was a key of a mapping
     * pair. The builder skips over the Pair to the parent YAMLMap.
     */
    parentOfKey() {
        const node = this.get();
        this.parent(yaml_1.Pair);
        const pairNode = this.get();
        if ((0, yaml_1.isPair)(pairNode) && pairNode.key === node) {
            this.parent(yaml_1.YAMLMap);
        }
        else {
            this._index = Number.MIN_SAFE_INTEGER;
        }
        return this;
    }
    /**
     * Get node up to which the assertions have led.
     */
    get() {
        return this._path[this._index] || null;
    }
    /**
     * Get the key of the Pair one level down the path.
     *
     * The key is returned only if it indeed is a string Scalar.
     */
    // The `this` argument is for generics restriction of this method.
    getStringKey() {
        const node = this._path[this._index + 1];
        if (node &&
            (0, yaml_1.isPair)(node) &&
            (0, yaml_1.isScalar)(node.key) &&
            typeof node.key.value === "string") {
            return node.key.value;
        }
        return null;
    }
    /**
     * Get the value of the Pair one level down the path.
     */
    // The `this` argument is for generics restriction of this method.
    getValue() {
        const node = this._path[this._index + 1];
        if ((0, yaml_1.isPair)(node)) {
            return node.value;
        }
        return null;
    }
    /**
     * Get the path to which the assertions have led.
     *
     * The path will be a subpath of the original path.
     */
    getPath() {
        if (this._index < 0)
            return null;
        const path = this._path.slice(0, this._index + 1);
        return path;
    }
    /**
     * Get the path to the key of the Pair one level down the path to which the
     * assertions have led.
     *
     * The path will be a subpath of the original path.
     */
    // The `this` argument is for generics restriction of this method.
    getKeyPath() {
        if (this._index < 0)
            return null;
        const path = this._path.slice(0, this._index + 1);
        const node = this._path[this._index + 1];
        if ((0, yaml_1.isPair)(node)) {
            path.push(node);
            path.push(node.key);
            return path;
        }
        return null;
    }
}
exports.AncestryBuilder = AncestryBuilder;
function getPathAt(document, position, docs, inclusive = false) {
    const offset = document.offsetAt(position);
    const doc = _.find(docs, (d) => contains(d.contents, offset, inclusive));
    if (doc && doc.contents) {
        return getPathAtOffset([doc.contents], offset, inclusive, doc);
    }
    return null;
}
exports.getPathAt = getPathAt;
function contains(node, offset, inclusive) {
    const range = getOrigRange(node);
    return !!(range &&
        range[0] <= offset &&
        (range[1] > offset || (inclusive && range[1] >= offset)));
}
exports.contains = contains;
function getPathAtOffset(path, offset, inclusive, doc) {
    if (path) {
        const currentNode = path[path.length - 1];
        if ((0, yaml_1.isMap)(currentNode)) {
            let pair = _.find(currentNode.items, (p) => contains(p.key, offset, inclusive));
            if (pair) {
                return getPathAtOffset(path.concat(pair, pair.key), offset, inclusive, doc);
            }
            pair = _.find(currentNode.items, (p) => contains(p.value, offset, inclusive));
            if (pair) {
                return getPathAtOffset(path.concat(pair, pair.value), offset, inclusive, doc);
            }
            pair = _.find(currentNode.items, (p) => {
                const inBetweenNode = doc.createNode(null);
                const start = getOrigRange(p.key)?.[1];
                const end = getOrigRange(p.value)?.[0];
                if (start && end) {
                    inBetweenNode.range = [start, end - 1, end];
                    return contains(inBetweenNode, offset, inclusive);
                }
                else
                    return false;
            });
            if (pair) {
                return path.concat(pair, doc.createNode(null));
            }
        }
        else if ((0, yaml_1.isSeq)(currentNode)) {
            const item = _.find(currentNode.items, (n) => contains(n, offset, inclusive));
            if (item) {
                return getPathAtOffset(path.concat(item), offset, inclusive, doc);
            }
        }
        else if (contains(currentNode, offset, inclusive)) {
            return path;
        }
        return path.concat(doc.createNode(null)); // empty node as indentation marker
    }
    return null;
}
exports.getPathAtOffset = getPathAtOffset;
exports.tasksKey = /^(tasks|pre_tasks|post_tasks|block|rescue|always|handlers)$/;
/**
 * Determines whether the path points at a parameter key of an Ansible task.
 */
function isTaskParam(path) {
    const taskListPath = new AncestryBuilder(path)
        .parentOfKey()
        .parent(yaml_1.YAMLSeq)
        .getPath();
    if (taskListPath) {
        // basic shape of the task list has been found
        if (isPlayParam(path) || isBlockParam(path) || isRoleParam(path))
            return false;
        if (taskListPath.length === 1) {
            // case when the task list is at the top level of the document
            return true;
        }
        const taskListKey = new AncestryBuilder(taskListPath)
            .parent(yaml_1.YAMLMap)
            .getStringKey();
        if (taskListKey && exports.tasksKey.test(taskListKey)) {
            // case when a task list is defined explicitly by a keyword
            return true;
        }
    }
    return false;
}
exports.isTaskParam = isTaskParam;
/**
 * Tries to find the list of collections declared at the Ansible play/block/task level.
 */
function getDeclaredCollections(modulePath) {
    const declaredCollections = [];
    const taskParamsNode = new AncestryBuilder(modulePath).parent(yaml_1.YAMLMap).get();
    declaredCollections.push(...getDeclaredCollectionsForMap(taskParamsNode));
    let path = new AncestryBuilder(modulePath)
        .parent(yaml_1.YAMLMap)
        .getPath();
    while (true) {
        // traverse the YAML up through the Ansible blocks
        const builder = new AncestryBuilder(path).parent(yaml_1.YAMLSeq).parent(yaml_1.YAMLMap);
        const key = builder.getStringKey();
        if (key && /^block|rescue|always$/.test(key)) {
            declaredCollections.push(...getDeclaredCollectionsForMap(builder.get()));
            path = builder.getPath();
        }
        else {
            break;
        }
    }
    // now we should be at the tasks/pre_tasks/post_tasks level
    const playParamsNode = new AncestryBuilder(path)
        .parent(yaml_1.YAMLSeq)
        .parent(yaml_1.YAMLMap)
        .get();
    declaredCollections.push(...getDeclaredCollectionsForMap(playParamsNode));
    return [...new Set(declaredCollections)]; // deduplicate
}
exports.getDeclaredCollections = getDeclaredCollections;
function getDeclaredCollectionsForMap(playNode) {
    const declaredCollections = [];
    const collectionsPair = _.find(playNode?.items, (pair) => (0, yaml_1.isScalar)(pair.key) && pair.key.value === "collections");
    if (collectionsPair) {
        // we've found the collections declaration
        const collectionsNode = collectionsPair.value;
        if ((0, yaml_1.isSeq)(collectionsNode)) {
            for (const collectionNode of collectionsNode.items) {
                if ((0, yaml_1.isScalar)(collectionNode)) {
                    declaredCollections.push(String(collectionNode.value));
                }
            }
        }
    }
    return declaredCollections;
}
/**
 * Heuristically determines whether the path points at an Ansible play. The
 * `fileUri` helps guessing in case the YAML tree doesn't give any clues.
 *
 * Returns `undefined` if highly uncertain.
 */
function isPlayParam(path, fileUri) {
    const isAtRoot = new AncestryBuilder(path).parentOfKey().parent(yaml_1.YAMLSeq).getPath()
        ?.length === 1;
    if (isAtRoot) {
        const mapNode = new AncestryBuilder(path).parentOfKey().get();
        const providedKeys = getYamlMapKeys(mapNode);
        const containsPlayKeyword = providedKeys.some((p) => ansible_1.playExclusiveKeywords.has(p));
        if (containsPlayKeyword) {
            return true;
        }
        if (fileUri) {
            const isInRole = /\/roles\/[^/]+\/tasks\//.test(fileUri);
            if (isInRole) {
                return false;
            }
        }
    }
    else {
        return false;
    }
}
exports.isPlayParam = isPlayParam;
/**
 * Determines whether the path points at one of Ansible block parameter keys.
 */
function isBlockParam(path) {
    const builder = new AncestryBuilder(path).parentOfKey();
    const mapNode = builder.get();
    // the block must have a list as parent
    const isInYAMLSeq = !!builder.parent(yaml_1.YAMLSeq).get();
    if (mapNode && isInYAMLSeq) {
        const providedKeys = getYamlMapKeys(mapNode);
        return providedKeys.includes("block");
    }
    return false;
}
exports.isBlockParam = isBlockParam;
/**
 * Determines whether the path points at one of Ansible role parameter keys.
 */
function isRoleParam(path) {
    const rolesKey = new AncestryBuilder(path)
        .parentOfKey()
        .parent(yaml_1.YAMLSeq)
        .parent(yaml_1.YAMLMap)
        .getStringKey();
    return rolesKey === "roles";
}
exports.isRoleParam = isRoleParam;
/**
 * If the path points at a parameter or sub-parameter provided for a module, it
 * will return the list of all possible options or sub-options at that
 * level/indentation.
 */
async function getPossibleOptionsForPath(path, document, docsLibrary) {
    const [taskParamPath, suboptionTrace] = getTaskParamPathWithTrace(path);
    if (!taskParamPath)
        return null;
    const optionTraceElement = suboptionTrace.pop();
    if (!optionTraceElement || optionTraceElement[1] !== "dict") {
        // that element must always be a `dict`
        // (unlike for sub-options, which can also be a 'list')
        return null;
    }
    // The module name is a key of the task parameters map
    const taskParamNode = taskParamPath[taskParamPath.length - 1];
    if (!(0, yaml_1.isScalar)(taskParamNode))
        return null;
    let module;
    // Module options can either be directly under module or in 'args'
    if (taskParamNode.value === "args") {
        module = await findProvidedModule(taskParamPath, document, docsLibrary);
    }
    else {
        [module] = await docsLibrary.findModule(taskParamNode.value, taskParamPath, document.uri);
    }
    if (!module || !module.documentation)
        return null;
    let options = module.documentation.options;
    suboptionTrace.reverse(); // now going down the path
    for (const [optionName, optionType] of suboptionTrace) {
        const option = options.get(optionName);
        if (optionName && option?.type === optionType && option.suboptions) {
            options = option.suboptions;
        }
        else {
            return null; // suboption structure mismatch
        }
    }
    return options;
}
exports.getPossibleOptionsForPath = getPossibleOptionsForPath;
/**
 * For a given path, it searches up that path until a path to the task parameter
 * (typically a module name) is found. The trace of keys with indication whether
 * the values hold a 'list' or a 'dict' is preserved along the way and returned
 * alongside.
 */
function getTaskParamPathWithTrace(path) {
    const trace = [];
    while (!isTaskParam(path)) {
        let parentKeyPath = new AncestryBuilder(path)
            .parentOfKey()
            .parent(yaml_1.YAMLMap)
            .getKeyPath();
        if (parentKeyPath) {
            const parentKeyNode = parentKeyPath[parentKeyPath.length - 1];
            if ((0, yaml_1.isScalar)(parentKeyNode) && typeof parentKeyNode.value === "string") {
                trace.push([parentKeyNode.value, "dict"]);
                path = parentKeyPath;
                continue;
            }
        }
        parentKeyPath = new AncestryBuilder(path)
            .parentOfKey()
            .parent(yaml_1.YAMLSeq)
            .parent(yaml_1.YAMLMap)
            .getKeyPath();
        if (parentKeyPath) {
            const parentKeyNode = parentKeyPath[parentKeyPath.length - 1];
            if ((0, yaml_1.isScalar)(parentKeyNode) && typeof parentKeyNode.value === "string") {
                trace.push([parentKeyNode.value, "list"]);
                path = parentKeyPath;
                continue;
            }
        }
        return [[], []]; // return empty if no structural match found
    }
    return [path, trace];
}
exports.getTaskParamPathWithTrace = getTaskParamPathWithTrace;
/**
 * For a given Ansible task parameter path, find the module if it has been
 * provided for the task.
 */
async function findProvidedModule(taskParamPath, document, docsLibrary) {
    const taskParameterMap = new AncestryBuilder(taskParamPath)
        .parent(yaml_1.YAMLMap)
        .get();
    if (taskParameterMap) {
        // find task parameters that have been provided by the user
        const providedParameters = new Set(getYamlMapKeys(taskParameterMap));
        // should usually be 0 or 1
        const providedModuleNames = [...providedParameters].filter((x) => !x || !(0, ansible_1.isTaskKeyword)(x));
        // find the module if it has been provided
        for (const m of providedModuleNames) {
            const [module] = await docsLibrary.findModule(m, taskParamPath, document.uri);
            if (module) {
                return module;
            }
        }
    }
}
exports.findProvidedModule = findProvidedModule;
function getYamlMapKeys(mapNode) {
    return mapNode.items
        .map((pair) => {
        if (pair.key && (0, yaml_1.isScalar)(pair.key)) {
            return String(pair.key.value);
        }
    })
        .filter((e) => !!e);
}
exports.getYamlMapKeys = getYamlMapKeys;
function getOrigRange(node) {
    if (node?.range &&
        node.range[0] !== undefined &&
        node.range[1] !== undefined) {
        return [node.range[0], node.range[1]];
    }
    return undefined;
}
exports.getOrigRange = getOrigRange;
/** Parsing with the YAML library tailored to the needs of this extension */
function parseAllDocuments(str, options) {
    if (!str) {
        return [];
    }
    const doc = (0, yaml_1.parseDocument)(str, Object.assign({ keepSourceTokens: true, options }));
    return [doc];
}
exports.parseAllDocuments = parseAllDocuments;
/**
 * For a given yaml file that is recognized as Ansible file, the function
 * checks whether the file is a playbook or not
 * @param textDocument - the text document to check
 */
function isPlaybook(textDocument) {
    // Check for empty file
    if (textDocument.getText().trim().length === 0) {
        return false;
    }
    const yamlDocs = parseAllDocuments(textDocument.getText());
    const path = getPathAt(textDocument, { line: 1, character: 1 }, yamlDocs);
    //   Check if keys are present or not
    if (!path) {
        return false;
    }
    //   A playbook is always YAML sequence
    if (!(0, yaml_1.isSeq)(path[0])) {
        return false;
    }
    const playbookKeysSet = new Set();
    const playbookJSON = path[0].toJSON();
    playbookJSON.forEach((item) => {
        if (item) {
            Object.keys(item).forEach((item) => playbookKeysSet.add(item));
        }
    });
    const playbookKeys = [...playbookKeysSet];
    const playKeywordsList = [...ansible_2.playKeywords.keys()];
    const taskKeywordsList = [...ansible_2.taskKeywords.keys()];
    //   Filters out all play keywords that are task keywords
    const filteredList = playKeywordsList.filter((value) => !taskKeywordsList.includes(value));
    //   Check if any top-level key of the ansible file is a part of filtered list
    //    If it is: The file is a playbook
    //    Else: The file is not a playbook
    const isPlaybookValue = playbookKeys.some((r) => filteredList.includes(r));
    return isPlaybookValue;
}
exports.isPlaybook = isPlaybook;
/**
 * A function to check if the cursor is present inside valid jinja inline brackets in a yaml file
 * @param document - text document on which the function is to be checked
 * @param position - current cursor position
 * @param path - array of nodes leading to that position
 * @returns boolean true if the cursor is inside valid jinja inline brackets, else false
 */
function isCursorInsideJinjaBrackets(document, position, path) {
    const node = path?.[path?.length - 1];
    let nodeObject;
    try {
        nodeObject = node.toJSON();
    }
    catch (error) {
        // return early if invalid yaml syntax
        return false;
    }
    if (nodeObject && !nodeObject.includes("{{ ")) {
        // this handles the case that if a value starts with {{ foo }}, the whole expression must be quoted
        // to create a valid syntax
        // refer: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html#when-to-quote-variables-a-yaml-gotcha
        return false;
    }
    // get text from the beginning of current line till the cursor
    const lineText = document.getText(vscode_languageserver_1.Range.create(position.line, 0, position.line, position.character));
    const jinjaInlineBracketStartIndex = lineText.lastIndexOf("{{ ");
    const lineAfterCursor = document.getText(vscode_languageserver_1.Range.create(position, document.positionAt(document.offsetAt(position) + lineText.length)));
    // this is a safety check in case of multiple jinja inline brackets in a single line
    let jinjaInlineBracketEndIndex = lineAfterCursor.indexOf(" }}");
    if (lineAfterCursor.indexOf("{{ ") !== -1 &&
        lineAfterCursor.indexOf("{{ ") < jinjaInlineBracketEndIndex) {
        jinjaInlineBracketEndIndex = -1;
    }
    if (jinjaInlineBracketStartIndex > -1 &&
        jinjaInlineBracketEndIndex > -1 &&
        position.character > jinjaInlineBracketStartIndex &&
        position.character <=
            jinjaInlineBracketEndIndex +
                jinjaInlineBracketStartIndex +
                lineText.length) {
        return true;
    }
    return false;
}
exports.isCursorInsideJinjaBrackets = isCursorInsideJinjaBrackets;
