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

#include "energyImpl.hpp"
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <date/date.h>
#include <date/tz.h>
#include <fmt/core.h>
#include <string>
#include <utils/date.hpp>
#include <utils/formatter.hpp>

namespace module {
namespace energy_grid {

// helper to find out if voltage changed (more then noise)
static bool voltage_changed(float a, float b) {
    constexpr float noise_voltage = 1;
    return (fabs(a - b) > noise_voltage);
}

void energyImpl::init() {

    charger_state = Charger::EvseState::Disabled;

    source_base = mod->info.id;
    source_bsp_caps = source_base + "/evse_board_support_caps";
    source_psu_caps = source_base + "/powersupply_dc_caps";

    std::srand((unsigned)time(0));

    // UUID must be unique also beyond this charging station -> will be handled on framework level and above later
    energy_flow_request.uuid = mod->info.id;
    energy_flow_request.node_type = types::energy::NodeType::Evse;

    if (mod->r_powermeter_car_side.size()) {
        mod->r_powermeter_car_side[0]->subscribe_powermeter([this](types::powermeter::Powermeter p) {
            // Received new power meter values, update our energy object.
            std::lock_guard<std::mutex> lock(this->energy_mutex);
            energy_flow_request.energy_usage_leaves = p;
        });
    }

    if (mod->r_powermeter_grid_side.size()) {
        mod->r_powermeter_grid_side[0]->subscribe_powermeter([this](types::powermeter::Powermeter p) {
            // Received new power meter values, update our energy object.
            std::lock_guard<std::mutex> lock(this->energy_mutex);
            energy_flow_request.energy_usage_root = p;
        });
    }
}

void energyImpl::clear_import_request_schedule() {
    types::energy::ScheduleReqEntry entry_import;
    const auto tpnow = date::utc_clock::now();
    const auto tp =
        Everest::Date::to_rfc3339(date::floor<std::chrono::hours>(tpnow) + date::get_leap_second_info(tpnow).elapsed);

    entry_import.timestamp = tp;
    entry_import.limits_to_root.ac_max_phase_count = {hw_caps.max_phase_count_import};
    entry_import.limits_to_root.ac_min_phase_count = {hw_caps.min_phase_count_import};
    entry_import.limits_to_root.ac_max_current_A = {hw_caps.max_current_A_import};
    entry_import.limits_to_root.ac_min_current_A = {hw_caps.min_current_A_import};
    entry_import.limits_to_root.ac_supports_changing_phases_during_charging =
        hw_caps.supports_changing_phases_during_charging;
    entry_import.limits_to_root.ac_number_of_active_phases = mod->ac_nr_phases_active;

    if (mod->config.charge_mode == "DC") {
        // For DC, apply our power supply capabilities as limit on leaves side
        const auto caps = mod->get_powersupply_capabilities();
        entry_import.limits_to_leaves.total_power_W = {caps.max_export_power_W,
                                                       source_base + "/clear_import_request_schedule"};
        // Note both sides are optionals
        entry_import.conversion_efficiency = caps.conversion_efficiency_export;
    }

    energy_flow_request.schedule_import = std::vector<types::energy::ScheduleReqEntry>({entry_import});
}

void energyImpl::clear_export_request_schedule() {
    types::energy::ScheduleReqEntry entry_export;
    const auto tpnow = date::utc_clock::now();
    const auto tp =
        Everest::Date::to_rfc3339(date::floor<std::chrono::hours>(tpnow) + date::get_leap_second_info(tpnow).elapsed);

    entry_export.timestamp = tp;
    entry_export.limits_to_root.ac_max_phase_count = {hw_caps.max_phase_count_export, source_bsp_caps};
    entry_export.limits_to_root.ac_min_phase_count = {hw_caps.min_phase_count_export, source_bsp_caps};
    entry_export.limits_to_root.ac_max_current_A = {hw_caps.max_current_A_export, source_bsp_caps};
    entry_export.limits_to_root.ac_min_current_A = {hw_caps.min_current_A_export, source_bsp_caps};
    entry_export.limits_to_root.ac_supports_changing_phases_during_charging =
        hw_caps.supports_changing_phases_during_charging;
    entry_export.limits_to_root.ac_number_of_active_phases = mod->ac_nr_phases_active;

    if (mod->config.charge_mode == "DC") {
        // For DC, apply our power supply capabilities as limit on leaves side
        const auto caps = mod->get_powersupply_capabilities();
        entry_export.limits_to_leaves.total_power_W = {caps.max_import_power_W.value_or(0), source_psu_caps};
        // Note that both sides are optionals
        entry_export.conversion_efficiency = caps.conversion_efficiency_import;
    }

    energy_flow_request.schedule_export = std::vector<types::energy::ScheduleReqEntry>({entry_export});
}

void energyImpl::clear_request_schedules() {
    this->clear_import_request_schedule();
    this->clear_export_request_schedule();
}

void energyImpl::ready() {
    hw_caps = mod->get_hw_capabilities();
    last_powersupply_capabilities = mod->get_powersupply_capabilities();
    clear_request_schedules();

    // request energy now
    request_energy_from_energy_manager(true);

    // request energy every second
    std::thread([this] {
        while (true) {
            hw_caps = mod->get_hw_capabilities();
            request_energy_from_energy_manager(false);
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    }).detach();

    // request energy at the start and end of a charging session
    mod->charger->signal_state.connect([this](Charger::EvseState s) {
        charger_state = s;
        if (s == Charger::EvseState::WaitingForAuthentication || s == Charger::EvseState::Finished) {
            std::thread request_energy_thread([this]() { request_energy_from_energy_manager(true); });
            request_energy_thread.detach();
        }
    });
}

types::energy::EvseState to_energy_evse_state(const Charger::EvseState charger_state) {
    switch (charger_state) {
    case Charger::EvseState::Disabled:
        return types::energy::EvseState::Disabled;
        break;
    case Charger::EvseState::Idle:
        return types::energy::EvseState::Unplugged;
        break;
    case Charger::EvseState::WaitingForAuthentication:
        return types::energy::EvseState::WaitForAuth;
        break;
    case Charger::EvseState::PrepareCharging:
        return types::energy::EvseState::PrepareCharging;
        break;
    case Charger::EvseState::Charging:
        return types::energy::EvseState::Charging;
        break;
    case Charger::EvseState::ChargingPausedEV:
        return types::energy::EvseState::PausedEV;
        break;
    case Charger::EvseState::ChargingPausedEVSE:
        return types::energy::EvseState::PausedEVSE;
        break;
    case Charger::EvseState::StoppingCharging:
        return types::energy::EvseState::Finished;
        break;
    case Charger::EvseState::Finished:
        return types::energy::EvseState::Finished;
        break;
    case Charger::EvseState::T_step_EF:
        return types::energy::EvseState::PrepareCharging;
        break;
    case Charger::EvseState::T_step_X1:
        return types::energy::EvseState::PrepareCharging;
        break;
    case Charger::EvseState::SwitchPhases:
        return types::energy::EvseState::Charging;
        break;
    }
    return types::energy::EvseState::Disabled;
}

void energyImpl::request_energy_from_energy_manager(bool priority_request) {
    std::lock_guard<std::mutex> lock(this->energy_mutex);
    clear_import_request_schedule();
    clear_export_request_schedule();

    // If we need energy, copy local limit schedules to energy_flow_request.
    if (charger_state == Charger::EvseState::Charging || charger_state == Charger::EvseState::PrepareCharging ||
        charger_state == Charger::EvseState::WaitingForAuthentication ||
        charger_state == Charger::EvseState::ChargingPausedEV || !mod->config.request_zero_power_in_idle) {

        // copy complete external limit schedules for import
        if (not mod->get_local_energy_limits().schedule_import.empty()) {
            energy_flow_request.schedule_import = mod->get_local_energy_limits().schedule_import;

            if (mod->config.charge_mode == "DC") {
                // For DC, apply our power supply capabilities as an additional limit on leaves side
                const auto caps = mod->get_powersupply_capabilities();
                for (auto& entry : energy_flow_request.schedule_import) {
                    // Apply caps limit if we request a leaves side watt value and it is larger than the capabilities
                    if (entry.limits_to_leaves.total_power_W.has_value() and
                        entry.limits_to_leaves.total_power_W.value().value > caps.max_export_power_W) {
                        entry.limits_to_leaves.total_power_W = {caps.max_export_power_W, source_psu_caps};
                    }
                }
            }
        }

        // apply our local hardware limits on root side
        for (auto& e : energy_flow_request.schedule_import) {
            if (!e.limits_to_root.ac_max_current_A.has_value() ||
                e.limits_to_root.ac_max_current_A.value().value > hw_caps.max_current_A_import) {
                e.limits_to_root.ac_max_current_A = {hw_caps.max_current_A_import, source_bsp_caps};

                // are we in EV pause mode? -> Reduce requested current to minimum just to see when car
                // wants to start charging again. The energy manager may pause us externally to reduce to
                // zero
                if (charger_state == Charger::EvseState::ChargingPausedEV && mod->config.request_zero_power_in_idle) {
                    e.limits_to_root.ac_max_current_A = {hw_caps.min_current_A_import, source_bsp_caps};
                }
            }

            if (!e.limits_to_root.ac_max_phase_count.has_value() ||
                e.limits_to_root.ac_max_phase_count.value().value > hw_caps.max_phase_count_import)
                e.limits_to_root.ac_max_phase_count = {hw_caps.max_phase_count_import, source_bsp_caps};

            // copy remaining hw limits on root side
            e.limits_to_root.ac_min_phase_count = {hw_caps.min_phase_count_import, source_bsp_caps};
            e.limits_to_root.ac_min_current_A = {hw_caps.min_current_A_import, source_bsp_caps};
            e.limits_to_root.ac_supports_changing_phases_during_charging =
                hw_caps.supports_changing_phases_during_charging;
            e.limits_to_root.ac_number_of_active_phases = mod->ac_nr_phases_active;
        }

        // copy complete external limit schedules for export
        if (not mod->get_local_energy_limits().schedule_export.empty()) {
            energy_flow_request.schedule_export = mod->get_local_energy_limits().schedule_export;

            if (mod->config.charge_mode == "DC") {
                // For DC, apply our power supply capabilities as an additional limit on leaves side
                const auto caps = mod->get_powersupply_capabilities();
                for (auto& entry : energy_flow_request.schedule_export) {
                    if (entry.limits_to_leaves.total_power_W.has_value() and caps.max_import_power_W.has_value() and
                        entry.limits_to_leaves.total_power_W.value().value > caps.max_import_power_W.value()) {
                        entry.limits_to_leaves.total_power_W = {caps.max_import_power_W.value(), source_bsp_caps};
                    }
                }
            }
        }

        // apply our local hardware limits on root side
        for (auto& e : energy_flow_request.schedule_export) {
            if (!e.limits_to_root.ac_max_current_A.has_value() ||
                e.limits_to_root.ac_max_current_A.value().value > hw_caps.max_current_A_export) {
                e.limits_to_root.ac_max_current_A = {hw_caps.max_current_A_export, source_bsp_caps};

                // are we in EV pause mode? -> Reduce requested current to minimum just to see when car
                // wants to start discharging again. The energy manager may pause us externally to reduce to
                // zero
                if (charger_state == Charger::EvseState::ChargingPausedEV) {
                    e.limits_to_root.ac_max_current_A = {hw_caps.min_current_A_export, source_bsp_caps + "_pause"};
                }
            }

            if (!e.limits_to_root.ac_max_phase_count.has_value() ||
                e.limits_to_root.ac_max_phase_count.value().value > hw_caps.max_phase_count_export)
                e.limits_to_root.ac_max_phase_count = {hw_caps.max_phase_count_export, source_bsp_caps};

            // copy remaining hw limits on root side
            e.limits_to_root.ac_min_phase_count = {hw_caps.min_phase_count_export, source_bsp_caps};
            e.limits_to_root.ac_min_current_A = {hw_caps.min_current_A_export, source_bsp_caps};
            e.limits_to_root.ac_supports_changing_phases_during_charging =
                hw_caps.supports_changing_phases_during_charging;
            e.limits_to_root.ac_number_of_active_phases = mod->ac_nr_phases_active;
        }

        // Cap by PP cable rating so the EnergyManager cannot allocate more than the cable allows
        if (mod->config.charge_mode == "AC") {
            const auto pp_rating = mod->bsp->read_pp_ampacity();
            if (pp_rating) {
                const std::string source_pp = source_base + "/pp_ampacity";
                for (auto& e : energy_flow_request.schedule_import) {
                    if (e.limits_to_root.ac_max_current_A.has_value() &&
                        e.limits_to_root.ac_max_current_A.value().value > pp_rating.value()) {
                        e.limits_to_root.ac_max_current_A = {static_cast<float>(pp_rating.value()), source_pp};
                    }
                }
            }
        }

        if (mod->config.charge_mode == "DC") {
            // For DC mode remove amp limit on leave side if any
            for (auto& e : energy_flow_request.schedule_import) {
                e.limits_to_leaves.ac_max_current_A.reset();
            }
            for (auto& e : energy_flow_request.schedule_export) {
                e.limits_to_leaves.ac_max_current_A.reset();
            }
        }
    } else {
        if (mod->config.charge_mode == "DC") {
            // we dont need power at the moment
            energy_flow_request.schedule_import[0].limits_to_leaves.total_power_W = {0., "Idle"};
            energy_flow_request.schedule_export[0].limits_to_leaves.total_power_W = {0., "Idle"};
        } else {
            energy_flow_request.schedule_import[0].limits_to_leaves.ac_max_current_A = {0., "Idle"};
            energy_flow_request.schedule_export[0].limits_to_leaves.ac_max_current_A = {0., "Idle"};
        }
    }

    if (priority_request) {
        energy_flow_request.priority_request = true;
    } else {
        energy_flow_request.priority_request = false;
    }

    // Attach our state
    energy_flow_request.evse_state = to_energy_evse_state(charger_state);

    publish_energy_flow_request(energy_flow_request);
    // EVLOG_info << "Outgoing request " << energy_flow_request;
}

static bool almost_eq(float a, float b) {
    return a > b - 0.1 and a < b + 0.1;
}

static bool almost_eq(std::optional<float> const& a, std::optional<float> const& b) {
    if (a.has_value() and b.has_value()) {
        return almost_eq(a.value(), b.value());
    }
    if (not a.has_value() and not b.has_value()) {
        return true;
    }
    return false;
}

static bool almost_eq(types::power_supply_DC::Capabilities const& lhs,
                      types::power_supply_DC::Capabilities const& rhs) {
    bool result = lhs.bidirectional == rhs.bidirectional and
                  almost_eq(lhs.current_regulation_tolerance_A, rhs.current_regulation_tolerance_A) and
                  almost_eq(lhs.peak_current_ripple_A, rhs.peak_current_ripple_A) and
                  almost_eq(lhs.max_export_voltage_V, rhs.max_export_voltage_V) and
                  almost_eq(lhs.min_export_voltage_V, rhs.min_export_voltage_V) and
                  almost_eq(lhs.max_export_current_A, rhs.max_export_current_A) and
                  almost_eq(lhs.min_export_current_A, rhs.min_export_current_A) and
                  almost_eq(lhs.max_export_power_W, rhs.max_export_power_W) and
                  almost_eq(lhs.max_import_voltage_V, rhs.max_import_voltage_V) and
                  almost_eq(lhs.min_import_voltage_V, rhs.min_import_voltage_V) and
                  almost_eq(lhs.max_import_current_A, rhs.max_import_current_A) and
                  almost_eq(lhs.min_import_current_A, rhs.min_import_current_A) and
                  almost_eq(lhs.max_import_power_W, rhs.max_import_power_W) and
                  almost_eq(lhs.conversion_efficiency_import, rhs.conversion_efficiency_import) and
                  almost_eq(lhs.conversion_efficiency_export, rhs.conversion_efficiency_export);
    return result;
}

// This is the decision logic when limits are changing.
bool energyImpl::random_delay_needed(float last_limit, float limit) {

    if (mod->config.uk_smartcharging_random_delay_at_any_change) {
        if (not almost_eq(last_limit, limit)) {
            return true;
        }
    } else {
        if (almost_eq(last_limit, 0.) and limit > 0.1) {
            return true;
        } else if (last_limit > 0.1 and almost_eq(limit, 0.)) {
            return true;
        }
    }

    // Are we starting up with a car attached? This will need a random delay as well
    if ((charger_state == Charger::EvseState::PrepareCharging or charger_state == Charger::EvseState::Charging or
         charger_state == Charger::EvseState::WaitingForAuthentication) and
        std::chrono::steady_clock::now() - mod->timepoint_ready_for_charging.load() <
            detect_startup_with_ev_attached_duration) {
        last_enforced_limit = 0.;
        return true;
    }

    return false;
}

void energyImpl::handle_enforce_limits(types::energy::EnforcedLimits& value) {
    if (value.uuid == energy_flow_request.uuid) {
        // EVLOG_info << "Incoming enforce limits" << value;

        //   set hardware limit
        float limit = 0.;
        int active_phasecount = mod->ac_nr_phases_active;

        // apply enforced limits

        // set enforced AC current limit
        if (value.limits_root_side.ac_max_current_A.has_value()) {
            limit = value.limits_root_side.ac_max_current_A.value().value;
        }

        // apply number of phase limit
        if (value.limits_root_side.ac_max_phase_count.has_value() &&
            value.limits_root_side.ac_max_phase_count.value().value not_eq active_phasecount) {
            if (mod->get_hw_capabilities().supports_changing_phases_during_charging) {
                if (mod->charger->switch_three_phases_while_charging(
                        value.limits_root_side.ac_max_phase_count.value().value == 3)) {
                    mod->ac_nr_phases_active = value.limits_root_side.ac_max_phase_count.value().value;
                    EVLOG_info << fmt::format("3ph/1ph: Switching #ph from {} to {}", active_phasecount,
                                              value.limits_root_side.ac_max_phase_count.value().value);
                } else {
                    EVLOG_warning << fmt::format(
                        "3ph/1ph: Energymanager requests switching #ph from {} to {}, ignored.", active_phasecount,
                        value.limits_root_side.ac_max_phase_count.value().value);
                }
            } else {
                EVLOG_error << fmt::format(
                    "Energy manager requests switching #ph from {} to {}, but switching phases during "
                    "charging is not supported by HW.",
                    active_phasecount, value.limits_root_side.ac_max_phase_count.value().value);
            }
        }

        // apply watt limit
        if (value.limits_root_side.total_power_W.has_value()) {
            mod->mqtt.publish(fmt::format("everest_external/nodered/{}/state/max_watt", mod->config.connector_id),
                              value.limits_root_side.total_power_W.value().value);
            // watt limit converted to current limit
            const float current_limit_power = value.limits_root_side.total_power_W.value().value /
                                              mod->config.ac_nominal_voltage / mod->ac_nr_phases_active;
            if ((limit >= 0 and limit > current_limit_power) or (limit < 0 and limit < current_limit_power)) {
                limit = current_limit_power;
            }
        }

        const auto enforced_limit = limit;

        // check if we need to add a random delay for UK smart charging regs
        if (mod->random_delay_enabled) {

            // Are we in a state where a random delay makes sense?
            if (not(charger_state == Charger::EvseState::PrepareCharging or
                    charger_state == Charger::EvseState::Charging or
                    charger_state == Charger::EvseState::WaitingForAuthentication)) {
                mod->random_delay_running = false;
            }

            // Do we need to start a new random delay?
            // Ignore changes of less then 0.1 amps
            if (not mod->random_delay_running and random_delay_needed(last_enforced_limit, limit)) {
                mod->random_delay_running = true;
                mod->random_delay_start_time = date::utc_clock::now();
                auto random_delay_s = std::rand() % mod->random_delay_max_duration.load().count();
                mod->random_delay_end_time = std::chrono::steady_clock::now() + std::chrono::seconds(random_delay_s);
                EVLOG_info << "UK Smart Charging regulations: Starting random delay of " << random_delay_s << "s";
                limit_when_random_delay_started = last_enforced_limit;
            }

            // If a delay is running, replace the current limit with the stored value
            if (mod->random_delay_running) {
                // use limit from the time point when the random delay started
                limit = limit_when_random_delay_started;
                // publish the current random delay timer
                auto seconds_left = std::chrono::duration_cast<std::chrono::seconds>(mod->random_delay_end_time -
                                                                                     std::chrono::steady_clock::now())
                                        .count();
                types::uk_random_delay::CountDown c;
                c.current_limit_after_delay_A = enforced_limit;
                c.current_limit_during_delay_A = limit_when_random_delay_started;
                if (seconds_left <= 0) {
                    EVLOG_info << "UK Smart Charging regulations: Random delay elapsed.";
                    c.countdown_s = 0;
                    mod->random_delay_running = false;
                } else {
                    EVLOG_debug << "Random delay running, " << seconds_left
                                << "s left. Applying the limit before the random delay ("
                                << limit_when_random_delay_started << "A) instead of requested limit ("
                                << enforced_limit << "A)";
                    c.countdown_s = seconds_left;
                    c.start_time = Everest::Date::to_rfc3339(mod->random_delay_start_time);
                }
                mod->p_random_delay->publish_countdown(c);
            } else {
                types::uk_random_delay::CountDown c;
                c.countdown_s = 0;
                c.current_limit_after_delay_A = enforced_limit;
                c.current_limit_during_delay_A = limit_when_random_delay_started;
                mod->p_random_delay->publish_countdown(c);
            }
        }

        last_enforced_limit = enforced_limit;

        // publish for e.g. OCPP module with the updated limit
        if (value.limits_root_side.ac_max_current_A) {
            // update based on the PP cable rating
            const auto pp_rating = mod->bsp->read_pp_ampacity();
            if (pp_rating) {
                types::energy::NumberWithSource nws_updated{std::min(limit, static_cast<float>(pp_rating.value())),
                                                            value.limits_root_side.ac_max_current_A.value().source};
                value.limits_root_side.ac_max_current_A = std::move(nws_updated);
            }
        }
        mod->p_evse->publish_enforced_limits(value);

        // update limit at the charger
        const auto valid_until = steady_clock::now() + seconds(value.valid_for);
        if (limit >= 0) {
            // import
            mod->charger->set_max_current(limit, valid_until);
        } else {
            // export
            if (mod->session_is_iso_d20_ac_bpt()) {
                mod->charger->set_max_current(limit, valid_until);
            } else {
                // FIXME: we cannot discharge on PWM charging or with -2, so we fake a charging current here.
                mod->charger->set_max_current(0, valid_until);
            }
        }

        mod->signalNrOfPhasesAvailable(mod->ac_nr_phases_active);

        if (mod->config.charge_mode == "DC") {
            // DC mode apply limit at the leave side, we get root side limits here from EnergyManager on ACDC!
            // FIXME: multiply by conversion_efficiency here!
            if (value.limits_root_side.total_power_W.has_value() and
                value.limits_root_side.ac_max_current_A.has_value()) {
                float watt_leave_side = value.limits_root_side.total_power_W.value().value;
                float ampere_root_side = value.limits_root_side.ac_max_current_A.value().value;

                auto ev_info = mod->get_ev_info();
                float target_voltage = ev_info.target_voltage.value_or(0.);
                float actual_voltage = ev_info.present_voltage.value_or(0.);

                bool values_changed = true;
                auto powersupply_capabilities = mod->get_powersupply_capabilities();

                // did the values change since the last call?
                if (almost_eq(last_enforced_limits_watt, watt_leave_side) and
                    almost_eq(last_enforced_limits_ampere, ampere_root_side) and
                    almost_eq(target_voltage, last_target_voltage) and
                    not voltage_changed(actual_voltage, last_actual_voltage) and
                    almost_eq(powersupply_capabilities, last_powersupply_capabilities)) {
                    values_changed = false;
                }

                if (values_changed) {
                    last_enforced_limits_ampere = ampere_root_side;
                    last_enforced_limits_watt = watt_leave_side;
                    last_target_voltage = target_voltage;
                    last_actual_voltage = actual_voltage;
                    last_powersupply_capabilities = powersupply_capabilities;

                    // tell car our new limits
                    types::iso15118::DcEvseMaximumLimits evse_max_limits;
                    types::iso15118::DcEvseMinimumLimits evse_min_limits;

                    // Current Limits (min & max)
                    evse_max_limits.evse_maximum_current_limit = powersupply_capabilities.max_export_current_A;
                    evse_max_limits.evse_maximum_discharge_current_limit =
                        powersupply_capabilities.max_import_current_A;

                    evse_min_limits.evse_minimum_current_limit = powersupply_capabilities.min_export_current_A;
                    evse_min_limits.evse_minimum_discharge_current_limit =
                        powersupply_capabilities.min_import_current_A;

                    float total_current{0.0};

                    if (target_voltage > 10) {
                        // we use target_voltage here to calculate current limit.
                        // If target_voltage is a lot higher then the actual voltage the
                        // current limit is too low, i.e. charging will not reach the actual watt value.
                        // FIXME: we could use some magic here that involves actual measured voltage as well.
                        if (actual_voltage > 10) {
                            total_current = watt_leave_side / actual_voltage;
                        } else {
                            total_current = watt_leave_side / target_voltage;
                        }
                    } else {
                        total_current = powersupply_capabilities.max_export_current_A;
                    }

                    if (total_current >= 0.0) {
                        evse_max_limits.evse_maximum_current_limit =
                            std::min(total_current, powersupply_capabilities.max_export_current_A);
                    } else {
                        evse_max_limits.evse_maximum_discharge_current_limit.emplace(std::fabs(total_current));
                        if (powersupply_capabilities.max_import_current_A.has_value() and
                            std::fabs(total_current) > powersupply_capabilities.max_import_current_A.value()) {
                            evse_max_limits.evse_maximum_discharge_current_limit =
                                powersupply_capabilities.max_import_current_A;
                        }
                    }

                    // Power limits (min & max)
                    evse_max_limits.evse_maximum_power_limit = powersupply_capabilities.max_export_power_W;
                    evse_max_limits.evse_maximum_discharge_power_limit = powersupply_capabilities.max_import_power_W;

                    evse_min_limits.evse_minimum_power_limit =
                        powersupply_capabilities.min_export_voltage_V * powersupply_capabilities.min_export_current_A;
                    evse_min_limits.evse_minimum_discharge_power_limit =
                        powersupply_capabilities.min_import_voltage_V.value_or(0.0) *
                        powersupply_capabilities.min_import_current_A.value_or(0.0);

                    if (watt_leave_side >= 0) {
                        evse_max_limits.evse_maximum_power_limit =
                            std::min(watt_leave_side, powersupply_capabilities.max_export_power_W);
                    } else {
                        evse_max_limits.evse_maximum_discharge_power_limit.emplace(std::fabs(watt_leave_side));
                        if (powersupply_capabilities.max_import_power_W.has_value() and
                            std::fabs(watt_leave_side) > powersupply_capabilities.max_import_power_W.value()) {
                            evse_max_limits.evse_maximum_discharge_power_limit =
                                powersupply_capabilities.max_import_power_W;
                        }
                    }

                    // Voltage limits (min & max)
                    evse_max_limits.evse_maximum_voltage_limit = powersupply_capabilities.max_export_voltage_V;
                    evse_min_limits.evse_minimum_voltage_limit = powersupply_capabilities.min_export_voltage_V;

                    // FIXME: we tell the ISO stack positive numbers for DIN spec and ISO-2 here in case of exporting to
                    // grid. This needs to be fixed in the transition to -20 for BPT.

                    mod->is_actually_exporting_to_grid = false;
                    if (watt_leave_side < 0 and total_current < 0 and
                        evse_max_limits.evse_maximum_discharge_power_limit.has_value() and
                        evse_max_limits.evse_maximum_discharge_current_limit.has_value()) {

                        // we are exporting power back to the grid
                        if (mod->config.hack_allow_bpt_with_iso2) {
                            evse_max_limits.evse_maximum_power_limit =
                                evse_max_limits.evse_maximum_discharge_power_limit.value();
                            evse_max_limits.evse_maximum_current_limit =
                                evse_max_limits.evse_maximum_discharge_current_limit.value();
                            mod->is_actually_exporting_to_grid = true;
                        } else if (mod->sae_bidi_active) {
                            evse_max_limits.evse_maximum_power_limit =
                                -evse_max_limits.evse_maximum_discharge_power_limit.value();
                            evse_max_limits.evse_maximum_current_limit =
                                -evse_max_limits.evse_maximum_discharge_current_limit.value();
                            mod->is_actually_exporting_to_grid = true;
                        } else if (mod->session_is_iso_d20_dc_bpt()) {
                            mod->is_actually_exporting_to_grid = true;
                        } else {
                            EVLOG_error << "Bidirectional export back to grid requested, but not supported.";
                            evse_max_limits.evse_maximum_power_limit = 0.;
                            evse_max_limits.evse_maximum_current_limit = 0.;
                            evse_max_limits.evse_maximum_discharge_power_limit = 0.;
                            evse_max_limits.evse_maximum_discharge_current_limit = 0.;
                        }
                    }

                    session_log.evse(
                        true, fmt::format(
                                  "Change HLC Limits: {}W/{}A, target_voltage {}, actual_voltage {}, bpt_active {}",
                                  evse_max_limits.evse_maximum_power_limit, evse_max_limits.evse_maximum_current_limit,
                                  target_voltage, actual_voltage, mod->is_actually_exporting_to_grid));
                    mod->r_hlc[0]->call_update_dc_maximum_limits(evse_max_limits);
                    mod->r_hlc[0]->call_update_dc_minimum_limits(evse_min_limits);
                    mod->charger->inform_new_evse_max_hlc_limits(evse_max_limits);
                    mod->charger->inform_new_evse_min_hlc_limits(evse_min_limits);

                    // This is just neccessary to switch between charging and discharging
                    if (target_voltage > 0) {
                        mod->apply_new_target_voltage_current();
                    }

                    // Note: If the limits are lower then before, we could tell the DC power supply to
                    // ramp down already here instead of waiting for the car to request less power.
                    // Some cars may not like it, so we wait for the car to request less for now.
                }
            }
        }
    }
}

} // namespace energy_grid
} // namespace module
