/*

HTTP(s) OTA MODULE

Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>

*/

// -----------------------------------------------------------------------------
// OTA by using Core's HTTP(s) updater
// -----------------------------------------------------------------------------

#if OTA_CLIENT == OTA_CLIENT_HTTPUPDATE

#include <memory>

#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>
#include <Schedule.h>

#include "system.h"
#include "libs/URL.h"
#include "libs/TypeChecks.h"
#include "libs/SecureClientHelpers.h"

#if SECURE_CLIENT != SECURE_CLIENT_NONE

    #if OTA_SECURE_CLIENT_INCLUDE_CA
    #include "static/ota_client_trusted_root_ca.h"
    #else
    #include "static/digicert_evroot_pem.h"
    #define _ota_client_trusted_root_ca _ssl_digicert_ev_root_ca
    #endif

#endif // SECURE_CLIENT != SECURE_CLIENT_NONE

// -----------------------------------------------------------------------------
// Configuration templates
// -----------------------------------------------------------------------------

template <typename T>
void _otaFollowRedirects(const std::true_type&, T& instance) {
    instance.followRedirects(true);
}

template <typename T>
void _otaFollowRedirects(const std::false_type&, T& instance) {
}

template <typename T>
t_httpUpdate_return _otaClientUpdate(const std::true_type&, T& instance, WiFiClient* client, const String& url) {
    return instance.update(*client, url);
}

template <typename T>
t_httpUpdate_return _otaClientUpdate(const std::false_type&, T& instance, WiFiClient*, const String& url) {
    return instance.update(url);
}

namespace ota {
    template <typename T>
    using has_followRedirects_t = decltype(std::declval<T>().followRedirects(std::declval<bool>()));

    template <typename T>
    using has_followRedirects = is_detected<has_followRedirects_t, T>;

    template <typename T>
    using has_WiFiClient_argument_t = decltype(std::declval<T>().update(std::declval<WiFiClient&>(), std::declval<const String&>()));

    template <typename T>
    using has_WiFiClient_argument = is_detected<has_WiFiClient_argument_t, T>;
}

// -----------------------------------------------------------------------------
// Settings helper
// -----------------------------------------------------------------------------

#if SECURE_CLIENT == SECURE_CLIENT_AXTLS
SecureClientConfig _ota_sc_config {
    "OTA",
    []() -> String {
        return String(); // NOTE: unused
    },
    []() -> int {
        return getSetting("otaScCheck", OTA_SECURE_CLIENT_CHECK).toInt();
    },
    []() -> String {
        return getSetting("otaFP", OTA_FINGERPRINT);
    },
    true
};
#endif

#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL
SecureClientConfig _ota_sc_config {
    "OTA",
    []() -> int {
        return getSetting("otaScCheck", OTA_SECURE_CLIENT_CHECK).toInt();
    },
    []() -> PGM_P {
        return _ota_client_trusted_root_ca;
    },
    []() -> String {
        return getSetting("otaFP", OTA_FINGERPRINT);
    },
    []() -> uint16_t {
        return getSetting("otaScMFLN", OTA_SECURE_CLIENT_MFLN).toInt();
    },
    true
};
#endif

// -----------------------------------------------------------------------------
// Generic update methods
// -----------------------------------------------------------------------------

void _otaClientRunUpdater(WiFiClient* client, const String& url, const String& fp = "") {

    UNUSED(client);
    UNUSED(fp);

    // Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade
    eepromRotate(false);

    DEBUG_MSG_P(PSTR("[OTA] Downloading %s ...\n"), url.c_str());

    // TODO: support currentVersion (string arg after 'url')
    // NOTE: ESPhttpUpdate.update(..., fp) will **always** fail with empty fingerprint
    _otaFollowRedirects(ota::has_followRedirects<decltype(ESPhttpUpdate)>{}, ESPhttpUpdate);

    ESPhttpUpdate.rebootOnUpdate(false);
    t_httpUpdate_return result = HTTP_UPDATE_NO_UPDATES;

    // We expect both .update(url, "", String_fp) and .update(url) to survice until axTLS is removed from the Core
    #if (SECURE_CLIENT == SECURE_CLIENT_AXTLS)
        if (url.startsWith("https://")) {
            result = ESPhttpUpdate.update(url, "", fp);
        } else {
            result = ESPhttpUpdate.update(url);
        }
    #else
    // TODO: implement through callbacks?
    //       see https://github.com/esp8266/Arduino/pull/6796
        result = _otaClientUpdate(ota::has_WiFiClient_argument<decltype(ESPhttpUpdate)>{}, ESPhttpUpdate, client, url);
    #endif

    switch (result) {
        case HTTP_UPDATE_FAILED:
            DEBUG_MSG_P(PSTR("[OTA] Update failed (error %d): %s\n"), ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str());
            eepromRotate(true);
            break;
        case HTTP_UPDATE_NO_UPDATES:
            DEBUG_MSG_P(PSTR("[OTA] No updates"));
            eepromRotate(true);
            break;
        case HTTP_UPDATE_OK:
            DEBUG_MSG_P(PSTR("[OTA] Done, restarting..."));
            deferredReset(500, CUSTOM_RESET_OTA); // wait a bit more than usual
            break;
    }

}

void _otaClientFromHttp(const String& url) {
    std::unique_ptr<WiFiClient> client(nullptr);
    if (ota::has_WiFiClient_argument<decltype(ESPhttpUpdate)>{}) {
        client = std::make_unique<WiFiClient>();
    }
    _otaClientRunUpdater(client.get(), url, "");
}

#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL

void _otaClientFromHttps(const String& url) {

    // Check for NTP early to avoid constructing SecureClient prematurely
    const int check = _ota_sc_config.on_check();
    if (!ntpSynced() && (check == SECURE_CLIENT_CHECK_CA)) {
        DEBUG_MSG_P(PSTR("[OTA] Time not synced! Cannot use CA validation\n"));
        return;
    }

    // unique_ptr self-destructs after exiting function scope
    // create the client on heap to use less stack space
    auto client = std::make_unique<SecureClient>(_ota_sc_config);
    if (!client->beforeConnected()) {
        return;
    }

    _otaClientRunUpdater(&client->get(), url);

}

#endif // SECURE_CLIENT_BEARSSL


#if SECURE_CLIENT == SECURE_CLIENT_AXTLS

void _otaClientFromHttps(const String& url) {

    // Note: this being the legacy option, only supporting legacy methods on ESPHttpUpdate itself
    //       no way to know when it is connected, so no afterConnected
    const int check = _ota_sc_config.on_check();
    const String fp_string = _ota_sc_config.on_fingerprint();

    if (check == SECURE_CLIENT_CHECK_FINGERPRINT) {
        if (!fp_string.length() || !sslCheckFingerPrint(fp_string.c_str())) {
            DEBUG_MSG_P(PSTR("[OTA] Wrong fingerprint\n"));
            return;
        }
    }

    _otaClientRunUpdater(nullptr, url, fp_string);

}

#endif // SECURE_CLIENT_AXTLS

void _otaClientFrom(const String& url) {

    if (url.startsWith("http://")) {
        _otaClientFromHttp(url);
        return;
    }

    #if SECURE_CLIENT != SECURE_CLIENT_NONE
        if (url.startsWith("https://")) {
            _otaClientFromHttps(url);
            return;
        }
    #endif

    DEBUG_MSG_P(PSTR("[OTA] Incorrect URL specified\n"));

}

#if TERMINAL_SUPPORT

void _otaClientInitCommands() {

    terminalRegisterCommand(F("OTA"), [](Embedis* e) {
        if (e->argc < 2) {
            terminalError(F("OTA <url>"));
        } else {
            _otaClientFrom(String(e->argv[1]));
            terminalOK();
        }
    });

}

#endif // TERMINAL_SUPPORT

#if (MQTT_SUPPORT && OTA_MQTT_SUPPORT)

bool _ota_do_update = false;

void _otaClientMqttCallback(unsigned int type, const char * topic, const char * payload) {

    if (type == MQTT_CONNECT_EVENT) {
        mqttSubscribe(MQTT_TOPIC_OTA);
    }

    if (type == MQTT_MESSAGE_EVENT) {
        const String t = mqttMagnitude((char *) topic);
        if (!_ota_do_update && t.equals(MQTT_TOPIC_OTA)) {
            DEBUG_MSG_P(PSTR("[OTA] Queuing from URL: %s\n"), payload);
            // TODO: c++14 support is required for `[_payload = String(payload)]() { ... }`
            //       c++11 also supports basic `std::bind(func, arg)`, but we need to reset the lock
            _ota_do_update = true;

            const String _payload(payload);
            schedule_function([_payload]() {
                _otaClientFrom(_payload);
                _ota_do_update = false;
            });
        }
    }

}

#endif // MQTT_SUPPORT

// -----------------------------------------------------------------------------

void otaClientSetup() {

    // Backwards compatibility
    moveSetting("otafp", "otaFP");

    #if TERMINAL_SUPPORT
        _otaClientInitCommands();
    #endif

    #if (MQTT_SUPPORT && OTA_MQTT_SUPPORT)
        mqttRegister(_otaClientMqttCallback);
    #endif

}

#endif // OTA_CLIENT == OTA_CLIENT_HTTPUPDATE
