/*
 * (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 crypto = require('crypto');
const sqlBase = require('./databaseConnectors/baseConnector');
const constants = require('./../../Common/sources/constants');
const commonDefines = require('./../../Common/sources/commondefines');
const tenantManager = require('./../../Common/sources/tenantManager');
const config = require('config');

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

const addSqlParam = sqlBase.addSqlParameter;
const concatParams = sqlBase.concatParams;

const RANDOM_KEY_MAX = 10000;

function TaskResultData() {
  this.tenant = null;
  this.key = null;
  this.status = null;
  this.statusInfo = null;
  this.lastOpenDate = null;
  this.creationDate = null;
  this.userIndex = null;
  this.changeId = null;
  this.callback = null;
  this.baseurl = null;
  this.password = null;
  this.additional = null;

  this.innerPasswordChange = null; //not a DB field
}
TaskResultData.prototype.completeDefaults = function () {
  if (!this.tenant) {
    this.tenant = tenantManager.getDefautTenant();
  }
  if (!this.key) {
    this.key = '';
  }
  if (!this.status) {
    this.status = commonDefines.FileStatus.None;
  }
  if (!this.statusInfo) {
    this.statusInfo = constants.NO_ERROR;
  }
  if (!this.lastOpenDate) {
    this.lastOpenDate = new Date();
  }
  if (!this.creationDate) {
    this.creationDate = new Date();
  }
  if (!this.userIndex) {
    this.userIndex = 1;
  }
  if (!this.changeId) {
    this.changeId = 0;
  }
  if (!this.callback) {
    this.callback = '';
  }
  if (!this.baseurl) {
    this.baseurl = '';
  }
};

function upsert(ctx, task) {
  return sqlBase.upsert(ctx, task);
}
/**
 * Return TaskResult rows for docId, caching the last query result on ctx.taskResultCache or fetching from the database.
 * @param {object} ctx
 * @param {string} docId
 * @returns {Promise<Array<object>>}
 */
async function selectWithCache(ctx, docId) {
  //todo merge with select and remove on update
  if (ctx.taskResultCache && ctx.taskResultCache[0].id === docId) {
    return ctx.taskResultCache;
  }
  ctx.taskResultCache = await select(ctx, docId);
  return ctx.taskResultCache;
}
function select(ctx, docId) {
  return new Promise((resolve, reject) => {
    const values = [];
    const p1 = addSqlParam(ctx.tenant, values);
    const p2 = addSqlParam(docId, values);
    const sqlCommand = `SELECT * FROM ${cfgTableResult} WHERE tenant=${p1} AND id=${p2};`;
    sqlBase.sqlQuery(
      ctx,
      sqlCommand,
      (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      },
      undefined,
      undefined,
      values
    );
  });
}
/**
 * Generate SQL SET/WHERE clauses from task object
 * @param {TaskResultData} task - Task data object
 * @param {boolean} updateTime - Whether to update last_open_date
 * @param {boolean} isMask - Whether this is for WHERE clause (mask mode)
 * @param {Array} values - SQL parameter values array
 * @param {boolean} setPassword - Whether to set password directly
 * @returns {Array<string>} Array of SQL conditions/assignments
 *
 * Special mask values:
 * - Use 'NOT_EMPTY' as field value in mask mode to check for non-empty callback
 * - Uses baseConnector.getNotEmptyCondition() for database-specific SQL generation
 */
function toUpdateArray(task, updateTime, isMask, values, setPassword) {
  const res = [];
  if (null != task.status) {
    const sqlParam = addSqlParam(task.status, values);
    res.push(`status=${sqlParam}`);
  }
  if (null != task.statusInfo) {
    const sqlParam = addSqlParam(task.statusInfo, values);
    res.push(`status_info=${sqlParam}`);
  }
  if (updateTime) {
    const sqlParam = addSqlParam(new Date(), values);
    res.push(`last_open_date=${sqlParam}`);
  }
  if (null != task.indexUser) {
    const sqlParam = addSqlParam(task.indexUser, values);
    res.push(`user_index=${sqlParam}`);
  }
  if (null != task.changeId) {
    const sqlParam = addSqlParam(task.changeId, values);
    res.push(`change_id=${sqlParam}`);
  }
  if (null != task.callback && !isMask) {
    const userCallback = new sqlBase.UserCallback();
    userCallback.fromValues(task.indexUser, task.callback);
    const sqlParam = addSqlParam(userCallback.toSQLInsert(), values);
    res.push(`callback=${concatParams('callback', sqlParam)}`);
  }
  // Add callback non-empty check for mask
  if (isMask && task.callback === 'NOT_EMPTY') {
    // Use database-specific condition (Oracle NCLOB needs special handling)
    res.push(sqlBase.getNotEmptyCondition('callback'));
  }
  if (null != task.baseurl) {
    const sqlParam = addSqlParam(task.baseurl, values);
    res.push(`baseurl=${sqlParam}`);
  }
  if (setPassword) {
    const sqlParam = addSqlParam(task.password, values);
    res.push(`password=${sqlParam}`);
  } else if (null != task.password || setPassword) {
    const documentPassword = new sqlBase.DocumentPassword();
    documentPassword.fromValues(task.password, task.innerPasswordChange);
    const sqlParam = addSqlParam(documentPassword.toSQLInsert(), values);
    res.push(`password=${concatParams('password', sqlParam)}`);
  }
  if (null != task.additional) {
    const sqlParam = addSqlParam(task.additional, values);
    res.push(`additional=${concatParams('additional', sqlParam)}`);
  }
  return res;
}

function update(ctx, task, setPassword) {
  return new Promise((resolve, reject) => {
    const values = [];
    const updateElems = toUpdateArray(task, true, false, values, setPassword);
    const sqlSet = updateElems.join(', ');
    const p1 = addSqlParam(task.tenant, values);
    const p2 = addSqlParam(task.key, values);
    const sqlCommand = `UPDATE ${cfgTableResult} SET ${sqlSet} WHERE tenant=${p1} AND id=${p2};`;
    sqlBase.sqlQuery(
      ctx,
      sqlCommand,
      (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      },
      undefined,
      undefined,
      values
    );
  });
}

function updateIf(ctx, task, mask) {
  return new Promise((resolve, reject) => {
    const values = [];
    const commandArg = toUpdateArray(task, true, false, values, false);
    const commandArgMask = toUpdateArray(mask, false, true, values, false);
    commandArgMask.push('tenant=' + addSqlParam(mask.tenant, values));
    commandArgMask.push('id=' + addSqlParam(mask.key, values));
    const sqlSet = commandArg.join(', ');
    const sqlWhere = commandArgMask.join(' AND ');
    const sqlCommand = `UPDATE ${cfgTableResult} SET ${sqlSet} WHERE ${sqlWhere};`;
    sqlBase.sqlQuery(
      ctx,
      sqlCommand,
      (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      },
      undefined,
      undefined,
      values
    );
  });
}
function restoreInitialPassword(ctx, docId) {
  return select(ctx, docId).then(selectRes => {
    if (selectRes.length > 0) {
      const row = selectRes[0];
      const docPassword = sqlBase.DocumentPassword.prototype.getDocPassword(ctx, row.password);
      const updateTask = new TaskResultData();
      updateTask.tenant = ctx.tenant;
      updateTask.key = docId;
      if (docPassword.initial) {
        const documentPassword = new sqlBase.DocumentPassword();
        documentPassword.fromValues(docPassword.initial);
        updateTask.password = documentPassword.toSQLInsert();
        return update(ctx, updateTask, true);
      } else if (docPassword.current) {
        updateTask.password = null;
        return update(ctx, updateTask, true);
      }
    }
  });
}

function addRandomKey(ctx, task, key, opt_prefix, opt_size) {
  return new Promise((resolve, reject) => {
    task.tenant = ctx.tenant;
    if (undefined !== opt_prefix && undefined !== opt_size) {
      task.key = opt_prefix + crypto.randomBytes(opt_size).toString('hex');
    } else {
      task.key = key + '_' + Math.round(Math.random() * RANDOM_KEY_MAX);
    }
    task.completeDefaults();
    const values = [];
    const p0 = addSqlParam(task.tenant, values);
    const p1 = addSqlParam(task.key, values);
    const p2 = addSqlParam(task.status, values);
    const p3 = addSqlParam(task.statusInfo, values);
    const p4 = addSqlParam(new Date(), values);
    const p5 = addSqlParam(task.userIndex, values);
    const p6 = addSqlParam(task.changeId, values);
    const p7 = addSqlParam(task.callback, values);
    const p8 = addSqlParam(task.baseurl, values);
    const sqlCommand =
      `INSERT INTO ${cfgTableResult} (tenant, id, status, status_info, last_open_date, user_index, change_id, callback, baseurl)` +
      ` VALUES (${p0}, ${p1}, ${p2}, ${p3}, ${p4}, ${p5}, ${p6}, ${p7}, ${p8});`;
    sqlBase.sqlQuery(
      ctx,
      sqlCommand,
      (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      },
      undefined,
      true,
      values
    );
  });
}
function* addRandomKeyTask(ctx, key, opt_prefix, opt_size) {
  const task = new TaskResultData();
  task.tenant = ctx.tenant;
  task.key = key;
  task.status = commonDefines.FileStatus.WaitQueue;
  task.statusInfo = Math.floor(Date.now() / 60000); //minutes
  //nTryCount so as not to freeze if there are really problems with the DB
  let nTryCount = RANDOM_KEY_MAX;
  let addRes = null;
  while (nTryCount-- > 0) {
    try {
      addRes = yield addRandomKey(ctx, task, key, opt_prefix, opt_size);
    } catch (_e) {
      addRes = null;
      ctx.logger.debug('addRandomKeyTask %s exists, try again', task.key);
    }
    if (addRes && addRes.affectedRows > 0) {
      break;
    }
  }
  if (addRes && addRes.affectedRows > 0) {
    return task;
  } else {
    throw new Error('addRandomKeyTask Error');
  }
}

function remove(ctx, docId) {
  return new Promise((resolve, reject) => {
    const values = [];
    const p1 = addSqlParam(ctx.tenant, values);
    const p2 = addSqlParam(docId, values);
    const sqlCommand = `DELETE FROM ${cfgTableResult} WHERE tenant=${p1} AND id=${p2};`;
    sqlBase.sqlQuery(
      ctx,
      sqlCommand,
      (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      },
      undefined,
      undefined,
      values
    );
  });
}
function removeIf(ctx, mask) {
  return new Promise((resolve, reject) => {
    const values = [];
    const commandArgMask = toUpdateArray(mask, false, true, values, false);
    commandArgMask.push('tenant=' + addSqlParam(mask.tenant, values));
    commandArgMask.push('id=' + addSqlParam(mask.key, values));
    const sqlWhere = commandArgMask.join(' AND ');
    const sqlCommand = `DELETE FROM ${cfgTableResult} WHERE ${sqlWhere};`;
    sqlBase.sqlQuery(
      ctx,
      sqlCommand,
      (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      },
      undefined,
      undefined,
      values
    );
  });
}

exports.TaskResultData = TaskResultData;
exports.upsert = upsert;
exports.select = select;
exports.selectWithCache = selectWithCache;
exports.update = update;
exports.updateIf = updateIf;
exports.restoreInitialPassword = restoreInitialPassword;
exports.addRandomKeyTask = addRandomKeyTask;
exports.remove = remove;
exports.removeIf = removeIf;
exports.getExpired = sqlBase.getExpired;
