// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#include <http/request.h>
#include <boost/algorithm/string.hpp>
#include <boost/lexical_cast.hpp>

namespace isc {
namespace http {

HttpRequest::HttpRequest()
    : required_methods_(),required_versions_(), required_headers_(),
      created_(false), finalized_(false), method_(Method::HTTP_METHOD_UNKNOWN),
      headers_(), context_(new HttpRequestContext()) {
}

HttpRequest::~HttpRequest() {
}

void
HttpRequest::requireHttpMethod(const HttpRequest::Method& method) {
    required_methods_.insert(method);
}

void
HttpRequest::requireHttpVersion(const HttpVersion& version) {
    required_versions_.insert(version);
}

void
HttpRequest::requireHeader(const std::string& header_name) {
    // Empty value denotes that the header is required but no specific
    // value is expected.
    required_headers_[header_name] = "";
}

void
HttpRequest::requireHeaderValue(const std::string& header_name,
                                const std::string& header_value) {
    required_headers_[header_name] = header_value;
}

bool
HttpRequest::requiresBody() const {
    // If Content-Length is required the body must exist too. There may
    // be probably some cases when Content-Length is not provided but
    // the body is provided. But, probably not in our use cases.
    return (required_headers_.find("Content-Length") != required_headers_.end());
}

void
HttpRequest::create() {
    try {
        // The RequestParser doesn't validate the method name. Thus, this
        // may throw an exception. But, we're fine with lower case names,
        // e.g. get, post etc.
        method_ = methodFromString(context_->method_);

        // Check if the method is allowed for this request.
        if (!inRequiredSet(method_, required_methods_)) {
            isc_throw(BadValue, "use of HTTP " << methodToString(method_)
                      << " not allowed");
        }

        // Check if the HTTP version is allowed for this request.
        if (!inRequiredSet(HttpVersion(context_->http_version_major_,
                                       context_->http_version_minor_),
                           required_versions_)) {
            isc_throw(BadValue, "use of HTTP version "
                      << context_->http_version_major_ << "."
                      << context_->http_version_minor_
                      << " not allowed");
        }

        // Copy headers from the context.
        for (auto header = context_->headers_.begin();
             header != context_->headers_.end();
             ++header) {
            headers_[header->name_] = header->value_;
        }

        // Iterate over required headers and check that they exist
        // in the HTTP request.
        for (auto req_header = required_headers_.begin();
             req_header != required_headers_.end();
             ++req_header) {
            auto header = headers_.find(req_header->first);
            if (header == headers_.end()) {
                isc_throw(BadValue, "required header " << req_header->first
                          << " not found in the HTTP request");
            } else if (!req_header->second.empty() &&
                       header->second != req_header->second) {
                // If specific value is required for the header, check
                // that the value in the HTTP request matches it.
                isc_throw(BadValue, "required header's " << header->first
                          << " value is " << req_header->second
                          << ", but " << header->second << " was found");
            }
        }

    } catch (const std::exception& ex) {
        // Reset the state of the object if we failed at any point.
        reset();
        isc_throw(HttpRequestError, ex.what());
    }

    // All ok.
    created_ = true;
}

void
HttpRequest::finalize() {
    if (!created_) {
        create();
    }

    // In this specific case, we don't need to do anything because the
    // body is retrieved from the context object directly. We also don't
    // know what type of body we have received. Derived classes should
    // override this method and handle various types of bodies.
    finalized_ = true;
}

void
HttpRequest::reset() {
    created_ = false;
    finalized_ = false;
    method_ = HttpRequest::Method::HTTP_METHOD_UNKNOWN;
    headers_.clear();
}

HttpRequest::Method
HttpRequest::getMethod() const {
    checkCreated();
    return (method_);
}

std::string
HttpRequest::getUri() const {
    checkCreated();
    return (context_->uri_);
}

HttpVersion
HttpRequest::getHttpVersion() const {
    checkCreated();
    return (HttpVersion(context_->http_version_major_,
                        context_->http_version_minor_));
}

std::string
HttpRequest::getHeaderValue(const std::string& header) const {
    checkCreated();

    auto header_it = headers_.find(header);
    if (header_it != headers_.end()) {
        return (header_it->second);
    }
    // No such header.
    isc_throw(HttpRequestNonExistingHeader, header << " HTTP header"
              " not found in the request");
}

uint64_t
HttpRequest::getHeaderValueAsUint64(const std::string& header) const {
    // This will throw an exception if the header doesn't exist.
    std::string header_value = getHeaderValue(header);

    try {
        return (boost::lexical_cast<uint64_t>(header_value));

    } catch (const boost::bad_lexical_cast& ex) {
        // The specified header does exist, but the value is not a number.
        isc_throw(HttpRequestError, header << " HTTP header value "
                  << header_value << " is not a valid number");
    }
}

std::string
HttpRequest::getBody() const {
    checkFinalized();
    return (context_->body_);
}

void
HttpRequest::checkCreated() const {
    if (!created_) {
        isc_throw(HttpRequestError, "unable to retrieve values of HTTP"
                  " request because the HttpRequest::create() must be"
                  " called first. This is a programmatic error");
    }
}

void
HttpRequest::checkFinalized() const {
    if (!finalized_) {
        isc_throw(HttpRequestError, "unable to retrieve body of HTTP"
                  " request because the HttpRequest::finalize() must be"
                  " called first. This is a programmatic error");
    }
}

template<typename T>
bool
HttpRequest::inRequiredSet(const T& element,
                           const std::set<T>& element_set) const {
    return (element_set.empty() || element_set.count(element) > 0);
}


HttpRequest::Method
HttpRequest::methodFromString(std::string method) const {
    boost::to_upper(method);
    if (method == "GET") {
        return (Method::HTTP_GET);
    } else if (method == "POST") {
        return (Method::HTTP_POST);
    } else if (method == "HEAD") {
        return (Method::HTTP_HEAD);
    } else if (method == "PUT") {
        return (Method::HTTP_PUT);
    } else if (method == "DELETE") {
        return (Method::HTTP_DELETE);
    } else if (method == "OPTIONS") {
        return (Method::HTTP_OPTIONS);
    } else if (method == "CONNECT") {
        return (Method::HTTP_CONNECT);
    } else {
        isc_throw(HttpRequestError, "unknown HTTP method " << method);
    }
}

std::string
HttpRequest::methodToString(const HttpRequest::Method& method) const {
    switch (method) {
    case Method::HTTP_GET:
        return ("GET");
    case Method::HTTP_POST:
        return ("POST");
    case Method::HTTP_HEAD:
        return ("HEAD");
    case Method::HTTP_PUT:
        return ("PUT");
    case Method::HTTP_DELETE:
        return ("DELETE");
    case Method::HTTP_OPTIONS:
        return ("OPTIONS");
    case Method::HTTP_CONNECT:
        return ("CONNECT");
    default:
        return ("unknown HTTP method");
    }
}

}
}