/*global BigInt, FOXX_QUEUE_VERSION, FOXX_QUEUE_VERSION_BUMP */
'use strict';
// //////////////////////////////////////////////////////////////////////////////
// / DISCLAIMER
// /
// / Copyright 2014-2024 ArangoDB GmbH, Cologne, Germany
// / Copyright 2004-2014 triAGENS GmbH, Cologne, Germany
// /
// / Licensed under the Business Source License 1.1 (the "License");
// / you may not use this file except in compliance with the License.
// / You may obtain a copy of the License at
// /
// /     https://github.com/arangodb/arangodb/blob/devel/LICENSE
// /
// / Unless required by applicable law or agreed to in writing, software
// / distributed under the License is distributed on an "AS IS" BASIS,
// / WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// / See the License for the specific language governing permissions and
// / limitations under the License.
// /
// / Copyright holder is ArangoDB GmbH, Cologne, Germany
// /
// //////////////////////////////////////////////////////////////////////////////

const cluster = require('@arangodb/cluster');
const isCluster = cluster.isCluster();
const tasks = require('@arangodb/tasks');
const db = require('@arangodb').db;
const foxxManager = require('@arangodb/foxx/manager');
const wait = require('internal').wait;
const warn = require('console').warn;
const errors = require('internal').errors;

const coordinatorId = (
  isCluster && cluster.isCoordinator()
  ? cluster.coordinatorId()
  : undefined
);

let ensureDefaultQueue = function () {
  if (!global.KEY_GET('queue-control', 'default-queue')) {
    try {
      const queues = require('@arangodb/foxx/queues');
      queues.create('default');
      global.KEY_SET('queue-control', 'default-queue', 1);
    } catch (err) {}
  }
};

var runInDatabase = function () {
  var busy = false;
  db._executeTransaction({
    collections: {
      read: ['_queues', '_jobs'],
      exclusive: ['_jobs']
    },
    action: function () {
      db._queues.all().toArray()
        .forEach(function (queue) {
          var numBusy = db._jobs.byExample({
            queue: queue._key,
            status: 'progress'
          }).count();

          if (numBusy >= queue.maxWorkers) {
            busy = true;
            return;
          }

          var now = Date.now();
          var max = queue.maxWorkers - numBusy;
          var queueName = queue._key;
          var query = global.aqlQuery`/*findPendingJobs*/ FOR job IN _jobs
            FILTER ((job.queue      == ${queueName}) &&
                    (job.status     == 'pending') &&
                    (job.delayUntil <= ${now}))
            SORT job.delayUntil ASC LIMIT ${max} RETURN job`;

          var jobs = db._query(query).toArray();

          if (jobs.length > 0) {
            busy = true;
          }

          jobs.forEach(function (job) {
            const update = {
              status: 'progress'
            };
            if (isCluster) {
              update.startedBy = coordinatorId;
            }
            const updateQuery = global.aqlQuery`
            UPDATE ${job} WITH ${update} IN _jobs
            `;
            updateQuery.options = { ttl: 5, maxRuntime: 5 };

            db._query(updateQuery);

            // generate unique task id (uniqueness only required during
            // runtime of arangod process)
            const id = "foxx-queue-worker-" + require("@arangodb/crypto").uuidv4();

            tasks.register({
              id,
              command: function (cfg) {
                var db = require('@arangodb').db;
                var initialDatabase = db._name();
                db._useDatabase(cfg.db);
                try {
                  require('@arangodb/foxx/queues/worker').work(cfg.job);
                } catch (e) {}
                db._useDatabase(initialDatabase);
              },
              offset: 0,
              isSystem: true,
              params: {
                job: Object.assign({}, job, {
                  status: 'progress'
                }),
                db: db._name()
              }
            });
          });
        });
    }
  });
  if (!busy) {
    require('@arangodb/foxx/queues')._updateQueueDelay();
  }
};

//
// If a Foxxmaster failover happened it can be the case that
// some jobs were in state 'progress' on the failed Foxxmaster.
// This procedure resets these jobs on all databases to 'pending'
// state to restart execution.
//
// Since the failed Foxxmaster might have held a lock on the _jobs
// collection, we have to retry sufficiently long, keeping in mind
// that while retrying, the database might be deleted or the server
// might be shut down.
//
const resetDeadJobs = function () {
  const queues = require('@arangodb/foxx/queues');
  var query = global.aqlQuery`/*resetDeadJobs*/ FOR doc IN _jobs
        FILTER doc.status == 'progress'
          UPDATE doc
        WITH { status: 'pending' }
        IN _jobs`;
  query.options = { ttl: 5, maxRuntime: 5 };

  const initialDatabase = db._name();
  db._databases().forEach(function (name) {
    var done = false;
    // The below code retries under the assumption that it should be
    // sufficient that
    //   * the database exists
    //   * the collection _jobs exists
    //   * we are not shutting down
    // for this operation to eventually succeed work.
    // If any one of the above conditions is violated we abort,
    // otherwise we retry for some time (currently a hard-coded minute)
    var maxTries = 6;
    while (!done && maxTries > 0) {
      try {
        // this will throw when DB does not exist (anymore)
        db._useDatabase(name);

        // this might throw if the _jobs collection does not
        // exist (or the database was deleted between the
        // statement above and now...)
        db._query(query);

        // Now the jobs are reset
        if (!isCluster) {
          queues._updateQueueDelay();
        } else {
          global.KEYSPACE_CREATE('queue-control', 1, true);
        }
        done = true;
      } catch (e) {
        if (e.errorNum === errors.ERROR_SHUTTING_DOWN.code) {
          warn("Shutting down while resetting dead jobs on database " + name + ", aborting.");
          done = true; // we're quitting because shutdown is in progress
        } else if (e.errorNum === errors.ERROR_ARANGO_DATA_SOURCE_NOT_FOUND.code) {
          warn("'_jobs' collection not found while resetting dead jobs on database " + name + ", aborting.");
          done = true; // we're quitting because the _jobs collection is missing
        } else if (e.errorNum === errors.ERROR_ARANGO_READ_ONLY.code) {
          warn("'_jobs' collection is read only while resetting dead jobs on database " + name + ", aborting.");
          done = true;
        } else {
          maxTries--;
          warn("Exception while resetting dead jobs on database " + name + ": " + e.message +
               ", retrying in 10s. " + maxTries + " retries left.");
          wait(10);
        }
      }
    }
  });
  db._useDatabase(initialDatabase);
};

const resetDeadJobsOnFirstRun = function () {
  const foxxmasterInitialized
    = global.KEY_GET('queue-control', 'foxxmasterInitialized') || 0;
  const foxxmasterSince = global.ArangoServerState.getFoxxmasterSince();

  if (foxxmasterSince <= 0) {
    console.error(
      "It's unknown since when this is a Foxxmaster. "
      + 'This is probably a bug in the Foxx queues, please report this error.'
    );
  }
  if (foxxmasterInitialized > foxxmasterSince) {
    console.error(
      'HLC seems to have decreased, which can never happen. '
      + 'This is probably a bug in the Foxx queues, please report this error.'
    );
  }

  if (foxxmasterInitialized < foxxmasterSince) {
    // We've not run this (successfully) since we got to be Foxxmaster.
    resetDeadJobs();
    global.KEY_SET('queue-control', 'foxxmasterInitialized', foxxmasterSince);
  }
};

exports.manage = function () {
  ensureDefaultQueue();

  if (!global.ArangoServerState.isFoxxmaster()) {
    if (isCluster) {
      // publish our own queue updates to the agency even if we exit early.
      // this coordinator, though not the Foxxmaster, could have been used
      // for inserting new queue jobs
      FOXX_QUEUE_VERSION_BUMP();
    }
    return;
  }

  if (global.ArangoServerState.getFoxxmasterQueueupdate()) {
    // do not call again immediately
    global.ArangoServerState.setFoxxmasterQueueupdate(false);

    try {
      // Reset jobs before updating the queue delay. Don't continue on errors,
      // but retry later.
      resetDeadJobsOnFirstRun();
      if (isCluster) {
        let foxxQueues = require('@arangodb/foxx/queues');
        foxxQueues._updateQueueDelay();
      }
    } catch (err) {
      // an error occurred. we need to reinstantiate the queue
      // update, so in the next round the code for the queue update
      // will be run
      global.ArangoServerState.setFoxxmasterQueueupdate(true);
      if (err.errorNum === errors.ERROR_SHUTTING_DOWN.code) {
        return;
      }
      throw err;
    }
  }

  db._useDatabase("_system");
  
  let recheckAllQueues = false;
  let clusterQueueVersion = BigInt("0");
  if (isCluster) {
    // check the cluster-wide queue version
    clusterQueueVersion = BigInt(FOXX_QUEUE_VERSION() || 0);
    // compare to our locally stored version
    let localQueueVersion = BigInt(global.KEY_GET('queue-control', 'version') || 0);

    if (clusterQueueVersion > localQueueVersion) {
      // queue was updated on another coordinator
      recheckAllQueues = true;
    }
  }

  db._databases().forEach(function (database) {
    try {
      db._useDatabase(database);
      global.KEYSPACE_CREATE('queue-control', 1, true);
      let delayUntil = global.KEY_GET('queue-control', 'delayUntil') || 0;

      if (!recheckAllQueues &&
          (delayUntil === -1 || delayUntil > Date.now())) {
        return;
      }

      const queues = db._collection('_queues');
      const jobs = db._collection('_jobs');

      if (!queues || !jobs) {
        // _queues or _jobs collections do not exist
        global.KEY_SET('queue-control', 'delayUntil', -1);
      } else if (!recheckAllQueues && (!queues.count() || !jobs.count())) {
        // _queues or _jobs collections do exist, but are empty
        global.KEY_SET('queue-control', 'delayUntil', -1);
      } else {
        global.KEY_SET('queue-control', 'delayUntil', 0);
        runInDatabase();
      }
    } catch (e) {
      // it is possible that the underlying database is deleted while we are in here.
      // this is not an error
      if (e.errorNum !== errors.ERROR_ARANGO_DATABASE_NOT_FOUND.code &&
          e.errorNum !== errors.ERROR_SHUTTING_DOWN.code) {
        warn("An exception occurred during Foxx queue handling in database '"
              + database + "' "
              + e.message + ": "
              + JSON.stringify(e));
        // noop
      }
    }
  });

  // switch back into previous database
  db._useDatabase('_system');

  if (isCluster) {
    if (recheckAllQueues) {
      // once we have rechecked all queues, we can update our local queue version
      // to the version we checked for
      global.KEY_SET('queue-control', 'version', clusterQueueVersion.toString());
    }

    // publish our own queue updates to the agency
    FOXX_QUEUE_VERSION_BUMP();
  }
};

exports.run = function () {
  let period = global.FOXX_QUEUES_POLL_INTERVAL;

  // disable foxx queues
  if (period < 0) {
    return;
  }

  // this function is called at server startup. we must not do
  // anything expensive here, or anything that could block the
  // startup procedure

  // wakeup/poll interval for Foxx queues
  global.KEYSPACE_CREATE('queue-control', 1, true);
  if (!isCluster) {
    ensureDefaultQueue();
    resetDeadJobs();
  }

  if (tasks.register !== undefined) {
    // move the actual foxx queue operations execution to a background task
    tasks.register({
      id: "foxx-queue-manager",
      command: function () {
        require('@arangodb/foxx/queues/manager').manage();
      },
      period: period,
      isSystem: true
    });
  }
};
