import { Logger } from '@jig-software/logger';
import { Inject, Lifecycle, MongoOrmValidationError } from '@jig-software/sdk';
import { EventEmitter } from 'eventemitter3';
import { ChangeStream } from 'mongodb';
import { AppConfig } from '../config';
import { AppSettingsRepository } from '../repositories';
import { AppSettings, AppSettingsInit } from '../shared';

export type AppSettingsChangeEmitter = EventEmitter<{
  change: (appSettings: AppSettingsInit) => void;
  error: (err: unknown) => void;
}>;

export class ActiveAppSettings extends Lifecycle {
  @Inject()
  protected logger!: Logger;

  @Inject()
  private appConfig!: AppConfig;

  @Inject()
  private appSettingsRepo!: AppSettingsRepository;

  private _changeStream: ChangeStream | null = null;

  private _appSettings: AppSettings | null = null;

  private _changeEmitter: AppSettingsChangeEmitter = new EventEmitter();

  constructor() {
    super();
    this.logger.setTag(this.constructor.name);
  }

  get changeEmitter() {
    return this._changeEmitter;
  }

  get(): Readonly<AppSettingsInit> {
    return (
      (this._appSettings as unknown as AppSettingsInit) ??
      this.appConfig.defaultSettings
    );
  }

  /**
   * Modify appSettings in-place.
   * Will persist modification. If fails will rollback to existing one.
   */
  async set(modify: (appSettings: AppSettingsInit) => void) {
    if (!this._appSettings) {
      throw new Error('AppSettings is not ready.'); // This should not happens.
    }
    const _id = this._appSettings._id;
    modify(this._appSettings as unknown as AppSettingsInit);
    try {
      await this.appSettingsRepo.persist(this._appSettings);
    } catch (err) {
      this._appSettings = await this.appSettingsRepo.get(_id);
      throw err;
    }
  }

  async onStart() {
    await this.ensureAppSettings();
    this.setUpChangeStream();
  }

  onStop() {
    this._appSettings = null;
    this.cleanUpChangeStream();
  }

  private async ensureAppSettings() {
    const count = await this.appSettingsRepo.count({});
    if (count === 0) {
      this.logger.info('AppSettings is not found. Create one with default.');
      this._appSettings = await this.appSettingsRepo.create(
        this.appConfig.defaultSettings,
      );
    } else {
      if (count > 1)
        this.logger.warn(
          `Found ${count} AppSettings. Only the latest will be used.`,
        );
      else this.logger.info('Found AppSettings.');
      this._appSettings = await this.appSettingsRepo.findOne(
        {},
        { sort: { updatedAt: -1 } },
      );
      try {
        this.appSettingsRepo.validatePersisted(this._appSettings);
      } catch (err) {
        this.logger.warn(
          'Invalid AppSettings. AppSettings may not be properly migrated!',
        );
        if (err instanceof MongoOrmValidationError)
          this.logger.error(err.message);
      }
    }
    this._changeEmitter.emit(
      'change',
      this._appSettings! as unknown as AppSettingsInit,
    );
  }

  private setUpChangeStream() {
    if (this._changeStream) {
      this.logger.warn(`ChangeStream already setUp. Skip.`);
      return;
    }
    this._changeStream = this.appSettingsRepo
      .watch(
        [
          {
            $match: {
              operationType: { $in: ['replace', 'update', 'delete'] },
            },
          },
        ],
        { fullDocument: 'updateLookup' },
      )
      .on('change', (change) => {
        switch (change.operationType) {
          case 'update':
          case 'replace': {
            const appSettings = change.fullDocument!;
            this.logger.debug(`Detect ${change.operationType}.`);
            if (this._appSettings?._id.equals(appSettings._id)) {
              this.logger.info('Current AppSettings is updated.');
              this._appSettings = appSettings;
              this._changeEmitter.emit(
                'change',
                this._appSettings! as unknown as AppSettingsInit,
              );
            }
            break;
          }
          case 'delete': {
            const { _id: deletedId } = change.documentKey;
            this.logger.debug(`Detect delete (${deletedId}).`);
            if (this._appSettings?._id.equals(deletedId)) {
              this.logger.info('Current AppSettings is deleted. Reinitialize.');
              this._appSettings = null;
              this.ensureAppSettings().catch((err) => {
                this.logger.error('Fail to reinitialize.');
                this.logger.error(err);
              });
            }
            break;
          }
        }
      })
      .once('error', (err) => {
        this.logger.error(`Error in ChangeStream. Restart.`);
        this.logger.error(err.message);
        this.launch(() => {
          throw err;
        }).catch(() => {});
      });
  }

  private cleanUpChangeStream() {
    this._changeStream?.close().catch((err) => {
      this.logger.error('Fail to close ChangeStream.');
      this.logger.error(err);
    });
    this._changeStream = null;
  }
}
