import { helper } from '../entity/wrap.js';
import { Utils } from '../utils/Utils.js';
import { ReferenceKind } from '../enums.js';
import { Reference } from '../entity/Reference.js';
import { SerializationContext } from './SerializationContext.js';
import { isRaw } from '../utils/RawQueryFragment.js';
function isVisible(meta, propName, options) {
  const prop = meta.properties[propName];
  if (options.groups && prop?.groups) {
    return prop.groups.some(g => options.groups.includes(g));
  }
  if (
    Array.isArray(options.populate) &&
    options.populate?.find(item => item === propName || item.startsWith(propName + '.') || item === '*')
  ) {
    return true;
  }
  if (options.exclude?.find(item => item === propName)) {
    return false;
  }
  const visible = prop && !(prop.hidden && !options.includeHidden);
  const prefixed = prop && !prop.primary && !prop.accessor && propName.startsWith('_'); // ignore prefixed properties, if it's not a PK
  return visible && !prefixed;
}
function isPopulated(propName, options) {
  if (
    typeof options.populate !== 'boolean' &&
    options.populate?.find(item => item === propName || item.startsWith(propName + '.') || item === '*')
  ) {
    return true;
  }
  if (typeof options.populate === 'boolean') {
    return options.populate;
  }
  return false;
}
/** Converts entity instances to plain DTOs via `serialize()`, with fine-grained control over populate, exclude, and serialization groups. */
export class EntitySerializer {
  /** Serializes an entity to a plain DTO, with fine-grained control over population, exclusion, groups, and custom types. */
  static serialize(entity, options = {}) {
    const wrapped = helper(entity);
    const meta = wrapped.__meta;
    let contextCreated = false;
    if (!wrapped.__serializationContext.root) {
      const root = new SerializationContext();
      SerializationContext.propagate(
        root,
        entity,
        (meta, prop) => meta.properties[prop]?.kind !== ReferenceKind.SCALAR,
      );
      options.populate = options.populate ? Utils.asArray(options.populate) : options.populate;
      contextCreated = true;
    }
    const root = wrapped.__serializationContext.root;
    const ret = {};
    const props = new Set();
    if (meta.serializedPrimaryKey && !meta.compositePK) {
      props.add(meta.serializedPrimaryKey);
    } else {
      meta.primaryKeys.forEach(pk => props.add(pk));
    }
    if (wrapped.isInitialized() || !wrapped.hasPrimaryKey()) {
      const entityKeys = new Set(Object.keys(entity));
      for (const prop of meta.props) {
        if (entityKeys.has(prop.name) || (prop.getter && prop.accessor === prop.name)) {
          props.add(prop.name);
        }
      }
      for (const key of entityKeys) {
        if (!meta.properties[key]) {
          props.add(key);
        }
      }
    }
    const visited = root.visited.has(entity);
    if (!visited) {
      root.visited.add(entity);
    }
    for (const prop of props) {
      if (!isVisible(meta, prop, options)) {
        continue;
      }
      const cycle = root.visit(meta.class, prop);
      if (cycle && visited) {
        continue;
      }
      const val = this.processProperty(prop, entity, options);
      if (!cycle) {
        root.leave(meta.class, prop);
      }
      if (options.skipNull && Utils.isPlainObject(val)) {
        Utils.dropUndefinedProperties(val, null);
      }
      if (isRaw(val)) {
        throw new Error(`Trying to serialize raw SQL fragment: '${val.sql}'`);
      }
      const visible = typeof val !== 'undefined' && !(val === null && options.skipNull);
      if (visible) {
        ret[this.propertyName(meta, prop)] = val;
      }
    }
    if (contextCreated) {
      root.close();
    }
    if (!wrapped.isInitialized()) {
      return ret;
    }
    for (const prop of meta.getterProps) {
      // decorated get methods
      if (prop.getterName != null) {
        const visible = entity[prop.getterName] instanceof Function && isVisible(meta, prop.name, options);
        if (visible) {
          ret[this.propertyName(meta, prop.name)] = this.processProperty(prop.getterName, entity, options);
        }
      } else {
        // decorated getters
        const visible = typeof entity[prop.name] !== 'undefined' && isVisible(meta, prop.name, options);
        if (visible) {
          ret[this.propertyName(meta, prop.name)] = this.processProperty(prop.name, entity, options);
        }
      }
    }
    return ret;
  }
  static propertyName(meta, prop) {
    /* v8 ignore next */
    if (meta.properties[prop]?.serializedName) {
      return meta.properties[prop].serializedName;
    }
    if (meta.properties[prop]?.primary && meta.serializedPrimaryKey) {
      return meta.serializedPrimaryKey;
    }
    return prop;
  }
  static processProperty(prop, entity, options) {
    const parts = prop.split('.');
    prop = parts[0];
    const wrapped = helper(entity);
    const property = wrapped.__meta.properties[prop] ?? { name: prop };
    const serializer = property?.serializer;
    const value = entity[prop];
    // getter method
    if (entity[prop] instanceof Function) {
      const returnValue = entity[prop]();
      if (!options.ignoreSerializers && serializer) {
        return serializer(returnValue, this.extractChildOptions(options, prop));
      }
      return returnValue;
    }
    /* v8 ignore next */
    if (!options.ignoreSerializers && serializer) {
      return serializer(value);
    }
    if (Utils.isCollection(value)) {
      return this.processCollection(property, entity, options);
    }
    if (Utils.isEntity(value, true)) {
      return this.processEntity(property, entity, wrapped.__platform, options);
    }
    if (Utils.isScalarReference(value)) {
      return value.unwrap();
    }
    /* v8 ignore next */
    if (property?.kind === ReferenceKind.EMBEDDED) {
      if (Array.isArray(value)) {
        return value.map(item => helper(item).toJSON());
      }
      if (Utils.isObject(value)) {
        return helper(value).toJSON();
      }
    }
    if (property.customType) {
      return this.processCustomType(value, property, wrapped.__platform, options.convertCustomTypes);
    }
    return wrapped.__platform.normalizePrimaryKey(value);
  }
  static processCustomType(value, prop, platform, convertCustomTypes) {
    if (!prop.customType) {
      return value;
    }
    if (convertCustomTypes) {
      return prop.customType.convertToDatabaseValue(value, platform, { mode: 'serialization' });
    }
    return prop.customType.toJSON(value, platform);
  }
  static extractChildOptions(options, prop) {
    return {
      ...options,
      populate: Array.isArray(options.populate)
        ? Utils.extractChildElements(options.populate, prop, '*')
        : options.populate,
      exclude: Array.isArray(options.exclude) ? Utils.extractChildElements(options.exclude, prop) : options.exclude,
    };
  }
  static processEntity(prop, entity, platform, options) {
    const child = Reference.unwrapReference(entity[prop.name]);
    const wrapped = helper(child);
    const populated = isPopulated(prop.name, options) && wrapped.isInitialized();
    const expand = populated || !wrapped.__managed;
    const meta = wrapped.__meta;
    const childOptions = this.extractChildOptions(options, prop.name);
    const visible = meta.primaryKeys.filter(prop => isVisible(meta, prop, childOptions));
    if (expand) {
      return this.serialize(child, childOptions);
    }
    const pk = this.processCustomType(wrapped.getPrimaryKey(), prop, wrapped.__platform, options.convertCustomTypes);
    if (options.forceObject || wrapped.__config.get('serialization').forceObject) {
      return Utils.primaryKeyToObject(meta, pk, visible);
    }
    if (Utils.isPlainObject(pk)) {
      const pruned = Utils.primaryKeyToObject(meta, pk, visible);
      if (visible.length === 1) {
        return platform.normalizePrimaryKey(pruned[visible[0]]);
      }
      return pruned;
    }
    return platform.normalizePrimaryKey(pk);
  }
  static processCollection(prop, entity, options) {
    const col = entity[prop.name];
    if (!col.isInitialized()) {
      return undefined;
    }
    return col.getItems(false).map(item => {
      const populated = isPopulated(prop.name, options);
      const wrapped = helper(item);
      if (populated || !wrapped.__managed) {
        return this.serialize(item, this.extractChildOptions(options, prop.name));
      }
      const pk = this.processCustomType(wrapped.getPrimaryKey(), prop, wrapped.__platform, options.convertCustomTypes);
      if (options.forceObject || wrapped.__config.get('serialization').forceObject) {
        return Utils.primaryKeyToObject(wrapped.__meta, pk);
      }
      return pk;
    });
  }
}
/**
 * Converts entity instance to POJO, converting the `Collection`s to arrays and unwrapping the `Reference` wrapper, while respecting the serialization options.
 * This method accepts either a single entity or an array of entities, and returns the corresponding POJO or an array of POJO.
 * To serialize a single entity, you can also use `wrap(entity).serialize()` which handles a single entity only.
 *
 * ```ts
 * const dtos = serialize([user1, user, ...], { exclude: ['id', 'email'], forceObject: true });
 * const [dto2, dto3] = serialize([user2, user3], { exclude: ['id', 'email'], forceObject: true });
 * const dto1 = serialize(user, { exclude: ['id', 'email'], forceObject: true });
 * const dto2 = wrap(user).serialize({ exclude: ['id', 'email'], forceObject: true });
 * ```
 */
export function serialize(entities, options) {
  if (Array.isArray(entities)) {
    return entities.map(e => EntitySerializer.serialize(e, options));
  }
  return EntitySerializer.serialize(entities, options);
}
