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

#include "RpcApi.hpp"
#include "helpers/Conversions.hpp"
#include "helpers/ErrorHandler.hpp"

#include <cmath>

namespace module {

void RpcApi::init() {
    for (const auto& evse_manager : r_evse_manager) {
        // create one DataStore object per EVSE
        auto& evse_data = this->data.evses.emplace_back(std::make_unique<data::DataStoreEvse>());
        // subscribe to evse_manager interface variables
        this->subscribe_evse_manager(evse_manager, *evse_data);
    }

    if (r_evse_energy_sink.empty()) {
        EVLOG_warning << "No EVSE energy sinks configured. Configuration of EVSE external limits will not be possible.";
    }

    // Check how EVSEs are mapped
    this->check_evse_mapping();

    // Create the request handler
    m_request_handler = std::make_unique<RpcApiRequestHandler>(data, r_evse_manager, r_evse_energy_sink);

    std::vector<std::shared_ptr<server::TransportInterface>> transport_interfaces;

    if (config.websocket_enabled) {
        m_websocket_server = std::make_unique<server::WebSocketServer>(
            config.websocket_tls_enabled, config.websocket_port, config.websocket_interface);
        transport_interfaces.push_back(std::shared_ptr<server::TransportInterface>(std::move(m_websocket_server)));
    }

    if (transport_interfaces.empty()) {
        throw std::runtime_error("No other transports currently available, please enable websocket transport");
    }

    m_rpc_handler = std::make_unique<rpc::RpcHandler>(std::move(transport_interfaces), data,
                                                      std::move(m_request_handler), config.max_decimal_places_other);

    subscribe_global_errors();
}

void RpcApi::ready() {
    // get charger information (cmd not available during init())
    if (r_charger_information.size() > 0) {
        types::json_rpc_api::ChargerInfoObj charger_info;
        const auto info = r_charger_information[0]->call_get_charger_information();
        // mandatory members
        charger_info.vendor = info.vendor;
        charger_info.model = info.model;
        charger_info.serial = info.chargepoint_serial.value_or("unknown");
        charger_info.firmware_version = info.firmware_version.value_or("unknown");
        // optional members
        if (info.friendly_name.has_value()) {
            charger_info.friendly_name = info.friendly_name.value();
        }
        if (info.manufacturer.has_value()) {
            charger_info.manufacturer = info.manufacturer.value();
        }
        if (info.manufacturer_url.has_value()) {
            charger_info.manufacturer_url = info.manufacturer_url.value();
        }
        if (info.model_number.has_value()) {
            charger_info.model_no = info.model_number.value();
        }
        if (info.model_revision.has_value()) {
            charger_info.revision = info.model_revision.value();
        }
        if (info.board_revision.has_value()) {
            charger_info.board_revision = info.board_revision.value();
        }
        this->data.chargerinfo.set_data(charger_info);
    } else {
        this->data.chargerinfo.set_unknown();
    }

    // Start server instances
    m_rpc_handler->start_server();
}

void RpcApi::check_evse_session_event(data::DataStoreEvse& evse_data,
                                      const types::evse_manager::SessionEvent& session_event) {
    // store the session info in the data store
    types::json_rpc_api::EVSEStateEnum evse_state =
        types::json_rpc_api::evse_manager_session_event_to_evse_state(session_event);
    evse_data.evsestatus.set_state(evse_state);
    evse_data.sessioninfo.update_state(session_event);

    if (evse_state == types::json_rpc_api::EVSEStateEnum::Charging) {
        evse_data.evsestatus.set_charging_allowed(true);
    }

    if (session_event.source.has_value()) {
        const auto source = session_event.source.value();
        evse_data.sessioninfo.set_enable_disable_source(
            types::evse_manager::enable_source_to_string(source.enable_source),
            types::evse_manager::enable_state_to_string(source.enable_state), source.enable_priority);
        if (source.enable_state == types::evse_manager::Enable_state::Disable) {
            evse_data.evsestatus.set_available(false);
        } else if (source.enable_state == types::evse_manager::Enable_state::Enable) {
            evse_data.evsestatus.set_available(true);
        }
    }

    if (session_event.event == types::evse_manager::SessionEventEnum::TransactionStarted) {
        if (session_event.transaction_started.has_value()) {
            const auto transaction_started = session_event.transaction_started.value();
            const auto energy_Wh_import = transaction_started.meter_value.energy_Wh_import.total;
            evse_data.sessioninfo.set_start_energy_import_wh(energy_Wh_import);

            if (transaction_started.meter_value.energy_Wh_export.has_value()) {
                const auto energy_Wh_export = transaction_started.meter_value.energy_Wh_export.value().total;
                evse_data.sessioninfo.set_start_energy_export_wh(energy_Wh_export);
            } else {
                evse_data.sessioninfo.start_energy_export_wh_was_set = false;
            }
        }
    } else if (session_event.event == types::evse_manager::SessionEventEnum::TransactionFinished) {
        if (session_event.transaction_finished.has_value()) {
            const auto transaction_finished = session_event.transaction_finished.value();
            const auto energy_Wh_import = transaction_finished.meter_value.energy_Wh_import.total;
            evse_data.sessioninfo.set_end_energy_import_wh(energy_Wh_import);

            if (transaction_finished.meter_value.energy_Wh_export.has_value()) {
                const auto energy_Wh_export = transaction_finished.meter_value.energy_Wh_export.value().total;
                evse_data.sessioninfo.set_end_energy_export_wh(energy_Wh_export);
            } else {
                evse_data.sessioninfo.end_energy_export_wh_was_set = false;
            }
        }
        evse_data.evsestatus.set_charged_energy_wh(evse_data.sessioninfo.get_charged_energy_wh());
        evse_data.evsestatus.set_discharged_energy_wh(evse_data.sessioninfo.get_discharged_energy_wh());
        evse_data.evsestatus.set_charging_duration_s(evse_data.sessioninfo.get_charging_duration_s().count());
    }
}

void RpcApi::subscribe_evse_manager(const std::unique_ptr<evse_managerIntf>& evse_manager,
                                    data::DataStoreEvse& evse_data) {
    evse_manager->subscribe_powermeter([this, &evse_data](const types::powermeter::Powermeter& powermeter) {
        this->meterdata_var_to_datastore(powermeter, evse_data.meterdata);

        evse_data.sessioninfo.set_latest_energy_import_wh(powermeter.energy_Wh_import.total);
        if (powermeter.energy_Wh_export.has_value()) {
            evse_data.sessioninfo.set_latest_energy_export_wh(powermeter.energy_Wh_export.value().total);
        }
        if (powermeter.power_W.has_value()) {
            evse_data.sessioninfo.set_latest_total_w(powermeter.power_W.value().total);
        }
        // update duration and energy values in the EVSE status store
        evse_data.evsestatus.set_charging_duration_s(evse_data.sessioninfo.get_charging_duration_s().count());
        evse_data.evsestatus.set_charged_energy_wh(evse_data.sessioninfo.get_charged_energy_wh());
        evse_data.evsestatus.set_discharged_energy_wh(evse_data.sessioninfo.get_discharged_energy_wh());
    });

    evse_manager->subscribe_hw_capabilities(
        [this, &evse_data](const types::evse_board_support::HardwareCapabilities& hwcaps) {
            // there is only one connector supported currently
            this->hwcaps_var_to_datastore(hwcaps, evse_data.hardwarecapabilities);
            // also update evse_max_phase_count
            evse_data.evsestatus.set_ac_charge_param_evse_max_phase_count(hwcaps.max_phase_count_import);
        });

    evse_manager->subscribe_evse_id([&evse_data](const std::string& evse_id) {
        // set the EVSE id in the data store
        evse_data.evseinfo.set_id(evse_id);
    });

    evse_manager->subscribe_session_event([this, &evse_data](types::evse_manager::SessionEvent session_event) {
        this->check_evse_session_event(evse_data, session_event);
    });

    evse_manager->subscribe_selected_protocol([&evse_data](const std::string& selected_protocol) {
        const auto var_selected_protocol =
            types::json_rpc_api::evse_manager_protocol_to_charge_protocol(selected_protocol);
        evse_data.evsestatus.set_charge_protocol(var_selected_protocol);
    });

    evse_manager->subscribe_enforced_limits([&evse_data](const types::energy::EnforcedLimits& enforced_limits) {
        // set the external limits in the data store
        if (evse_data.evseinfo.get_is_ac_transfer_mode()) {
            const auto& max_current = enforced_limits.limits_root_side.ac_max_current_A;
            if (max_current.has_value()) {
                evse_data.evsestatus.set_ac_charge_param_evse_max_current(max_current.value().value);
            }

            RPCDataTypes::ACChargeStatusObj ac_charge_status;
            const auto& max_phase_count = enforced_limits.limits_root_side.ac_max_phase_count;
            ac_charge_status.evse_active_phase_count = max_phase_count.has_value() ? max_phase_count.value().value : 3;
            evse_data.evsestatus.set_ac_charge_status(ac_charge_status);
        } else {
            evse_data.evsestatus.set_ac_charge_param(std::nullopt);
        }
    });

    evse_manager->subscribe_supported_energy_transfer_modes(
        [&evse_data](const std::vector<types::iso15118::EnergyTransferMode>& supported_energy_transfer_modes) {
            // convert to rpc type
            bool is_ac_transfer_mode = false;
            const auto rpc_supported_energy_transfer_modes =
                RPCDataTypes::iso15118_energy_transfer_modes_to_json_rpc_api(supported_energy_transfer_modes,
                                                                             is_ac_transfer_mode);
            evse_data.evseinfo.set_supported_energy_transfer_modes(rpc_supported_energy_transfer_modes);
            evse_data.evseinfo.set_is_ac_transfer_mode(is_ac_transfer_mode);
        });

    evse_manager->subscribe_ev_info([&evse_data](types::evse_manager::EVInfo ev_info) {
        RPCDataTypes::DisplayParametersObj display_parameters;

        if (ev_info.soc.has_value()) {
            // for some reason, soc in types::evse_manager::EVInfo is declared as float;
            // all integers from 0 to 100 can be exactly represented in float, but let's
            // (l)round them just in case:
            display_parameters.present_soc = std::lround(ev_info.soc.value());
        }

        if (ev_info.battery_capacity.has_value()) {
            display_parameters.battery_energy_capacity = ev_info.battery_capacity;
        }

        if (ev_info.estimated_time_full.has_value()) {
            // calculate the distance of estimated_time_full to now, and if greater 0,
            // report it
            const auto time_until =
                Everest::Date::from_rfc3339(ev_info.estimated_time_full.value()) - date::utc_clock::now();
            const auto secs = std::chrono::duration_cast<std::chrono::seconds>(time_until).count();
            if (secs > 0) {
                display_parameters.remaining_time_to_maximum_soc = secs;
            }
        }
        // pass an empty optional object if no member was set
        std::optional<RPCDataTypes::DisplayParametersObj> result{};
        if (display_parameters != RPCDataTypes::DisplayParametersObj()) {
            result = display_parameters;
        }
        evse_data.evsestatus.set_display_parameters(result);
    });
}

void RpcApi::subscribe_global_errors() {
    // Subscribe to global error events
    const auto error_handler = [this](const Everest::error::Error& error) {
        const auto tmp_error = types::json_rpc_api::everest_error_to_rpc_error(error);
        helpers::handle_error_raised(this->data, tmp_error);
    };
    const auto error_cleared_handler = [this](const Everest::error::Error& error) {
        const auto tmp_error = types::json_rpc_api::everest_error_to_rpc_error(error);
        helpers::handle_error_cleared(this->data, tmp_error);
    };
    subscribe_global_all_errors(error_handler, error_cleared_handler);
}

void RpcApi::meterdata_var_to_datastore(const types::powermeter::Powermeter& powermeter,
                                        data::MeterDataStore& meter_data) {
    types::json_rpc_api::MeterDataObj meter_data_new; // default initialized
    if (const auto _data = meter_data.get_data(); _data.has_value()) {
        // initialize with existing values
        meter_data_new = _data.value();
    }

    // mandatory objects from the EVerest powermeter interface variable
    // timestamp
    meter_data_new.timestamp = powermeter.timestamp;

    // energy_Wh_import
    if (powermeter.energy_Wh_import.L1.has_value()) {
        meter_data_new.energy_Wh_import.L1 = powermeter.energy_Wh_import.L1.value();
    }
    if (powermeter.energy_Wh_import.L2.has_value()) {
        meter_data_new.energy_Wh_import.L2 = powermeter.energy_Wh_import.L2.value();
    }
    if (powermeter.energy_Wh_import.L3.has_value()) {
        meter_data_new.energy_Wh_import.L3 = powermeter.energy_Wh_import.L3.value();
    }
    meter_data_new.energy_Wh_import.total = powermeter.energy_Wh_import.total;

    // optional objects from the EVerest powermeter interface
    if (powermeter.current_A.has_value()) {
        meter_data_new.current_A.emplace();
        const auto& inobj = powermeter.current_A.value();
        if (inobj.L1.has_value()) {
            meter_data_new.current_A.value().L1 = inobj.L1.value();
        }
        if (inobj.L2.has_value()) {
            meter_data_new.current_A.value().L2 = inobj.L2.value();
        }
        if (inobj.L3.has_value()) {
            meter_data_new.current_A.value().L3 = inobj.L3.value();
        }
    }
    if (powermeter.energy_Wh_export.has_value()) {
        // a shortcut reference to the input data sub-object
        const auto& inobj = powermeter.energy_Wh_export.value();
        // a shortcut reference to the output data sub-object optional
        auto& export_opt = meter_data_new.energy_Wh_export;
        // keep original (copied) optional value, or emplace empty if non exist
        auto& newobj = export_opt.emplace(export_opt.value_or(types::json_rpc_api::Energy_Wh_export{}));
        if (inobj.L1.has_value()) {
            newobj.L1 = inobj.L1.value();
        }
        if (inobj.L2.has_value()) {
            newobj.L2 = inobj.L2.value();
        }
        if (inobj.L3.has_value()) {
            newobj.L3 = inobj.L3.value();
        }
        newobj.total = inobj.total;
    }
    if (powermeter.frequency_Hz.has_value()) {
        // a shortcut reference to the input data sub-object
        const auto& inobj = powermeter.frequency_Hz.value();
        // a shortcut reference to the output data sub-object optional
        auto& frequency_optional = meter_data_new.frequency_Hz;
        // keep original (copied) optional value, or emplace empty if non exist
        auto& newobj = frequency_optional.emplace(frequency_optional.value_or(types::json_rpc_api::Frequency_Hz{}));
        newobj.L1 = inobj.L1;
        if (inobj.L2.has_value()) {
            newobj.L2 = inobj.L2.value();
        }
        if (inobj.L3.has_value()) {
            newobj.L3 = inobj.L3.value();
        }
    }
    if (powermeter.meter_id.has_value()) {
        meter_data_new.meter_id = powermeter.meter_id.value();
    }
    // serial_number  is not yet available
    if (powermeter.phase_seq_error.has_value()) {
        meter_data_new.phase_seq_error = powermeter.phase_seq_error.value();
    }
    if (powermeter.power_W.has_value()) {
        // a shortcut reference to the input data sub-object
        const auto& inobj = powermeter.power_W.value();
        // a shortcut reference to the output data sub-object optional
        auto& export_opt = meter_data_new.power_W;
        // keep original (copied) optional value, or emplace empty if non exist
        auto& newobj = export_opt.emplace(export_opt.value_or(types::json_rpc_api::Power_W{}));
        if (inobj.L1.has_value()) {
            newobj.L1 = inobj.L1.value();
        }
        if (inobj.L2.has_value()) {
            newobj.L2 = inobj.L2.value();
        }
        if (inobj.L3.has_value()) {
            newobj.L3 = inobj.L3.value();
        }
        newobj.total = inobj.total;
    }
    if (powermeter.voltage_V.has_value()) {
        // a shortcut reference to the input data sub-object
        const auto& inobj = powermeter.voltage_V.value();
        // a shortcut reference to the output data sub-object optional
        auto& export_opt = meter_data_new.voltage_V;
        // keep original (copied) optional value, or emplace empty if non exist
        auto& newobj = export_opt.emplace(export_opt.value_or(types::json_rpc_api::Voltage_V{}));
        if (inobj.L1.has_value()) {
            newobj.L1 = inobj.L1.value();
        }
        if (inobj.L2.has_value()) {
            newobj.L2 = inobj.L2.value();
        }
        if (inobj.L3.has_value()) {
            newobj.L3 = inobj.L3.value();
        }
    }

    // submit changes
    // Note: timestamp will skew this, as it will always change, and therefore always trigger a notification for the
    // complete dataset
    meter_data.set_data(meter_data_new);
}

void RpcApi::hwcaps_var_to_datastore(const types::evse_board_support::HardwareCapabilities& hwcaps,
                                     data::HardwareCapabilitiesStore& hw_caps_data) {
    types::json_rpc_api::HardwareCapabilitiesObj hw_caps_data_new; // default initialized
    if (const auto _data = hw_caps_data.get_data(); _data.has_value()) {
        // initialize with existing values
        hw_caps_data_new = _data.value();
    }

    // mandatory objects from the EVerest hw_capabilites interface variable
    hw_caps_data_new.max_current_A_export = hwcaps.max_current_A_import;
    hw_caps_data_new.max_current_A_import = hwcaps.max_current_A_import;
    hw_caps_data_new.max_phase_count_export = hwcaps.max_phase_count_export;
    hw_caps_data_new.max_phase_count_import = hwcaps.max_phase_count_import;
    hw_caps_data_new.min_current_A_export = hwcaps.min_current_A_export;
    hw_caps_data_new.min_current_A_import = hwcaps.min_current_A_import;
    hw_caps_data_new.min_phase_count_export = hwcaps.min_phase_count_export;
    hw_caps_data_new.min_phase_count_import = hwcaps.min_phase_count_import;
    hw_caps_data_new.phase_switch_during_charging = hwcaps.supports_changing_phases_during_charging;

    // submit changes
    hw_caps_data.set_data(hw_caps_data_new);
}

bool RpcApi::check_evse_mapping() {
    // Iterate over the configured EVSE mapping and configure the data store accordingly
    if (r_evse_manager.size() != this->data.evses.size()) {
        throw std::runtime_error("The number of EVSE managers does not match the number of EVSE data stores.");
    }
    // As long as the EvseManager only supports one statically configured connector, we extract the
    // connector id from the mapping. Only the connector type is retrieved from the EvseManager.
    // Iterate over all over the mapping of the EVSE's and configure the data store accordingly
    for (std::size_t idx = 0; idx < r_evse_manager.size(); idx++) {
        const auto& evse_manager = r_evse_manager[idx];
        const auto& evse_data = this->data.evses[idx];
        // Initialize connector index for the case of no mapping information
        types::json_rpc_api::ConnectorInfoObj connector;
        connector.index = 1;                                              // default connector id
        connector.type = types::json_rpc_api::ConnectorTypeEnum::Unknown; // default type
        evse_data->evseinfo.set_available_connector(connector);
        evse_data->evsestatus.set_active_connector_index(connector.index); // TODO: support multiple connectors
        // create one DataStore object per EVSE sink
        if (const auto _mapping = evse_manager->get_mapping(); _mapping.has_value()) {
            // Write EVSE index and connector index to the datastore
            evse_data->evseinfo.set_index(_mapping.value().evse);
            if (_mapping.value().connector.has_value()) {
                // Initialize connector index
                connector.index = _mapping.value().connector.value();
                types::evse_manager::Evse evse = evse_manager->call_get_evse();
                if (!evse.connectors.empty() && evse.connectors[0].type.has_value()) {
                    try {
                        connector.type = types::json_rpc_api::string_to_connector_type_enum(
                            types::evse_manager::connector_type_enum_to_string(
                                evse.connectors[0].type.value())); // use the first connector type
                    } catch (const std::out_of_range& e) {
                        EVLOG_debug << "Unknown connector type for connector index " << connector.index;
                    }
                } else {
                    EVLOG_debug << "No connector type determined for connector index " << connector.index;
                }
                evse_data->evseinfo.set_available_connector(connector);
                evse_data->evsestatus.set_active_connector_index(connector.index); // TODO: support multiple connectors
            } else {
                EVLOG_debug << "No connector index configured in the EVSE mapping, using default connector index "
                            << connector.index;
            }
        } else {
            // no mappings, setting limits et.al. will not work
            // begin with index 1, as 0 is reserved for the complete charger
            const auto evse_index = idx + 1;
            EVLOG_warning << "No mapping found for EVSE manager with module_id \"" << evse_manager->module_id
                          << "\". No control of limits is possible for this EVSE.";
            EVLOG_warning << "Assigning index " << evse_index << " and connector index " << connector.index;
            evse_data->evseinfo.set_index(evse_index);
        }
    }
    return true;
}
} // namespace module
