import { DiagnosticSeverity } from 'vscode-languageserver-types';
import { NodePatternContext, RelationshipPatternContext, } from '../generated-parser/CypherCmdParser.js';
import { translateTokensToRange, } from '../cypherLanguageService.js';
import { isAnyNode, isNotAnyNode, removeInnerAnys, } from '../labelTreeRewriting.js';
import { getNodesFromRelsSet } from '../labelTreeWalking.js';
import { isLabelLeaf } from '../types.js';
import { getDirection } from '../helpers.js';
// A relationship has exactly one type; this stand-in represents "some
// relationship type that is not mentioned anywhere" when checking whether a
// relationship type expression is satisfiable at all.
const NON_SCHEMA_RELTYPE = '__non_schema_reltype__';
/**
 * Warns when a node-relationship adjacency (a "path segment") in a read pattern
 * is not possible according to the graph schema. Works at the pair level: for
 * each relationship we check the node before it and the node after it against
 * the relationship's matching types on the relevant endpoint side.
 */
export function warnOnSchemaPathViolations(parseResult, dbSchema, symbolTable) {
    if (!dbSchema.graphSchema) {
        return [];
    }
    const diagnostics = [];
    const { fromRels, toRels } = getNodesFromRelsSet(dbSchema);
    const graphSchemaRelTypes = new Set(fromRels.keys());
    for (const patternElement of parseResult.collectedReadPatternElements) {
        const elements = (patternElement.children ?? []).filter((c) => c instanceof NodePatternContext ||
            c instanceof RelationshipPatternContext);
        for (let i = 0; i < elements.length; i++) {
            const rel = elements[i];
            if (!(rel instanceof RelationshipPatternContext)) {
                continue;
            }
            const left = nodeOrUndefined(elements[i - 1]);
            const right = nodeOrUndefined(elements[i + 1]);
            collectSegmentWarnings(rel, left, right, symbolTable, graphSchemaRelTypes, fromRels, toRels).forEach((d) => diagnostics.push(d));
        }
    }
    return diagnostics;
}
function nodeOrUndefined(element) {
    return element instanceof NodePatternContext ? element : undefined;
}
function collectSegmentWarnings(rel, left, right, symbolTable, schemaRelTypes, fromRels, toRels) {
    // Variable-length relationships are not linted.
    if (rel.pathLength()) {
        return [];
    }
    const relEntry = findEntry(rel, symbolTable);
    if (!relEntry || isAnyNode(relEntry.labels)) {
        return [];
    }
    const relTree = relEntry.labels;
    // `!%` and other relationship type expressions that no single type can
    // satisfy are handled by Neo4j's own semantic error; don't pile on.
    if (!relTypeSatisfiable(relTree, schemaRelTypes)) {
        return [];
    }
    const validRelTypes = [...schemaRelTypes].filter((relType) => evalLabelTree(relTree, new Set([relType])));
    const direction = getDirection(rel);
    const relStr = renderLabelTree(relTree);
    const warnings = [];
    if (left) {
        const warning = checkAdjacency({
            node: left,
            position: 'left',
            rel,
            relStr,
            validRelTypes: validRelTypes,
            role: roleFor('left', direction),
            qualifier: qualifierFor(direction),
            symbolTable,
            fromRels,
            toRels,
        });
        if (warning) {
            warnings.push(warning);
        }
    }
    if (right) {
        const warning = checkAdjacency({
            node: right,
            position: 'right',
            rel,
            relStr,
            validRelTypes: validRelTypes,
            role: roleFor('right', direction),
            qualifier: qualifierFor(direction),
            symbolTable,
            fromRels,
            toRels,
        });
        if (warning) {
            warnings.push(warning);
        }
    }
    return warnings;
}
function checkAdjacency({ node, position, rel, relStr, validRelTypes, role, qualifier, symbolTable, fromRels, toRels, }) {
    const nodeEntry = findEntry(node, symbolTable);
    if (!nodeEntry || isAnyNode(nodeEntry.labels)) {
        return undefined;
    }
    const nodeTree = nodeEntry.labels;
    const cleaned = removeInnerAnys(nodeTree);
    // `%` (any) is a tautology and `!%` matches only label-less nodes - neither
    // can be disproven against a label-keyed schema.
    if (isAnyNode(cleaned) || isNotAnyNode(cleaned)) {
        return undefined;
    }
    let supported = true;
    try {
        supported = validRelTypes.some((relType) => {
            const from = fromRels.get(relType) ?? new Set();
            const to = toRels.get(relType) ?? new Set();
            if (role === 'from') {
                return labelTreeSatisfiableBy(cleaned, from);
            }
            if (role === 'to') {
                return labelTreeSatisfiableBy(cleaned, to);
            }
            return (labelTreeSatisfiableBy(cleaned, from) ||
                labelTreeSatisfiableBy(cleaned, to));
        });
    }
    catch (err) {
        if (String(err).includes('Bailing schema based linting on large label tree')) {
            return undefined;
        }
        else {
            throw err;
        }
    }
    if (supported) {
        return undefined;
    }
    const nodeStr = renderLabelTree(nodeTree);
    if (position === 'left') {
        return {
            message: `[:${relStr}] has no ${qualifier}(:${nodeStr})`,
            ...translateTokensToRange(node.start, rel.stop),
            severity: DiagnosticSeverity.Warning,
        };
    }
    return {
        message: `(:${nodeStr}) has no ${qualifier}[:${relStr}]`,
        ...translateTokensToRange(rel.start, node.stop),
        severity: DiagnosticSeverity.Warning,
    };
}
function roleFor(position, direction) {
    if (direction === 'undirected') {
        return 'either';
    }
    if (direction === 'right') {
        return position === 'left' ? 'from' : 'to';
    }
    // left arrow
    return position === 'left' ? 'to' : 'from';
}
function qualifierFor(direction) {
    if (direction === 'right') {
        return 'incoming ';
    }
    if (direction === 'left') {
        return 'outgoing ';
    }
    return 'incoming/outgoing ';
}
/**
 * Finds the (possibly anonymous) variable's symbol-table entry for a node or
 * relationship pattern. Named variables are matched by reference offset;
 * anonymous ones by the synthetic definition position inside the pattern span.
 */
function findEntry(element, symbolTable) {
    const variable = element.variable();
    if (variable === null) {
        return symbolTable.find((entry) => entry.definitionPosition >= element.start.start &&
            entry.definitionPosition <= (element.stop?.stop ?? -1));
    }
    return symbolTable.find((entry) => entry.references.includes(variable.start.start));
}
/**
 * Is the cleaned (any-free) node label tree satisfiable by some non-empty set
 * of labels that the schema allows on this side (`candidates`)?
 *
 * `evalNodeTree` only inspects labels that appear in the tree, so any candidate
 * NOT named in the tree is interchangeable with every other such candidate. We
 * therefore only enumerate subsets of the labels that are both named in the
 * tree and valid candidates (`candInTree`). That keeps the 2^n search bounded by
 * the number of labels written in the query's label expression, not by the
 * (potentially schema-sized) candidate set.
 *
 * `otherAvailable` records that at least one valid candidate is not named in the
 * tree. Such a label doesn't change the tree's truth value (the tree never
 * references it), but it does make the node's label set genuinely non-empty.
 * That is what lets e.g. `(:!Pokemon)` be satisfied by a `Gym` node when `Gym`
 * is a valid candidate: the empty `candInTree` subset evaluates the tree as
 * true, and `otherAvailable` confirms a real (non-empty) node can back it.
 *
 * Bails on too large label trees by throwing an error, caught by the caller
 */
function labelTreeSatisfiableBy(tree, candidates) {
    const mentioned = labelsIn(tree);
    const candInTree = [...candidates].filter((c) => mentioned.has(c));
    const otherAvailable = candidates.size > candInTree.length;
    const n = candInTree.length;
    if (n > 10) {
        throw new Error('Bailing schema based linting on large label tree');
    }
    // Goes through all subsets of candInTrees until it finds one that is valid
    // Mask in binary is a filter of which of the candidate labels to include -
    // 1 on position i of the binary number meaning "include the ith label"
    for (let mask = 0; mask < 1 << n; mask++) {
        const labelSubSet = new Set();
        for (let i = 0; i < n; i++) {
            if (mask & (1 << i)) {
                labelSubSet.add(candInTree[i]);
            }
        }
        // The assignment must correspond to a real, non-empty node: it either uses
        // a named candidate, or relies on some other (unnamed) candidate existing.
        if (evalLabelTree(tree, labelSubSet) &&
            (labelSubSet.size > 0 || otherAvailable)) {
            return true;
        }
    }
    return false;
}
/** Evaluate a label tree as a boolean over the label set `S`. */
function evalLabelTree(tree, labelSet) {
    if (isLabelLeaf(tree)) {
        return labelSet.has(tree.value);
    }
    switch (tree.condition) {
        case 'and':
            return tree.children.every((c) => evalLabelTree(c, labelSet));
        case 'or':
            return tree.children.some((c) => evalLabelTree(c, labelSet));
        case 'not':
            return !evalLabelTree(tree.children[0], labelSet);
        case 'any':
            return labelSet.size > 0;
    }
}
/** Whether any single relationship type could satisfy the type expression. */
function relTypeSatisfiable(tree, schemaRelTypes) {
    const universe = new Set([
        ...schemaRelTypes,
        ...labelsIn(tree),
        NON_SCHEMA_RELTYPE,
    ]);
    for (const relType of universe) {
        if (evalLabelTree(tree, new Set([relType]))) {
            return true;
        }
    }
    return false;
}
function labelsIn(tree, acc = new Set()) {
    if (isLabelLeaf(tree)) {
        acc.add(tree.value);
    }
    else {
        tree.children.forEach((c) => labelsIn(c, acc));
    }
    return acc;
}
/** Renders a label/type tree back to its written form (e.g. `(A | !B)`). */
function renderLabelTree(tree) {
    if (isLabelLeaf(tree)) {
        return tree.value;
    }
    switch (tree.condition) {
        case 'any':
            return '%';
        case 'not':
            return '!' + renderLabelTree(tree.children[0]);
        case 'and':
        case 'or': {
            if (tree.children.length <= 1) {
                return tree.children.length === 1
                    ? renderLabelTree(tree.children[0])
                    : '';
            }
            const separator = tree.condition === 'and' ? ' & ' : ' | ';
            return '(' + tree.children.map(renderLabelTree).join(separator) + ')';
        }
    }
}
//# sourceMappingURL=schemaBasedValidation.js.map