// Copyright (C) 2021-2022 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 <config.h>

#include <config/cmd_response_creator.h>
#include <config/command_mgr.h>
#include <config/config_log.h>
#include <cc/command_interpreter.h>
#include <http/post_request_json.h>
#include <http/response_json.h>
#include <boost/pointer_cast.hpp>
#include <iostream>

using namespace isc::config;
using namespace isc::data;
using namespace isc::http;
using namespace std;

namespace isc {
namespace config {

HttpAuthConfigPtr CmdResponseCreator::http_auth_config_;

unordered_set<string> CmdResponseCreator::command_accept_list_;

HttpRequestPtr
CmdResponseCreator::createNewHttpRequest() const {
    return (HttpRequestPtr(new PostHttpRequestJson()));
}

HttpResponsePtr
CmdResponseCreator::
createStockHttpResponse(const HttpRequestPtr& request,
                        const HttpStatusCode& status_code) const {
    HttpResponsePtr response = createStockHttpResponseInternal(request, status_code);
    response->finalize();
    return (response);
}

HttpResponsePtr
CmdResponseCreator::
createStockHttpResponseInternal(const HttpRequestPtr& request,
                                const HttpStatusCode& status_code) const {
    // The request hasn't been finalized so the request object
    // doesn't contain any information about the HTTP version number
    // used. But, the context should have this data (assuming the
    // HTTP version is parsed OK).
    HttpVersion http_version(request->context()->http_version_major_,
                             request->context()->http_version_minor_);
    // We only accept HTTP version 1.0 or 1.1. If other version number is found
    // we fall back to HTTP/1.0.
    if ((http_version < HttpVersion(1, 0)) || (HttpVersion(1, 1) < http_version)) {
        http_version.major_ = 1;
        http_version.minor_ = 0;
    }
    // This will generate the response holding JSON content.
    HttpResponsePtr response(new HttpResponseJson(http_version, status_code));
    return (response);
}

HttpResponsePtr
CmdResponseCreator::createDynamicHttpResponse(HttpRequestPtr request) {
    HttpResponseJsonPtr http_response;

    // Check the basic HTTP authentication.
    if (http_auth_config_) {
        http_response = http_auth_config_->checkAuth(*this, request);
        if (http_response) {
            return (http_response);
        }
    }

    // The request is always non-null, because this is verified by the
    // createHttpResponse method. Let's try to convert it to the
    // PostHttpRequestJson type as this is the type generated by the
    // createNewHttpRequest. If the conversion result is null it means that
    // the caller did not use createNewHttpRequest method to create this
    // instance. This is considered an error in the server logic.
    PostHttpRequestJsonPtr request_json = boost::dynamic_pointer_cast<
                                          PostHttpRequestJson>(request);
    if (!request_json) {
        // Notify the client that we have a problem with our server.
        return (createStockHttpResponse(request, HttpStatusCode::INTERNAL_SERVER_ERROR));
    }

    // We have already checked that the request is finalized so the call
    // to getBodyAsJson must not trigger an exception.
    ConstElementPtr command = request_json->getBodyAsJson();

    // Filter the command.
    http_response = filterCommand(request, command, command_accept_list_);
    if (http_response) {
        return (http_response);
    }

    // Process command doesn't generate exceptions but can possibly return
    // null response, if the handler is not implemented properly. This is
    // again an internal server issue.
    ConstElementPtr response = config::CommandMgr::instance().processCommand(command);

    if (!response) {
        // Notify the client that we have a problem with our server.
        return (createStockHttpResponse(request, HttpStatusCode::INTERNAL_SERVER_ERROR));
    }

    // Normal Responses coming from the Kea Control Agent must always be wrapped in
    // a list as they may contain responses from multiple daemons.
    // If we're emulating that for backward compatibility, then we need to wrap
    // the answer in a list if it isn't in one already.
    if (emulateAgentResponse() && (response->getType() != Element::list)) {
        ElementPtr response_list = Element::createList();
        response_list->add(boost::const_pointer_cast<Element>(response));
        response = response_list;
    }

    // The response is OK, so let's create new HTTP response with the status OK.
    http_response = boost::dynamic_pointer_cast<
        HttpResponseJson>(createStockHttpResponseInternal(request, HttpStatusCode::OK));
    http_response->setBodyAsJson(response);
    http_response->finalize();

    return (http_response);
}

HttpResponseJsonPtr
CmdResponseCreator::filterCommand(const HttpRequestPtr& request,
                                  const ConstElementPtr& body,
                                  const unordered_set<string>& accept) {
    HttpResponseJsonPtr response;
    if (!body || accept.empty()) {
        return (response);
    }
    if (body->getType() != Element::map) {
        return (response);
    }
    ConstElementPtr elem = body->get(CONTROL_COMMAND);
    if (!elem || (elem->getType() != Element::string)) {
        return (response);
    }
    string command = elem->stringValue();
    if (command.empty() || accept.count(command)) {
        return (response);
    }

    // Reject the command.
    LOG_DEBUG(command_logger, DBG_COMMAND,
              COMMAND_HTTP_LISTENER_COMMAND_REJECTED)
        .arg(command)
        .arg(request->getRemote());
    // From CtrlAgentResponseCreator::createStockHttpResponseInternal.
    HttpVersion http_version(request->context()->http_version_major_,
                             request->context()->http_version_minor_);
    if ((http_version < HttpVersion(1, 0)) ||
        (HttpVersion(1, 1) < http_version)) {
        http_version.major_ = 1;
        http_version.minor_ = 0;
    }
    HttpStatusCode status_code = HttpStatusCode::FORBIDDEN;
    response.reset(new HttpResponseJson(http_version, status_code));
    ElementPtr response_body = Element::createMap();
    uint16_t result = HttpResponse::statusCodeToNumber(status_code);
    response_body->set(CONTROL_RESULT,
                       Element::create(static_cast<long long>(result)));
    const string& text = HttpResponse::statusCodeToString(status_code);
    response_body->set(CONTROL_TEXT, Element::create(text));
    response->setBodyAsJson(response_body);
    response->finalize();
    return (response);
}

} // end of namespace isc::config
} // end of namespace isc
