import {
  Client,
  Connection
} from 'pg';
import Logger from './logger';
import fs from 'fs';

/**
 * Classe Db
 */
export default class Db {

  // Properties
  private properties_: any;

  // Paramètres de connexion
  private connexionParams_: any;

  // Client base de données
  private client_: any;

  // Définit si la connexion est en cours
  private connected_: boolean = false;

  // Définit si l'on doit tenter de se connecter en ssl si possible
  private preferMode_: boolean = false;

  // Timeout avant déconnexion
  private connectionTimeout_: number = 10000;

  // Date de la dernière requête
  private lastQueryDate_: number = 0;

  // fonction timeout de déconnexion
  private disconnectFncTimeout_: any;

  // Logger
  private logger_: Logger;

  /**
   * Constructeur
   */
  constructor(properties: any, connexionParams: any, logger: Logger) {
    this.properties_ = properties;
    this.connexionParams_ = connexionParams;
    this.logger_ = logger;

    if (this.connexionParams_.autoSslConfig === true) {
      this.connexionParams_.ssl = this.loadSslConfigFromProperties();
    }
  }

  /**
   * 
   * @returns {boolean}
   */
  public isConnected(): boolean {
    return this.connected_;
  }

  /**
   * Connexion à la base
   */
  public connect(): Promise<void> {
    return new Promise<void>((resolve, reject) => {
      this.client_ = new Client(this.connexionParams_);
      this.client_.connect().then(() => {
        this.logger_.log('Connected to the database', null, 'DEBUG');
        this.connected_ = true;
      }).catch((err: any) => {
        this.connected_ = false;
        this.logger_.log('DB ERROR : Cannot connect to the database', err, 'ERROR');
      }).finally(() => {
        resolve();
      })
    });
  }
  /**
   * Connexion à la base
   */
  public connectWithSslCheck() {
    return new Promise<void>((resolve, reject) => {
      const self = this;
      const tmp_con_: any = new Connection();

      tmp_con_.stream.once('data', function (buffer: any) {
        const responseCode = buffer.toString('utf8');
        switch (responseCode) {
          case 'S': // Server supports SSL connections, continue with a secure connection
            break;
          case 'N': // Server does not support SSL connections
            // si l'appli est en mode prefer et que le SSL n'est pas actif 
            // on désactive le ssl
            if (self.preferMode_ && self.connexionParams_.ssl !== false) {
              self.logger_.log('SSL is disabled for this database', null, 'DEBUG');
              delete self.connexionParams_.autoSslConfig;
              delete self.connexionParams_.ssl;
            }
            break;
          default: // Any other response byte, including 'E' (ErrorResponse) indicating a server error
            self.logger_.log('DB ERROR : Cannot get SSL informations', null, 'ERROR');
            return;
        }
        tmp_con_.end()
        tmp_con_.stream.destroy()

        self.connect().then(() => {
          resolve();
        });
      });
      tmp_con_.stream.on('error', function (error: Error) {

        self.logger_.log('DB ERROR : Cannot get SSL informations', error, 'ERROR');
        reject();
      });
      tmp_con_.stream.once('ready', function () {
        setTimeout(() => {
          tmp_con_.requestSsl();
        }, 0)
      });

      tmp_con_.stream.connect(this.connexionParams_.port, this.connexionParams_.host)
    });
  }

  /**
   * Surcharge la configuration SSL pour la connection postgres
   */
  private loadSslConfigFromProperties() {
    // require, prefer et allow ne nécéssite pas la vérification des certificats
    const oOptions: any = { rejectUnauthorized: false };
    // on active le mode pour se reconnecter en non ssl en cas d'echec
    if (['prefer', 'allow'].indexOf(this.properties_.db_ssl_mode) > -1) {
      this.preferMode_ = true;
      return oOptions;
    }

    if (this.properties_.db_ssl_mode === 'require') {
      return oOptions;
    }

    if (['verify-ca', 'verify-full'].indexOf(this.properties_.db_ssl_mode) > -1) {
      // active la vérification des certificats serveurs
      oOptions.rejectUnauthorized = true;
      // Cert Authority
      if (fs.existsSync(this.properties_.db_ssl_root_cert)) {
        oOptions.ca = fs.readFileSync(this.properties_.db_ssl_root_cert).toString();
      }
      // Key
      if (fs.existsSync(this.properties_.db_ssl_key)) {
        oOptions.key = fs.readFileSync(this.properties_.db_ssl_key).toString();
      }
      // Certificat
      if (fs.existsSync(this.properties_.db_ssl_cert)) {
        oOptions.cert = fs.readFileSync(this.properties_.db_ssl_cert).toString();
      }

      return oOptions;
    }
    // si le ssl n'est pas requis on le désactive
    // disabled
    return false;
  }

  /**
   * Change la durée maximale d'une connexion à la base
   * @param iDuration Durée en millisecond
   */
  public setMaxConnectionTime(iDuration: number) {
    if (iDuration > 0) {
      this.connectionTimeout_ = iDuration;
    } else {
      this.logger_.log('DB ERROR : you are trying to set a max connection time negative or null (value unchanged)', null, 'ERROR');
    }
  }

  /**
   * Désactive la déconnexion automatique à la base
   */
  public disableTimeOut() {
    this.connectionTimeout_ = 0;
  }

  /**
   * Déconnexion de la base
   */
  public disconnect() {
    this.client_.end();
    this.connected_ = false;
    if (this.disconnectFncTimeout_) {
      clearTimeout(this.disconnectFncTimeout_);
    }
  }

  /**
   * Lance une requette
   * @param {string} sQuery Requette, pour utiliser des paramêtres utiliser $1, $2...
   * @param {Array | undefined} aParams Paramètres sous forme de tableau
   */
  private execQuery_(sQuery: string, aParams: any[] = []) {
    return new Promise((resolve, reject) => {

      // Date de dernière requête
      this.lastQueryDate_ = Date.now();

      // Lancement de la requête
      this.client_.query(sQuery, aParams, (err: any, res: any) => {

        // Retour
        if (err) {
          this.logger_.log('DB ERROR : ', err.stack);
          reject(err.stack);
        } else {
          resolve(res.rows);
        }

        // Déconnexion de la base après 10 secondes d'innactivité
        if (this.disconnectFncTimeout_) {
          clearTimeout(this.disconnectFncTimeout_);
        }

        if (this.connectionTimeout_ > 0) {
          this.disconnectFncTimeout_ = setTimeout(() => {
            if (Date.now() > (this.lastQueryDate_ - this.connectionTimeout_)) {
              if (this.connected_) {
                this.disconnect();
              }
            }
          }, this.connectionTimeout_ + 10);
        }
      });
    });
  }

  public query(sQuery: string, aParams: any[] = [], bAutoConnect = true) {
    // si la connection est déjà faite
    if (this.connected_ && this.client_) {
      return this.execQuery_(sQuery, aParams);
    }

    if (!this.connected_ && bAutoConnect) {
      return new Promise((resolve, reject) => {
        // Connexion à la base
        this.connectWithSslCheck().then(() => {
          this.execQuery_(sQuery, aParams).then((rows) => {
            resolve(rows);
          }).catch((err) => {
            reject(err);
          });
        }).catch((err) => {
          reject(err);
        })
      });
    }

    return new Promise((resolve, reject) => {
      reject();
    })
  }


}