// SPDX-License-Identifier: GPL-2.0-or-later
#include "Configuration.h"
#include "HttpPowerMeter.h"
#include "MessageOutput.h"
#include <WiFiClientSecure.h>
#include <FirebaseJson.h>
#include <Crypto.h>
#include <SHA256.h>
#include <base64.h>
#include <memory>

void HttpPowerMeterClass::init()
{
}

float HttpPowerMeterClass::getPower(int8_t phase)
{
    return power[phase - 1];
}

bool HttpPowerMeterClass::updateValues()
{
    const CONFIG_T& config = Configuration.get();

    char response[2000],
        errorMessage[256];

    for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
        POWERMETER_HTTP_PHASE_CONFIG_T phaseConfig = config.PowerMeter.Http_Phase[i];

        if (!phaseConfig.Enabled) {
            power[i] = 0.0;
            continue;	
        } 

        if (i == 0 || config.PowerMeter.HttpIndividualRequests) {
            if (httpRequest(phaseConfig.Url, phaseConfig.AuthType, phaseConfig.Username, phaseConfig.Password, phaseConfig.HeaderKey, phaseConfig.HeaderValue, phaseConfig.Timeout,
                response, sizeof(response), errorMessage, sizeof(errorMessage))) {
                  if (!getFloatValueByJsonPath(response, phaseConfig.JsonPath, power[i])) {
                      MessageOutput.printf("[HttpPowerMeter] Couldn't find a value with Json query \"%s\"\r\n", phaseConfig.JsonPath);
                      return false;
                  }
            } else {
                MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d failed. Error: %s\r\n",
                    i + 1, errorMessage);
                return false;
            }
        }
    }

    return true;
}

bool HttpPowerMeterClass::httpRequest(const char* url, Auth authType, const char* username, const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout,
        char* response, size_t responseSize, char* error, size_t errorSize)
{
    String urlProtocol;
    String urlHostname;
    String urlUri;
    extractUrlComponents(url, urlProtocol, urlHostname, urlUri);

    response[0] = '\0';
    error[0] = '\0';

    // secureWifiClient MUST be created before HTTPClient
    // see discussion: https://github.com/helgeerbe/OpenDTU-OnBattery/issues/381
    std::unique_ptr<WiFiClient> wifiClient;

    if (urlProtocol == "https") {
      auto secureWifiClient = std::make_unique<WiFiClientSecure>();
      secureWifiClient->setInsecure();
      wifiClient = std::move(secureWifiClient);
    } else {
      wifiClient = std::make_unique<WiFiClient>();
    }

   
    if (!httpClient.begin(*wifiClient, url)) {
      snprintf_P(error, errorSize, "httpClient.begin(%s) failed", url);
      return false;
    }
    prepareRequest(timeout, httpHeader, httpValue);
    
    if (authType == Auth::digest) {
        const char *headers[1] = {"WWW-Authenticate"};
        httpClient.collectHeaders(headers, 1);
    } else if (authType == Auth::basic) {
        String authString = username;
        authString += ":";
        authString += password;
        String auth = "Basic ";
        auth.concat(base64::encode(authString));
        httpClient.addHeader("Authorization", auth);
    }

    int httpCode = httpClient.GET();
    if (httpCode == HTTP_CODE_UNAUTHORIZED && authType == Auth::digest) {
        // Handle authentication challenge
        char realm[256];  // Buffer to store the realm received from the server
        char nonce[256];  // Buffer to store the nonce received from the server
        if (httpClient.hasHeader("WWW-Authenticate")) {
            String authHeader = httpClient.header("WWW-Authenticate");
            if (authHeader.indexOf("Digest") != -1) {
                int realmIndex = authHeader.indexOf("realm=\"");
                int nonceIndex = authHeader.indexOf("nonce=\"");
                if (realmIndex != -1 && nonceIndex != -1) {
                    int realmEndIndex = authHeader.indexOf("\"", realmIndex + 7);
                    int nonceEndIndex = authHeader.indexOf("\"", nonceIndex + 7);
                    if (realmEndIndex != -1 && nonceEndIndex != -1) {
                        authHeader.substring(realmIndex + 7, realmEndIndex).toCharArray(realm, sizeof(realm));
                        authHeader.substring(nonceIndex + 7, nonceEndIndex).toCharArray(nonce, sizeof(nonce));
                    }
                }
                String cnonce = String(random(1000)); // Generate client nonce
                String str = username;
                str += ":";
                str += realm;
                str += ":";
                str += password;
                String ha1 = sha256(str);
                str = "GET:";
                str += urlUri;
                String ha2 = sha256(str);
                str = ha1;
                str += ":";
                str += nonce;
                str +=  ":00000001:";
                str += cnonce;
                str += ":auth:";
                str +=  ha2;
                String response = sha256(str);

                String authorization = "Digest username=\""; 
                authorization += username;
                authorization += "\", realm=\"";
                authorization += realm;
                authorization += "\", nonce=\"";
                authorization += nonce;
                authorization += "\", uri=\"";
                authorization += urlUri;
                authorization += "\", cnonce=\"";
                authorization += cnonce;
                authorization += "\", nc=00000001, qop=auth, response=\"";
                authorization += response;
                authorization += "\", algorithm=SHA-256";
                httpClient.end();
                if (!httpClient.begin(*wifiClient, url)) {
                    snprintf_P(error, errorSize, "httpClient.begin(%s) for digest auth failed", url);
                    return false;
                }
                prepareRequest(timeout, httpHeader, httpValue);
                httpClient.addHeader("Authorization", authorization);
                httpCode = httpClient.GET();
            }
        }        
    }

    if (httpCode == HTTP_CODE_OK) {
        String responseBody = httpClient.getString();

        if (responseBody.length() > (responseSize - 1)) {
            snprintf_P(error, errorSize, "Response too large! Response length: %d Body start: %s",
                httpClient.getSize(), responseBody.c_str());
        } else {
            snprintf(response, responseSize, responseBody.c_str());
        }
    } else if (httpCode <= 0) {
        snprintf_P(error, errorSize, "Error(%s): %s", url, httpClient.errorToString(httpCode).c_str());
    } else if (httpCode != HTTP_CODE_OK) {
        snprintf_P(error, errorSize, "Bad HTTP code: %d", httpCode);
    }

    httpClient.end();

    if (error[0] != '\0') {
        return false;
    }

    return true;
}

float HttpPowerMeterClass::getFloatValueByJsonPath(const char* jsonString, const char* jsonPath, float& value)
{
    FirebaseJson firebaseJson;
    firebaseJson.setJsonData(jsonString);

    FirebaseJsonData firebaseJsonResult;
    if (!firebaseJson.get(firebaseJsonResult, jsonPath)) {
        return false;
    }

    value = firebaseJsonResult.to<float>();

    firebaseJson.clear();

    return true;
}

 void HttpPowerMeterClass::extractUrlComponents(const String& url, String& protocol, String& hostname, String& uri) {
    // Find protocol delimiter
    int protocolEndIndex = url.indexOf(":");
    if (protocolEndIndex != -1) {
        protocol = url.substring(0, protocolEndIndex);

        // Find double slash delimiter
        int doubleSlashIndex = url.indexOf("//", protocolEndIndex);
        if (doubleSlashIndex != -1) {
            // Find slash after double slash delimiter
            int slashIndex = url.indexOf("/", doubleSlashIndex + 2);
            if (slashIndex != -1) {
                // Extract hostname and uri
                hostname = url.substring(doubleSlashIndex + 2, slashIndex);
                uri = url.substring(slashIndex);
            } else {
                // No slash after double slash delimiter, so the whole remaining part is the hostname
                hostname = url.substring(doubleSlashIndex + 2);
                uri = "/";
            }
        }
    }

    // Remove username:password if present in the hostname
    int atIndex = hostname.indexOf("@");
    if (atIndex != -1) {
        hostname = hostname.substring(atIndex + 1);
    }
}

#define HASH_SIZE 32

String HttpPowerMeterClass::sha256(const String& data) {
  SHA256 sha256;
  uint8_t hash[HASH_SIZE];

  sha256.reset();
  sha256.update(data.c_str(), data.length());
  sha256.finalize(hash, HASH_SIZE);

  String hashStr = "";
  for (int i = 0; i < HASH_SIZE; i++) {
    String hex = String(hash[i], HEX);
    if (hex.length() == 1) {
      hashStr += "0";
    }
    hashStr += hex;
  }

  return hashStr;
}

void HttpPowerMeterClass::prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue) {
    httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
    httpClient.setUserAgent("OpenDTU-OnBattery");
    httpClient.setConnectTimeout(timeout);
    httpClient.setTimeout(timeout);
    httpClient.addHeader("Content-Type", "application/json");
    httpClient.addHeader("Accept", "application/json");

    if (strlen(httpHeader) > 0) {
        httpClient.addHeader(httpHeader, httpValue);
    }
}

HttpPowerMeterClass HttpPowerMeter;
