#include <Arduino.h>
#include <Wire.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <Adafruit_PN532.h>

#if __has_include("l484_door_config.h")
  #include "l484_door_config.h"
#else
  #include "l484_door_config.example.h"
#endif

Adafruit_PN532 nfc(PN532_IRQ_PIN, PN532_RESET_PIN, &Wire);

static unsigned long lastReadAt = 0;
static unsigned long lastWifiAttemptAt = 0;
static unsigned long lastPn532InitAt = 0;
static unsigned long lastPn532HealthAt = 0;
static String lastCredential = "";
static bool wifiScanPrinted = false;
static bool pn532Ready = false;
static uint16_t emptyReadCount = 0;

const unsigned long WIFI_RETRY_MS = 15000;
const unsigned long PN532_INIT_RETRY_MS = 5000;
const unsigned long PN532_HEALTH_CHECK_MS = 120000;
const uint16_t PN532_EMPTY_READ_REINIT_COUNT = 300;

String bytesToHex(const uint8_t *data, uint8_t length) {
  String out;
  out.reserve(length * 2);
  for (uint8_t i = 0; i < length; i++) {
    if (data[i] < 0x10) out += "0";
    out += String(data[i], HEX);
  }
  out.toUpperCase();
  return out;
}

String jsonEscape(const String &value) {
  String out;
  out.reserve(value.length() + 8);
  for (size_t i = 0; i < value.length(); i++) {
    char c = value[i];
    if (c == '"' || c == '\\') out += '\\';
    out += c;
  }
  return out;
}

const char *wifiStatusName(wl_status_t status) {
  switch (status) {
    case WL_IDLE_STATUS:
      return "idle";
    case WL_NO_SSID_AVAIL:
      return "ssid not available";
    case WL_SCAN_COMPLETED:
      return "scan completed";
    case WL_CONNECTED:
      return "connected";
    case WL_CONNECT_FAILED:
      return "connect failed";
    case WL_CONNECTION_LOST:
      return "connection lost";
    case WL_DISCONNECTED:
      return "disconnected";
    default:
      return "unknown";
  }
}

const char *encryptionName(wifi_auth_mode_t type) {
  switch (type) {
    case WIFI_AUTH_OPEN:
      return "open";
    case WIFI_AUTH_WEP:
      return "WEP";
    case WIFI_AUTH_WPA_PSK:
      return "WPA";
    case WIFI_AUTH_WPA2_PSK:
      return "WPA2";
    case WIFI_AUTH_WPA_WPA2_PSK:
      return "WPA/WPA2";
    case WIFI_AUTH_WPA2_ENTERPRISE:
      return "WPA2 enterprise";
    case WIFI_AUTH_WPA3_PSK:
      return "WPA3";
    case WIFI_AUTH_WPA2_WPA3_PSK:
      return "WPA2/WPA3";
    default:
      return "unknown";
  }
}

void printWifiScan() {
  if (wifiScanPrinted) return;
  wifiScanPrinted = true;

  Serial.println("Scanning Wi-Fi networks...");
  int count = WiFi.scanNetworks(false, true);
  if (count < 0) {
    Serial.printf("Wi-Fi scan failed: %d\n", count);
    return;
  }

  bool found = false;
  for (int i = 0; i < count; i++) {
    if (WiFi.SSID(i) != WIFI_SSID) continue;
    found = true;
    Serial.printf(
      "Found %s: RSSI %d dBm, channel %d, auth %s\n",
      WIFI_SSID,
      WiFi.RSSI(i),
      WiFi.channel(i),
      encryptionName(WiFi.encryptionType(i))
    );
  }

  if (!found) {
    Serial.printf("SSID %s was not found in scan.\n", WIFI_SSID);
  }
}

void connectWifi() {
  if (WiFi.status() == WL_CONNECTED) return;

  unsigned long now = millis();
  if (lastWifiAttemptAt && now - lastWifiAttemptAt < WIFI_RETRY_MS) return;
  lastWifiAttemptAt = now;

  WiFi.persistent(false);
  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  printWifiScan();

  Serial.printf("Connecting to Wi-Fi %s", WIFI_SSID);
  WiFi.disconnect(false, false);
  delay(100);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  unsigned long startedAt = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - startedAt < 30000) {
    Serial.print(".");
    delay(350);
  }
  Serial.println();

  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("Wi-Fi connected: ");
    Serial.println(WiFi.localIP());
  } else {
    wl_status_t status = WiFi.status();
    Serial.printf("Wi-Fi connection failed: %s (%d).\n", wifiStatusName(status), status);
  }
}

bool initializePn532(bool force = false) {
  unsigned long now = millis();
  if (!force && lastPn532InitAt && now - lastPn532InitAt < PN532_INIT_RETRY_MS) return pn532Ready;
  lastPn532InitAt = now;
  pn532Ready = false;

  Serial.println("Initializing PN532...");
  pinMode(PN532_RESET_PIN, OUTPUT);
  digitalWrite(PN532_RESET_PIN, LOW);
  delay(25);
  digitalWrite(PN532_RESET_PIN, HIGH);
  delay(120);

  Wire.end();
  delay(25);
  Wire.begin(PN532_SDA_PIN, PN532_SCL_PIN);
  Wire.setClock(100000);
  Wire.setTimeOut(60);

  nfc.begin();
  uint32_t version = nfc.getFirmwareVersion();
  if (!version) {
    Serial.println("PN532 not found. Will retry without stopping controller.");
    return false;
  }

  nfc.SAMConfig();
  emptyReadCount = 0;
  lastPn532HealthAt = now;
  pn532Ready = true;
  Serial.println("PN532 ready.");
  return true;
}

void maintainPn532() {
  unsigned long now = millis();
  if (!pn532Ready) {
    initializePn532();
    return;
  }

  if (now - lastPn532HealthAt < PN532_HEALTH_CHECK_MS) return;
  lastPn532HealthAt = now;

  uint32_t version = nfc.getFirmwareVersion();
  if (!version) {
    Serial.println("PN532 health check failed; reinitializing.");
    initializePn532(true);
  }
}

bool readCardUid(uint8_t *uid, uint8_t *uidLength) {
  if (!pn532Ready && !initializePn532()) return false;
  return nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, uidLength, 350);
}

String readMifareSecret(uint8_t *uid, uint8_t uidLength) {
  uint8_t data[16];
  uint8_t key[6];
  memcpy(key, MIFARE_KEY_A, sizeof(key));

  bool authed = nfc.mifareclassic_AuthenticateBlock(uid, uidLength, MIFARE_SECRET_BLOCK, 0, key);
  if (!authed) {
    Serial.println("MIFARE auth failed.");
    return "";
  }

  bool readOk = nfc.mifareclassic_ReadDataBlock(MIFARE_SECRET_BLOCK, data);
  if (!readOk) {
    Serial.println("MIFARE block read failed.");
    return "";
  }

  String secret;
  for (uint8_t i = 0; i < sizeof(data); i++) {
    if (data[i] == 0x00) break;
    if (data[i] >= 32 && data[i] <= 126) secret += static_cast<char>(data[i]);
  }
  secret.trim();
  return secret;
}

String readCredential() {
  uint8_t uid[7] = { 0 };
  uint8_t uidLength = 0;
  if (!readCardUid(uid, &uidLength)) {
    emptyReadCount++;
    if (emptyReadCount >= PN532_EMPTY_READ_REINIT_COUNT) {
      Serial.println("PN532 idle/read timeout threshold reached; reinitializing.");
      initializePn532(true);
    }
    return "";
  }

  emptyReadCount = 0;

#if CARD_MODE == CARD_MODE_MIFARE_CLASSIC_BLOCK
  String secret = readMifareSecret(uid, uidLength);
  if (secret.length() >= 6) return secret;
  Serial.println("No usable card secret found.");
  return "";
#else
  return bytesToHex(uid, uidLength);
#endif
}

bool requestAccess(const String &credential) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("No Wi-Fi; deny.");
    return false;
  }

  HTTPClient http;
  http.setTimeout(ACCESS_TIMEOUT_MS);
  http.begin(L484_ACCESS_URL);
  http.addHeader("Content-Type", "application/json");
  http.addHeader("X-Controller-Token", L484_CONTROLLER_TOKEN);

  String body = "{\"doorId\":\"" + jsonEscape(L484_DOOR_ID) + "\",\"cardCredential\":\"" + jsonEscape(credential) + "\"}";
  int status = http.POST(body);
  String response = http.getString();
  http.end();

  Serial.printf("Access API status: %d\n", status);
  Serial.println(response);

  if (status < 200 || status >= 300) return false;
  return response.indexOf("\"allow\":true") >= 0;
}

void setup() {
  Serial.begin(115200);
  delay(200);

  connectWifi();

  initializePn532(true);
}

void loop() {
  connectWifi();
  maintainPn532();

  String credential = readCredential();
  if (!credential.length()) {
    delay(120);
    return;
  }

  unsigned long now = millis();
  if (credential == lastCredential && now - lastReadAt < READ_COOLDOWN_MS) {
    delay(120);
    return;
  }

  lastCredential = credential;
  lastReadAt = now;

  Serial.print("Card credential: ");
  Serial.println(credential);

  bool allowed = requestAccess(credential);
  Serial.println(allowed ? "ACCESS ALLOWED" : "ACCESS DENIED");

  delay(READ_COOLDOWN_MS);
}
