"use strict";
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Red Hat, Inc. All rights reserved.
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.YAMLSchemaService = exports.MODIFICATION_ACTIONS = void 0;
const jsonSchemaService_1 = require("vscode-json-languageservice/lib/umd/services/jsonSchemaService");
const jsonSchema_1 = require("../jsonSchema");
const yamlLanguageService_1 = require("../yamlLanguageService");
const l10n = require("@vscode/l10n");
const path = require("path");
const vscode_uri_1 = require("vscode-uri");
const modelineUtil_1 = require("./modelineUtil");
const ajv_1 = require("ajv");
const ajv_draft_04_1 = require("ajv-draft-04");
const _2019_1 = require("ajv/dist/2019");
const _2020_1 = require("ajv/dist/2020");
const Json = require("jsonc-parser");
const yaml_1 = require("yaml");
const schemaUrls_1 = require("../utils/schemaUrls");
const k8sSchemaUtil_1 = require("./k8sSchemaUtil");
const ajv4 = new ajv_draft_04_1.default({ allErrors: true });
const ajv7 = new ajv_1.default({ allErrors: true });
const ajv2019 = new _2019_1.default({ allErrors: true });
const ajv2020 = new _2020_1.default({ allErrors: true });
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsonSchema04 = require('ajv-draft-04/dist/refs/json-schema-draft-04.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsonSchema07 = require('ajv/dist/refs/json-schema-draft-07.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsonSchema2019 = require('ajv/dist/refs/json-schema-2019-09/schema.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsonSchema2020 = require('ajv/dist/refs/json-schema-2020-12/schema.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ajvLocalizers = require('ajv-i18n');
const schema04Validator = ajv4.compile(jsonSchema04);
const schema07Validator = ajv7.compile(jsonSchema07);
const schema2019Validator = ajv2019.compile(jsonSchema2019);
const schema2020Validator = ajv2020.compile(jsonSchema2020);
const schemaDialectCache = new Map();
const schemaDialectInFlight = new Map();
const AJV_LOCALE_ALIASES = new Map([
    ['zh-cn', 'zh'],
    ['zh-tw', 'zh-TW'],
]);
// metadata/keywords that don't add constraints and thus don't count as $ref siblings
const REF_SIBLING_NONCONSTRAINT_KEYS = new Set([
    '$ref',
    '_$ref',
    '$schema',
    '$id',
    'id',
    '_baseUri',
    '_sourceUri',
    '_dialect',
    '$anchor',
    '$dynamicAnchor',
    '$dynamicRef',
    '$recursiveAnchor',
    '$recursiveRef',
    'definitions',
    '$defs',
    '$comment',
    'title',
    'description',
    'markdownDescription',
    '$vocabulary',
    'examples',
    'default',
    'url',
    'closestTitle',
    'unevaluatedProperties',
    'unevaluatedItems',
]);
var MODIFICATION_ACTIONS;
(function (MODIFICATION_ACTIONS) {
    MODIFICATION_ACTIONS[MODIFICATION_ACTIONS["delete"] = 0] = "delete";
    MODIFICATION_ACTIONS[MODIFICATION_ACTIONS["add"] = 1] = "add";
    MODIFICATION_ACTIONS[MODIFICATION_ACTIONS["deleteAll"] = 2] = "deleteAll";
})(MODIFICATION_ACTIONS = exports.MODIFICATION_ACTIONS || (exports.MODIFICATION_ACTIONS = {}));
class YAMLSchemaService extends jsonSchemaService_1.JSONSchemaService {
    constructor(requestService, contextService, promiseConstructor, yamlSettings) {
        super(requestService, contextService, promiseConstructor);
        this.schemaUriToNameAndDescription = new Map();
        this.customSchemaProvider = undefined;
        this.requestService = requestService;
        this.schemaPriorityMapping = new Map();
        this.yamlSettings = yamlSettings;
    }
    registerCustomSchemaProvider(customSchemaProvider) {
        this.customSchemaProvider = customSchemaProvider;
    }
    getAllSchemas() {
        const result = [];
        const schemaUris = new Set();
        for (const filePattern of this.filePatternAssociations) {
            const schemaUri = filePattern.uris[0];
            if (schemaUris.has(schemaUri)) {
                continue;
            }
            schemaUris.add(schemaUri);
            const schemaHandle = {
                uri: schemaUri,
                fromStore: false,
                usedForCurrentFile: false,
            };
            if (this.schemaUriToNameAndDescription.has(schemaUri)) {
                const { name, description, versions } = this.schemaUriToNameAndDescription.get(schemaUri);
                schemaHandle.name = name;
                schemaHandle.description = description;
                schemaHandle.fromStore = true;
                schemaHandle.versions = versions;
            }
            result.push(schemaHandle);
        }
        return result;
    }
    collectSchemaNodes(push, ...values) {
        const collect = (value) => {
            if (!value || typeof value !== 'object')
                return;
            if (Array.isArray(value)) {
                for (const entry of value) {
                    collect(entry);
                }
                return;
            }
            push(value);
        };
        for (const value of values) {
            collect(value);
        }
    }
    schemaMapValues(map) {
        if (!map || typeof map !== 'object')
            return undefined;
        return Object.values(map);
    }
    async resolveSchemaContent(schemaToResolve, schemaURL, dependencies) {
        const resolveErrors = schemaToResolve.errors.slice(0);
        const loc = toDisplayString(schemaURL);
        const raw = schemaToResolve.schema;
        if (raw === null || Array.isArray(raw) || (typeof raw !== 'object' && typeof raw !== 'boolean')) {
            const got = raw === null ? 'null' : Array.isArray(raw) ? 'array' : typeof raw;
            resolveErrors.push(l10n.t("Schema '{0}' is not valid: {1}", loc, `expected a JSON Schema object or boolean, got "${got}".`));
            return new jsonSchemaService_1.ResolvedSchema({}, resolveErrors);
        }
        const _setSourceUri = (node, sourceUri) => {
            if (!node || typeof node !== 'object' || !sourceUri)
                return;
            node._sourceUri = sourceUri;
        };
        const _cloneSchema = (value, seen, stopCondition) => {
            // primitives and null
            if (value === null || typeof value !== 'object')
                return value;
            if (stopCondition) {
                const replacement = stopCondition(value, seen.size);
                if (replacement !== undefined)
                    return replacement;
            }
            // already cloned
            if (seen.has(value))
                return seen.get(value);
            // clone arrays
            if (Array.isArray(value)) {
                const arr = [];
                seen.set(value, arr);
                for (const item of value) {
                    arr.push(_cloneSchema(item, seen, stopCondition));
                }
                return arr;
            }
            // clone objects
            const result = {};
            seen.set(value, result);
            for (const prop in value) {
                result[prop] = _cloneSchema(value[prop], seen, stopCondition);
            }
            _setSourceUri(result, value._sourceUri);
            return result;
        };
        /**
         * ----------------------------
         * Meta-validate a schema node against its dialect's meta-schema
         * ----------------------------
         */
        const _loadSchema = this.loadSchema.bind(this);
        const ajvErrorLocale = this.yamlSettings?.locale;
        async function _metaValidateSchemaNode(node, hasNestedSchema) {
            if (!node || typeof node !== 'object')
                return;
            const dialect = await pickSchemaDialect(node.$schema, _loadSchema);
            dialect && (node._dialect = dialect);
            const validator = pickMetaValidator(dialect);
            if (!validator)
                return;
            let toValidate = node;
            if (hasNestedSchema) {
                // clone for meta-validation: stop at dialect boundaries abd replace with {}
                const stopAtDialectBoundary = (val, seenSize) => {
                    if (seenSize !== 0 && val && typeof val === 'object' && val.$schema)
                        return {};
                    return undefined;
                };
                toValidate = _cloneSchema(node, new Map(), stopAtDialectBoundary);
            }
            if (!validator(toValidate)) {
                localizeAjvErrors(validator.errors, ajvErrorLocale);
                const errs = [];
                for (const err of validator.errors) {
                    errs.push(`${err.instancePath} : ${err.message}`);
                }
                resolveErrors.push(l10n.t("Schema '{0}' is not valid: {1}", loc, `\n${errs.join('\n')}`));
            }
        }
        const resourceIndexByUri = new Map();
        const _getResourceIndex = (resourceUri) => {
            let entry = resourceIndexByUri.get(resourceUri);
            if (!entry) {
                entry = { fragments: new Map() };
                resourceIndexByUri.set(resourceUri, entry);
            }
            return entry;
        };
        /**
         * Adds a resource's dynamic anchors to the inherited scope from parent resources
         *
         * Draft 2020-12: For $dynamicRef resolution, when schema A references schema B,
         * B's dynamic anchors are added to A's scope. This builds a chain where $dynamicRef
         * looks for the outermost (first) matching anchor.
         */
        const _addResourceDynamicAnchors = (scope, resourceUri) => {
            const entry = resourceIndexByUri.get(resourceUri);
            if (!entry || entry.fragments.size === 0)
                return scope;
            let result = scope;
            for (const [name, entryItem] of entry.fragments) {
                if (!entryItem.dynamic)
                    continue;
                const current = result?.get(name) ?? [];
                if (current.some((existing) => existing._baseUri === resourceUri))
                    continue;
                // clone map on first modification
                if (result === scope)
                    result = scope ? new Map(scope) : new Map();
                result.set(name, current.concat(entryItem.node));
            }
            return result;
        };
        // resolve relative URI against base URI
        // e.g. resolve "./foo.json" against "http://example.com/bar.json" => "http://example.com/foo.json"
        const _resolveAgainstBase = (baseUri, ref) => {
            if (this.contextService)
                return this.contextService.resolveRelativePath(ref, baseUri);
            return this.normalizeId(ref);
        };
        const _indexSchemaResources = async (root, initialBaseUri) => {
            const preOrderStack = [{ node: root, baseUri: initialBaseUri, sourceUri: initialBaseUri }];
            const postOrderStack = [];
            const childListByNode = new WeakMap();
            const seen = new Set();
            while (preOrderStack.length) {
                const current = preOrderStack.pop();
                if (!current)
                    continue;
                const node = current.node;
                if (!node || typeof node !== 'object' || seen.has(node))
                    continue;
                seen.add(node);
                let baseUri = current.baseUri;
                _setSourceUri(node, current.sourceUri);
                const id = node.$id || node.id;
                if (id) {
                    const resolvedBaseUri = _resolveAgainstBase(baseUri, id);
                    node._baseUri = resolvedBaseUri;
                    const hashIndex = resolvedBaseUri.indexOf('#');
                    if (hashIndex !== -1 && hashIndex < resolvedBaseUri.length - 1) {
                        // Draft-07 and earlier: $id with fragment defines a plain-name anchor scoped to the resolved base
                        const frag = resolvedBaseUri.slice(hashIndex + 1);
                        _getResourceIndex(baseUri).fragments.set(frag, { node });
                    }
                    else {
                        // $id without fragment creates a new embedded resource scope
                        baseUri = resolvedBaseUri;
                        const entry = _getResourceIndex(resolvedBaseUri);
                        if (!entry.root) {
                            entry.root = node;
                        }
                    }
                }
                // Draft 2019-09+: $anchor keyword
                if (node.$anchor) {
                    _getResourceIndex(baseUri).fragments.set(node.$anchor, { node });
                }
                // Draft 2020-12+: $dynamicAnchor keyword
                if (node.$dynamicAnchor) {
                    node._baseUri = baseUri;
                    _getResourceIndex(baseUri).fragments.set(node.$dynamicAnchor, { node, dynamic: true });
                }
                const children = [];
                childListByNode.set(node, children);
                // collect all child schemas
                this.collectSchemaNodes((entry) => {
                    children.push(entry);
                    preOrderStack.push({ node: entry, baseUri, sourceUri: current.sourceUri });
                }, node.not, node.if, node.then, node.else, node.contains, node.propertyNames, node.additionalProperties, node.items, node.additionalItems, node.prefixItems, this.schemaMapValues(node.properties), this.schemaMapValues(node.patternProperties), this.schemaMapValues(node.definitions), this.schemaMapValues(node.$defs), this.schemaMapValues(node.dependentSchemas), this.schemaMapValues(node.dependencies), node.allOf, node.anyOf, node.oneOf, node.schemaSequence);
                postOrderStack.push(node);
            }
            const hasNestedSchema = new WeakMap();
            while (postOrderStack.length) {
                const node = postOrderStack.pop();
                let hasNested = false;
                for (const child of childListByNode.get(node)) {
                    if (child.$schema || hasNestedSchema.get(child)) {
                        hasNested = true;
                        break;
                    }
                }
                hasNestedSchema.set(node, hasNested);
                if (node === root || node.$schema)
                    await _metaValidateSchemaNode(node, hasNested);
            }
        };
        let schema = raw;
        const schemaBaseURL = schemaToResolve.uri ?? schemaURL;
        await _indexSchemaResources(schema, schemaBaseURL);
        const _findSection = (schemaRoot, refPath, sourceURI) => {
            if (!refPath) {
                return schemaRoot;
            }
            // JSON pointer style
            if (refPath[0] === '/') {
                let current = schemaRoot;
                const parts = refPath.substr(1).split('/');
                for (const part of parts) {
                    // in JSON Pointer: ~ must be escaped as ~0, / must be escaped as ~1
                    current = current?.[part.replace(/~1/g, '/').replace(/~0/g, '~')];
                    if (current === null)
                        return undefined;
                }
                return current;
            }
            // plain-name fragment ($anchor or $id#fragment) -> lookup in collected fragments
            return _getResourceIndex(sourceURI).fragments.get(refPath).node;
        };
        const _merge = (target, sourceRoot, sourceURI, refPath, clone = false) => {
            const section = _findSection(sourceRoot, refPath, sourceURI);
            if (typeof section === 'boolean') {
                if (!section)
                    target.not = {};
                return;
            }
            if (typeof section === 'object' && section) {
                const source = clone ? _cloneSchema(section, new Map()) : section;
                for (const key in source) {
                    if (Object.prototype.hasOwnProperty.call(source, key) && !Object.prototype.hasOwnProperty.call(target, key)) {
                        target[key] = source[key];
                    }
                }
                _setSourceUri(target, source._sourceUri);
                return;
            }
            else {
                resolveErrors.push(l10n.t("$ref '{0}' in '{1}' cannot be resolved.", refPath, sourceURI));
            }
        };
        const _resolveRefUri = (parentSchemaURL, refUri) => {
            const resolvedAgainstParent = _resolveAgainstBase(parentSchemaURL, refUri);
            if (!refUri.startsWith('/'))
                return resolvedAgainstParent;
            const parentResource = resourceIndexByUri.get(parentSchemaURL)?.root;
            const parentResourceId = parentResource?.$id || parentResource?.id;
            if (!parentResourceId)
                return resolvedAgainstParent;
            const resolvedParentId = _resolveAgainstBase(parentSchemaURL, parentResourceId);
            if (!resolvedParentId.startsWith('http://') && !resolvedParentId.startsWith('https://'))
                return resolvedAgainstParent;
            return _resolveAgainstBase(resolvedParentId, refUri);
        };
        const _resolveLocalSiblingFromRemoteUri = (parentSchemaURL, resolvedRefUri) => {
            try {
                const parentUri = vscode_uri_1.URI.parse(parentSchemaURL);
                const targetUri = vscode_uri_1.URI.parse(resolvedRefUri);
                if (parentUri.scheme !== 'file')
                    return undefined;
                if (targetUri.scheme !== 'http' && targetUri.scheme !== 'https')
                    return undefined;
                const localFileName = path.posix.basename(targetUri.path);
                if (!localFileName)
                    return undefined;
                const localDir = path.posix.dirname(parentUri.path);
                const localPath = path.posix.join(localDir, localFileName);
                return parentUri.with({ path: localPath, query: targetUri.query, fragment: targetUri.fragment }).toString();
            }
            catch {
                return undefined;
            }
        };
        const resolveExternalLink = (node, uri, linkPath, parentSchemaURL, parentSchemaSourceUri, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, inheritedDynamicScope
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        ) => {
            const _attachResolvedSchema = (node, schemaRoot, schemaUri, linkPath, parentSchemaDependencies, resolveRefDependencies, resolutionStack, recursiveAnchorBase, inheritedDynamicScope
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            ) => {
                parentSchemaDependencies[schemaUri] = true;
                _merge(node, schemaRoot, schemaUri, linkPath, !!inheritedDynamicScope || !!recursiveAnchorBase);
                if (!recursiveAnchorBase || !node._baseUri)
                    node._baseUri = schemaUri;
                node.url = schemaUri;
                const nextStack = new Set(resolutionStack);
                nextStack.add(schemaUri);
                // eslint-disable-next-line @typescript-eslint/no-use-before-define
                return resolveRefs(node, schemaRoot, schemaUri, resolveRefDependencies, nextStack, recursiveAnchorBase, inheritedDynamicScope);
            };
            const _resolveByUri = (targetUris, index = 0) => {
                const targetUri = targetUris[index];
                const embeddedSchema = resourceIndexByUri.get(targetUri)?.root;
                if (embeddedSchema) {
                    return _attachResolvedSchema(node, embeddedSchema, targetUri, linkPath, parentSchemaDependencies, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, inheritedDynamicScope);
                }
                const referencedHandle = this.getOrAddSchemaHandle(targetUri);
                return referencedHandle.getUnresolvedSchema().then(async (unresolvedSchema) => {
                    if (unresolvedSchema.errors?.some((error) => error.toLowerCase().includes('unable to load schema from')) &&
                        index + 1 < targetUris.length) {
                        return _resolveByUri(targetUris, index + 1);
                    }
                    if (unresolvedSchema.errors.length) {
                        const loc = linkPath ? targetUri + '#' + linkPath : targetUri;
                        resolveErrors.push(l10n.t("Problems loading reference '{0}': {1}", loc, unresolvedSchema.errors[0]));
                    }
                    // index resources for the newly loaded schema
                    await _indexSchemaResources(unresolvedSchema.schema, targetUri);
                    return _attachResolvedSchema(node, unresolvedSchema.schema, targetUri, linkPath, parentSchemaDependencies, referencedHandle.dependencies, resolutionStack, recursiveAnchorBase, inheritedDynamicScope);
                });
            };
            const resolvedUri = _resolveRefUri(parentSchemaURL, uri);
            const embeddedTarget = resourceIndexByUri.get(resolvedUri)?.root;
            const localSiblingUri = embeddedTarget ? undefined : _resolveLocalSiblingFromRemoteUri(parentSchemaSourceUri, resolvedUri);
            const targetUris = localSiblingUri && localSiblingUri !== resolvedUri ? [localSiblingUri, resolvedUri] : [resolvedUri];
            return _resolveByUri(targetUris);
        };
        const resolveRefs = async (node, parentSchema, parentSchemaURL, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, inheritedDynamicScope
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        ) => {
            if (!node || typeof node !== 'object') {
                return null;
            }
            const toWalk = [
                {
                    node,
                    baseUri: parentSchemaURL,
                    sourceUri: node._sourceUri ?? parentSchemaURL,
                    recursiveAnchorBase,
                    inheritedDynamicScope,
                },
            ];
            const seen = new WeakSet(); // prevents re-walking the same schema object graph
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            const openPromises = [];
            // handle $ref with siblings based on dialect
            const _handleRef = (next, nodeBaseUri, nodeSourceUri, nodeDialect, recursiveAnchorBase, inheritedDynamicScope, siblingRefCycleKeys) => {
                const currentDynamicScope = _addResourceDynamicAnchors(inheritedDynamicScope, nodeBaseUri);
                this.collectSchemaNodes((entry) => toWalk.push({
                    node: entry,
                    baseUri: nodeBaseUri,
                    sourceUri: nodeSourceUri,
                    recursiveAnchorBase,
                    inheritedDynamicScope: currentDynamicScope,
                }), this.schemaMapValues(next.definitions || next.$defs));
                // checks if a node with $ref has other constraint keywords
                const _hasRefSiblings = (node) => {
                    for (const k of Object.keys(node)) {
                        if (REF_SIBLING_NONCONSTRAINT_KEYS.has(k))
                            continue;
                        return true;
                    }
                    return false;
                };
                /**
                 * For Draft-2019+:
                 *   { $ref: "...", <siblings...> }
                 * becomes
                 *   { allOf: [ { $ref: "..." }, <siblings...> ] }
                 */
                const _rewriteRefWithSiblingsToAllOf = (node) => {
                    const siblings = {};
                    for (const k of Object.keys(node)) {
                        if (!REF_SIBLING_NONCONSTRAINT_KEYS.has(k)) {
                            siblings[k] = node[k];
                            delete node[k];
                        }
                    }
                    const refValue = node.$dynamicRef ?? node.$recursiveRef ?? node.$ref;
                    if (typeof refValue !== 'string')
                        return;
                    node.allOf = [
                        { [node.$dynamicRef ? '$dynamicRef' : node.$recursiveRef ? '$recursiveRef' : '$ref']: refValue },
                        siblings,
                    ];
                    delete node.$dynamicRef;
                    delete node.$recursiveRef;
                    delete node.$ref;
                };
                const _stripRefSiblings = (node) => {
                    for (const k of Object.keys(node)) {
                        if (!REF_SIBLING_NONCONSTRAINT_KEYS.has(k))
                            delete node[k];
                    }
                };
                const seenRefs = new Set();
                const _mergeIfResourceAlreadyInResolutionStack = (ref, resolvedResource, frag) => {
                    if (!resolutionStack.has(resolvedResource))
                        return false;
                    if (!seenRefs.has(ref)) {
                        const source = resourceIndexByUri.get(resolvedResource)?.root;
                        if (source && typeof source === 'object') {
                            _merge(next, source, resolvedResource, frag, !!recursiveAnchorBase);
                        }
                        seenRefs.add(ref);
                    }
                    return true;
                };
                while (next.$dynamicRef || next.$recursiveRef || next.$ref) {
                    const isDynamicRef = typeof next.$dynamicRef === 'string';
                    const isRecursiveRef = !isDynamicRef && typeof next.$recursiveRef === 'string';
                    const rawRef = next.$dynamicRef ?? next.$recursiveRef ?? next.$ref;
                    if (typeof rawRef !== 'string')
                        break;
                    next._$ref = rawRef;
                    // parse ref into base URI and fragment
                    const ref = decodeURIComponent(rawRef);
                    const segments = ref.split('#', 2);
                    const baseUri = segments[0];
                    const frag = segments.length > 1 ? segments[1] : '';
                    const resolvedRefKey = `${baseUri ? _resolveAgainstBase(nodeBaseUri, baseUri) : nodeBaseUri}#${frag}`;
                    if (_hasRefSiblings(next)) {
                        // Draft-07 and earlier: ignore siblings
                        if (nodeDialect === jsonSchema_1.SchemaDialect.draft04 || nodeDialect === jsonSchema_1.SchemaDialect.draft07) {
                            _stripRefSiblings(next);
                        }
                        else {
                            if (siblingRefCycleKeys?.has(resolvedRefKey))
                                break;
                            // Draft-2019+: support sibling keywords
                            _rewriteRefWithSiblingsToAllOf(next);
                            if (Array.isArray(next.allOf)) {
                                for (let i = 0; i < next.allOf.length; i++) {
                                    const entry = next.allOf[i];
                                    if (entry && typeof entry === 'object') {
                                        let nextSiblingRefCycleKeys;
                                        if (i === 0) {
                                            nextSiblingRefCycleKeys = new Set(siblingRefCycleKeys);
                                            nextSiblingRefCycleKeys.add(resolvedRefKey);
                                        }
                                        toWalk.push({
                                            node: entry,
                                            baseUri: nodeBaseUri,
                                            sourceUri: nodeSourceUri,
                                            recursiveAnchorBase,
                                            inheritedDynamicScope: currentDynamicScope,
                                            siblingRefCycleKeys: nextSiblingRefCycleKeys,
                                        });
                                    }
                                }
                            }
                            return;
                        }
                    }
                    delete next.$dynamicRef;
                    delete next.$recursiveRef;
                    delete next.$ref;
                    // Draft-2019+: $recursiveRef
                    if (isRecursiveRef && (ref === '#' || ref === '')) {
                        const targetRoot = resourceIndexByUri.get(nodeBaseUri)?.root;
                        const recursiveBase = targetRoot?.$recursiveAnchor && recursiveAnchorBase ? recursiveAnchorBase : nodeBaseUri;
                        if (recursiveBase.length > 0) {
                            if (resolutionStack?.has(recursiveBase) || recursiveBase === nodeBaseUri) {
                                const sourceRoot = resourceIndexByUri.get(recursiveBase)?.root ?? parentSchema;
                                if (!seenRefs.has(ref)) {
                                    _merge(next, sourceRoot, recursiveBase, '', false);
                                    seenRefs.add(ref);
                                }
                                continue;
                            }
                            openPromises.push(resolveExternalLink(next, recursiveBase, '', nodeBaseUri, nodeSourceUri, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, currentDynamicScope));
                            return;
                        }
                        continue;
                    }
                    // Draft-2020+: $dynamicRef
                    else if (isDynamicRef) {
                        const targetResource = baseUri.length > 0 ? _resolveAgainstBase(nodeBaseUri, baseUri) : nodeBaseUri;
                        const targetFragments = resourceIndexByUri.get(targetResource)?.fragments;
                        const targetHasDynamicAnchor = frag.length > 0 && targetFragments?.get(frag)?.dynamic;
                        const dynamicTarget = targetHasDynamicAnchor ? currentDynamicScope?.get(frag)?.[0] : undefined;
                        const resolveResource = dynamicTarget ? dynamicTarget._baseUri : targetResource;
                        if (dynamicTarget && (resolveResource === nodeBaseUri || resolutionStack.has(resolveResource))) {
                            if (!seenRefs.has(ref)) {
                                _merge(next, dynamicTarget, resolveResource, '', false);
                                seenRefs.add(ref);
                            }
                            continue;
                        }
                        if (baseUri.length > 0 || targetHasDynamicAnchor) {
                            if (_mergeIfResourceAlreadyInResolutionStack(ref, resolveResource, frag))
                                continue;
                            openPromises.push(resolveExternalLink(next, resolveResource, frag, nodeBaseUri, nodeSourceUri, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, currentDynamicScope));
                            return;
                        }
                    }
                    // normal $ref with external baseUri
                    else if (baseUri.length > 0) {
                        const resolvedBaseUri = _resolveAgainstBase(nodeBaseUri, baseUri);
                        if (_mergeIfResourceAlreadyInResolutionStack(ref, resolvedBaseUri, frag))
                            continue;
                        // resolve relative to this node's base URL
                        openPromises.push(resolveExternalLink(next, baseUri, frag, nodeBaseUri, nodeSourceUri, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, currentDynamicScope));
                        return;
                    }
                    // local $ref or $dynamicRef
                    if (!seenRefs.has(ref)) {
                        _merge(next, parentSchema, nodeBaseUri, frag, isDynamicRef && !!currentDynamicScope);
                        seenRefs.add(ref);
                    }
                }
                // recursively process children
                this.collectSchemaNodes((entry) => toWalk.push({
                    node: entry,
                    baseUri: next._baseUri || nodeBaseUri,
                    sourceUri: next._sourceUri || nodeSourceUri,
                    dialect: nodeDialect,
                    recursiveAnchorBase,
                    inheritedDynamicScope: currentDynamicScope,
                }), next.not, next.if, next.then, next.else, next.contains, next.propertyNames, next.additionalProperties, next.items, next.additionalItems, next.prefixItems, this.schemaMapValues(next.properties), this.schemaMapValues(next.patternProperties), this.schemaMapValues(next.dependentSchemas), this.schemaMapValues(next.dependencies), next.allOf, next.anyOf, next.oneOf, next.schemaSequence);
            };
            // handle file path with fragments
            if (parentSchemaURL.indexOf('#') > 0) {
                const segments = parentSchemaURL.split('#', 2);
                if (segments[0].length > 0 && segments[1].length > 0) {
                    const newSchema = {};
                    await resolveExternalLink(newSchema, segments[0], segments[1], parentSchemaURL, schema._sourceUri ?? parentSchemaURL, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, inheritedDynamicScope);
                    for (const key in schema) {
                        if (key === 'required') {
                            continue;
                        }
                        if (Object.prototype.hasOwnProperty.call(schema, key) && !Object.prototype.hasOwnProperty.call(newSchema, key)) {
                            newSchema[key] = schema[key];
                        }
                    }
                    schema = newSchema;
                }
            }
            while (toWalk.length) {
                const item = toWalk.pop();
                const next = item.node;
                const nodeBaseUri = next._baseUri || item.baseUri;
                const nodeSourceUri = next._sourceUri || nodeBaseUri;
                const nodeDialect = next._dialect || item.dialect;
                const nodeRecursiveAnchorBase = item.recursiveAnchorBase ?? (next.$recursiveAnchor ? nodeBaseUri : undefined);
                if (seen.has(next))
                    continue;
                seen.add(next);
                _handleRef(next, nodeBaseUri, nodeSourceUri, nodeDialect, nodeRecursiveAnchorBase, item.inheritedDynamicScope, item.siblingRefCycleKeys);
            }
            return Promise.all(openPromises);
        };
        const resolutionStack = new Set(); // prevents $ref/$recursiveRef/$dynamicRef loops across schema URIs
        const rootResource = schema._baseUri || schemaURL;
        if (rootResource)
            resolutionStack.add(rootResource);
        await resolveRefs(schema, schema, schemaURL, dependencies, resolutionStack);
        return new jsonSchemaService_1.ResolvedSchema(schema, resolveErrors);
    }
    getSchemaForResource(resource, doc) {
        const resolveModelineSchema = () => {
            let schemaFromModeline = (0, modelineUtil_1.getSchemaFromModeline)(doc);
            if (schemaFromModeline !== undefined) {
                if (!schemaFromModeline.startsWith('file:') && !schemaFromModeline.startsWith('http')) {
                    // If path contains a fragment and it is left intact, "#" will be
                    // considered part of the filename and converted to "%23" by
                    // path.resolve() -> take it out and add back after path.resolve
                    let appendix = '';
                    if (schemaFromModeline.indexOf('#') > 0) {
                        const segments = schemaFromModeline.split('#', 2);
                        schemaFromModeline = segments[0];
                        appendix = segments[1];
                    }
                    if (!path.isAbsolute(schemaFromModeline)) {
                        const resUri = vscode_uri_1.URI.parse(resource);
                        schemaFromModeline = vscode_uri_1.URI.file(path.resolve(path.parse(resUri.fsPath).dir, schemaFromModeline)).toString();
                    }
                    else {
                        schemaFromModeline = vscode_uri_1.URI.file(schemaFromModeline).toString();
                    }
                    if (appendix.length > 0) {
                        schemaFromModeline += '#' + appendix;
                    }
                }
                return schemaFromModeline;
            }
        };
        const resolveSchemaForResource = (schemas) => {
            const schemaHandle = super.createCombinedSchema(resource, schemas);
            return schemaHandle.getResolvedSchema().then((schema) => {
                return this.finalizeResolvedSchema(schema, schemaHandle.url, doc, false);
            });
        };
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const resolveSchema = async () => {
            const seen = Object.create(null);
            const schemas = [];
            let k8sAllSchema = undefined;
            for (const entry of this.filePatternAssociations) {
                if (entry.matchesPattern(resource)) {
                    for (const schemaId of entry.getURIs()) {
                        if (!seen[schemaId]) {
                            if (this.yamlSettings?.kubernetesCRDStoreEnabled && schemaId === schemaUrls_1.KUBERNETES_SCHEMA_URL) {
                                if (!k8sAllSchema) {
                                    k8sAllSchema = await this.getResolvedSchema(schemaUrls_1.KUBERNETES_SCHEMA_URL);
                                }
                                const kubeSchema = (0, k8sSchemaUtil_1.autoDetectKubernetesSchema)(doc, k8sAllSchema, this.yamlSettings.kubernetesCRDStoreUrl ?? schemaUrls_1.CRD_CATALOG_URL);
                                if (kubeSchema) {
                                    schemas.push(kubeSchema);
                                    seen[schemaId] = true;
                                }
                                else {
                                    schemas.push(schemaId);
                                    seen[schemaId] = true;
                                }
                            }
                            else {
                                schemas.push(schemaId);
                                seen[schemaId] = true;
                            }
                        }
                    }
                }
            }
            if (schemas.length > 0) {
                // Join all schemas with the highest priority.
                const highestPrioSchemas = this.highestPrioritySchemas(schemas);
                return resolveSchemaForResource(highestPrioSchemas);
            }
            return Promise.resolve(null);
        };
        const modelineSchema = resolveModelineSchema();
        if (modelineSchema) {
            return resolveSchemaForResource([modelineSchema]);
        }
        if (this.customSchemaProvider) {
            return this.customSchemaProvider(resource)
                .then((schemaUri) => {
                if (Array.isArray(schemaUri)) {
                    if (schemaUri.length === 0) {
                        return resolveSchema();
                    }
                    return Promise.all(schemaUri.map((schemaUri) => {
                        return this.resolveCustomSchema(schemaUri, doc);
                    })).then((schemas) => {
                        return {
                            errors: [],
                            schema: {
                                allOf: schemas.map((schemaObj) => {
                                    return schemaObj.schema;
                                }),
                            },
                        };
                    }, () => {
                        return resolveSchema();
                    });
                }
                if (!schemaUri) {
                    return resolveSchema();
                }
                return this.resolveCustomSchema(schemaUri, doc);
            })
                .then((schema) => {
                return schema;
            }, () => {
                return resolveSchema();
            });
        }
        return resolveSchema();
    }
    finalizeResolvedSchema(schema, schemaUrl, doc, includeErrorsForSequence) {
        if (schema.schema && typeof schema.schema === 'object') {
            schema.schema.url = schemaUrl;
            if (schema.schema.schemaSequence && schema.schema.schemaSequence[doc.currentDocIndex]) {
                const selectedSchema = schema.schema.schemaSequence[doc.currentDocIndex];
                if (includeErrorsForSequence) {
                    return new jsonSchemaService_1.ResolvedSchema(selectedSchema, schema.errors);
                }
                return new jsonSchemaService_1.ResolvedSchema(selectedSchema);
            }
        }
        return schema;
    }
    // Set the priority of a schema in the schema service
    addSchemaPriority(uri, priority) {
        let currSchemaArray = this.schemaPriorityMapping.get(uri);
        if (currSchemaArray) {
            currSchemaArray = currSchemaArray.add(priority);
            this.schemaPriorityMapping.set(uri, currSchemaArray);
        }
        else {
            this.schemaPriorityMapping.set(uri, new Set().add(priority));
        }
    }
    /**
     * Search through all the schemas and find the ones with the highest priority
     */
    highestPrioritySchemas(schemas) {
        let highestPrio = 0;
        const priorityMapping = new Map();
        schemas.forEach((schema) => {
            // If the schema does not have a priority then give it a default one of [0]
            const priority = this.schemaPriorityMapping.get(schema) || [0];
            priority.forEach((prio) => {
                if (prio > highestPrio) {
                    highestPrio = prio;
                }
                // Build up a mapping of priority to schemas so that we can easily get the highest priority schemas easier
                let currPriorityArray = priorityMapping.get(prio);
                if (currPriorityArray) {
                    currPriorityArray = currPriorityArray.concat(schema);
                    priorityMapping.set(prio, currPriorityArray);
                }
                else {
                    priorityMapping.set(prio, [schema]);
                }
            });
        });
        return priorityMapping.get(highestPrio) || [];
    }
    async resolveCustomSchema(schemaUri, doc) {
        const unresolvedSchema = await this.loadSchema(schemaUri);
        const schema = await this.resolveSchemaContent(unresolvedSchema, schemaUri, []);
        return this.finalizeResolvedSchema(schema, schemaUri, doc, true);
    }
    /**
     * Save a schema with schema ID and schema content.
     * Overrides previous schemas set for that schema ID.
     */
    async saveSchema(schemaId, schemaContent) {
        const id = this.normalizeId(schemaId);
        this.getOrAddSchemaHandle(id, schemaContent);
        this.schemaPriorityMapping.set(id, new Set().add(yamlLanguageService_1.SchemaPriority.Settings));
        return Promise.resolve(undefined);
    }
    /**
     * Delete schemas on specific path
     */
    async deleteSchemas(deletions) {
        deletions.schemas.forEach((s) => {
            this.deleteSchema(s);
        });
        return Promise.resolve(undefined);
    }
    /**
     * Delete a schema with schema ID.
     */
    async deleteSchema(schemaId) {
        const id = this.normalizeId(schemaId);
        if (this.schemasById[id]) {
            delete this.schemasById[id];
        }
        this.schemaPriorityMapping.delete(id);
        return Promise.resolve(undefined);
    }
    /**
     * Add content to a specified schema at a specified path
     */
    async addContent(additions) {
        const schema = await this.getResolvedSchema(additions.schema);
        if (schema) {
            const resolvedSchemaLocation = this.resolveJSONSchemaToSection(schema.schema, additions.path);
            if (typeof resolvedSchemaLocation === 'object') {
                resolvedSchemaLocation[additions.key] = additions.content;
            }
            await this.saveSchema(additions.schema, schema.schema);
        }
    }
    /**
     * Delete content in a specified schema at a specified path
     */
    async deleteContent(deletions) {
        const schema = await this.getResolvedSchema(deletions.schema);
        if (schema) {
            const resolvedSchemaLocation = this.resolveJSONSchemaToSection(schema.schema, deletions.path);
            if (typeof resolvedSchemaLocation === 'object') {
                delete resolvedSchemaLocation[deletions.key];
            }
            await this.saveSchema(deletions.schema, schema.schema);
        }
    }
    /**
     * Take a JSON Schema and the path that you would like to get to
     * @returns the JSON Schema resolved at that specific path
     */
    resolveJSONSchemaToSection(schema, paths) {
        const splitPathway = paths.split('/');
        let resolvedSchemaLocation = schema;
        for (const path of splitPathway) {
            if (path === '') {
                continue;
            }
            this.resolveNext(resolvedSchemaLocation, path);
            resolvedSchemaLocation = resolvedSchemaLocation[path];
        }
        return resolvedSchemaLocation;
    }
    /**
     * Resolve the next Object if they have compatible types
     * @param object a location in the JSON Schema
     * @param token the next token that you want to search for
     */
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    resolveNext(object, token) {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        if (Array.isArray(object) && isNaN(token)) {
            throw new Error('Expected a number after the array object');
        }
        else if (typeof object === 'object' && typeof token !== 'string') {
            throw new Error('Expected a string after the object');
        }
    }
    /**
     * Everything below here is needed because we're importing from vscode-json-languageservice umd and we need
     * to provide a wrapper around the javascript methods we are calling since they have no type
     */
    normalizeId(id) {
        // The parent's `super.normalizeId(id)` isn't visible, so duplicated the code here
        try {
            return vscode_uri_1.URI.parse(id).toString();
        }
        catch (e) {
            return id;
        }
    }
    getOrAddSchemaHandle(id, unresolvedSchemaContent) {
        return super.getOrAddSchemaHandle(id, unresolvedSchemaContent);
    }
    loadSchema(schemaUri) {
        const requestService = this.requestService;
        return super.loadSchema(schemaUri).then(async (unresolvedJsonSchema) => {
            // If json-language-server failed to parse the schema, attempt to parse it as YAML instead.
            // If the YAML file starts with %YAML 1.x or contains a comment with a number the schema will
            // contain a number instead of being undefined, so we need to check for that too.
            if (unresolvedJsonSchema.errors &&
                (unresolvedJsonSchema.schema === undefined || typeof unresolvedJsonSchema.schema === 'number')) {
                return requestService(schemaUri).then((content) => {
                    if (!content) {
                        const errorMessage = l10n.t("Unable to load schema from '{0}': No content. {1}", toDisplayString(schemaUri), unresolvedJsonSchema.errors);
                        return new jsonSchemaService_1.UnresolvedSchema({}, [errorMessage]);
                    }
                    try {
                        const schemaContent = (0, yaml_1.parse)(content);
                        return new jsonSchemaService_1.UnresolvedSchema(schemaContent, []);
                    }
                    catch (yamlError) {
                        const errorMessage = l10n.t("Unable to parse content from '{0}': {1}.", toDisplayString(schemaUri), yamlError);
                        return new jsonSchemaService_1.UnresolvedSchema({}, [errorMessage]);
                    }
                }, 
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                (error) => {
                    let errorMessage = error.toString();
                    const errorSplit = error.toString().split('Error: ');
                    if (errorSplit.length > 1) {
                        // more concise error message, URL and context are attached by caller anyways
                        errorMessage = errorSplit[1];
                    }
                    return new jsonSchemaService_1.UnresolvedSchema({}, [errorMessage]);
                });
            }
            unresolvedJsonSchema.uri = schemaUri;
            if (this.schemaUriToNameAndDescription.has(schemaUri)) {
                const { name, description, versions } = this.schemaUriToNameAndDescription.get(schemaUri);
                unresolvedJsonSchema.schema.title = name ?? unresolvedJsonSchema.schema.title;
                unresolvedJsonSchema.schema.description = description ?? unresolvedJsonSchema.schema.description;
                unresolvedJsonSchema.schema.versions = versions ?? unresolvedJsonSchema.schema.versions;
            }
            else if (unresolvedJsonSchema.errors && unresolvedJsonSchema.errors.length > 0) {
                let errorMessage = unresolvedJsonSchema.errors[0];
                if (errorMessage.toLowerCase().indexOf('load') !== -1) {
                    errorMessage = l10n.t("Unable to load schema from '{0}': No content.", toDisplayString(schemaUri));
                }
                else if (errorMessage.toLowerCase().indexOf('parse') !== -1) {
                    const content = await requestService(schemaUri);
                    const jsonErrors = [];
                    const schemaContent = Json.parse(content, jsonErrors);
                    if (jsonErrors.length && schemaContent) {
                        const { offset } = jsonErrors[0];
                        const { line, column } = getLineAndColumnFromOffset(content, offset);
                        errorMessage = l10n.t("Unable to parse content from '{0}': Parse error at line: {1} column: {2}", toDisplayString(schemaUri), line, column);
                    }
                }
                return new jsonSchemaService_1.UnresolvedSchema({}, [errorMessage]);
            }
            return unresolvedJsonSchema;
        });
    }
    registerExternalSchema(uri, filePatterns, unresolvedSchema, name, description, versions) {
        if (name || description) {
            this.schemaUriToNameAndDescription.set(uri, { name, description, versions });
        }
        return super.registerExternalSchema(uri, filePatterns, unresolvedSchema);
    }
    clearExternalSchemas() {
        super.clearExternalSchemas();
    }
    setSchemaContributions(schemaContributions) {
        super.setSchemaContributions(schemaContributions);
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    getRegisteredSchemaIds(filter) {
        return super.getRegisteredSchemaIds(filter);
    }
    getResolvedSchema(schemaId) {
        return super.getResolvedSchema(schemaId);
    }
    onResourceChange(uri) {
        return super.onResourceChange(uri);
    }
}
exports.YAMLSchemaService = YAMLSchemaService;
function toDisplayString(url) {
    try {
        const uri = vscode_uri_1.URI.parse(url);
        if (uri.scheme === 'file') {
            return uri.fsPath;
        }
    }
    catch (e) {
        // ignore
    }
    return url;
}
function getLineAndColumnFromOffset(text, offset) {
    const lines = text.slice(0, offset).split(/\r?\n/);
    const line = lines.length; // 1-based line number
    const column = lines[lines.length - 1].length + 1; // 1-based column number
    return { line, column };
}
function normalizeSchemaUri(uri) {
    if (!uri)
        return '';
    let s;
    if (typeof uri === 'string') {
        s = uri;
    }
    else {
        s = uri.$id || uri.id || '';
    }
    s = s.trim();
    // strips fragment (# or #/something)
    const hash = s.indexOf('#');
    s = hash === -1 ? s : s.slice(0, hash);
    // normalize http to https (don't normalize custom dialects)
    s = s.replace(/^http:\/\/json-schema\.org\//i, 'https://json-schema.org/');
    // normalize to no trailing slash
    s = s.replace(/\/+$/g, '');
    return s;
}
function knownDialectFromSchemaUri(schemaUri) {
    if (schemaUri === normalizeSchemaUri(ajv4.defaultMeta()))
        return jsonSchema_1.SchemaDialect.draft04;
    if (schemaUri === normalizeSchemaUri(ajv7.defaultMeta()))
        return jsonSchema_1.SchemaDialect.draft07;
    if (schemaUri === normalizeSchemaUri(ajv2019.defaultMeta()))
        return jsonSchema_1.SchemaDialect.draft2019;
    if (schemaUri === normalizeSchemaUri(ajv2020.defaultMeta()))
        return jsonSchema_1.SchemaDialect.draft2020;
    return undefined;
}
function getAjvLocalizer(locale) {
    if (!locale)
        return undefined;
    const lowerLocale = locale.trim().toLowerCase();
    const aliasedLocale = AJV_LOCALE_ALIASES.get(lowerLocale);
    return ajvLocalizers[locale] || ajvLocalizers[lowerLocale] || (aliasedLocale ? ajvLocalizers[aliasedLocale] : undefined);
}
function localizeAjvErrors(errors, locale) {
    const localizer = getAjvLocalizer(locale) || ajvLocalizers.en;
    localizer(errors);
}
async function pickSchemaDialect($schema, loadSchema) {
    if (!$schema)
        return undefined;
    const s = normalizeSchemaUri($schema || '');
    const dialect = knownDialectFromSchemaUri(s);
    if (dialect)
        return dialect;
    // cache custom dialect result
    const cached = schemaDialectCache.get(s);
    if (cached) {
        return cached;
    }
    const inflight = schemaDialectInFlight.get(s);
    if (inflight) {
        return inflight;
    }
    if (!loadSchema)
        return undefined;
    // resolve custom dialect: load the dialect meta-schema doc and infer base dialect from its $schema
    const promise = (async () => {
        const meta = await loadSchema(s);
        if (meta.errors?.length)
            return undefined;
        const metaSchema = meta.schema;
        if (!metaSchema || typeof metaSchema !== 'object')
            return undefined;
        const metaDialect = knownDialectFromSchemaUri(metaSchema.$schema);
        if (metaDialect)
            return metaDialect;
        return undefined;
    })();
    schemaDialectInFlight.set(s, promise);
    try {
        const result = await promise;
        schemaDialectCache.set(s, result);
        return result;
    }
    finally {
        schemaDialectInFlight.delete(s);
    }
}
function pickMetaValidator(dialect) {
    switch (dialect) {
        case jsonSchema_1.SchemaDialect.draft04:
            return schema04Validator;
        case jsonSchema_1.SchemaDialect.draft07:
            return schema07Validator;
        case jsonSchema_1.SchemaDialect.draft2019:
            return schema2019Validator;
        case jsonSchema_1.SchemaDialect.draft2020:
            return schema2020Validator;
        default:
            // don't meta-validate unknown schema URI
            return undefined;
    }
}
//# sourceMappingURL=yamlSchemaService.js.map