// Copyright 2021 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { Diagnostic } from '@codemirror/lint';
import { SyntaxNode, Tree } from '@lezer/common';
import {
  AggregateExpr,
  AnchoredExpr,
  And,
  BinaryExpr,
  BoolModifier,
  Bottomk,
  Changes,
  CountValues,
  Delta,
  Eql,
  EqlSingle,
  FunctionCall,
  FunctionCallBody,
  Gte,
  Gtr,
  Identifier,
  Increase,
  LabelMatchers,
  LimitK,
  LimitRatio,
  Lss,
  Lte,
  MatrixSelector,
  Neq,
  Or,
  ParenExpr,
  Quantile,
  QuotedLabelMatcher,
  QuotedLabelName,
  Rate,
  Resets,
  SmoothedExpr,
  StepInvariantExpr,
  SubqueryExpr,
  Topk,
  TrimLower,
  TrimUpper,
  UnaryExpr,
  Unless,
  UnquotedLabelMatcher,
  VectorSelector,
} from '@prometheus-io/lezer-promql';
import { containsAtLeastOneChild, containsChild } from './path-finder';
import { getType } from './type';
import { buildLabelMatchers } from './matcher';
import { EditorState } from '@codemirror/state';
import { syntaxTree } from '@codemirror/language';
import { getFunction, Matcher, ValueType, VectorMatchCardinality } from '../types';
import { buildVectorMatching } from './vector';

export class Parser {
  private readonly tree: Tree;
  private readonly state: EditorState;
  private readonly diagnostics: Diagnostic[];

  constructor(state: EditorState) {
    this.tree = syntaxTree(state);
    this.state = state;
    this.diagnostics = [];
  }

  getDiagnostics(): Diagnostic[] {
    return this.diagnostics.sort((a, b) => {
      return a.from - b.from;
    });
  }

  analyze(): void {
    // when you are at the root of the tree, the first node is not `Expr` but a node with no name.
    // So to be able to iterate other the node relative to the promql node, we have to get the first child at the beginning
    this.checkAST(this.tree.topNode.firstChild);
    this.diagnoseAllErrorNodes();
  }

  private diagnoseAllErrorNodes() {
    const cursor = this.tree.cursor();
    while (cursor.next()) {
      // usually there is an error node at the end of the expression when user is typing
      // so it's not really a useful information to say the expression is wrong.
      // Hopefully if there is an error node at the end of the tree, checkAST should yell more precisely
      if (cursor.type.id === 0 && cursor.to !== this.tree.topNode.to) {
        const node = cursor.node.parent;
        this.diagnostics.push({
          severity: 'error',
          message: 'unexpected expression',
          from: node ? node.from : cursor.from,
          to: node ? node.to : cursor.to,
        });
      }
    }
  }

  // checkAST is inspired of the same named method from prometheus/prometheus:
  // https://github.com/prometheus/prometheus/blob/3470ee1fbf9d424784eb2613bab5ab0f14b4d222/promql/parser/parse.go#L433
  checkAST(node: SyntaxNode | null): ValueType {
    if (!node) {
      return ValueType.none;
    }
    switch (node.type.id) {
      case AggregateExpr:
        this.checkAggregationExpr(node);
        break;
      case BinaryExpr:
        this.checkBinaryExpr(node);
        break;
      case FunctionCall:
        this.checkCallFunction(node);
        break;
      case ParenExpr:
        this.checkAST(node.getChild('Expr'));
        break;
      case UnaryExpr: {
        const unaryExprType = this.checkAST(node.getChild('Expr'));
        if (unaryExprType !== ValueType.scalar && unaryExprType !== ValueType.vector) {
          this.addDiagnostic(node, `unary expression only allowed on expressions of type scalar or instant vector, got ${unaryExprType}`);
        }
        break;
      }
      case SmoothedExpr: {
        this.checkAnchoredSmoothedExpr(node, [Rate, Increase, Delta]);
        break;
      }
      case AnchoredExpr: {
        this.checkAnchoredSmoothedExpr(node, [Resets, Changes, Rate, Increase, Delta]);
        break;
      }
      case SubqueryExpr: {
        const subQueryExprType = this.checkAST(node.getChild('Expr'));
        if (subQueryExprType !== ValueType.vector) {
          this.addDiagnostic(node, `subquery is only allowed on instant vector, got ${subQueryExprType} in ${node.name} instead`);
        }
        break;
      }
      case MatrixSelector:
        this.checkAST(node.getChild('Expr'));
        break;
      case VectorSelector:
        this.checkVectorSelector(node);
        break;
      case StepInvariantExpr: {
        const exprValue = this.checkAST(node.getChild('Expr'));
        if (exprValue !== ValueType.vector && exprValue !== ValueType.matrix) {
          this.addDiagnostic(node, `@ modifier must be preceded by an instant selector vector or range vector selector or a subquery`);
        }
        // if you are looking at the Prometheus code, you will likely find that some checks are missing here.
        // Specially the one checking if the timestamp after the `@` is ok: https://github.com/prometheus/prometheus/blob/ad5ed416ba635834370bfa06139258b31f8c33f9/promql/parser/parse.go#L722-L725
        // Since Javascript is managing the number as a float64 and so on 53 bits, we cannot validate that the maxInt64 number is a valid value.
        // So, to manage properly this issue, we would need to use the BigInt which is possible or by using ES2020.BigInt, or by using the lib: https://github.com/GoogleChromeLabs/jsbi.
        //   * Introducing a lib just for these checks is quite overkilled
        //   * Using ES2020 would be the way to go. Unfortunately moving to ES2020 is breaking the build of the lib.
        //     So far I didn't find the way to fix it. I think it's likely due to the fact we are building an ESM package which is now something stable in nodeJS/javascript but still experimental in typescript.
        // For the above reason, we decided to drop these checks.
        break;
      }
    }

    return getType(node);
  }

  private checkAggregationExpr(node: SyntaxNode): void {
    // according to https://github.com/promlabs/lezer-promql/blob/master/src/promql.grammar#L26
    // the name of the aggregator function is stored in the first child
    const aggregateOp = node.firstChild?.firstChild;
    if (!aggregateOp) {
      this.addDiagnostic(node, 'aggregation operator expected in aggregation expression but got nothing');
      return;
    }
    const body = node.getChild(FunctionCallBody);
    const params = body ? body.getChildren('Expr') : [];
    if (!params.length) {
      this.addDiagnostic(node, 'unable to find the parameter for the expression');
      return;
    }
    this.expectType(params[params.length - 1], ValueType.vector, 'aggregation expression');
    // get the parameter of the aggregation operator
    if (
      aggregateOp.type.id === Topk ||
      aggregateOp.type.id === Bottomk ||
      aggregateOp.type.id === LimitK ||
      aggregateOp.type.id === LimitRatio ||
      aggregateOp.type.id === Quantile
    ) {
      this.expectType(params[0], ValueType.scalar, 'aggregation parameter');
    }
    if (aggregateOp.type.id === CountValues) {
      this.expectType(params[0], ValueType.string, 'aggregation parameter');
    }
  }

  private checkBinaryExpr(node: SyntaxNode): void {
    // Following the definition of the BinaryExpr, the left and the right
    // expression are respectively the first and last child
    // https://github.com/promlabs/lezer-promql/blob/master/src/promql.grammar#L52
    const lExpr = node.firstChild;
    const rExpr = node.lastChild;
    if (!lExpr || !rExpr) {
      this.addDiagnostic(node, 'left or right expression is missing in binary expression');
      return;
    }
    const lt = this.checkAST(lExpr);
    const rt = this.checkAST(rExpr);
    const boolModifierUsed = node.getChild(BoolModifier);
    const isComparisonOperator = containsAtLeastOneChild(node, Eql, Neq, Lte, Lss, Gte, Gtr);
    const isTrimLowerOperator = containsChild(node, TrimLower);
    const isTrimUpperOperator = containsChild(node, TrimUpper);
    const isSetOperator = containsAtLeastOneChild(node, And, Or, Unless);

    // BOOL modifier check
    if (boolModifierUsed) {
      if (!isComparisonOperator) {
        this.addDiagnostic(node, 'bool modifier can only be used on comparison operators');
      }
    } else {
      if (lt === ValueType.scalar && rt === ValueType.scalar) {
        if (isComparisonOperator) {
          this.addDiagnostic(node, 'comparisons between scalars must use BOOL modifier');
        } else if (isTrimLowerOperator) {
          this.addDiagnostic(node, 'operator ">/" not allowed for Scalar operations');
        } else if (isTrimUpperOperator) {
          this.addDiagnostic(node, 'operator "</" not allowed for Scalar operations');
        }
      }
    }

    const vectorMatching = buildVectorMatching(this.state, node);
    if (vectorMatching !== null && vectorMatching.on) {
      for (const l1 of vectorMatching.matchingLabels) {
        for (const l2 of vectorMatching.include) {
          if (l1 === l2) {
            this.addDiagnostic(node, `label "${l1}" must not occur in ON and GROUP clause at once`);
          }
        }
      }
    }

    if (lt !== ValueType.scalar && lt !== ValueType.vector) {
      this.addDiagnostic(lExpr, 'binary expression must contain only scalar and instant vector types');
    }
    if (rt !== ValueType.scalar && rt !== ValueType.vector) {
      this.addDiagnostic(rExpr, 'binary expression must contain only scalar and instant vector types');
    }

    if ((lt !== ValueType.vector || rt !== ValueType.vector) && vectorMatching !== null) {
      if (vectorMatching.matchingLabels.length > 0) {
        this.addDiagnostic(node, 'vector matching only allowed between instant vectors');
      }
    } else {
      if (isSetOperator) {
        if (vectorMatching?.card === VectorMatchCardinality.CardOneToMany || vectorMatching?.card === VectorMatchCardinality.CardManyToOne) {
          this.addDiagnostic(node, 'no grouping allowed for set operations');
        }
        if (vectorMatching?.card !== VectorMatchCardinality.CardManyToMany) {
          this.addDiagnostic(node, 'set operations must always be many-to-many');
        }
      }
    }
    if ((lt === ValueType.scalar || rt === ValueType.scalar) && isSetOperator) {
      this.addDiagnostic(node, 'set operator not allowed in binary scalar expression');
    }
  }

  private checkCallFunction(node: SyntaxNode): void {
    const funcID = node.firstChild?.firstChild;
    if (!funcID) {
      this.addDiagnostic(node, 'function not defined');
      return;
    }

    const body = node.getChild(FunctionCallBody);
    const args = body ? body.getChildren('Expr') : [];
    const funcSignature = getFunction(funcID.type.id);
    const nargs = funcSignature.argTypes.length;

    if (funcSignature.variadic === 0) {
      if (args.length !== nargs) {
        this.addDiagnostic(node, `expected ${nargs} argument(s) in call to "${funcSignature.name}", got ${args.length}`);
      }
    } else {
      const na = nargs - 1;
      if (na > args.length) {
        this.addDiagnostic(node, `expected at least ${na} argument(s) in call to "${funcSignature.name}", got ${args.length}`);
      } else {
        const nargsmax = na + funcSignature.variadic;
        if (funcSignature.variadic > 0 && nargsmax < args.length) {
          this.addDiagnostic(node, `expected at most ${nargsmax} argument(s) in call to "${funcSignature.name}", got ${args.length}`);
        }
      }
    }

    if (funcSignature.name === 'info') {
      // Verify that the data label selector expression is not prefixed with metric name.
      if (args.length > 1 && args[1].getChild(Identifier)) {
        this.addDiagnostic(node, `expected label selectors as the second argument to "info" function, got ${args[1].type}`);
      }
    }

    let j = 0;
    for (let i = 0; i < args.length; i++) {
      j = i;
      if (j >= funcSignature.argTypes.length) {
        if (funcSignature.variadic === 0) {
          // This is not a vararg function so we should not check the
          // type of the extra arguments.
          break;
        }
        j = funcSignature.argTypes.length - 1;
      }
      this.expectType(args[i], funcSignature.argTypes[j], `call to function "${funcSignature.name}"`);
    }
  }

  private checkAnchoredSmoothedExpr(node: SyntaxNode, allowedFunctions: number[]): void {
    // A smoothed/anchored expression is supposed to work with range vectors or instant vectors.
    // So first thing to do is to check the type of the child.
    // Then, if this is used inside a function call, we need to check that the function is one of the given allowedFunctions.
    const nodeType = getType(node);
    if (nodeType !== ValueType.vector && nodeType !== ValueType.matrix) {
      this.addDiagnostic(node, `smoothed/anchored expression only allowed on instant vector or range vector selector, got ${nodeType} instead`);
      return;
    }
    const parent = node.parent?.parent;
    if (!parent || parent.type.id !== FunctionCall) {
      // Since the anchored/smoothed expression is not inside a function call, we cannot check the function name.
      // This is an acceptable case as the anchored/smoothed expression can be used on any vector expression.
      return;
    }
    const funcID = parent.firstChild?.firstChild;
    if (!funcID) {
      this.addDiagnostic(node, 'function not defined');
      return;
    }
    if (!allowedFunctions.includes(funcID.type.id)) {
      this.addDiagnostic(node, 'smoothed/anchored expression can only be used in specific functions');
    }
  }

  private checkVectorSelector(node: SyntaxNode): void {
    const matchList = node.getChild(LabelMatchers);
    const labelMatcherOpts = [QuotedLabelName, QuotedLabelMatcher, UnquotedLabelMatcher];
    let labelMatchers: Matcher[] = [];
    for (const labelMatcherOpt of labelMatcherOpts) {
      labelMatchers = labelMatchers.concat(buildLabelMatchers(matchList ? matchList.getChildren(labelMatcherOpt) : [], this.state));
    }
    let vectorSelectorName = '';
    // VectorSelector ( Identifier )
    // https://github.com/promlabs/lezer-promql/blob/71e2f9fa5ae6f5c5547d5738966cd2512e6b99a8/src/promql.grammar#L200
    const vectorSelectorNodeName = node.getChild(Identifier);
    if (vectorSelectorNodeName) {
      vectorSelectorName = this.state.sliceDoc(vectorSelectorNodeName.from, vectorSelectorNodeName.to);
    }
    if (vectorSelectorName !== '') {
      // In this case the last LabelMatcher is checking for the metric name
      // set outside the braces. This checks if the name has already been set
      // previously
      const labelMatcherMetricName = labelMatchers.find((lm) => lm.name === '__name__');
      if (labelMatcherMetricName) {
        this.addDiagnostic(node, `metric name must not be set twice: ${vectorSelectorName} or ${labelMatcherMetricName.value}`);
      }
      // adding the metric name as a Matcher to avoid a false positive for this kind of expression:
      // foo{bare=''}
      labelMatchers.push(new Matcher(EqlSingle, '__name__', vectorSelectorName));
    } else {
      // In this case when metric name is not set outside the braces
      // It is checking whether metric name is set twice like in :
      // {__name__:"foo", "foo"}, {"foo", "bar"}
      const labelMatchersMetricName = labelMatchers.filter((lm) => lm.name === '__name__');
      if (labelMatchersMetricName.length > 1) {
        this.addDiagnostic(node, `metric name must not be set twice: ${labelMatchersMetricName[0].value} or ${labelMatchersMetricName[1].value}`);
      }
    }

    // A Vector selector must contain at least one non-empty matcher to prevent
    // implicit selection of all metrics (e.g. by a typo).
    const empty = labelMatchers.every((lm) => lm.matchesEmpty());
    if (empty) {
      this.addDiagnostic(node, 'vector selector must contain at least one non-empty matcher');
    }
  }

  private expectType(node: SyntaxNode, want: ValueType, context: string): void {
    const t = this.checkAST(node);
    if (t !== want) {
      this.addDiagnostic(node, `expected type ${want} in ${context}, got ${t}`);
    }
  }

  private addDiagnostic(node: SyntaxNode, msg: string): void {
    this.diagnostics.push({
      severity: 'error',
      message: msg,
      from: node.from,
      to: node.to,
    });
  }
}
