#include "everestrs/src/everestrs_sys.hpp"
#include "everestrs/src/lib.rs.h"

#include <everest/logging.hpp>
#include <utils/conversions.hpp>
#include <utils/error/error_manager_impl.hpp>
#include <utils/error/error_manager_req.hpp>
#include <utils/exceptions.hpp>
#include <utils/types.hpp>

#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <mutex>
#include <stdexcept>
#include <type_traits>
#include <variant>

namespace {

JsonBlob json2blob(const json& j) {
    // I did not find a way to not copy the data at least once here.
    const std::string dumped = j.dump();
    rust::Vec<uint8_t> vec;
    vec.reserve(dumped.size());
    std::copy(dumped.begin(), dumped.end(), std::back_inserter(vec));
    return JsonBlob{vec};
}

// Below are overloads to be used with std::visit and our std::variant. We force
// a compilation error if someone changes the underlying std::variant without
// extending/adjusting the functions below.

template <typename T, typename... VARIANT_T> struct VariantMemberImpl : public std::false_type {};

template <typename T, typename... VARIANT_T>
struct VariantMemberImpl<T, std::variant<VARIANT_T...>> : public std::disjunction<std::is_same<T, VARIANT_T>...> {};

/// @brief Static checker if the type T can be converted to `everest::config::ConfigEntry`.
///
/// We use this to detect `get_config_field` overloads which receive arguments
/// which aren't part of our `everest::config::ConfigEntry` variant.
template <typename T> struct ConfigEntryMember : public VariantMemberImpl<T, everest::config::ConfigEntry> {};

inline ConfigField get_config_field(const std::string& _name, bool _value) {
    static_assert(ConfigEntryMember<decltype(_value)>::value);
    return {_name, ConfigType::Boolean, _value, {}, 0, 0};
}

inline ConfigField get_config_field(const std::string& _name, const std::string& _value) {
    static_assert(ConfigEntryMember<std::remove_cv_t<std::remove_reference_t<decltype(_value)>>>::value);
    return {_name, ConfigType::String, false, _value, 0, 0};
}

inline ConfigField get_config_field(const std::string& _name, double _value) {
    static_assert(ConfigEntryMember<decltype(_value)>::value);
    return {_name, ConfigType::Number, false, {}, _value, 0};
}

inline ConfigField get_config_field(const std::string& _name, int _value) {
    static_assert(ConfigEntryMember<decltype(_value)>::value);
    return {_name, ConfigType::Integer, false, {}, 0, _value};
}

} // namespace

std::unique_ptr<Module> create_module(rust::Str module_id, rust::Str prefix, rust::Str mqtt_broker_socket_path,
                                      rust::Str mqtt_broker_host, const std::uint16_t& mqtt_broker_port,
                                      rust::Str mqtt_everest_prefix, rust::Str mqtt_external_prefix) {
    auto socket_path = std::string(mqtt_broker_socket_path);
    Everest::MQTTSettings mqtt_settings;
    if (not socket_path.empty()) {
        Everest::populate_mqtt_settings(mqtt_settings, socket_path, std::string(mqtt_everest_prefix),
                                        std::string(mqtt_external_prefix));
    } else {
        Everest::populate_mqtt_settings(mqtt_settings, std::string(mqtt_broker_host), mqtt_broker_port,
                                        std::string(mqtt_everest_prefix), std::string(mqtt_external_prefix));
    }
    return std::make_unique<Module>(std::string(module_id), std::string(prefix), mqtt_settings);
}

Module::Module(const std::string& module_id, const std::string& prefix, const Everest::MQTTSettings& mqtt_settings) :
    module_id_(module_id) {

    const auto mqtt_abstraction =
        std::shared_ptr<Everest::MQTTAbstraction>(Everest::make_mqtt_abstraction(mqtt_settings));
    // TODO(ddo) what happens when this returns false?
    mqtt_abstraction->connect();
    mqtt_abstraction->spawn_main_loop_thread();

    const auto result = Everest::get_module_config(mqtt_abstraction, this->module_id_);

    this->rs_ = std::make_unique<Everest::RuntimeSettings>(result.at("settings"));

    config_ = std::make_shared<Everest::Config>(mqtt_settings, result);

    handle_ = std::make_unique<Everest::Everest>(this->module_id_, *this->config_, this->rs_->validate_schema,
                                                 mqtt_abstraction, this->rs_->telemetry_prefix,
                                                 this->rs_->telemetry_enabled, this->rs_->forward_exceptions);

    // Not needed but done to be congruent with the other bindings.
    handle_->spawn_main_loop_thread();
}

Module::~Module() {
    // MQTTAbstractionImpl spawns a main loop thread whose only exit condition
    // is `disconnect_event` being notified. Without this call the `Thread`
    // member destructor joins a thread that will never exit.
    if (handle_) {
        handle_->disconnect();
    }
}

JsonBlob Module::get_interface(rust::Str interface_name) const {
    const auto& interface_def = config_->get_interface_definition(std::string(interface_name));
    return json2blob(interface_def);
}

JsonBlob Module::get_manifest() const {
    const std::string& module_name = config_->get_module_name(std::string(module_id_));
    return json2blob(config_->get_manifests().at(module_name));
}

void Module::signal_ready(const Runtime& rt) const {
    handle_->register_on_ready_handler([&rt]() { rt.on_ready(); });
    handle_->signal_ready();
}

void Module::provide_command(const Runtime& rt, rust::String implementation_id, rust::String name) const {
    using namespace Everest;
    handle_->provide_cmd(std::string(implementation_id), std::string(name), [&rt, implementation_id, name](json args) {
        JsonBlob blob = rt.handle_command(implementation_id, name, json2blob(args));
        const auto retval = json::parse(blob.data.begin(), blob.data.end());

        // Check if our command handler failed.
        if (retval.contains(conversions::ERROR_TYPE)) {
            const auto error_str = retval.at(conversions::ERROR_TYPE).get<std::string>();
            const auto error_msg = retval.at(conversions::ERROR_MSG).get<std::string>();
            const auto error_enm = conversions::string_to_cmd_error_type(error_str);
            switch (error_enm) {
            case CmdErrorType::MessageParsingError:
                throw MessageParsingError(error_msg);
            case CmdErrorType::SchemaValidationError:
                throw SchemaValidationError(error_msg);
            case CmdErrorType::HandlerException:
                throw HandlerException(error_msg);
            case CmdErrorType::CmdTimeout:
                throw CmdTimeout(error_msg);
            case CmdErrorType::Shutdown:
                throw Shutdown(error_msg);
            case CmdErrorType::NotReady:
                throw NotReady(error_msg);
            }
        }
        return retval;
    });
}

void Module::subscribe_variable(const Runtime& rt, rust::String implementation_id, std::size_t index,
                                rust::String name) const {
    const auto req = Requirement{std::string(implementation_id), index};
    // The handle_ptr is guaranteed to be alive in the callback.
    const auto handle_ptr = handle_.get();
    handle_->subscribe_var(req, std::string(name), [&rt, implementation_id, index, name, handle_ptr](json args) {
        handle_ptr->ensure_ready();
        rt.handle_variable(implementation_id, index, name, json2blob(args));
    });
}

void Module::subscribe_all_errors(const Runtime& rt) const {
    for (const Requirement& req : config_->get_requirements(module_id_)) {
        std::shared_ptr<Everest::error::ErrorManagerReq> error_manager_ptr = nullptr;
        try {
            error_manager_ptr = handle_->get_error_manager_req(req);
        } catch (const std::runtime_error& ex) {
            // This is expected if the manifest has `ignore.errors=true`
            // configured.
            continue;
        }
        const auto handle_ptr = handle_.get();
        error_manager_ptr->subscribe_all_errors(
            [&rt, req, handle_ptr](Everest::error::Error error) {
                handle_ptr->ensure_ready();
                const ErrorType rust_error{rust::String(error.type), rust::String(error.description),
                                           rust::String(error.message), static_cast<ErrorSeverity>(error.severity)};
                rt.handle_on_error(rust::Str(req.id), req.index, rust_error, true);
            },
            [&rt, req, handle_ptr](Everest::error::Error error) {
                handle_ptr->ensure_ready();
                const ErrorType rust_error{rust::String(error.type), rust::String(error.description),
                                           rust::String(error.message), static_cast<ErrorSeverity>(error.severity)};
                rt.handle_on_error(rust::Str(req.id), req.index, rust_error, false);
            });
    }
}

JsonBlob Module::call_command(rust::Str implementation_id, std::size_t index, rust::Str name, JsonBlob blob) const {
    using namespace Everest;
    const auto req = Requirement{std::string(implementation_id), index};
    json retval;
    try {
        retval = handle_->call_cmd(req, std::string(name), json::parse(blob.data.begin(), blob.data.end()));
    } catch (const MessageParsingError& ex) {
        to_json(retval, CmdResultError{CmdErrorType::MessageParsingError, ex.what(), nullptr});
    } catch (const SchemaValidationError& ex) {
        to_json(retval, CmdResultError{CmdErrorType::SchemaValidationError, ex.what(), nullptr});
    } catch (const HandlerException& ex) {
        to_json(retval, CmdResultError{CmdErrorType::HandlerException, ex.what(), nullptr});
    } catch (const CmdTimeout& ex) {
        to_json(retval, CmdResultError{CmdErrorType::CmdTimeout, ex.what(), nullptr});
    } catch (const Shutdown& ex) {
        to_json(retval, CmdResultError{CmdErrorType::Shutdown, ex.what(), nullptr});
    } catch (const NotReady& ex) {
        to_json(retval, CmdResultError{CmdErrorType::NotReady, ex.what(), nullptr});
    }
    return json2blob(retval);
}

void Module::publish_variable(rust::Str implementation_id, rust::Str name, JsonBlob blob) const {
    handle_->publish_var(std::string(implementation_id), std::string(name),
                         json::parse(blob.data.begin(), blob.data.end()));
}

void Module::raise_error(rust::Str implementation_id, ErrorType error_type) const {
    // Here everything is called implementation_id: We have the Rust string type
    // then we have its c++ counterpart as std::string. And then we have the
    // ImplementationIdentifier type :S
    const std::string impl_id = std::string(implementation_id);
    const auto full_mapping = handle_->get_3_tier_model_mapping();
    std::optional<Mapping> mapping;
    if (full_mapping.has_value()) {
        const auto& inner = *full_mapping;
        mapping = inner.module;
        // We might have multiple mappings. In this case we pick the
        // implementation mapping is since this is more specific.
        const auto impl_mapping_iter = inner.implementations.find(impl_id);
        if (impl_mapping_iter != inner.implementations.end()) {
            mapping = impl_mapping_iter->second;
        }
    }
    const ImplementationIdentifier id{module_id_, impl_id, mapping};
    const Everest::error::Error error{std::string(error_type.error_type),
                                      std::string{},
                                      std::string(error_type.message),
                                      std::string(error_type.description),
                                      id,
                                      static_cast<Everest::error::Severity>(error_type.severity)};
    handle_->get_error_manager_impl(std::string(implementation_id))->raise_error(error);
}

void Module::clear_error(rust::Str implementation_id, rust::Str error_type, bool clear_all) const {
    const auto manager = handle_->get_error_manager_impl(std::string(implementation_id));

    if (error_type.empty()) {
        manager->clear_all_errors();
    } else {
        if (clear_all) {
            manager->clear_all_errors(std::string(error_type));
        } else {
            manager->clear_error(std::string(error_type));
        }
    }
}

rust::Vec<RsModuleConnections> Module::get_module_connections() const {
    const auto connections = config_->get_module_config().connections;

    // Iterate over the connections block.
    rust::Vec<RsModuleConnections> out;
    out.reserve(connections.size());
    for (const auto& [req_id, fulfillment] : connections) {
        out.emplace_back(RsModuleConnections{rust::String{req_id}, fulfillment.size()});
    };
    return out;
}

rust::Vec<RsModuleConfig> Module::get_module_configs(rust::Str module_id) const {
    // TODO(ddo) We call this before initializing the logger.
    const auto module_configs = config_->get_module_configs(std::string(module_id));

    rust::Vec<RsModuleConfig> out;
    out.reserve(module_configs.size());

    // Iterate over all modules stored in the module_config.
    for (const auto& mm : module_configs) {
        RsModuleConfig mm_out{mm.first, {}};
        mm_out.data.reserve(mm.second.size());

        // Iterate over all configs stored in the mm (our current module).
        for (const auto& cc : mm.second) {
            mm_out.data.emplace_back(
                std::visit([&](auto&& _value) { return ::get_config_field(cc.first, _value); }, cc.second));
        }
        out.emplace_back(std::move(mm_out));
    }

    return out;
}

int init_logging(rust::Str module_id, rust::Str prefix, rust::Str logging_config_file) {
    const std::string module_id_cpp{module_id};
    const std::string logging_config_file_cpp{logging_config_file};

    // // Init the CPP logger.
    return Everest::Logging::init(logging_config_file_cpp, module_id_cpp);
}

void log2cxx(int level, int line, rust::Str file, rust::Str message) {
    Everest::Logging::ffi_log(level, line, std::string(file), std::string(message));
}
