#include <maps/libs/cmdline/include/cmdline.h>

#include <maps/wikimap/mapspro/services/tasks-ng/lib/config.h>
#include <maps/wikimap/mapspro/services/tasks-ng/lib/db_pools.h>
#include <maps/wikimap/mapspro/services/tasks-ng/lib/exception.h>
#include <maps/wikimap/mapspro/services/tasks-ng/lib/magic_strings.h>
#include <maps/wikimap/mapspro/services/tasks-ng/lib/task_module.h>
#include <maps/wikimap/mapspro/services/tasks-ng/lib/task_module_registry.h>
#include <maps/wikimap/mapspro/services/tasks-ng/lib/task_type.h>
#include <maps/wikimap/mapspro/services/tasks-ng/lib/tasks.h>
#include <maps/wikimap/mapspro/services/tasks-ng/lib/xml_response_wrapper.h>

#include <yandex/maps/wiki/common/pg_utils.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/xml/include/xmlexception.h>
#include <maps/infra/yacare/include/yacare.h>

namespace srv = maps::wiki::tasks_ng;

using namespace maps;
using namespace srv;

namespace maps {
namespace wiki {
namespace tasks_ng {

namespace {

template <typename T>
std::optional<T>
getOptionalParam(const yacare::Request& r, const std::string& name)
{
    auto strParam = r.input()[name];
    if (strParam.empty()) {
        return std::nullopt;
    }
    try {
        return boost::lexical_cast<T>(strParam);
    } catch (const boost::bad_lexical_cast&) {
        THROW_TASKS_ERROR(
            ERR_BAD_REQUEST,
            "Invalid parameter: " << name << " : " << strParam);
    }
}

template <typename T>
T getParam(const yacare::Request& r, const std::string& name, const T& defaultValue)
{
    auto value = getOptionalParam<T>(r, name);
    return value ? *value : defaultValue;
}

template <typename T>
T getParam(const yacare::Request& r, const std::string& name)
{
    auto value = getOptionalParam<T>(r, name);
    if (!value) {
        THROW_TASKS_ERROR(
            ERR_BAD_REQUEST, "Parameter: " << name << " not exists");
    }
    return *value;
}

void errorReporter(const yacare::Request&, yacare::Response& response)
{
    auto makeError = [&] (const auto& status, const auto& error) {
        XmlResponseWrapper wrapper(response);
        std::ostringstream os;
        os << error;
        makeResponseError(wrapper, status, os.str());
    };

    try {
        throw;
    } catch (const maps::xml3::NodeNotFound& ex) {
        makeError(ERR_BAD_REQUEST, ex);
    } catch (const maps::xml3::AttributeNotFound& ex) {
        makeError(ERR_BAD_REQUEST, ex);
    } catch (const maps::xml3::NodeIsBlank& ex) {
        makeError(ERR_BAD_REQUEST, ex);
    } catch (const yacare::ClientError& ex) {
        makeError(ERR_BAD_REQUEST, ex);
    } catch (const Error& ex) {
        makeError(ex.status(), ex);
    } catch (const maps::Exception& ex) {
        makeError(INTERNAL_ERROR, ex);
    } catch (const std::exception& ex) {
        makeError(INTERNAL_ERROR, ex.what());
    }
}

FilesInRequest parseFilesFromRequest(const yacare::Request& request)
{
    FilesInRequest filesInRequest;

    for (const auto& requestPart : request.multipart()) {
        const auto& disposition = requestPart.env("HTTP_CONTENT_DISPOSITION");
        if (disposition.find("filename") == std::string::npos) {
            continue;
        }

        //Example: `Content-Disposition: form-data; name="data"; filename="badgename.add.txt"`
        auto dispositionAttrs = common::split(disposition, "; ");
        TASKS_REQUIRE(dispositionAttrs.size() == 3,
            ERR_BAD_REQUEST,
            "Content-Disposition should contain 3 attributes");

        auto [nameKey, paramName] = common::splitKeyValue(dispositionAttrs[1]);
        auto [fileNameKey, fileName] = common::splitKeyValue(dispositionAttrs[2]);

        auto strippedParamName = common::stripQuotas(paramName);

        FileInRequest fileInRequest;
        fileInRequest.fileName = common::stripQuotas(fileName);
        fileInRequest.fileBody = requestPart.body();

        INFO() << "File in request:"
            << " paramName=" << strippedParamName
            << " fileName=" << fileInRequest.fileName;

        filesInRequest.emplace(strippedParamName, std::move(fileInRequest));
    }

    return filesInRequest;
}

} // namespace

} // namespace tasks_ng
} // namespace wiki
} // namespace maps


// Common params
YCR_QUERY_CUSTOM_PARAM(("uid"), uid, srv::Uid)
{
    auto uid = srv::getParam<srv::Uid>(request, "uid");
    TASKS_REQUIRE(uid, srv::ERR_FORBIDDEN, "Unknown user");
    dest = uid;
    return true;
}

YCR_QUERY_PARAM(token, std::string, YCR_DEFAULT({}));

YCR_QUERY_CUSTOM_PARAM((), parentId, srv::Id, YCR_DEFAULT(0))
{ return yacare::impl::parseArg(dest, request, "parent"); }

YCR_QUERY_CUSTOM_PARAM((), createdBy, srv::Uid, YCR_DEFAULT(0))
{ return yacare::impl::parseArg(dest, request, "created-by"); }


// Paging params
YCR_QUERY_PARAM(page, size_t, YCR_DEFAULT(0));

YCR_QUERY_CUSTOM_PARAM((), perPage, size_t, YCR_DEFAULT(0))
{ return yacare::impl::parseArg(dest, request, "per-page"); }

YCR_QUERY_CUSTOM_PARAM((), taskType, std::string, YCR_DEFAULT({}))
{ return yacare::impl::parseArg(dest, request, "type"); }


yacare::VirtualHost vhost {
    yacare::VirtualHost::SLB { "tasks-ng.um" },
};

YCR_SET_DEFAULT(vhost)

YCR_RESPOND_TO("GET /capabilities", taskType)
{
    XmlResponseWrapper wrapper(response);
    makeResponseCapabilities(wrapper, taskType);
}

YCR_RESPOND_TO("GET /tasks/$", token)
{
    auto taskId = pathnameParam<Id>(0);

    auto ctx = DbContext::toRead(token);

    XmlResponseWrapper wrapper(response);
    makeResponseTask(ctx, wrapper, taskId);
}

YCR_RESPOND_TO("GET /tasks/$/log", token)
{
    auto taskId = pathnameParam<Id>(0);

    try {
        auto ctx = DbContext::toRead(token);

        response["Content-Type"] = "text/plain";
        response << getTaskLog(ctx, taskId);
    } catch (const std::exception& e) {
        ERROR() << e.what();
    }
}

YCR_RESPOND_TO("GET /tasks", token, page, perPage, taskType, createdBy, parentId)
{
    auto ctx = DbContext::toRead(token);

    XmlResponseWrapper wrapper(response);
    makeResponseTasks(ctx, wrapper, taskType, createdBy, parentId, page, perPage);
}

YCR_RESPOND_TO("POST /tasks", uid, taskType, parentId)
{
    RequestParameters params(request.input(), parseFilesFromRequest(request));

    auto ctx = DbContext::toWrite();

    XmlResponseWrapper wrapper(response);
    makeResponseCreateTask(ctx, wrapper, taskType, uid, parentId, params);
}

YCR_RESPOND_TO("PUT /tasks/$", uid)
{
    auto taskId = pathnameParam<Id>(0);
    auto status = getParam<ServiceTaskStatus>(request, "status");

    auto ctx = DbContext::toWrite();

    XmlResponseWrapper wrapper(response);
    makeResponseSetTaskStatus(ctx, wrapper, taskId, uid, status);
}

YCR_RESPOND_TO("GET /types/$/parameters", token)
{
    auto taskType = pathnameParam<std::string>(0);
    TaskModuleRegistry::get().module(taskType);

    auto ctx = DbContext::toRead(token);

    XmlResponseWrapper wrapper(response);
    makeResponseTaskTypeParameters(wrapper, getTaskTypeParameters(ctx, taskType));
}

YCR_RESPOND_TO("PUT /types/$/parameters", uid)
{
    auto taskType = pathnameParam<std::string>(0);
    TaskModuleRegistry::get().module(taskType);
    INFO() << "Change " << taskType << " parameters, uid: " << uid;

    auto attrs = parseTaskTypeParameters(request.body());

    auto ctx = DbContext::toWrite();
    setTaskTypeParameters(ctx, taskType, attrs);

    XmlResponseWrapper wrapper(response);
    makeResponseTaskTypeParameters(wrapper, getTaskTypeParameters(ctx, taskType));
    ctx.commit();
}

YCR_MAIN(argc, argv)
{
    try {
        maps::cmdline::Parser parser;
        auto config = parser.string("config")
            .help("path to configuration");
        parser.parse(argc, argv);

        srv::ConfigScope cfgScope(config.defined() ? config : std::string());
        DbPools::init(srv::cfg());
        yacare::setErrorReporter(errorReporter);
        yacare::run(yacare::RunSettings{.useSystemDefaultLocale = true});
        DbPools::destroy();
        return 0;
    } catch (maps::Exception& e) {
        std::cerr << e << std::endl;
        return 1;
    } catch (std::exception& e) {
        std::cerr << e.what() << std::endl;
        return 1;
    }
}
