/*
 * (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 config = require('config');
const ms = require('ms');
const utils = require('./../../Common/sources/utils');
const commonDefines = require('./../../Common/sources/commondefines');
const tenantManager = require('./../../Common/sources/tenantManager');

const cfgExpMonthUniqueUsers = ms(config.get('services.CoAuthoring.expire.monthUniqueUsers'));

function EditorCommon() {
  this.data = {};
}
EditorCommon.prototype.connect = async function () {};
EditorCommon.prototype.isConnected = function () {
  return true;
};
EditorCommon.prototype.ping = async function () {
  return 'PONG';
};
EditorCommon.prototype.close = async function () {};
EditorCommon.prototype.healthCheck = async function () {
  if (this.isConnected()) {
    await this.ping();
    return true;
  }
  return false;
};
EditorCommon.prototype._getDocumentData = function (ctx, docId) {
  let tenantData = this.data[ctx.tenant];
  if (!tenantData) {
    this.data[ctx.tenant] = tenantData = {};
  }
  let options = tenantData[docId];
  if (!options) {
    tenantData[docId] = options = {};
  }
  return options;
};
EditorCommon.prototype._checkAndLock = function (ctx, name, docId, fencingToken, ttl) {
  const data = this._getDocumentData(ctx, docId);
  const now = Date.now();
  let res = true;
  if (data[name] && now < data[name].expireAt && fencingToken !== data[name].fencingToken) {
    res = false;
  } else {
    const expireAt = now + ttl * 1000;
    data[name] = {fencingToken, expireAt};
  }
  return res;
};
EditorCommon.prototype._checkAndUnlock = function (ctx, name, docId, fencingToken) {
  const data = this._getDocumentData(ctx, docId);
  const now = Date.now();
  let res;
  if (data[name] && now < data[name].expireAt) {
    if (fencingToken === data[name].fencingToken) {
      res = commonDefines.c_oAscUnlockRes.Unlocked;
      delete data[name];
    } else {
      res = commonDefines.c_oAscUnlockRes.Locked;
    }
  } else {
    res = commonDefines.c_oAscUnlockRes.Empty;
    delete data[name];
  }
  return res;
};

function EditorData() {
  EditorCommon.call(this);
  this.forceSaveTimer = {};
}
EditorData.prototype = Object.create(EditorCommon.prototype);
EditorData.prototype.constructor = EditorData;

EditorData.prototype.addPresence = async function (_ctx, _docId, _userId, _userInfo) {};
EditorData.prototype.updatePresence = async function (_ctx, _docId, _userId) {};
EditorData.prototype.removePresence = async function (_ctx, _docId, _userId) {};
EditorData.prototype.getPresence = async function (ctx, docId, connections) {
  const hvals = [];
  if (connections) {
    for (let i = 0; i < connections.length; ++i) {
      const conn = connections[i];
      if (conn.docId === docId && ctx.tenant === tenantManager.getTenantByConnection(ctx, conn)) {
        hvals.push(utils.getConnectionInfoStr(conn));
      }
    }
  }
  return hvals;
};

EditorData.prototype.lockSave = async function (ctx, docId, userId, ttl) {
  return this._checkAndLock(ctx, 'lockSave', docId, userId, ttl);
};
EditorData.prototype.unlockSave = async function (ctx, docId, userId) {
  return this._checkAndUnlock(ctx, 'lockSave', docId, userId);
};
EditorData.prototype.lockAuth = async function (ctx, docId, userId, ttl) {
  return this._checkAndLock(ctx, 'lockAuth', docId, userId, ttl);
};
EditorData.prototype.unlockAuth = async function (ctx, docId, userId) {
  return this._checkAndUnlock(ctx, 'lockAuth', docId, userId);
};

EditorData.prototype.getDocumentPresenceExpired = async function (_now) {
  return [];
};
EditorData.prototype.removePresenceDocument = async function (_ctx, _docId) {};

EditorData.prototype.addLocks = async function (ctx, docId, locks) {
  const data = this._getDocumentData(ctx, docId);
  if (!data.locks) {
    data.locks = {};
  }
  Object.assign(data.locks, locks);
};
EditorData.prototype.addLocksNX = async function (ctx, docId, locks) {
  const data = this._getDocumentData(ctx, docId);
  if (!data.locks) {
    data.locks = {};
  }
  const lockConflict = {};
  for (const lockId in locks) {
    if (undefined === data.locks[lockId]) {
      data.locks[lockId] = locks[lockId];
    } else {
      lockConflict[lockId] = locks[lockId];
    }
  }
  return {lockConflict, allLocks: data.locks};
};
EditorData.prototype.removeLocks = async function (ctx, docId, locks) {
  const data = this._getDocumentData(ctx, docId);
  if (data.locks) {
    for (const lockId in locks) {
      delete data.locks[lockId];
    }
  }
};
EditorData.prototype.removeAllLocks = async function (ctx, docId) {
  const data = this._getDocumentData(ctx, docId);
  data.locks = undefined;
};
EditorData.prototype.getLocks = async function (ctx, docId) {
  const data = this._getDocumentData(ctx, docId);
  return data.locks || {};
};

EditorData.prototype.addMessage = async function (ctx, docId, msg) {
  const data = this._getDocumentData(ctx, docId);
  if (!data.messages) {
    data.messages = [];
  }
  data.messages.push(msg);
};
EditorData.prototype.removeMessages = async function (ctx, docId) {
  const data = this._getDocumentData(ctx, docId);
  data.messages = undefined;
};
EditorData.prototype.getMessages = async function (ctx, docId) {
  const data = this._getDocumentData(ctx, docId);
  return data.messages || [];
};

EditorData.prototype.setSaved = async function (ctx, docId, status) {
  const data = this._getDocumentData(ctx, docId);
  data.saved = status;
};
EditorData.prototype.getdelSaved = async function (ctx, docId) {
  const data = this._getDocumentData(ctx, docId);
  const res = data.saved;
  data.saved = null;
  return res;
};
EditorData.prototype.setForceSave = async function (ctx, docId, time, index, baseUrl, changeInfo, convertInfo) {
  const data = this._getDocumentData(ctx, docId);
  data.forceSave = {time, index, baseUrl, changeInfo, started: false, ended: false, convertInfo};
};
EditorData.prototype.getForceSave = async function (ctx, docId) {
  const data = this._getDocumentData(ctx, docId);
  return data.forceSave || null;
};
EditorData.prototype.checkAndStartForceSave = async function (ctx, docId) {
  const data = this._getDocumentData(ctx, docId);
  let res;
  if (data.forceSave && !data.forceSave.started) {
    data.forceSave.started = true;
    data.forceSave.ended = false;
    res = data.forceSave;
  }
  return res;
};
EditorData.prototype.checkAndSetForceSave = async function (ctx, docId, time, index, started, ended, convertInfo) {
  const data = this._getDocumentData(ctx, docId);
  let res;
  if (data.forceSave && time === data.forceSave.time && index === data.forceSave.index) {
    data.forceSave.started = started;
    data.forceSave.ended = ended;
    data.forceSave.convertInfo = convertInfo;
    res = data.forceSave;
  }
  return res;
};
EditorData.prototype.removeForceSave = async function (ctx, docId) {
  const data = this._getDocumentData(ctx, docId);
  data.forceSave = undefined;
};

EditorData.prototype.cleanDocumentOnExit = async function (ctx, docId) {
  const tenantData = this.data[ctx.tenant];
  if (tenantData) {
    delete tenantData[docId];
  }
  const tenantTimer = this.forceSaveTimer[ctx.tenant];
  if (tenantTimer) {
    delete tenantTimer[docId];
  }
};

EditorData.prototype.addForceSaveTimerNX = async function (ctx, docId, expireAt) {
  let tenantTimer = this.forceSaveTimer[ctx.tenant];
  if (!tenantTimer) {
    this.forceSaveTimer[ctx.tenant] = tenantTimer = {};
  }
  if (!tenantTimer[docId]) {
    tenantTimer[docId] = expireAt;
  }
};
EditorData.prototype.getForceSaveTimer = async function (now) {
  const res = [];
  for (const tenant in this.forceSaveTimer) {
    if (Object.hasOwn(this.forceSaveTimer, tenant)) {
      const tenantTimer = this.forceSaveTimer[tenant];
      for (const docId in tenantTimer) {
        if (Object.hasOwn(tenantTimer, docId)) {
          if (tenantTimer[docId] < now) {
            res.push([tenant, docId]);
            delete tenantTimer[docId];
          }
        }
      }
    }
  }
  return res;
};

function EditorStat() {
  EditorCommon.call(this);
  this.uniqueUser = {};
  this.uniqueUsersOfMonth = {};
  this.uniqueViewUser = {};
  this.uniqueViewUsersOfMonth = {};
  this.stat = {};
  this.shutdown = {};
  this.license = {};
}
EditorStat.prototype = Object.create(EditorCommon.prototype);
EditorStat.prototype.constructor = EditorStat;
EditorStat.prototype.addPresenceUniqueUser = async function (ctx, userId, expireAt, userInfo) {
  let tenantUser = this.uniqueUser[ctx.tenant];
  if (!tenantUser) {
    this.uniqueUser[ctx.tenant] = tenantUser = {};
  }
  tenantUser[userId] = {expireAt, userInfo};
};
EditorStat.prototype.getPresenceUniqueUser = async function (ctx, nowUTC) {
  const res = [];
  let tenantUser = this.uniqueUser[ctx.tenant];
  if (!tenantUser) {
    this.uniqueUser[ctx.tenant] = tenantUser = {};
  }
  for (const userId in tenantUser) {
    if (Object.hasOwn(tenantUser, userId)) {
      if (tenantUser[userId].expireAt > nowUTC) {
        const elem = tenantUser[userId];
        const newElem = {userid: userId, expire: new Date(elem.expireAt * 1000)};
        Object.assign(newElem, elem.userInfo);
        res.push(newElem);
      } else {
        delete tenantUser[userId];
      }
    }
  }
  return res;
};
EditorStat.prototype.addPresenceUniqueUsersOfMonth = async function (ctx, userId, period, userInfo) {
  let tenantUser = this.uniqueUsersOfMonth[ctx.tenant];
  if (!tenantUser) {
    this.uniqueUsersOfMonth[ctx.tenant] = tenantUser = {};
  }
  if (!tenantUser[period]) {
    const expireAt = Date.now() + cfgExpMonthUniqueUsers;
    tenantUser[period] = {expireAt, data: {}};
  }
  tenantUser[period].data[userId] = userInfo;
};
EditorStat.prototype.getPresenceUniqueUsersOfMonth = async function (ctx) {
  const res = {};
  const nowUTC = Date.now();
  let tenantUser = this.uniqueUsersOfMonth[ctx.tenant];
  if (!tenantUser) {
    this.uniqueUsersOfMonth[ctx.tenant] = tenantUser = {};
  }
  for (const periodId in tenantUser) {
    if (Object.hasOwn(tenantUser, periodId)) {
      if (tenantUser[periodId].expireAt <= nowUTC) {
        delete tenantUser[periodId];
      } else {
        const date = new Date(parseInt(periodId)).toISOString();
        res[date] = tenantUser[periodId].data;
      }
    }
  }
  return res;
};

EditorStat.prototype.addPresenceUniqueViewUser = async function (ctx, userId, expireAt, userInfo) {
  let tenantUser = this.uniqueViewUser[ctx.tenant];
  if (!tenantUser) {
    this.uniqueViewUser[ctx.tenant] = tenantUser = {};
  }
  tenantUser[userId] = {expireAt, userInfo};
};
EditorStat.prototype.getPresenceUniqueViewUser = async function (ctx, nowUTC) {
  const res = [];
  let tenantUser = this.uniqueViewUser[ctx.tenant];
  if (!tenantUser) {
    this.uniqueViewUser[ctx.tenant] = tenantUser = {};
  }
  for (const userId in tenantUser) {
    if (Object.hasOwn(tenantUser, userId)) {
      if (tenantUser[userId].expireAt > nowUTC) {
        const elem = tenantUser[userId];
        const newElem = {userid: userId, expire: new Date(elem.expireAt * 1000)};
        Object.assign(newElem, elem.userInfo);
        res.push(newElem);
      } else {
        delete tenantUser[userId];
      }
    }
  }
  return res;
};
EditorStat.prototype.addPresenceUniqueViewUsersOfMonth = async function (ctx, userId, period, userInfo) {
  let tenantUser = this.uniqueViewUsersOfMonth[ctx.tenant];
  if (!tenantUser) {
    this.uniqueViewUsersOfMonth[ctx.tenant] = tenantUser = {};
  }
  if (!tenantUser[period]) {
    const expireAt = Date.now() + cfgExpMonthUniqueUsers;
    tenantUser[period] = {expireAt, data: {}};
  }
  tenantUser[period].data[userId] = userInfo;
};
EditorStat.prototype.getPresenceUniqueViewUsersOfMonth = async function (ctx) {
  const res = {};
  const nowUTC = Date.now();
  let tenantUser = this.uniqueViewUsersOfMonth[ctx.tenant];
  if (!tenantUser) {
    this.uniqueViewUsersOfMonth[ctx.tenant] = tenantUser = {};
  }
  for (const periodId in tenantUser) {
    if (Object.hasOwn(tenantUser, periodId)) {
      if (tenantUser[periodId].expireAt <= nowUTC) {
        delete tenantUser[periodId];
      } else {
        const date = new Date(parseInt(periodId)).toISOString();
        res[date] = tenantUser[periodId].data;
      }
    }
  }
  return res;
};
EditorStat.prototype.setEditorConnections = async function (ctx, countEdit, countLiveView, countView, now, precision) {
  let tenantStat = this.stat[ctx.tenant];
  if (!tenantStat) {
    this.stat[ctx.tenant] = tenantStat = [];
  }
  tenantStat.push({time: now, edit: countEdit, liveview: countLiveView, view: countView});
  let i = 0;
  while (i < tenantStat.length && tenantStat[i] < now - precision[precision.length - 1].val) {
    i++;
  }
  tenantStat.splice(0, i);
};
EditorStat.prototype.getEditorConnections = async function (ctx) {
  let tenantStat = this.stat[ctx.tenant];
  if (!tenantStat) {
    this.stat[ctx.tenant] = tenantStat = [];
  }
  return tenantStat;
};
EditorStat.prototype.setEditorConnectionsCountByShard = async function (_ctx, _shardId, _count) {};
EditorStat.prototype.incrEditorConnectionsCountByShard = async function (_ctx, _shardId, _count) {};
EditorStat.prototype.getEditorConnectionsCount = async function (ctx, connections) {
  let count = 0;
  if (connections) {
    for (let i = 0; i < connections.length; ++i) {
      const conn = connections[i];
      if (!(conn.isCloseCoAuthoring || (conn.user && conn.user.view)) && ctx.tenant === tenantManager.getTenantByConnection(ctx, conn)) {
        count++;
      }
    }
  }
  return count;
};
EditorStat.prototype.setViewerConnectionsCountByShard = async function (_ctx, _shardId, _count) {};
EditorStat.prototype.incrViewerConnectionsCountByShard = async function (_ctx, _shardId, _count) {};
EditorStat.prototype.getViewerConnectionsCount = async function (ctx, connections) {
  let count = 0;
  if (connections) {
    for (let i = 0; i < connections.length; ++i) {
      const conn = connections[i];
      if (conn.isCloseCoAuthoring || (conn.user && conn.user.view && ctx.tenant === tenantManager.getTenantByConnection(ctx, conn))) {
        count++;
      }
    }
  }
  return count;
};
EditorStat.prototype.setLiveViewerConnectionsCountByShard = async function (_ctx, _shardId, _count) {};
EditorStat.prototype.incrLiveViewerConnectionsCountByShard = async function (_ctx, _shardId, _count) {};
EditorStat.prototype.getLiveViewerConnectionsCount = async function (ctx, connections) {
  let count = 0;
  if (connections) {
    for (let i = 0; i < connections.length; ++i) {
      const conn = connections[i];
      if (utils.isLiveViewer(conn) && ctx.tenant === tenantManager.getTenantByConnection(ctx, conn)) {
        count++;
      }
    }
  }
  return count;
};
EditorStat.prototype.addShutdown = async function (key, docId) {
  if (!this.shutdown[key]) {
    this.shutdown[key] = {};
  }
  this.shutdown[key][docId] = 1;
};
EditorStat.prototype.removeShutdown = async function (key, docId) {
  if (!this.shutdown[key]) {
    this.shutdown[key] = {};
  }
  delete this.shutdown[key][docId];
};
EditorStat.prototype.getShutdownCount = async function (key) {
  let count = 0;
  if (this.shutdown[key]) {
    for (const docId in this.shutdown[key]) {
      if (Object.hasOwn(this.shutdown[key], docId)) {
        count++;
      }
    }
  }
  return count;
};
EditorStat.prototype.cleanupShutdown = async function (key) {
  delete this.shutdown[key];
};
EditorStat.prototype.setLicense = async function (key, val) {
  this.license[key] = val;
};
EditorStat.prototype.getLicense = async function (key) {
  return this.license[key] || null;
};
EditorStat.prototype.removeLicense = async function (key) {
  delete this.license[key];
};
EditorStat.prototype.lockNotification = async function (ctx, notificationType, ttl) {
  //true NaN !== NaN
  return this._checkAndLock(ctx, notificationType, notificationType, NaN, ttl);
};
EditorStat.prototype.deleteKey = async function (_key) {
  //no need
};

module.exports = {
  EditorData,
  EditorStat
};
