// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest

#include <AuthHandler.hpp>

#include <everest/helpers/helpers.hpp>
#include <everest/logging.hpp>
#include <generated/interfaces/kvs/Interface.hpp>

using everest::helpers::is_equal_case_insensitive;

namespace module {

/// \brief helper method to intersect referenced_connectors (from ProvidedIdToken) with evses that are listed
/// within ValidationResult
std::vector<int> intersect(std::vector<int>& a, std::vector<int>& b) {
    std::vector<int> result;
    std::sort(a.begin(), a.end());
    std::sort(b.begin(), b.end());
    std::set_intersection(a.begin(), a.end(), b.begin(), b.end(), std::back_inserter(result));
    return result;
}

namespace conversions {
std::string token_handling_result_to_string(const TokenHandlingResult& result) {
    switch (result) {
    case TokenHandlingResult::ALREADY_IN_PROCESS:
        return "ALREADY_IN_PROCESS";
    case TokenHandlingResult::NO_CONNECTOR_AVAILABLE:
        return "NO_CONNECTOR_AVAILABLE";
    case TokenHandlingResult::REJECTED:
        return "REJECTED";
    case TokenHandlingResult::TIMEOUT:
        return "TIMEOUT";
    case TokenHandlingResult::USED_TO_START_TRANSACTION:
        return "USED_TO_START_TRANSACTION";
    case TokenHandlingResult::USED_TO_STOP_TRANSACTION:
        return "USED_TO_STOP_TRANSACTION";
    case TokenHandlingResult::WITHDRAWN:
        return "WITHDRAWN";
    default:
        throw std::runtime_error("No known conversion for the given token handling result");
    }
}
} // namespace conversions

AuthHandler::AuthHandler(const SelectionAlgorithm& selection_algorithm, const int connection_timeout,
                         bool plug_in_timeout_enabled, bool prioritize_authorization_over_stopping_transaction,
                         bool ignore_faults, const std::string& id, kvsIntf* store) :
    selection_algorithm(selection_algorithm),
    connection_timeout(connection_timeout),
    plug_in_timeout_enabled(plug_in_timeout_enabled),
    prioritize_authorization_over_stopping_transaction(prioritize_authorization_over_stopping_transaction),
    ignore_faults(ignore_faults),
    reservation_handler(evses, id, store) {
}

AuthHandler::~AuthHandler() {
}

void AuthHandler::init_evse(const int evse_id, const int evse_index, const std::vector<Connector>& connectors) {
    std::lock_guard<std::mutex> lock(this->event_mutex);
    EVLOG_debug << "Add evse with evse id " << evse_id;

    if (evse_id <= 0) {
        EVLOG_error << "Can not initialize EVSE: evse id is <= 0.";
        return;
    }

    this->evses[evse_id] = std::make_unique<EVSEContext>(evse_id, evse_index, connectors);
}

int32_t AuthHandler::get_evse_id_by_index(const int evse_index) {
    std::lock_guard<std::mutex> lock(this->event_mutex);
    for (const auto& [evse_id, evse_context] : this->evses) {
        if (evse_context->evse_index == evse_index) {
            return evse_id;
        }
    }
    throw std::out_of_range("No EVSE found for evse_index: " + std::to_string(evse_index));
}

void AuthHandler::initialize() {
    std::lock_guard<std::mutex> lock(this->event_mutex);
    this->reservation_handler.load_reservations();
    check_evse_reserved_and_send_updates();
}

TokenHandlingResult AuthHandler::on_token(const ProvidedIdToken& provided_token) {
    std::unique_lock<std::mutex> lk(this->event_mutex);
    if (!this->publish_token_validation_status_callback) {
        return TokenHandlingResult::REJECTED;
    }

    TokenHandlingResult result;
    ProvidedIdToken provided_token_copy = provided_token;

    // check if token is already currently processed
    EVLOG_info << "Received new token: " << everest::helpers::redact(provided_token);
    const auto referenced_evses = this->get_referenced_evses(provided_token);

    if (!this->is_token_already_in_process(provided_token, referenced_evses)) {
        // process token if not already in process
        this->tokens_in_process.insert(provided_token);
        this->publish_token_validation_status(provided_token, TokenValidationStatus::Processing);
        result = this->handle_token(provided_token_copy, lk);
    } else {
        // do nothing if token is currently processed
        EVLOG_info << "Received token " << everest::helpers::redact(provided_token.id_token.value)
                   << " repeatedly while still processing it";
        result = TokenHandlingResult::ALREADY_IN_PROCESS;
    }

    switch (result) {
    case TokenHandlingResult::ALREADY_IN_PROCESS:
        break;
    case TokenHandlingResult::TIMEOUT: // Timeout means accepted but failed to pick contactor
        this->publish_token_validation_status(provided_token_copy, TokenValidationStatus::TimedOut);
        break;
    case TokenHandlingResult::NO_CONNECTOR_AVAILABLE:
    case TokenHandlingResult::REJECTED:
        this->publish_token_validation_status(provided_token_copy, TokenValidationStatus::Rejected);
        break;
    case TokenHandlingResult::USED_TO_START_TRANSACTION:
        this->publish_token_validation_status(provided_token_copy, TokenValidationStatus::UsedToStart);
        break;
    case TokenHandlingResult::USED_TO_STOP_TRANSACTION:
        this->publish_token_validation_status(provided_token_copy, TokenValidationStatus::UsedToStop);
        break;
    case TokenHandlingResult::WITHDRAWN:
        this->publish_token_validation_status(provided_token_copy, TokenValidationStatus::Withdrawn);
        break;
    }

    if (result != TokenHandlingResult::ALREADY_IN_PROCESS) {
        this->tokens_in_process.erase(provided_token);
    }

    EVLOG_info << "Result for token: " << everest::helpers::redact(provided_token.id_token.value) << ": "
               << conversions::token_handling_result_to_string(result);
    this->processing_finished_cv.notify_all();
    return result;
}

void AuthHandler::handle_token_validation_result_update(const ValidationResultUpdate& validation_result_update) {
    std::unique_lock<std::mutex> lk(this->event_mutex);
    auto connector_id = validation_result_update.connector_id;
    if (this->evses.find(connector_id) != this->evses.end() and this->evses.at(connector_id)->identifier.has_value()) {
        // "connector" is old OCPP speak and corrsponds to "EVSE":
        EVLOG_info << "Updating validation result on evse#" << connector_id << ": "
                   << validation_result_update.validation_result.authorization_status;
        // Currently we only support updating the parent id token
        this->evses.at(connector_id)->identifier->authorization_status =
            validation_result_update.validation_result.authorization_status;
        this->evses.at(connector_id)->identifier->parent_id_token =
            validation_result_update.validation_result.parent_id_token;
        types::authorization::ProvidedIdToken provided_token;
        provided_token.id_token = this->evses.at(connector_id)->identifier->id_token;
        provided_token.authorization_type = this->evses.at(connector_id)->identifier->type;
        provided_token.parent_id_token = validation_result_update.validation_result.parent_id_token;
        std::vector<int32_t> connectors_allowed{connector_id};
        provided_token.connectors = connectors_allowed;
        this->publish_token_validation_status(provided_token, types::authorization::TokenValidationStatus::Accepted,
                                              validation_result_update.validation_result.tariff_messages);
    } else {
        EVLOG_error << "Unknown evse#" << connector_id
                    << " or unknown authorization identifier on the evse for validation result update.";
    }
}

TokenHandlingResult AuthHandler::handle_token(ProvidedIdToken& provided_token, std::unique_lock<std::mutex>& lk) {
    std::vector<int> referenced_evses = this->get_referenced_evses(provided_token);

    // Only provided token with type RFID can be used to stop a transaction
    if (provided_token.authorization_type == AuthorizationType::RFID) {
        // check if id_token is used for an active transaction
        const auto evse_used_for_transaction =
            this->used_for_transaction(referenced_evses, provided_token.id_token.value);
        if (evse_used_for_transaction != -1) {
            StopTransactionRequest req;
            req.reason = StopTransactionReason::Local;
            req.id_tag.emplace(provided_token);
            this->stop_transaction_callback(this->evses.at(evse_used_for_transaction)->evse_index, req);
            EVLOG_info << "Transaction was stopped because id_token was used for transaction for evse#"
                       << evse_used_for_transaction;
            if (this->evses.at(evse_used_for_transaction)->identifier->parent_id_token.has_value()) {
                provided_token.parent_id_token =
                    this->evses.at(evse_used_for_transaction)->identifier->parent_id_token.value();
            }
            provided_token.connectors = std::vector<int32_t>{evse_used_for_transaction};
            return TokenHandlingResult::USED_TO_STOP_TRANSACTION;
        }
    }

    /** Check if validation of token shall be requested. In some situations its not useful to validate
     * the token because either no evse is available anyways or the provided token does not match a present
     * reservation. Yet it has to be checked if the incoming token can be used to stop an active transaction or if the
     * parent id of the token (that is only known after validation) can be used to stop or start transactions
     */

    /* If no evse is available AND no parent_id is deposited at any evse and no master pass group id is
    configured, we can immediately respond with NO_CONNECTOR_AVAILABLE */
    if (!this->any_evse_available(referenced_evses) and !this->any_parent_id_present(referenced_evses) and
        !this->master_pass_group_id.has_value()) {
        return TokenHandlingResult::NO_CONNECTOR_AVAILABLE;
    }

    /* If all evses are reserved and the given identifier doesnt match any reserved identifier and no parent id is
     * deposited for a reservation, we can immediately respond with NO_CONNECTOR_AVAILABLE */
    bool all_evses_reserved_and_tag_does_not_match = true;
    for (const auto evse_id : referenced_evses) {
        if (evse_id < 0) {
            EVLOG_warning << "Handle token: Evse id is negative: that should not be possible.";
            continue;
        }

        const uint32_t evse_id_u = static_cast<uint32_t>(evse_id);

        if (!this->reservation_handler.is_evse_reserved(evse_id_u) &&
            this->reservation_handler.is_charging_possible(evse_id_u)) {
            all_evses_reserved_and_tag_does_not_match = false;
            break;
        }

        const std::optional<int32_t> reservation_id = this->reservation_handler.matches_reserved_identifier(
            provided_token.id_token.value, evse_id_u, std::nullopt);

        if (reservation_id.has_value()) {
            all_evses_reserved_and_tag_does_not_match = false;
            break;
        }
        if (this->reservation_handler.has_reservation_parent_id(evse_id_u)) {
            all_evses_reserved_and_tag_does_not_match = false;
            break;
        }
    }

    if (all_evses_reserved_and_tag_does_not_match) {
        return TokenHandlingResult::NO_CONNECTOR_AVAILABLE;
    }

    // Validate the provided token using the available validators
    std::vector<ValidationResult> validation_results;
    // only validate if token is not prevalidated
    if (provided_token.prevalidated && provided_token.prevalidated.value()) {
        ValidationResult validation_result;
        validation_result.authorization_status = AuthorizationStatus::Accepted;
        validation_result.parent_id_token = provided_token.parent_id_token;
        validation_results.push_back(validation_result);
    } else {
        // release event_mutex before potentially blocking callback(s) via MQTT
        // validate_token_callback does not touch any shared state
        lk.unlock();
        validation_results = this->validate_token_callback(provided_token);
        lk.lock();
    }

    bool attempt_stop_with_parent_id_token = false;
    if (this->prioritize_authorization_over_stopping_transaction) {
        // check if any evse is available
        if (!this->any_evse_available(referenced_evses)) {
            // check if parent_id_token can be used to finish transaction
            attempt_stop_with_parent_id_token = true;
        }
    } else {
        attempt_stop_with_parent_id_token = true;
    }

    if (attempt_stop_with_parent_id_token) {
        for (const auto& validation_result : validation_results) {
            if (validation_result.authorization_status == AuthorizationStatus::Accepted &&
                validation_result.parent_id_token.has_value()) {
                // check if parent_id_token is equal to master_pass_group_id
                if (this->equals_master_pass_group_id(validation_result.parent_id_token)) {
                    EVLOG_info << "Provided parent_id_token is equal to master_pass_group_id. Stopping all active "
                                  "transactions!";
                    std::vector<int32_t> connectors;
                    for (const auto evse_id : referenced_evses) {
                        if (this->evses[evse_id]->transaction_active) {
                            StopTransactionRequest req;
                            req.reason = StopTransactionReason::MasterPass;
                            req.id_tag.emplace(provided_token);
                            this->stop_transaction_callback(this->evses.at(evse_id)->evse_index, req);
                            connectors.emplace_back(evse_id);
                        }
                    }
                    // TOOD: Add handling in case there is a display which can be used which transaction should stop
                    // (see C16 of OCPP2.0.1 spec)
                    provided_token.parent_id_token = validation_result.parent_id_token.value();
                    provided_token.connectors = connectors;
                    return TokenHandlingResult::USED_TO_STOP_TRANSACTION;
                }

                const auto evse_used_for_transaction =
                    this->used_for_transaction(referenced_evses, validation_result.parent_id_token.value().value);
                if (evse_used_for_transaction != -1) {
                    provided_token.connectors = std::vector<int32_t>{evse_used_for_transaction};
                    if (!this->evses[evse_used_for_transaction]->transaction_active) {
                        return TokenHandlingResult::ALREADY_IN_PROCESS;
                    } else {
                        StopTransactionRequest req;
                        req.reason = StopTransactionReason::Local;
                        req.id_tag.emplace(provided_token);
                        this->stop_transaction_callback(this->evses.at(evse_used_for_transaction)->evse_index, req);
                        EVLOG_info << "Transaction was stopped because parent_id_token was used for transaction";
                        provided_token.parent_id_token = validation_result.parent_id_token.value();
                        return TokenHandlingResult::USED_TO_STOP_TRANSACTION;
                    }
                }
            }
        }
    }

    // check if any evse is available
    if (!this->any_evse_available(referenced_evses)) {
        return TokenHandlingResult::NO_CONNECTOR_AVAILABLE;
    }

    // We can remove evse_ids from referenced_evses that already have an identifier assigend, since we don't want to
    // consider those when selecting an evse
    referenced_evses.erase(
        std::remove_if(referenced_evses.begin(), referenced_evses.end(),
                       [this](int32_t evse_id) { return this->evses.at(evse_id)->identifier != std::nullopt; }),
        referenced_evses.end());

    types::authorization::ValidationResult validation_result = {types::authorization::AuthorizationStatus::Unknown};
    if (!validation_results.empty()) {
        bool authorized = false;
        std::vector<ValidationResult>::size_type i = 0;
        // iterate over validation results
        while (i < validation_results.size() && !authorized && !referenced_evses.empty()) {
            validation_result = validation_results.at(i);
            if (validation_result.authorization_status == AuthorizationStatus::Accepted) {

                if (this->equals_master_pass_group_id(validation_result.parent_id_token)) {
                    EVLOG_info << "parent_id_token of validation result is equal to master_pass_group_id. Not allowed "
                                  "to authorize this token for starting transactions!";
                    return TokenHandlingResult::REJECTED;
                }

                if (validation_result.parent_id_token.has_value()) {
                    provided_token.parent_id_token = validation_result.parent_id_token.value();
                }
                this->publish_token_validation_status(provided_token,
                                                      types::authorization::TokenValidationStatus::Accepted,
                                                      validation_result.tariff_messages);
                /* although validator accepts the authorization request, the Auth module still needs to
                    - select the evse for the authorization request
                    - process it against placed reservations
                    - compare referenced_evses against the evses listed in the validation_result
                    - check if request has been withdrawn while selecting an evse
                */
                const auto select_evse_result =
                    this->select_evse(referenced_evses, provided_token.id_token, lk); // might block

                if (not select_evse_result.evse_id.has_value()) {
                    if (select_evse_result.status == SelectEvseReturnStatus::TimeOut) {
                        return TokenHandlingResult::TIMEOUT;
                    } else if (select_evse_result.status == SelectEvseReturnStatus::Interrupted) {
                        return TokenHandlingResult::WITHDRAWN;
                    }
                }

                int evse_id = select_evse_result.evse_id.value();
                EVLOG_debug << "Selected evse#" << evse_id
                            << " for token: " << everest::helpers::redact(provided_token.id_token.value);
                std::optional<std::string> parent_id_token;
                if (validation_result.parent_id_token.has_value()) {
                    parent_id_token = validation_result.parent_id_token.value().value;
                }
                const std::optional<int32_t> reservation_id = this->reservation_handler.matches_reserved_identifier(
                    provided_token.id_token.value, static_cast<uint32_t>(evse_id), parent_id_token);

                if (validation_result.evse_ids.has_value() and
                    intersect(referenced_evses, validation_result.evse_ids.value()).empty()) {
                    EVLOG_debug << "Empty intersection between referenced evses and evses that are authorized";
                    validation_result.authorization_status = AuthorizationStatus::NotAtThisLocation;
                } else if (reservation_id == std::nullopt &&
                           !this->reservation_handler.is_charging_possible(static_cast<uint32_t>(evse_id))) {
                    validation_result.authorization_status = AuthorizationStatus::NotAtThisTime;
                } else if (!this->reservation_handler.is_evse_reserved(static_cast<uint32_t>(evse_id)) &&
                           (reservation_id == std::nullopt)) {
                    EVLOG_info << "Providing authorization to evse#" << evse_id;
                    authorized = true;
                    provided_token.connectors = std::vector<int32_t>{evse_id};
                } else {
                    EVLOG_debug << "Evse is reserved. Checking if token matches...";

                    if (reservation_id.has_value()) {
                        EVLOG_info << "Evse#" << evse_id << " is reserved and token is valid for this reservation";
                        this->reservation_handler.on_reservation_used(reservation_id.value());
                        authorized = true;
                        provided_token.connectors = std::vector<int32_t>{evse_id};
                        validation_result.reservation_id = reservation_id.value();
                    } else {
                        EVLOG_info << "Evse#" << evse_id << " is reserved but token is not valid for this reservation";
                        validation_result.authorization_status = AuthorizationStatus::NotAtThisTime;
                    }
                }
                this->notify_evse(evse_id, provided_token, validation_result, lk);
            }
            i++;
        }
        if (authorized) {
            return TokenHandlingResult::USED_TO_START_TRANSACTION;
        } else {
            EVLOG_debug << "id_token could not be validated by any validator";
            // in case the validation was not successful, we need to notify the evse and transmit the validation result.
            // This is especially required for Plug&Charge with ISO15118 in order to allow the ISO15118 state machine to
            // escape the Authorize loop. We do this for all evses that were referenced
            if (provided_token.connectors.has_value()) {
                const auto connectors = provided_token.connectors.value();
                std::for_each(connectors.begin(), connectors.end(),
                              [this, provided_token, validation_result, &lk](int32_t connector) {
                                  this->notify_evse(connector, provided_token, validation_result, lk);
                              });
            }
            return TokenHandlingResult::REJECTED;
        }
    } else {
        EVLOG_warning << "No validation result was received by any validator.";
        return TokenHandlingResult::REJECTED;
    }
}

std::vector<int> AuthHandler::get_referenced_evses(const ProvidedIdToken& provided_token) {
    std::vector<int> evse_ids;

    // either insert the given connector references of the provided token
    if (provided_token.connectors) {
        std::copy_if(provided_token.connectors.value().begin(), provided_token.connectors.value().end(),
                     std::back_inserter(evse_ids), [this](int evse_id) {
                         if (this->evses.find(evse_id) != this->evses.end()) {
                             return !this->evses.at(evse_id)->is_unavailable();
                         } else {
                             EVLOG_warning << "Provided token included references to evse_id that does not exist";
                             return false;
                         }
                     });
    }
    // or if there is no reference to connectors take all connectors
    else {
        for (const auto& entry : this->evses) {
            if (!entry.second->is_unavailable()) {
                evse_ids.push_back(entry.first);
            }
        }
    }
    return evse_ids;
}

int AuthHandler::used_for_transaction(const std::vector<int>& evse_ids, const std::string& token) {
    for (const auto evse_id : evse_ids) {
        if (this->evses.at(evse_id)->identifier.has_value()) {
            const auto& identifier = this->evses.at(evse_id)->identifier.value();
            // check against id_token
            if (is_equal_case_insensitive(identifier.id_token.value, token)) {
                return evse_id;
            }
            // check against parent_id_token
            else if (identifier.parent_id_token.has_value() &&
                     is_equal_case_insensitive(identifier.parent_id_token.value().value, token)) {
                return evse_id;
            }
        }
    }
    return -1;
}

bool AuthHandler::is_token_already_in_process(const ProvidedIdToken& provided_id_token,
                                              const std::vector<int>& referenced_evses) {

    // checks if the token is currently already processed by the module (because already swiped)
    if (this->tokens_in_process.find(provided_id_token) != this->tokens_in_process.end()) {
        return true;
    } else {
        // check if id_token was already used to authorize evse but no transaction has been started yet
        for (const auto evse_id : referenced_evses) {
            const auto& evse = this->evses.at(evse_id);
            if (evse->identifier.has_value() &&
                is_equal_case_insensitive(evse->identifier.value().id_token, provided_id_token.id_token) &&
                !evse->transaction_active) {
                return true;
            }
        }
    }
    return false;
}

bool AuthHandler::any_evse_available(const std::vector<int>& evse_ids) {
    EVLOG_debug << "Checking availability of evses...";
    for (const auto evse_id : evse_ids) {
        if (this->evses.at(evse_id)->is_available()) {
            EVLOG_debug << "There is at least one evse available";
            return true;
        }
    }
    EVLOG_debug << "No evse is available for this id_token";
    return false;
}

bool AuthHandler::any_parent_id_present(const std::vector<int>& evse_ids) {
    for (const auto evse_id : evse_ids) {
        if (this->evses.at(evse_id)->identifier.has_value() and
            this->evses.at(evse_id)->identifier.value().parent_id_token.has_value()) {
            EVLOG_debug << "Parent id is currently present";
            return true;
        }
    }
    EVLOG_debug << "No parent id is currently present";
    return false;
}

bool AuthHandler::equals_master_pass_group_id(const std::optional<types::authorization::IdToken> parent_id_token) {
    if (!this->master_pass_group_id.has_value()) {
        return false;
    }

    if (!parent_id_token.has_value()) {
        return false;
    }

    return is_equal_case_insensitive(parent_id_token.value().value, this->master_pass_group_id.value());
}

int AuthHandler::get_latest_plugin(const std::vector<int>& evse_ids) {
    for (const auto evse_id : this->plug_in_queue) {
        if (std::find(evse_ids.begin(), evse_ids.end(), evse_id) != evse_ids.end()) {
            return evse_id;
        }
    }
    return -1;
}

AuthHandler::SelectEvseResult AuthHandler::select_evse(const std::vector<int>& selected_evses, const IdToken& id_token,
                                                       std::unique_lock<std::mutex>& lk) {
    SelectEvseResult result;

    if (selected_evses.size() == 1) {
        result.status = SelectEvseReturnStatus::EvseSelected;
        result.evse_id = selected_evses.at(0);
        return result;
    }

    if (this->selection_algorithm == SelectionAlgorithm::PlugEvents) {
        // locks all referenced evses for this request. Subsequent requests referencing one or more of the locked
        // evses are blocked until handle_token returns
        if (this->get_latest_plugin(selected_evses) == -1) {
            // no EV has been plugged in yet at the referenced evses
            EVLOG_debug << "No evse in authorization queue. Waiting for a plug in...";
            // blocks until respective plugin for evse occurred or until timeout
            if (!this->cv.wait_for(lk, std::chrono::seconds(this->connection_timeout),
                                   [this, selected_evses, id_token] {
                                       return this->get_latest_plugin(selected_evses) != -1 ||
                                              this->is_authorization_withdrawn(selected_evses, id_token);
                                   })) {
                result.status = SelectEvseReturnStatus::TimeOut;
                return result;
            }
            EVLOG_debug << "Plug in at evse occured or authorization withdrawn";
        }

        if (this->is_authorization_withdrawn(selected_evses, id_token)) {
            result.status = SelectEvseReturnStatus::Interrupted;
        } else {
            result.status = SelectEvseReturnStatus::EvseSelected;
            result.evse_id = this->get_latest_plugin(selected_evses);
        }

        return result;
    } else if (this->selection_algorithm == SelectionAlgorithm::FindFirst) {
        EVLOG_debug << "SelectionAlgorithm FindFirst: Selecting first available evse without an active transaction";
        const auto selected_evse_id = this->get_latest_plugin(selected_evses);
        if (selected_evse_id != -1 and !this->evses.at(selected_evse_id)->transaction_active) {
            // an EV has been plugged in yet at the referenced evses
            result.status = SelectEvseReturnStatus::EvseSelected;
            result.evse_id = this->get_latest_plugin(selected_evses);
            return result;
        } else {
            // no EV has been plugged in yet at the referenced evses; choosing the first one where no
            // transaction is active
            for (const auto& evse_id : selected_evses) {
                const auto& evse = this->evses.at(evse_id);
                if (evse->is_available()) {
                    result.status = SelectEvseReturnStatus::EvseSelected;
                    result.evse_id = evse_id;
                    return result;
                }
            }
        }
        result.status = SelectEvseReturnStatus::TimeOut;
        return result;
    } else {
        throw std::runtime_error("SelectionAlgorithm not implemented: " +
                                 selection_algorithm_to_string(this->selection_algorithm));
    }
}

/// Checks if given \p withdraw_request matches given \p id_token and/or one evse of given \p selected_evses
bool does_withdraw_request_match(const WithdrawAuthorizationRequest& withdraw_request,
                                 const std::vector<int>& selected_evses, const IdToken& id_token) {
    // Check if the withdraw request is specific to an EVSE
    const bool has_evse_id = withdraw_request.evse_id.has_value();
    const bool is_evse_in_selected =
        has_evse_id and (std::find(selected_evses.begin(), selected_evses.end(), withdraw_request.evse_id.value()) !=
                         selected_evses.end());

    // Check if the ID token matches or is absent
    const bool id_token_matches = not withdraw_request.id_token.has_value() or
                                  (is_equal_case_insensitive(withdraw_request.id_token.value(), id_token));

    return id_token_matches and (not has_evse_id or is_evse_in_selected);
}

bool AuthHandler::is_authorization_withdrawn(const std::vector<int>& selected_evses, const IdToken& id_token) {
    if (withdraw_request == nullptr) {
        return false;
    }
    return does_withdraw_request_match(*this->withdraw_request, selected_evses, id_token);
}

void AuthHandler::notify_evse(int evse_id, const ProvidedIdToken& provided_token,
                              const ValidationResult& validation_result, std::unique_lock<std::mutex>& lk) {
    const auto evse_index = this->evses.at(evse_id)->evse_index;

    if (validation_result.authorization_status == AuthorizationStatus::Accepted) {
        Identifier identifier{provided_token.id_token, provided_token.authorization_type,
                              validation_result.authorization_status, validation_result.expiry_time,
                              validation_result.parent_id_token};
        this->evses.at(evse_id)->identifier.emplace(identifier);

        // wait for potentially running timeout task to finish execution
        this->cv.wait(lk, [&evse = this->evses.at(evse_id)]() { return !evse->timeout_in_progress.load(); });

        this->evses.at(evse_id)->timeout_timer.stop();
        this->evses.at(evse_id)->timeout_timer.timeout(
            [this, evse_index, &evse = this->evses.at(evse_id), provided_token]() {
                evse->timeout_in_progress = true;
                std::lock_guard<std::mutex> lk(this->event_mutex);
                EVLOG_debug << "Authorization timeout for evse#" << evse_index;
                evse->identifier.reset();
                this->withdraw_authorization_callback(evse_index);
                this->publish_token_validation_status(provided_token, TokenValidationStatus::TimedOut);
                evse->timeout_in_progress = false;
                this->cv.notify_all();
            },
            std::chrono::seconds(this->connection_timeout));
        this->plug_in_queue.remove_if([evse_id](int value) { return value == evse_id; });
    }

    this->notify_evse_callback(evse_index, provided_token, validation_result);
}

types::reservation::ReservationResult AuthHandler::handle_reservation(const Reservation& reservation) {
    std::lock_guard<std::mutex> lk(this->event_mutex);
    std::optional<uint32_t> evse;
    if (reservation.evse_id.has_value()) {
        if (reservation.evse_id.value() >= 0) {
            evse = static_cast<uint32_t>(reservation.evse_id.value());
        }
    }

    return reservation_handler.make_reservation(evse, reservation);
}

std::pair<bool, std::optional<int32_t>> AuthHandler::handle_cancel_reservation(const int32_t reservation_id) {
    std::lock_guard<std::mutex> lk(this->event_mutex);
    std::pair<bool, std::optional<uint32_t>> reservation_cancelled = this->reservation_handler.cancel_reservation(
        reservation_id, false, types::reservation::ReservationEndReason::Cancelled);

    if (reservation_cancelled.first) {
        if (reservation_cancelled.second.has_value()) {
            return {true, static_cast<int32_t>(reservation_cancelled.second.value())};
        }
        return {true, std::nullopt};
    }

    return {false, std::nullopt};
}

ReservationCheckStatus AuthHandler::handle_reservation_exists(std::string& id_token, const std::optional<int>& evse_id,
                                                              std::optional<std::string>& group_id_token) {
    std::lock_guard<std::mutex> lk(this->event_mutex);
    // Evse id has no value.
    std::optional<int32_t> reservation_id =
        this->reservation_handler.matches_reserved_identifier(id_token, evse_id, group_id_token);

    if (!evse_id.has_value()) {
        if (reservation_id.has_value()) {
            return ReservationCheckStatus::ReservedForToken;
        }

        return ReservationCheckStatus::NotReserved;
    }

    // Evse id has a value.
    if (!this->reservation_handler.is_evse_reserved(evse_id.has_value())) {
        // There is an evse id, but the evse is not reserved.
        return ReservationCheckStatus::NotReserved;
    }

    if (reservation_id.has_value()) {
        // There is an evse id and the reservation is for the given token.
        return ReservationCheckStatus::ReservedForToken;
    }

    // Evse is reserved. No reservation for the given id_token. But there is also the group id token, let's do some
    // checks here.
    if (!group_id_token.has_value()) {
        if (reservation_handler.has_reservation_parent_id(evse_id)) {
            // Group id token has no value, but the reservation for this evse has a parent token. It might be that
            // this token will be checked later.
            return ReservationCheckStatus::ReservedForOtherTokenAndHasParentToken;
        }

        // Group id token has no value and the reservation for this evse has no parent token.
        return ReservationCheckStatus::ReservedForOtherToken;
    }

    // Group id token has a value but it is not valid for this reservation
    return ReservationCheckStatus::ReservedForOtherToken;
}

bool AuthHandler::call_reserved(const int reservation_id, const std::optional<int>& evse_id) {
    const bool reserved = this->reserved_callback(evse_id, reservation_id);
    if (reserved) {
        this->check_evse_reserved_and_send_updates();
    }

    return reserved;
}

void AuthHandler::call_reservation_cancelled(const int32_t reservation_id,
                                             const types::reservation::ReservationEndReason reason,
                                             const std::optional<int>& evse_id, const bool send_reservation_update) {
    std::optional<int32_t> evse_index;
    if (evse_id.has_value() && evse_id.value() > 0) {
        EVLOG_info << "Cancel reservation for evse id " << evse_id.value();
    }

    this->reservation_cancelled_callback(evse_id, reservation_id, reason, send_reservation_update);

    // If this was a global reservation or there are global reservations which made evse's go to reserved because of
    // this reservation, they should be cancelled.
    this->check_evse_reserved_and_send_updates();
}

void AuthHandler::handle_permanent_fault_raised(const int evse_id, const int32_t connector_id) {
    std::lock_guard<std::mutex> lk(this->event_mutex);
    if (not ignore_faults) {
        this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::FAULTED);
    }
}

void AuthHandler::handle_permanent_fault_cleared(const int evse_id, const int32_t connector_id) {
    std::lock_guard<std::mutex> lk(this->event_mutex);
    if (not ignore_faults) {
        this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::ERROR_CLEARED);
    }
}

void AuthHandler::handle_session_event(const int evse_id, const SessionEvent& event) {
    std::unique_lock<std::mutex> lk(this->event_mutex);
    // When connector id is not specified, it is assumed to be '1'.
    const int32_t connector_id = event.connector_id.value_or(1);
    if (evse_id <= 0) {
        EVLOG_error << "Handle session event: Evse id is <= 0: That should not be possible.";
        return;
    }

    if (connector_id <= 0) {
        EVLOG_error << "Handle session event: connector id is <= 0: That should not be possible.";
        return;
    }

    if (this->evses.count(evse_id) == 0) {
        EVLOG_warning << "Handle session event: no evse found with evse id " << evse_id;
        return;
    }

    const auto event_type = event.event;
    bool check_reservations = false;

    switch (event_type) {
    case SessionEventEnum::SessionStarted: {

        // only set plug in timeout when SessionStart is caused by plug in
        if (event.session_started.value().reason == StartSessionReason::EVConnected) {
            this->plug_in_queue.push_back(evse_id);
            this->cv.notify_all();
            this->evses.at(evse_id)->plugged_in = true;
            EVLOG_info << "Plug In event for evse#" << evse_id << ", starting auth";

            // only set timeout if there are multiple evses managed by this auth handler
            // or plug in timeout is explicitly enabled
            if (this->evses.size() == 1 or !this->plug_in_timeout_enabled) {
                break;
            }

            this->evses.at(evse_id)->timeout_timer.timeout(
                [this, evse_id]() {
                    this->evses.at(evse_id)->timeout_in_progress = true;
                    std::lock_guard<std::mutex> lk(this->event_mutex);

                    EVLOG_info << "Plug In timeout for evse#" << evse_id << ". Replug required for this EVSE";
                    this->withdraw_authorization_callback(this->evses.at(evse_id)->evse_index);

                    this->plug_in_queue.remove_if([evse_id](int value) { return value == evse_id; });
                    this->evses.at(evse_id)->plug_in_timeout = true;
                    this->evses.at(evse_id)->timeout_in_progress = false;
                    this->cv.notify_all();
                },
                std::chrono::seconds(this->connection_timeout));
        }
    } break;
    case SessionEventEnum::TransactionStarted: {
        this->evses.at(evse_id)->plugged_in = true;
        this->evses.at(evse_id)->transaction_active = true;
        this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::TRANSACTION_STARTED);
        // wait for potentially running timeout task to finish execution
        this->cv.wait(lk, [&evse = this->evses.at(evse_id)]() { return !evse->timeout_in_progress.load(); });
        this->evses.at(evse_id)->timeout_timer.stop();
        check_reservations = true;
        break;
    }
    case SessionEventEnum::TransactionFinished:
        this->evses.at(evse_id)->transaction_active = false;
        this->evses.at(evse_id)->identifier.reset();
        break;
    case SessionEventEnum::SessionFinished: {
        this->evses.at(evse_id)->plugged_in = false;
        this->evses.at(evse_id)->plug_in_timeout = false;
        this->evses.at(evse_id)->identifier.reset();
        this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::SESSION_FINISHED);
        // wait for potentially running timeout task to finish execution
        this->cv.wait(lk, [&evse = this->evses.at(evse_id)]() { return !evse->timeout_in_progress.load(); });
        this->evses.at(evse_id)->timeout_timer.stop();
        this->plug_in_queue.remove_if([evse_id](int value) { return value == evse_id; });
        check_reservations = true;
        break;
    }
    case SessionEventEnum::Disabled:
        this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::DISABLE);
        check_reservations = true;
        break;
    case SessionEventEnum::Enabled:
        this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::ENABLE);
        check_reservations = true;
        break;
    case SessionEventEnum::Deauthorized:
        this->evses.at(evse_id)->identifier.reset();
        // wait for potentially running timeout task to finish execution
        this->cv.wait(lk, [&evse = this->evses.at(evse_id)]() { return !evse->timeout_in_progress.load(); });
        this->evses.at(evse_id)->timeout_timer.stop();
        break;
    case SessionEventEnum::ReservationStart:
        break;
    case SessionEventEnum::ReservationEnd: {
        if (reservation_handler.is_evse_reserved(evse_id)) {
            reservation_handler.cancel_reservation(evse_id, true);
        }
        break;
    }
    /// explicitly fall through all the SessionEventEnum values we are not handling
    case SessionEventEnum::Authorized:
        [[fallthrough]];
    case SessionEventEnum::AuthRequired:
        [[fallthrough]];
    case SessionEventEnum::PrepareCharging:
        [[fallthrough]];
    case SessionEventEnum::ChargingStarted:
        [[fallthrough]];
    case SessionEventEnum::ChargingPausedEV:
        [[fallthrough]];
    case SessionEventEnum::ChargingPausedEVSE:
        [[fallthrough]];
    case SessionEventEnum::StoppingCharging:
        [[fallthrough]];
    case SessionEventEnum::ChargingFinished:
        [[fallthrough]];
    case SessionEventEnum::PluginTimeout:
        [[fallthrough]];
    case SessionEventEnum::SwitchingPhases:
        [[fallthrough]];
    case SessionEventEnum::SessionResumed:
        break;
    }

    // When reservation is started or ended, check if the number of reservations match the number of evses and
    // send 'reserved' notifications to the evse manager accordingly if needed.
    if (check_reservations) {
        this->check_evse_reserved_and_send_updates();
    }
}

void AuthHandler::set_connection_timeout(const int connection_timeout) {
    std::lock_guard<std::mutex> lk(this->event_mutex);
    this->connection_timeout = connection_timeout;
};

void AuthHandler::set_plug_in_timeout_enabled(bool enabled) {
    std::lock_guard<std::mutex> lk(this->event_mutex);
    this->plug_in_timeout_enabled = enabled;
}

void AuthHandler::set_master_pass_group_id(const std::string& master_pass_group_id) {
    std::lock_guard<std::mutex> lk(this->event_mutex);
    if (master_pass_group_id.empty()) {
        this->master_pass_group_id = std::nullopt;
    } else {
        this->master_pass_group_id = master_pass_group_id;
    }
}

void AuthHandler::set_prioritize_authorization_over_stopping_transaction(bool b) {
    std::lock_guard<std::mutex> lk(this->event_mutex);
    this->prioritize_authorization_over_stopping_transaction = b;
}

void AuthHandler::register_notify_evse_callback(
    const std::function<void(const int evse_index, const ProvidedIdToken& provided_token,
                             const ValidationResult& validation_result)>& callback) {
    this->notify_evse_callback = callback;
}

void AuthHandler::register_withdraw_authorization_callback(const std::function<void(const int evse_index)>& callback) {
    this->withdraw_authorization_callback = callback;
}
void AuthHandler::register_validate_token_callback(
    const std::function<std::vector<ValidationResult>(const ProvidedIdToken& provided_token)>& callback) {
    this->validate_token_callback = callback;
}
void AuthHandler::register_stop_transaction_callback(
    const std::function<void(const int evse_index, const StopTransactionRequest& request)>& callback) {
    this->stop_transaction_callback = callback;
}

void AuthHandler::register_reserved_callback(
    const std::function<bool(const std::optional<int>& evse_id, const int& reservation_id)>& callback) {
    this->reserved_callback = callback;
}

void AuthHandler::register_reservation_cancelled_callback(
    const std::function<void(const std::optional<int32_t>& evse_id, const int32_t reservation_id,
                             const ReservationEndReason reason, const bool send_reservation_update)>& callback) {
    this->reservation_cancelled_callback = callback;
    this->reservation_handler.register_reservation_cancelled_callback(
        [this](const std::optional<int32_t>& evse_id, const int32_t reservation_id,
               const types::reservation::ReservationEndReason reason, const bool send_reservation_update) {
            if (evse_id.has_value() && evse_id.value() < 0) {
                EVLOG_warning << "Reservation cancelled: evse id is negative (" << evse_id.value()
                              << "), that should not be possible.";
                return;
            }

            this->call_reservation_cancelled(reservation_id, reason, evse_id, send_reservation_update);
        });
}

void AuthHandler::register_publish_token_validation_status_callback(
    const std::function<void(const ProvidedIdToken&, TokenValidationStatus, const std::vector<MessageContent>&)>&
        callback) {
    this->publish_token_validation_status_callback = callback;
}

void AuthHandler::publish_token_validation_status(const ProvidedIdToken& token, TokenValidationStatus status,
                                                  const std::vector<MessageContent>& tariff_messages) {
    this->publish_token_validation_status_callback(token, status, tariff_messages);
}

WithdrawAuthorizationResult AuthHandler::handle_withdraw_authorization(const WithdrawAuthorizationRequest& request) {
    std::lock_guard<std::mutex> lg(this->withdraw_mutex);
    EVLOG_info << "Witdrawing authorization"
               << (request.evse_id.has_value() ? " evse: " + std::to_string(request.evse_id.value()) : "")
               << (request.id_token.has_value() ? " id token: " + request.id_token.value().value : "");

    if (request.evse_id.has_value() and this->evses.find(request.evse_id.value()) == this->evses.end()) {
        return WithdrawAuthorizationResult::EvseNotFound;
    }

    auto result = WithdrawAuthorizationResult::AuthorizationNotFound; // default

    // Wait for processing threads to finish executing
    std::unique_lock lock(this->event_mutex);

    const auto is_withdraw_request_targeting_token_in_process = [this](const WithdrawAuthorizationRequest& request) {
        for (const auto& token_in_process : this->tokens_in_process) {
            auto referenced_evses = this->get_referenced_evses(token_in_process);
            if (does_withdraw_request_match(request, referenced_evses, token_in_process.id_token)) {
                return true;
            }
        }
        return false;
    };

    this->withdraw_request = std::make_unique<WithdrawAuthorizationRequest>(request);

    if (is_withdraw_request_targeting_token_in_process(request)) {
        // Notify processing threads that wait within select_evse
        // This will interrupt the wait for a plug in in case the authorization is withdrawn by this request
        this->cv.notify_all();

        // Release the event_mutex lock and wait for threads to finish...
        this->processing_finished_cv.wait(
            lock, [&]() { return not is_withdraw_request_targeting_token_in_process(request); });

        result = WithdrawAuthorizationResult::Accepted;
    }

    // It might still be the case that the withdraw request is also targeting already granted authorization so we
    // continue checking for this

    const auto withdraw_authorization_or_stop_transaction = [this](const EVSEContext& evse) {
        if (evse.transaction_active) {
            StopTransactionRequest req;
            req.reason = StopTransactionReason::DeAuthorized;
            this->stop_transaction_callback(evse.evse_index, req);
        } else {
            this->withdraw_authorization_callback(evse.evse_index);
        }
    };

    if (request.evse_id.has_value() and request.id_token.has_value()) {
        // evse_id and id_token is specified
        // find if there is a granted authorization for id_token and evse_id
        const auto evse_id = request.evse_id.value();
        const auto id_token = request.id_token.value();
        const auto& evse = this->evses.at(evse_id);
        if (evse->identifier.has_value() &&
            is_equal_case_insensitive(request.id_token.value(), evse->identifier.value().id_token)) {
            withdraw_authorization_or_stop_transaction(*evse);
            result = WithdrawAuthorizationResult::Accepted;
        }

    } else if (request.evse_id.has_value()) {
        // only evse_id is specified
        // find if there is a granted authorization for evse_id
        const auto evse_id = request.evse_id.value();
        const auto& evse = this->evses.at(evse_id);
        if (evse->identifier.has_value()) {
            withdraw_authorization_or_stop_transaction(*evse);
            result = WithdrawAuthorizationResult::Accepted;
        }
    } else if (request.id_token.has_value()) {
        // only id_token is specified
        // find if there is a granted authorization for id_token
        for (const auto& evse : this->evses) {
            if (evse.second->identifier.has_value() &&
                is_equal_case_insensitive(request.id_token.value(), evse.second->identifier.value().id_token)) {
                withdraw_authorization_or_stop_transaction(*evse.second);
                result = WithdrawAuthorizationResult::Accepted;
            }
        }
    } else {
        // neither evse_id nor id_token is specified, withdraw all authorizations that have been granted
        for (const auto& evse : this->evses) {
            if (evse.second->identifier.has_value()) {
                withdraw_authorization_or_stop_transaction(*evse.second);
                result = WithdrawAuthorizationResult::Accepted;
            }
        }
    }

    // result was either set in one of the if statements above or is still AuthorizationNotFound
    return result;
}

void AuthHandler::submit_event_for_connector(const int32_t evse_id, const int32_t connector_id,
                                             const ConnectorEvent connector_event) {
    for (auto& connector : this->evses.at(evse_id)->connectors) {
        if (connector.id == connector_id) {
            connector.submit_event(connector_event);
            this->reservation_handler.on_connector_state_changed(connector.get_state(), evse_id, connector_id);
            break;
        }
    }
}

void AuthHandler::check_evse_reserved_and_send_updates() {
    ReservationEvseStatus reservation_status =
        this->reservation_handler.check_number_global_reservations_match_number_available_evses();
    for (const auto& available_evse : reservation_status.available) {
        EVLOG_debug << "Evse " << available_evse << " is now available";
        this->reservation_cancelled_callback(
            available_evse, -1, types::reservation::ReservationEndReason::GlobalReservationRequirementDropped, false);
    }

    for (const auto& reserved_evse : reservation_status.reserved) {
        EVLOG_debug << "Evse " << reserved_evse << " is now reserved";
        if (this->reserved_callback != nullptr) {
            const bool reserved = this->reserved_callback(reserved_evse, -1);
            if (!reserved) {
                EVLOG_warning << "Could not reserve " << reserved_evse << " for non evse specific reservations";
            }
        }
    }
}

} // namespace module
