import { Injectable, Logger } from '@nestjs/common';
import { WorkingHours } from '../../../working-hours/classes/working-hours';
import {
  Configuration,
  ConfigurationNotFoundException,
} from '../../configuration.entity';
import { EEnvVariable } from '../../enums/env-variable.enum';
import { EConfigurationProperty } from '../../helpers/configuration-type.helper';

/**
 * Timeout value constant for validation function to be used if calling on service startup
 */
export const CONFIGURATION_VALIDATION_TIMEOUT = 400;

/**
 * List of optional Configuration properties.
 * Optional property can be undefined in the DB or `null` value
 */
export const OPTIONAL_CONFIGURATION_PROPERTIES: EConfigurationProperty[] = [
  EConfigurationProperty.MOTD_TIME,
  EConfigurationProperty.MOTD_BODY,
];

/**
 * List of optional Environment variables.
 * Optional variable can be undefined (not set).
 */
export const OPTIONAL_ENV_PROPERTIES: EEnvVariable[] = [
  EEnvVariable.NODE_ENV,
  EEnvVariable.IS_MASTER,
];

/**
 * Validation function type
 */
type ValidatorFn = (value?: string) => Promise<unknown>;

@Injectable()
export class ConfigurationValidationService {
  private readonly logger = new Logger('ConfigurationValidationService');

  /**
   * Common validators
   * List is not full yet - other common validators can be added, such as:
   * boolean, number, object, etc-etc.
   * @private
   */
  private readonly COMMON_VALIDATORS = {
    string: (value: string): Promise<unknown> => Promise.resolve(value),
  };

  /**
   * Map of all validation functions for each Configuration property.
   * There must be one for each defined configuration property in `EConfigurationProperty`
   * If there's none it will ignore it but the Unit Test will fail
   */
  public readonly VALIDATIONS: {
    [E in EConfigurationProperty]: ValidatorFn;
  } = {
    [EConfigurationProperty.WORKING_HOURS]: () =>
      WorkingHours.createFromConfiguration(),
    [EConfigurationProperty.MOTD_TIME]:
      ConfigurationValidationService.validateMotdTime,
    [EConfigurationProperty.MOTD_BODY]: this.COMMON_VALIDATORS.string,
  };

  /**
   * Map of all validations functions for every environment variable.
   * There must be one for each defined configuration property in `EEnvVariable`.
   */
  public readonly ENV_VALIDATIONS: {
    [E in EEnvVariable]: ValidatorFn;
  } = {
    [EEnvVariable.NODE_ENV]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.API_PORT]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.URL]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.URL_PREFIX]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.IS_MASTER]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.SOCKET_PATH_SUBDIRECTORY]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.SLACK_BOLT_PATH_SUBDIRECTORY]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.DATABASE_HOST]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.DATABASE_PORT]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.DATABASE_USERNAME]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.DATABASE_PASSWORD]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.DATABASE_SCHEMA]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.SLACK_SIGNING_SECRET]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.SLACK_BOT_TOKEN]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.SLACK_WORKING_CHANNEL_NAME]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.AMAZON_ACCESS_KEY_ID]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.AMAZON_SECRET_ACCESS_KEY]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.AMAZON_REGION]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.AMAZON_BUCKET]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.REDIS_HOST]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.REDIS_PORT]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.REDIS_SCOPE]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.REDIS_MODE]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.MAX_TEMPORARY_ATTACHMENTS]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.TEMPORARY_ATTACHMENT_REMOVE_WORKER_DAYS_BEFORE]: this
      .COMMON_VALIDATORS.string,
    [EEnvVariable.MAILER_GOOGLE_CLIENT_ID]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.MAILER_GOOGLE_CLIENT_SECRET]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.MAILER_GOOGLE_REFRESH_TOKEN]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.MAILER_USER]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.MAILER_REPLY_TO]: this.COMMON_VALIDATORS.string,
    [EEnvVariable.MAILER_EXCEPTION_NOTIFICATION_TO]: this.COMMON_VALIDATORS
      .string,
  };

  /**
   * Validate all the properties
   */
  public async validate() {
    for (const property in this.VALIDATIONS) {
      let value: string;
      try {
        value = await Configuration.get(property as EConfigurationProperty);
      } catch (e) {
        if (
          e instanceof ConfigurationNotFoundException &&
          OPTIONAL_CONFIGURATION_PROPERTIES.includes(
            property as EConfigurationProperty
          )
        ) {
          continue;
        } else {
          throw e;
        }
      }

      if (value === null) {
        if (
          !OPTIONAL_CONFIGURATION_PROPERTIES.includes(
            property as EConfigurationProperty
          )
        ) {
          throw new Error(
            `Missing required "${property}" Configuration property!`
          );
        } else {
          continue;
        }
      }

      // Validation will fail if exception is thrown
      await this.VALIDATIONS[property](value);
    }

    this.logger.log('Configuration properties successfully validated');
  }

  /**
   * Validate all environment variables
   */
  public async validateEnv() {
    // fixme: consider using some library instead (maybe `envalid`)
    for (const property in this.ENV_VALIDATIONS) {
      const value = process.env[property];

      if (
        value === undefined &&
        !OPTIONAL_ENV_PROPERTIES.includes(property as EEnvVariable)
      ) {
        throw new Error(`Missing required "${property}" environment variable!`);
      }

      await this.ENV_VALIDATIONS[property](value);
    }

    this.logger.log('Environment variables successfully validated');
  }

  // Property validators
  // MOTD_TIME
  public static async validateMotdTime(value: string) {
    const split = value.split('|');

    if (split.length !== 2) {
      throw new Error(
        'MOTD_TIME validation failed: value must have a separator "|"'
      );
    }

    if (split.some((s) => s.length > 0 && new Date(s).toISOString() !== s)) {
      throw new Error(
        'MOTD_TIME validation failed: time value, if provided, must be valid ISO string'
      );
    }
  }
}
