/*
 * (c) Copyright Ascensio System SIA 2010-2024
 *
 * This program is a free software product. You can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License (AGPL)
 * version 3 as published by the Free Software Foundation. In accordance with
 * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
 * that Ascensio System SIA expressly excludes the warranty of non-infringement
 * of any third-party rights.
 *
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR  PURPOSE. For
 * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
 *
 * You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
 * street, Riga, Latvia, EU, LV-1050.
 *
 * The  interactive user interfaces in modified source and object code versions
 * of the Program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU AGPL version 3.
 *
 * Pursuant to Section 7(b) of the License you must retain the original Product
 * logo when distributing the program. Pursuant to Section 7(e) we decline to
 * grant you any rights under trademark law for use of our trademarks.
 *
 * All the Product's GUI elements, including illustrations and icon sets, as
 * well as technical writing content are licensed under the terms of the
 * Creative Commons Attribution-ShareAlike 4.0 International. See the License
 * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
 *
 */

'use strict';

const mysql = require('mysql2/promise');
const connectorUtilities = require('./connectorUtilities');
const config = require('config');

const configSql = config.get('services.CoAuthoring.sql');
const cfgTableResult = configSql.get('tableResult');

const connectionConfiguration = {
  host: configSql.get('dbHost'),
  port: parseInt(configSql.get('dbPort')),
  user: configSql.get('dbUser'),
  password: configSql.get('dbPass'),
  database: configSql.get('dbName'),
  charset: configSql.get('charset'),
  connectionLimit: configSql.get('connectionlimit'),
  timezone: 'Z',
  flags: '-FOUND_ROWS'
};

const additionalOptions = config.util.cloneDeep(configSql.get('mysqlExtraOptions'));
const configuration = Object.assign({}, connectionConfiguration, additionalOptions);
let queryTimeout = undefined;
if (configuration.queryTimeout) {
  queryTimeout = configuration.queryTimeout;
  delete configuration.queryTimeout;
}

const pool = mysql.createPool(configuration);

function sqlQuery(ctx, sqlCommand, callbackFunction, opt_noModifyRes = false, opt_noLog = false, opt_values = []) {
  return executeQuery(ctx, sqlCommand, opt_values, opt_noModifyRes, opt_noLog).then(
    result => callbackFunction?.(null, result),
    error => callbackFunction?.(error)
  );
}

async function executeQuery(ctx, sqlCommand, values = [], noModifyRes = false, noLog = false) {
  let connection = null;
  try {
    connection = await pool.getConnection();

    // Ensure session autocommit=1 once per physical connection; avoids pool 'connection' race and per-query overhead
    if (!connection.__autocommitSet) {
      await connection.query('SET autocommit=1');
      connection.__autocommitSet = true;
    }

    const result = await connection.query({sql: sqlCommand, timeout: queryTimeout, values});

    let output;
    if (!noModifyRes) {
      output = result[0]?.affectedRows ? {affectedRows: result[0].affectedRows} : result[0];
    } else {
      output = result[0];
    }

    return output ?? {rows: [], affectedRows: 0};
  } catch (error) {
    if (!noLog) {
      ctx.logger.error(`sqlQuery() error while executing query: ${sqlCommand}\n${error.stack}`);
    }

    throw error;
  } finally {
    if (connection) {
      try {
        // Put the connection back in the pool
        connection.release();
      } catch (error) {
        if (!noLog) {
          ctx.logger.error(`connection.release() error while executing query: ${sqlCommand}\n${error.stack}`);
        }
      }
    }
  }
}

async function closePool() {
  return await pool.end();
}

function addSqlParameter(parameter, accumulatedArray) {
  accumulatedArray.push(parameter);
  return '?';
}

function concatParams(firstParameter, secondParameter) {
  return `CONCAT(COALESCE(${firstParameter}, ''), COALESCE(${secondParameter}, ''))`;
}

async function upsert(ctx, task) {
  task.completeDefaults();
  const dateNow = new Date();

  let cbInsert = task.callback;
  if (task.callback) {
    const userCallback = new connectorUtilities.UserCallback();
    userCallback.fromValues(task.userIndex, task.callback);
    cbInsert = userCallback.toSQLInsert();
  }

  const values = [];
  const valuesPlaceholder = [
    addSqlParameter(task.tenant, values),
    addSqlParameter(task.key, values),
    addSqlParameter(task.status, values),
    addSqlParameter(task.statusInfo, values),
    addSqlParameter(dateNow, values),
    addSqlParameter(task.userIndex, values),
    addSqlParameter(task.changeId, values),
    addSqlParameter(cbInsert, values),
    addSqlParameter(task.baseurl, values)
  ];

  let updateStatement = `last_open_date = ${addSqlParameter(dateNow, values)}`;
  if (task.callback) {
    const callbackPlaceholder = addSqlParameter(JSON.stringify(task.callback), values);
    updateStatement += `, callback = CONCAT(callback , '${connectorUtilities.UserCallback.prototype.delimiter}{"userIndex":' , (user_index + 1) , ',"callback":', ${callbackPlaceholder}, '}')`;
  }

  if (task.baseurl) {
    const baseUrlPlaceholder = addSqlParameter(task.baseurl, values);
    updateStatement += `, baseurl = ${baseUrlPlaceholder}`;
  }

  updateStatement += ', user_index = LAST_INSERT_ID(user_index + 1);';

  const sqlCommand =
    `INSERT INTO ${cfgTableResult} (tenant, id, status, status_info, last_open_date, user_index, change_id, callback, baseurl) ` +
    `VALUES (${valuesPlaceholder.join(', ')}) ` +
    `ON DUPLICATE KEY UPDATE ${updateStatement}`;

  const result = await executeQuery(ctx, sqlCommand, values, true);
  const insertId = result.affectedRows === 1 ? task.userIndex : result.insertId;
  //if CLIENT_FOUND_ROWS don't specify 1 row is inserted , 2 row is updated, and 0 row is set to its current values
  //http://dev.mysql.com/doc/refman/5.7/en/insert-on-duplicate.html
  const isInsert = result.affectedRows === 1;

  return {isInsert, insertId};
}

module.exports.sqlQuery = sqlQuery;
module.exports.closePool = closePool;
module.exports.addSqlParameter = addSqlParameter;
module.exports.concatParams = concatParams;
module.exports.upsert = upsert;
