import fs from 'fs';
import os from 'os';
import path from 'path';
import moment from 'moment';

/**
 * Class logger
 * Permet de logger dans les différents fichiers
 */
export default class Logger {

  // Properties
  private properties_: any;

  // Dossier de logs
  private logFolder_: string = 'log';

  // chemin vers le fichier de log
  private logFullPath_: string[] = [];

  // Niveaux de log
  private appLogLevel: string = 'INFO';
  private aLogLevels_: string[] = [
    'DEBUG',
    'INFO',
    'NOTICE',
    'WARNING',
    'ERROR',
    'CRITICAL',
    'ALERT',
    'EMERGENCY',
  ];

  private pid_: number = 0;

  // Moteur utilisé
  private engine_: any;

  private allow_console_return: boolean = true

  /**
   * Constructeur
   * @param properties
   */
  constructor(properties: any) {

    // Properties
    this.properties_ = properties;

    // Niveau de log
    if (this.properties_.log_level) {
      this.appLogLevel = this.properties_.log_level;
    } else {
      this.appLogLevel = 'DEBUG';
    }

    this.logFolder_ = this.properties_.log_dir;
    this.pid_ = process.pid;

    this.logFullPath_ = this.getLogFilePath_();
  }

  /**
   * Set moteur pour l'utiliser dans les logs
   * @param {any} engine engine_id
   */
  public setEngine(engine: any): void {
    this.engine_ = engine;
    // on met à jour le chemin 
    this.logFullPath_ = this.getLogFilePath_();
  }

  /**
   * Set le chemin vers le fichier e log à utiliser 
   * @param {string} sPath chemin à utiliser pour logger
   */
  public setLogFilePath(sPath: string): void {

    // Conversion du format de date de php vers js
    const sFormat: string = this.getDateFormat();
    const sLogDate: string = this.getFormatedDate_(new Date(), sFormat)
    sPath = sPath.replace(/@date/, sLogDate);

    // on met à jour le chemin 
    this.logFullPath_ = sPath.split('/');
  }

  /**
   * 
   * @returns {string}
   */
  private getDateFormat(): string {
    let sFormat: string = this.properties_.log_period
    sFormat = sFormat.replace('Y', 'YYYY');
    sFormat = sFormat.replace('m', 'MM');
    sFormat = sFormat.replace('d', 'DD');

    return sFormat;
  }

  /**
   * désactive les console.log du logger
   */
  public disableConsoleMessage(): void {
    this.allow_console_return = false;
  }
  /**
   * active les console.log du logger
   */
  public enableConsoleMessage(): void {
    this.allow_console_return = true;
  }

  /**
   * Écrit un message dans le logs
   * @param {string} message
   * @param {*} variable variable à logger dans le message
   * @param {string|undefined} level
   */
  public log(message: any, variable?: any, level: string = 'INFO'): void {
    // si le message est manquant on a rien à log
    if (!message) {
      return;
    }

    if (this.allow_console_return) {
      // envoi du log dans la console
      if (variable) {
        console.log(message, variable);
      } else {
        console.log(message);
      }
    }

    // récupération des niveaux de log disponibles
    const appLogLevels = this.getAppLogLevels_();
    // si le niveau n'est pas disponible rien à logger
    if (appLogLevels.indexOf(level) === -1) {
      return;
    }

    // formattage du message
    message = this.getFormatedMessage_(message, variable, level);

    // Chemin vers le fichier
    const aFilePath = this.logFullPath_;
    const sFilePath = this.logFullPath_.join('/');

    // analyse de la taille du fichier pour chunk si besoin
    if (fs.existsSync(sFilePath)) {
      // taille du fichier de log en octet
      const stats = fs.statSync(sFilePath)
      const iLogFileSize = stats["size"]
      // taille du message en octet
      const iMessageSize = Buffer.byteLength(message, 'utf8')

      const iFinalSize = iLogFileSize + iMessageSize;
      if ((this.properties_.log_size - 1) <= iFinalSize) {
        try {
          this.chunkLogFile(aFilePath);
        } catch (error) {
          console.log('could not rename log file', error);
          return;
        }
      }
    }

    try {
      // Crée le fichier de log si il n'existe pas
      this.createLogFile_(aFilePath);
    } catch (error) {
      console.log('could not create log file', error);
      return;
    }

    // Écrit dans le fichier
    this.appendMessage_(sFilePath, message);
  }

  /**
   * Calcule le nouveau nom du fichier de log et renome l'ancien fichier
   * @param {string[]} aFilePath tableau chemin vers le fichier d'origine
   */
  private chunkLogFile(aFilePath: string[]): void {
    // création d'une copie du tableau pour modification
    const aFinalFilePath = [...aFilePath];
    // analyse du chemin pour modification du nom de chemin
    if (aFinalFilePath) {
      const sFileName: string | undefined = aFinalFilePath.pop();
      if (typeof sFileName === 'string') {
        const sChunkedFileName: string = sFileName.replace('.log', '.' + this.getFormatedDate_(new Date(), 'YYYY_MM_DD_X') + '.log');
        aFinalFilePath.push(sChunkedFileName);
        fs.renameSync(aFilePath.join('/'), aFinalFilePath.join('/'));
      }
    }
  }

  /**
   * Retourne les niveaux de logs disponibles selon la configuration utilisateur
   * @returns {Array}
   */
  private getAppLogLevels_(): string[] {

    // Filtrage en fonction du niveau de log
    const availableLogLevels = [];
    for (let i = this.aLogLevels_.length - 1; i >= 0; i--) {
      const logLevel = this.aLogLevels_[i];
      availableLogLevels.push(logLevel);
      if (logLevel === this.appLogLevel) {
        break;
      }
    }
    return availableLogLevels;
  }

  /**
   * Retourne le chemin vers le fichier de log
   * @return {Array} chemin vers la fichier de log
   */
  private getLogFilePath_(): string[] {
    const path = [this.logFolder_];

    const sFormat: string = this.getDateFormat();
    path.push(this.getFormatedDate_(new Date(), sFormat));
    path.push('engines');
    if (this.properties_.use_hostname_in_logdir === true) {
      path.push(os.hostname());
    }
    if (this.engine_ && this.engine_.gtf_engine_id) {
      path.push(`engine_${this.engine_.gtf_engine_id}`);
    }
    path.push('engine.log');
    return path;
  }

  /**
   * Retourne la date formatée
   * @param {Date} d Date
   * @param {string | undefined} format https://momentjs.com/docs/#/displaying/format/
   * @returns {string} formated date
   */
  private getFormatedDate_(d: Date, format: string = 'YYYY/MM/DD'): string {
    const oDate = moment(d);
    return oDate.format(format);
  }

  /**
   * Crée le fichier de log si il n'existe pas
   * @param {Array} chemin vers le fichier
   */
  private createLogFile_(filePath: string[]): void {
    const sFilePath = filePath.join('/');
    const directory = path.dirname(sFilePath);
    const oldMask = process.umask(0)

    // Crée les sous dossiers si besoin
    fs.mkdirSync(directory, { recursive: true, mode: 0o777 });

    // Crée le fichier si besoin
    if (!fs.existsSync(sFilePath)) {
      fs.writeFileSync(sFilePath, '', {
        encoding: 'utf8',
        mode: 0o766,
      });
    }
    process.umask(oldMask);
  }

  /**
   * Écrit le message
   * @param {Array} filePath
   * @param {string} message message formatté
   */
  private appendMessage_(filePath: string, message: string): void {
    if (filePath && message) {
      // Écrit le message
      fs.appendFileSync(filePath, message);
    }
  }

  /**
   * écrit un message dans un fichier de log
   * @param {string} message message à logger
   * @param {any} variable variable à associé au message
   * @param {string} level Niveau de log
   */
  private getFormatedMessage_(message: string, variable?: any, level: string = 'INFO'): string {
    let formattedMessage = `${this.getFormatedDate_(new Date(), 'DD-MM-YYYY HH:mm:ss')}`;
    formattedMessage += ` | ${this.pid_}`;
    formattedMessage += ` | ${level}`;
    formattedMessage += ` | ${message}`;
    if (variable) {
      try {
        formattedMessage += ` ${JSON.stringify(variable)}`;
      } catch (error) {
        formattedMessage += ` ${String(variable)}`;
      }
    }
    formattedMessage += `\n`;

    return formattedMessage;
  }
}