#include "freertos/portable.h"
#include "alarm.h"
#include "Times.h"
#include <HTTPClient.h>
#include <iostream>
#include <ArduinoJson.h>

class ServerConnector
{
private:
  AlarmStatus *status;
  long lastUpdate;
  // WiFiClient wifiClient;
  // CardReader *_cardReader;
  WiegandReader *_cardReader;

  const String AUTHORIZED_ENTRANCE = "AUTHORIZE_ENTRANCE";
  const String ServerAddress = "alarm.int.francelsoft.com";
  // const String ServerAddress = "10.88.88.169:9003";
  const String EVEN_TYPE_FIRED = "Fired";
  const String EVENT_TYPE_EVENT_UPDATE = "EventUpdate";
  const String EVENT_TYPE_UPDATE = "Update";

  const String FIELD_DISARM = "disarm";
  const String FIELD_ALLOWED_CARDS = "cards";

  int failedAttempts;
  bool running;

  // long lastHeapSize=0;
  // long initHeapSize=0;

  void HandTask()
  {
    // if(initHeapSize == 0)
    // {
    //   initHeapSize=xPortGetFreeHeapSize();
    // }
    // long currentHeapSize=0;
    running = true;
    for (;;)
    {
      // every 15 seconds ask for update: DISSARM & CARDS
      unsigned long currentMillis = millis();
      if (lastUpdate + FromSeconds(15) < currentMillis)
      {
        lastUpdate = currentMillis;
        // currentHeapSize= xPortGetFreeHeapSize();
        // if(abs(currentHeapSize - lastHeapSize) > 32)
        // {
        //   Serial.print("[MEMORY] xPortGetFreeHeapSize: "); Serial.print(currentHeapSize);
        //   Serial.print( "| Total Variation: "); Serial.print(currentHeapSize - initHeapSize);
        //   Serial.print(" | Variation: "); Serial.println(currentHeapSize - lastHeapSize);
        //   lastHeapSize=currentHeapSize;
        // }
          

        if(failedAttempts == 5)
        {
          Serial.println("[HTTP] 5 fail connections, restarting thread.");
          failedAttempts=6;
          running=false;
          vTaskDelete(NULL);
          return;
        }
        
        if(!CheckWifiConnection()){
          Serial.println("[HTTP] could not recconect, trying later");
          continue;
        }
        RequestUpdate();
      }

      if (!status->sendNotif && !status->isFired)
      {
        vTaskDelay(100 / portTICK_PERIOD_MS);
        continue;
      }
      if (status->sendNotif)
      {
        status->eventId = currentMillis;
      }
      SendAlarm();
      status->sendNotif = false;
      vTaskDelay(FromSeconds(3) / portTICK_PERIOD_MS);
    }
  }

  bool CheckWifiConnection()
  {

    if(failedAttempts >= 10)
    {
      Serial.println("[HTTP] Too many faliures, restarting system");
      ESP.restart();
    }

    if(WiFi.status() == WL_CONNECTED){
      return true;
    }
    
    Serial.println("[HTTP] Reconnecting to WiFi...");
    WiFi.disconnect();
    WiFi.reconnect();
    return (WiFi.status() == WL_CONNECTED);
    
  }

  void SendAlarm()
  {
    String eventtype = status->sendNotif ? EVEN_TYPE_FIRED : EVENT_TYPE_EVENT_UPDATE;
    String url = "http://" + ServerAddress + "/?eventId=" + String(status->eventId) + "&eventType=" + eventtype + "&eventData=t";
    Serial.println("[HTTP] Contacting Alarm Server: " + url);
    String response = MakeHttpCall(url);
    if (response == "")
    {
      Serial.println("[HTTP] Alarm Server not recheable");
      return;
    }

    if (response == AUTHORIZED_ENTRANCE)
    {
      // Todo Extract to Disarm method
      Serial.println("[HTTP] Entrace authorized by server.");
      status->isFired = false;
      status->isArmed = status->doorStatus == DOOR_CLOSED;
    }
  }

  void RequestUpdate()
  {
    StaticJsonDocument<200> docStatus;
    docStatus["doorStatus"] = status->doorStatus;
    docStatus["armed"] = status->isArmed;
    docStatus["fireing"] = status->isFired;
    docStatus["lastUser"] = status->userAllowed;
    docStatus["lastEntrance"] = status->lastEntrance;
    docStatus["millis"] = millis();

    String jsonString;
    serializeJson(docStatus, jsonString);
    
    String encodedJson = standardUrlEncode(jsonString);

    String url = "http://" + ServerAddress + "/?eventId=0&eventType=" + EVENT_TYPE_UPDATE + "&eventData=" + encodedJson;

    // Serial.println(url);
    

    String response = MakeHttpCall(url);
    if (response == "")
    {
      Serial.println("[HTTP] Update Server not recheable");
      return;
    }

    JsonDocument doc;
    deserializeJson(doc, response);

    DeserializationError error = deserializeJson(doc, response);

    if (error)
    {
      Serial.print("[HTTP] Serialize error: ");
      Serial.println(error.c_str());
      return;
    }

    if (doc[FIELD_DISARM].is<bool>() && doc[FIELD_DISARM])
    {
      // disarm requested
      //  Todo Extract to Disarm method, and add to alarm.h
      //  same in card reader
      Serial.println("[HTTP] Disarm request by server");
      disarm(status);
    }

    // Serial.print(FIELD_ALLOWED_CARDS);
    int cardsNo = doc[FIELD_ALLOWED_CARDS].size();

    for (int i = 0; i < cardsNo; i++)
    {
      _cardReader->AddUser(doc[FIELD_ALLOWED_CARDS][i]["id"], doc[FIELD_ALLOWED_CARDS][i]["name"]);
    }
  }

  String MakeHttpCall(String url)
  {   
    HTTPClient http;
    http.setReuse(false);

    // if (!http.begin(wifiClient, url))
    if (!http.begin(url))
    { 
      failedAttempts= failedAttempts + 1;
      Serial.println("[HTTP] not able to start http call");
      http.end();
      return "";
    }
    
    int httpCode = http.GET();
    // httpCode will be negative on error

    // Serial.printf("[HTTP] wifi.available: %d, connected: %d \r\n", wifiClient.available(), wifiClient.connected());
    // Serial.printf("[HTTP] http.connected: %d \r\n", http.connected());
    

    if (httpCode <= 0)
    {
      failedAttempts= failedAttempts + 1;
      Serial.printf("[HTTP] Failed connetion - code: %d , error: %s\r\n", httpCode, http.errorToString(httpCode).c_str());

      http.end();
      return "";
    }

    // HTTP header has been send and Server response header has been handled
    // Serial.printf("[HTTPS] GET Finished... code: %d\n", httpCode);
    // file found at server
    if (httpCode != HTTP_CODE_OK)
    {
      failedAttempts= failedAttempts + 1;
      Serial.println("[HTTP] Server responded non 200 code");
      http.end();
      return "";
    }

    // print server response payload
    failedAttempts=0;

    String payload = http.getString();
    http.end();
    return payload;
  }

  String standardUrlEncode(const String &str) {
    String encoded = "";
    char c;
    char code0;
    char code1;
    for (size_t i = 0; i < str.length(); i++) {
      c = str.charAt(i);
      // Check if character is unreserved per RFC 3986
      if ((c >= 'A' && c <= 'Z') ||
          (c >= 'a' && c <= 'z') ||
          (c >= '0' && c <= '9') ||
          c == '-' || c == '_' || c == '.' || c == '~') {
        encoded += c;
      } else {
        encoded += '%';
        code0 = (c >> 4) & 0xF;
        code1 = c & 0xF;
        encoded += (code0 < 10) ? char(code0 + '0') : char(code0 - 10 + 'A');
        encoded += (code1 < 10) ? char(code1 + '0') : char(code1 - 10 + 'A');
      }
    }
    return encoded;
  }

public:
  
  ServerConnector(AlarmStatus *statusObj, WiegandReader *cardReader)
  {
    status = statusObj;
    running = false;
    // wifiClient = new WiFiClient();
    _cardReader = cardReader;
    failedAttempts=0;
  }

  bool IsRunning()
  {
    return running;
  }

  void StartNotifierAsync()
  {
    running = true;
    BaseType_t xReturned;
    TaskHandle_t xHandle = NULL;
    xReturned = xTaskCreate(this->asynTask, "sendNotif", 3000, (void *)this, 2, &xHandle);
    failedAttempts=0;
    Serial.print("Start: "); Serial.println(xReturned == pdPASS ? "Pass" : "Not pass");
    Serial.print("Status: "); Serial.println(eTaskGetState(xHandle));
  }

  static void asynTask(void *pvParameter)
  {
    ServerConnector *serverInstance = (ServerConnector *)pvParameter;
    serverInstance->HandTask();
  }
};
