import { DataloaderType } from '../enums.js';
import { helper, wrap } from './wrap.js';
import { Utils } from '../utils/Utils.js';
import { QueryHelper } from '../utils/QueryHelper.js';
import { NotFoundError } from '../errors.js';
import { inspect } from '../logging/inspect.js';
/** Wrapper around an entity that provides lazy loading capabilities and identity-preserving reference semantics. */
export class Reference {
  entity;
  property;
  constructor(entity) {
    this.entity = entity;
    this.set(entity);
    const meta = helper(this.entity).__meta;
    meta.primaryKeys.forEach(primaryKey => {
      Object.defineProperty(this, primaryKey, {
        get() {
          return this.entity[primaryKey];
        },
      });
    });
    if (meta.serializedPrimaryKey && meta.primaryKeys[0] !== meta.serializedPrimaryKey) {
      Object.defineProperty(this, meta.serializedPrimaryKey, {
        get() {
          return helper(this.entity).getSerializedPrimaryKey();
        },
      });
    }
  }
  /** Creates a Reference wrapper for the given entity, preserving identity if one already exists. */
  static create(entity) {
    const unwrapped = Reference.unwrapReference(entity);
    const ref = helper(entity).toReference();
    if (unwrapped !== ref.unwrap()) {
      ref.set(unwrapped);
    }
    return ref;
  }
  /** Creates a Reference wrapper for an entity identified by its primary key, wrapped in a Ref. */
  static createFromPK(entityType, pk, options) {
    const ref = this.createNakedFromPK(entityType, pk, options);
    return helper(ref)?.toReference() ?? ref;
  }
  /** Creates an uninitialized entity reference by primary key without wrapping it in a Reference. */
  static createNakedFromPK(entityType, pk, options) {
    const factory = entityType.prototype.__factory;
    if (!factory) {
      // this can happen only if `ref()` is used as a property initializer, and the value is important only for the
      // inference of defaults, so it's fine to return it directly without wrapping with `Reference` class
      return pk;
    }
    const entity = factory.createReference(entityType, pk, {
      merge: false,
      convertCustomTypes: false,
      ...options,
    });
    const wrapped = helper(entity);
    wrapped.__meta.primaryKeys.forEach(key => wrapped.__loadedProperties.add(key));
    wrapped.__originalEntityData = factory.getComparator().prepareEntity(entity);
    return entity;
  }
  /**
   * Checks whether the argument is instance of `Reference` wrapper.
   */
  static isReference(data) {
    return data && !!data.__reference;
  }
  /**
   * Wraps the entity in a `Reference` wrapper if the property is defined as `ref`.
   */
  static wrapReference(entity, prop) {
    if (entity && prop.ref && !Reference.isReference(entity)) {
      const ref = Reference.create(entity);
      ref.property = prop;
      return ref;
    }
    return entity;
  }
  /**
   * Returns wrapped entity.
   */
  static unwrapReference(ref) {
    return Reference.isReference(ref) ? ref.unwrap() : ref;
  }
  /**
   * Ensures the underlying entity is loaded first (without reloading it if it already is loaded). Returns the entity.
   * If the entity is not found in the database (e.g. it was deleted in the meantime, or currently active filters disallow loading of it)
   * the method returns `null`. Use `loadOrFail()` if you want an error to be thrown in such a case.
   */
  async load(options = {}) {
    const wrapped = helper(this.entity);
    if (!wrapped.__em) {
      return this.entity;
    }
    options = { ...options, filters: QueryHelper.mergePropertyFilters(this.property?.filters, options.filters) };
    if (this.isInitialized() && !options.refresh && options.populate) {
      await wrapped.__em.populate(this.entity, options.populate, options);
    }
    if (!this.isInitialized() || options.refresh) {
      if (
        options.dataloader ??
        [DataloaderType.ALL, DataloaderType.REFERENCE].includes(wrapped.__em.config.getDataloaderType())
      ) {
        const dataLoader = await wrapped.__em.getDataLoader('ref');
        return dataLoader.load([this, options]);
      }
      return wrapped.init(options);
    }
    return this.entity;
  }
  /**
   * Ensures the underlying entity is loaded first (without reloading it if it already is loaded).
   * Returns the entity or throws an error just like `em.findOneOrFail()` (and respects the same config options).
   */
  async loadOrFail(options = {}) {
    const ret = await this.load(options);
    if (!ret) {
      const wrapped = helper(this.entity);
      options.failHandler ??= wrapped.__em.config.get('findOneOrFailHandler');
      const entityName = this.entity.constructor.name;
      const where = wrapped.getPrimaryKey();
      throw options.failHandler(entityName, where);
    }
    return ret;
  }
  set(entity) {
    this.entity = Reference.unwrapReference(entity);
    delete helper(this.entity).__reference;
  }
  /** Returns the underlying entity without checking initialization state. */
  unwrap() {
    return this.entity;
  }
  /** Returns the underlying entity, throwing an error if the reference is not initialized. */
  getEntity() {
    if (!this.isInitialized()) {
      throw new Error(
        `Reference<${helper(this.entity).__meta.name}> ${helper(this.entity).getPrimaryKey()} not initialized`,
      );
    }
    return this.entity;
  }
  /** Returns the value of a property on the underlying entity. Throws if the reference is not initialized. */
  getProperty(prop) {
    return this.getEntity()[prop];
  }
  /** Loads the entity if needed, then returns the value of the specified property. */
  async loadProperty(prop, options) {
    await this.loadOrFail(options);
    return this.getEntity()[prop];
  }
  /** Returns whether the underlying entity has been fully loaded from the database. */
  isInitialized() {
    return helper(this.entity).__initialized;
  }
  /** Marks the underlying entity as populated or not for serialization purposes. */
  populated(populated) {
    helper(this.entity).populated(populated);
  }
  /** Serializes the underlying entity to a plain JSON object. */
  toJSON(...args) {
    return wrap(this.entity).toJSON(...args);
  }
  /** @ignore */
  [Symbol.for('nodejs.util.inspect.custom')](depth = 2) {
    const object = { ...this };
    const hidden = ['meta', 'property'];
    hidden.forEach(k => delete object[k]);
    const ret = inspect(object, { depth });
    const wrapped = helper(this.entity);
    const meta = wrapped.__meta;
    /* v8 ignore next */
    const pk = wrapped.hasPrimaryKey() ? '<' + wrapped.getSerializedPrimaryKey() + '>' : '';
    const name = `Ref<${meta.className}${pk}>`;
    return ret === '[Object]' ? `[${name}]` : name + ' ' + ret;
  }
}
/** Wrapper for lazy scalar properties that provides on-demand loading from the database. */
export class ScalarReference {
  value;
  entity;
  #property;
  #initialized;
  constructor(value, initialized = value != null) {
    this.value = value;
    this.#initialized = initialized;
  }
  /**
   * Ensures the underlying entity is loaded first (without reloading it if it already is loaded).
   * Returns either the whole entity, or the requested property.
   */
  async load(options) {
    const opts = typeof options === 'object' ? options : { prop: options };
    if (!this.#initialized || opts.refresh) {
      if (this.entity == null || this.#property == null) {
        throw new Error('Cannot load scalar reference that is not bound to an entity property.');
      }
      await helper(this.entity).populate([this.#property], opts);
    }
    return this.value;
  }
  /**
   * Ensures the underlying entity is loaded first (without reloading it if it already is loaded).
   * Returns the entity or throws an error just like `em.findOneOrFail()` (and respects the same config options).
   */
  async loadOrFail(options = {}) {
    const ret = await this.load(options);
    if (ret == null) {
      const wrapped = helper(this.entity);
      options.failHandler ??= wrapped.__em.config.get('findOneOrFailHandler');
      const entityName = this.entity.constructor.name;
      throw NotFoundError.failedToLoadProperty(entityName, this.#property, wrapped.getPrimaryKey());
    }
    return ret;
  }
  /** Sets the scalar value and marks the reference as initialized. */
  set(value) {
    this.value = value;
    this.#initialized = true;
  }
  /** Binds this scalar reference to a specific entity and property for lazy loading support. */
  bind(entity, property) {
    this.entity = entity;
    this.#property = property;
    Object.defineProperty(this, 'entity', { enumerable: false, value: entity });
  }
  /** Returns the current scalar value, or undefined if not yet loaded. */
  unwrap() {
    return this.value;
  }
  /** Returns whether the scalar value has been loaded. */
  isInitialized() {
    return this.#initialized;
  }
  /** @ignore */
  /* v8 ignore next */
  [Symbol.for('nodejs.util.inspect.custom')]() {
    return this.#initialized ? `Ref<${inspect(this.value)}>` : `Ref<?>`;
  }
}
Object.defineProperties(Reference.prototype, {
  __reference: { value: true, enumerable: false },
  __meta: {
    get() {
      return this.entity.__meta;
    },
  },
  __platform: {
    get() {
      return this.entity.__platform;
    },
  },
  __helper: {
    get() {
      return this.entity.__helper;
    },
  },
  $: {
    get() {
      return this.entity;
    },
  },
  get: {
    get() {
      return () => this.entity;
    },
  },
});
Object.defineProperties(ScalarReference.prototype, {
  __scalarReference: { value: true, enumerable: false },
  $: {
    get() {
      return this.value;
    },
  },
  get: {
    get() {
      return () => this.value;
    },
  },
});
/**
 * shortcut for `wrap(entity).toReference()`
 */
export function ref(entityOrType, pk) {
  if (entityOrType == null) {
    return entityOrType;
  }
  if (Utils.isEntity(entityOrType, true)) {
    return helper(entityOrType).toReference();
  }
  if (Utils.isEntity(pk, true)) {
    return helper(pk).toReference();
  }
  if (arguments.length === 1) {
    return new ScalarReference(entityOrType, true);
  }
  if (pk == null) {
    return pk;
  }
  return Reference.createFromPK(entityOrType, pk);
}
/**
 * shortcut for `Reference.createNakedFromPK(entityType, pk)`
 */
export function rel(entityType, pk) {
  if (pk == null || Utils.isEntity(pk)) {
    return pk;
  }
  return Reference.createNakedFromPK(entityType, pk);
}
export { Reference as Ref };
