import { Reference } from '../entity/Reference.js';
import { Utils } from './Utils.js';
import { ARRAY_OPERATORS, GroupOperator, JSON_KEY_OPERATORS, ReferenceKind } from '../enums.js';
import { JsonType } from '../types/JsonType.js';
import { helper } from '../entity/wrap.js';
import { isRaw, Raw } from './RawQueryFragment.js';
/** @internal */
export class QueryHelper {
  static SUPPORTED_OPERATORS = ['>', '<', '<=', '>=', '!', '!='];
  /**
   * Finds the discriminator value (key) for a given entity class in a discriminator map.
   */
  static findDiscriminatorValue(discriminatorMap, targetClass) {
    return Object.entries(discriminatorMap).find(([, cls]) => cls === targetClass)?.[0];
  }
  static processParams(params) {
    if (Reference.isReference(params)) {
      params = params.unwrap();
    }
    if (Utils.isEntity(params)) {
      if (helper(params).__meta.compositePK) {
        return helper(params).__primaryKeys;
      }
      return helper(params).getPrimaryKey();
    }
    if (params === undefined) {
      return null;
    }
    if (Array.isArray(params)) {
      return params.map(item => QueryHelper.processParams(item));
    }
    if (Utils.isPlainObject(params)) {
      QueryHelper.processObjectParams(params);
    }
    return params;
  }
  static processObjectParams(params = {}) {
    Utils.getObjectQueryKeys(params).forEach(k => {
      params[k] = QueryHelper.processParams(params[k]);
    });
    return params;
  }
  /**
   * converts `{ account: { $or: [ [Object], [Object] ] } }`
   * to `{ $or: [ { account: [Object] }, { account: [Object] } ] }`
   */
  static liftGroupOperators(where, meta, metadata, key) {
    if (!Utils.isPlainObject(where)) {
      return undefined;
    }
    const keys = Object.keys(where);
    const groupOperator = keys.find(k => {
      return (
        k in GroupOperator &&
        Array.isArray(where[k]) &&
        where[k].every(cond => {
          return (
            Utils.isPlainObject(cond) &&
            Object.keys(cond).every(k2 => {
              if (Utils.isOperator(k2, false)) {
                if (k2 === '$not') {
                  return Object.keys(cond[k2]).every(k3 => meta.primaryKeys.includes(k3));
                }
                /* v8 ignore next */
                return true;
              }
              return meta.primaryKeys.includes(k2);
            })
          );
        })
      );
    });
    if (groupOperator) {
      return groupOperator;
    }
    for (const k of keys) {
      const value = where[k];
      const prop = meta.properties[k];
      // Polymorphic relations use multiple columns (discriminator + FK), so they cannot
      // participate in the standard single-column FK expansion. Query by discriminator
      // column directly instead, e.g. { likeableType: 'post', likeableId: 1 }.
      if (!prop || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) || prop.polymorphic) {
        continue;
      }
      const op = this.liftGroupOperators(value, prop.targetMeta, metadata, k);
      if (op) {
        delete where[k];
        where[op] = value[op].map(v => {
          return { [k]: v };
        });
      }
    }
    return undefined;
  }
  static inlinePrimaryKeyObjects(where, meta, metadata, key) {
    if (Array.isArray(where)) {
      where.forEach((item, i) => {
        if (this.inlinePrimaryKeyObjects(item, meta, metadata, key)) {
          where[i] = Utils.getPrimaryKeyValues(item, meta, false);
        }
      });
    }
    if (!Utils.isPlainObject(where) || (key && meta.properties[key]?.customType instanceof JsonType)) {
      return false;
    }
    if (meta.primaryKeys.every(pk => pk in where) && Utils.getObjectKeysSize(where) === meta.primaryKeys.length) {
      return (
        !!key &&
        !GroupOperator[key] &&
        key !== '$not' &&
        Object.keys(where).every(
          k =>
            !Utils.isPlainObject(where[k]) ||
            Object.keys(where[k]).every(v => {
              if (Utils.isOperator(v, false)) {
                return true;
              }
              if (
                meta.properties[k].primary &&
                [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(meta.properties[k].kind)
              ) {
                return this.inlinePrimaryKeyObjects(where[k], meta.properties[k].targetMeta, metadata, v);
              }
              /* v8 ignore next */
              return true;
            }),
        )
      );
    }
    Object.keys(where).forEach(k => {
      const meta2 = metadata.find(meta.properties[k]?.targetMeta?.class) || meta;
      if (this.inlinePrimaryKeyObjects(where[k], meta2, metadata, k)) {
        where[k] = Utils.getPrimaryKeyValues(where[k], meta2, true);
      }
    });
    return false;
  }
  static processWhere(options) {
    // eslint-disable-next-line prefer-const
    let { where, entityName, metadata, platform, aliased = true, convertCustomTypes = true, root = true } = options;
    const meta = metadata.find(entityName);
    // inline PK-only objects in M:N queries, so we don't join the target entity when not needed
    if (meta && root) {
      QueryHelper.liftGroupOperators(where, meta, metadata);
      QueryHelper.inlinePrimaryKeyObjects(where, meta, metadata);
    }
    if (meta && root) {
      QueryHelper.convertCompositeEntityRefs(where, meta);
    }
    if (platform.getConfig().get('ignoreUndefinedInQuery') && where && typeof where === 'object') {
      Utils.dropUndefinedProperties(where);
    }
    where = QueryHelper.processParams(where) ?? {};
    /* v8 ignore next */
    if (!root && Utils.isPrimaryKey(where)) {
      return where;
    }
    if (meta && Utils.isPrimaryKey(where, meta.compositePK)) {
      where = { [Utils.getPrimaryKeyHash(meta.primaryKeys)]: where };
    }
    if (Array.isArray(where) && root) {
      const rootPrimaryKey = meta ? Utils.getPrimaryKeyHash(meta.primaryKeys) : Utils.className(entityName);
      let cond = { [rootPrimaryKey]: { $in: where } };
      // @ts-ignore
      // detect tuple comparison, use `$or` in case the number of constituents don't match
      if (
        meta &&
        !where.every(
          c =>
            Utils.isPrimaryKey(c) ||
            (Array.isArray(c) && c.length === meta.primaryKeys.length && c.every(i => Utils.isPrimaryKey(i))),
        )
      ) {
        cond = { $or: where };
      }
      return QueryHelper.processWhere({ ...options, where: cond, root: false });
    }
    if (!Utils.isPlainObject(where)) {
      return where;
    }
    return Utils.getObjectQueryKeys(where).reduce((o, key) => {
      let value = where[key];
      const customExpression = Raw.isKnownFragmentSymbol(key);
      if (Array.isArray(value) && value.length === 0 && customExpression) {
        o[key] = value;
        return o;
      }
      if (key in GroupOperator) {
        o[key] = value.map(sub => QueryHelper.processWhere({ ...options, where: sub, root }));
        return o;
      }
      // wrap top level operators (except platform allowed operators) with PK
      if (Utils.isOperator(key) && root && meta && !platform.isAllowedTopLevelOperator(key)) {
        const rootPrimaryKey = Utils.getPrimaryKeyHash(meta.primaryKeys);
        o[rootPrimaryKey] = { [key]: QueryHelper.processWhere({ ...options, where: value, root: false }) };
        return o;
      }
      const prop = customExpression ? null : this.findProperty(key, options);
      const keys = prop?.joinColumns?.length ?? 0;
      const composite = keys > 1;
      if (prop?.customType && convertCustomTypes && !isRaw(value)) {
        value = QueryHelper.processCustomType(prop, value, platform, undefined, true);
      }
      // oxfmt-ignore
      const isJsonProperty = prop?.customType instanceof JsonType && !isRaw(value) && (Utils.isPlainObject(value) ? !['$eq', '$elemMatch'].includes(Object.keys(value)[0]) : !Array.isArray(value));
      if (isJsonProperty && prop?.kind !== ReferenceKind.EMBEDDED) {
        return this.processJsonCondition(o, value, [prop.fieldNames[0]], platform, aliased);
      }
      // oxfmt-ignore
      if (Array.isArray(value) && !Utils.isOperator(key) && !QueryHelper.isSupportedOperator(key) && !(customExpression && Raw.getKnownFragment(key).params.length > 0) && options.type !== 'orderBy') {
                // comparing single composite key - use $eq instead of $in
                const op = composite && !value.every(v => Array.isArray(v)) ? '$eq' : '$in';
                o[key] = { [op]: value };
                return o;
            }
      if (Utils.isPlainObject(value)) {
        o[key] = QueryHelper.processWhere({
          ...options,
          where: value,
          entityName: prop?.targetMeta?.class ?? entityName,
          root: false,
        });
      } else {
        o[key] = value;
      }
      return o;
    }, {});
  }
  static getActiveFilters(meta, options, filters) {
    if (options === false) {
      return [];
    }
    const opts = {};
    if (Array.isArray(options)) {
      options.forEach(filter => (opts[filter] = true));
    } else if (Utils.isPlainObject(options)) {
      Object.keys(options).forEach(filter => (opts[filter] = options[filter]));
    }
    return Object.keys(filters)
      .filter(f => QueryHelper.isFilterActive(meta, f, filters[f], opts))
      .map(f => {
        filters[f].name = f;
        return filters[f];
      });
  }
  static mergePropertyFilters(propFilters, options) {
    if (!options || !propFilters || options === true || propFilters === true) {
      return options ?? propFilters;
    }
    if (Array.isArray(propFilters)) {
      propFilters = propFilters.reduce((o, item) => {
        o[item] = true;
        return o;
      }, {});
    }
    if (Array.isArray(options)) {
      options = options.reduce((o, item) => {
        o[item] = true;
        return o;
      }, {});
    }
    return Utils.mergeConfig({}, propFilters, options);
  }
  static isFilterActive(meta, filterName, filter, options) {
    if (filter.entity && !filter.entity.includes(meta.className)) {
      return false;
    }
    if (options[filterName] === false) {
      return false;
    }
    return filter.default || filterName in options;
  }
  static processCustomType(prop, cond, platform, key, fromQuery) {
    if (Utils.isPlainObject(cond)) {
      return Utils.getObjectQueryKeys(cond).reduce((o, k) => {
        if (!Raw.isKnownFragmentSymbol(k) && (Utils.isOperator(k, true) || prop.referencedPKs?.includes(k))) {
          o[k] = QueryHelper.processCustomType(prop, cond[k], platform, k, fromQuery);
        } else {
          o[k] = cond[k];
        }
        return o;
      }, {});
    }
    if (key && JSON_KEY_OPERATORS.includes(key)) {
      return Array.isArray(cond) ? platform.marshallArray(cond) : cond;
    }
    if (Array.isArray(cond) && !(key && ARRAY_OPERATORS.includes(key))) {
      return cond.map(v => QueryHelper.processCustomType(prop, v, platform, key, fromQuery));
    }
    if (isRaw(cond)) {
      return cond;
    }
    return prop.customType.convertToDatabaseValue(cond, platform, { fromQuery, key, mode: 'query' });
  }
  static isSupportedOperator(key) {
    return !!QueryHelper.SUPPORTED_OPERATORS.find(op => key === op);
  }
  static processJsonCondition(o, value, path, platform, alias) {
    return platform.processJsonCondition(o, value, path, alias);
  }
  static findProperty(fieldName, options) {
    const parts = fieldName.split('.');
    const propName = parts.pop();
    const alias = parts.length > 0 ? parts.join('.') : undefined;
    const entityName = alias ? options.aliasMap?.[alias] : options.entityName;
    const meta = entityName ? options.metadata.find(entityName) : undefined;
    return meta?.properties[propName];
  }
  /**
   * Converts entity references for composite FK properties into flat arrays
   * of correctly-ordered join column values, before processParams flattens them
   * incorrectly due to shared FK columns.
   */
  static convertCompositeEntityRefs(where, meta) {
    if (!Utils.isPlainObject(where)) {
      return;
    }
    for (const k of Object.keys(where)) {
      if (k in GroupOperator) {
        if (Array.isArray(where[k])) {
          where[k].forEach(sub => this.convertCompositeEntityRefs(sub, meta));
        }
        continue;
      }
      if (k === '$not') {
        this.convertCompositeEntityRefs(where[k], meta);
        continue;
      }
      const prop = meta.properties[k];
      if (!prop?.joinColumns || prop.joinColumns.length <= 1) {
        continue;
      }
      const w = where[k];
      if (Utils.isEntity(w)) {
        where[k] = this.extractJoinColumnValues(w, prop);
      } else if (Utils.isPlainObject(w)) {
        for (const op of Object.keys(w)) {
          if (Utils.isOperator(op, false) && Array.isArray(w[op])) {
            w[op] = w[op].map(item => (Utils.isEntity(item) ? this.extractJoinColumnValues(item, prop) : item));
          }
        }
      }
    }
  }
  /**
   * Extracts values for a FK's join columns from an entity by traversing the FK chain.
   * Handles shared FK columns (e.g., tenant_id referenced by multiple FKs) correctly.
   */
  static extractJoinColumnValues(entity, prop) {
    return prop.referencedColumnNames.map(refCol => {
      return this.extractColumnValue(entity, prop.targetMeta, refCol);
    });
  }
  /**
   * Extracts the value for a specific column from an entity by finding which PK property
   * owns that column and recursively traversing FK references.
   */
  static extractColumnValue(entity, meta, columnName) {
    for (const pk of meta.primaryKeys) {
      const pkProp = meta.properties[pk];
      const colIdx = pkProp.fieldNames.indexOf(columnName);
      if (colIdx !== -1) {
        const value = entity[pk];
        if (pkProp.targetMeta && Utils.isEntity(value, true)) {
          const refCol = pkProp.referencedColumnNames[colIdx];
          return this.extractColumnValue(value, pkProp.targetMeta, refCol);
        }
        return value;
      }
    }
    return null;
  }
  /**
   * Merges multiple orderBy sources with key-level deduplication (first-seen key wins).
   * RawQueryFragment symbol keys are never deduped (each is unique).
   */
  static mergeOrderBy(...sources) {
    const result = [];
    const seenKeys = new Set();
    for (const source of sources) {
      if (source == null) {
        continue;
      }
      for (const item of Utils.asArray(source)) {
        for (const key of Utils.getObjectQueryKeys(item)) {
          if (typeof key === 'symbol') {
            result.push({ [key]: item[key] });
          } else if (!seenKeys.has(key)) {
            seenKeys.add(key);
            result.push({ [key]: item[key] });
          }
        }
      }
    }
    return result;
  }
}
