import {
  GroupOperator,
  isRaw,
  JsonType,
  RawQueryFragment,
  ReferenceKind,
  Utils,
  ValidationError,
} from '@mikro-orm/core';
import { ObjectCriteriaNode } from './ObjectCriteriaNode.js';
import { ArrayCriteriaNode } from './ArrayCriteriaNode.js';
import { ScalarCriteriaNode } from './ScalarCriteriaNode.js';
import { EMBEDDABLE_ARRAY_OPS } from './enums.js';
/**
 * @internal
 */
export class CriteriaNodeFactory {
  static createNode(metadata, entityName, payload, parent, key, validate = true) {
    const rawField = RawQueryFragment.isKnownFragmentSymbol(key);
    const scalar =
      Utils.isPrimaryKey(payload) || isRaw(payload) || payload instanceof RegExp || payload instanceof Date || rawField;
    if (Array.isArray(payload) && !scalar) {
      return this.createArrayNode(metadata, entityName, payload, parent, key, validate);
    }
    if (Utils.isPlainObject(payload) && !scalar) {
      return this.createObjectNode(metadata, entityName, payload, parent, key, validate);
    }
    return this.createScalarNode(metadata, entityName, payload, parent, key, validate);
  }
  static createScalarNode(metadata, entityName, payload, parent, key, validate = true) {
    const node = new ScalarCriteriaNode(metadata, entityName, parent, key, validate);
    node.payload = payload;
    return node;
  }
  static createArrayNode(metadata, entityName, payload, parent, key, validate = true) {
    const node = new ArrayCriteriaNode(metadata, entityName, parent, key, validate);
    node.payload = payload.map((item, index) => {
      const n = this.createNode(metadata, entityName, item, node, undefined, validate);
      // we care about branching only for $and
      if (key === '$and' && payload.length > 1) {
        n.index = index;
      }
      return n;
    });
    return node;
  }
  static createObjectNode(metadata, entityName, payload, parent, key, validate = true) {
    const meta = metadata.find(entityName);
    const node = new ObjectCriteriaNode(metadata, entityName, parent, key, validate, payload.__strict);
    node.payload = {};
    for (const k of Utils.getObjectQueryKeys(payload)) {
      node.payload[k] = this.createObjectItemNode(metadata, entityName, node, payload, k, meta, validate);
    }
    return node;
  }
  static createObjectItemNode(metadata, entityName, node, payload, key, meta, validate = true) {
    const rawField = RawQueryFragment.isKnownFragmentSymbol(key);
    const prop = rawField ? null : meta?.properties[key];
    const childEntity = prop && prop.kind !== ReferenceKind.SCALAR ? prop.targetMeta.class : entityName;
    const isNotEmbedded = rawField || prop?.kind !== ReferenceKind.EMBEDDED;
    const val = payload[key];
    if (isNotEmbedded && prop?.customType instanceof JsonType) {
      return this.createScalarNode(metadata, childEntity, val, node, key, validate);
    }
    if (prop?.kind === ReferenceKind.SCALAR && val != null && Object.keys(val).some(f => f in GroupOperator)) {
      throw ValidationError.cannotUseGroupOperatorsInsideScalars(entityName, prop.name, payload);
    }
    if (isNotEmbedded) {
      return this.createNode(metadata, childEntity, val, node, key, validate);
    }
    if (val == null) {
      const map = Object.keys(prop.embeddedProps).reduce((oo, k) => {
        oo[prop.embeddedProps[k].name] = null;
        return oo;
      }, {});
      return this.createNode(metadata, entityName, map, node, key, validate);
    }
    // For array embeddeds stored as real columns, route property-level queries
    // as scalar nodes so QueryBuilderHelper generates EXISTS subqueries with
    // JSON array iteration.  Keys containing `~` indicate the property lives
    // inside a parent's object-mode JSON column (MetadataDiscovery uses `~` as
    // the glue for object embeds), where JSON path access is used instead.
    if (prop.array && !String(key).includes('~')) {
      const keys = Object.keys(val);
      const hasOnlyArrayOps = keys.every(k => EMBEDDABLE_ARRAY_OPS.includes(k));
      if (!hasOnlyArrayOps) {
        return this.createScalarNode(metadata, entityName, val, node, key, validate);
      }
    }
    // array operators can be used on embedded properties
    const operator = Object.keys(val).some(f => Utils.isOperator(f) && !EMBEDDABLE_ARRAY_OPS.includes(f));
    if (operator) {
      throw ValidationError.cannotUseOperatorsInsideEmbeddables(entityName, prop.name, payload);
    }
    const map = Object.keys(val).reduce((oo, k) => {
      const embeddedProp = prop.embeddedProps[k] ?? Object.values(prop.embeddedProps).find(p => p.name === k);
      if (!embeddedProp && !EMBEDDABLE_ARRAY_OPS.includes(k)) {
        throw ValidationError.invalidEmbeddableQuery(entityName, k, prop.type);
      }
      if (embeddedProp) {
        oo[embeddedProp.name] = val[k];
      } else if (typeof val[k] === 'object') {
        oo[k] = JSON.stringify(val[k]);
      } else {
        oo[k] = val[k];
      }
      return oo;
    }, {});
    return this.createNode(metadata, entityName, map, node, key, validate);
  }
}
