/*
 * (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 configCoAuthoring = config.get('services.CoAuthoring');
const co = require('co');
const pubsubService = require('./pubsubRabbitMQ');
const commonDefines = require('./../../Common/sources/commondefines');
const constants = require('./../../Common/sources/constants');
const utils = require('./../../Common/sources/utils');
const storage = require('./../../Common/sources/storage/storage-base');
const queueService = require('./../../Common/sources/taskqueueRabbitMQ');
const operationContext = require('./../../Common/sources/operationContext');
const sqlBase = require('./databaseConnectors/baseConnector');
const docsCoServer = require('./DocsCoServer');
const taskResult = require('./taskresult');
const cfgEditorDataStorage = config.get('services.CoAuthoring.server.editorDataStorage');
const cfgEditorStatStorage = config.get('services.CoAuthoring.server.editorStatStorage');
const editorStatStorage = require('./' + (cfgEditorStatStorage || cfgEditorDataStorage));

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

const cfgRedisPrefix = configCoAuthoring.get('redis.prefix');
const redisKeyShutdown = cfgRedisPrefix + constants.REDIS_KEY_SHUTDOWN;

const WAIT_TIMEOUT = 30000;
const LOOP_TIMEOUT = 1000;
const EXEC_TIMEOUT = WAIT_TIMEOUT + utils.getConvertionTimeout(undefined);

const addSqlParam = sqlBase.addSqlParameter;

function updateDoc(ctx, docId, status, callback) {
  return new Promise((resolve, reject) => {
    const values = [];
    const p1 = addSqlParam(status, values);
    const p2 = addSqlParam(callback, values);
    const p3 = addSqlParam(ctx.tenant, values);
    const p4 = addSqlParam(docId, values);
    const sqlCommand = `UPDATE ${cfgTableResult} SET status=${p1},callback=${p2} WHERE tenant=${p3} AND id=${p4};`;
    sqlBase.sqlQuery(
      ctx,
      sqlCommand,
      (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      },
      undefined,
      undefined,
      values
    );
  });
}

function shutdown() {
  return co(function* () {
    let res = true;
    const ctx = new operationContext.Context();
    try {
      const editorStat = editorStatStorage.EditorStat ? new editorStatStorage.EditorStat() : new editorStatStorage();
      ctx.logger.debug('shutdown start:' + EXEC_TIMEOUT);

      //redisKeyShutdown is not a simple counter, so it doesn't get decremented by a build that started before Shutdown started
      //reset redisKeyShutdown just in case the previous run didn't finish yield editorData.cleanupShutdown(redisKeyShutdown);
      const queue = new queueService();
      yield queue.initPromise(true, false, false, false, false, false);

      const pubsub = new pubsubService();
      yield pubsub.initPromise();
      //inner ping to update presence
      ctx.logger.debug('shutdown pubsub shutdown message');
      yield pubsub.publish(JSON.stringify({type: commonDefines.c_oPublishType.shutdown, ctx, status: true}));
      ctx.logger.debug('shutdown start wait pubsub deliver');
      yield utils.sleep(LOOP_TIMEOUT);

      const documentsWithChanges = yield sqlBase.getDocumentsWithChanges(ctx);
      ctx.logger.debug('shutdown docs with changes count = %s', documentsWithChanges.length);
      const docsWithEmptyForgotten = [];
      const docsWithOutOfDateForgotten = [];
      for (let i = 0; i < documentsWithChanges.length; ++i) {
        const tenant = documentsWithChanges[i].tenant;
        const docId = documentsWithChanges[i].id;
        ctx.setTenant(tenant);
        const forgotten = yield storage.listObjects(ctx, docId, cfgForgottenFiles);
        if (forgotten.length > 0) {
          const selectRes = yield taskResult.select(ctx, docId);
          if (selectRes.length > 0) {
            const row = selectRes[0];
            if (commonDefines.FileStatus.SaveVersion !== row.status && commonDefines.FileStatus.UpdateVersion !== row.status) {
              docsWithOutOfDateForgotten.push([tenant, docId]);
            }
          }
        } else {
          docsWithEmptyForgotten.push([tenant, docId]);
        }
      }
      ctx.initDefault();
      ctx.logger.debug('shutdown docs with changes and empty forgotten count = %s', docsWithEmptyForgotten.length);
      ctx.logger.debug('shutdown docs with changes and out of date forgotten count = %s', docsWithOutOfDateForgotten.length);
      const docsToConvert = docsWithEmptyForgotten.concat(docsWithOutOfDateForgotten);
      for (let i = 0; i < docsToConvert.length; ++i) {
        const tenant = docsToConvert[i][0];
        const docId = docsToConvert[i][1];
        //todo refactor. group tenants?
        ctx.setTenant(tenant);
        yield ctx.initTenantCache();

        yield updateDoc(ctx, docId, commonDefines.FileStatus.Ok, '');
        yield editorStat.addShutdown(redisKeyShutdown, docId);
        ctx.logger.debug('shutdown createSaveTimerPromise %s', docId);
        yield docsCoServer.createSaveTimer(ctx, docId, null, null, null, queue, true);
      }
      ctx.initDefault();
      //sleep because of bugs in createSaveTimerPromise
      yield utils.sleep(LOOP_TIMEOUT);

      const startTime = new Date().getTime();
      while (true) {
        const remainingFiles = yield editorStat.getShutdownCount(redisKeyShutdown);
        ctx.logger.debug('shutdown remaining files:%d', remainingFiles);
        const curTime = new Date().getTime() - startTime;
        if (curTime >= EXEC_TIMEOUT || remainingFiles <= 0) {
          if (curTime >= EXEC_TIMEOUT) {
            ctx.logger.debug('shutdown timeout');
          }
          break;
        }
        yield utils.sleep(LOOP_TIMEOUT);
      }
      let countInForgotten = 0;
      for (let i = 0; i < docsToConvert.length; ++i) {
        const tenant = docsToConvert[i][0];
        const docId = docsToConvert[i][1];
        ctx.setTenant(tenant);
        const forgotten = yield storage.listObjects(ctx, docId, cfgForgottenFiles);
        if (forgotten.length > 0) {
          countInForgotten++;
        } else {
          ctx.logger.warn('shutdown missing in forgotten:%s', docId);
        }
      }
      ctx.initDefault();
      ctx.logger.debug('shutdown docs placed in forgotten:%d', countInForgotten);
      ctx.logger.debug('shutdown docs with unknown status:%d', docsToConvert.length - countInForgotten);

      //todo needs to check queues, because there may be long conversions running before Shutdown
      //clean up
      yield editorStat.cleanupShutdown(redisKeyShutdown);
      yield pubsub.close();
      yield queue.close();

      ctx.logger.debug('shutdown end');
    } catch (e) {
      res = false;
      ctx.logger.error('shutdown error:\r\n%s', e.stack);
    }
    process.exit(0);
    return res;
  });
}
exports.shutdown = shutdown;
shutdown();
