import { RawQueryFragment, ReferenceKind, Utils, inspect } from '@mikro-orm/core';
/**
 * Helper for working with deeply nested where/orderBy/having criteria. Uses composite pattern to build tree from the payload.
 * Auto-joins relations and converts payload from { books: { publisher: { name: '...' } } } to { 'publisher_alias.name': '...' }
 * @internal
 */
export class CriteriaNode {
  metadata;
  entityName;
  parent;
  key;
  validate;
  strict;
  payload;
  prop;
  index;
  constructor(metadata, entityName, parent, key, validate = true, strict = false) {
    this.metadata = metadata;
    this.entityName = entityName;
    this.parent = parent;
    this.key = key;
    this.validate = validate;
    this.strict = strict;
    const meta = parent && metadata.find(parent.entityName);
    if (meta && key && !RawQueryFragment.isKnownFragmentSymbol(key)) {
      const pks = Utils.splitPrimaryKeys(key);
      if (pks.length > 1) {
        return;
      }
      for (const k of pks) {
        this.prop = meta.props.find(
          prop =>
            prop.name === k || (prop.fieldNames?.length === 1 && prop.fieldNames[0] === k && prop.persist !== false),
        );
        const isProp = this.prop || meta.props.find(prop => (prop.fieldNames || []).includes(k));
        // do not validate if the key is prefixed or type casted (e.g. `k::text`)
        if (validate && !isProp && !k.includes('.') && !k.includes('::') && !Utils.isOperator(k)) {
          throw new Error(`Trying to query by not existing property ${Utils.className(entityName)}.${k}`);
        }
      }
    }
  }
  process(qb, options) {
    return this.payload;
  }
  unwrap() {
    return this.payload;
  }
  shouldInline(payload) {
    return false;
  }
  willAutoJoin(qb, alias, options) {
    return false;
  }
  shouldRename(payload) {
    const type = this.prop ? this.prop.kind : null;
    const composite = this.prop?.joinColumns ? this.prop.joinColumns.length > 1 : false;
    const rawField = RawQueryFragment.isKnownFragmentSymbol(this.key);
    const scalar =
      payload === null ||
      Utils.isPrimaryKey(payload) ||
      payload instanceof RegExp ||
      payload instanceof Date ||
      rawField;
    const operator =
      Utils.isPlainObject(payload) && Utils.getObjectQueryKeys(payload).every(k => Utils.isOperator(k, false));
    if (composite) {
      return true;
    }
    switch (type) {
      case ReferenceKind.MANY_TO_ONE:
        return false;
      case ReferenceKind.ONE_TO_ONE:
        return !this.prop.owner;
      case ReferenceKind.ONE_TO_MANY:
        return scalar || operator;
      case ReferenceKind.MANY_TO_MANY:
        return scalar || operator;
      default:
        return false;
    }
  }
  renameFieldToPK(qb, ownerAlias) {
    const joinAlias = qb.getAliasForJoinPath(this.getPath(), { matchPopulateJoins: true });
    if (
      !joinAlias &&
      this.parent &&
      [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind) &&
      this.prop.owner
    ) {
      const alias = qb.getAliasForJoinPath(this.parent.getPath()) ?? ownerAlias ?? qb.alias;
      return Utils.getPrimaryKeyHash(this.prop.joinColumns.map(col => `${alias}.${col}`));
    }
    const alias = joinAlias ?? ownerAlias ?? qb.alias;
    if (this.prop.kind === ReferenceKind.MANY_TO_MANY) {
      return Utils.getPrimaryKeyHash(this.prop.inverseJoinColumns.map(col => `${alias}.${col}`));
    }
    return Utils.getPrimaryKeyHash(this.prop.referencedColumnNames.map(col => `${alias}.${col}`));
  }
  getPath(opts) {
    // use index on parent only if we are processing to-many relation
    const addParentIndex =
      this.prop && [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(this.prop.kind);
    const parentPath =
      opts?.parentPath ?? this.parent?.getPath({ addIndex: addParentIndex }) ?? Utils.className(this.entityName);
    const index = opts?.addIndex && this.index != null ? `[${this.index}]` : '';
    // ignore group operators to allow easier mapping (e.g. for orderBy)
    const key =
      this.key && !RawQueryFragment.isKnownFragmentSymbol(this.key) && !['$and', '$or', '$not'].includes(this.key)
        ? '.' + this.key
        : '';
    const ret = parentPath + index + key;
    if (this.isPivotJoin()) {
      // distinguish pivot table join from target entity join
      return this.getPivotPath(ret);
    }
    return ret;
  }
  isPivotJoin() {
    if (!this.key || !this.prop) {
      return false;
    }
    const rawField = RawQueryFragment.isKnownFragmentSymbol(this.key);
    const scalar =
      this.payload === null ||
      Utils.isPrimaryKey(this.payload) ||
      this.payload instanceof RegExp ||
      this.payload instanceof Date ||
      rawField;
    const operator =
      Utils.isObject(this.payload) && Utils.getObjectQueryKeys(this.payload).every(k => Utils.isOperator(k, false));
    return this.prop.kind === ReferenceKind.MANY_TO_MANY && (scalar || operator);
  }
  getPivotPath(path) {
    return `${path}[pivot]`;
  }
  aliased(field, alias) {
    return alias ? `${alias}.${field}` : field;
  }
  isStrict() {
    return this.strict;
  }
  /** @ignore */
  /* v8 ignore next */
  [Symbol.for('nodejs.util.inspect.custom')]() {
    const o = {};
    ['entityName', 'key', 'index', 'payload'].filter(k => this[k] !== undefined).forEach(k => (o[k] = this[k]));
    return `${this.constructor.name} ${inspect(o)}`;
  }
}
