// SPDX-License-Identifier: GPL-3.0-or-later

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-plugins.
*
* nymea-plugins 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, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-plugins 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 nymea-plugins. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "integrationpluginopenweathermap.h"
#include "plugininfo.h"

#include <QDebug>
#include <QJsonDocument>
#include <QVariantMap>
#include <QUrl>
#include <QUrlQuery>
#include <QDateTime>
#include <QTimeZone>
#include <QSettings>

#include <nymeasettings.h>
#include <integrations/thing.h>
#include <network/networkaccessmanager.h>

IntegrationPluginOpenweathermap::IntegrationPluginOpenweathermap()
{
}

IntegrationPluginOpenweathermap::~IntegrationPluginOpenweathermap()
{
}

void IntegrationPluginOpenweathermap::init()
{
    updateApiKey();

    connect(this, &IntegrationPlugin::configValueChanged, this, &IntegrationPluginOpenweathermap::updateApiKey);
    connect(apiKeyStorage(), &ApiKeyStorage::keyAdded, this, &IntegrationPluginOpenweathermap::updateApiKey);
    connect(apiKeyStorage(), &ApiKeyStorage::keyUpdated, this, &IntegrationPluginOpenweathermap::updateApiKey);
}

void IntegrationPluginOpenweathermap::discoverThings(ThingDiscoveryInfo *info)
{
    QString location = info->params().paramValue(openweathermapDiscoveryLocationParamTypeId).toString();

    // if we have an empty search string, perform an autodetection of the location with the WAN ip...
    if (location.isEmpty()){
        searchAutodetect(info);
    } else {
        search(location, info);
    }
}

void IntegrationPluginOpenweathermap::setupThing(ThingSetupInfo *info)
{
    update(info->thing());

    if (m_apiKey.isEmpty()) {
        info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("No API key for OpenWeatherMap available."));
        return;
    }
    info->finish(Thing::ThingErrorNoError);

    if (!m_pluginTimer) {
        m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(900);
        connect(m_pluginTimer, &PluginTimer::timeout, this, [this](){
            foreach (Thing *thing, myThings()) {
                update(thing);
            }
        });
    }
}

void IntegrationPluginOpenweathermap::executeAction(ThingActionInfo *info)
{
    update(info->thing());
    info->finish(Thing::ThingErrorNoError);
}

void IntegrationPluginOpenweathermap::thingRemoved(Thing *thing)
{
    Q_UNUSED(thing)
    if (myThings().isEmpty()) {
        hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer);
        m_pluginTimer = nullptr;
    }
}

void IntegrationPluginOpenweathermap::updateApiKey()
{
    if (!(m_apiKey = configValue(openWeatherMapPluginCustomApiKeyParamTypeId).toString()).isEmpty()) {
        qCDebug(dcOpenWeatherMap()) << "Using API key from plugin settings.";
    } else if (!(m_apiKey = apiKeyStorage()->requestKey("openweathermap").data("appid")).isEmpty()) {
        qCDebug(dcOpenWeatherMap()) << "Using API key from nymea API keys provider";
    } else {
        qCWarning(dcOpenWeatherMap()) << "No API key set. This plugin might not work correctly.";
        qCWarning(dcOpenWeatherMap()) << "Either install an API key pacakge (nymea-apikeysprovider-plugin-*) or provide a key in the plugin settings.";
    }
}

void IntegrationPluginOpenweathermap::update(Thing *thing)
{
    qCDebug(dcOpenWeatherMap()) << "Refreshing data for" << thing->name();
    QUrl url("http://api.openweathermap.org/data/2.5/weather");
    QUrlQuery query;
    query.addQueryItem("id", thing->paramValue(openweathermapThingIdParamTypeId).toString());
    query.addQueryItem("mode", "json");
    query.addQueryItem("units", "metric");
    query.addQueryItem("appid", m_apiKey);
    url.setQuery(query);

    QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url));
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, thing, [thing, reply](){
        if (reply->error() != QNetworkReply::NoError) {
            qCWarning(dcOpenWeatherMap) << "OpenWeatherMap reply error: " << reply->errorString();
            return;
        }

        QByteArray data = reply->readAll();

        QJsonParseError error;
        QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);

        if (error.error != QJsonParseError::NoError) {
            qCWarning(dcOpenWeatherMap()) << "failed to parse weather data for thing " << thing->name() << "\n" << data << "\n" << error.errorString();
            return;
        }

        qCDebug(dcOpenWeatherMap()) << "Received" << qUtf8Printable(jsonDoc.toJson());

        // http://openweathermap.org/current
        QVariantMap dataMap = jsonDoc.toVariant().toMap();
        if (dataMap.contains("clouds")) {
            int cloudiness = dataMap.value("clouds").toMap().value("all").toInt();
            thing->setStateValue(openweathermapCloudinessStateTypeId, cloudiness);
        }
        if (dataMap.contains("dt")) {
            uint lastUpdate = dataMap.value("dt").toUInt();
            thing->setStateValue(openweathermapUpdateTimeStateTypeId, lastUpdate);
        }

        if (dataMap.contains("main")) {

            double temperatur = dataMap.value("main").toMap().value("temp").toDouble();
            double temperaturMax = dataMap.value("main").toMap().value("temp_max").toDouble();
            double temperaturMin = dataMap.value("main").toMap().value("temp_min").toDouble();
            double pressure = dataMap.value("main").toMap().value("pressure").toDouble();
            int humidity = dataMap.value("main").toMap().value("humidity").toInt();

            thing->setStateValue(openweathermapTemperatureStateTypeId, temperatur);
            thing->setStateValue(openweathermapTemperatureMinStateTypeId, temperaturMin);
            thing->setStateValue(openweathermapTemperatureMaxStateTypeId, temperaturMax);
            thing->setStateValue(openweathermapPressureStateTypeId, pressure);
            thing->setStateValue(openweathermapHumidityStateTypeId, humidity);
        }

        if (dataMap.contains("sys")) {
            qint64 sunrise = dataMap.value("sys").toMap().value("sunrise").toLongLong();
            qint64 sunset = dataMap.value("sys").toMap().value("sunset").toLongLong();

            thing->setStateValue(openweathermapSunriseTimeStateTypeId, sunrise);
            thing->setStateValue(openweathermapSunsetTimeStateTypeId, sunset);
            QTimeZone tz = QTimeZone(QTimeZone::systemTimeZoneId());
            QDateTime up = QDateTime::fromMSecsSinceEpoch(sunrise * 1000);
            QDateTime down = QDateTime::fromMSecsSinceEpoch(sunset * 1000);
            QDateTime now = QDateTime::currentDateTime().toTimeZone(tz);
            thing->setStateValue(openweathermapDaylightStateTypeId, up < now && down > now);
        }

        if (dataMap.contains("visibility")) {
            int visibility = dataMap.value("visibility").toInt();
            thing->setStateValue(openweathermapVisibilityStateTypeId, visibility);
        }

        // http://openweathermap.org/weather-conditions
        if (dataMap.contains("weather") && dataMap.value("weather").toList().count() > 0) {
            int conditionId = dataMap.value("weather").toList().first().toMap().value("id").toInt();
            if (conditionId == 800) {
                if (thing->stateValue(openweathermapUpdateTimeStateTypeId).toInt() > thing->stateValue(openweathermapSunriseTimeStateTypeId).toInt() &&
                    thing->stateValue(openweathermapUpdateTimeStateTypeId).toInt() < thing->stateValue(openweathermapSunsetTimeStateTypeId).toInt()) {
                    thing->setStateValue(openweathermapWeatherConditionStateTypeId, "clear-day");
                } else {
                    thing->setStateValue(openweathermapWeatherConditionStateTypeId, "clear-night");
                }
            } else if (conditionId == 801) {
                if (thing->stateValue(openweathermapUpdateTimeStateTypeId).toInt() > thing->stateValue(openweathermapSunriseTimeStateTypeId).toInt() &&
                    thing->stateValue(openweathermapUpdateTimeStateTypeId).toInt() < thing->stateValue(openweathermapSunsetTimeStateTypeId).toInt()) {
                    thing->setStateValue(openweathermapWeatherConditionStateTypeId, "few-clouds-day");
                } else {
                    thing->setStateValue(openweathermapWeatherConditionStateTypeId, "few-clouds-night");
                }
            } else if (conditionId == 802) {
                thing->setStateValue(openweathermapWeatherConditionStateTypeId, "clouds");
            } else if (conditionId >= 803 && conditionId < 900) {
                thing->setStateValue(openweathermapWeatherConditionStateTypeId, "overcast");
            } else if (conditionId >= 300 && conditionId < 400) {
                thing->setStateValue(openweathermapWeatherConditionStateTypeId, "light-rain");
            } else if (conditionId >= 500 && conditionId < 600) {
                thing->setStateValue(openweathermapWeatherConditionStateTypeId, "shower-rain");
            } else if (conditionId >= 200 && conditionId < 300) {
                thing->setStateValue(openweathermapWeatherConditionStateTypeId, "thunderstorm");
            } else if (conditionId >= 600 && conditionId < 700) {
                thing->setStateValue(openweathermapWeatherConditionStateTypeId, "snow");
            } else if (conditionId >= 700 && conditionId < 800) {
                thing->setStateValue(openweathermapWeatherConditionStateTypeId, "fog");
            }
            QString description = dataMap.value("weather").toList().first().toMap().value("description").toString();
            thing->setStateValue(openweathermapWeatherDescriptionStateTypeId, description);
        }

        if (dataMap.contains("wind")) {
            int windDirection = dataMap.value("wind").toMap().value("deg").toInt();
            double windSpeed = dataMap.value("wind").toMap().value("speed").toDouble();

            thing->setStateValue(openweathermapWindDirectionStateTypeId, windDirection);
            thing->setStateValue(openweathermapWindSpeedStateTypeId, windSpeed);
        }
    });
}

void IntegrationPluginOpenweathermap::searchAutodetect(ThingDiscoveryInfo *info)
{
    QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(QUrl("http://ip-api.com/json")));
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, info, [this, info, reply](){
        if (reply->error()) {
            qCWarning(dcOpenWeatherMap) << "OpenWeatherMap reply error: " << reply->errorString();
            info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error detecting current location."));
            return;
        }
        QByteArray data = reply->readAll();
        QJsonParseError error;
        QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);

        if(error.error != QJsonParseError::NoError) {
            qCWarning(dcOpenWeatherMap) << "failed to parse data" << data << ":" << error.errorString();
            info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Received unexpected data detecting current location."));
            return;
        }

        // search by geographic coordinates
        QVariantMap dataMap = jsonDoc.toVariant().toMap();
        QString country = dataMap.value("countryCode").toString();
        QString cityName = dataMap.value("city").toString();
        QHostAddress wanIp = QHostAddress(dataMap.value("query").toString());
        double longitude = dataMap.value("lon").toDouble();
        double latitude = dataMap.value("lat").toDouble();

        qCDebug(dcOpenWeatherMap) << "----------------------------------------";
        qCDebug(dcOpenWeatherMap) << "Autodetection of location: ";
        qCDebug(dcOpenWeatherMap) << "----------------------------------------";
        qCDebug(dcOpenWeatherMap) << "       name:" << cityName;
        qCDebug(dcOpenWeatherMap) << "    country:" << country;
        qCDebug(dcOpenWeatherMap) << "     WAN IP:" << wanIp.toString();
        qCDebug(dcOpenWeatherMap) << "   latitude:" << latitude;
        qCDebug(dcOpenWeatherMap) << "  longitude:" << longitude;
        qCDebug(dcOpenWeatherMap) << "----------------------------------------";
        searchGeoLocation(latitude, longitude, country, info);

    });
}

void IntegrationPluginOpenweathermap::search(QString searchString, ThingDiscoveryInfo *info)
{
    QUrl url("http://api.openweathermap.org/data/2.5/find");
    QUrlQuery query;
    query.addQueryItem("q", searchString);
    query.addQueryItem("type", "like");
    query.addQueryItem("mode", "json");
    query.addQueryItem("units", "metric");
    query.addQueryItem("appid", m_apiKey);
    url.setQuery(query);

    QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url));
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);

    connect(reply, &QNetworkReply::finished, info, [this, info, reply](){
        if (reply->error()) {
            qCWarning(dcOpenWeatherMap) << "OpenWeatherMap reply error: " << reply->errorString();
            info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error searching for weather stations."));
            return;
        }
        QByteArray data = reply->readAll();
        QJsonParseError error;
        QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);

        if(error.error != QJsonParseError::NoError) {
            qCWarning(dcOpenWeatherMap) << "failed to parse data" << data << ":" << error.errorString();
            info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Received unexpected data while searching for weather stations."));
            return;
        }

        QVariantMap dataMap = jsonDoc.toVariant().toMap();
        QList<QVariantMap> cityList;
        if (dataMap.contains("list")) {
            QVariantList list = dataMap.value("list").toList();
            foreach (QVariant key, list) {
                QVariantMap elemant = key.toMap();
                QVariantMap city;
                city.insert("name",elemant.value("name").toString());
                city.insert("country", elemant.value("sys").toMap().value("country").toString());
                city.insert("id",elemant.value("id").toString());
                cityList.append(city);
            }
        }
        processSearchResults(cityList, info);

    });
}

void IntegrationPluginOpenweathermap::searchGeoLocation(double lat, double lon, const QString &country, ThingDiscoveryInfo *info)
{
    QUrl url("http://api.openweathermap.org/data/2.5/find");
    QUrlQuery query;
    query.addQueryItem("lat", QString::number(lat));
    query.addQueryItem("lon", QString::number(lon));
    query.addQueryItem("cnt", QString::number(3)); // 3 km radius
    query.addQueryItem("type", "like");
    query.addQueryItem("mode", "json");
    query.addQueryItem("units", "metric");
    query.addQueryItem("appid", m_apiKey);
    url.setQuery(query);

    QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url));
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, info, [this, info, reply, country](){
        if (reply->error()) {
            qCWarning(dcOpenWeatherMap) << "OpenWeatherMap reply error: " << reply->errorString();
            info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error searching for weather stations in current location."));
            return;
        }
        QByteArray data = reply->readAll();
        QJsonParseError error;
        QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);

        if(error.error != QJsonParseError::NoError) {
            qCWarning(dcOpenWeatherMap) << "failed to parse data" << data << ":" << error.errorString();
            info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Received unexpected data while searching for weather stations."));
            return;
        }

        QVariantMap dataMap = jsonDoc.toVariant().toMap();
        QList<QVariantMap> cityList;
        if (dataMap.contains("list")) {
            QVariantList list = dataMap.value("list").toList();
            foreach (QVariant key, list) {
                QVariantMap elemant = key.toMap();
                QVariantMap city;
                city.insert("name",elemant.value("name").toString());
                if(elemant.value("sys").toMap().value("country").toString().isEmpty()){
                    city.insert("country",country);
                }else{
                    city.insert("country", elemant.value("sys").toMap().value("country").toString());
                }
                city.insert("id",elemant.value("id").toString());
                cityList.append(city);
            }
        }
        processSearchResults(cityList, info);
    });
}

void IntegrationPluginOpenweathermap::processSearchResults(const QList<QVariantMap> &cityList, ThingDiscoveryInfo *info)
{
    foreach (QVariantMap element, cityList) {
        ThingDescriptor descriptor(openweathermapThingClassId, element.value("name").toString(), element.value("country").toString());
        ParamList params;
        Param nameParam(openweathermapThingNameParamTypeId, element.value("name"));
        params.append(nameParam);
        Param countryParam(openweathermapThingCountryParamTypeId, element.value("country"));
        params.append(countryParam);
        Param idParam(openweathermapThingIdParamTypeId, element.value("id"));
        params.append(idParam);
        descriptor.setParams(params);
        foreach (Thing *existingThing, myThings()) {
            if (existingThing->paramValue(openweathermapThingIdParamTypeId).toString() == element.value("id")) {
                descriptor.setThingId(existingThing->id());
                break;
            }
        }
        info->addThingDescriptor(descriptor);
    }
    info->finish(Thing::ThingErrorNoError);
}


