#include "daemonfacade.h"
#include <myapp/logging/loggerfactory.h>
#include <myapp/modbus/devicefactory.h>
#include <myapp/persistence/databasefactory.h>
#include <myapp/rcon/unixprovider.h>
#include "utils/stringtrim.h"
#include "utils/stringcompare.h"
#include <iostream>
#include <thread>
#include <unistd.h>

using namespace myapp::async;
using namespace myapp::cmdargs;
using namespace myapp::configuration;
using namespace myapp::logging;
using namespace myapp::rcon;

namespace myapp {

static void stop_loop(event_loop& loop)
{
    loop.stop();
}

static void repeat_until_nothrow(std::function<void()> && f, std::function<void()> && otherwise)
{
	for (;;) {
		try {
			return f();
		}
		catch (std::runtime_error&) {
			otherwise();
		}
	}
}

static std::shared_ptr<logger> create_logger(options& opts, config& cfg)
{
    if (opts.nofork)
        return logger_factory::create_console_logger();

//    if (cfg.log_path)
//        return logger_factory::create_text_logger(cfg.log_path);

    return logger_factory::create_journal_logger();
}

daemon_facade::daemon_facade(options& opts, config& cfg)
  : options_(opts)
  , config_(cfg)
  , logger_(create_logger(opts, cfg))
  , loop_(event_loop::getDefault())
  , sigint_(signal::create(loop_))
  , sigterm_(signal::create(loop_))
  , stdin_(nullptr)
  , rcon_(logger_, rcon::unix_provider::create(loop_))
  , databases_()
  , modbus_connections_()
  , modbus_devices_()
  , subscribers_()
{
    set_console_termination_handlers();
    open_local_console_if_nofork();
    open_remote_console();
    open_database();
    create_modbus_devices();
}

std::string daemon_facade::handle_command(std::string msg)
{
    if (msg.empty())
        return std::string{ };

    for (auto& mbdevice : modbus_devices_)
    {
        using namespace std::string_literals;
        auto device_name_with_whitespace = mbdevice->get_name() + " "s;
        if (utils::starts_with(msg, device_name_with_whitespace))
            return mbdevice->handle_command(msg.substr(device_name_with_whitespace.size()));
    }

    return "No handler for command found.";
}

void daemon_facade::set_console_termination_handlers()
{
    sigint_->on(SIGINT, [this](int) { stop_loop(*loop_); });
    sigterm_->on(SIGTERM, [this](int) { stop_loop(*loop_); });
}

void daemon_facade::open_remote_console()
{
    rcon_.start(config_.remote_console_unix_socket_path);
    rcon_.set_on_data([this](std::string data) -> std::string
    {
        utils::trim(data);
        return handle_command(std::move(data));
    });

    subscribers_.emplace_back([this](std::string const& topic, std::string const& value)
    {
        rcon_.publish("Status update '" + topic + "' with value '" + value + "'.");
    });
}

void daemon_facade::open_local_console_if_nofork()
{
    if (!options_.nofork) return;
    stdin_ = pipe::create(loop_);

    stdin_->on_error([this](auto async_result)
    {
        try { async_result(); }
        catch (std::exception& e) { std::cerr << e.what() << std::endl; }

        stop_loop(*loop_);
    });

    stdin_->open(STDIN_FILENO);
    stdin_->read([this](std::string data)
    {
        utils::trim(data);
        auto reply = handle_command(std::move(data));
        if (!reply.empty())
            std::cout << reply << std::endl;
    }, [this] { stdin_->close(); });
}

void daemon_facade::open_database()
{
    using namespace std::string_literals;

    // sqlite3
    try 
    {
        auto connection_string = config_.sqlite_persistence_connection_string;
        if (!connection_string.empty())
        {
            databases_.emplace_back(persistence::database_factory::create_sqlite3(std::move(connection_string)));
        }
        else
        {
            logger_->notice("PERSISTENCE", "Sqlite3 database connection string is not configured. Persisting disabled."s);
            return;
        }
    }
    catch (std::runtime_error const& e)
    {
        logger_->warning("PERSISTENCE", "Exception thrown while opening sqlite3 database, persisting disabled: "s + e.what());
        return;
    }

    // redis
    try
    {
        auto connection_string = config_.redis_persistence_connection_string;
        if (!connection_string.empty())
        {
            databases_.emplace_back(persistence::database_factory::create_redis(std::move(connection_string), logger_));
        }
        else
        {
            logger_->notice("PERSISTENCE", "Redis connection string is not configured. Persisting disabled.");
            return;
        }
    }
    catch (std::runtime_error const& e)
    {
        logger_->warning("PERSISTENCE", "Exception thrown while connecting to redis, persisting disabled: "s + e.what());
        return;
    }

    // pub-sub
    subscribers_.emplace_back([this](std::string const& topic, std::string const& value)
    {
        for (auto& db : databases_)
        {
            try
            {
                db->save_readout(topic, value);
            }
            catch (std::runtime_error const& e)
            {
                logger_->warning("PERSISTENCE", "Error occured while persisting readout '"s + topic + "': "s + e.what());
            }
        }
    });
}

void daemon_facade::create_modbus_devices()
{
	for (auto const& connections_config : config_.modbus)
	{
		auto const& connection_string = connections_config.first;
		auto connection = modbus::connection::create(logger_, connection_string);

		for (auto const& devices_config : connections_config.second)
		{
			auto const& device_name = devices_config.first;
			auto const& device_connection_string = devices_config.second;

            modbus_devices_.emplace_back(modbus::device_factory::create(connection, device_name, device_connection_string, logger_));
		}

		modbus_connections_.emplace_back(std::move(connection));
	}

    for (auto& mbdev : modbus_devices_)
    {
        mbdev->initialize();
        mbdev->status_update_callback([this](std::string key, std::string value)
        {
            for (auto const& sub : subscribers_)
            {
                try 
                {
                    sub(key, value);
                }
                catch (...)
                {
                    // ignored
                }
            }
        });
    }
}

void daemon_facade::connect_modbus()
{
	for (auto& mbconn : modbus_connections_)
	{
		std::thread{[mbconn]
		{
			repeat_until_nothrow(
				[&mbconn] { mbconn->connect(); },
				[] { std::this_thread::sleep_for(std::chrono::seconds(5)); });

			mbconn->get_poller()->run();
		} }.detach();
	}
}

void daemon_facade::close_modbus()
{
    for (auto& mbconn : modbus_connections_)
        mbconn->get_poller()->abort();
    for (auto& mbconn : modbus_connections_)
        mbconn->close();
}

void daemon_facade::loop()
{
    connect_modbus();
    loop_->run();
    close_modbus();
}

} /* namespace myapp */
