/*
 * (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 co = require('co');
const cron = require('cron');
const ms = require('ms');
const taskResult = require('./taskresult');
const docsCoServer = require('./DocsCoServer');
const canvasService = require('./canvasservice');
const commondefines = require('./../../Common/sources/commondefines');
const queueService = require('./../../Common/sources/taskqueueRabbitMQ');
const operationContext = require('./../../Common/sources/operationContext');
const pubsubService = require('./pubsubRabbitMQ');
const sqlBase = require('./databaseConnectors/baseConnector');

const cfgExpFilesCron = config.get('services.CoAuthoring.expire.filesCron');
const cfgExpDocumentsCron = config.get('services.CoAuthoring.expire.documentsCron');
const cfgExpFiles = config.get('services.CoAuthoring.expire.files');
const cfgExpFilesRemovedAtOnce = config.get('services.CoAuthoring.expire.filesremovedatonce');
const cfgForceSaveStep = config.get('services.CoAuthoring.autoAssembly.step');

function getCronStep(cronTime) {
  const cronJob = new cron.CronJob(cronTime, () => {});
  const dates = cronJob.nextDates(2);
  return dates[1] - dates[0];
}
const expFilesStep = getCronStep(cfgExpFilesCron);
const expDocumentsStep = getCronStep(cfgExpDocumentsCron);

const checkFileExpire = function (expireSeconds) {
  return co(function* () {
    const ctx = new operationContext.Context();
    let currentExpFilesStep = expFilesStep;
    try {
      ctx.logger.info('checkFileExpire start');
      yield ctx.initTenantCache();
      const expFiles = ctx.getCfg('services.CoAuthoring.expire.files', cfgExpFiles);
      const expFilesRemovedAtOnce = ctx.getCfg('services.CoAuthoring.expire.filesremovedatonce', cfgExpFilesRemovedAtOnce);
      const currentFilesCron = ctx.getCfg('services.CoAuthoring.expire.filesCron', cfgExpFilesCron);
      currentExpFilesStep = getCronStep(currentFilesCron);

      let removedCount = 0;
      let expired;
      let currentRemovedCount;
      do {
        currentRemovedCount = 0;
        expired = yield taskResult.getExpired(ctx, expFilesRemovedAtOnce, expireSeconds ?? expFiles);

        expired.sort((a, b) => a.tenant.localeCompare(b.tenant));
        let currentTenant = null;

        for (let i = 0; i < expired.length; ++i) {
          const tenant = expired[i].tenant;
          const docId = expired[i].id;
          const shardKey = sqlBase.DocumentAdditional.prototype.getShardKey(expired[i].additional);
          const wopiSrc = sqlBase.DocumentAdditional.prototype.getWopiSrc(expired[i].additional);

          try {
            if (currentTenant !== tenant) {
              ctx.init(tenant, docId, ctx.userId, shardKey, wopiSrc);
              yield ctx.initTenantCache();
              currentTenant = tenant;
            } else {
              ctx.setDocId(docId);
              ctx.setShardKey(shardKey);
              ctx.setWopiSrc(wopiSrc);
            }

            //todo tenant
            //check that no one is in the document
            const editorsCount = yield docsCoServer.getEditorsCountPromise(ctx, docId);
            if (0 === editorsCount) {
              if (yield canvasService.cleanupCache(ctx, docId)) {
                currentRemovedCount++;
              }
            } else {
              ctx.logger.debug('checkFileExpire expire but presence: editorsCount = %d', editorsCount);
            }
          } catch (error) {
            ctx.logger.error('checkFileExpire file error: %s', error.stack);
            // Continue processing other files
          }
        }
        removedCount += currentRemovedCount;
      } while (currentRemovedCount > 0);
      ctx.initDefault();
      ctx.logger.info('checkFileExpire end: removedCount = %d', removedCount);
    } catch (e) {
      ctx.logger.error('checkFileExpire error: %s', e.stack);
    } finally {
      setTimeout(checkFileExpire, currentExpFilesStep);
    }
  });
};
const checkDocumentExpire = function () {
  return co(function* () {
    let queue = null;
    let removedCount = 0;
    let startSaveCount = 0;
    let currentExpDocumentsStep = expDocumentsStep;
    const ctx = new operationContext.Context();
    try {
      ctx.logger.info('checkDocumentExpire start');
      yield ctx.initTenantCache();
      const currentDocumentsCron = ctx.getCfg('services.CoAuthoring.expire.documentsCron', cfgExpDocumentsCron);
      currentExpDocumentsStep = getCronStep(currentDocumentsCron);
      const now = new Date().getTime();
      const expiredKeys = yield docsCoServer.editorData.getDocumentPresenceExpired(now);
      if (expiredKeys.length > 0) {
        queue = new queueService();
        yield queue.initPromise(true, false, false, false, false, false);

        expiredKeys.sort((a, b) => a[0].localeCompare(b[0]));
        let currentTenant = null;

        for (let i = 0; i < expiredKeys.length; ++i) {
          const tenant = expiredKeys[i][0];
          const docId = expiredKeys[i][1];
          if (docId) {
            if (currentTenant !== tenant) {
              ctx.init(tenant, docId, ctx.userId);
              yield ctx.initTenantCache();
              currentTenant = tenant;
            } else {
              ctx.setDocId(docId);
            }

            const hasChanges = yield docsCoServer.hasChanges(ctx, docId);
            if (hasChanges) {
              //todo opt_initShardKey from getDocumentPresenceExpired data or from db
              yield docsCoServer.createSaveTimer(ctx, docId, null, null, null, queue, true, true);
              startSaveCount++;
            } else {
              yield docsCoServer.cleanDocumentOnExitNoChangesPromise(ctx, docId);
              removedCount++;
            }
          }
        }
      }
      ctx.initDefault();
      ctx.logger.info('checkDocumentExpire end: startSaveCount = %d, removedCount = %d', startSaveCount, removedCount);
    } catch (e) {
      ctx.logger.error('checkDocumentExpire error: %s', e.stack);
    } finally {
      try {
        if (queue) {
          yield queue.close();
        }
      } catch (e) {
        ctx.logger.error('checkDocumentExpire error: %s', e.stack);
      }

      setTimeout(checkDocumentExpire, currentExpDocumentsStep);
    }
  });
};
const forceSaveTimeout = function () {
  return co(function* () {
    let queue = null;
    let pubsub = null;
    let currentForceSaveStep = cfgForceSaveStep;
    const ctx = new operationContext.Context();
    try {
      ctx.logger.info('forceSaveTimeout start');
      yield ctx.initTenantCache();
      currentForceSaveStep = ctx.getCfg('services.CoAuthoring.autoAssembly.step', cfgForceSaveStep);
      const now = new Date().getTime();
      const expiredKeys = yield docsCoServer.editorData.getForceSaveTimer(now);
      if (expiredKeys.length > 0) {
        queue = new queueService();
        yield queue.initPromise(true, false, false, false, false, false);

        pubsub = new pubsubService();
        yield pubsub.initPromise();

        expiredKeys.sort((a, b) => a[0].localeCompare(b[0]));

        const actions = [];
        let currentTenant = null;

        for (let i = 0; i < expiredKeys.length; ++i) {
          const tenant = expiredKeys[i][0];
          const docId = expiredKeys[i][1];
          if (docId) {
            if (currentTenant !== tenant) {
              ctx.init(tenant, docId, ctx.userId);
              yield ctx.initTenantCache();
              //todo opt_initShardKey from ForceSave data or from db
              currentTenant = tenant;
            } else {
              ctx.setDocId(docId);
            }

            actions.push(
              docsCoServer.startForceSave(
                ctx,
                docId,
                commondefines.c_oAscForceSaveTypes.Timeout,
                undefined,
                undefined,
                undefined,
                undefined,
                undefined,
                undefined,
                undefined,
                undefined,
                queue,
                pubsub,
                undefined,
                true
              )
            );
          }
        }
        yield Promise.all(actions);
        ctx.logger.debug('forceSaveTimeout actions.length %d', actions.length);
      }
      ctx.initDefault();
      ctx.logger.info('forceSaveTimeout end');
    } catch (e) {
      ctx.logger.error('forceSaveTimeout error: %s', e.stack);
    } finally {
      try {
        if (queue) {
          yield queue.close();
        }
        if (pubsub) {
          yield pubsub.close();
        }
      } catch (e) {
        ctx.logger.error('forceSaveTimeout cleanup error: %s', e.stack);
      }
      setTimeout(forceSaveTimeout, ms(currentForceSaveStep));
    }
  });
};

exports.startGC = function () {
  //runtime config is read on start
  setTimeout(checkDocumentExpire, expDocumentsStep);
  setTimeout(checkFileExpire, expFilesStep);
  setTimeout(forceSaveTimeout, ms(cfgForceSaveStep));
};
exports.getCronStep = getCronStep;
exports.checkFileExpire = checkFileExpire;
