/* ======================================================================
   SIMAP — Mesin Absensi RFID 125 kHz (RDM6300)
   ESP32 DevKit v1 + RDM6300 + LCD 16x2 I2C + RTC DS3231 + buzzer + 2 LED

   Berbeda dari RC522, RDM6300 membaca kartu EM4100 125 kHz dan hanya
   mengeluarkan nomor ID — tidak ada memori kartu yang bisa dibaca atau
   ditulis. Untuk absensi sekolah itu sudah cukup: yang diperlukan hanya
   nomor unik tiap kartu.

   Komunikasinya lewat serial 9600 bps, bukan SPI. Tiap kartu dikirim
   sebagai bingkai 14 byte:
       [0x02][10 digit heks ASCII][2 digit checksum][0x03]
   Sepuluh digit itu terdiri atas 2 digit versi dan 8 digit nomor kartu.

   Pustaka (Library Manager):
     LiquidCrystal_I2C by Frank de Brabander
     RTClib by Adafruit
     ArduinoJson by Benoit Blanchon (v7)
   Tidak perlu pustaka RFID — protokolnya dibaca langsung.
   ====================================================================== */

#include <WiFi.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <RTClib.h>
#include <ArduinoJson.h>
#include <Preferences.h>
#include <time.h>

// ---------------------------------------------------------- KONFIGURASI
const char* WIFI_SSID    = "NAMA_WIFI_SEKOLAH";
const char* WIFI_PASS    = "SANDI_WIFI";

const char* SERVER_URL   = "https://sekolah.sch.id";   // tanpa garis miring akhir
const char* DEVICE_KODE  = "GERBANG-01";
const char* DEVICE_KEY   = "ganti-dengan-api-key-dari-menu-kartu-rfid";
const char* FIRMWARE_VER = "1.1.0-rdm6300";

/* Bentuk nomor kartu yang dikirim ke server.
   DESIMAL  → 10 digit desimal, mis. 0006168273. Ini yang dipakai kebanyakan
              pembaca USB desktop, jadi kartu yang didaftarkan lewat komputer
              akan cocok dengan yang ditempelkan di gerbang.
   HEKS     → 8 digit heks apa adanya dari kartu, mis. 005E24D1.
   Ubah hanya bila kartu terlanjur didaftarkan dengan format heks. */
const bool KIRIM_DESIMAL = true;

/* Setel false bila mesin dipasang tanpa LED hijau dan merah.
   Isyarat keberhasilan tetap terbaca dari layar LCD dan bunyi buzzer,
   jadi mesin tetap berfungsi penuh tanpa lampu apa pun. */
const bool PAKAI_LED = true;

const uint32_t JEDA_TAP_SAMA_MS = 5000;
const uint32_t JEDA_PING_MS     = 60000;
const uint16_t TIMEOUT_HTTP_MS  = 6000;
const uint8_t  MAKS_ANTREAN     = 60;

// ------------------------------------------------------------- PIN PETA
/* PENTING: keluaran TX modul bertegangan 5V, sedangkan pin ESP32 hanya
   tahan 3,3V. Pasang pembagi tegangan dua resistor pada jalur ini —
   lihat PEMASANGAN.md. Menyambung langsung sering terlihat berhasil,
   tetapi memperpendek umur pin dan dapat merusaknya. */
#define PIN_RDM_RX  16    // TX modul RDM6300 → (pembagi tegangan) → RX2 ESP32
#define PIN_RDM_TX  17    // tidak dipakai, RDM6300 hanya mengirim
#define PIN_BUZZER  4
#define PIN_LED_OK  2
#define PIN_LED_ERR 15
// I2C bawaan: SDA 21, SCL 22

HardwareSerial RdmSerial(2);          // UART2
LiquidCrystal_I2C lcd(0x27, 16, 2);
RTC_DS3231 rtc;
Preferences simpanan;

bool adaRtc = false, adaLcd = true;
String uidTerakhir = "";
uint32_t waktuTapTerakhir = 0, waktuPingTerakhir = 0;
String namaSekolah = "SIMAP";
uint8_t jumlahAntrean = 0;

// Penampung bingkai RDM6300
uint8_t bufBingkai[14];
uint8_t isiBuf = 0;

String waktuSekarang();

// ===================================================================
//  Tampilan
// ===================================================================
void tampil(const String& b1, const String& b2 = "") {
  if (!adaLcd) return;
  lcd.clear();
  lcd.setCursor(0, 0); lcd.print(b1.substring(0, 16));
  if (b2.length()) { lcd.setCursor(0, 1); lcd.print(b2.substring(0, 16)); }
}

void bunyi(uint8_t n, uint16_t ms = 90) {
  for (uint8_t i = 0; i < n; i++) {
    digitalWrite(PIN_BUZZER, HIGH); delay(ms); digitalWrite(PIN_BUZZER, LOW);
    if (i < n - 1) delay(80);
  }
}

void lampu(bool ok, bool err) {
  if (!PAKAI_LED) return;
  digitalWrite(PIN_LED_OK, ok);
  digitalWrite(PIN_LED_ERR, err);
}

void layarSiaga() {
  if (!adaLcd) return;
  String w = waktuSekarang();
  String b2 = w.substring(11, 16) + "  " + w.substring(8, 10) + "/" + w.substring(5, 7);
  if (jumlahAntrean > 0) b2 += "  ~" + String(jumlahAntrean);
  else if (WiFi.status() != WL_CONNECTED) b2 += "  ..";
  tampil("Tempel kartu", b2);
}

// ===================================================================
//  Waktu
// ===================================================================
String duaDigit(int n) { return n < 10 ? "0" + String(n) : String(n); }

String waktuSekarang() {
  time_t t = time(nullptr);
  if (t > 1700000000) {
    struct tm tmv; localtime_r(&t, &tmv);
    return String(tmv.tm_year + 1900) + "-" + duaDigit(tmv.tm_mon + 1) + "-" + duaDigit(tmv.tm_mday)
         + " " + duaDigit(tmv.tm_hour) + ":" + duaDigit(tmv.tm_min) + ":" + duaDigit(tmv.tm_sec);
  }
  if (adaRtc) {
    DateTime n = rtc.now();
    return String(n.year()) + "-" + duaDigit(n.month()) + "-" + duaDigit(n.day())
         + " " + duaDigit(n.hour()) + ":" + duaDigit(n.minute()) + ":" + duaDigit(n.second());
  }
  return "";
}

void setelWaktu(uint32_t epoch) {
  struct timeval tv = { (time_t) epoch, 0 };
  settimeofday(&tv, nullptr);
  if (adaRtc) rtc.adjust(DateTime(epoch));
}

// ===================================================================
//  Pembacaan RDM6300
// ===================================================================

/* Ubah dua karakter heks ASCII menjadi satu byte */
uint8_t heksKeByte(uint8_t hi, uint8_t lo) {
  auto nilai = [](uint8_t c) -> uint8_t {
    if (c >= '0' && c <= '9') return c - '0';
    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
    return 0;
  };
  return (nilai(hi) << 4) | nilai(lo);
}

/* Baca satu bingkai lengkap. Mengembalikan UID siap kirim, atau string kosong. */
String bacaKartu() {
  while (RdmSerial.available()) {
    uint8_t b = RdmSerial.read();

    if (b == 0x02) { isiBuf = 0; bufBingkai[isiBuf++] = b; continue; }
    if (isiBuf == 0) continue;                       // belum ada awalan, abaikan

    if (isiBuf < sizeof(bufBingkai)) bufBingkai[isiBuf++] = b;

    if (b != 0x03 || isiBuf < 14) continue;          // belum sampai akhir bingkai

    // Bingkai utuh: [STX][10 heks][2 heks checksum][ETX]
    uint8_t data[5], cek = 0;
    for (uint8_t i = 0; i < 5; i++) {
      data[i] = heksKeByte(bufBingkai[1 + i * 2], bufBingkai[2 + i * 2]);
      cek ^= data[i];
    }
    uint8_t cekKartu = heksKeByte(bufBingkai[11], bufBingkai[12]);
    isiBuf = 0;

    if (cek != cekKartu) {
      // Pembacaan rusak — biasanya kartu ditarik terlalu cepat
      Serial.println("Checksum tidak cocok, kartu dibaca ulang");
      continue;
    }

    // data[0] = versi, data[1..4] = nomor kartu 32 bit
    uint32_t nomor = ((uint32_t) data[1] << 24) | ((uint32_t) data[2] << 16)
                   | ((uint32_t) data[3] << 8)  |  (uint32_t) data[4];

    if (KIRIM_DESIMAL) {
      char buf[11];
      snprintf(buf, sizeof(buf), "%010lu", (unsigned long) nomor);
      return String(buf);
    }

    char buf[9];
    snprintf(buf, sizeof(buf), "%08lX", (unsigned long) nomor);
    return String(buf);
  }
  return "";
}

// ===================================================================
//  Antrean luring
// ===================================================================
void antreTap(const String& uid, const String& waktu) {
  if (jumlahAntrean >= MAKS_ANTREAN) {
    for (uint8_t i = 0; i < jumlahAntrean - 1; i++) {
      simpanan.putString(("t" + String(i)).c_str(),
                         simpanan.getString(("t" + String(i + 1)).c_str(), ""));
    }
    jumlahAntrean--;
  }
  simpanan.putString(("t" + String(jumlahAntrean)).c_str(), uid + "|" + waktu);
  jumlahAntrean++;
  simpanan.putUChar("n", jumlahAntrean);
}

void hapusAntreanDepan() {
  for (uint8_t i = 0; i < jumlahAntrean - 1; i++) {
    simpanan.putString(("t" + String(i)).c_str(),
                       simpanan.getString(("t" + String(i + 1)).c_str(), ""));
  }
  if (jumlahAntrean > 0) jumlahAntrean--;
  simpanan.putUChar("n", jumlahAntrean);
}

// ===================================================================
//  Komunikasi server
// ===================================================================
bool kirimTap(const String& uid, const String& waktu,
              String& namaOut, String& pesanOut, String& statusOut) {
  if (WiFi.status() != WL_CONNECTED) return false;

  HTTPClient http;
  http.setTimeout(TIMEOUT_HTTP_MS);
  http.begin(String(SERVER_URL) + "/rfid/tap");
  http.addHeader("Content-Type", "application/json");

  JsonDocument body;
  body["device"] = DEVICE_KODE;
  body["key"]    = DEVICE_KEY;
  body["uid"]    = uid;
  if (waktu.length()) body["waktu"] = waktu;

  String muatan; serializeJson(body, muatan);
  int kode = http.POST(muatan);

  if (kode != 200) {
    http.end();
    if (kode == 401) { pesanOut = "Perangkat ditolak"; statusOut = "AUTH"; return true; }
    return false;
  }

  String balasan = http.getString();
  http.end();

  JsonDocument d;
  if (deserializeJson(d, balasan)) return false;

  namaOut   = d["nama"]   | "";
  pesanOut  = d["pesan"]  | "";
  statusOut = d["status"] | "";
  return true;
}

void ping() {
  if (WiFi.status() != WL_CONNECTED) return;
  HTTPClient http;
  http.setTimeout(TIMEOUT_HTTP_MS);
  http.begin(String(SERVER_URL) + "/rfid/ping?device=" + DEVICE_KODE
             + "&key=" + DEVICE_KEY + "&fw=" + FIRMWARE_VER);
  if (http.GET() == 200) {
    JsonDocument d;
    if (!deserializeJson(d, http.getString())) {
      uint32_t epoch = d["epoch"] | 0;
      if (epoch > 1700000000) setelWaktu(epoch);
      const char* s = d["sekolah"] | "";
      if (strlen(s)) namaSekolah = String(s);
    }
  }
  http.end();
}

void prosesAntrean() {
  if (jumlahAntrean == 0 || WiFi.status() != WL_CONNECTED) return;
  String isi = simpanan.getString("t0", "");
  int pisah = isi.indexOf('|');
  if (pisah < 0) { hapusAntreanDepan(); return; }

  String nama, pesan, status;
  if (kirimTap(isi.substring(0, pisah), isi.substring(pisah + 1), nama, pesan, status)) {
    hapusAntreanDepan();
  }
}

// ===================================================================
//  Alur tap
// ===================================================================
void tanganiTap(const String& uid) {
  String waktu = waktuSekarang();
  String nama, pesan, status;

  tampil("Memproses...", uid);

  if (kirimTap(uid, waktu, nama, pesan, status)) {
    if (status == "AUTH") {
      lampu(false, true); bunyi(3);
      tampil("Mesin ditolak", "Cek API key");
      delay(2500); lampu(false, false); return;
    }

    bool berhasil = (status == "HADIR" || status == "TERLAMBAT" || status == "PULANG");
    if (berhasil) {
      lampu(true, false);
      bunyi(status == "TERLAMBAT" ? 2 : 1);
      tampil(nama.length() ? nama : "Tercatat", pesan);
    } else {
      lampu(false, true); bunyi(3);
      tampil("Gagal", pesan.length() ? pesan : "Kartu tak dikenal");
    }
    delay(2200);
  } else {
    antreTap(uid, waktu);
    lampu(true, true); bunyi(2, 60);
    tampil("Tersimpan", "Kirim nanti ~" + String(jumlahAntrean));
    delay(2000);
  }
  lampu(false, false);
}

// ===================================================================
//  Setup & loop
// ===================================================================
void sambungWifi(uint16_t batasMs = 12000) {
  if (WiFi.status() == WL_CONNECTED) return;
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  uint32_t mulai = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - mulai < batasMs) delay(300);
}

void setup() {
  Serial.begin(115200);
  RdmSerial.begin(9600, SERIAL_8N1, PIN_RDM_RX, PIN_RDM_TX);

  pinMode(PIN_BUZZER, OUTPUT);
  if (PAKAI_LED) {
    pinMode(PIN_LED_OK, OUTPUT);
    pinMode(PIN_LED_ERR, OUTPUT);
    lampu(false, false);
  }

  Wire.begin();
  lcd.init(); lcd.backlight();
  tampil("SIMAP Absensi", "RDM6300 125kHz");

  adaRtc = rtc.begin();
  simpanan.begin("simap", false);
  jumlahAntrean = simpanan.getUChar("n", 0);

  tampil("Menyambung", String(WIFI_SSID).substring(0, 16));
  sambungWifi();

  if (WiFi.status() == WL_CONNECTED) {
    ping();
    tampil(namaSekolah, "Siap digunakan");
    bunyi(1);
  } else {
    tampil("Mode luring", "Tap tetap dicatat");
    bunyi(2, 60);
  }
  delay(1500);
  layarSiaga();
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    static uint32_t coba = 0;
    if (millis() - coba > 20000) { coba = millis(); sambungWifi(6000); }
  }

  if (millis() - waktuPingTerakhir > JEDA_PING_MS) {
    waktuPingTerakhir = millis();
    ping();
    prosesAntrean();
    layarSiaga();
  }

  String uid = bacaKartu();
  if (uid.length()) {
    // RDM6300 mengirim ulang terus-menerus selama kartu menempel,
    // jadi tap yang sama dalam beberapa detik diabaikan.
    bool ulangCepat = (uid == uidTerakhir) && (millis() - waktuTapTerakhir < JEDA_TAP_SAMA_MS);
    if (!ulangCepat) {
      uidTerakhir = uid;
      waktuTapTerakhir = millis();
      tanganiTap(uid);
      layarSiaga();
    } else {
      waktuTapTerakhir = millis();   // perpanjang jeda selama kartu masih menempel
    }
  }

  delay(20);
}
