import { Reference } from './Reference.js';
import { EntityTransformer } from '../serialization/EntityTransformer.js';
import { EntityAssigner } from './EntityAssigner.js';
import { Utils } from '../utils/Utils.js';
import { ValidationError } from '../errors.js';
import { helper } from './wrap.js';
import { EntitySerializer } from '../serialization/EntitySerializer.js';
import { expandDotPaths } from './utils.js';
/** @internal Wrapper attached to every managed entity, holding ORM state such as initialization flags, identity map references, and change tracking snapshots. */
export class WrappedEntity {
  constructor(entity, hydrator, pkGetter, pkSerializer, pkGetterConverted) {
    this.entity = entity;
    this.hydrator = hydrator;
    this.pkGetter = pkGetter;
    this.pkSerializer = pkSerializer;
    this.pkGetterConverted = pkGetterConverted;
    this.__initialized = true;
    this.__serializationContext = {};
    this.__loadedProperties = new Set();
    this.__data = {};
    this.__processing = false;
  }
  /** Returns whether the entity has been fully loaded from the database. */
  isInitialized() {
    return this.__initialized;
  }
  /** Returns whether the entity is managed by an EntityManager (tracked in the identity map). */
  isManaged() {
    return !!this.__managed;
  }
  /** Marks the entity as populated or not for serialization purposes. */
  populated(populated = true) {
    this.__populated = populated;
  }
  /** Sets the serialization context with populate hints, field selections, and exclusions. */
  setSerializationContext(options) {
    const exclude = options.exclude ?? [];
    const context = this.__serializationContext;
    const populate = expandDotPaths(this.__meta, options.populate);
    context.populate = context.populate ? context.populate.concat(populate) : populate;
    context.exclude = context.exclude ? context.exclude.concat(exclude) : exclude;
    if (context.fields && options.fields) {
      options.fields.forEach(f => context.fields.add(f));
    } else if (options.fields) {
      context.fields = new Set(options.fields);
    } else {
      context.fields = new Set(['*']);
    }
  }
  /** Returns a Reference wrapper for this entity, creating one if it does not already exist. */
  toReference() {
    this.__reference ??= new Reference(this.entity);
    return this.__reference;
  }
  /** Converts the entity to a plain object representation, optionally excluding specified fields. */
  toObject(ignoreFields) {
    return EntityTransformer.toObject(this.entity, ignoreFields);
  }
  /** Serializes the entity with control over which relations and fields to include or exclude. */
  serialize(options) {
    return EntitySerializer.serialize(this.entity, options);
  }
  /** Converts the entity to a plain object, including all properties regardless of serialization rules. */
  toPOJO() {
    return EntityTransformer.toObject(this.entity, [], true);
  }
  /** Serializes the entity using its `toJSON` method (supports `JSON.stringify`). */
  toJSON(...args) {
    // toJSON methods is added to the prototype during discovery to support automatic serialization via JSON.stringify()
    return this.entity.toJSON(...args);
  }
  /** Assigns the given data to this entity, updating its properties and relations. */
  assign(data, options) {
    if ('assign' in this.entity) {
      return this.entity.assign(data, options);
    }
    return EntityAssigner.assign(this.entity, data, options);
  }
  /** Initializes (refreshes) the entity by reloading it from the database. Returns null if not found. */
  async init(options) {
    if (!this.__em) {
      throw ValidationError.entityNotManaged(this.entity);
    }
    return this.__em.findOne(this.entity.constructor, this.entity, {
      ...options,
      refresh: true,
      schema: this.__schema,
    });
  }
  /** Loads the specified relations on this entity. */
  async populate(populate, options = {}) {
    if (!this.__em) {
      throw ValidationError.entityNotManaged(this.entity);
    }
    // @ts-ignore hard to type
    await this.__em.populate(this.entity, populate, options);
    return this.entity;
  }
  /** Returns whether this entity has a primary key value set. */
  hasPrimaryKey() {
    const pk = this.getPrimaryKey();
    return pk != null;
  }
  /** Returns the primary key value, optionally converting custom types to their database representation. */
  getPrimaryKey(convertCustomTypes = false) {
    const prop = this.__meta.getPrimaryProps()[0];
    if (!prop) {
      return null;
    }
    if (this.__pk != null && this.__meta.compositePK) {
      return Utils.getCompositeKeyValue(
        this.__pk,
        this.__meta,
        convertCustomTypes ? 'convertToDatabaseValue' : false,
        this.__platform,
      );
    }
    if (convertCustomTypes && this.__pk != null && prop.customType) {
      return prop.customType.convertToDatabaseValue(this.__pk, this.__platform);
    }
    if (convertCustomTypes) {
      return this.__pk ?? this.pkGetterConverted(this.entity);
    }
    return this.__pk ?? this.pkGetter(this.entity);
  }
  /** Returns all primary key values as an array. Used internally for composite key handling. */
  // TODO: currently used only in `Driver.syncCollection` — candidate for removal
  getPrimaryKeys(convertCustomTypes = false) {
    const pk = this.getPrimaryKey(convertCustomTypes);
    if (pk == null) {
      return null;
    }
    if (this.__meta.compositePK) {
      return this.__meta.primaryKeys.reduce((ret, pk) => {
        const child = this.entity[pk];
        if (Utils.isEntity(child, true)) {
          const childPk = helper(child).getPrimaryKeys(convertCustomTypes);
          ret.push(...childPk);
        } else {
          ret.push(child);
        }
        return ret;
      }, []);
    }
    return [pk];
  }
  /** Returns the database schema this entity belongs to. */
  getSchema() {
    return this.__schema;
  }
  /** Sets the database schema for this entity. */
  setSchema(schema) {
    this.__schema = schema;
  }
  /** Sets the primary key value on the entity. */
  setPrimaryKey(id) {
    this.entity[this.__meta.primaryKeys[0]] = id;
    this.__pk = id;
  }
  /** Returns the primary key serialized as a string suitable for identity map lookups. */
  getSerializedPrimaryKey() {
    return this.pkSerializer(this.entity);
  }
  get __meta() {
    return this.entity.__meta;
  }
  get __platform() {
    return this.entity.__platform;
  }
  get __config() {
    return this.__em?.config ?? this.entity.__config;
  }
  get __primaryKeys() {
    return Utils.getPrimaryKeyValues(this.entity, this.__meta);
  }
  /** @ignore */
  [Symbol.for('nodejs.util.inspect.custom')]() {
    return `[WrappedEntity<${this.__meta.className}>]`;
  }
}
