#pragma once

#define DEBUGLOG_DEFAULT_LOG_LEVEL_DEBUG

#include <DebugLog.h>
#include <Arduino.h>
#include <ArduinoJson.h>
#include <FFat.h>

#include <mutex>

class FSmount
{
public:
    FSmount()
    {
        if (!FFat.begin(false))
        {
            LOG_ERROR("Unable to mount filesystem without formatting");
            if (!FFat.begin(true))
            {
                LOG_ERROR("Formatted and mounted filesystem");
            }
        }

        LOG_INFO("Local Filesystem Mounted Correctly");
        const auto totalBytes = FFat.totalBytes();
        const auto freeBytes = FFat.freeBytes();
        const auto usedBytes = FFat.usedBytes();
        const auto mountPoint = FFat.mountpoint();
        LOG_INFO("Local filesystem, total", totalBytes / 1024, "KB - used", usedBytes / 1024, "KB - free", freeBytes / 1024, "KB");
        LOG_INFO("Local filesystem, mountpoint", mountPoint);
    }

    ~FSmount()
    {
        FFat.end(); // unmout filesystem to avoid corruption
        LOG_INFO("Local Filesystem Unmounted Correctly");
    }
};

class Config
{

public:
    static Config &getInstance()
    {
        static Config instance;
        return instance;
    }

private:
    Config() = default;
    Config(const Config &) = delete;
    Config &operator=(const Config &) = delete;

public:
    void init()
    {
        FSmount mount; // scoped mount of the filesystem

        // Initialize and mount filesystem
        LOG_INFO("Initializing Config");

        if (!FFat.exists("/config.json"))
        {
            LOG_WARN("Initializing default config");
            saveConfig();
        }

        File file = FFat.open("/config.json", FILE_READ, false);
        if (!file)
        {
            LOG_ERROR("Unable to open config.json");
            return;
        }

        if (ArduinoJson::deserializeJson(m_configJson, file) != ArduinoJson::DeserializationError::Ok)
        {
            LOG_ERROR("Unable to load config.json");
        }

        std::string loadedConf;
        ArduinoJson::serializeJsonPretty(m_configJson, loadedConf);
        LOG_INFO("Loaded Configuration\n", loadedConf.c_str());

        deserialize(); // convert from json format to class members
        file.close();  // close config file before unmounting filesystem
    };

    void updateConfig(ArduinoJson::JsonDocument &json)
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        {
            FSmount mount;
            m_configJson = json;
            deserialize();
            saveConfig();
        }; // filesystem is unmounted here
        delay(500);
        esp_restart(); // configuration updates trigger a cpu restart
    }

    void resetConfig()
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        {
            FSmount mount;
            LOG_WARN("Removing config.json");
            if (!FFat.remove("/config.json"))
            {
                LOG_ERROR("Unable to remove config.json");
            }
            LOG_WARN("Configuration reset, Restarting");
        }; // filesystem is unmounted here
        delay(500);
        esp_restart();
    }

private:
    void saveConfig() // write configuration to flash memory
    {
        File file = FFat.open("/config.json", FILE_WRITE, true);
        if (!file)
        {
            LOG_ERROR("Unable to open config.json for writing");
            return;
        }
        serialize(); // serialize default configuration
        if (ArduinoJson::serializeJson(m_configJson, file) == 0)
        {
            LOG_ERROR("Serialization Failed");
        }
        file.close();
    }

    //////////////////////////////////////////////////////////////
    ////////////// SERIALIZATION + DESERIALIZATION ///////////////
    //////////////////////////////////////////////////////////////

    void serialize()
    {
        // form class members to json document
        {
            auto globals = m_configJson["globals"].to<ArduinoJson::JsonObject>();
            globals["loopDelay"] = m_globalLoopDelay;
        };

        {
            auto ethernet = m_configJson["ethernet"].to<ArduinoJson::JsonObject>();
            ethernet["hostname"] = m_ethHostname;
            ethernet["ipAddr"] = m_ethIpAddr;
            ethernet["netmask "] = m_ethNetmask;
            ethernet["gateway "] = m_ethGateway;
        };

        {
            auto modbus = m_configJson["modbus"].to<ArduinoJson::JsonObject>();
            modbus["relayAddr"] = m_modbusRelayAddr;
            modbus["temperatureAddr"] = m_modbusTemperatureAddr;
            modbus["senecaAddr"] = m_modbusSenecaAddr;
            modbus["flowmeterAddr"] = m_modbusFlowmeterAddr;
            modbus["tankLevelAddr"] = m_modbusTankLevelAddr;
        };

        {
            auto temperature = m_configJson["temperature"].to<ArduinoJson::JsonObject>();
            temperature["expectedSensors"] = m_tempExpectedSensors;
            auto values = temperature["correctionValues"].to<ArduinoJson::JsonArray>();
            for (auto v : m_tempCorrectionValues)
            {
                values.add(v);
            }
        };

        {
            auto ntp = m_configJson["ntp"].to<ArduinoJson::JsonObject>();
            ntp["pool"] = m_ntpPool;
            ntp["timezone"] = m_ntpTimezone;
            ntp["updateInterval"] = m_ntpUpdateInterval;
            ntp["retries"] = m_ntpRetries;
        };

        {
            auto mqtt = m_configJson["mqtt"].to<ArduinoJson::JsonObject>();
            mqtt["host"] = m_mqttHost;
            mqtt["port"] = m_mqttPort;
            mqtt["loopTime"] = m_mqttLoopTime;
            mqtt["clientName"] = m_mqttClientName;
            mqtt["retries"] = m_mqttRetries;
            auto publish = mqtt["publish"].to<ArduinoJson::JsonObject>();
            for (auto v : m_mqttSubscribe)
            {
                publish[v.first] = v.second;
            }
            auto subscribe = mqtt["subscribe"].to<ArduinoJson::JsonObject>();
            for (auto v : m_mqttPublish)
            {
                subscribe[v.first] = v.second;
            }
        };
    };

    void deserialize()
    { // from json document to class members
        if (m_configJson.isNull())
        {
            LOG_ERROR("NUll config document");
            return;
        }

        {
            auto globals = m_configJson["globals"];
            m_globalLoopDelay = globals["loopDelay"].as<uint16_t>();
        };

        {
            auto ethernet = m_configJson["ethernet"];
            m_ethHostname = ethernet["hostname"].as<std::string>();
            m_ethIpAddr = ethernet["ipAddr"].as<std::string>();
            m_ethNetmask = ethernet["netmask"].as<std::string>();
            m_ethGateway = ethernet["gateway"].as<std::string>();
        };

        {
            auto modbus = m_configJson["modbus"];
            m_modbusRelayAddr = modbus["relayAddr"].as<uint8_t>();
            m_modbusTemperatureAddr = modbus["temperatureAddr"].as<uint8_t>();
            m_modbusSenecaAddr = modbus["senecaAddr"].as<uint8_t>();
            m_modbusFlowmeterAddr = modbus["flowmeterAddr"].as<uint8_t>();
            m_modbusTankLevelAddr = modbus["tankLevelAddr"].as<uint8_t>();
        };

        {
            auto temperature = m_configJson["temperature"];
            m_tempExpectedSensors = temperature["expectedSensors"].as<uint8_t>();
            auto values = temperature["correctionValues"].as<JsonArray>();
            m_tempCorrectionValues.reserve(values.size());
            for (auto v : values)
            {
                m_tempCorrectionValues.push_back(v.as<float>());
            }
        };

        {
            auto ntp = m_configJson["ntp"];
            m_ntpPool = ntp["pool"].as<std::string>();
            m_ntpTimezone = ntp["timezone"].as<uint16_t>();
            m_ntpUpdateInterval = ntp["updateInterval"].as<uint16_t>();
            m_ntpRetries = ntp["retries"].as<uint8_t>();
        };

        {
            auto mqtt = m_configJson["mqtt"];
            m_mqttHost = mqtt["host"].as<std::string>();
            m_mqttPort = mqtt["port"].as<uint16_t>();
            m_mqttLoopTime = mqtt["loopTime"].as<uint16_t>();
            m_mqttRetries = mqtt["retries"].as<uint16_t>();
            auto subscribe = mqtt["subsribe"].as<ArduinoJson::JsonObject>();
            for (auto v : subscribe)
            {
                m_mqttSubscribe[v.key().c_str()] = v.value().as<std::string>();
            }
            auto publish = mqtt["publish"].as<ArduinoJson::JsonObject>();
            for (auto v : publish)
            {
                m_mqttPublish[v.key().c_str()] = v.value().as<std::string>();
            }
        };
    };

private:
    ArduinoJson::JsonDocument m_configJson;
    std::mutex m_mutex;

public:
    // Globals
    std::uint16_t m_globalLoopDelay = 1000; // in milliseconds

    // Ethernet
    std::string m_ethHostname = "ETcontroller_PRO";
    std::string m_ethIpAddr = "10.0.2.251";
    std::string m_ethNetmask = "255.255.255.0";
    std::string m_ethGateway = "10.0.2.1";

    // MODBUS
    uint8_t m_modbusRelayAddr = 0x01;
    uint8_t m_modbusTemperatureAddr = 0xAA;
    uint8_t m_modbusSenecaAddr = 0xBB;
    uint8_t m_modbusFlowmeterAddr = 0xCC;
    uint8_t m_modbusTankLevelAddr = 0xDD;

    // Temperature Board
    uint8_t m_tempExpectedSensors = 1;
    std::vector<float> m_tempCorrectionValues = std::vector<float>(8, 0.0f);

    // NTP
    std::string m_ntpPool = "pool.ntp.org";
    uint16_t m_ntpTimezone = 3600;       // GTM +1
    uint16_t m_ntpUpdateInterval = 3600; // every hour
    uint8_t m_ntpRetries = 5;

    // MQTT
    std::string m_mqttHost = "10.0.2.249";
    uint16_t m_mqttPort = 1883;
    uint16_t m_mqttLoopTime = 100; // in milliseconds
    uint8_t m_mqttRetries = 5;
    std::string m_mqttClientName = "etcontrollerPRO";

    std::map<const std::string, std::string> m_mqttSubscribe = {
        {"commands", "test/etcontroller/commands"}};
    std::map<const std::string, std::string> m_mqttPublish = {
        {"heatpump", "test/etcontroller/heatpump"},
        {"temperature", "test/etcontroller/temperatures"},
        {"irrigation", "test/etcontroller/irrigation"}};
};