import { Utils } from './Utils.js';
export class AbstractMigrator {
  em;
  runner;
  storage;
  generator;
  driver;
  config;
  options;
  absolutePath;
  initialized = false;
  #listeners = new Map();
  constructor(em) {
    this.em = em;
    this.driver = this.em.getDriver();
    this.config = this.em.config;
    this.options = this.config.get('migrations');
    this.initServices();
    this.registerDefaultListeners();
  }
  /**
   * @inheritDoc
   */
  on(eventName, listener) {
    if (!this.#listeners.has(eventName)) {
      this.#listeners.set(eventName, new Set());
    }
    this.#listeners.get(eventName).add(listener);
    return this;
  }
  /**
   * @inheritDoc
   */
  off(eventName, listener) {
    this.#listeners.get(eventName)?.delete(listener);
    return this;
  }
  /**
   * @inheritDoc
   */
  async getExecuted() {
    await this.init();
    return this.storage.getExecutedMigrations();
  }
  /**
   * @inheritDoc
   */
  async getPending() {
    await this.init();
    const all = await this.discoverMigrations();
    const executed = new Set(await this.storage.executed());
    return all.filter(m => !executed.has(m.name)).map(m => ({ name: m.name, path: m.path }));
  }
  /**
   * @inheritDoc
   */
  async up(options) {
    return this.runMigrations('up', options);
  }
  /**
   * @inheritDoc
   */
  async down(options) {
    return this.runMigrations('down', options);
  }
  async init() {
    if (this.initialized) {
      return;
    }
    this.initialized = true;
    if (!this.options.migrationsList) {
      const { fs } = await import('@mikro-orm/core/fs-utils');
      this.detectSourceFolder(fs);
      /* v8 ignore next */
      const key =
        this.config.get('preferTs', Utils.detectTypeScriptSupport()) && this.options.pathTs ? 'pathTs' : 'path';
      this.absolutePath = fs.absolutePath(this.options[key], this.config.get('baseDir'));
      fs.ensureDir(this.absolutePath);
    }
  }
  initServices() {
    this.runner = this.createRunner();
    this.storage = this.createStorage();
    if (this.options.generator) {
      this.generator = new this.options.generator(this.driver, this.config.getNamingStrategy(), this.options);
    } else {
      this.generator = this.getDefaultGenerator();
    }
  }
  resolve(params) {
    const createMigrationHandler = async method => {
      const { fs } = await import('@mikro-orm/core/fs-utils');
      const migration = await fs.dynamicImport(params.path);
      const MigrationClass = Object.values(migration).find(
        cls => typeof cls === 'function' && typeof cls.constructor === 'function',
      );
      const instance = new MigrationClass(this.driver, this.config);
      await this.runner.run(instance, method);
    };
    return {
      name: this.storage.getMigrationName(params.name),
      path: params.path,
      up: () => createMigrationHandler('up'),
      down: () => createMigrationHandler('down'),
    };
  }
  initialize(MigrationClass, name) {
    const instance = new MigrationClass(this.driver, this.config);
    return {
      name: this.storage.getMigrationName(name),
      up: () => this.runner.run(instance, 'up'),
      down: () => this.runner.run(instance, 'down'),
    };
  }
  /**
   * Checks if `src` folder exists, it so, tries to adjust the migrations and seeders paths automatically to use it.
   * If there is a `dist` or `build` folder, it will be used for the JS variant (`path` option), while the `src` folder will be
   * used for the TS variant (`pathTs` option).
   *
   * If the default folder exists (e.g. `/migrations`), the config will respect that, so this auto-detection should not
   * break existing projects, only help with the new ones.
   */
  detectSourceFolder(fs) {
    const baseDir = this.config.get('baseDir');
    const defaultPath = './migrations';
    if (!fs.pathExists(baseDir + '/src')) {
      this.options.path ??= defaultPath;
      return;
    }
    const exists = fs.pathExists(`${baseDir}/${defaultPath}`);
    const distDir = fs.pathExists(baseDir + '/dist');
    const buildDir = fs.pathExists(baseDir + '/build');
    // if neither `dist` nor `build` exist, we use the `src` folder as it might be a JS project without building, but with `src` folder
    /* v8 ignore next */
    const path = distDir ? './dist' : buildDir ? './build' : './src';
    // only if the user did not provide any values and if the default path does not exist
    if (!this.options.path && !this.options.pathTs && !exists) {
      this.options.path = `${path}/migrations`;
      this.options.pathTs = './src/migrations';
    }
  }
  registerDefaultListeners() {
    /* v8 ignore else */
    if (!this.options.silent) {
      const logger = this.config.getLogger();
      this.on('migrating', event => logger.log('migrator', `Processing '${event.name}'`, { enabled: true }));
      this.on('migrated', event => logger.log('migrator', `Applied '${event.name}'`, { enabled: true }));
      this.on('reverting', event => logger.log('migrator', `Processing '${event.name}'`, { enabled: true }));
      this.on('reverted', event => logger.log('migrator', `Reverted '${event.name}'`, { enabled: true }));
    }
  }
  async emit(event, data) {
    for (const listener of this.#listeners.get(event) ?? []) {
      await listener(data);
    }
  }
  async discoverMigrations() {
    if (this.options.migrationsList) {
      return this.options.migrationsList.map(migration => {
        if (typeof migration === 'function') {
          return this.initialize(migration, migration.name);
        }
        return this.initialize(migration.class, migration.name);
      });
    }
    const { fs } = await import('@mikro-orm/core/fs-utils');
    const pattern = fs.normalizePath(this.absolutePath, this.options.glob);
    const files = fs.glob(pattern).sort();
    return files.map(filePath =>
      this.resolve({
        name: filePath.replace(/\\/g, '/').split('/').pop(),
        path: filePath,
      }),
    );
  }
  async executeMigrations(method, options = {}) {
    const all = await this.discoverMigrations();
    const executed = await this.storage.executed();
    const executedSet = new Set(executed);
    let toRun;
    if (method === 'up') {
      toRun = this.filterUp(all, executedSet, options);
    } else {
      toRun = this.filterDown(all, executed, options);
    }
    const result = [];
    const eventBefore = method === 'up' ? 'migrating' : 'reverting';
    const eventAfter = method === 'up' ? 'migrated' : 'reverted';
    for (const migration of toRun) {
      const event = { name: migration.name, path: migration.path };
      await this.emit(eventBefore, event);
      await migration[method]();
      if (method === 'up') {
        await this.storage.logMigration({ name: migration.name });
      } else {
        await this.storage.unlogMigration({ name: migration.name });
      }
      await this.emit(eventAfter, event);
      result.push(event);
    }
    return result;
  }
  filterUp(all, executed, options) {
    let pending = all.filter(m => !executed.has(m.name));
    if (options.migrations) {
      const set = new Set(options.migrations);
      return pending.filter(m => set.has(m.name));
    }
    if (options.from) {
      const idx = all.findIndex(m => m.name === options.from);
      if (idx >= 0) {
        const names = new Set(all.slice(idx + 1).map(m => m.name));
        pending = pending.filter(m => names.has(m.name));
      }
    }
    if (options.to && typeof options.to === 'string') {
      const idx = all.findIndex(m => m.name === options.to);
      if (idx >= 0) {
        const names = new Set(all.slice(0, idx + 1).map(m => m.name));
        pending = pending.filter(m => names.has(m.name));
      }
    }
    return pending;
  }
  filterDown(all, executed, options) {
    const migrationMap = new Map(all.map(m => [m.name, m]));
    const executedReversed = [...executed].reverse();
    if (options.migrations) {
      const set = new Set(options.migrations);
      return executedReversed
        .filter(name => set.has(name))
        .map(name => migrationMap.get(name))
        .filter(Boolean);
    }
    if (options.to === 0) {
      return executedReversed.map(name => migrationMap.get(name)).filter(Boolean);
    }
    if (options.to) {
      const result = [];
      for (const name of executedReversed) {
        if (name === String(options.to)) {
          break;
        }
        const m = migrationMap.get(name);
        if (m) {
          result.push(m);
        }
      }
      return result;
    }
    // Default: revert last 1
    if (executedReversed.length > 0) {
      const m = migrationMap.get(executedReversed[0]);
      return m ? [m] : [];
    }
    return [];
  }
  getMigrationFilename(name) {
    name = name.replace(/\.[jt]s$/, '');
    return /^\d{14}$/.exec(name) ? this.options.fileName(name) : name;
  }
  prefix(options) {
    if (typeof options === 'string' || Array.isArray(options)) {
      return { migrations: Utils.asArray(options).map(name => this.getMigrationFilename(name)) };
    }
    if (!options) {
      return {};
    }
    const result = {};
    if (options.migrations) {
      result.migrations = options.migrations.map(name => this.getMigrationFilename(name));
    }
    if (options.from) {
      result.from = this.getMigrationFilename(String(options.from));
    }
    if (options.to && options.to !== 0) {
      result.to = this.getMigrationFilename(String(options.to));
    } else if (options.to === 0) {
      result.to = 0;
    }
    return result;
  }
  async runMigrations(method, options) {
    await this.init();
    if (!this.options.transactional || !this.options.allOrNothing) {
      return this.executeMigrations(method, this.prefix(options));
    }
    if (Utils.isObject(options) && options.transaction) {
      return this.runInTransaction(options.transaction, method, options);
    }
    return this.driver.getConnection().transactional(trx => this.runInTransaction(trx, method, options));
  }
  async runInTransaction(trx, method, options) {
    this.runner.setMasterMigration(trx);
    this.storage.setMasterMigration(trx);
    try {
      return await this.executeMigrations(method, this.prefix(options));
    } finally {
      this.runner.unsetMasterMigration();
      this.storage.unsetMasterMigration();
    }
  }
}
