/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *                                                                         *
 *  Copyright (C) 2016 Simon Stürz <simon.stuerz@guh.io>                   *
 *                                                                         *
 *  This file is part of guh.                                              *
 *                                                                         *
 *  Guh is free software: you can redistribute it and/or modify            *
 *  it under the terms of the GNU General Public License as published by   *
 *  the Free Software Foundation, version 2 of the License.                *
 *                                                                         *
 *  Guh is distributed in the hope that it will be useful,                 *
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of         *
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          *
 *  GNU General Public License for more details.                           *
 *                                                                         *
 *  You should have received a copy of the GNU General Public License      *
 *  along with guh. If not, see <http://www.gnu.org/licenses/>.            *
 *                                                                         *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "loggingcategories.h"
#include "guhconfiguration.h"
#include "guhsettings.h"

#include <QTimeZone>
#include <QCoreApplication>
#include <QFile>

namespace guhserver {

GuhConfiguration::GuhConfiguration(QObject *parent) :
    QObject(parent)
{
    // Init server uuid if we don't have one.
    QUuid id = serverUuid();
    if (id.isNull()) {
        // If we can, let's use the system's machine-id as our UUID.
        QFile f("/etc/machine-id");
        if (f.open(QFile::ReadOnly)) {
            QString tmpId = QString::fromLatin1(f.readAll()).trimmed();
            tmpId.insert(8, "-");
            tmpId.insert(13, "-");
            tmpId.insert(18, "-");
            tmpId.insert(23, "-");
            setServerUuid(QUuid(tmpId));
        } else {
            qWarning(dcApplication()) << "Failed to open /etc/machine-id for reading. Generating a new UUID for this server instance.";
            setServerUuid(QUuid::createUuid());
        }
    }
    qCDebug(dcApplication()) << "System UUID is:" << serverUuid().toString();

    // Make sure default values are in configuration file so that it's easier for users to modify
    setServerName(serverName());
    setTimeZone(timeZone());
    setLocale(locale());
    setBluetoothServerEnabled(bluetoothServerEnabled());
    setSslCertificate(sslCertificate(), sslCertificateKey());

    GuhSettings settings(GuhSettings::SettingsRoleGlobal);

    // TcpServer
    bool createDefaults = !settings.childGroups().contains("TcpServer");
    if (settings.childGroups().contains("TcpServer")) {
        settings.beginGroup("TcpServer");
        if (settings.value("disabled").toBool()) {
            qCDebug(dcApplication) << "TCP Server disabled by configuration";
        } else if (!settings.childGroups().isEmpty()) {
            foreach (const QString &key, settings.childGroups()) {
                ServerConfiguration config = readServerConfig("TcpServer", key);
                m_tcpServerConfigs[config.id] = config;
            }
        } else {
            createDefaults = true;
        }
        settings.endGroup();
    }
    if (createDefaults) {
        qCWarning(dcApplication) << "No TCP Server configuration found. Generating default of 0.0.0.0:2222";
        ServerConfiguration config;
        config.id = "default";
        config.address = QHostAddress("0.0.0.0");
        config.port = 2222;
        // TODO enable encryption/authentication by default once the important clients are supporting it
        config.sslEnabled = false;
        config.authenticationEnabled = false;
        m_tcpServerConfigs[config.id] = config;
        storeServerConfig("TcpServer", config);
    }

    // Webserver
    createDefaults = !settings.childGroups().contains("WebServer");
    if (settings.childGroups().contains("WebServer")) {
        settings.beginGroup("WebServer");
        if (settings.value("disabled").toBool()) {
            qCDebug(dcApplication) << "WebServer disabled by configuration";
        } else if (!settings.childGroups().isEmpty()) {
            foreach (const QString &key, settings.childGroups()) {
                WebServerConfiguration config = readWebServerConfig(key);
                m_webServerConfigs[config.id] = config;
            }
        } else {
            createDefaults = true;
        }
        settings.endGroup();
    }
    if (createDefaults) {
        qCWarning(dcApplication) << "No WebServer configuration found. Generating default of 0.0.0.0:3333";
        WebServerConfiguration insecureConfig;
        insecureConfig.id = "insecure";
        insecureConfig.address = QHostAddress("0.0.0.0");
        insecureConfig.port = 80;
        insecureConfig.sslEnabled = false;
        insecureConfig.authenticationEnabled = false;
        insecureConfig.publicFolder = defaultWebserverPublicFolderPath();
        m_webServerConfigs[insecureConfig.id] = insecureConfig;
        storeWebServerConfig(insecureConfig);

        WebServerConfiguration secureConfig;
        secureConfig.id = "secure";
        secureConfig.address = QHostAddress("0.0.0.0");
        secureConfig.port = 443;
        secureConfig.sslEnabled = true;
        secureConfig.authenticationEnabled = false;
        secureConfig.publicFolder = defaultWebserverPublicFolderPath();
        m_webServerConfigs[secureConfig.id] = secureConfig;
        storeWebServerConfig(secureConfig);
    }

    // WebSocket Server
    createDefaults = !settings.childGroups().contains("WebSocketServer");
    if (settings.childGroups().contains("WebSocketServer")) {
        settings.beginGroup("WebSocketServer");
        if (settings.value("disabled").toBool()) {
            qCDebug(dcApplication) << "WebSocket Server disabled by configuration.";
        } else if (!settings.childGroups().isEmpty()) {
            foreach (const QString &key, settings.childGroups()) {
                ServerConfiguration config = readServerConfig("WebSocketServer", key);
                m_webSocketServerConfigs[config.id] = config;
            }
        } else {
            createDefaults = true;
        }
        settings.endGroup();
    }
    if (createDefaults) {
        qCWarning(dcApplication) << "No WebSocketServer configuration found. Generating default of 0.0.0.0:4444";
        ServerConfiguration config;
        config.id = "default";
        config.address = QHostAddress("0.0.0.0");
        config.port = 4444;
        // TODO enable encryption/authentication by default once the important clients are supporting it
        config.sslEnabled = false;
        config.authenticationEnabled = false;
        m_webSocketServerConfigs[config.id] = config;
        storeServerConfig("WebSocketServer", config);
    }
}

QUuid GuhConfiguration::serverUuid() const
{
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("guhd");
    return settings.value("uuid", QUuid()).toUuid();
}

QString GuhConfiguration::serverName() const
{
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("guhd");
    return settings.value("name", "guhIO").toString();
}

void GuhConfiguration::setServerName(const QString &serverName)
{
    qCDebug(dcApplication()) << "Configuration: Server name:" << serverName;

    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("guhd");
    if (settings.value("name").toString() == serverName) {
        qCDebug(dcApplication()) << "Configuration: Server name unchanged.";
        settings.endGroup();
    } else {
        settings.setValue("name", serverName);
        settings.endGroup();
        emit serverNameChanged(serverName);
    }
}

QByteArray GuhConfiguration::timeZone() const
{
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("guhd");
    return settings.value("timeZone", QTimeZone::systemTimeZoneId()).toByteArray();
}

void GuhConfiguration::setTimeZone(const QByteArray &timeZone)
{
    qCDebug(dcApplication()) << "Configuration: Time zone:" << QString::fromUtf8(timeZone);

    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("guhd");
    if (settings.value("timeZone").toByteArray() == timeZone) {
        qCDebug(dcApplication()) << "Configuration: Time zone unchanged.";
        settings.endGroup();
    } else {
        settings.setValue("timeZone", timeZone);
        settings.endGroup();
        emit timeZoneChanged();
    }
}

QLocale GuhConfiguration::locale() const
{
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("guhd");
    return settings.value("language", "en_US").toString();
}

void GuhConfiguration::setLocale(const QLocale &locale)
{
    qCDebug(dcApplication()) << "Configuration: set locale:" << locale.name() << locale.nativeCountryName() << locale.nativeLanguageName();

    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("guhd");
    if (settings.value("language").toString() == locale.name()) {
        qCDebug(dcApplication()) << "Configuration: Language unchanged.";
        settings.endGroup();
    } else {
        settings.setValue("language", locale.name());
        settings.endGroup();
        emit localeChanged();
    }
}

QHash<QString, ServerConfiguration> GuhConfiguration::tcpServerConfigurations() const
{
    return m_tcpServerConfigs;
}

void GuhConfiguration::setTcpServerConfiguration(const ServerConfiguration &config)
{
    m_tcpServerConfigs[config.id] = config;
    storeServerConfig("TcpServer", config);
    emit tcpServerConfigurationChanged(config.id);
}

void GuhConfiguration::removeTcpServerConfiguration(const QString &id)
{
    m_tcpServerConfigs.take(id);
    deleteServerConfig("TcpServer", id);
    emit tcpServerConfigurationRemoved(id);
}

QHash<QString, WebServerConfiguration> GuhConfiguration::webServerConfigurations() const
{
    return m_webServerConfigs;
}

void GuhConfiguration::setWebServerConfiguration(const WebServerConfiguration &config)
{
    m_webServerConfigs[config.id] = config;

    storeServerConfig("WebServer", config);

    // This is a bit odd that we need to open the config once more just for the publicFolder...
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("WebServer");
    settings.beginGroup(config.id);
    settings.setValue("publicFolder", config.publicFolder);
    settings.endGroup();
    settings.endGroup();

    emit webServerConfigurationChanged(config.id);
}

void GuhConfiguration::removeWebServerConfiguration(const QString &id)
{
    m_webServerConfigs.take(id);
    deleteServerConfig("WebServer", id);
    emit webServerConfigurationRemoved(id);
}

QHash<QString, ServerConfiguration> GuhConfiguration::webSocketServerConfigurations() const
{
    return m_webSocketServerConfigs;
}

void GuhConfiguration::setWebSocketServerConfiguration(const ServerConfiguration &config)
{
    m_webSocketServerConfigs[config.id] = config;
    storeServerConfig("WebSocketServer", config);
    emit webSocketServerConfigurationChanged(config.id);
}

void GuhConfiguration::removeWebSocketServerConfiguration(const QString &id)
{
    m_webSocketServerConfigs.take(id);
    deleteServerConfig("WebSocketServer", id);
    emit webSocketServerConfigurationRemoved(id);
}

bool GuhConfiguration::bluetoothServerEnabled() const
{
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("BluetoothServer");
    return settings.value("enabled", false).toBool();
}

void GuhConfiguration::setBluetoothServerEnabled(const bool &enabled)
{
    qCDebug(dcApplication()) << "Configuration: Bluetooth server" << (enabled ? "enabled" : "disabled");

    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("BluetoothServer");
    settings.setValue("enabled", enabled);
    settings.endGroup();
    emit bluetoothServerEnabled();
}

bool GuhConfiguration::cloudEnabled() const
{
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("Cloud");
    return settings.value("enabled", false).toBool();
}

void GuhConfiguration::setCloudEnabled(bool enabled)
{
    if (cloudEnabled() != enabled) {
        GuhSettings settings(GuhSettings::SettingsRoleGlobal);
        settings.beginGroup("Cloud");
        settings.setValue("enabled", enabled);
        settings.endGroup();
        emit cloudEnabledChanged(enabled);
    }
}

QString GuhConfiguration::cloudServerUrl() const
{
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("Cloud");
    return settings.value("cloudServerUrl").toString();
}

QString GuhConfiguration::cloudCertificateCA() const
{
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("Cloud");
    return settings.value("cloudCertificateCA").toString();
}

QString GuhConfiguration::cloudCertificate() const
{
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("Cloud");
    return settings.value("cloudCertificate").toString();
}

QString GuhConfiguration::cloudCertificateKey() const
{
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("Cloud");
    return settings.value("cloudCertificateKey").toString();
}

QString GuhConfiguration::sslCertificate() const
{
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("SSL");
    return settings.value("certificate").toString();
}

QString GuhConfiguration::sslCertificateKey() const
{
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("SSL");
    return settings.value("certificate-key").toString();
}

void GuhConfiguration::setSslCertificate(const QString &sslCertificate, const QString &sslCertificateKey)
{
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("SSL");
    settings.setValue("certificate", sslCertificate);
    settings.setValue("certificate-key", sslCertificateKey);
    settings.endGroup();
}

void GuhConfiguration::setServerUuid(const QUuid &uuid)
{
    qCDebug(dcApplication()) << "Configuration: Server uuid:" << uuid.toString();

    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("guhd");
    settings.setValue("uuid", uuid);
    settings.endGroup();
}

QString GuhConfiguration::defaultWebserverPublicFolderPath() const
{
    QString publicFolderPath;
    if (!qgetenv("SNAP").isEmpty()) {
        // FIXME: one could point to sensible data by changing the SNAP env to i.e /etc
        publicFolderPath = QString(qgetenv("SNAP")) + "/guh-webinterface";
    } else {
        publicFolderPath = "/usr/share/guh-webinterface/public/";
    }
    return publicFolderPath;
}

void GuhConfiguration::storeServerConfig(const QString &group, const ServerConfiguration &config)
{
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup(group);
    settings.remove("disabled");
    settings.beginGroup(config.id);
    settings.setValue("address", config.address.toString());
    settings.setValue("port", config.port);
    settings.setValue("sslEnabled", config.sslEnabled);
    settings.setValue("authenticationEnabled", config.authenticationEnabled);
    settings.endGroup();
    settings.endGroup();
}

ServerConfiguration GuhConfiguration::readServerConfig(const QString &group, const QString &id)
{
    ServerConfiguration config;
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup(group);
    settings.beginGroup(id);
    config.id = id;
    config.address = QHostAddress(settings.value("address").toString());
    config.port = settings.value("port").toUInt();
    config.sslEnabled = settings.value("sslEnabled", true).toBool();
    config.authenticationEnabled = settings.value("authenticationEnabled", true).toBool();
    settings.endGroup();
    settings.endGroup();
    return config;
}

void GuhConfiguration::deleteServerConfig(const QString &group, const QString &id)
{
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup(group);
    settings.remove(id);
    if (settings.childGroups().isEmpty()) {
        settings.setValue("disabled", true);
    }
    settings.endGroup();
}

void GuhConfiguration::storeWebServerConfig(const WebServerConfiguration &config)
{
    storeServerConfig("WebServer", config);
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("WebServer");
    settings.beginGroup(config.id);
    settings.setValue("publicFolder", config.publicFolder);
    settings.endGroup();
    settings.endGroup();
}

WebServerConfiguration GuhConfiguration::readWebServerConfig(const QString &id)
{
    WebServerConfiguration config;
    GuhSettings settings(GuhSettings::SettingsRoleGlobal);
    settings.beginGroup("WebServer");
    settings.beginGroup(id);
    config.id = id;
    config.address = QHostAddress(settings.value("address").toString());
    config.port = settings.value("port").toUInt();
    config.sslEnabled = settings.value("sslEnabled", true).toBool();
    config.authenticationEnabled = settings.value("authenticationEnabled", true).toBool();
    config.publicFolder = settings.value("publicFolder").toString();
    settings.endGroup();
    settings.endGroup();
    return config;
}

QDebug operator <<(QDebug debug, const ServerConfiguration &configuration)
{
    debug.nospace() << "ServerConfiguration(" << configuration.address;
    debug.nospace() << ", " << configuration.id;
    debug.nospace() << ", " << QString("%1:%2").arg(configuration.address.toString()).arg(configuration.port);
    debug.nospace() << ") ";
    return debug;
}

}
