// file      : mod/mod-repository-root.cxx -*- C++ -*-
// license   : MIT; see accompanying LICENSE file

#include <mod/mod-repository-root.hxx>

#include <time.h> // tzset()

#include <cmark-gfm-core-extensions.h>

#include <sstream>

#include <web/server/module.hxx>

#include <mod/module.hxx>
#include <mod/module-options.hxx>

#include <mod/mod-ci.hxx>
#include <mod/mod-submit.hxx>
#include <mod/mod-builds.hxx>
#include <mod/mod-packages.hxx>
#include <mod/mod-build-log.hxx>
#include <mod/mod-build-task.hxx>
#include <mod/mod-build-force.hxx>
#include <mod/mod-build-result.hxx>
#include <mod/mod-build-configs.hxx>
#include <mod/mod-package-details.hxx>
#include <mod/mod-repository-details.hxx>
#include <mod/mod-package-version-details.hxx>

using namespace std;
using namespace brep::cli;

namespace brep
{
  // Request proxy.
  //
  // Removes the first parameter, that is assumed to be a function name, if its
  // value is not present. Otherwise, considers it as the handler's "default"
  // parameter value and renames the parameter to "_".
  //
  class request_proxy: public request
  {
  public:
    request_proxy (request& r): request_ (r) {}

    virtual const path_type&
    path () {return request_.path ();}

    virtual const name_values&
    parameters (size_t limit, bool url_only)
    {
      if (!parameters_ || url_only < url_only_parameters_)
      {
        parameters_ = request_.parameters (limit, url_only);
        assert (!parameters_->empty ()); // Always starts with a function name.

        auto i (parameters_->begin ());
        removed_ = !i->value;

        if (removed_)
          parameters_->erase (i);
        else
          i->name = "_";

        url_only_parameters_ = url_only;
      }

      return *parameters_;
    }

    istream&
    open_upload (size_t index)
    {
      // Shift the index if the function name parameter was removed.
      //
      return request_.open_upload (index + (removed_ ? 1 : 0));
    }

    istream&
    open_upload (const string& name)
    {
      // We don't expect the function name here as a parameter name.
      //
      return request_.open_upload (name);
    }

    virtual const name_values&
    headers () {return request_.headers ();}

    virtual const name_values&
    cookies () {return request_.cookies ();}

    virtual istream&
    content (size_t limit, size_t buffer)
    {
      return request_.content (limit, buffer);
    }

  private:
    request& request_;
    optional<name_values> parameters_;
    bool url_only_parameters_; // Meaningless if parameters_ is not present.
    bool removed_ = false;     // If the function name parameter was removed.
  };

  // repository_root
  //
  repository_root::
  repository_root ()
      : packages_ (make_shared<packages> ()),
        package_details_ (make_shared<package_details> ()),
        package_version_details_ (make_shared<package_version_details> ()),
        repository_details_ (make_shared<repository_details> ()),
        build_task_ (make_shared<build_task> ()),
        build_result_ (make_shared<build_result> ()),
        build_force_ (make_shared<build_force> ()),
        build_log_ (make_shared<build_log> ()),
        builds_ (make_shared<builds> ()),
        build_configs_ (make_shared<build_configs> ()),
        submit_ (make_shared<submit> ()),
        ci_ (make_shared<ci> ())
  {
  }

  repository_root::
  repository_root (const repository_root& r)
      : handler (r),
        //
        // Deep/shallow-copy sub-handlers depending on whether this is an
        // exemplar/handler.
        //
        packages_ (
          r.initialized_
          ? r.packages_
          : make_shared<packages> (*r.packages_)),
        package_details_ (
          r.initialized_
          ? r.package_details_
          : make_shared<package_details> (*r.package_details_)),
        package_version_details_ (
          r.initialized_
          ? r.package_version_details_
          : make_shared<package_version_details> (
              *r.package_version_details_)),
        repository_details_ (
          r.initialized_
          ? r.repository_details_
          : make_shared<repository_details> (*r.repository_details_)),
        build_task_ (
          r.initialized_
          ? r.build_task_
          : make_shared<build_task> (*r.build_task_)),
        build_result_ (
          r.initialized_
          ? r.build_result_
          : make_shared<build_result> (*r.build_result_)),
        build_force_ (
          r.initialized_
          ? r.build_force_
          : make_shared<build_force> (*r.build_force_)),
        build_log_ (
          r.initialized_
          ? r.build_log_
          : make_shared<build_log> (*r.build_log_)),
        builds_ (
          r.initialized_
          ? r.builds_
          : make_shared<builds> (*r.builds_)),
        build_configs_ (
          r.initialized_
          ? r.build_configs_
          : make_shared<build_configs> (*r.build_configs_)),
        submit_ (
          r.initialized_
          ? r.submit_
          : make_shared<submit> (*r.submit_)),
        ci_ (
          r.initialized_
          ? r.ci_
          : make_shared<ci> (*r.ci_)),
        options_ (
          r.initialized_
          ? r.options_
          : nullptr)
  {
  }

  // Return amalgamation of repository_root and all its sub-handlers option
  // descriptions.
  //
  option_descriptions repository_root::
  options ()
  {
    option_descriptions r (handler::options ());
    append (r, packages_->options ());
    append (r, package_details_->options ());
    append (r, package_version_details_->options ());
    append (r, repository_details_->options ());
    append (r, build_task_->options ());
    append (r, build_result_->options ());
    append (r, build_force_->options ());
    append (r, build_log_->options ());
    append (r, builds_->options ());
    append (r, build_configs_->options ());
    append (r, submit_->options ());
    append (r, ci_->options ());
    return r;
  }

  // Initialize sub-handlers and parse own configuration options.
  //
  void repository_root::
  init (const name_values& v)
  {
    auto sub_init = [this, &v] (handler& m, const char* name)
    {
      // Initialize sub-handler. Intercept exception handling to add
      // sub-handler attribution.
      //
      try
      {
        m.init (filter (v, m.options ()), *log_);
      }
      catch (const std::exception& e)
      {
        // Any exception thrown by this function terminates the web server. All
        // exception types inherited from std::exception are handled by the web
        // server as std::exception. The only sensible way to handle them is to
        // log the error prior terminating. By that reason it is valid to
        // reduce all these types to a single one.
        //
        ostringstream os;
        os << name << ": " << e;
        throw runtime_error (os.str ());
      }
    };

    // Initialize sub-handlers.
    //
    sub_init (*packages_, "packages");
    sub_init (*package_details_, "package_details");
    sub_init (*package_version_details_, "package_version_details");
    sub_init (*repository_details_, "repository_details");
    sub_init (*build_task_, "build_task");
    sub_init (*build_result_, "build_result");
    sub_init (*build_force_, "build_force");
    sub_init (*build_log_, "build_log");
    sub_init (*builds_, "builds");
    sub_init (*build_configs_, "build_configs");
    sub_init (*submit_, "submit");
    sub_init (*ci_, "ci");

    // Parse own configuration options.
    //
    handler::init (
      filter (v, convert (options::repository_root::description ())));
  }

  void repository_root::
  init (scanner& s)
  {
    HANDLER_DIAG;

    options_ = make_shared<options::repository_root> (
      s, unknown_mode::fail, unknown_mode::fail);

    // Verify that the root default views are properly configured.
    //
    auto verify = [&fail] (const string& v, const char* what)
    {
      cstrings vs ({
          "packages", "builds", "build-configs", "about", "submit", "ci"});

      if (find (vs.begin (), vs.end (), v) == vs.end ())
        fail << what << " value '" << v << "' is invalid";
    };

    verify (options_->root_global_view (), "root-global-view");
    verify (options_->root_tenant_view (), "root-tenant-view");

    if (options_->root ().empty ())
      options_->root (dir_path ("/"));

    // To use libbutl timestamp printing functions later on (specifically in
    // sub-handlers, while handling requests).
    //
    tzset ();

    // To recognize cmark-gfm extensions while parsing Markdown later on.
    //
    cmark_gfm_core_extensions_ensure_registered ();
  }

  bool repository_root::
  handle (request& rq, response& rs)
  {
    HANDLER_DIAG;

    const dir_path& root (options_->root ());

    const path& rpath (rq.path ());
    if (!rpath.sub (root))
      return false;

    path lpath (rpath.leaf (root));

    if (!lpath.empty ())
    {
      path::iterator i (lpath.begin ());
      const string& s (*i);

      if (s[0] == '@' && s.size () > 1)
      {
        tenant = string (s, 1);
        lpath = path (++i, lpath.end ());
      }
    }

    // Delegate the request handling to the selected sub-handler. Intercept
    // exception handling to add sub-handler attribution.
    //
    auto handle = [&rq, &rs, this] (const char* nm, bool fn = false)
    {
      handler_->tenant = move (tenant);

      try
      {
        // Delegate the handling straight away if the sub-handler is not a
        // function. Otherwise, cleanup the request not to confuse the
        // sub-handler with the unknown parameter.
        //
        if (!fn)
          return handler_->handle (rq, rs, *log_);

        request_proxy rp (rq);
        return handler_->handle (rp, rs, *log_);
      }
      catch (const invalid_request&)
      {
        // Preserve invalid_request exception type, so the web server can
        // properly respond to the client with a 4XX error code.
        //
        throw;
      }
      catch (const std::exception& e)
      {
        // All exception types inherited from std::exception (and different
        // from invalid_request) are handled by the web server as
        // std::exception. The only sensible way to handle them is to respond
        // to the client with the internal server error (500) code. By that
        // reason it is valid to reduce all these types to a single one. Note
        // that the server_error exception is handled internally by the
        // handler::handle() function call.
        //
        ostringstream os;
        os << nm << ": " << e;
        throw runtime_error (os.str ());
      }
    };

    // Note that while selecting the sub-handler type for handling the request,
    // we rely on the fact that the initial and all the subsequent function
    // calls (that may take place after the retry exception is thrown) will
    // end-up with the same type, and so using the single handler instance for
    // all of these calls is safe. Note that the selection also sets up the
    // handling context (sub-handler name and optionally the request proxy).
    //
    if (lpath.empty ())
    {
      // Dispatch request handling to one of the sub-handlers depending on the
      // function name passed as a first HTTP request parameter (example:
      // cppget.org/?about). If it doesn't denote a handler or there are no
      // parameters, then dispatch to the default handler.
      //
      const name_values& params (rq.parameters (0 /* limit */,
                                                true /* url_only */));

      auto dispatch = [&handle, this] (const string& func,
                                       bool param) -> optional<bool>
      {
        // When adding a new handler don't forget to check if need to add it
        // to the default view list in the init() function.
        //
        if (func == "build-task")
        {
          if (handler_ == nullptr)
            handler_.reset (new build_task (*build_task_));

          return handle ("build_task", param);
        }
        else if (func == "build-result")
        {
          if (handler_ == nullptr)
            handler_.reset (new build_result (*build_result_));

          return handle ("build_result", param);
        }
        else if (func == "build-force")
        {
          if (handler_ == nullptr)
            handler_.reset (new build_force (*build_force_));

          return handle ("build_force", param);
        }
        else if (func == "builds")
        {
          if (handler_ == nullptr)
            handler_.reset (new builds (*builds_));

          return handle ("builds", param);
        }
        else if (func == "build-configs")
        {
          if (handler_ == nullptr)
            handler_.reset (new build_configs (*build_configs_));

          return handle ("build_configs", param);
        }
        else if (func == "packages")
        {
          if (handler_ == nullptr)
            handler_.reset (new packages (*packages_));

          return handle ("packages", param);
        }
        else if (func == "about")
        {
          if (handler_ == nullptr)
            handler_.reset (new repository_details (*repository_details_));

          return handle ("repository_details", param);
        }
        else if (func == "submit")
        {
          if (handler_ == nullptr)
            handler_.reset (new submit (*submit_));

          return handle ("submit", param);
        }
        else if (func == "ci")
        {
          if (handler_ == nullptr)
            handler_.reset (new ci (*ci_));

          return handle ("ci", param);
        }
        else
          return nullopt;
      };

      optional<bool> r;

      if (!params.empty () &&
          (r = dispatch (params.front ().name, true /* param */)))
        return *r;

      const string& view (!tenant.empty ()
                          ? options_->root_tenant_view ()
                          : options_->root_global_view ());

      r = dispatch (view, false /* param */);

      // The default views are verified in the init() function.
      //
      assert (r);

      return *r;
    }
    else
    {
      // Dispatch request handling to the package_details, the
      // package_version_details or the build_log handler depending on the HTTP
      // request URL path.
      //
      auto i (lpath.begin ());
      assert (i != lpath.end ());

      const string& n (*i++); // Package name.

      // Check if this is a package name and not a brep static content files
      // (CSS) directory name, a repository directory name, or a special file
      // name (the one starting with '.'). Note that HTTP request URL path
      // (without the root directory) must also have one of the following
      // layouts:
      //
      // <package-name>
      // <package-name>/<package-version>
      // <package-name>/<package-version>/log[/...]
      //
      // If any of the checks fails, then the handling is declined.
      //
      if (n != "@" && n.find_first_not_of ("0123456789") != string::npos &&
          n[0] != '.')
      {
        if (i == lpath.end ())
        {
          if (handler_ == nullptr)
            handler_.reset (new package_details (*package_details_));

          return handle ("package_details");
        }
        else if (++i == lpath.end ())
        {
          if (handler_ == nullptr)
            handler_.reset (
              new package_version_details (*package_version_details_));

          return handle ("package_version_details");
        }
        else if (*i == "log")
        {
          if (handler_ == nullptr)
            handler_.reset (new build_log (*build_log_));

          return handle ("build_log");
        }
      }
    }

    // We shouldn't be selecting a handler if decline to handle the request.
    //
    assert (handler_ == nullptr);
    return false;
  }

  void repository_root::
  version ()
  {
    HANDLER_DIAG;

    info << "module " << BREP_VERSION_ID
         << ", libbrep " << LIBBREP_VERSION_ID
         << ", libbbot " << LIBBBOT_VERSION_ID
         << ", libbpkg " << LIBBPKG_VERSION_ID
         << ", libbutl " << LIBBUTL_VERSION_ID;
  }
}