#include <Arduino.h>
#include <WiFi.h>
#include <WiFiAP.h>
#include <Thread.h> // https://github.com/ivanseidel/ArduinoThread
#include <ThreadController.h>
#include <AsyncMqttClient.h>
#include <Ticker.h>
#include "Streaming.h"
#include "FileOperations.h"
#include <AsyncTCP.h>
#include "ESPAsyncWebServer.h"
#include <ArduinoJson.h>
#include <ESP32Time.h>
#include <Wire.h>
#include "mbedtls/aes.h"
#include <AsyncElegantOTA.h>
#include "PCF85063A.h"
#include "FFG3105.h"
#include "main.h"
#include <NTPClient.h>

#define MQTT_BROKER
// #define TEST_SENSOR

const char *host = "oupdate";
const char *version = "H3V_1.0.12";
const char *ttime;

AsyncWebServer server(80);

#define QOS1 1
#define QOS2 2
#define QOS3 3
#define OPEN_TIME 500

#define DEBUG

// #define SLIM
// #define SLEEPLESS
#define OLOCK
// #define PERIODIC_WAKEUP
#define CLONE_FREE
#define USE_RTC

// #define OFFLINE // Like Tarsis - wakeup on update-sleep card and sends logs

// #define INVERSE_MOTOR
#define RFID_ONLY // A retrofit device without a fingerprint sensor

#define FACTORY_RESET 39 // needs to be defined
#define EVENT_TYPE "access"

#define DND 36        // privacy — GPIO 36 has no internal pull-up, requires external 10kΩ to 3.3V
#define DND_ENABLED 0 // set to 1 when DND switch is wired correctly
#ifdef OLOCK
#define ZUMER 22
#define MOTOR_ENABLE 27
#define MOTOR_DIRECTION 12
#define MOTOR_DELAY 140
// #define RELAY 27 // door open
// #define BUTTON 36 // ?
#define PROXIMITY 34
#define LED_RED 21
#define LED_GREEN 4
#define FORMAT_SPIFFS_IF_FAILED true
#define ENABLE_PERIPHERY 25
#define DEVICE_TYPE "Retro"
#endif
#define BATTERY_IN 35     // battery measurements
#define BATTERY_ENABLE 26 // enable measurement
#define BATTERY_TRIGGER 2300
#define NFC_RESET 17

#define SOUND_OPEN 100
#define CONNFIG_FILE "/configjson.txt"
#define USERS_FILE "/users.txt"
#define AWAKE_TIME 5000               // 2 sec
#define HOLD_TIME_FACTORY_RESET 10000 // 10 sec
#define WIFI_TIME 10000               // 5 sec
#define BUFF_FILE "/buffer.txt"
#define UPDATE_TIME 300000 // 5 minutes
#define TTP_WAIT_TIME 3000 // 3 sec

#define PARAM_AUTOLEARN true
#define PARAM_DUPLICATION_CHECK false
#define PARAM_SECURITY_LEVEL 1
// #define SDA_PROXY 22
// #define SCL_PROXY 21

// --- RFID
#include <SPI.h>
#include <MFRC522.h>
// #define RST_PIN 14 // Configurable, see typical pin layout above
#define RST_PIN 17 // Configurable, RETROFIT

// #define RST_PIN 35                // New hardware
#define SS_PIN 5 // Configurable, see typical pin layout above
// #define IRQ_PIN 2                 // Configurable, depends on hardware
MFRC522 mfrc522(SS_PIN, RST_PIN); // Create MFRC522 instance.
MFRC522::MIFARE_Key key;
MFRC522::StatusCode status;
// Init array that will store new NUID
byte nuidPICC[4];
byte card_block1[18];

byte card_block2[18];
byte card_block4[18];
byte card_block5[18];
byte card_block6[18];
// byte card_block4[16];
// byte card_block5[16];
//  --- end RFID
//  TIME_TO_LIVE
RTC_DATA_ATTR int bootCount = 0;
RTC_DATA_ATTR int time_for_sleep = 60; // 1 minute
// RTC_DATA_ATTR uint64_t cards[100];
// RTC_DATA_ATTR uint64_t users[100];

WiFiClient espClient;
AsyncMqttClient mqttClient;
Ticker mqttReconnectTimer;
IPAddress IP;

ThreadController threadControl = ThreadController();
Thread thread = Thread();
FileOperations filesys;

char MAC_char[18];
uint8_t MAC_array[6];
// const char *mymac = "240ac40457d0";
const char *type = DEVICE_TYPE;
char clientName[20] = "Olock_240ac40457d0";
char out_message_buffer[2048];
char operations_buffer[2048];
const String s = "";
uint32_t beginSound = 0;
uint32_t startRelay = 0;
uint16_t relay_count = 1000;
uint16_t sound_count = 200;
uint64_t awake_timer = 0; // awake state
uint16_t battery = 0;

bool factory_mode = false;
bool insomnia = false; // newer sleep
bool door_opened = false;
bool open_once = false;
String topic_late;
String message_late;
String error_late = "unknown";
long timeMeasure;
bool ready_to_sleep = false;

MyTools tools = MyTools();
bool master_access = false;
bool awake_update = false;

String ledState;

void lowBatteryAlert();

// CONFIG PARAMETERS
const char *PARAM_HOTEL = "hotel";
const char *PARAM_NAME = "name";
const char *PARAM_SSID = "ssid";
const char *PARAM_PASS = "pass";
const char *PARAM_AP_SSID = "ap_ssid";
const char *PARAM_AP_PASS = "ap_pass";
const char *PARAM_MQTT = "mqtt";
const char *PARAM_MQTT_PORT = "mqtt_port";
const char *PARAM_MQTT_USER = "mqtt_user";
const char *PARAM_MQTT_USER_PASS = "mqtt_user_pass";
const char *PARAM_IP = "ip_addres";
const char *PARAM_MASK = "mask";
const char *PARAM_GATEWAY = "gateway";
const char *PARAM_DHCP = "dhcp";
const char *PARAM_TYPE = "type";
const char *PARAM_RELAY = "relay_time";
const char *PARAM_EVENT = "event_type";
const char *PARAM_SOUND = "sound";
const char *PARAM_GROUP = "group";
const char *PARAM_ROOM = "room";
const char *PARAM_HOUR = "hour";
const char *PARAM_MINUTES = "minutes";
const char *PARAM_YEAR = "year";
const char *PARAM_MONTH = "month";
const char *PARAM_DAY = "day";

struct Config
{
  uint16_t hotel_code;
  uint16_t room;
  uint64_t group; //? from String
  uint16_t guest_count;
  uint16_t staff_count;
  uint16_t master_count;
  char aes_key[17];
  char name[16];
  char ssid[30];
  char pass[30];
  char mqtt[30];
  char mqtt_user[16];
  char mqtt_user_pass[16];
  int mqtt_port;
  int dhcp;
  char ip_address[16];
  char ip_mask[16];
  char gateway[16];
  char type[16];
  int relay_time = 3;
  char event[16];
  int sound;
  char apssid[16];
  char appass[16];
  char update_code[17]; // code for activating update card
  uint8_t today;        // getting time once a day after event or timer wakeup
};

RTC_DATA_ATTR Config config;

RTC_DATA_ATTR uint64_t access_late_user;
RTC_DATA_ATTR uint64_t access_late_card;
RTC_DATA_ATTR bool to_send_late;

RTC_DATA_ATTR uint64_t sleep_time;

// WiFi Static config
// Set your Static IP address
IPAddress local_IP(192, 168, 1, 14);
// Set your Gateway IP address
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
IPAddress primaryDNS(8, 8, 8, 8);   // optional
IPAddress secondaryDNS(8, 8, 4, 4); // optional

// TIME
// ESP32Time time_clock;

PCF85063A rtc;
FFG3105 bat_measure;
// NTP
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);

//  ######   ########  ####  #######
// ##    ##  ##     ##  ##  ##     ##
// ##        ##     ##  ##  ##     ##
// ##   #### ########   ##  ##     ##
// ##    ##  ##         ##  ##     ##
// ##    ##  ##         ##  ##     ##
//  ######   ##        ####  #######

void initGPIO()
{

  pinMode(DND, INPUT_PULLUP);
  pinMode(ZUMER, OUTPUT);
  pinMode(NFC_RESET, OUTPUT);

  pinMode(MOTOR_DIRECTION, OUTPUT);
  pinMode(MOTOR_ENABLE, OUTPUT);
  digitalWrite(ENABLE_PERIPHERY, LOW); // enable periphery
  pinMode(ENABLE_PERIPHERY, OUTPUT);

  pinMode(PROXIMITY, INPUT);
  pinMode(LED_RED, OUTPUT);
  pinMode(LED_GREEN, OUTPUT);
  pinMode(FACTORY_RESET, INPUT);
  pinMode(BATTERY_IN, ANALOG);
  pinMode(BATTERY_ENABLE, OUTPUT);
  digitalWrite(BATTERY_ENABLE, HIGH);  // enable measuring
  digitalWrite(ENABLE_PERIPHERY, LOW); // enable periphery
  digitalWrite(MOTOR_ENABLE, 0);
  digitalWrite(NFC_RESET, LOW);
}

void ensureRtcTime()
{
  int year = rtc.getYear();
  int month = rtc.getMonth();
  int day = rtc.getDay();
  int hour = rtc.getHour();
  int minute = rtc.getMinute();
  int second = rtc.getSecond();

  bool invalid = (year < 2024 || year > 2099 || month < 1 || month > 12 || day < 1 || day > 31 ||
                  hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59);
  if (invalid)
  {
    rtc.setDate(0, 1, 1, 2024);
    rtc.setTime(0, 0, 0);
  }
}

/*
 ######  ######## ######## ##     ## ########
##    ## ##          ##    ##     ## ##     ##
##       ##          ##    ##     ## ##     ##
 ######  ######      ##    ##     ## ########
      ## ##          ##    ##     ## ##
##    ## ##          ##    ##     ## ##
 ######  ########    ##     #######  ##
*/

/**
 * @brief SETUP
 */
void setup()
{
#ifdef SLEEPLESS
  insomnia = true;
#endif
  awake_timer = millis();

  // SET PINS
  initGPIO();
  setupSound();
  //
  // Serial << "Test:" << bat_measure.test() << endl;

  // bat_measure.wake();
  // bat_measure.measuring();

  esp_sleep_enable_ext0_wakeup(GPIO_NUM_34, 1); // wakeup on proximity enable

  // Init serial ports
  Serial.begin(115200);

  ensureRtcTime();

  // Init buffer
  for (byte i = 0; i < 6; i++)
  {
    key.keyByte[i] = 0xFF;
  }

  // startSound();
  // delay(30);
  // stopSound();
  // rtc.setTime(6, 54, 00); // 24H mode, ex. 6:54:00
  // //  setDate(weekday, day, month, yr);
  // rtc.setDate(6, 16, 5, 2024);

  char stime[22];
  sprintf(stime, "%02d-%02d-%d %02d:%02d:%02d", rtc.getDay(), rtc.getMonth(), rtc.getYear(), rtc.getHour(), rtc.getMinute(), rtc.getSecond());
  Serial << stime << endl;

  // Serial << "Status:" << bat_measure.readStatus() << endl;
  // Serial << "Battery:" << bat_measure.readVoltage() << endl;
  // Serial << "Temperature:" << bat_measure.readTemperature() << endl;
  // WAKEUP REASON
  int w_reason = print_wakeup_reason();

  if (w_reason == 2 /*on proxy*/ && digitalRead(PROXIMITY) == 0) // wakeup on transition to low
  {
    Serial << "Proximity transition to LOW detected. Sleep again with the correct wakeup polarity." << endl;
    goSleep();
    return;
  }

  time_t t;
  time(&t);

  if (w_reason == 4 /*on timer*/ && sleep_time != 0 && /*(sleep_time < t) &&*/ (sleep_time < t - 20 && sleep_time > t - 30)) // wakeup on 25 seconds timer
  {
    Serial << "Wakeup on 25 seconds timer. Sleep again with the correct wakeup polarity." << endl;
    if (digitalRead(PROXIMITY) == 0)
    {
      Serial << "Proximity is LOW. Sleep again with the correct wakeup polarity." << endl;
      if ((config.today == 0) || (config.today != rtc.getDay())) /// ONCE A DAY
      {
        uint64_t tm = time_for_sleep * 1000000; // timer in microseconds
        esp_sleep_enable_timer_wakeup(tm);
      }
    }
    else
    {
      Serial << "Proximity is still HIGH. Wait some more time." << endl;
    }
    goSleep();
    return;
  }

  if (w_reason == 0)
  {
    // Wakeup on reset
    battery = tools.calc_battery_percentage(analogRead(BATTERY_IN));
    digitalWrite(ENABLE_PERIPHERY, HIGH); // enable periphery
    digitalWrite(NFC_RESET, HIGH);

    startSound();
    delay(30);
    stopSound();
    wakeup_on_reset();
  }
  else if (w_reason == 4)
  {
    digitalWrite(ENABLE_PERIPHERY, LOW); // disable periphery
    wakeup_by_time();
  }
  else if (w_reason == 2)
  {
    // Wakeup on proxy
    battery = tools.calc_battery_percentage(analogRead(BATTERY_IN));
    digitalWrite(ENABLE_PERIPHERY, HIGH); // enable periphery
    digitalWrite(NFC_RESET, HIGH);

    digitalWrite(LED_RED, HIGH); // on proxy
    initSPI();
    if ((config.today == 0) || (config.today != rtc.getDay())) /// ONCE A DAY
    {
      uint64_t tm = time_for_sleep * 1000000; // timer in microseconds
      esp_sleep_enable_timer_wakeup(tm);
    }
    wakeup_by_proxy();
  }
  awake_timer = millis();
  //  STOP HERE

  Serial << "Version: " << version << endl;
}

/**
 * @brief On subscription
 *
 * @param packetId
 * @param qos
 */
void onMqttSubscribe(uint16_t packetId, uint8_t qos)
{
  Serial.println(F("Subscribe acknowledged."));
  // Serial.print(F("  packetId: "));
  // Serial.println(packetId);
  // Serial.print(F("  qos: "));
  // Serial.println(qos);
}

/**
 * @brief For time measurements
 *
 */
void measureTime()
{
  Serial.print(F("Response time:"));
  Serial.println(millis() - timeMeasure);
}

/**
 * @brief  MQTT message receiver
 *
 * @param topic
 * @param payload
 * @param properties
 * @param len
 * @param index
 * @param total
 */
void onMqttMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total)
{
  timeMeasure = millis();
  awake_timer = millis(); // wait after each message before sleep
  Serial.print(F("Received: "));
  // mqttClient.disconnect();
  // mqttClient.unsubscribe(mymac);
  String message = "";
  for (size_t i = 0; i < len; i++)
  {
    message += payload[i];
  }

  Serial << topic << ":" << message << endl;
  // Serial << "len:" << len << " index:" << index << " total:" << total << endl;
  topic_late = topic;
  message_late = message;

  // mqttClient.subscribe(mymac, 1);
  //  connectBroker();
  // confirmOkReceiving();
}

/**
 * @brief Connect to MQTT brocker
 *
 */
void connectBroker()
{
  if (mqttClient.connected())
    return;
  uint8_t tries = 15;
  mqttClient.onConnect(onMqttConnect);
  mqttClient.onDisconnect(onMqttDisconnect);
  mqttClient.onSubscribe(onMqttSubscribe);
  mqttClient.onUnsubscribe(onMqttUnsubscribe);
  mqttClient.onPublish(onMqttPublish);
  // mqttClient.setServer(MQTT_HOST, MQTT_PORT);

  mqttClient.setCleanSession(true);
  mqttClient.setClientId(clientName);
  mqttClient.setCredentials(config.mqtt_user, config.mqtt_user_pass);
  mqttClient.setServer(config.mqtt, config.mqtt_port);
  char will[30];
  sprintf(will, "olock/disconnect/%s/", MAC_char);
  mqttClient.setWill("system", 0, false, will, 30);
  mqttClient.onMessage(onMqttMessage);

  while (!mqttClient.connected())
  {
    relayLoop();
    cardLoop();
    mqttClient.connect();
    delay(200);
    Serial << "." << endl;
    tries--;
    if (tries < 1)
      break;
  }
  awake_timer = millis();
  mqttClient.subscribe(MAC_char, 2);
  if (!mqttClient.connected())
  {
    delay(100);
    goSleep();
  }
}

/**
 * @brief  RESPONSE ALIVE
 */
void responseAlive()
{
  String message(MAC_char);
  message += "/";
  message += type;
  message += "/";
  message += IP.toString().c_str();
  message += "/";
  message += version;
  message += "/";
  message += config.name;
  message += config.room;
  message += "/";
  message += tools.calc_battery_percentage(analogRead(BATTERY_IN));
  message += "/";
  message += WiFi.RSSI();
  message += "/";
  char stime[22];
  sprintf(stime, "%02d-%02d-%d %02d:%02d:%02d", rtc.getDay(), rtc.getMonth(), rtc.getYear(), rtc.getHour(), rtc.getMinute(), rtc.getSecond());
  message += stime;
  //
  // measure battery

  // int len = message.length() + 1;
  // char msg[len];
  // message.toCharArray(msg, len);
  // mqttClient.publish("alive", 1, false, msg, len);
  if (awake_update)
  {
    mqttClient.publish("update", 1, false, message.c_str());
  }
  else
  {
    mqttClient.publish("alive", 1, false, message.c_str());
  }
}

/**
 * @brief  Set relay in open state. Closed by main loop=
 *
 * @param time
 */
void open_relay(word time)
{
#ifdef SLIM
  digitalWrite(RELAY, 1);
#endif

#ifdef OLOCK
  digitalWrite(MOTOR_DIRECTION, 1); // TODO chose direction
#ifdef INVERSE_MOTOR
  digitalWrite(MOTOR_DIRECTION, 0); // TODO chose direction
#endif
  digitalWrite(MOTOR_ENABLE, 1); // start motor
  delay(MOTOR_DELAY);
  digitalWrite(MOTOR_ENABLE, 0);

  digitalWrite(MOTOR_DIRECTION, 0);
#ifdef INVERSE_MOTOR
  digitalWrite(MOTOR_DIRECTION, 1); // TODO chose direction
#endif
#endif

  digitalWrite(LED_GREEN, 1);
  startRelay = millis() + time;
}

/**
 * @brief Start sound. Sound should be set off from main loop.
 *
 * @param time
 */
void zumerMillis(byte time)
{
  // digitalWrite(ZUMER, 1);
  startSound();
  beginSound = millis() + time;
}

/**
 * @brief Indicate errors
 *
 */
void errorSound()
{
  int interval = 50;
#ifdef RFID_ONLY
  interval = 20;
#endif
  for (uint8_t i = 0; i < 5; i++)
  {
    // digitalWrite(ZUMER, 1);
    startSound();
    digitalWrite(LED_RED, 1);
    delay(interval);
    // digitalWrite(ZUMER, 0);
    stopSound();
    digitalWrite(LED_RED, 0);
    delay(interval);
  }
}

/**
 * @brief Time for sleep in seconds
 *
 * @param sl_time
 */
void goSleep()
{
  if (door_opened)
  {
    closedoor();
  }
  espClient.stop();
  mfrc522.PCD_SoftPowerDown();
  digitalWrite(NFC_RESET, LOW);
  delay(50);
  digitalWrite(BATTERY_ENABLE, LOW);   // disable measuring
  digitalWrite(ENABLE_PERIPHERY, LOW); // disable periphery
  delay(1500);

  awake_timer = millis();
  digitalWrite(LED_GREEN, 1);
  while (digitalRead(PROXIMITY))
  {
    if (millis() - awake_timer > TTP_WAIT_TIME)
    {
      break;
    }
  }
  if (digitalRead(PROXIMITY))
  {
    // Proximity is still on. Might take up to 20 seconds. No need to wait in live mode.
    //    Change the wakeup setting to Proximity = LOW and also a timer for 25 seconds.
    Serial.print(F("Proximty is STILL HIGH!"));
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_34, 0);   // wakeup on proximity enable
    esp_sleep_enable_timer_wakeup(25 * 1000000ULL); // timer in microseconds
    digitalWrite(LED_GREEN, 1);
  }
  else
  {
    // Proximity is low. Wait a little more for the voltage to settle and sleep.
    Serial.print(F("Proximty is low."));
    delay(1000);
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_34, 1); // wakeup on proximity enable
    digitalWrite(LED_GREEN, 0);
  }

  time_t t;
  sleep_time = time(&t);

  // rtc.get
  Serial.print(F("Proxy wait millis:"));
  Serial << (millis() - awake_timer) << endl;
  Serial.println(F(" sleeping..."));
  esp_deep_sleep_start();
  delay(50);
}

/*
##     ##  #######  ######## ########
###   ### ##     ##    ##       ##
#### #### ##     ##    ##       ##
## ### ## ##     ##    ##       ##
##     ## ##  ## ##    ##       ##
##     ## ##    ##     ##       ##
##     ##  ##### ##    ##       ##
*/

void confirmOkReceiving()
{
  //[MAC]/OK
  if (!mqttClient.connected())
    connectBroker();
  sprintf(out_message_buffer, "%s/OK", MAC_char);
  Serial << "Confirm message:" << out_message_buffer << endl;
  mqttClient.publish("confirm", 0, false, out_message_buffer);
}

void confirmNokReceiving(const char *message)
{
  //[MAC]/NOK/[message]
  if (!mqttClient.connected())
    connectBroker();
  sprintf(out_message_buffer, "%s/NOK/%s", MAC_char, message);
  Serial << "Confirm message:" << out_message_buffer << endl;
  mqttClient.publish("confirm", 0, false, out_message_buffer);
}

/**
 * @brief Response on topic own MAC
 *
 * @param topic
 * @param payload
 * @param length
 *
 */
void mac_subscriber(String topic, String payload)
{
  String msg[7] = {"", "", "", "", "", "", ""};
  int chunks = tools.parseMessage(msg, payload);
  if (topic.equals("all"))
  {
    if (msg[0].equals("sleep"))
    {
      uint64_t ts = msg[1].toInt();
      if ((ts > 110 && ts < 86400) || ts == 0)
      {
        time_for_sleep = ts;
      }
      goSleep();
    }
    else
    {
      if (msg[0].equals("timesync"))
      {
        setClock(msg[1]);
      }
    }
    return;
  }

  if (payload.equals("open"))
  {
    opendoor(config.relay_time);
    return;
  }
  else if (payload.equals("start_web"))
  {
    startServer();
    responseAlive(); // уведомява Django за IP-то веднага след старта
  }
  else if (payload.equals("restart"))
  {
    esp_restart();
  }
  // else if (payload.equals("writecard"))
  // {
  //   writeCard();
  //   return;
  // }
  else if (payload.equals("getconfig"))
  {
    // Only serial print - sensitive informatin
    readConfig();
    return;
  }
  else if (payload.indexOf("mqtt_port") > 0)
  {
    // SET CONFIG
    setConfig(payload);
    measureTime();
    return;
  }
  else if (payload.equals("clear"))
  {
    SPIFFS.begin(false);
    deleteFile(SPIFFS, "/users.txt");
    confirmOkReceiving();
    return;
  }
  else if (payload.equals("ping"))
  {
    responseAlive();
    return;
  }

  if (msg[0].equals("timesync"))
  {
    setClock(msg[1]);
  }

  if (msg[0].equals("sleep"))
  {
    uint16_t tim = msg[1].toInt();
    time_for_sleep = tim;
    goSleep();
  }
  else if (msg[0].equals("time"))
  {
    // time_clock.setTime(00, 18, 13, 10, 1, 2022);  // 10th Jan 2022 13:18:00
    // time_clock.setTime(00, msg[5].toInt(), msg[4].toInt(), msg[1].toInt(), msg[2].toInt(), msg[3].toInt());

    //  setTime(hour, minute, sec);
    rtc.setTime(msg[4].toInt(), msg[5].toInt(), 00); // 24H mode, ex. 6:54:00
    //  setDate(weekday, day, month, yr);
    rtc.setDate(6, msg[1].toInt(), msg[2].toInt(), msg[3].toInt());
    // set time to external clock
  }
  else if (msg[0].charAt(0) == '{')
  {
    // SET Room Hotel
    setRoomHotelGroup(payload);
  }
}

//  #######  ##    ##    ########  ########  ######  ######## ########
// ##     ## ###   ##    ##     ## ##       ##    ## ##          ##
// ##     ## ####  ##    ##     ## ##       ##       ##          ##
// ##     ## ## ## ##    ########  ######    ######  ######      ##
// ##     ## ##  ####    ##   ##   ##             ## ##          ##
// ##     ## ##   ###    ##    ##  ##       ##    ## ##          ##
//  #######  ##    ##    ##     ## ########  ######  ########    ##

void wakeup_on_reset()
{
  access_late_user = 0;
  access_late_card = 0;
  // Populate RTC config from SPIFFS so card checks work correctly on the first
  // proxy wakeup (RTC memory is cleared after every hard reset / OTA flash).
  if (SPIFFS.begin(false))
    loadConfiguration();
  Serial.println(F("Initialize after reset"));
  // Initial clock set.
  // set time to last update
  // time_clock.setTime(30, 18, 13, 27, 1, 2023); // 27th January 2023 13:18:30

  //  setTime(hour, minute, sec);
  // rtc.setTime(6, 54, 00); // 24H mode, ex. 6:54:00
  //  setDate(weekday, day, month, yr);
  // rtc.setDate(2, 16, 5, 2023); // 0 for Sunday, ex. Saturday, 16.5.2023.
  if (analogRead(BATTERY_IN) < BATTERY_TRIGGER)
    sos();
  closedoor();
  // Sleep for 1 minute then wake via timer (w_reason==4) → wakeup_by_time()
  // This syncs the clock and sends alive so Django can confirm the OTA version.
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_34, 1); // wake also on card swipe
  esp_sleep_enable_timer_wakeup(60 * 1000000ULL);
  esp_deep_sleep_start();
}

// ######## #### ##     ## ########    ##      ##    ###    ##    ## ######## ##     ## ########
//    ##     ##  ###   ### ##          ##  ##  ##   ## ##   ##   ##  ##       ##     ## ##     ##
//    ##     ##  #### #### ##          ##  ##  ##  ##   ##  ##  ##   ##       ##     ## ##     ##
//    ##     ##  ## ### ## ######      ##  ##  ## ##     ## #####    ######   ##     ## ########
//    ##     ##  ##     ## ##          ##  ##  ## ######### ##  ##   ##       ##     ## ##
//    ##     ##  ##     ## ##          ##  ##  ## ##     ## ##   ##  ##       ##     ## ##
//    ##    #### ##     ## ########     ###  ###  ##     ## ##    ## ########  #######  ##

void wakeup_by_time()
{
#ifdef OFFLINE
  return;
#endif
  connectWiFi();
  if (!WiFi.isConnected())
  {
    to_send_late = false;
    goSleep();
  }
  awake_timer = millis();
  // Connect to MQTT
  Serial.print(s + "Connecting to MQTT: " + config.mqtt + " ... ");
  // TODO clientName.getBytes()
  // client.connect(const char *id, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage)
  connectBroker();
  if (!mqttClient.connected())
  {
    to_send_late = false;
    goSleep();
  }
  awake_timer = millis();
  responseAlive();
  awake_timer = millis();
  // sendLateMessage();
  sendLog();
  awake_timer = millis();
  printLocalTime(); // for TEST
  to_send_late = false;
}

// ########  ########   #######  ##     ## ##    ##    ##      ##    ###    ##    ## ######## ##     ## ########
// ##     ## ##     ## ##     ##  ##   ##   ##  ##     ##  ##  ##   ## ##   ##   ##  ##       ##     ## ##     ##
// ##     ## ##     ## ##     ##   ## ##     ####      ##  ##  ##  ##   ##  ##  ##   ##       ##     ## ##     ##
// ########  ########  ##     ##    ###       ##       ##  ##  ## ##     ## #####    ######   ##     ## ########
// ##        ##   ##   ##     ##   ## ##      ##       ##  ##  ## ######### ##  ##   ##       ##     ## ##
// ##        ##    ##  ##     ##  ##   ##     ##       ##  ##  ## ##     ## ##   ##  ##       ##     ## ##
// ##        ##     ##  #######  ##     ##    ##        ###  ###  ##     ## ##    ## ########  #######  ##

void wakeup_by_proxy()
{
  awake_timer = millis();
  bool card_found = false;
  timeMeasure = millis();
  Serial << "PROXY" << endl;

  Serial << "Battery:" << tools.calc_battery_percentage(analogRead(BATTERY_IN)) << endl;

  while (!card_found)
  {
    relayLoop();
    card_found = readCard();
    if (millis() - timeMeasure > 2000) // 2 seconds waiting for a card
      break;
  }

  bool spiffs_ok = SPIFFS.begin(false);
  if (!spiffs_ok)
  {
    Serial.println("An Error has occurred while mounting SPIFFS");
  }
  awake_timer = millis();
  // Read card and open before wifi connection — config is in RTC, works without SPIFFS
  if (card_found)
  {
    if (tools.compareByteArrays(16, card_block2, config.update_code))
    {
      specialCardCheck();
      return;
    }
    else if (checkAccess())
    {
      Serial.println(F("open door "));
    }
    else
    {
      Serial.println(F("publish access denied"));
      // #ifndef SLEEPLESS
      //       goSleep(); // wrong card found
      // #endif
    }
  }
  if (spiffs_ok)
  {
    loadConfiguration();
  }
}

/**
 * @brief Send buffered message
 *
 */
void sendLateMessage()
{
  if (!to_send_late)
  {
    return;
  }
  //[MAC]/access/[slot]/[user_id]/[time]/[shift]
  if (!mqttClient.connected())
    connectBroker();
  if (access_late_user != 0)
  {
    publishUserAcces(access_late_user, access_late_card);
  }
  else
  {
    publishErrorCard(access_late_card);
  }
  to_send_late = false;
}

/**
 * @brief Set the Sound in milliseconds
 *
 * @param time
 */
void setSound(uint16_t time)
{
  // digitalWrite(ZUMER, 1);
  startSound();
  beginSound = millis() + time;
}

/**
 * @brief Open door
 *
 * @param milis
 */
void opendoor(int seconds)
{
#if DND_ENABLED
  if (!digitalRead(DND))
  {
    if (!master_access)
    {
      errorSound();
      Serial.println("DND is set!");
      return;
    }
  }
#endif
  if (open_once)
    return;
  digitalWrite(LED_GREEN, 1);
  setSound(SOUND_OPEN);
  open_relay(seconds * 1000);
  door_opened = true;
  open_once = true;
}

/**
 * @brief Close door
 *
 * @param milis
 */
void closedoor()
{
  Serial.println("close door");
  door_opened = false;
#ifdef OLOCK
  digitalWrite(MOTOR_DIRECTION, 0);
#ifdef INVERSE_MOTOR
  digitalWrite(MOTOR_DIRECTION, 1);
#endif
  digitalWrite(MOTOR_ENABLE, 1);
  delay(MOTOR_DELAY);
  digitalWrite(MOTOR_ENABLE, 0);
  digitalWrite(MOTOR_DIRECTION, 0);
#ifdef INVERSE_MOTOR
  digitalWrite(MOTOR_DIRECTION, 1);
#endif
#endif
  digitalWrite(LED_GREEN, 0);
  digitalWrite(LED_RED, 0);
  // digitalWrite(ZUMER, 0);
  stopSound();

  if (ready_to_sleep)
  {
    Serial.println("go to sleep...");
    goSleep();
  }
}

/**
 * @brief Sends error event to broker
 *
 */
void publishErrorCard(uint64_t card)
{
  //[MAC]/access/[slot]/[user_id]/[time]/[shift]
  // ttime = time_clock.getTime("%d-%m-%Y %H:%M:%S").c_str();
  char stime[22];
  sprintf(stime, "%02d-%02d-%d %02d:%02d:%02d", rtc.getDay(), rtc.getMonth(), rtc.getYear(), rtc.getHour(), rtc.getMinute(), rtc.getSecond());
  // set time from external clock
  sprintf(out_message_buffer, "%s/error/%010llu/%s/%s", MAC_char, card, error_late.c_str(), stime); // unknown user
  Serial << "Send message:" << out_message_buffer << endl;
  mqttClient.publish("event", 0, false, out_message_buffer);
}

/**
 * @brief Send success open to broker
 *
 * @param userId
 * @param card
 */
void publishUserAcces(uint64_t userId, uint64_t card)
{
#ifdef MQTT_BROKER
  //[MAC]/access/[slot]/[user_id]/[time]/[shift]

  if (userId == 0)
    return;
  if (!mqttClient.connected())
  {
    connectBroker();
  }

  // ttime = time_clock.getTime("%d-%m-%Y %H:%M:%S").c_str();
  // String tm = time_clock.getTime("%d-%m-%Y %H:%M:%S");
  char stime[22];
  sprintf(stime, "%02d-%02d-%d %02d:%02d:%02d", rtc.getDay(), rtc.getMonth(), rtc.getYear(), rtc.getHour(), rtc.getMinute(), rtc.getSecond());
  // set time from external clock
  if (card > 200)
  {
    sprintf(out_message_buffer, "%s/%s/%010llu/%llu/%s/%u", MAC_char, EVENT_TYPE, card, userId, stime, tools.calc_battery_percentage(analogRead(BATTERY_IN)));
  }
  else
  {
    sprintf(out_message_buffer, "%s/%s/%llu/%llu/%s/%u", MAC_char, EVENT_TYPE, card, userId, stime, tools.calc_battery_percentage(analogRead(BATTERY_IN))); // unknown user
  }
  Serial << "Send message:" << out_message_buffer << endl;
  mqttClient.publish("event", 0, false, out_message_buffer);
#endif
  return;
}

/**
 * @brief Reading CARD and store its number in buffer
 *
 */
boolean readCard()
{
  // Clear buffer
  for (size_t i = 0; i < 16; i++)
  {
    card_block1[i] = 0;
    card_block2[i] = 0;
    card_block4[i] = 0;
    card_block5[i] = 0;
    card_block6[i] = 0;
  }
  // https://biodit.atlassian.net/wiki/spaces/OLOCK/pages/851969/Records+in+the+card
  // Reset the loop if no new card present on the sensor/reader. This saves the entire process when idle.
  if (!mfrc522.PICC_IsNewCardPresent())
  {
    // Serial << "-";
    mfrc522.PICC_HaltA();
    mfrc522.PCD_StopCrypto1();
    return false;
  }

  // Verify if the NUID has been readed
  if (!mfrc522.PICC_ReadCardSerial())
  {
    Serial << "no UID" << endl;
    mfrc522.PICC_HaltA();
    mfrc522.PCD_StopCrypto1();
    return false;
  }

  MFRC522::PICC_Type piccType = mfrc522.PICC_GetType(mfrc522.uid.sak);
  Serial.println(mfrc522.PICC_GetTypeName(piccType));
  if (piccType != MFRC522::PICC_TYPE_MIFARE_1K && piccType != MFRC522::PICC_TYPE_MIFARE_4K)
  {
    Serial.println(F("This reader only works with MIFARE Classic cards."));
    errorSound();
    // Halt PICC
    mfrc522.PICC_HaltA();
    // Stop encryption on PCD
    mfrc522.PCD_StopCrypto1();
#ifdef RFID_ONLY
    goSleep();
#endif
    return false;
  }

  // Store NUID into nuidPICC array
  for (byte i = 0; i < 4; i++)
  {
    nuidPICC[i] = mfrc522.uid.uidByte[i];
  }
  readBlock(1, card_block1);          // auth sector 0, read block 1
  readBlockNoAuth(2, card_block2);    // same sector 0, no re-auth
  readBlock(4, card_block4);          // auth sector 1, read block 4
  readBlockNoAuth(5, card_block5);    // same sector 1, no re-auth
  readBlockNoAuth(6, card_block6);    // same sector 1, no re-auth
  // readSector();
  // Halt PICC
  mfrc522.PICC_HaltA();
  // Stop encryption on PCD
  mfrc522.PCD_StopCrypto1();
  Serial << "find card" << endl;
  return true;
}

/**
 * @brief Validate CARD. Fast opening if card is valid
 *
 * @return boolean
 */
boolean checkAccess()
{
  uint64_t num;
  num = nuidPICC[3];
  num <<= 8;
  num += nuidPICC[2];
  num <<= 8;
  num += nuidPICC[1];
  num <<= 8;
  num += nuidPICC[0];
  sprintf(out_message_buffer, "%010llu", num);
  Serial << "Access card:" << num << endl;

  // Check offline
  uint64_t user_id = checkOfflineCardAccess();
  if (user_id != 0)
  {
    opendoor(config.relay_time);

    mfrc522.PCD_SoftPowerDown();

    Serial << "need send late" << endl;
    access_late_user = user_id;
    access_late_card = num;
    // to_send_late = true;
    saveEvent(user_id);
    return true;
  }
  access_late_card = num;
  errorSound();
  return false;
}

/**
 * @brief Stops sound after given period
 *
 */
void soundLoop()
{
  if (beginSound > millis())
    return;
  // timer
  // digitalWrite(ZUMER, 0);
  stopSound();
}

/**
 * @brief Close relay after given period
 *
 */
void relayLoop()
{
  if (startRelay > millis())
    return;
  if (door_opened)
  {
    closedoor();
  }
}

/*
##     ##    ###    #### ##    ##    ##        #######   #######  ########
###   ###   ## ##    ##  ###   ##    ##       ##     ## ##     ## ##     ##
#### ####  ##   ##   ##  ####  ##    ##       ##     ## ##     ## ##     ##
## ### ## ##     ##  ##  ## ## ##    ##       ##     ## ##     ## ########
##     ## #########  ##  ##  ####    ##       ##     ## ##     ## ##
##     ## ##     ##  ##  ##   ###    ##       ##     ## ##     ## ##
##     ## ##     ## #### ##    ##    ########  #######   #######  ##
*/
/**
 * @brief Main loop
 *
 */
void loop()
{
  // if (isTimeToSleep() && !digitalRead(PROXIMITY))
  if (isTimeToSleep())
  {
    // if (to_send_late)
    // {
    //   prepare_and_sleep();
    // }
    // else
    // {
    digitalWrite(LED_RED, LOW); // on proxy
    goSleep();
    // }
  }

  cardLoop();
  soundLoop();
  relayLoop();
  messageLoop();
}

void cardLoop()
{
  if (!factory_mode)
  {
    if (!door_opened && !open_once)
    {
      if (readCard())
      {
        if (tools.compareByteArrays(16, card_block2, config.update_code))
        {
          specialCardCheck();
          return;
        }
        checkAccess();
        awake_timer = millis();
      }
    }
  }
  else
  {
    // count time for reset
    if (digitalRead(FACTORY_RESET) == 0)
    {
      if (millis() - timeMeasure > HOLD_TIME_FACTORY_RESET)
      {
        // factory_reset();
      }
    }
    else
    {
      // quit factory reset
      timeMeasure = millis();
    }
  }
}

/**
 * @brief Message loop
 *
 */
void messageLoop()
{
  if (topic_late.length() == 0 || message_late.length() == 0)
    return;
  mac_subscriber(topic_late, message_late);
  topic_late.clear();
  message_late.clear();
}

/**
 * @brief Read file
 *
 * @param fs
 * @param path
 */
void readFile(fs::FS &fs, const char *path)
{
  Serial.printf("Reading file: %s\n", path);

  File file = fs.open(path);
  if (!file)
  {
    Serial.println("Failed to open file for reading");
    return;
  }
  awake_timer = millis();
  Serial.println("Read from file: ");
  while (file.available())
  {
    relayLoop();
    Serial.write(file.read());
  }
  Serial.println();
  file.close();
}

/**
 * @brief Delete file
 *
 * @param fs
 * @param path
 */
void deleteFile(fs::FS &fs, const char *path)
{
  Serial.printf("Deleting file: %s\n", path);
  if (fs.remove(path))
  {
    Serial.println("File deleted");
  }
  else
  {
    Serial.println("Delete failed");
  }
}

/**
 * @brief Print wake up reason
 *
 */
uint8_t print_wakeup_reason()
{
  uint8_t reason = 0;
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  // Serial << "Wakeup reason number:" << wakeup_reason << endl;
  // Serial << "Factory button state:" << digitalRead(FACTORY_RESET) << endl;
  switch (wakeup_reason)
  {
  case 1:
    reason = 1;
    Serial.println("Wakeup caused by external signal using RTC_IO");
    break;
  case 2:
    reason = 2;
    Serial.println("Wakeup caused by PROXIMITY");
    break;
  case 3:
    reason = 3;
    Serial.println("Wakeup caused by timer");
    break;
  case 4:
    reason = 4;
    Serial.println("Wakeup caused by TIME");
    break;
  case 5:
    reason = 5;
    Serial.println("Wakeup caused by ULP program");
    break;
  default:
    reason = 0;
    Serial.println("Wakeup on RESET");
    break;
  }
  return reason;
}

/*
   ###    ########
  ## ##   ##     ##
 ##   ##  ##     ##
##     ## ########
######### ##
##     ## ##
##     ## ##
*/

void activate_AP()
{
  factory_mode = true;
  // configure AP mode
  // activate watchdog for 3 minutes

  Serial.begin(115200);
  Serial.println();
  Serial.println("Factory reset ...");
  insomnia = true;
  if (!SPIFFS.begin(false))
  {
    Serial.println("An Error has occurred while mounting SPIFFS");
    return;
  }

  // You can remove the password parameter if you want the AP to be open.

  WiFi.softAP(config.apssid, config.appass);
  IPAddress myIP = WiFi.softAPIP();
  Serial.print(F("AP IP address: "));
  Serial.println(myIP);
  Serial.println("Server started");
  WiFi.macAddress(MAC_array);
  for (int i = 0; i < sizeof(MAC_array); ++i)
  {
    sprintf(MAC_char, "%s%02x", MAC_char, MAC_array[i]);
  }
  Serial.println(MAC_char);
  startServer();

  soundLong();
}

void soundLong()
{
  // digitalWrite(ZUMER, 1);
  startSound();
  delay(200);
  // digitalWrite(ZUMER, 0);
  stopSound();
}

/*
##      ## ######## ########
##  ##  ## ##       ##     ##
##  ##  ## ##       ##     ##
##  ##  ## ######   ########
##  ##  ## ##       ##     ##
##  ##  ## ##       ##     ##
 ###  ###  ######## ########

*/

/**
 * @brief Start WEB server
 *
 */
void startServer()
{
  insomnia = true;
  setSound(200);
  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(SPIFFS, "/index.html", String(), false, processor); });
  // Route for root / web page
  server.on("/config.html", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(SPIFFS, "/config.html", String(), false, processor); });

  // Route to load style.css file
  server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(SPIFFS, "/style.css", "text/css"); });

  // Send a POST request to <IP>/post with a form field message set to <message>
  server.on("/config", HTTP_POST, [](AsyncWebServerRequest *request)
            {
			  String message = "";
			  String param;
			  config.dhcp=1;
			  config.sound=1;
			  if (request->hasParam(PARAM_NAME, true))
			  {
				param = request->getParam(PARAM_NAME, true)->value();
				message += "NAME:";
				message += param;
				message += "\r\n";
				strlcpy(config.name, param.c_str(), sizeof(config.name));
			  }
			  if(request->hasParam(PARAM_AP_PASS,true))
			  {
				param = request->getParam(PARAM_AP_PASS, true)->value();
				message += "AP_PASS:";
				message += param;
				message += "\r\n";
				strlcpy(config.appass, param.c_str(), sizeof(config.appass));
			  }
			  if (request->hasParam(PARAM_SSID, true))
			  {
				param = request->getParam(PARAM_SSID, true)->value();
				message += "SSID:";
				message += param;
				message += "\r\n";
				strlcpy(config.ssid, param.c_str(), sizeof(config.ssid));
			  }
			  if (request->hasParam(PARAM_PASS, true))
			  {
				param = request->getParam(PARAM_PASS, true)->value();
				message += "PASS:";
				message += param;
				message += "\r\n";
				strlcpy(config.pass, param.c_str(), sizeof(config.pass));
			  }
			  if (request->hasParam(PARAM_AP_SSID, true))
			  {
				param = request->getParam(PARAM_AP_SSID, true)->value();
				message += "AP_SSID:";
				message += param;
				message += "\r\n";
				strlcpy(config.apssid, param.c_str(), sizeof(config.apssid));
			  }
			  if (request->hasParam(PARAM_MQTT, true))
			  {
				param = request->getParam(PARAM_MQTT, true)->value();
				message += "MQTT:";
				message += param;
				message += "\r\n";
				strlcpy(config.mqtt, param.c_str(), sizeof(config.mqtt));
			  }
			  if (request->hasParam(PARAM_MQTT_PORT, true))
			  {
				param = request->getParam(PARAM_MQTT_PORT, true)->value();
				message += "MQTT_PORT:";
				message += param;
				message += "\r\n";
				config.mqtt_port = tools.StringTo64(param);
			  }
			   if (request->hasParam(PARAM_MQTT_USER, true))
			  {
				param = request->getParam(PARAM_MQTT_USER, true)->value();
				message += "MQTT_USER:";
				message += param;
				message += "\r\n";
				strlcpy(config.mqtt_user, param.c_str(), sizeof(config.mqtt_user));
			  }
			  if (request->hasParam(PARAM_MQTT_USER_PASS, true))
			  {
				param = request->getParam(PARAM_MQTT_USER_PASS, true)->value();
				message += "MQTT_USER_PASS:";
				message += param;
				message += "\r\n";
				strlcpy(config.mqtt_user_pass, param.c_str(), sizeof(config.mqtt_user_pass));
			  }
			  if (request->hasParam(PARAM_DHCP, true))
			  {
				param = request->getParam(PARAM_DHCP, true)->value();
				message += "DHCP:";
				message += param;
				message += "\r\n";
				config.dhcp = 1;
			  }              
			  if (request->hasParam(PARAM_IP, true))
			  {
				param = request->getParam(PARAM_IP, true)->value();
				message += "IP:";
				message += param;
				message += "\r\n";
				strlcpy(config.ip_address, param.c_str(), sizeof(config.ip_address));
			  }
			  if (request->hasParam(PARAM_MASK, true))
			  {
				param = request->getParam(PARAM_MASK, true)->value();
				message += "MASK:";
				message += param;
				message += "\r\n";
				strlcpy(config.ip_mask, param.c_str(), sizeof(config.ip_mask));
			  }
			  if (request->hasParam(PARAM_GATEWAY, true))
			  {
				param = request->getParam(PARAM_GATEWAY, true)->value();
				message += "GATEWAY:";
				message += param;
				message += "\r\n";
				strlcpy(config.gateway, param.c_str(), sizeof(config.gateway));
			  }
			  if (request->hasParam(PARAM_TYPE, true))
			  {
				param = request->getParam(PARAM_TYPE, true)->value();
				message += "TYPE:";
				message += param;
				message += "\r\n";
				strlcpy(config.type, param.c_str(), sizeof(config.type));
			  }
			  if (request->hasParam(PARAM_RELAY, true))
			  {
				param = request->getParam(PARAM_RELAY, true)->value();
				message += "RELAY:";
				message += param;
				message += "\r\n";
				config.relay_time = tools.StringTo64(param);
			  }
			  if (request->hasParam(PARAM_EVENT, true))
			  {
				param = request->getParam(PARAM_EVENT, true)->value();
				message += "EVENT:";
				message += param;
				message += "\r\n";
				strlcpy(config.event, param.c_str(), sizeof(config.event));
			  }
			  if (request->hasParam(PARAM_AP_SSID, true))
			  {
				param = request->getParam(PARAM_AP_SSID, true)->value();
				message += "APSSID:";
				message += param;
				message += "\r\n";
				strlcpy(config.apssid, param.c_str(), sizeof(config.apssid));
			  }
			   if (request->hasParam(PARAM_GROUP, true))
			  {
				param = request->getParam(PARAM_GROUP, true)->value();
				message += "GROUP:";
				message += param;
				message += "\r\n";
				config.group = (uint16_t) tools.StringTo64(param);
			  }
			  if (request->hasParam(PARAM_HOTEL, true))
			  {
				param = request->getParam(PARAM_HOTEL, true)->value();
				message += "HOTEL:";
				message += param;
				message += "\r\n";
				config.hotel_code = tools.StringTo64(param);
			  }
			  if (request->hasParam(PARAM_ROOM, true))
			  {
				param = request->getParam(PARAM_ROOM, true)->value();
				message += "ROOM:";
				message += param;
				message += "\r\n";
				config.room = tools.StringTo64(param);
			  }
			  if (request->hasParam(PARAM_SOUND, true))
			  {
				param = request->getParam(PARAM_SOUND, true)->value();
				message += "SOUND:";
				message += param;
				message += "\r\n";
				config.sound = 1;
			  }
			 
			  Serial << message << endl;
			  request->send(200, "text/plain", message);

			  saveConfiguration(); 
			  restart(); });

  server.on("/list.html", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(SPIFFS, "/list.html", String(), false, processor); });

  server.on("/time.html", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(SPIFFS, "/time.html", String(), false, processor); });

  server.on("/settime", HTTP_POST, [](AsyncWebServerRequest *request)
            { handleTimeData(request); });

  server.on("/log.csv", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(SPIFFS, "/log.csv", "text/plain"); });

  server.on("/exit", HTTP_GET, [](AsyncWebServerRequest *request)
            { restart(); });

  server.on("/clear", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(SPIFFS, "/index.html", String(), false, processor); 
  clearLog(); });

  // server.on("/update", HTTP_POST, [](AsyncWebServerRequest *request)
  //           { update(request); });

  // MDNS.begin(host);
  // MDNS.addService("http", "tcp", 80);

  // Start server
  AsyncElegantOTA.begin(&server);
  server.begin();
}

/**
 * @brief Updates state in html page
 *
 * @param var
 * @return String
 */
String processor(const String &var)
{
  Serial.println(var);
  if (var == "ap_ssid")
  {
    return config.apssid;
  }
  else if (var == "ssid")
  {
    return config.ssid;
  }
  else if (var == "pass")
  {
    return config.pass;
  }
  else if (var == "ip_address")
  {
    return config.ip_address;
  }
  else if (var == "event")
  {
    return config.event;
  }
  else if (var == "ip_mask")
  {
    return config.ip_mask;
  }
  else if (var == "name")
  {
    return config.name;
  }
  else if (var == "relay_time")
  {
    return String(config.relay_time, DEC);
  }
  else if (var == "mqtt")
  {
    return config.mqtt;
  }
  else if (var == "mqtt_port")
  {
    return String(config.mqtt_port, DEC);
  }
  else if (var == "sound")
  {
    if (config.sound)
    {
      return "true";
    }
    return "false";
  }
  else if (var == "dhcp")
  {
    if (config.dhcp == 1)
    {
      return "true";
    }
    return "false";
  }
  return String();
}

/**
 * @brief Load configuration file
 *
 */
void loadConfiguration()
{
  File file = SPIFFS.open(CONNFIG_FILE);
  if (!file)
  {
    File backup = SPIFFS.open("/configjson.bak");
    if (!backup)
    {
      Serial.println("Failed to open file for reading");
      return;
    }
    Serial.println("Config file missing; using backup");
    file = backup;
  }
  Serial.println("Config file loaded:");

#ifdef DEBUG
  filesys.printFile(SPIFFS, CONNFIG_FILE);
#endif

  StaticJsonDocument<2048> doc;
  // Deserialize the JSON document
  DeserializationError error = deserializeJson(doc, file);
  file.close();
  if (error == error.IncompleteInput)
  {
    Serial << "incomplete" << endl;
  }
  if (error == error.InvalidInput)
  {
    Serial << "invaliv input" << endl;
  }
  if (error == error.EmptyInput)
  {
    Serial << "empty input" << endl;
  }
  if (error == error.NoMemory)
  {
    Serial << "no memory" << endl;
  }
  if (error == error.TooDeep)
  {
    Serial << "too deep" << endl;
  }
  if (error == error.Ok)
  {
    Serial << "OK" << endl;
  }

  if (error)
  {
    Serial.println(F("Deserialization Error"));
    // factory_reset();
    setSound(150);
    return;
  }

  // Copy values from the JsonDocument to the Config
  config.dhcp = doc["dhcp"] | 1;
  strlcpy(config.event,                                                                           // <- destination
          doc["event"] | "access",                                                                // <- source
          sizeof(config.event));                                                                  // <- destination's capacity
  strlcpy(config.ip_address, doc["ip_address"] | "192.168.1.100", sizeof(config.ip_address));
  strlcpy(config.ip_mask, doc["ip_mask"] | "255.255.255.0", sizeof(config.ip_mask));
  strlcpy(config.mqtt, doc["mqtt"] | "192.168.1.15", sizeof(config.mqtt));
  config.mqtt_port = doc["mqtt_port"] | 1883;
  strlcpy(config.pass, doc["pass"] | "Biodit234", sizeof(config.pass));
  config.relay_time = doc["relay_time"] | 3;
  config.sound = doc["sound"] | 1;
  strlcpy(config.ssid, doc["ssid"] | "BIODIT", sizeof(config.ssid));
  strlcpy(config.apssid, doc["apssid"] | "Olock", sizeof(config.apssid));
  strlcpy(config.type, doc["type"] | "olock", sizeof(config.type));
  strlcpy(config.name, doc["name"] | "Room", sizeof(config.name));
  strlcpy(config.appass, doc["appass"] | "biodit2021", sizeof(config.appass));
  strlcpy(config.mqtt_user, doc["mqtt_user"] | "olock", sizeof(config.mqtt_user));
  strlcpy(config.mqtt_user_pass, doc["mqtt_user_pass"] | "pass", sizeof(config.mqtt_user_pass));
  strlcpy(config.aes_key, doc["aes_key"] | "xX17y#y%qhvMjt&9", sizeof(config.aes_key));
  config.hotel_code = doc["hotel_code"] | 1;
  config.room = doc["room"] | 1;
  char grps[20];
  strlcpy(grps, doc["group"] | "1", sizeof(grps));
  config.group = tools.StringTo64(grps);
  Serial << "Group in file:" << config.group << endl;

  config.guest_count = doc["guest_count"] | 1;
  config.staff_count = doc["staff_count"] | 1;
  config.master_count = doc["master_count"] | 1;
  strlcpy(config.update_code, doc["update_code"] | "t3o6z7fryx3lrt1c", sizeof(config.update_code));
}

/**
 * @brief Saves the configuration to a file
 *
 * @param filename
 * @param config
 */
void saveConfiguration()
{
  uint16_t adc = 0;
  if (digitalRead(BATTERY_ENABLE) == LOW)
  {
    digitalWrite(BATTERY_ENABLE, HIGH);
    delay(5);
  }
  adc = analogRead(BATTERY_IN);
  if (adc < BATTERY_TRIGGER)
  {
    Serial.println(F("Battery too low: skip config save"));
    lowBatteryAlert();
    return;
  }

  const char *tmpFile = "/configjson.tmp";
  const char *bakFile = "/configjson.bak";

  SPIFFS.remove(tmpFile);

  // Open temp file for writing
  File file = SPIFFS.open(tmpFile, FILE_WRITE);
  if (!file)
  {
    Serial.println(F("Failed to create temp config file"));
    return;
  }

  // Allocate a temporary JsonDocument
  // Don't forget to change the capacity to match your requirements.
  // Use arduinojson.org/assistant to compute the capacity.
  StaticJsonDocument<1024> doc;

  // Set the values in the document
  doc["dhcp"] = config.dhcp;
  doc["event"] = config.event;
  doc["ip_address"] = config.ip_address;
  doc["ip_mask"] = config.ip_mask;
  doc["mqtt"] = config.mqtt;
  doc["mqtt_port"] = config.mqtt_port;
  doc["pass"] = config.pass;
  doc["relay_time"] = config.relay_time;
  doc["sound"] = config.sound;
  doc["ssid"] = config.ssid;
  doc["type"] = config.type;
  doc["apssid"] = config.apssid;
  doc["appass"] = config.appass;
  doc["name"] = config.name;
  doc["device_mac"] = MAC_char;
  doc["mqtt_user"] = config.mqtt_user;
  doc["mqtt_user_pass"] = config.mqtt_user_pass;
  doc["hotel_code"] = config.hotel_code;
  doc["gateway"] = config.gateway;
  doc["room"] = config.room;
  char grps[20];
  sprintf(grps, "%llu", config.group);
  doc["group"] = grps;
  Serial << "for save group:" << grps << endl;
  doc["guest_count"] = config.guest_count;
  doc["staff_count"] = config.staff_count;
  doc["master_count"] = config.master_count;
  doc["update_code"] = config.update_code;

  // Serialize JSON to file
  if (serializeJson(doc, file) == 0)
  {
    Serial.println(F("Failed to write to temp config file"));
    file.close();
    SPIFFS.remove(tmpFile);
    return;
  }
  // Close the file
  file.close();

  if (SPIFFS.exists(CONNFIG_FILE))
  {
    SPIFFS.remove(bakFile);
    SPIFFS.rename(CONNFIG_FILE, bakFile);
  }

  if (!SPIFFS.rename(tmpFile, CONNFIG_FILE))
  {
    Serial.println(F("Failed to replace config file"));
    if (SPIFFS.exists(bakFile))
    {
      SPIFFS.rename(bakFile, CONNFIG_FILE);
    }
    SPIFFS.remove(tmpFile);
    return;
  }

  SPIFFS.remove(bakFile);
  Serial.println("Config file saved");
  filesys.printFile(SPIFFS, CONNFIG_FILE);
}

/**
 * @brief Check if time for sleep
 *
 * @return true
 * @return false
 */
bool isTimeToSleep()
{
  if (insomnia)
  {
    return (millis() - awake_timer > UPDATE_TIME); // WEB settings in 5 minutes
  }
  return (millis() - awake_timer > AWAKE_TIME);
}

void setConfig(const String &var)
{
  setRoomHotelGroup(var);
}

/**
 * @brief Factory reset
 *
 */
void factory_reset()
{
  Serial << "Full reset" << endl;
  strlcpy(config.name, "name", sizeof(config.name));
  config.room = 1;
  config.group = 1;
  config.hotel_code = 1;
  strlcpy(config.update_code, "t3o6z7fryx3lrt1c", sizeof(config.update_code));
  strlcpy(config.apssid, "Olock", sizeof(config.apssid));
  strlcpy(config.appass, "biodit2022", sizeof(config.appass));
  strlcpy(config.pass, "4ETj5dPy", sizeof(config.pass));
  strlcpy(config.ssid, "biodit_sys", sizeof(config.ssid));
  strlcpy(config.event, "access", sizeof(config.event));
  strlcpy(config.ip_address, "192.168.1.19", sizeof(config.ip_address));
  strlcpy(config.ip_mask, "255.255.255.0", sizeof(config.ip_mask));
  strlcpy(config.mqtt, "192.168.1.15", sizeof(config.mqtt));
  strlcpy(config.gateway, "192.168.1.1", sizeof(config.gateway));
  strlcpy(config.mqtt_user, "olock", sizeof(config.mqtt_user));
  strlcpy(config.mqtt_user_pass, "pass", sizeof(config.mqtt_user_pass));
  strlcpy(config.aes_key, "xX17y#y%qhvMjt&9", sizeof(config.aes_key));
  config.dhcp = 1;
  config.sound = 1;
  config.mqtt_port = 1883;
  config.relay_time = 3;
  saveConfiguration();
  setSound(200);
  delay(200);
  // time_clock.setTime(30, 18, 13, 10, 1, 2022); // 10th Jan 2022 13:18:30
  // set time to external clock
  rtc.setDate(0, 16, 5, 2023);
  rtc.setTime(12, 0, 0);
  restart();
}

// void restart()
// {
// #ifndef OFFLINE
//   esp_sleep_enable_timer_wakeup(1000000); // sleep 1m
// #endif
//   Serial.println(F("Restarting..."));
//   esp_deep_sleep_start();
// }

// MQTT TESTS
void onMqttPublish(uint16_t packetId)
{
  Serial.println("Publish acknowledged.");
  Serial.print("  packetId: ");
  Serial.println(packetId);
}

// void connectToMqtt()
// {
//   Serial.println("Connecting to MQTT...");
//   if (!mqttClient.connected())
//   {
//     connectBroker();
//   }
// }

void onMqttUnsubscribe(uint16_t packetId)
{
  Serial.println("Unsubscribe acknowledged.");
  Serial.print("  packetId: ");
  Serial.println(packetId);
}

void onMqttDisconnect(AsyncMqttClientDisconnectReason reason)
{
  Serial.println("Disconnected from MQTT.");

  if (reason == AsyncMqttClientDisconnectReason::TLS_BAD_FINGERPRINT)
  {
    Serial.println("Bad server fingerprint.");
  }
  else if (reason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED)
  {
    Serial.println("TCP_DISCONNECTED");
  }
  else if (reason == AsyncMqttClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION)
  {
    Serial.println("MQTT_UNACCEPTABLE_PROTOCOL_VERSION");
  }
  else if (reason == AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED)
  {
    Serial.println("MQTT_IDENTIFIER_REJECTED");
  }
  else if (reason == AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE)
  {
    Serial.println("MQTT_SERVER_UNAVAILABLE");
  }
  else if (reason == AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS)
  {
    Serial.println("MQTT_MALFORMED_CREDENTIALS");
  }
  else if (reason == AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED)
  {
    Serial.println("MQTT_NOT_AUTHORIZED");
  }
  else if (reason == AsyncMqttClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE)
  {
    Serial.println("ESP8266_NOT_ENOUGH_SPACE");
  }
  // goSleep();
  //  if (WiFi.isConnected())
  //  {
  //      mqttReconnectTimer.once(2, connectToMqtt);
  //  }
}

void onMqttConnect(bool sessionPresent)
{
  mqttClient.subscribe("all", 0);
  awake_timer = millis();
  Serial.println("Connected to MQTT.");
}

void initSPI()
{
  // Init SPI bus
  SPI.begin();
  delay(50);
  mfrc522.PCD_Init(); // Init MFRC522 card
  mfrc522.PCD_SoftPowerUp();
  delay(50);
  mfrc522.PCD_SetAntennaGain(mfrc522.RxGain_max); // max gain for better read range
  mfrc522.PCD_DumpVersionToSerial();
}

// ##      ## #### ######## ####
// ##  ##  ##  ##  ##        ##
// ##  ##  ##  ##  ##        ##
// ##  ##  ##  ##  ######    ##
// ##  ##  ##  ##  ##        ##
// ##  ##  ##  ##  ##        ##
//  ###  ###  #### ##       ####

void connectWiFi()
{
  if (WiFi.isConnected())
  {
    return;
  }
  WiFi.disconnect();
  // Connect to WiFi
  char host[16];
  sprintf(host, "room_%d", config.room);
  WiFi.setHostname(host);
  Serial.print(s + "Connecting to WiFi: " + config.ssid + " ");
  awake_timer = millis();
  if (config.dhcp == 1)
  {
    WiFi.begin(config.ssid, config.pass);
  }
  else
  {
    local_IP.fromString(config.ip_address);
    gateway.fromString(config.gateway);
    subnet.fromString(config.ip_mask);
    if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS))
    {
      Serial.println("STA Failed to configure");
    }
    else
    {
      Serial << "Static IP" << endl;
      Serial << local_IP << ", " << gateway << ", " << subnet << ", " << primaryDNS << ", " << secondaryDNS << endl;
      Serial << WiFi.localIP() << ", " << WiFi.gatewayIP() << ", " << WiFi.subnetMask() << ", " << WiFi.dnsIP() << endl;
      WiFi.begin(config.ssid, config.pass);
    }
  }

  awake_timer = millis();
  while (WiFi.status() != WL_CONNECTED)
  {
    relayLoop();
    cardLoop();
    // TODO set timeout to go sleep
    delay(100);
    digitalWrite(ZUMER, 0); // stop sound
    Serial.print(".");
    // restarts if no Wifi connection
    if (millis() - awake_timer > WIFI_TIME)
    {
      Serial.println(F("WiFi not connected"));
      return;
      // goSleep();
    }
  }

  Serial.println(F("connected"));
  // GET MAC
  WiFi.macAddress(MAC_array);
  for (int i = 0; i < sizeof(MAC_array); ++i)
  {
    sprintf(MAC_char, "%s%02x", MAC_char, MAC_array[i]);
  }

  // mymac = MAC_char;
  // Set client name
  // Olock_240ac40457d0
  sprintf(clientName, "Olock_%s", MAC_char);
  Serial << "set MQTT client name:" << clientName << endl;

  IP = WiFi.localIP();
}

void goTest()
{
  printLocalTime();
  measureTime();
  // TEST
  time_for_sleep = 10; // 3 sec for test
  goSleep();
}

void printLocalTime()
{
  // set time from external clock
  char stime[22];
  sprintf(stime, "%02d-%02d-%d %02d:%02d:%02d", rtc.getDay(), rtc.getMonth(), rtc.getYear(), rtc.getHour(), rtc.getMinute(), rtc.getSecond());
  Serial << stime << endl;
}

void readBlock(byte block, byte *buffer)
{
  byte len = 18;
  MFRC522::StatusCode status;
  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, block, &key, &(mfrc522.uid));
  if (status != MFRC522::STATUS_OK)
  {
    Serial.print(F("Authentication failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }

  status = mfrc522.MIFARE_Read(block, buffer, &len);
  if (status != MFRC522::STATUS_OK)
  {
    Serial.print(F("Reading failed: "));
    Serial.println(mfrc522.GetStatusCodeName(status));
    return;
  }
}

// Read block without re-authenticating — only valid within the same sector as the last readBlock() call
void readBlockNoAuth(byte block, byte *buffer)
{
  byte len = 18;
  MFRC522::StatusCode status = mfrc522.MIFARE_Read(block, buffer, &len);
  if (status != MFRC522::STATUS_OK)
  {
    Serial.print(F("Reading failed (no auth): "));
    Serial.println(mfrc522.GetStatusCodeName(status));
  }
}

void test()
{
  // TEST AES

  // char *input = "Tech tutorials x";
  unsigned char output[16];
  decrypt((char *)card_block1, config.aes_key, output);
  for (size_t i = 0; i < 16; i++)
  {
    /* code */
    Serial.print(tools.byteToHex(output[i]));
  }
  Serial.println();
  decrypt((char *)card_block2, config.aes_key, output);
  for (size_t i = 0; i < 16; i++)
  {
    /* code */
    Serial.print(tools.byteToHex(output[i]));
  }
  Serial.println();
  Serial.println("key:");
  for (size_t j = 0; j < 16; j++)
  {
    Serial.print(tools.byteToHex(config.aes_key[j]));
  }
  Serial.println();
}

void encrypt(char *plainText, const char *key, unsigned char *outputBuffer)
{
  mbedtls_aes_context aes;
  mbedtls_aes_init(&aes);
  mbedtls_aes_setkey_enc(&aes, (const unsigned char *)key, strlen(key) * 8);
  mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, (const unsigned char *)plainText, outputBuffer);
  mbedtls_aes_free(&aes);
}

void decrypt(char *chipherText, const char *key, unsigned char *outputBuffer)
{
  mbedtls_aes_context aes;
  mbedtls_aes_init(&aes);
  mbedtls_aes_setkey_dec(&aes, (const unsigned char *)key, strlen(key) * 8);
  mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_DECRYPT, (const unsigned char *)chipherText, outputBuffer);
  mbedtls_aes_free(&aes);
}

//  #######  ######## ######## ##       #### ##    ## ########     ######     ###    ########  ########
// ##     ## ##       ##       ##        ##  ###   ## ##          ##    ##   ## ##   ##     ## ##     ##
// ##     ## ##       ##       ##        ##  ####  ## ##          ##        ##   ##  ##     ## ##     ##
// ##     ## ######   ######   ##        ##  ## ## ## ######      ##       ##     ## ########  ##     ##
// ##     ## ##       ##       ##        ##  ##  #### ##          ##       ######### ##   ##   ##     ##
// ##     ## ##       ##       ##        ##  ##   ### ##          ##    ## ##     ## ##    ##  ##     ##
//  #######  ##       ##       ######## #### ##    ## ########     ######  ##     ## ##     ## ########

/**
 * @brief Check offline card access
 *
 * @return uint16_t
 */
uint64_t checkOfflineCardAccess()
{
  for (byte i = 0; i < 4; i++)
  {
    nuidPICC[i] = mfrc522.uid.uidByte[i];
  }
  Serial << tools.byteToHex(nuidPICC[0]) << tools.byteToHex(nuidPICC[1]) << tools.byteToHex(nuidPICC[2]) << tools.byteToHex(nuidPICC[3]) << endl;
  // tools.dump_byte_array(card_block1, 16);
  // tools.dump_byte_array(card_block2, 16);
  // tools.dump_byte_array(card_block4, 16);
  // tools.dump_byte_array(card_block5, 16);
  // tools.dump_byte_array(card_block6, 16);
  Serial << endl;
  // Decrypting
  unsigned char card_block1_dec[16];
  decrypt((char *)card_block1, config.aes_key, card_block1_dec);
  unsigned char card_block2_dec[16];
  decrypt((char *)card_block2, config.aes_key, card_block2_dec);
  unsigned char card_block4_dec[16];
  decrypt((char *)card_block4, config.aes_key, card_block4_dec);
  unsigned char card_block5_dec[16];
  decrypt((char *)card_block5, config.aes_key, card_block5_dec);
  unsigned char card_block6_dec[16];
  decrypt((char *)card_block6, config.aes_key, card_block6_dec);
  // tools.dump_byte_array(card_block1_dec, 16);
  // tools.dump_byte_array(card_block2_dec, 16);
  // tools.dump_byte_array(card_block4_dec, 16);
  // tools.dump_byte_array(card_block5_dec, 16);
  // tools.dump_byte_array(card_block6_dec, 16);

#ifdef USE_RTC
  // Check valid Time
  if (!tools.isTimeValid(card_block2_dec))
  {
    return 0;
  }
#endif
  for (size_t i = 0; i < 16; i++)
  {
    card_block1[i] = card_block1_dec[i];
    card_block2[i] = card_block2_dec[i];
    card_block4[i] = card_block4_dec[i];
    card_block5[i] = card_block5_dec[i];
    card_block6[i] = card_block6_dec[i];
  }

#ifndef CLONE_FREE
  // 1. Check valid card number (An intrusion warning may be sent)
  if (nuidPICC[0] != card_block1[12] || nuidPICC[1] != card_block1[13] || nuidPICC[2] != card_block1[14] || nuidPICC[3] != card_block1[15])
  {
    Serial << "Fake card. The decrypted number doesn't match!" << endl;
    Serial << "Found " << tools.byteToHex(card_block1[12]) << tools.byteToHex(card_block1[13]) << tools.byteToHex(card_block1[14]) << tools.byteToHex(card_block1[15]) << endl;
    error_late = "Fake card. The decrypted number doesn't match!";
    return 0;
  }
#endif

  // 2. Check hotel number
  uint16_t hotel;
  hotel = card_block1[0];
  hotel <<= 8;
  hotel += card_block1[1];
  if (config.hotel_code != hotel)
  {
    Serial << "hotel:" << config.hotel_code << endl;
    Serial << "Hotel number doesn't match! - found " << hotel << endl;
    error_late = "Hotel number doesn't match!";
    checkSpecialCards();
    return 0;
  }
  // 3. Check room or group matching. If bouth are zero check if user is employee. In this case card have full system access.
  // TODO - bit check
  uint64_t group = 0;
  // [0 - 7]
  group += card_block5[0];
  group <<= 8;
  group += card_block5[1];
  group <<= 8;
  group += card_block5[2];
  group <<= 8;
  group += card_block5[3];
  group <<= 8;
  group += card_block5[4];
  group <<= 8;
  group += card_block5[5];
  group <<= 8;
  group += card_block5[6];
  group <<= 8;
  group += card_block5[7];

  // Validate group number
  for (size_t i = 0; i < 8; i++)
  {
    if (card_block5[i] != card_block5[i + 8])
    {
      Serial << "Invalid group. Bad reading! group:" << group << endl;
      return 0;
      // group = 0;
      // break;
    }
  }

  uint16_t room;
  room = card_block1[2];
  room <<= 8;
  room += card_block1[3];
  uint8_t user_type = card_block1[11];

  uint32_t c_guest_count = 0;
  c_guest_count += card_block4[0];
  c_guest_count <<= 8;
  c_guest_count += card_block4[1];
  c_guest_count <<= 8;
  c_guest_count += card_block4[2];
  c_guest_count <<= 8;
  c_guest_count += card_block4[3];

  uint32_t c_staff_count = 0;
  c_staff_count += card_block4[4];
  c_staff_count <<= 8;
  c_staff_count += card_block4[5];
  c_staff_count <<= 8;
  c_staff_count += card_block4[6];
  c_staff_count <<= 8;
  c_staff_count += card_block4[7];

  uint32_t c_master_count = 0;
  c_master_count += card_block4[8];
  c_master_count <<= 8;
  c_master_count += card_block4[9];
  c_master_count <<= 8;
  c_master_count += card_block4[10];
  c_master_count <<= 8;
  c_master_count += card_block4[11];

  uint32_t c_crc_count = 0;
  c_crc_count += card_block4[12];
  c_crc_count <<= 8;
  c_crc_count += card_block4[13];
  c_crc_count <<= 8;
  c_crc_count += card_block4[14];
  c_crc_count <<= 8;
  c_crc_count += card_block4[15];

  // Check CRC
  uint32_t countersCRC = 0;
  // for (size_t i = 0; i < 12; i++)
  // {
  //   countersCRC += card_block4[i];
  // }
  countersCRC = c_guest_count + c_staff_count + c_master_count;

  if (c_crc_count != countersCRC)
  {
    Serial << "Wrong CRC " << endl;
    error_late = "Guests must have a room!";
    Serial << " Calculated CRC vs CRC in card:" << countersCRC << " - " << c_crc_count << endl;
    return 0;
  }
  Serial << " Calculated CRC vs CRC in card:" << countersCRC << " - " << c_crc_count << endl;

#ifdef USE_RTC
  // 4. Check end time
  if (!compareTime())
  {
    return 0;
  }
#endif
  // USER CARD
  if (user_type == 0)
  {
    Serial << "Guest card" << endl;
    // check guest room
    if (room == 0)
    {
      Serial << "Guests must have a room!" << endl;
      error_late = "Guests must have a room!";
      return 0;
    }
    // TODO: виж docs/bugfix-room-access.md
    else if (room != config.room)
    {
      Serial << "Wrong room! In card " << room << " vs door " << config.room << endl;
      // Check group access for user
      if (!groupAcces(group))
      {
        Serial << "Wrong group!" << endl;
        error_late = "Wrong room!";
        return 0;
      }
      else
      {
        Serial << "Group access for user!" << endl;
      }
    }
    else if (c_guest_count < config.guest_count)
    {
      Serial << "Expired guest counter " << c_guest_count << "! New is:" << config.guest_count << endl;
      error_late = "Expired guest counter!";
      return 0;
    }
    else if (c_guest_count > config.guest_count)
    {
      config.guest_count = c_guest_count;
      Serial << "Update guest count!" << endl;
      saveConfiguration();
    }
  }

  // Serial << "Group in memory vs card:" << config.group << " " << group << endl;
  Serial << "Guest counts in memory vs card:" << config.guest_count << " " << c_guest_count << endl;
  Serial << "Staff counts in memory vs card:" << config.staff_count << " " << c_staff_count << endl;
  Serial << "Master counts in memory vs card:" << config.master_count << " " << c_master_count << endl;
  // tools.dump_byte_array(card_block4, 16);
  // tools.dump_byte_array(card_block4_dec, 16);

  // MASTER CARD

  if (user_type == 255)
  {
    if (analogRead(BATTERY_IN) < BATTERY_TRIGGER)
      sos();
    Serial << "Staff or master user type!" << endl;
  }

  if (group == 0 && user_type == 255)
  {
    Serial << "Global administrator access!" << endl;
    if (c_master_count < config.master_count)
    {
      Serial << "Expired global administrator card!" << endl;
      error_late = "Expired global administrator card!";
      return 0;
    }
    else if (c_master_count > config.master_count)
    {
      config.master_count = c_master_count;
      Serial << "Update administrator count!" << endl;
      saveConfiguration();
    }
    master_access = true; // Only the master has access when DND is set
  }

  // STAFF CARD
  if (group != 0 && user_type == 255)
  {
    Serial << "Staff access! Group in card vs group in memo:" << group << " - " << config.group << endl;
    if (!groupAcces(group))
    {
      Serial << "Wrong group staff card!" << endl;
      error_late = "Wrong group staff card!";
      return 0;
    }
    if (c_staff_count < config.staff_count)
    {
      Serial << "Expired staff card!" << endl;
      error_late = "Expired staff card!";
      return 0;
    }
    else if (c_staff_count > config.staff_count)
    {
      config.staff_count = c_staff_count;
      Serial << "Update staff count!" << endl;
      saveConfiguration();
    }
    // Check group access
  }
  Serial << "User type:" << user_type << endl;

  if (user_type == 0)
  {
    Serial << "Guest user type!" << endl;
  }
  else if (user_type == 255)
  {
    Serial << "Staff or master user type!" << endl;
  }
  else
  {
    Serial << "Unknown user type!" << endl;
    error_late = "Unknown user type!";
    return 0;
  }

  measureTime();
  digitalWrite(LED_RED, 0);
  // 5. Return user id from card
  uint32_t userid = 0;
  userid = card_block1[7];
  userid <<= 8;
  userid += card_block1[8];
  userid <<= 8;
  userid += card_block1[9];
  userid <<= 8;
  userid += card_block1[10];
  Serial << "Found user: " << userid << endl;
  return userid;
}

bool groupAcces(uint64_t group)
{
  if (group == 0)
    return false;
  return (group & config.group) > 0;
}

// ##     ## ########  ########     ###    ######## ########
// ##     ## ##     ## ##     ##   ## ##      ##    ##
// ##     ## ##     ## ##     ##  ##   ##     ##    ##
// ##     ## ########  ##     ## ##     ##    ##    ######
// ##     ## ##        ##     ## #########    ##    ##
// ##     ## ##        ##     ## ##     ##    ##    ##
//  #######  ##        ########  ##     ##    ##    ########

/**
 * @brief Activate update and config
 *
 */
void activateUpdate()
{

  Serial << "Activate Update" << endl;
  awake_update = true;
  setSound(100);
  tools.dump_byte_array(card_block4, 16);
  tools.dump_byte_array(card_block5, 16);
  tools.dump_byte_array(card_block6, 16);

  // SSID and PASS
  for (size_t i = 0; i < 16; i++)
  {
    config.ssid[i] = card_block4[i];
    config.pass[i] = card_block5[i];
    // AES key from card
    config.aes_key[i] = card_block1[i];
  }

  Serial << "SSID:" << config.ssid << endl;
  Serial << "PASS:" << config.pass << endl;
  // MQTT from card
  sprintf(config.mqtt, "%d.%d.%d.%d", card_block6[0], card_block6[1], card_block6[2], card_block6[3]);
  Serial << "mqtt_server:" << config.mqtt << endl;
  config.mqtt_port = card_block6[4] * 256 + card_block6[5];
  Serial << "mqtt_port:" << config.mqtt_port << endl;

  bool update = true;
  if (card_block6[15] == 0xFF)
  {
    update = false;
    awake_update = false;
  }
  // connectWiFi();
  factory_mode = true;
  Serial.println("Update process ...");
  if (update)
    insomnia = true;
  if (!SPIFFS.begin(false))
  {
    Serial.println("An Error has occurred while mounting SPIFFS");
    errorSound();
    errorSound();
    esp_restart();
    return;
  }
  saveConfiguration();
  connectWiFi();
  connectBroker();
  responseAlive();
  sendLog();
  if (update)
  {
    // You can remove the password parameter if you want the AP to be open.
    WiFi.softAP(config.apssid, config.appass);
    IPAddress myIP = WiFi.softAPIP();
    Serial.print(F("AP IP address: "));
    Serial.println(myIP);
    Serial.println("Server started");
    WiFi.macAddress(MAC_array);
    for (int i = 0; i < sizeof(MAC_array); ++i)
    {
      sprintf(MAC_char, "%s%02x", MAC_char, MAC_array[i]);
    }
    Serial.println(MAC_char);
    startServer();
  }
  setSound(200);
}

//  ######  ######## ########     ######  ##        #######   ######  ##    ##
// ##    ## ##          ##       ##    ## ##       ##     ## ##    ## ##   ##
// ##       ##          ##       ##       ##       ##     ## ##       ##  ##
//  ######  ######      ##       ##       ##       ##     ## ##       #####
//       ## ##          ##       ##       ##       ##     ## ##       ##  ##
// ##    ## ##          ##       ##    ## ##       ##     ## ##    ## ##   ##
//  ######  ########    ##        ######  ########  #######   ######  ##    ##

bool corectTime()
{

#ifdef OFFLINE
  return true;
#endif
  Serial << "Check clock" << endl;
  if ((config.today == 0) || (config.today != rtc.getDay()))
  {
    Serial << "today is " << config.today << " time clock is " << rtc.getDay() << endl;
    awake_timer = millis();
    connectWiFi();
    awake_timer = millis();
    connectBroker();
    awake_timer = millis();
    responseAlive();
    awake_timer = millis();
    sendLog();
    ready_to_sleep = false;
    return false;
  }
  return true;
}

//  ######  ######## ##    ## ########     ##        #######   ######
// ##    ## ##       ###   ## ##     ##    ##       ##     ## ##    ##
// ##       ##       ####  ## ##     ##    ##       ##     ## ##
//  ######  ######   ## ## ## ##     ##    ##       ##     ## ##   ####
//       ## ##       ##  #### ##     ##    ##       ##     ## ##    ##
// ##    ## ##       ##   ### ##     ##    ##       ##     ## ##    ##
//  ######  ######## ##    ## ########     ########  #######   ######

void saveEvent(uint64_t user_id)
{
  if (!SPIFFS.begin(false))
  {
    Serial.println(F("Card Mount Failed"));
    errorSound();
    return;
  }

  // dd-MM-yyyy HH:mm:ss EET
  // get time from external clock
  char stime[22];
  sprintf(stime, "%02d-%02d-%d, %02d:%02d:%02d", rtc.getDay(), rtc.getMonth(), rtc.getYear(), rtc.getHour(), rtc.getMinute(), rtc.getSecond());
  sprintf(operations_buffer, "%llu, %s, %010llu, b%d \n", user_id, stime, access_late_card, battery);
  filesys.appendFile(SPIFFS, "/log.csv", operations_buffer);
  // filesys.listDir(SPIFFS, "/", 0);
  filesys.printFile(SPIFFS, "/log.csv");
}

void setRoomHotelGroup(String payload)
{
  StaticJsonDocument<500> doc;
  DeserializationError error = deserializeJson(doc, payload.c_str());
  if (error)
  {
    Serial.print(F("deserializeJson() failed: "));
    Serial.println(error.f_str());
    return;
  }

  if (doc.containsKey("dhcp"))
  {
    config.dhcp = doc["dhcp"];
    Serial << "new dhcp:" << config.dhcp << endl;
  }
  if (doc.containsKey("mqtt"))
  {
    strlcpy(config.mqtt, doc["mqtt"], sizeof(config.mqtt));
    Serial << "new mqtt:" << config.mqtt << endl;
  }
  if (doc.containsKey("mqtt_port"))
  {
    config.mqtt_port = doc["mqtt_port"];
    Serial << "new mqtt_port:" << config.mqtt_port << endl;
  }
  if (doc.containsKey("relay_time"))
  {
    config.relay_time = doc["relay_time"];
    Serial << "new relay_time:" << config.relay_time << endl;
  }
  if (doc.containsKey("sound"))
  {
    config.sound = doc["sound"];
    Serial << "new sound:" << config.sound << endl;
  }
  if (doc.containsKey("name"))
  {
    strlcpy(config.name, doc["name"], sizeof(config.name));
    Serial << "new name:" << config.name << endl;
  }
  if (doc.containsKey("ssid"))
  {
    strlcpy(config.ssid, doc["ssid"], sizeof(config.ssid));
    Serial << "new ssid:" << config.ssid << endl;
  }
  if (doc.containsKey("pass"))
  {
    strlcpy(config.pass, doc["pass"], sizeof(config.pass));
    Serial << "new pass:" << config.pass << endl;
  }
  if (doc.containsKey("apssid"))
  {
    strlcpy(config.apssid, doc["apssid"], sizeof(config.apssid));
    Serial << "new apssid:" << config.name << endl;
  }
  if (doc.containsKey("appass"))
  {
    strlcpy(config.appass, doc["appass"], sizeof(config.appass));
    Serial << "new appass:" << config.appass << endl;
  }
  if (doc.containsKey("hotel_code"))
  {
    config.hotel_code = doc["hotel_code"];
    Serial << "new hotel code:" << config.hotel_code << endl;
  }
  if (doc.containsKey("room"))
  {
    config.room = doc["room"];
    Serial << "new room:" << config.room << endl;
  }
  if (doc.containsKey("group"))
  {
    config.group = doc["group"];
    Serial << "new group:" << config.group << endl;
  }
  if (doc.containsKey("guest_count"))
  {
    config.guest_count = doc["guest_count"];
    Serial << "new guest_count:" << config.guest_count << endl;
  }
  if (doc.containsKey("staff_count"))
  {
    config.staff_count = doc["staff_count"];
    Serial << "new staff_count:" << config.staff_count << endl;
  }
  if (doc.containsKey("master_count"))
  {
    config.master_count = doc["master_count"];
    Serial << "new master_count:" << config.master_count << endl;
  }
  if (doc.containsKey("update_code"))
  {
    strlcpy(config.update_code, doc["update_code"], sizeof(config.update_code));
    Serial << "new update_code:" << config.update_code << endl;
  }

  saveConfiguration();
}

void setClock(String msg)
{
  // TODO check if valid time
  Serial << "Clock" << endl;
  // timesync/13-01-2022 08:03:26
  int mn = msg.substring(14, 16).toInt();
  int hr = msg.substring(11, 13).toInt();
  int dy = msg.substring(0, 2).toInt();
  int mt = msg.substring(3, 5).toInt();
  int yr = msg.substring(6, 10).toInt();
  int ss = msg.substring(17, 19).toInt();
  // Serial<<msg[1].substring(0, 2)<<" "<<msg[1].substring(3, 5).toInt()<<endl;
  // Serial<<msg[1].substring(6, 10).toInt()<<" "<<msg[1].substring(11, 13).toInt()<<endl;
  // Serial<<msg[1].substring(14, 16).toInt()<<" "<<msg[1].substring(17, 19).toInt()<<endl;

  // setTime(int sc, int mn, int hr, int dy, int mt, int yr, int ms)
  // time_clock.setTime(ss, mn, hr, dy, mt, yr, 0);
  // set time to external clock
  rtc.setDate(0, dy, mt, yr);
  rtc.setTime(hr, mn, ss);

  config.today = (uint8_t)dy; // Once a day time update
  Serial << "The clock is set" << endl;
}

//    ###     ######  ######## #### ##     ##    ###    ######## ########       ###    ########
//   ## ##   ##    ##    ##     ##  ##     ##   ## ##      ##    ##            ## ##   ##     ##
//  ##   ##  ##          ##     ##  ##     ##  ##   ##     ##    ##           ##   ##  ##     ##
// ##     ## ##          ##     ##  ##     ## ##     ##    ##    ######      ##     ## ########
// ######### ##          ##     ##   ##   ##  #########    ##    ##          ######### ##
// ##     ## ##    ##    ##     ##    ## ##   ##     ##    ##    ##          ##     ## ##
// ##     ##  ######     ##    ####    ###    ##     ##    ##    ########    ##     ## ##

void activateAP()
{
  Serial << "Activate AP" << endl;
  awake_update = true;
  factory_mode = true;
  Serial.println("Update process ...");
  insomnia = true;
  if (!SPIFFS.begin(false))
  {
    Serial.println("An Error has occurred while mounting SPIFFS");
    errorSound();
    errorSound();
    esp_restart();
    return;
  }
  WiFi.softAP(config.apssid, config.appass);
  IPAddress myIP = WiFi.softAPIP();
  Serial.print(F("AP IP address: "));
  Serial.println(myIP);
  Serial.println("Server started");
  WiFi.macAddress(MAC_array);
  for (int i = 0; i < sizeof(MAC_array); ++i)
  {
    sprintf(MAC_char, "%s%02x", MAC_char, MAC_array[i]);
  }
  Serial.println(MAC_char);
  startServer();
  setSound(200);
}

// ######## #### ##     ## ########    ##     ## ########  ########     ###    ######## ########
//    ##     ##  ###   ### ##          ##     ## ##     ## ##     ##   ## ##      ##    ##
//    ##     ##  #### #### ##          ##     ## ##     ## ##     ##  ##   ##     ##    ##
//    ##     ##  ## ### ## ######      ##     ## ########  ##     ## ##     ##    ##    ######
//    ##     ##  ##     ## ##          ##     ## ##        ##     ## #########    ##    ##
//    ##     ##  ##     ## ##          ##     ## ##        ##     ## ##     ##    ##    ##
//    ##    #### ##     ## ########     #######  ##        ########  ##     ##    ##    ########

void activateCardTimeUpdate()
{
  Serial << "Time update by brocker" << endl;
  startSound();
  // SSID and PASS
  for (size_t i = 0; i < 16; i++)
  {
    config.ssid[i] = card_block4[i];
    config.pass[i] = card_block5[i];
    // AES key from card
    config.aes_key[i] = card_block1[i];
  }

  // MQTT from card
  sprintf(config.mqtt, "%d.%d.%d.%d", card_block6[0], card_block6[1], card_block6[2], card_block6[3]);
  Serial << "mqtt_server:" << config.mqtt << endl;
  config.mqtt_port = card_block6[4] * 256 + card_block6[5];
  Serial << "mqtt_port:" << config.mqtt_port << endl;

  if (!SPIFFS.begin(false))
  {
    Serial.println("An Error has occurred while mounting SPIFFS");
    errorSound();
    errorSound();
    esp_restart();
    return;
  }
  stopSound();

  digitalWrite(ZUMER, 1);
  delay(50);
  digitalWrite(ZUMER, 0);

  connectWiFi();

  digitalWrite(ZUMER, 1);
  delay(100);
  digitalWrite(ZUMER, 0);

  connectBroker();
  // sendLastEvent();
  sendLog();
  responseAlive();

  setSound(50);
  delay(100);
}

void activateOtaCheck()
{
  Serial << "OTA check by card" << endl;
  startSound();
  // Read WiFi / MQTT config from card blocks (same layout as time card)
  for (size_t i = 0; i < 16; i++)
  {
    config.ssid[i]    = card_block4[i];
    config.pass[i]    = card_block5[i];
    config.aes_key[i] = card_block1[i];
  }
  sprintf(config.mqtt, "%d.%d.%d.%d",
          card_block6[0], card_block6[1], card_block6[2], card_block6[3]);
  config.mqtt_port = card_block6[4] * 256 + card_block6[5];

  if (!SPIFFS.begin(false))
  {
    Serial.println("SPIFFS mount failed");
    errorSound();
    return;
  }
  stopSound();

  connectWiFi();
  if (!WiFi.isConnected())
  {
    errorSound();
    return;
  }

  connectBroker();
  sendLog();

  // Notify server: MAC/IP/version — server creates OTA job and sends start_web if update needed
  String payload = String(MAC_char) + "/" + IP.toString() + "/" + String(version);
  mqttClient.publish("check_update", 1, false, payload.c_str());

  responseAlive();
  awake_timer = millis(); // fresh 5-second window for start_web to arrive

  setSound(50);
  delay(100);
}

void activateCardNTPTimeUpdate()
{
  Serial << "Timeupdate by NTP" << endl;
  startSound();
  // SSID and PASS
  for (size_t i = 0; i < 16; i++)
  {
    config.ssid[i] = card_block4[i];
    config.pass[i] = card_block5[i];
    // AES key from card
    config.aes_key[i] = card_block1[i];
  }

  if (!SPIFFS.begin(false))
  {
    Serial.println("An Error has occurred while mounting SPIFFS");
    errorSound();
    errorSound();
    esp_restart();
    return;
  }

  digitalWrite(ZUMER, 1);
  delay(50);
  digitalWrite(ZUMER, 0);

  connectWiFi();

  digitalWrite(ZUMER, 1);
  delay(100);
  digitalWrite(ZUMER, 0);

  // NTP update
  timeClient.begin();
  // timeClient.setTimeOffset(10800);

  int awake_timer = millis();
  while (!timeClient.update())
  {
    timeClient.forceUpdate();
    if (millis() - awake_timer > WIFI_TIME)
    {
      Serial << "NTP failure" << endl;
      goSleep();
    }
  }

  String formattedDate = timeClient.getFormattedDate();
  Serial.println(formattedDate);

  // 2022-11-08T18:04:17Z
  int mt = formattedDate.substring(5, 7).toInt();
  int hr = formattedDate.substring(11, 13).toInt();
  int dy = formattedDate.substring(8, 10).toInt();
  int mn = formattedDate.substring(14, 16).toInt();
  int yr = formattedDate.substring(0, 4).toInt();
  int ss = formattedDate.substring(17, 19).toInt();
  Serial << dy << "-" << mt << "-" << yr << " " << hr << ":" << mn << ":" << ss << endl;

  rtc.setDate(0, dy, mt, yr);
  rtc.setTime(hr, mn, ss);

  config.today = (uint8_t)dy; // Once a day time update if configured
  Serial << "The clock is set" << endl;
  digitalWrite(ZUMER, 1);
  delay(150);
  digitalWrite(ZUMER, 0);

  setSound(50);
  delay(100);
}

// void sendLastEvent()
// {
//   publishUserAcces(access_late_user, access_late_card);
//   to_send_late = false;
// }

//  ######  ##     ##    ###    ##    ##  ######   ########    ##    ## ######## ##    ##
// ##    ## ##     ##   ## ##   ###   ## ##    ##  ##          ##   ##  ##        ##  ##
// ##       ##     ##  ##   ##  ####  ## ##        ##          ##  ##   ##         ####
// ##       ######### ##     ## ## ## ## ##   #### ######      #####    ######      ##
// ##       ##     ## ######### ##  #### ##    ##  ##          ##  ##   ##          ##
// ##    ## ##     ## ##     ## ##   ### ##    ##  ##          ##   ##  ##          ##
//  ######  ##     ## ##     ## ##    ##  ######   ########    ##    ## ########    ##

void changeUpdateKey()
{
  Serial << "Changed Update Key to:";
  for (size_t i = 0; i < 16; i++)
  {
    // New update key from card
    config.update_code[i] = card_block1[i];
  }
  Serial << "Changed Update Key" << endl;
  if (!SPIFFS.begin(false))
  {
    Serial.println("An Error has occurred while mounting SPIFFS");
    errorSound();
    errorSound();
    esp_restart();
    return;
  }
  saveConfiguration();
  setSound(200);
  delay(200);
  goSleep();
}

void readConfig()
{
  Serial.println("Reading config file");
  String doc = "";
  File file = SPIFFS.open(CONNFIG_FILE);
  if (!file)
  {
    Serial.println("Failed to open file for reading");
    return;
  }
  while (file.available())
  {
    doc += char(file.read());
  }

  file.close();

  String myString = "config/";
  myString.concat(MAC_char);
  char myCharArray[myString.length() + 1];
  myString.toCharArray(myCharArray, myString.length() + 1);
  Serial.println(myCharArray);

  sprintf(out_message_buffer, "%s ", doc.c_str()); // unknown user
  mqttClient.publish(myCharArray, 0, false, out_message_buffer);
}

void startSound()
{
  ledcWriteTone(0, 1000); // Start generating a tone with a frequency of 1000 Hz
}

void stopSound()
{
  ledcWriteTone(0, 0); // Stop generating the tone
}

void setupSound()
{
  ledcSetup(0, 4000, 8);   // Configure LEDC channel 0 with a frequency of 2000 Hz and 8-bit resolution
  ledcAttachPin(ZUMER, 0); // Attach the PWM pin to LEDC channel 0
}

/**
 * @brief Clear log.txt
 *
 */
void clearLog()
{
  if (!SPIFFS.begin(false))
  {
    Serial.println(F("Card Mount Failed"));
    errorSound();
    return;
  }
  filesys.writeFile(SPIFFS, "/log.csv", ""); // Empty file
}

void soundExit()
{
  digitalWrite(ZUMER, 1);
  delay(100);
  digitalWrite(ZUMER, 0);
}

void lowBatteryAlert()
{
  for (uint8_t i = 0; i < 4; i++)
  {
    digitalWrite(LED_RED, 1);
    startSound();
    delay(120);
    stopSound();
    digitalWrite(LED_RED, 0);
    delay(120);
  }
}

void checkSpecialCards()
{
  if (tools.compareByteArrays(16, card_block2, config.update_code))
  {
    if (card_block6[15] == 0xFF)
    {
      activateAP(); // no broker
    }
    else if (card_block6[15] == 0x00)
    {
      activateUpdate();
    }
    else if (card_block6[15] == 0x55)
    {
      activateCardTimeUpdate(); // Get time using broker
    }
    else if (card_block6[15] == 0x56)
    {
      activateOtaCheck(); // Check for OTA update via server
    }
    else if (card_block6[15] == 0x5A)
    {
      activateCardNTPTimeUpdate(); // Get time using NTP server
    }
    else if (card_block6[15] == 0xA5)
    {
      changeUpdateKey();
    }
    else if (card_block6[15] == 0xAA)
    {
      /* RESET COUNTERS */
      config.guest_count = 1;
      config.staff_count = 1;
      config.master_count = 1;
      saveConfiguration();
      setSound(200);
      delay(200);
    }
    return;
  }
}

void handleTimeData(AsyncWebServerRequest *request)
{
  String message = "";
  String hour;
  String minutes;
  String year;
  String month;
  String day;

  if (request->hasParam(PARAM_HOUR, true))
  {
    hour = request->getParam(PARAM_HOUR, true)->value();
  }
  if (request->hasParam(PARAM_MINUTES, true))
  {
    minutes = request->getParam(PARAM_MINUTES, true)->value();
  }
  if (request->hasParam(PARAM_YEAR, true))
  {
    year = request->getParam(PARAM_YEAR, true)->value();
  }
  if (request->hasParam(PARAM_MONTH, true))
  {
    month = request->getParam(PARAM_MONTH, true)->value();
  }
  if (request->hasParam(PARAM_DAY, true))
  {
    day = request->getParam(PARAM_DAY, true)->value();
  }
  Serial << "Set time to " << hour << ":" << minutes << endl;
  //  setTime(hour, minute, sec);
  rtc.setDate(0, tools.StringTo64(day), tools.StringTo64(month), tools.StringTo64(year));
  rtc.setTime(tools.StringTo64(hour), tools.StringTo64(minutes), 0); // 24H mode, ex. 6:54:00
  printLocalTime();
  request->send(SPIFFS, "/index.html", String(), false, processor);
}

void specialCardCheck()
{
  if (card_block6[15] == 0xFF)
  {
    activateAP(); // no broker
  }
  else if (card_block6[15] == 0x00)
  {
    activateUpdate();
  }
  else if (card_block6[15] == 0x55)
  {
    activateCardTimeUpdate(); // Get time using broker
  }
  else if (card_block6[15] == 0x56)
  {
    activateOtaCheck(); // Check for OTA update via server
  }
  else if (card_block6[15] == 0x5A)
  {
    activateCardNTPTimeUpdate(); // Get time using NTP server
  }
  else if (card_block6[15] == 0xA5)
  {
    changeUpdateKey();
  }
  else if (card_block6[15] == 0xAA)
  {
    /* RESET COUNTERS */
    config.guest_count = 1;
    config.staff_count = 1;
    config.master_count = 1;
    saveConfiguration();
    setSound(200);
    delay(200);
  }
}

/**
 * Check for valid time
 */
bool compareTime()
{
  Serial << "Current time is:" << endl;
  printLocalTime();
  uint16_t end_year = tools.bcd2bin(card_block2[0]) * 100 + tools.bcd2bin(card_block2[1]);
  timeMeasure = millis();
  if (rtc.getYear() > end_year)
  {
    Serial << "Expired year in card:" << end_year << endl;
    error_late = "Expired year!";
    return false;
  }
  else if (rtc.getYear() < end_year)
  {
    return true;
  } // continue if year is the same

  uint8_t month = tools.bcd2bin(card_block2[2]);
  if (rtc.getMonth() > month)
  {
    Serial << "Expired month in card:" << month << endl;
    error_late = "Expired month!";
    return false;
  }
  else if (rtc.getMonth() < month)
  {
    return true;
  }

  uint8_t day = tools.bcd2bin(card_block2[3]);
  if (rtc.getDay() > day)
  {
    Serial << "Expired day in card:" << day << endl;
    error_late = "Expired day!";
    return false;
  }
  else if (rtc.getDay() < day)
  {
    return true;
  }
  uint8_t hour = tools.bcd2bin(card_block2[4]);
  if (rtc.getHour() > hour)
  {
    Serial << "Expired hour in card:" << hour << endl;
    error_late = "Expired hour!";
    return false;
  }
  else if (rtc.getHour() < hour)
  {
    return true;
  }

  uint8_t minutes = tools.bcd2bin(card_block2[5]);
  if (rtc.getMinute() > minutes)
  {
    Serial << "Expired minutes in card:" << minutes << endl;
    error_late = "Expired minutes!";
    return false;
  }
  return true;
}

/**
 * @brief Send log to broker and delete from file system
 *
 */
void sendLog()
{
  Serial << "Log sending..." << endl;
  if (!SPIFFS.begin(false))
  {
    Serial.println(F("Card Mount Failed"));
    errorSound();
    return;
  }
  char topic[50];
  filesys.readFile(operations_buffer, "/log.csv");
  Serial << operations_buffer << endl;
  sprintf(topic, "log/%s", MAC_char);
  Serial << "topic:" << topic << endl;
  if (mqttClient.connected())
  {
    mqttClient.publish(topic, 0, false, operations_buffer);
    // Empty file
    filesys.writeFile(SPIFFS, "/log.csv", "");
  }
}

void restart()
{
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_34, 1); // wakeup on proximity enable
  esp_deep_sleep_start();
}

void sos()
{
  startSound();
  delay(50);
  stopSound();
  delay(50);
  startSound();
  delay(50);
  stopSound();
  delay(50);
  startSound();
  delay(50);
  stopSound();
  delay(100);

  startSound();
  delay(100);
  stopSound();
  delay(50);
  startSound();
  delay(100);
  stopSound();
  delay(50);
  startSound();
  delay(100);
  stopSound();
  delay(100);

  startSound();
  delay(50);
  stopSound();
  delay(50);
  startSound();
  delay(50);
  stopSound();
  delay(50);
  startSound();
  delay(50);
  stopSound();
  delay(100);
}