// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef UTILS_CONFIG_HPP
#define UTILS_CONFIG_HPP

#include <filesystem>
#include <list>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <tuple>
#include <unordered_map>

#include <nlohmann/json-schema.hpp>

#include <utils/config/mqtt_settings.hpp>
#include <utils/config/settings.hpp>
#include <utils/config/storage_userconfig.hpp>
#include <utils/config_cache.hpp>

#include <utils/error.hpp>
#include <utils/error/error_type_map.hpp>
#include <utils/module_config.hpp>
#include <utils/types.hpp>

namespace Everest {
namespace fs = std::filesystem;

using everest::config::ModuleConfig;
using everest::config::ModuleConfigurations;

struct ManagerSettings;
struct RuntimeSettings;

///
/// \brief A structure that contains all available schemas
///
struct Schemas {
    nlohmann::json config;                 ///< The config schema
    nlohmann::json manifest;               ///< The manifest scheme
    nlohmann::json interface;              ///< The interface schema
    nlohmann::json type;                   ///< The type schema
    nlohmann::json error_declaration_list; ///< The error-declaration-list schema
};

struct Validators {
    nlohmann::json_schema::json_validator config;
    nlohmann::json_schema::json_validator manifest;
    nlohmann::json_schema::json_validator type;
    nlohmann::json_schema::json_validator interface;
    nlohmann::json_schema::json_validator error_declaration_list;
};

struct SchemaValidation {
    Schemas schemas;
    Validators validators;
};

struct ImplementationInfo {
    std::string module_id;
    std::string module_name;
    std::string impl_id;
    std::string impl_intf;
};

///
/// \brief A simple json schema loader that uses the builtin draft7 schema of
/// the json schema validator when it encounters it, throws an exception
/// otherwise
void loader(const nlohmann::json_uri& uri, nlohmann::json& schema);

///
/// \brief An extension to the default format checker of the json schema
/// validator supporting uris
void format_checker(const std::string& format, const std::string& value);

///
/// \brief loads and validates a json schema at the provided \p path
///
/// \returns the loaded json schema as a json object as well as a related schema validator
std::tuple<nlohmann::json, nlohmann::json_schema::json_validator> load_schema(const fs::path& path);

///
/// \brief loads the config.json and manifest.json in the schemes subfolder of
/// the provided \p schemas_dir
///
/// \returns the loaded configs and related validators
SchemaValidation load_schemas(const fs::path& schemas_dir);

/// \brief Serializes the given \p module_configuration and related data to JSON.
///
/// Includes the module's config, mappings (own and connected), and telemetry if present.
///
/// \param module_id ID of the module to serialize.
/// \param module_configurations Map of all module configurations.
/// \return JSON object with the serialized module configuration.
json get_serialized_module_config(std::string_view module_id, const ModuleConfigurations& module_configurations);

///
/// \brief Base class for configs
///
class ConfigBase {
protected:
    ModuleConfigurations module_configs;
    nlohmann::json settings;

    nlohmann::json manifests;
    nlohmann::json interfaces;
    nlohmann::json interface_definitions;
    nlohmann::json types;
    Schemas schemas;
    // experimental caches
    std::map<std::string, std::string, std::less<>> module_names;

    error::ErrorTypeMap error_map;

    const MQTTSettings mqtt_settings;

public:
    ///
    /// \brief Create a ConfigBase with the provided \p mqtt_settings
    explicit ConfigBase(const MQTTSettings& mqtt_settings) : mqtt_settings(mqtt_settings){};

    ///
    /// \brief turns then given \p module_id into a printable identifier
    ///
    /// \returns a string with the printable identifier
    std::string printable_identifier(std::string_view module_id) const;

    ///
    /// \brief turns then given \p module_id and \p impl_id into a printable identifier
    ///
    /// \returns a string with the printable identifier
    std::string printable_identifier(std::string_view module_id, std::string_view impl_id) const;

    ///
    /// \returns the module name matching the provided \p module_id
    std::string get_module_name(std::string_view module_id) const;

    ///
    /// \brief turns the given \p module_id and \p impl_id into a mqtt prefix
    ///
    std::string mqtt_prefix(std::string_view module_id, std::string_view impl_id);

    ///
    /// \brief turns the given \p module_id into a mqtt prefix
    ///
    std::string mqtt_module_prefix(std::string_view module_id) const;

    ///
    /// \returns a json object that contains the main config
    const ModuleConfigurations& get_module_configurations() const;

    ///
    /// \brief checks if the config contains the given \p module_id
    bool contains(std::string_view module_id) const;

    ///
    /// \returns a json object that contains the manifests
    const nlohmann::json& get_manifests() const;

    ///
    /// \returns a json object that contains the interface definitions
    const nlohmann::json& get_interface_definitions() const;

    ///
    /// \returns a json object that contains the available interfaces
    const nlohmann::json& get_interfaces() const;

    ///
    /// \returns a json object that contains the settings
    const nlohmann::json& get_settings() const;

    ///
    /// \returns an object that contains the schemas
    const Schemas& get_schemas() const;

    ///
    /// \returns an object that contains the error types
    const error::ErrorTypes& get_error_types() const;

    ///
    /// \returns a json object that contains the types
    const nlohmann::json& get_types() const;

    ///
    /// \return the cached mapping of module ids to module names
    const std::map<std::string, std::string, std::less<>>& get_module_names() const;

    ///
    /// \brief checks if the given \p module_id provides the requirement given in \p requirement_id
    ///
    /// \returns a json object that contains the requirement
    std::vector<Fulfillment> resolve_requirement(std::string_view module_id, std::string_view requirement_id) const;

    ///
    /// \brief resolves all Requirements of the given \p module_id to their Fulfillments
    ///
    /// \returns a map indexed by Requirements
    std::map<Requirement, Fulfillment> resolve_requirements(std::string_view module_id) const;

    ///
    /// \returns a list of Requirements for \p module_id
    std::list<Requirement> get_requirements(std::string_view module_id) const;

    ///
    /// \brief A Fulfillment is a combination of a Requirement and the module and implementation ids where this is
    /// implemented
    /// \returns a map of Fulfillments for \p module_id
    std::map<std::string, std::vector<Fulfillment>> get_fulfillments(std::string_view module_id) const;
};

///
/// \brief Config intended to be created by the manager for validation and serialization. Contains config and
/// manifest parsing
///
class ManagerConfig : public ConfigBase {
private:
    const ManagerSettings& ms;
    Validators validators;
    std::unique_ptr<nlohmann::json_schema::json_validator> draft7_validator;
    std::unique_ptr<everest::config::UserConfigStorage> user_config_storage;
    std::map<everest::config::ConfigurationParameterIdentifier, everest::config::GetConfigurationParameterResponse>
        database_get_config_parameter_response_cache;

    nlohmann::json apply_user_config_and_defaults();

    ///
    /// \brief loads and validates the manifest of the \p module_config
    void load_and_validate_manifest(ModuleConfig& module_config);

    ///
    /// \brief loads and validates the given file \p file_path with the schema \p schema
    ///
    /// \returns the loaded json and how long the validation took in ms
    std::tuple<nlohmann::json, int64_t> load_and_validate_with_schema(const fs::path& file_path,
                                                                      const nlohmann::json& schema);

    ///
    /// \brief resolves inheritance tree of json interface \p intf_name, throws an exception if variables or
    /// commands would be overwritten
    ///
    /// \returns the resulting interface definition
    nlohmann::json resolve_interface(std::string_view intf_name);

    ///
    /// \brief loads the contents of the interface file referenced by the give \p intf_name from disk and validates
    /// its contents
    ///
    /// \returns a json object containing the interface definition
    nlohmann::json load_interface_file(std::string_view intf_name);

    ///
    /// \brief loads the contents of an error or an error list referenced by the given \p reference.
    ///
    /// \returns a list of json objects containing the error definitions
    std::list<nlohmann::json> resolve_error_ref(std::string_view reference);

    ///
    /// \brief replaces all error references in the given \p interface_json with the actual error definitions
    ///
    /// \returns the interface_json with replaced error references
    nlohmann::json replace_error_refs(nlohmann::json& interface_json);

    ///
    /// \brief resolves all requirements (connections) of the modules in the main config
    void resolve_all_requirements();

    ///
    /// \brief parses the provided \p config resolving types, errors, manifests, requirements and 3 tier module
    /// mappings
    void parse(ModuleConfigurations& module_configs);

    ///
    /// \brief Parses the 3 tier model mappings in the config
    /// A "mapping" can be specified in the following way:
    /// You can set a EVSE id called "evse" and Connector id called "connector" for the whole module.
    /// Alternatively you can set individual mappings for implementations.
    /// mapping:
    ///   module:
    ///     evse: 1
    ///     connector: 1
    ///   implementations:
    ///     implementation_id:
    ///       evse: 1
    ///       connector: 1
    /// If no mappings are found it will be assumed that the module is mapped to the charging station.
    /// If only a module mapping is defined alle implementations are mapped to this module mapping.
    /// Implementations can have overwritten mappings.
    void parse_3_tier_model_mapping();

public:
    ///
    /// \brief Create a ManagerConfig from the provided ManagerSettings \p ms
    explicit ManagerConfig(const ManagerSettings& ms);

    /// \brief Sets the config \p value associated with the \p identifier
    /// \returns if the setting of the value was successful or not
    everest::config::SetConfigStatus
    set_config_value(const everest::config::ConfigurationParameterIdentifier& identifier,
                     const everest::config::ConfigEntry& value);

    /// \brief Gets the configuration parameter associated with the \p identifier
    /// \returns a result containing the configuration item or an error
    everest::config::GetConfigurationParameterResponse
    get_config_value(const everest::config::ConfigurationParameterIdentifier& identifier);
};

///
/// \brief Contains intended to be used by modules using a pre-parsed and validated config json serialized from
/// ManagerConfig
///
class Config : public ConfigBase {
private:
    ModuleConfig module_config;
    std::map<std::string, ModuleTierMappings, std::less<>> tier_mappings;
    std::optional<TelemetryConfig> telemetry_config;
    std::map<std::string, ConfigCache, std::less<>> module_config_cache;

    void populate_module_config_cache();

    void populate_error_map();

public:
    ///
    /// \brief creates a new Config object form the given \p mqtt_settings and \p config
    explicit Config(const MQTTSettings& mqtt_settings, const nlohmann::json& config);

    ///
    /// \returns object that contains the module config options
    ModuleConfig get_module_config() const;

    ///
    /// \returns the ErrorTypeMap
    error::ErrorTypeMap get_error_map() const;

    ///
    /// \returns true if the module \p module_name provides the implementation \p impl_id
    bool module_provides(std::string_view module_name, std::string_view impl_id);

    ///
    /// \returns the commands that the modules \p module_name implements from the given implementation \p impl_id
    const nlohmann::json& get_module_cmds(std::string_view module_name, std::string_view impl_id);

    ///
    /// \brief A RequirementInitialization contains everything needed to initialize a requirement in user code. This
    /// includes the Requirement, its Fulfillment and an optional Mapping
    /// \returns a RequirementInitialization
    RequirementInitialization get_requirement_initialization(std::string_view module_id) const;

    ///
    /// \returns a map of module config options
    ModuleConfigs get_module_configs(std::string_view module_id) const;

    //
    /// \returns the 3 tier model mappings for the given \p module_id
    std::optional<ModuleTierMappings> get_module_3_tier_model_mappings(std::string_view module_id) const;

    //
    /// \returns the 3 tier model mapping for the given \p module_id and \p impl_id
    std::optional<Mapping> get_3_tier_model_mapping(std::string_view module_id, std::string_view impl_id) const;

    ///
    /// \brief assemble basic information about the module (id, name,
    /// authors, license)
    ///
    /// \returns a ModuleInfo object
    ModuleInfo get_module_info(std::string_view module_id) const;

    ///
    /// \returns a TelemetryConfig if this has been configured
    std::optional<TelemetryConfig> get_telemetry_config();

    ///
    /// \returns a json object that contains the interface definition
    nlohmann::json get_interface_definition(std::string_view interface_name) const;

    ///
    /// \brief A json schema loader that can handle type refs and otherwise uses the builtin draft7 schema of
    /// the json schema validator when it encounters it. Throws an exception
    /// otherwise
    void ref_loader(const nlohmann::json_uri& uri, nlohmann::json& schema);

    ///
    /// \brief loads all module manifests relative to the \p main_dir
    ///
    /// \returns all module manifests as a json object
    static nlohmann::json load_all_manifests(std::string_view modules_dir, std::string_view schemas_dir);

    ///
    /// \brief Extracts the keys of the provided json \p object
    ///
    /// \returns a set of object keys
    static everest::config::Keys keys(const nlohmann::json& object);
};
} // namespace Everest

NLOHMANN_JSON_NAMESPACE_BEGIN
template <> struct adl_serializer<Everest::Schemas> {
    static void to_json(nlohmann::json& j, const Everest::Schemas& s);

    static void from_json(const nlohmann::json& j, Everest::Schemas& s);
};
NLOHMANN_JSON_NAMESPACE_END

#endif // UTILS_CONFIG_HPP
