#include <cstdio>
#include <cxxopts.hpp>
#include <iomanip>
#include <optional>
#include <solar/logger/database.hpp>
#include <solar/logger/envoydata.hpp>
#include <solar/logger/zeverdata.hpp>
#include <spdlog/spdlog.h>
#include <sstream>
#include <string>

namespace detail
{
    // libcurl callback
    size_t write_to_string(void * ptr, size_t size, size_t nmemb, void * stream)
    {
        std::string line((char *)ptr, nmemb);
        std::string * buffer = (std::string *)stream;
        buffer->append(line);

        return nmemb * size;
    }
}

std::optional<cxxopts::ParseResult> ExtractArgs(int argc, char ** argv)
{
    cxxopts::Options options(
        "solar-logger",
        "solar-logger is a small program that retrieves solarpower generation statistics from smart inverters. It supports the Zeverlution Sxxxx and the Envoy S Metered (v5.x.x)");

    options.add_options()(
        "u,url",
        "Fully qualified URL path and protocol to the web resource containing the stastics. For the Zeverlution platform this is /home.cgi and for the Envoy S this is /production.json.",
        cxxopts::value<std::string>())(
        "connection-string",
        "Path to the sqlite3 database file",
        cxxopts::value<std::string>())(
        "type",
        "Type of web resource to fetch; \"Zeverlution\" or \"Envoy\"",
        cxxopts::value<std::string>())(
        "t,timeout",
        "Fetch time out in milliseconds",
        cxxopts::value<unsigned>()->default_value("1000"));

    if(argc == 1)
    {
        std::cout << options.help() << std::endl;
        return {};
    }

    try
    {
        auto const parsed = options.parse(argc, argv);
        return parsed;
    }
    catch(cxxopts::OptionException const & e)
    {
        spdlog::error(e.what());
        return {};
    }
}

std::optional<std::string> FetchDataFromURL(const std::string & url, const unsigned int timeout)
{
    curl_global_init(CURL_GLOBAL_ALL);

    CURL * curl = curl_easy_init();

    curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L);
    //curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);

    std::string buffer;
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, detail::write_to_string);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer);

    curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout);

    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());

    buffer.reserve(256);
    const auto result = curl_easy_perform(curl);
    if(result)
    {
        spdlog::error("Failed to fetch data from URL {} with cURL error code {}", url.c_str(), result);
        curl_easy_cleanup(curl);
        return {};
    }

    buffer.shrink_to_fit();

    curl_easy_cleanup(curl);

    return buffer;
}

int main(int argc, char ** argv)
{
    auto const maybeArgs = ExtractArgs(argc, argv);
    if(!maybeArgs.has_value())
    {
        return 1;
    }

    auto const & args = maybeArgs.value();

    Database db(args["connection-string"].as<std::string>());
    auto const now = std::time(nullptr);
    auto const resourceType = args["type"].as<std::string>();

    if(resourceType == "Zeverlution")
    {
        auto const webResource = FetchDataFromURL(args["url"].as<std::string>(), args["timeout"].as<unsigned>());
        if(!webResource.has_value())
        {
            return -1;
        }

        auto const zeverData = ZeverData::ParseString(webResource.value());
        if(!zeverData.has_value())
        {
            return -1;
        }

        ZeverRow row = {
            .epochTime = now,
            .watt = zeverData->watt,
            .kilowattPerHour = zeverData->kilowattPerHour,
        };

        if(!db.Insert(row))
        {
            return -1;
        }
    }
    else if(resourceType == "Envoy")
    {
        auto const webResource = FetchDataFromURL(args["url"].as<std::string>(), args["timeout"].as<unsigned>());
        if(!webResource.has_value())
        {
            return -1;
        }

        auto const parsed = EnvoyData::ParseJson(webResource.value());
        if(!parsed.has_value())
        {
            return -1;
        }

        EnvoyRow row = {
            .epochTime = now,
            .inverterCount = parsed->activeCount,
            .currentWatt = parsed->wNow,
            .lifetimeWattHour = parsed->whLifetime,
            .inverterTime = parsed->readingTime,
        };

        if(!db.Insert(row))
        {
            return -1;
        }
    }
    else
    {
        spdlog::error("Unrecognized Type argument {}", resourceType);
        return -1;
    }

    return 0;
}
