import { NullCacheAdapter } from '../cache/NullCacheAdapter.js';
import { ObjectHydrator } from '../hydration/ObjectHydrator.js';
import { NullHighlighter } from '../utils/NullHighlighter.js';
import { DefaultLogger } from '../logging/DefaultLogger.js';
import { colors } from '../logging/colors.js';
import { Utils } from '../utils/Utils.js';
import { MetadataProvider } from '../metadata/MetadataProvider.js';
import { NotFoundError } from '../errors.js';
import { RequestContext } from './RequestContext.js';
import { DataloaderType, FlushMode, LoadStrategy, PopulateHint } from '../enums.js';
import { MemoryCacheAdapter } from '../cache/MemoryCacheAdapter.js';
import { EntityComparator } from './EntityComparator.js';
import { setEnv } from './env-vars.js';
const DEFAULTS = {
  pool: {},
  entities: [],
  entitiesTs: [],
  extensions: [],
  subscribers: [],
  filters: {},
  discovery: {
    warnWhenNoEntities: true,
    checkDuplicateTableNames: true,
    checkDuplicateFieldNames: true,
    checkDuplicateEntities: true,
    checkNonPersistentCompositeProps: true,
    inferDefaultValues: true,
  },
  validateRequired: true,
  context: name => RequestContext.getEntityManager(name),
  contextName: 'default',
  allowGlobalContext: false,
  // eslint-disable-next-line no-console
  logger: console.log.bind(console),
  colors: true,
  findOneOrFailHandler: (entityName, where) => NotFoundError.findOneFailed(entityName, where),
  findExactlyOneOrFailHandler: (entityName, where) => NotFoundError.findExactlyOneFailed(entityName, where),
  baseDir: globalThis.process?.cwd?.(),
  hydrator: ObjectHydrator,
  flushMode: FlushMode.AUTO,
  loadStrategy: LoadStrategy.BALANCED,
  dataloader: DataloaderType.NONE,
  populateWhere: PopulateHint.ALL,
  ignoreUndefinedInQuery: false,
  onQuery: sql => sql,
  autoJoinOneToOneOwner: true,
  autoJoinRefsForFilters: true,
  filtersOnRelations: true,
  propagationOnPrototype: true,
  populateAfterFlush: true,
  serialization: {
    includePrimaryKeys: true,
  },
  assign: {
    updateNestedEntities: true,
    updateByPrimaryKey: true,
    mergeObjectProperties: false,
    mergeEmbeddedProperties: true,
    ignoreUndefined: false,
  },
  persistOnCreate: true,
  upsertManaged: true,
  forceEntityConstructor: false,
  forceUndefined: false,
  forceUtcTimezone: true,
  processOnCreateHooksEarly: true,
  ensureDatabase: true,
  ensureIndexes: false,
  batchSize: 300,
  debug: false,
  ignoreDeprecations: false,
  verbose: false,
  driverOptions: {},
  migrations: {
    tableName: 'mikro_orm_migrations',
    glob: '!(*.d).{js,ts,cjs}',
    silent: false,
    transactional: true,
    allOrNothing: true,
    dropTables: true,
    safe: false,
    snapshot: true,
    emit: 'ts',
    fileName: (timestamp, name) => `Migration${timestamp}${name ? '_' + name : ''}`,
  },
  schemaGenerator: {
    createForeignKeyConstraints: true,
    ignoreSchema: [],
    skipTables: [],
    skipViews: [],
    skipColumns: {},
  },
  embeddables: {
    prefixMode: 'relative',
  },
  entityGenerator: {
    forceUndefined: true,
    undefinedDefaults: false,
    scalarTypeInDecorator: false,
    bidirectionalRelations: true,
    identifiedReferences: true,
    scalarPropertiesForRelations: 'never',
    entityDefinition: 'defineEntity',
    decorators: 'legacy',
    enumMode: 'dictionary',
    /* v8 ignore next */
    fileName: className => className,
    onlyPurePivotTables: false,
    outputPurePivotTables: false,
    readOnlyPivotTables: false,
    useCoreBaseEntity: false,
  },
  metadataCache: {},
  resultCache: {
    adapter: MemoryCacheAdapter,
    expiration: 1000, // 1s
    options: {},
  },
  metadataProvider: MetadataProvider,
  highlighter: new NullHighlighter(),
  seeder: {
    defaultSeeder: 'DatabaseSeeder',
    glob: '!(*.d).{js,ts}',
    emit: 'ts',
    fileName: className => className,
  },
  preferReadReplicas: true,
  dynamicImportProvider: /* v8 ignore next */ id => import(id),
};
/** Holds and validates all ORM configuration options, providing access to drivers, loggers, cache adapters, and other services. */
export class Configuration {
  #options;
  #logger;
  #slowQueryLogger;
  #driver;
  #platform;
  #cache = new Map();
  #extensions = new Map();
  constructor(options, validate = true) {
    if (options.dynamicImportProvider) {
      globalThis.dynamicImportProvider = options.dynamicImportProvider;
    }
    this.#options = Utils.mergeConfig({}, DEFAULTS, options);
    if (validate) {
      this.validateOptions();
    }
    this.#options.loggerFactory ??= DefaultLogger.create;
    this.#logger = this.#options.loggerFactory({
      debugMode: this.#options.debug,
      ignoreDeprecations: this.#options.ignoreDeprecations,
      usesReplicas: (this.#options.replicas?.length ?? 0) > 0,
      highlighter: this.#options.highlighter,
      writer: this.#options.logger,
    });
    const cf = this.#options.compiledFunctions;
    if (cf && cf.__version !== Utils.getORMVersion()) {
      this.#logger.warn(
        'discovery',
        `Compiled functions were generated with MikroORM v${cf.__version ?? 'unknown'}, but the current version is v${Utils.getORMVersion()}. Please regenerate with \`npx mikro-orm compile\`.`,
      );
    }
    if (this.#options.driver) {
      this.#driver = new this.#options.driver(this);
      this.#platform = this.#driver.getPlatform();
      this.#platform.setConfig(this);
      this.init(validate);
    }
  }
  /** Returns the database platform instance. */
  getPlatform() {
    return this.#platform;
  }
  /**
   * Gets specific configuration option. Falls back to specified `defaultValue` if provided.
   */
  get(key, defaultValue) {
    if (typeof this.#options[key] !== 'undefined') {
      return this.#options[key];
    }
    return defaultValue;
  }
  /** Returns all configuration options. */
  getAll() {
    return this.#options;
  }
  /**
   * Overrides specified configuration value.
   */
  set(key, value) {
    this.#options[key] = value;
    this.sync();
  }
  /**
   * Resets the configuration to its default value
   */
  reset(key) {
    this.#options[key] = DEFAULTS[key];
  }
  /**
   * Gets Logger instance.
   */
  getLogger() {
    return this.#logger;
  }
  /**
   * Gets the logger instance for slow queries.
   * Falls back to the main logger if no custom slow query logger factory is configured.
   */
  getSlowQueryLogger() {
    this.#slowQueryLogger ??=
      this.#options.slowQueryLoggerFactory?.({
        debugMode: this.#options.debug,
        writer: this.#options.logger,
        highlighter: this.#options.highlighter,
        usesReplicas: (this.#options.replicas?.length ?? 0) > 0,
      }) ?? this.#logger;
    return this.#slowQueryLogger;
  }
  /** Returns the configured dataloader type, normalizing boolean values. */
  getDataloaderType() {
    if (typeof this.#options.dataloader === 'boolean') {
      return this.#options.dataloader ? DataloaderType.ALL : DataloaderType.NONE;
    }
    return this.#options.dataloader;
  }
  /** Returns the configured schema name, optionally skipping the platform's default schema. */
  getSchema(skipDefaultSchema = false) {
    if (skipDefaultSchema && this.#options.schema === this.#platform.getDefaultSchemaName()) {
      return undefined;
    }
    return this.#options.schema;
  }
  /**
   * Gets current database driver instance.
   */
  getDriver() {
    return this.#driver;
  }
  /** Registers a lazily-initialized extension by name. */
  registerExtension(name, cb) {
    this.#extensions.set(name, cb);
  }
  /** Returns a previously registered extension by name, initializing it on first access. */
  getExtension(name) {
    if (this.#cache.has(name)) {
      return this.#cache.get(name);
    }
    const ext = this.#extensions.get(name);
    /* v8 ignore next */
    if (!ext) {
      return undefined;
    }
    this.#cache.set(name, ext());
    return this.#cache.get(name);
  }
  /**
   * Gets instance of NamingStrategy. (cached)
   */
  getNamingStrategy() {
    return this.getCachedService(this.#options.namingStrategy || this.#platform.getNamingStrategy());
  }
  /**
   * Gets instance of Hydrator. (cached)
   */
  getHydrator(metadata) {
    return this.getCachedService(this.#options.hydrator, metadata, this.#platform, this);
  }
  /**
   * Gets instance of Comparator. (cached)
   */
  getComparator(metadata) {
    return this.getCachedService(EntityComparator, metadata, this.#platform, this);
  }
  /**
   * Gets instance of MetadataProvider. (cached)
   */
  getMetadataProvider() {
    return this.getCachedService(this.#options.metadataProvider, this);
  }
  /**
   * Gets instance of metadata CacheAdapter. (cached)
   */
  getMetadataCacheAdapter() {
    return this.getCachedService(
      this.#options.metadataCache.adapter,
      this.#options.metadataCache.options,
      this.#options.baseDir,
      this.#options.metadataCache.pretty,
    );
  }
  /**
   * Gets instance of CacheAdapter for result cache. (cached)
   */
  getResultCacheAdapter() {
    return this.getCachedService(this.#options.resultCache.adapter, {
      expiration: this.#options.resultCache.expiration,
      ...this.#options.resultCache.options,
    });
  }
  /**
   * Gets EntityRepository class to be instantiated.
   */
  getRepositoryClass(repository) {
    if (repository) {
      return repository();
    }
    if (this.#options.entityRepository) {
      return this.#options.entityRepository;
    }
    return this.#platform.getRepositoryClass();
  }
  /**
   * Creates instance of given service and caches it.
   */
  getCachedService(cls, ...args) {
    if (!this.#cache.has(cls.name)) {
      this.#cache.set(cls.name, new cls(...args));
    }
    return this.#cache.get(cls.name);
  }
  /** Clears the cached service instances, forcing re-creation on next access. */
  resetServiceCache() {
    this.#cache.clear();
  }
  init(validate) {
    const useCache = this.getMetadataProvider().useCache();
    const metadataCache = this.#options.metadataCache;
    if (!useCache) {
      metadataCache.adapter = NullCacheAdapter;
    }
    metadataCache.enabled ??= useCache;
    this.#options.clientUrl ??= this.#platform.getDefaultClientUrl();
    this.#options.implicitTransactions ??= this.#platform.usesImplicitTransactions();
    if (validate && metadataCache.enabled && !metadataCache.adapter) {
      throw new Error(
        'No metadata cache adapter specified, please fill in `metadataCache.adapter` option or use the async MikroORM.init() method which can autoload it.',
      );
    }
    try {
      const url = new URL(this.#options.clientUrl);
      if (url.pathname) {
        this.#options.dbName = this.get('dbName', decodeURIComponent(url.pathname).substring(1));
      }
    } catch {
      const url = /:\/\/.*\/([^?]+)/.exec(this.#options.clientUrl);
      if (url) {
        this.#options.dbName = this.get('dbName', decodeURIComponent(url[1]));
      }
    }
    if (validate && !this.#options.dbName && this.#options.clientUrl) {
      throw new Error("No database specified, `clientUrl` option provided but it's missing the pathname.");
    }
    this.#options.schema ??= this.#platform.getDefaultSchemaName();
    this.#options.charset ??= this.#platform.getDefaultCharset();
    Object.keys(this.#options.filters).forEach(key => {
      this.#options.filters[key].default ??= true;
    });
    if (!this.#options.filtersOnRelations) {
      this.#options.autoJoinRefsForFilters ??= false;
    }
    this.#options.subscribers = [...this.#options.subscribers].map(subscriber => {
      return subscriber.constructor.name === 'Function' ? new subscriber() : subscriber;
    });
    this.sync();
    if (!colors.enabled()) {
      this.#options.highlighter = new NullHighlighter();
    }
  }
  sync() {
    setEnv('MIKRO_ORM_COLORS', this.#options.colors);
    this.#logger.setDebugMode(this.#options.debug);
    this.#slowQueryLogger = undefined;
  }
  validateOptions() {
    /* v8 ignore next */
    if ('type' in this.#options) {
      throw new Error(
        "The `type` option has been removed in v6, please fill in the `driver` option instead or use `defineConfig` helper (to define your ORM config) or `MikroORM` class (to call the `init` method) exported from the driver package (e.g. `import { defineConfig } from '@mikro-orm/mysql'; export default defineConfig({ ... })`).",
      );
    }
    if (!this.#options.driver) {
      throw new Error(
        "No driver specified, please fill in the `driver` option or use `defineConfig` helper (to define your ORM config) or `MikroORM` class (to call the `init` method) exported from the driver package (e.g. `import { defineConfig } from '@mikro-orm/mysql'; export defineConfig({ ... })`).",
      );
    }
    if (!this.#options.dbName && !this.#options.clientUrl) {
      throw new Error('No database specified, please fill in `dbName` or `clientUrl` option');
    }
    if (this.#options.entities.length === 0 && this.#options.discovery.warnWhenNoEntities) {
      throw new Error('No entities found, please use `entities` option');
    }
    if (
      typeof this.#options.driverOptions === 'function' &&
      this.#options.driverOptions.constructor.name === 'AsyncFunction'
    ) {
      throw new Error('`driverOptions` callback cannot be async');
    }
  }
}
/**
 * Type helper to make it easier to use `mikro-orm.config.js`.
 */
export function defineConfig(options) {
  return options;
}
