'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 RequestContext = require('@arangodb/foxx/legacy/request_context');
const BaseMiddleware = require('@arangodb/foxx/legacy/base_middleware').BaseMiddleware;
const is = require('@arangodb/is');
const internal = require('@arangodb/foxx/legacy/internals');
const swagger = require('@arangodb/foxx/legacy/swagger');

var authControllerProps = {
  // //////////////////////////////////////////////////////////////////////////////
  // / @brief Get the users of this controller
  // //////////////////////////////////////////////////////////////////////////////
  getUsers() {
    const foxxAuthentication = require('@arangodb/foxx/legacy/authentication');
    return new foxxAuthentication.Users(this.applicationContext);
  },

  // //////////////////////////////////////////////////////////////////////////////
  // / @brief Get the auth object of this controller
  // //////////////////////////////////////////////////////////////////////////////
  getAuth() {
    return this.auth;
  },

  login(route, opts) {
    var authentication = require('@arangodb/foxx/legacy/authentication');
    return this.post(route, authentication.createStandardLoginHandler(this.getAuth(), this.getUsers(), opts));
  },

  logout(route, opts) {
    var authentication = require('@arangodb/foxx/legacy/authentication');
    return this.post(route, authentication.createStandardLogoutHandler(this.getAuth(), opts));
  },

  register(route, opts) {
    var authentication = require('@arangodb/foxx/legacy/authentication');
    return this.post(
      route,
      authentication.createStandardRegistrationHandler(this.getAuth(), this.getUsers(), opts)
    ).errorResponse(authentication.UserAlreadyExistsError, 400, 'User already exists');
  },

  changePassword(route, opts) {
    var authentication = require('@arangodb/foxx/legacy/authentication');
    return this.post(route, authentication.createStandardChangePasswordHandler(this.getUsers(), opts));
  }
};

var sessionControllerProps = {
  // //////////////////////////////////////////////////////////////////////////////
  // / @brief Get the sessions object of this controller
  // //////////////////////////////////////////////////////////////////////////////
  getSessions() {
    return this.sessions;
  },

  // //////////////////////////////////////////////////////////////////////////////
  // / @brief defines a route to logout/destroy the session
  // //////////////////////////////////////////////////////////////////////////////

  destroySession(route, opts) {
    var method = opts.method;
    if (typeof method === 'string') {
      method = method.toLowerCase();
    }
    if (!method || typeof this[method] !== 'function') {
      method = 'post';
    }
    var sessions = require('@arangodb/foxx/legacy/sessions');
    return this[method](route, sessions.createDestroySessionHandler(this.getSessions(), opts));
  }
};

class Controller {

  constructor (context, options) {
    this.currentPriority = 0;

    var urlPrefix, baseMiddleware;

    if (is.notExisty(context)) {
      throw new Error('parameter <context> is missing');
    }

    this.routingInfo = {
      routes: []
    };

    // Models for the Documentation
    this.models = {};

    options = options || {};
    urlPrefix = options.urlPrefix || '';

    if (urlPrefix === '') {
      urlPrefix = context.prefix;
    } else if (context.prefix !== '') {
      urlPrefix = context.prefix + '/' + urlPrefix;
    }

    this.injected = Object.create(null);
    this.injectors = Object.create(null);
    this.routingInfo.urlPrefix = urlPrefix;
    this.collectionPrefix = context.collectionPrefix;

    this.extensions = {};

    baseMiddleware = new BaseMiddleware();

    this.routingInfo.middleware = [
      {
        url: { match: '/*' },
        action: {
          callback: baseMiddleware.functionRepresentation,
          options: {
            name: context.name,
            version: context.version,
            mount: context.mount,
            isDevelopment: context.isDevelopment,
            isProduction: context.isProduction,
            prefix: context.prefix,
            options: context.options
          }
        }
      }
    ];

    this.allRoutes = new RequestContext.Buffer();

    context.foxxes.push(this);

    if (is.existy(context.manifest.rootElement)) {
      this.rootElement = context.manifest.rootElement;
    } else {
      this.rootElement = false;
    }

    this.applicationContext = context;
  }

  addInjector (name, factory) {
    if (factory === undefined) {
      Object.assign(this.injectors, name);
    } else {
      this.injectors[name] = factory;
    }
  }

  // //////////////////////////////////////////////////////////////////////////////
  // /
  // / The *handleRequest* method is the raw way to create a new route. You
  // / probably wont call it directly, but it is used in the other request methods:
  // /
  // / When defining a route you can also define a so called 'parameterized' route
  // / like */goose/:barn*. In this case you can later get the value the user
  // / provided for *barn* via the *params* function (see the Request object).
  // //////////////////////////////////////////////////////////////////////////////

  handleRequest (method, route, callback) {
    var constraints = {queryParams: {}, urlParams: {}};
    var newRoute = internal.constructRoute(method, route, callback, this, constraints);
    var requestContext = new RequestContext(
      this.allRoutes, this.models, newRoute, route, this.rootElement, constraints, this.extensions
    );

    this.routingInfo.routes.push(newRoute);

    if (method === 'post' || method === 'put' || method === 'patch') {
      const Model = require('@arangodb/foxx/legacy').Model;
      let UndocumentedBody = Model.extend({});
      requestContext.bodyParam('undocumented body', {
        description: 'Undocumented body param',
        type: UndocumentedBody,
        allowInvalid: true
      });
    }

    return requestContext;
  }

  head (route, callback) {
    return this.handleRequest('head', route, callback);
  }

  get (route, callback) {
    return this.handleRequest('get', route, callback);
  }

  post (route, callback) {
    return this.handleRequest('post', route, callback);
  }

  put (route, callback) {
    return this.handleRequest('put', route, callback);
  }

  patch (route, callback) {
    return this.handleRequest('patch', route, callback);
  }

  delete (route, callback) {
    return this.handleRequest('delete', route, callback);
  }

  del (route, callback) {
    return this.delete(route, callback);
  }

  before (path, func) {
    if (is.notExisty(func)) {
      func = path;
      path = '/*';
    }

    this.routingInfo.middleware.push({
      priority: this.currentPriority = this.currentPriority + 1,
      url: {match: path},
      action: {
        callback(req, res, opts, next) {
          var result = func(req, res, opts);
          if (result !== false) {
            next();
          }
        }
      }
    });
  }

  after (path, func) {
    if (is.notExisty(func)) {
      func = path;
      path = '/*';
    }

    this.routingInfo.middleware.push({
      priority: this.currentPriority = this.currentPriority + 1,
      url: {match: path},
      action: {
        callback(req, res, opts, next) { next(); func(req, res, opts); }
      }
    });
  }

  around (path, func) {
    if (is.notExisty(func)) {
      func = path;
      path = '/*';
    }

    this.routingInfo.middleware.push({
      priority: this.currentPriority = this.currentPriority + 1,
      url: {match: path},
      action: {
        callback(req, res, opts, next) {
          func(req, res, opts, next);
        }
      }
    });
  }

  activateAuthentication (opts) {
    var authentication = require('@arangodb/foxx/legacy/authentication');
    Object.assign(this, authControllerProps);

    this.auth = authentication.createAuthObject(this.applicationContext, opts);
    this.before('/*', authentication.createAuthenticationMiddleware(this.auth, this.applicationContext));
    this.after('/*', authentication.createSessionUpdateMiddleware());
  }

  // //////////////////////////////////////////////////////////////////////////////
  // / @brief activate sessions with the giveon options for this controller
  // //////////////////////////////////////////////////////////////////////////////

  activateSessions (opts) {
    var sessions = require('@arangodb/foxx/legacy/sessions');
    Object.assign(this, sessionControllerProps);

    this.sessions = new sessions.Sessions(opts);
    sessions.decorateController(this.sessions, this);
  }

  apiDocumentation (route, opts) {
    if (route.charAt(route.length - 1) !== '/') {
      route += '/';
    }
    var mountPath = this.applicationContext.mount;
    return this.get(route + '*', swagger.createSwaggerRouteHandler(mountPath, opts));
  }

  extend (extensions) {
    var attr;
    var extensionWrapper = function (scope, functionName) {
      return function () {
        this.applyChain.push({
          functionName: functionName,
          argumentList: arguments
        });
        return this;
      }.bind(scope);
    };
    for (attr in extensions) {
      if (extensions.hasOwnProperty(attr)) {
        this.extensions[attr] = extensions[attr];
        this.allRoutes[attr] = extensionWrapper(this.allRoutes, attr);
      }
    }
  }

}

exports.Controller = Controller;
