/*
 * (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 connectorUtilities = require('./connectorUtilities');
const db = require('dmdb');
const config = require('config');

const configSql = config.get('services.CoAuthoring.sql');
const cfgDbHost = configSql.get('dbHost');
const cfgDbPort = configSql.get('dbPort');
const cfgDbUser = configSql.get('dbUser');
const cfgDbPass = configSql.get('dbPass');
const cfgConnectionLimit = configSql.get('connectionlimit');
const cfgTableResult = configSql.get('tableResult');
const cfgDamengExtraOptions = config.util.cloneDeep(configSql.get('damengExtraOptions'));
const forceClosingCountdownMs = 2000;

// dmdb VARCHAR limit - strings >= this need TO_CLOB chunking
const VARCHAR_PREC = 8188;

// dmdb driver separates PoolAttributes and ConnectionAttributes.
// For some reason if you use pool you must define connection attributes in connectString, they are not included in config object, and pool.getConnection() can't configure it.
const poolHostInfo = `dm://${cfgDbUser}:${cfgDbPass}@${cfgDbHost}:${cfgDbPort}`;
const connectionOptions = Object.entries(cfgDamengExtraOptions)
  .map(option => option.join('='))
  .join('&');

let pool = null;
const poolConfig = {
  // String format dm://username:password@host:port[?prop1=val1[&prop2=val2]]
  connectString: `${poolHostInfo}${connectionOptions.length > 0 ? '?' : ''}${connectionOptions}`,
  poolMax: cfgConnectionLimit,
  poolMin: 0
};

function readLob(lob) {
  return new Promise((resolve, reject) => {
    let blobData = Buffer.alloc(0);
    let totalLength = 0;

    lob.on('data', chunk => {
      totalLength += chunk.length;
      blobData = Buffer.concat([blobData, chunk], totalLength);
    });

    lob.on('error', err => {
      reject(err);
    });

    lob.on('end', () => {
      resolve(blobData);
    });
  });
}

async function formatResult(result) {
  const res = [];
  if (result?.rows && result?.metaData) {
    for (let i = 0; i < result.rows.length; ++i) {
      const row = result.rows[i];
      const out = {};
      for (let j = 0; j < result.metaData.length; ++j) {
        const columnName = result.metaData[j].name;
        if (row[j]?.on) {
          const buf = await readLob(row[j]);
          out[columnName] = buf.toString('utf8');
        } else {
          out[columnName] = row[j];
        }
      }

      res.push(out);
    }
  }

  return res;
}

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 {
    if (!pool) {
      pool = await db.createPool(poolConfig);
    }

    connection = await pool.getConnection();
    const result = await connection.execute(sqlCommand, values, {resultSet: false});

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

    return output;
  } catch (error) {
    if (!noLog) {
      ctx.logger.warn('sqlQuery error sqlCommand: %s: %s', sqlCommand.slice(0, 50), error.stack);
    }

    throw error;
  } finally {
    connection?.close();
  }
}

function closePool() {
  return pool.close(forceClosingCountdownMs);
}
/**
 * @param {any} val - value to bind
 * @param {Array} values - bind parameters array
 * @returns {string} placeholder like :1, :2, etc.
 */
function addSqlParameter(val, values) {
  if (typeof val === 'string') {
    const len = val.length;
    // 2000 chars * 4 bytes (max utf8) = 8000 < 8188. Safe for all.
    if (len >= 2000) {
      if (len >= VARCHAR_PREC || Buffer.byteLength(val, 'utf8') >= VARCHAR_PREC) {
        // Workaround for dmdb 8188 byte limit.
        // Tried: {type: db.CLOB} (failed), TO_CLOB wrappers (verbose).
        // Implemented: Split into 2000-char chunks and concatenate (:1 || :2).
        // Future: Use native CLOB binding when driver support improves.
        const CHUNK_SIZE = 2000;
        const placeholders = [];
        for (let i = 0; i < len; i += CHUNK_SIZE) {
          const chunk = val.slice(i, i + CHUNK_SIZE);
          values.push({val: chunk});
          placeholders.push(`:${values.length}`);
        }

        if (placeholders.length === 1) {
          return placeholders[0];
        }

        return placeholders.join(' || ');
      }
    }
  }

  values.push({val});
  return `:${values.length}`;
}

function concatParams(val1, val2) {
  return `CONCAT(COALESCE(${val1}, ''), COALESCE(${val2}, ''))`;
}

async function getTableColumns(ctx, tableName) {
  const values = [];
  const sqlParam = addSqlParameter(tableName.toUpperCase(), values);
  const result = await executeQuery(ctx, `SELECT column_name FROM DBA_TAB_COLUMNS WHERE table_name = ${sqlParam};`, values);
  return result.map(row => {
    return {column_name: row.column_name.toLowerCase()};
  });
}

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

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

  const p0 = addSqlParameter(task.tenant, values);
  const p1 = addSqlParameter(task.key, values);
  const p2 = addSqlParameter(task.status, values);
  const p3 = addSqlParameter(task.statusInfo, values);
  const p4 = addSqlParameter(dateNow, values);
  const p5 = addSqlParameter(task.userIndex, values);
  const p6 = addSqlParameter(task.changeId, values);
  const p7 = addSqlParameter(cbInsert, values);
  const p8 = addSqlParameter(task.baseurl, values);
  const p9 = addSqlParameter(dateNow, values);

  let sqlCommand = `MERGE INTO ${cfgTableResult} USING dual ON (tenant = ${p0} AND id = ${p1}) `;
  sqlCommand += `WHEN NOT MATCHED THEN INSERT (tenant, id, status, status_info, last_open_date, user_index, change_id, callback, baseurl) `;
  sqlCommand += `VALUES (${p0}, ${p1}, ${p2}, ${p3}, ${p4}, ${p5}, ${p6}, ${p7}, ${p8}) `;
  sqlCommand += `WHEN MATCHED THEN UPDATE SET last_open_date = ${p9}`;

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

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

  sqlCommand += ', user_index = user_index + 1';
  sqlCommand += ';';
  sqlCommand += `SELECT user_index FROM ${cfgTableResult} WHERE tenant = ${p0} AND id = ${p1};`;

  const out = {};
  const result = await executeQuery(ctx, sqlCommand, values);
  if (result?.length > 0) {
    const first = result[0];
    out.isInsert = task.userIndex === first.user_index;
    out.insertId = first.user_index;
  }

  return out;
}

module.exports = {
  sqlQuery,
  closePool,
  addSqlParameter,
  concatParams,
  getTableColumns,
  upsert
};
