// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "evse_managerImpl.hpp"
#include <utils/date.hpp>

#include <date/date.h>
#include <date/tz.h>
#include <utils/date.hpp>

#include <fmt/core.h>

#include "SessionLog.hpp"

namespace module {

namespace evse {

bool str_to_bool(const std::string& data) {
    if (data == "true") {
        return true;
    }
    return false;
}

void evse_managerImpl::init() {
    limits.nr_of_phases_available = 1;
    limits.max_current = 0.;

    mod->signalNrOfPhasesAvailable.connect([this](const int n) {
        if (n >= 1 && n <= 3) {
            limits.nr_of_phases_available = n;
            publish_limits(limits);
        }
    });

    // Interface to Node-RED debug UI

    mod->mqtt.subscribe(fmt::format("everest_external/nodered/{}/cmd/enable", mod->config.connector_id),
                        [&charger = mod->charger](const std::string& data) {
                            charger->enable_disable(0, {types::evse_manager::Enable_source::LocalAPI,
                                                        types::evse_manager::Enable_state::Enable, 100});
                        });

    mod->mqtt.subscribe(fmt::format("everest_external/nodered/{}/cmd/disable", mod->config.connector_id),
                        [&charger = mod->charger](const std::string& data) {
                            charger->enable_disable(0, {types::evse_manager::Enable_source::LocalAPI,
                                                        types::evse_manager::Enable_state::Disable, 100});
                        });

    mod->mqtt.subscribe(
        fmt::format("everest_external/nodered/{}/cmd/switch_three_phases_while_charging", mod->config.connector_id),
        [&charger = mod->charger](const std::string& data) {
            charger->switch_three_phases_while_charging(str_to_bool(data));
        });

    mod->mqtt.subscribe(fmt::format("everest_external/nodered/{}/cmd/pause_charging", mod->config.connector_id),
                        [&charger = mod->charger](const std::string& data) { charger->pause_charging(); });

    mod->mqtt.subscribe(fmt::format("everest_external/nodered/{}/cmd/resume_charging", mod->config.connector_id),
                        [&charger = mod->charger](const std::string& data) { charger->resume_charging(); });

    // /Interface to Node-RED debug UI

    if (mod->r_powermeter_billing().size() > 0) {
        mod->r_powermeter_billing()[0]->subscribe_powermeter([this](const types::powermeter::Powermeter& p) {
            // Republish data on proxy powermeter struct
            publish_powermeter(p);
        });
        mod->r_powermeter_billing()[0]->subscribe_public_key_ocmf([this](const std::string& public_key_ocmf) {
            // Republish data on proxy powermeter public_key_ocmf
            publish_powermeter_public_key_ocmf(public_key_ocmf);
        });
    }
}

void evse_managerImpl::ready() {

    // publish evse id at least once
    publish_evse_id(mod->config.evse_id);

    mod->r_bsp->subscribe_telemetry([this](types::evse_board_support::Telemetry telemetry) {
        // external Nodered interface
        mod->mqtt.publish(fmt::format("everest_external/nodered/{}/state/temperature", mod->config.connector_id),
                          telemetry.evse_temperature_C);
        // external Nodered interface
        publish_telemetry(telemetry);
    });

    // The module code generates the reservation events and we merely publish them here
    mod->signalReservationEvent.connect([this](types::evse_manager::SessionEvent j) {
        j.uuid.clear(); // Reservation is not part of a session
        publish_session_event(j);
    });

    mod->charger->signal_session_started_event.connect(
        [this](const types::evse_manager::StartSessionReason& start_reason,
               const std::optional<types::authorization::ProvidedIdToken>& provided_id_token) {
            types::evse_manager::SessionEvent se;
            se.event = types::evse_manager::SessionEventEnum::SessionStarted;
            this->mod->selected_protocol = "IEC61851-1";
            types::evse_manager::SessionStarted session_started;

            se.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());
            session_started.meter_value = mod->get_latest_powermeter_data_billing();

            if (mod->config.disable_authentication &&
                start_reason == types::evse_manager::StartSessionReason::EVConnected) {

                // Free service, authorize immediately in a separate thread (to avoid dead lock with charger state
                // machine, this signal handler runs in state machine context)
                std::thread authorize_thread([this]() {
                    types::authorization::ProvidedIdToken provided_token;
                    provided_token.authorization_type = types::authorization::AuthorizationType::RFID;
                    provided_token.id_token = {"FREESERVICE", types::authorization::IdTokenType::Local};
                    provided_token.prevalidated = true;
                    mod->charger->authorize(true, provided_token,
                                            {types::authorization::AuthorizationStatus::Accepted});
                    mod->charger_was_authorized();
                });
                authorize_thread.detach();
            }

            session_started.reason = start_reason;
            const auto session_uuid = this->mod->charger->get_session_id();
            session_started.meter_value = mod->get_latest_powermeter_data_billing();
            session_started.id_tag = provided_id_token;
            if (mod->is_reserved()) {
                session_started.reservation_id = mod->get_reservation_id();
                if (start_reason == types::evse_manager::StartSessionReason::Authorized) {
                    this->mod->cancel_reservation(false);
                }
            }

            const auto logging_path = session_log.startSession(
                mod->config.logfile_suffix == "session_uuid" ? session_uuid : mod->config.logfile_suffix);

            if (logging_path.has_value()) {
                session_started.logging_path = logging_path.value().string();
            }
            session_log.evse(false, fmt::format("Session Started: {}",
                                                types::evse_manager::start_session_reason_to_string(start_reason)));

            mod->telemetry.publish("session", "events",
                                   {
                                       {"timestamp", Everest::Date::to_rfc3339(date::utc_clock::now())},
                                       {"type", "session_started"},
                                       {"session_id", session_uuid},
                                       {"reason", types::evse_manager::start_session_reason_to_string(start_reason)},
                                   });

            se.session_started = session_started;
            se.uuid = session_uuid;
            publish_session_event(se);
        });

    mod->charger->signal_transaction_started_event.connect([this](
                                                               const types::authorization::ProvidedIdToken& id_token) {
        types::evse_manager::SessionEvent se;
        se.event = types::evse_manager::SessionEventEnum::TransactionStarted;
        types::evse_manager::TransactionStarted transaction_started;
        se.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());

        transaction_started.meter_value = mod->get_latest_powermeter_data_billing();
        if (mod->is_reserved()) {
            transaction_started.reservation_id.emplace(mod->get_reservation_id());
            mod->cancel_reservation(false); // this allows OCPP1.6 to not move back to available.
        }

        transaction_started.id_tag = id_token;

        double energy_import = transaction_started.meter_value.energy_Wh_import.total;

        session_log.evse(false, fmt::format("Transaction Started ({} kWh)", energy_import / 1000.));
        const auto session_uuid = this->mod->charger->get_session_id();

        Everest::TelemetryMap telemetry_data = {
            {"timestamp", Everest::Date::to_rfc3339(date::utc_clock::now())},
            {"type", "transaction_started"},
            {"session_id", session_uuid},
            {"energy_counter_import_wh", transaction_started.meter_value.energy_Wh_import.total},
            {"id_tag", transaction_started.id_tag.id_token.value}};

        if (transaction_started.meter_value.energy_Wh_export.has_value()) {
            telemetry_data["energy_counter_export_wh"] = transaction_started.meter_value.energy_Wh_export.value().total;
        }
        mod->telemetry.publish("session", "events", telemetry_data);

        se.transaction_started.emplace(transaction_started);
        se.uuid = session_uuid;
        publish_session_event(se);
    });

    mod->charger->signal_transaction_finished_event.connect(
        [this](const types::evse_manager::StopTransactionReason& finished_reason,
               std::optional<types::authorization::ProvidedIdToken> finish_token) {
            types::evse_manager::SessionEvent se;

            se.event = types::evse_manager::SessionEventEnum::TransactionFinished;
            this->mod->selected_protocol = "Unknown";
            types::evse_manager::TransactionFinished transaction_finished;

            se.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());

            transaction_finished.meter_value = mod->get_latest_powermeter_data_billing();

            transaction_finished.reason.emplace(finished_reason);
            transaction_finished.id_tag = finish_token;

            double energy_import = transaction_finished.meter_value.energy_Wh_import.total;

            session_log.evse(false, fmt::format("Transaction Finished: {} ({} kWh)",
                                                types::evse_manager::stop_transaction_reason_to_string(finished_reason),
                                                energy_import / 1000.));
            const auto session_uuid = this->mod->charger->get_session_id();
            Everest::TelemetryMap telemetry_data = {
                {"timestamp", Everest::Date::to_rfc3339(date::utc_clock::now())},
                {"type", "transaction_finished"},
                {"session_id", session_uuid},
                {"energy_counter_import_wh", energy_import},
                {"reason", types::evse_manager::stop_transaction_reason_to_string(finished_reason)}};

            if (transaction_finished.meter_value.energy_Wh_export.has_value()) {
                telemetry_data["energy_counter_export_wh"] =
                    transaction_finished.meter_value.energy_Wh_export.value().total;
            }

            transaction_finished.start_signed_meter_value = mod->charger->get_start_signed_meter_value();
            transaction_finished.signed_meter_value = mod->charger->get_stop_signed_meter_value();
            mod->telemetry.publish("session", "events", telemetry_data);

            se.transaction_finished.emplace(transaction_finished);
            se.uuid = session_uuid;

            publish_session_event(se);
        });

    mod->charger->signal_charging_paused_evse_event.connect(
        [this](types::evse_manager::ChargingPausedEVSEReasons reason) {
            types::evse_manager::SessionEvent se;

            se.event = types::evse_manager::SessionEventEnum::ChargingPausedEVSE;
            se.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());

            const auto session_uuid = this->mod->charger->get_session_id();

            types::evse_manager::ChargingStateChangedEvent charging_state_changed_event;
            charging_state_changed_event.meter_value = mod->get_latest_powermeter_data_billing();
            se.charging_state_changed_event = charging_state_changed_event;

            se.uuid = session_uuid;

            se.charging_paused_evse = reason;

            publish_session_event(se);
        });

    mod->charger->signal_simple_event.connect([this](const types::evse_manager::SessionEventEnum& e) {
        types::evse_manager::SessionEvent se;

        se.event = e;
        se.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());

        const auto session_uuid = this->mod->charger->get_session_id();
        if (e == types::evse_manager::SessionEventEnum::SessionFinished) {
            types::evse_manager::SessionFinished session_finished;
            session_finished.meter_value = mod->get_latest_powermeter_data_billing();
            se.session_finished = session_finished;
            session_log.evse(false, fmt::format("Session Finished"));
            session_log.stopSession();
            mod->telemetry.publish("session", "events",
                                   {{"timestamp", Everest::Date::to_rfc3339(date::utc_clock::now())},
                                    {"type", "session_finished"},
                                    {"session_id", session_uuid}});
        } else if (e == types::evse_manager::SessionEventEnum::Enabled or
                   e == types::evse_manager::SessionEventEnum::Disabled) {
            if (connector_status_changed) {
                se.connector_id = 1;
            }

            // Add source information (Who initiated this state change)
            se.source = mod->charger->get_last_enable_disable_source();
        } else if (e == types::evse_manager::SessionEventEnum::ChargingPausedEV or
                   e == types::evse_manager::SessionEventEnum::ChargingPausedEVSE or
                   e == types::evse_manager::SessionEventEnum::ChargingStarted) {
            types::evse_manager::ChargingStateChangedEvent charging_state_changed_event;
            charging_state_changed_event.meter_value = mod->get_latest_powermeter_data_billing();
            se.charging_state_changed_event = charging_state_changed_event;
        } else if (e == types::evse_manager::SessionEventEnum::Authorized or
                   e == types::evse_manager::SessionEventEnum::Deauthorized) {
            types::evse_manager::AuthorizationEvent authorization_event;
            authorization_event.meter_value = mod->get_latest_powermeter_data_billing();
            se.authorization_event = authorization_event;
        }

        se.uuid = session_uuid;

        publish_session_event(se);

        if (e == types::evse_manager::SessionEventEnum::SessionFinished) {
            this->mod->selected_protocol = "Unknown";
        }

        // Cancel reservations if charger is disabled
        if (mod->is_reserved() and e == types::evse_manager::SessionEventEnum::Disabled) {
            mod->cancel_reservation(true);
        }

        publish_selected_protocol(this->mod->selected_protocol);
    });

    mod->charger->signal_session_resumed_event.connect([this](const std::string& session_id) {
        types::evse_manager::SessionEvent session_event;
        session_event.uuid = session_id;
        session_event.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());
        session_event.event = types::evse_manager::SessionEventEnum::SessionResumed;
        publish_session_event(session_event);
    });

    // Note: Deprecated. Only kept for Node red compatibility, will be removed in the future
    // Legacy external mqtt pubs
    mod->charger->signal_max_current.connect([this](float c) {
        mod->mqtt.publish(fmt::format("everest_external/nodered/{}/state/max_current", mod->config.connector_id), c);

        limits.uuid = mod->info.id;
        limits.max_current = c;
        publish_limits(limits);
    });

    mod->charger->signal_state.connect([this](Charger::EvseState s) {
        mod->mqtt.publish(fmt::format("everest_external/nodered/{}/state/state_string", mod->config.connector_id),
                          mod->charger->evse_state_to_string(s));
        mod->mqtt.publish(fmt::format("everest_external/nodered/{}/state/state", mod->config.connector_id),
                          static_cast<int>(s));
    });
}

types::evse_manager::Evse evse_managerImpl::handle_get_evse() {
    types::evse_manager::Evse evse;
    evse.id = this->mod->config.connector_id;

    std::vector<types::evse_manager::Connector> connectors;
    types::evse_manager::Connector connector;
    // EvseManager currently only supports a single connector with id: 1;
    connector.id = 1;
    connector.type = mod->connector_type;

    connectors.push_back(connector);
    evse.connectors = connectors;
    return evse;
}

bool evse_managerImpl::handle_enable_disable(int& connector_id, types::evse_manager::EnableDisableSource& cmd_source) {
    connector_status_changed = connector_id != 0;
    return mod->charger->enable_disable(connector_id, cmd_source);
};

void evse_managerImpl::handle_authorize_response(types::authorization::ProvidedIdToken& provided_token,
                                                 types::authorization::ValidationResult& validation_result) {
    const auto pnc = provided_token.authorization_type == types::authorization::AuthorizationType::PlugAndCharge;

    if (validation_result.authorization_status == types::authorization::AuthorizationStatus::Accepted) {

        if (this->mod->get_hlc_waiting_for_auth_pnc() && !pnc) {
            EVLOG_info
                << "EvseManager received Authorization other than PnC while waiting for PnC. This has no effect.";
            return;
        }

        this->mod->charger->authorize(true, provided_token, validation_result);
        mod->charger_was_authorized();
        if (validation_result.reservation_id.has_value()) {
            EVLOG_debug << "Reserve evse manager reservation id for id " << validation_result.reservation_id.value();
            // The validation result returns a reservation id. If this was a reservation for a specific evse, the
            // evse manager probably already stored the reservation id (and this call is not really necessary). But if
            // the reservation was not for a specific evse, the evse manager still has to send the reservation id in the
            // transaction event request. So that is why we call 'reserve' here, so the evse manager knows the
            // reservation id that belongs to this specific session and can send it accordingly.
            // As this is not a new reservation but an existing one, we don't signal a reservation event for this.
            mod->reserve(validation_result.reservation_id.value(), false);
        }
    } else if (pnc) {
        // we only send authorization responses to the HLC for PnC rejections. In case of EIM we could
        // still receive a successfull authorization later and therefore we don't inform the HLC
        this->mod->r_hlc[0]->call_authorization_response(
            validation_result.authorization_status,
            validation_result.certificate_status.value_or(types::authorization::CertificateStatus::Accepted));
    }
};

void evse_managerImpl::handle_withdraw_authorization() {
    // reservation_id might have been stored when reserved id token has been authorized, but timed out, so
    //  we can consider the reservation as consumed
    if (mod->charger->get_authorized_eim() and mod->is_reserved()) {
        mod->cancel_reservation(true);
    }
    this->mod->charger->deauthorize();
};

bool evse_managerImpl::handle_reserve(int& reservation_id) {
    return mod->reserve(reservation_id, true);
};

void evse_managerImpl::handle_cancel_reservation() {
    mod->cancel_reservation(true);
};

bool evse_managerImpl::handle_pause_charging() {
    return mod->charger->pause_charging();
};

bool evse_managerImpl::handle_resume_charging() {
    return mod->charger->resume_charging();
};

bool evse_managerImpl::handle_stop_transaction(types::evse_manager::StopTransactionRequest& request) {
    return mod->charger->cancel_transaction(request);
};

bool evse_managerImpl::handle_external_ready_to_start_charging() {
    if (mod->config.external_ready_to_start_charging) {
        EVLOG_info << "Received external ready to start charging command.";
        mod->ready_to_start_charging();
        return true;
    } else {
        EVLOG_warning
            << "Ignoring external ready to start charging command, this could be a configuration issue. Please check "
               "if 'external_ready_to_start_charging' is set to true if you want to use this feature.";
    }

    return false;
}

bool evse_managerImpl::handle_force_unlock(int& connector_id) {
    if (not mod->r_connector_lock.empty()) {
        types::evse_manager::StopTransactionRequest request;
        request.reason = types::evse_manager::StopTransactionReason::UnlockCommand;
        mod->charger->cancel_transaction(request);
        mod->bsp->connector_force_unlock();
        return true;
    }
    return false;
};

void evse_managerImpl::handle_set_plug_and_charge_configuration(
    types::evse_manager::PlugAndChargeConfiguration& plug_and_charge_configuration) {
    if (plug_and_charge_configuration.pnc_enabled.has_value()) {
        mod->set_pnc_enabled(plug_and_charge_configuration.pnc_enabled.value());
    }
    if (plug_and_charge_configuration.central_contract_validation_allowed.has_value()) {
        mod->set_central_contract_validation_allowed(
            plug_and_charge_configuration.central_contract_validation_allowed.value());
    }
    if (plug_and_charge_configuration.contract_certificate_installation_enabled.has_value()) {
        mod->set_contract_certificate_installation_enabled(
            plug_and_charge_configuration.contract_certificate_installation_enabled.value());
    }
}

types::evse_manager::UpdateAllowedEnergyTransferModesResult
evse_managerImpl::handle_update_allowed_energy_transfer_modes(
    std::vector<types::iso15118::EnergyTransferMode>& allowed_energy_transfer_modes) {
    std::vector<types::iso15118::EnergyTransferMode> filtered_energy_transfer_modes;

    if (mod->r_hlc.empty() or !mod->r_hlc[0]) {
        return types::evse_manager::UpdateAllowedEnergyTransferModesResult::NoHlc;
    }

    filtered_energy_transfer_modes.reserve(allowed_energy_transfer_modes.size());

    // TODO(mlitre): Add check for incompatible type(s), for now we just transform DC stuff
    // in case of MCS and only if a connector type was configured at all;
    // also TODO: for DC we can check whether BPT can be supported in case DC supply supports it
    std::transform(allowed_energy_transfer_modes.begin(), allowed_energy_transfer_modes.end(),
                   filtered_energy_transfer_modes.begin(), [&](types::iso15118::EnergyTransferMode m) {
                       // for MCS we have to replace DC types with MCS types
                       if (mod->connector_type.has_value() and
                           mod->connector_type == types::evse_manager::ConnectorTypeEnum::cMCS) {

                           if (m == types::iso15118::EnergyTransferMode::DC) {
                               return types::iso15118::EnergyTransferMode::MCS;
                           }
                           if (m == types::iso15118::EnergyTransferMode::DC_BPT) {
                               return types::iso15118::EnergyTransferMode::MCS_BPT;
                           }
                       }

                       // everything else pass untouched
                       return m;
                   });

    // check whether at least one mode has survived our filtering
    if (!filtered_energy_transfer_modes.size()) {
        return types::evse_manager::UpdateAllowedEnergyTransferModesResult::IncompatibleEnergyTransfer;
    }

    mod->r_hlc[0]->call_update_energy_transfer_modes(filtered_energy_transfer_modes);
    return types::evse_manager::UpdateAllowedEnergyTransferModesResult::Accepted;
}

} // namespace evse
} // namespace module
