import { helper, wrap } from '../entity/wrap.js';
import { Utils } from '../utils/Utils.js';
import { ReferenceKind } from '../enums.js';
import { SerializationContext } from './SerializationContext.js';
import { isRaw } from '../utils/RawQueryFragment.js';
function isVisible(meta, propName, ignoreFields = []) {
  const prop = meta.properties[propName];
  const visible = prop && !prop.hidden;
  const prefixed = prop && !prop.primary && !prop.accessor && propName.startsWith('_'); // ignore prefixed properties, if it's not a PK
  return visible && !prefixed && !ignoreFields.includes(propName);
}
/** Converts entity instances to plain objects via `toObject()`, respecting populate hints, hidden fields, and serialization context. */
export class EntityTransformer {
  /** Converts an entity to a plain object, respecting populate hints, hidden fields, and custom serializers. */
  static toObject(entity, ignoreFields = [], raw = false) {
    if (!Array.isArray(ignoreFields)) {
      ignoreFields = [];
    }
    const wrapped = helper(entity);
    let contextCreated = false;
    if (!wrapped) {
      return entity;
    }
    if (!wrapped.__serializationContext.root) {
      const root = new SerializationContext(
        wrapped.__serializationContext.populate,
        wrapped.__serializationContext.fields,
        wrapped.__serializationContext.exclude,
      );
      SerializationContext.propagate(root, entity, isVisible);
      contextCreated = true;
    }
    const root = wrapped.__serializationContext.root;
    const meta = wrapped.__meta;
    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);
    const includePrimaryKeys = wrapped.__config.get('serialization').includePrimaryKeys;
    if (!visited) {
      root.visited.add(entity);
    }
    for (const prop of props) {
      const visible = raw ? meta.properties[prop] : isVisible(meta, prop, ignoreFields);
      if (!visible) {
        continue;
      }
      const populated = root.isMarkedAsPopulated(meta.class, prop);
      if (!raw) {
        const partiallyLoaded = root.isPartiallyLoaded(meta.class, prop);
        const isPrimary = includePrimaryKeys && meta.properties[prop].primary;
        if (!partiallyLoaded && !populated && !isPrimary) {
          continue;
        }
        if (root.isExcluded(meta.class, prop) && !populated) {
          continue;
        }
      }
      const cycle = root.visit(meta.class, prop);
      if (cycle && visited) {
        continue;
      }
      const val = EntityTransformer.processProperty(prop, entity, raw, populated);
      if (!cycle) {
        root.leave(meta.class, prop);
      }
      if (isRaw(val)) {
        throw new Error(`Trying to serialize raw SQL fragment: '${val.sql}'`);
      }
      if (typeof val === 'undefined') {
        continue;
      }
      ret[this.propertyName(meta, prop, raw)] = val;
    }
    if (!wrapped.isInitialized() && wrapped.hasPrimaryKey()) {
      return ret;
    }
    for (const prop of meta.getterProps) {
      // decorated get methods
      if (prop.getterName != null) {
        const visible = !prop.hidden && entity[prop.getterName] instanceof Function;
        const populated = root.isMarkedAsPopulated(meta.class, prop.name);
        if (visible) {
          ret[this.propertyName(meta, prop.name, raw)] = this.processProperty(prop.getterName, entity, raw, populated);
        }
      } else {
        // decorated getters
        const visible = !prop.hidden && typeof entity[prop.name] !== 'undefined';
        const populated = root.isMarkedAsPopulated(meta.class, prop.name);
        if (visible) {
          ret[this.propertyName(meta, prop.name, raw)] = this.processProperty(prop.name, entity, raw, populated);
        }
      }
    }
    if (contextCreated) {
      root.close();
    }
    return ret;
  }
  static propertyName(meta, prop, raw) {
    if (raw) {
      return prop;
    }
    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, raw, populated) {
    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 (serializer && !raw) {
        return serializer(returnValue);
      }
      return returnValue;
    }
    if (serializer && !raw) {
      return serializer(value);
    }
    if (Utils.isCollection(value)) {
      return EntityTransformer.processCollection(property, entity, raw, populated);
    }
    if (Utils.isEntity(value, true)) {
      return EntityTransformer.processEntity(property, entity, wrapped.__platform, raw, populated);
    }
    if (Utils.isScalarReference(value)) {
      return value.unwrap();
    }
    if (property.kind === ReferenceKind.EMBEDDED) {
      if (Array.isArray(value)) {
        return value.map(item => {
          const wrapped = item && helper(item);
          return wrapped ? wrapped.toJSON() : item;
        });
      }
      const wrapped = value && helper(value);
      return wrapped ? wrapped.toJSON() : value;
    }
    const customType = property?.customType;
    if (customType) {
      return customType.toJSON(value, wrapped.__platform);
    }
    if (property?.primary) {
      return wrapped.__platform.normalizePrimaryKey(value);
    }
    return value;
  }
  static processEntity(prop, entity, platform, raw, populated) {
    const child = entity[prop.name];
    const wrapped = helper(child);
    const meta = wrapped.__meta;
    const visible = meta.primaryKeys.filter(prop => isVisible(meta, prop));
    if (raw && wrapped.isInitialized() && child !== entity) {
      return wrapped.toPOJO();
    }
    function isPopulated() {
      if (wrapped.__populated != null) {
        return wrapped.__populated;
      }
      if (populated) {
        return true;
      }
      return !wrapped.__managed;
    }
    if (wrapped.isInitialized() && isPopulated() && child !== entity) {
      return wrap(child).toJSON();
    }
    let pk = wrapped.getPrimaryKey();
    if (prop.customType) {
      pk = prop.customType.toJSON(pk, wrapped.__platform);
    }
    if (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, raw, populated) {
    const col = entity[prop.name];
    if (raw && col.isInitialized(true)) {
      return col.map(item => helper(item).toPOJO());
    }
    if (col.shouldPopulate(populated)) {
      return col.toArray();
    }
    if (col.isInitialized()) {
      const wrapped = helper(entity);
      const forceObject = wrapped.__config.get('serialization').forceObject;
      return col.map(item => {
        const wrapped = helper(item);
        const pk = wrapped.getPrimaryKey();
        if (prop.customType) {
          return prop.customType.toJSON(pk, wrapped.__platform);
        }
        if (forceObject) {
          return Utils.primaryKeyToObject(wrapped.__meta, pk);
        }
        return pk;
      });
    }
    return undefined;
  }
}
