#pragma once

#include "cookies.hpp"
#include "forward_unauthorized.hpp"
#include "http_request.hpp"
#include "http_response.hpp"
#include "http_utility.hpp"
#include "pam_authenticate.hpp"
#include "webroutes.hpp"

#include <boost/container/flat_set.hpp>

#include <random>
#include <utility>

namespace crow
{

namespace authentication
{

inline std::shared_ptr<persistent_data::UserSession> performBasicAuth(
    const boost::asio::ip::address& clientIp, std::string_view authHeader)
{
    BMCWEB_LOG_DEBUG("[AuthMiddleware] Basic authentication");

    if (!authHeader.starts_with("Basic "))
    {
        return nullptr;
    }

    std::string_view param = authHeader.substr(strlen("Basic "));
    std::string authData;

    if (!crow::utility::base64Decode(param, authData))
    {
        return nullptr;
    }
    std::size_t separator = authData.find(':');
    if (separator == std::string::npos)
    {
        return nullptr;
    }

    std::string user = authData.substr(0, separator);
    separator += 1;
    if (separator > authData.size())
    {
        return nullptr;
    }
    std::string pass = authData.substr(separator);

    BMCWEB_LOG_DEBUG("[AuthMiddleware] Authenticating user: {}", user);
    BMCWEB_LOG_DEBUG("[AuthMiddleware] User IPAddress: {}",
                     clientIp.to_string());

    int pamrc = pamAuthenticateUser(user, pass, std::nullopt);
    bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD;
    if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly)
    {
        return nullptr;
    }

    // Attempt to locate an existing Basic Auth session from the same ip address
    // and user
    for (auto& session :
         persistent_data::SessionStore::getInstance().getSessions())
    {
        if (session->sessionType != persistent_data::SessionType::Basic)
        {
            continue;
        }
        if (session->clientIp != redfish::ip_util::toString(clientIp))
        {
            continue;
        }
        if (session->username != user)
        {
            continue;
        }
        return session;
    }

    return persistent_data::SessionStore::getInstance().generateUserSession(
        user, clientIp, std::nullopt, persistent_data::SessionType::Basic,
        isConfigureSelfOnly);
}

inline std::shared_ptr<persistent_data::UserSession>
    performTokenAuth(std::string_view authHeader)
{
    BMCWEB_LOG_DEBUG("[AuthMiddleware] Token authentication");
    if (!authHeader.starts_with("Token "))
    {
        return nullptr;
    }
    std::string_view token = authHeader.substr(strlen("Token "));
    auto sessionOut =
        persistent_data::SessionStore::getInstance().loginSessionByToken(token);
    return sessionOut;
}

inline std::shared_ptr<persistent_data::UserSession>
    performXtokenAuth(const boost::beast::http::header<true>& reqHeader)
{
    BMCWEB_LOG_DEBUG("[AuthMiddleware] X-Auth-Token authentication");

    std::string_view token = reqHeader["X-Auth-Token"];
    if (token.empty())
    {
        return nullptr;
    }
    auto sessionOut =
        persistent_data::SessionStore::getInstance().loginSessionByToken(token);
    return sessionOut;
}

inline std::shared_ptr<persistent_data::UserSession>
    performCookieAuth(boost::beast::http::verb method [[maybe_unused]],
                      const boost::beast::http::header<true>& reqHeader)
{
    using headers = boost::beast::http::header<true>;
    std::pair<headers::const_iterator, headers::const_iterator> cookies =
        reqHeader.equal_range(boost::beast::http::field::cookie);

    for (auto it = cookies.first; it != cookies.second; it++)
    {
        std::string_view cookieValue = it->value();
        BMCWEB_LOG_DEBUG("Checking cookie {}", cookieValue);
        auto startIndex = cookieValue.find("SESSION=");
        if (startIndex == std::string::npos)
        {
            BMCWEB_LOG_DEBUG(
                "Cookie was present, but didn't look like a session {}",
                cookieValue);
            continue;
        }
        startIndex += sizeof("SESSION=") - 1;
        auto endIndex = cookieValue.find(';', startIndex);
        if (endIndex == std::string::npos)
        {
            endIndex = cookieValue.size();
        }
        std::string_view authKey =
            cookieValue.substr(startIndex, endIndex - startIndex);

        std::shared_ptr<persistent_data::UserSession> sessionOut =
            persistent_data::SessionStore::getInstance().loginSessionByToken(
                authKey);
        if (sessionOut == nullptr)
        {
            return nullptr;
        }
        sessionOut->cookieAuth = true;

        if constexpr (!BMCWEB_INSECURE_DISABLE_CSRF)
        {
            // RFC7231 defines methods that need csrf protection
            if (method != boost::beast::http::verb::get)
            {
                std::string_view csrf = reqHeader["X-XSRF-TOKEN"];
                // Make sure both tokens are filled
                if (csrf.empty() || sessionOut->csrfToken.empty())
                {
                    return nullptr;
                }

                if (csrf.size() != persistent_data::sessionTokenSize)
                {
                    return nullptr;
                }
                // Reject if csrf token not available
                if (!bmcweb::constantTimeStringCompare(csrf,
                                                       sessionOut->csrfToken))
                {
                    return nullptr;
                }
            }
        }
        return sessionOut;
    }
    return nullptr;
}

inline std::shared_ptr<persistent_data::UserSession> performTLSAuth(
    Response& res, const std::shared_ptr<persistent_data::UserSession>& session)
{
    if (session != nullptr)
    {
        res.addHeader(boost::beast::http::field::set_cookie,
                      "IsAuthenticated=true; Secure");
        BMCWEB_LOG_DEBUG(
            " TLS session: {} with cookie will be used for this request.",
            session->uniqueId);
    }

    return session;
}

// checks if request can be forwarded without authentication
inline bool isOnAllowlist(std::string_view url, boost::beast::http::verb method)
{
    // Handle the case where the router registers routes as both ending with /
    // and not.
    if (url.ends_with('/') && url != "/")
    {
        url.remove_suffix(1);
    }
    if (boost::beast::http::verb::get == method)
    {
        if ((url == "/redfish") ||          //
            (url == "/redfish/v1") ||       //
            (url == "/redfish/v1/odata") || //
            (url == "/redfish/v1/$metadata"))
        {
            return true;
        }
        if (crow::webroutes::routes.find(std::string(url)) !=
            crow::webroutes::routes.end())
        {
            return true;
        }
    }

    // it's allowed to POST on session collection & login without
    // authentication
    if (boost::beast::http::verb::post == method)
    {
        if ((url == "/redfish/v1/SessionService/Sessions") ||
            (url == "/redfish/v1/SessionService/Sessions/Members") ||
            (url == "/login"))
        {
            return true;
        }
    }

    return false;
}

inline std::shared_ptr<persistent_data::UserSession> authenticate(
    const boost::asio::ip::address& ipAddress [[maybe_unused]],
    Response& res [[maybe_unused]],
    boost::beast::http::verb method [[maybe_unused]],
    const boost::beast::http::header<true>& reqHeader,
    [[maybe_unused]] const std::shared_ptr<persistent_data::UserSession>&
        session)
{
    const persistent_data::AuthConfigMethods& authMethodsConfig =
        persistent_data::SessionStore::getInstance().getAuthMethodsConfig();

    std::shared_ptr<persistent_data::UserSession> sessionOut = nullptr;
    if constexpr (BMCWEB_MUTUAL_TLS_AUTH)
    {
        if (authMethodsConfig.tls)
        {
            sessionOut = performTLSAuth(res, session);
        }
    }
    if constexpr (BMCWEB_XTOKEN_AUTH)
    {
        if (sessionOut == nullptr && authMethodsConfig.xtoken)
        {
            sessionOut = performXtokenAuth(reqHeader);
        }
    }
    if constexpr (BMCWEB_COOKIE_AUTH)
    {
        if (sessionOut == nullptr && authMethodsConfig.cookie)
        {
            sessionOut = performCookieAuth(method, reqHeader);
        }
    }
    std::string_view authHeader = reqHeader["Authorization"];
    BMCWEB_LOG_DEBUG("authHeader={}", authHeader);
    if constexpr (BMCWEB_SESSION_AUTH)
    {
        if (sessionOut == nullptr && authMethodsConfig.sessionToken)
        {
            sessionOut = performTokenAuth(authHeader);
        }
    }
    if constexpr (BMCWEB_BASIC_AUTH)
    {
        if (sessionOut == nullptr && authMethodsConfig.basic)
        {
            sessionOut = performBasicAuth(ipAddress, authHeader);
        }
    }
    if (sessionOut != nullptr)
    {
        return sessionOut;
    }

    return nullptr;
}

} // namespace authentication
} // namespace crow