'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 internal = require('internal');
const arangodb = require('@arangodb');
const fs = require('fs');
const joi = require('joi');
const dd = require('dedent');
const errors = require('@arangodb').errors;
const FoxxManager = require('@arangodb/foxx/manager');
const store = require('@arangodb/foxx/store');
const cluster = require('@arangodb/cluster');
const request = require('@arangodb/request');
const FoxxGenerator = require('./generator');
const fmu = require('@arangodb/foxx/manager-utils');
const createRouter = require('@arangodb/foxx/router');
const joinPath = require('path').join;
const ArangoError = arangodb.ArangoError;

const DEFAULT_THUMBNAIL = module.context.fileName('default-thumbnail.png');

const anonymousRouter = createRouter();
const router = createRouter();
anonymousRouter.use(router);

module.exports = anonymousRouter;

router.use((req, res, next) => {
  if (internal.authenticationEnabled()) {
    if (!req.authorized) {
      res.throw('unauthorized');
    }
  }
  next();
})
.header('authorization', joi.string().optional(), 'ArangoDB credentials.');

const foxxRouter = createRouter();
router.use(foxxRouter)
.queryParam('mount', joi.string().required(), dd`
  The mount point of the service. Has to be url-encoded.
`);

const installer = createRouter();
foxxRouter.use(installer)
.queryParam('legacy', joi.boolean().default(false), dd`
  Flag to install the service in legacy mode.
`)
.queryParam('upgrade', joi.boolean().default(false), dd`
  Flag to upgrade the service installed at the mount point.
`)
.queryParam('replace', joi.boolean().default(false), dd`
  Flag to replace the service installed at the mount point.
`)
.queryParam('setup', joi.boolean().default(true), dd`
  Flag to run setup after install.
`)
.queryParam('teardown', joi.boolean().default(false), dd`
  Flag to run teardown before replace/upgrade.
`);

installer.use(function (req, res, next) {
  let coordinatorId = null;
  try {
    coordinatorId = JSON.parse(req.body).coordinatorId;
  } catch (e) {
    // noop
  }
  const thisCoordinatorId = FoxxManager._getMyCoordinatorId();
  if (thisCoordinatorId && coordinatorId && thisCoordinatorId !== coordinatorId) {
    // In case our original received request does not match the provided
    // coordinatorId (if available), we don't want to upgrade/replace or install
    // the foxx application on this coordinator, as another coordinator will
    // perform the actual operation.

    let coordinatorToEndpointMap = {};
    const coordinators = global.ArangoClusterInfo.getCoordinators();
    coordinators.forEach((coordinatorId) => {
      const endpoint = global.ArangoClusterInfo.getServerEndpoint(coordinatorId);
      coordinatorToEndpointMap[coordinatorId] = cluster.endpointToURL(endpoint);
    });

    const endpointToUse = coordinatorToEndpointMap[coordinatorId];
    if (!endpointToUse) {
      // if we could not map the supplied coordinatorId to a real coordinatorId, it must be invalid.
      throw new ArangoError({
        errorNum: errors.ERROR_ARANGO_ILLEGAL_NAME.code,
        errorMessage: 'Supplied wrong coordinatorId'
      });
    }

    const response = request[req.method.toLowerCase()]({
      url: `${endpointToUse}${req.originalUrl}`,
      headers: req.headers,
      body: req.body,
      json: false,
    });

    res.status(response.statusCode);
    res.set(response.headers);
    res.body = response.body;
    return;
  }
  
  const mount = decodeURIComponent(req.queryParams.mount);
  const upgrade = req.queryParams.upgrade;
  const replace = req.queryParams.replace;
  next();
  const options = {};
  const appInfo = req.body;
  options.legacy = req.queryParams.legacy;
  options.setup = req.queryParams.setup;
  options.teardown = req.queryParams.teardown;
  let service;

  try {
    if (upgrade) {
      service = FoxxManager.upgrade(appInfo, mount, options);
    } else if (replace) {
      service = FoxxManager.replace(appInfo, mount, options);
    } else {
      service = FoxxManager.install(appInfo, mount, options);
    }
  } catch (e) {
    if (e.isArangoError && [
      errors.ERROR_MODULE_FAILURE.code,
      errors.ERROR_MALFORMED_MANIFEST_FILE.code,
      errors.ERROR_INVALID_SERVICE_MANIFEST.code
    ].indexOf(e.errorNum) !== -1) {
      res.throw('bad request', e);
    }
    if (
      e.isArangoError &&
      e.errorNum === errors.ERROR_SERVICE_SOURCE_ERROR.code
    ) {
      let errorType = 'internal server error';
      if ([
        '/foxxes/store',
        '/foxxes/git',
        '/foxxes/url'
      ].indexOf(req.path) !== -1) {
        errorType = (
          e.message.includes('(timeout after') ?
          'gateway timeout' : 'bad gateway'
        );
      }
      res.throw(errorType, e);
    }
    if (
      e.isArangoError &&
      e.errorNum === errors.ERROR_SERVICE_NOT_FOUND.code
    ) {
      res.throw('not found', e);
    }
    if (
      e.isArangoError &&
      e.errorNum === errors.ERROR_SERVICE_MOUNTPOINT_CONFLICT.code
    ) {
      res.throw('conflict', e);
    }
    throw e;
  }

  const configuration = service.getConfiguration();
  res.json(Object.assign({
    error: false,
    configuration
  }, service.simpleJSON()));
});

installer.put('/store', function (req) {
  req.body = `${req.body.name}:${req.body.version}`;
})
.body(joi.object({
  name: joi.string().required(),
  version: joi.string().required()
}).required(), 'A Foxx service from the store.')
.summary('Install a Foxx from the store')
.description(dd`
  Downloads a Foxx from the store and installs it at the given mount.
`);

installer.put('/git', function (req) {
  const baseUrl = process.env.FOXX_BASE_URL || 'https://github.com/';
  req.body = `${baseUrl}${req.body.url}/archive/${req.body.version || 'master'}.zip`;
})
.body(joi.object({
  url: joi.string().required(),
  version: joi.string().default('master')
}).required(), 'A GitHub reference.')
.summary('Install a Foxx from Github')
.description(dd`
  Install a Foxx with user/repository and version.
`);

installer.put('/url', function (req) {
  req.body = `${req.body.url}`;
})
.body(joi.object({
  url: joi.string().required(),
  version: joi.string().default('master')
}).required(), '')
.summary('Install a Foxx from URL')
.description(dd`
  Install a Foxx from URL.
`);

installer.put('/generate', (req, res) => {
  const tempDir = fs.getTempFile('aardvark', false);
  const generated = FoxxGenerator.generate(req.body);
  FoxxGenerator.write(tempDir, generated.files, generated.folders);
  const tempFile = fs.zipDirectory(tempDir);
  req.body = fs.readFileSync(tempFile);
  try {
    fs.removeDirectoryRecursive(tempDir, true);
  } catch (e) {
    console.warn(`Failed to remove temporary Foxx generator folder: ${tempDir}`);
  }
  try {
    fs.remove(tempFile);
  } catch (e) {
    console.warn(`Failed to remove temporary Foxx generator bundle: ${tempFile}`);
  }
})
.body(joi.object().required(), 'A Foxx generator configuration.')
.summary('Generate a new foxx')
.description(dd`
  Generate a new empty foxx on the given mount point.
`);

installer.put('/zip', function (req) {
  const tempFile = joinPath(fs.getTempPath(), req.body.zipFile);
  req.body = fs.readFileSync(tempFile);
  try {
    fs.remove(tempFile);
  } catch (e) {
    console.warn(`Failed to remove uploaded file: ${tempFile}`);
  }
})
.body(joi.object({
  zipFile: joi.string().required(),
  coordinatorId: joi.string().optional()
}).required(), 'A zip file path.')
.summary('Install a Foxx from temporary zip file')
.description(dd`
  Install a Foxx from the given zip path.
  This path has to be created via _api/upload.
`);

installer.put('/raw', function (req) {
  req.body = req.rawBody;
})
.summary('Install a Foxx from a direct upload')
.description(dd`
  Install a Foxx from raw request body.
`);

foxxRouter.delete('/', function (req, res) {
  const mount = decodeURIComponent(req.queryParams.mount);
  const runTeardown = req.queryParams.teardown;
  const service = FoxxManager.uninstall(mount, {
    teardown: runTeardown,
    force: true
  });
  res.json(Object.assign(
    {error: false},
    service.simpleJSON()
  ));
})
.queryParam('teardown', joi.boolean().default(true))
.summary('Uninstall a Foxx')
.description(dd`
  Uninstall the Foxx at the given mount-point.
`);

router.get('/', function (req, res) {
  res.json(FoxxManager.installedServices().map(service => ({
    mount: service.mount,
    name: service.manifest.name,
    description: service.manifest.description,
    author: service.manifest.author,
    system: service.isSystem,
    development: service.isDevelopment,
    contributors: service.manifest.contributors || false,
    license: service.manifest.license,
    version: service.manifest.version,
    path: service.basePath,
    config: service.getConfiguration(),
    deps: service.getDependencies(),
    scripts: service.getScripts(),
    readme: service.readme
  })));
})
.summary('List all Foxxes')
.description(dd`
  Get a List of all running foxxes.
`);

foxxRouter.get('/thumbnail', function (req, res) {
  const mount = decodeURIComponent(req.queryParams.mount);
  const service = FoxxManager.lookupService(mount);
  res.sendFile(service.thumbnail || DEFAULT_THUMBNAIL);
})
.summary('Get the thumbnail of a Foxx')
.description(dd`
  Request the thumbnail of the given Foxx in order to display it on the screen.
`);

foxxRouter.get('/config', function (req, res) {
  const mount = decodeURIComponent(req.queryParams.mount);
  const service = FoxxManager.lookupService(mount);
  res.json(service.getConfiguration());
})
.summary('Get the configuration for a service')
.description(dd`
  Used to request the configuration options for services.
`);

foxxRouter.get('/deps', function (req, res) {
  const mount = decodeURIComponent(req.queryParams.mount);
  const service = FoxxManager.lookupService(mount);
  const deps = service.getDependencies();
  for (const key of Object.keys(deps)) {
    const dep = deps[key];
    deps[key] = {
      definition: dep,
      title: dep.title,
      current: dep.current
    };
    delete dep.title;
    delete dep.current;
  }
  res.json(deps);
})
.summary('Get the dependencies for a service')
.description(dd`
  Used to request the dependencies options for services.
`);

foxxRouter.patch('/config', function (req, res) {
  const mount = decodeURIComponent(req.queryParams.mount);
  const configuration = req.body;
  const service = FoxxManager.lookupService(mount);
  FoxxManager.setConfiguration(mount, {
    configuration,
    replace: !service.isDevelopment
  });
  res.json(service.getConfiguration());
})
.body(joi.object().optional(), 'Configuration to apply.')
.summary('Set the configuration for a service')
.description(dd`
  Used to overwrite the configuration options for services.
`);

foxxRouter.patch('/deps', function (req, res) {
  const mount = decodeURIComponent(req.queryParams.mount);
  const dependencies = req.body;
  const service = FoxxManager.lookupService(mount);
  FoxxManager.setDependencies(mount, {
    dependencies,
    replace: !service.isDevelopment
  });
  res.json(service.getDependencies());
})
.body(joi.object().optional(), 'Dependency options to apply.')
.summary('Set the dependencies for a service')
.description(dd`
  Used to overwrite the dependencies options for services.
`);

foxxRouter.post('/tests', function (req, res) {
  const mount = decodeURIComponent(req.queryParams.mount);
  res.json(FoxxManager.runTests(mount, req.body));
})
.body(joi.object().optional(), 'Options to pass to the test runner.')
.summary('Run tests for a service')
.description(dd`
  Used to run the tests of a service.
`);

foxxRouter.post('/scripts/:name', function (req, res) {
  const mount = decodeURIComponent(req.queryParams.mount);
  const name = req.pathParams.name;
  try {
    res.json(FoxxManager.runScript(name, mount, req.body));
  } catch (e) {
    throw e.cause || e;
  }
})
.pathParam('name', joi.string().required(), 'The name of a service\'s script to run.')
.body(joi.any().default(null), 'Options to pass to the script.')
.summary('Run a script for a service')
.description(dd`
  Used to trigger any script of a service.
`);

foxxRouter.patch('/devel', function (req, res) {
  const mount = decodeURIComponent(req.queryParams.mount);
  const activate = Boolean(req.body);
  res.json(FoxxManager[activate ? 'development' : 'production'](mount).simpleJSON());
})
.body(joi.boolean().optional())
.summary('Activate/Deactivate development mode for a service')
.description(dd`
  Used to toggle between production and development mode.
`);

router.get('/fishbowl', function (req, res) {
  if (internal.isFoxxStoreDisabled()) {
    res.json([]);
  } else {
    try {
      store.update();
    } catch (e) {
      console.warn('Failed to update Foxx store from GitHub.');
    }
    res.json(store.availableJson());
  } 
})
.summary('List of all Foxx services submitted to the Foxx store.')
.description(dd`
  This function contacts the fishbowl and reports which services are available for install.
`);


anonymousRouter.use('/docs/standalone', module.context.createDocumentationRouter((req, res) => {
  if (req.suffix === 'swagger.json' && !req.authorized && internal.authenticationEnabled()) {
    res.throw('unauthorized');
  }
  return {
    mount: decodeURIComponent(req.queryParams.mount)
  };
}));

anonymousRouter.use('/docs', module.context.createDocumentationRouter((req, res) => {
  if (req.suffix === 'swagger.json' && !req.authorized && internal.authenticationEnabled()) {
    res.throw('unauthorized');
  }
  return {
    mount: decodeURIComponent(req.queryParams.mount),
    indexFile: 'index.html'
  };
}));
