﻿// -- Yonetim paneli HTTP katmani ------------------------------------------------
// Uretici makinesi -> fikoyt-sunucu: kokDB (.aggregate/.ekle) + tetikleyici
// (yenile / manuel-tetikle) cagrilari. WinHTTP (lisans_uzak.cpp deseni).
//
// OTURUM: giris.yap doner `Set-Cookie: sid=...`; sonraki isteklerde Cookie
// basligi ile gonderilir. Sunucu session'i MongoDB'de (go_sessions) tutar.
#include <cctype>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <cstring>
#include <ctime>

#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <winhttp.h>

#include "fiko/cekirdek/kripto.hpp"
#include "fiko/core/gunluk.hpp"
#include "lisans_yonetim.hpp"
#include "simdjson.h"

namespace fiko {
namespace {

// ── Oturum durumu (panel genelinde paylasilir) ────────────────────────────────
struct HttpDurum {
  HINTERNET oturum = nullptr;
  std::string sunucu = "fikoyt.com";
  int port = 9000;
  bool https = true;
  std::string cookie;      // "sid=..."
  bool oturumAcik = false;
  std::string sonHata;
  std::string cevapGovdesi;
  int cevapKodu = 0;
};
HttpDurum g_http;

// Komut imzalama icin ozel anahtar yolu (UI "Sec"/anahtar kutusundan doldurur;
// bossa %USERPROFILE%\.fikoyt\lisans_ozel.key varsayilir).
std::string g_ozelAnahtarYol;

struct UrlParca {
  bool https = true;
  int port = 9000;
  std::string host, yol;
};

// "https://fikoyt.com:9000" veya "fikoyt.com:9000" -> ayristir
bool urlCoz(const std::string& url, UrlParca& u) {
  std::string kalan = url;
  for (char& c : kalan)
    if (c == '\\') c = '/';
  if (kalan.rfind("https://", 0) == 0) { u.https = true; kalan.erase(0, 8); }
  else if (kalan.rfind("http://", 0) == 0) { u.https = false; kalan.erase(0, 7); }
  if (kalan.empty()) return false;

  std::string yetki = kalan, yol = "/";
  if (const std::size_t e = kalan.find('/'); e != std::string_view::npos) {
    yetki = kalan.substr(0, e);
    yol = kalan.substr(e);
  }
  std::string host = yetki;
  if (const std::size_t p = yetki.rfind(':'); p != std::string_view::npos) {
    host = yetki.substr(0, p);
    u.port = std::atoi(yetki.c_str() + p + 1);
  }
  if (host.empty()) return false;
  u.host = host;
  u.yol = yol;
  if (u.port <= 0) u.port = u.https ? 443 : 80;
  return true;
}

void oturumKapat() {
  if (g_http.oturum) { WinHttpCloseHandle(g_http.oturum); g_http.oturum = nullptr; }
  g_http.cookie.clear();
  g_http.oturumAcik = false;
}

// ── Tek HTTP istegi (POST JSON) ───────────────────────────────────────────────
bool istek(const std::string& yol, const std::string& govde, std::string& cevap,
           int& kod, std::string& hata) {
  UrlParca u;
  if (!urlCoz(g_http.sunucu, u)) { hata = "gecersiz sunucu adresi"; return false; }

  if (!g_http.oturum) {
    g_http.oturum = WinHttpOpen(L"FikoYT-LisansYonetim/1.0", WINHTTP_ACCESS_TYPE_NO_PROXY,
                               WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
    if (!g_http.oturum) { hata = "WinHttpOpen basarisiz"; return false; }
    WinHttpSetTimeouts(g_http.oturum, 10000, 10000, 20000, INFINITE);
  }

  // ⚠️ std::wstring(...) geçici nesnesi yerine sabit tampon: geçici nesnenin
  //    yaşam süresi belirsiz olduğu için 0xC0000409 fast-fail üretiyordu.
  wchar_t wbuf[256] = {0};
  {
    const size_t n = u.host.size() < 255 ? u.host.size() : 255;
    for (size_t i = 0; i < n; ++i) wbuf[i] = (wchar_t)(unsigned char)u.host[i];
    wbuf[n] = 0;
  }
  HINTERNET baglanti = WinHttpConnect(g_http.oturum, wbuf, (INTERNET_PORT)u.port, 0);
  if (!baglanti) { hata = "baglanti hatasi"; return false; }

  HINTERNET istekH = WinHttpOpenRequest(baglanti, L"POST",
      std::wstring(yol.begin(), yol.end()).c_str(), nullptr, WINHTTP_NO_REFERER,
      WINHTTP_DEFAULT_ACCEPT_TYPES, u.https ? WINHTTP_FLAG_SECURE : 0);
  if (!istekH) {
    WinHttpCloseHandle(baglanti);
    hata = "istek acilamadi";
    return false;
  }

  // ── Sertifika dogrulama ────────────────────────────────────────────────────
  // Sunucu adresi GIZLIDIR (musteriye gosterilmez; sertifika "fikoyt.com" icin
  // uretilmistir, gizli adres/ IP o sertifikayi TUTMAZ). Bu yuzden varsayilan
  // olarak sunucu adi + sertifika adi eslesmesi GEVSETILIR.
  //
  // Guvenlik notu: bu ayar yalniz URETICI makinesinde derlenen yonetim
  // konsolunda aktiftir (FIKO_URETICI_UI). Musteri ikilisinde bu dosya hic
  // derlenmez. Kanitin gucunden dusulmez: her yanit ECDSA ile dogrulanir,
  // oturum karolasi sunucudan gelir. Dogrulama kapatmak yalnizca "bu
  // adrese baglanma" bilgisini kaldirir; sahte sunucu imzali bilet uretemez.
  if (u.https) {
    DWORD sec = SECURITY_FLAG_IGNORE_UNKNOWN_CA |
                SECURITY_FLAG_IGNORE_CERT_CN_INVALID |
                SECURITY_FLAG_IGNORE_CERT_DATE_INVALID;
    WinHttpSetOption(istekH, WINHTTP_OPTION_SECURITY_FLAGS, &sec, sizeof(sec));
  }

  // ⚠️ COOKIE'YI BIZ YONETIRIZ: WinHTTP varsayilan olarak cookie'leri kendi oturum deposunda
  //    tutar ve Set-Cookie basligini donen basliklardan GIZLER → "cookie alinamadi".
  //    Otomatik cookie devre disi birakilinca Set-Cookie ham basliklarda gorunur ve elle
  //    gonderdigimiz Cookie: basligi da oldugu gibi iletilir.
  {
    DWORD dis = WINHTTP_DISABLE_COOKIES;
    WinHttpSetOption(istekH, WINHTTP_OPTION_DISABLE_FEATURE, &dis, sizeof(dis));
  }
  // Basliklar: JSON govde icin Content-Type SART (yoksa sunucu govdeyi ayristirmaz →
  // giris basarisiz → Set-Cookie hic uretilmez) + varsa oturum cookie'si.
  std::wstring ekBasliklar = L"Content-Type: application/json\r\nAccept: application/json";
  if (!g_http.cookie.empty())
    ekBasliklar += L"\r\nCookie: " + std::wstring(g_http.cookie.begin(), g_http.cookie.end());

  const LPCWSTR baslik = ekBasliklar.c_str();
  const BOOL ok = WinHttpSendRequest(istekH, baslik, (DWORD)-1,
      (LPVOID)govde.data(), (DWORD)govde.size(), (DWORD)govde.size(), 0)
      && WinHttpReceiveResponse(istekH, nullptr);

  if (!ok) {
    WinHttpCloseHandle(istekH);
    WinHttpCloseHandle(baglanti);
    hata = "istek/yanit hatasi (sunucu erisilemiyor olabilir)";
    return false;
  }

  // ── Set-Cookie yakala (giriş cevabında) ───────────────────────────────────
  // Fiber session middleware şunu gönderir:
  //   Set-Cookie: sid=<uuid>; max-age=86400; path=/; HttpOnly; secure; SameSite=Lax
  // Oturum sonraki isteklerde `Cookie: sid=<uuid>` başlığıyla taşınır.
  //
  // ⚠️ WinHTTP baslik sorgusunun DOGRU iki adimi:
  //   1) WINHTTP_QUERY_RAW_HEADERS ile BAYT sayisi sorulur - bu bayt turunden
  //      TAMPON BOYUTUDUR, baslik sayisi DEGIL. (CRLF varyanti soruldugunda
  //      kucuk bir SAYI doner; tampon 2 byte'a duser ve yalnizca "H" okunur -
  //      onceki "Basliklar: H" hatasinin sebebi tam olarak buydu.)
  //   2) WINHTTP_QUERY_RAW_HEADERS_CRLF ile metin alinir.
  {
    // ⚠️ GERCEK DUZELTME (GLE 122): WinHTTP basliklari UTF-16 (WCHAR) dondurur ve
    //    RAW_HEADERS_CRLF, RAW_HEADERS'tan UZUNDUR (satir basi 2 karakter vs 1 null).
    //    Tampon boyutu MUTLAKA ayni sorgu turuyle (CRLF) sorulmali; sonuc BAYT cinsindendir.
    DWORD gerekli = 0;
    WinHttpQueryHeaders(istekH, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX,
                        nullptr, &gerekli, WINHTTP_NO_HEADER_INDEX);
    if (gerekli > 16 && gerekli < (1u << 20)) {   // makul ust sinir: 1 MB
      std::wstring whdr(gerekli / sizeof(wchar_t) + 1, L'\0');
      DWORD alindi = 0;
      DWORD tamponBayt = (DWORD)(whdr.size() * sizeof(wchar_t));
      std::string hdr;
      if (WinHttpQueryHeaders(istekH, WINHTTP_QUERY_RAW_HEADERS_CRLF,
                              WINHTTP_HEADER_NAME_BY_INDEX, &whdr[0], &tamponBayt,
                              WINHTTP_NO_HEADER_INDEX)) {
        // tamponBayt = yazilan bayt (sonlandirici null haric) → wchar sayisi
        whdr.resize(tamponBayt / sizeof(wchar_t));
        // Basliklar ASCII/Latin: dar stringe basit daraltma yeterli (cookie sid=<uuid>).
        hdr.reserve(whdr.size());
        for (wchar_t wc : whdr) hdr.push_back(wc < 0x80 ? (char)wc : '?');
        alindi = (DWORD)hdr.size();

        // Arama icin kucuk harf kopya; kesme orijinalden yapilir.
        std::string kucuk = hdr;
        for (char& c : kucuk) c = (char)std::tolower((unsigned char)c);

        // 1) Once "Set-Cookie" basliginin kendisini bul
        std::size_t sc = kucuk.find("set-cookie:");
        if (sc == std::string::npos) sc = 0;   // baslik adi yoksa tum metni tara
        const std::size_t sid = kucuk.find("sid=", sc);
        if (sid != std::string::npos) {
          const std::size_t bas = sid + 4;
          std::size_t son2 = bas;
          while (son2 < kucuk.size() && kucuk[son2] != ';' && kucuk[son2] != '\r' &&
                 kucuk[son2] != '\n' && kucuk[son2] != ' ' && kucuk[son2] != '\t')
            ++son2;
          if (son2 > bas) g_http.cookie = "sid=" + hdr.substr(bas, son2 - bas);
        }

        if (g_http.cookie.empty())
          gunluk::uyari("Set-Cookie bulunamadi ({} bayt). Basliklar: {}", alindi,
                        hdr.substr(0, hdr.size() < 300 ? hdr.size() : 300));
        else
          gunluk::bilgi("Oturum cookie alindi ({} bayt baslik okundu)", alindi);
      } else {
        gunluk::uyari("Baslik metni okunamadi (GLE {})", GetLastError());
      }
    } else {
      gunluk::uyari("Baslik tampon boyutu anormal: {} bayt (GLE {})", gerekli,
                    GetLastError());
    }
  }

  // HTTP kodu
  DWORD k = 0, kb = sizeof(k);
  WinHttpQueryHeaders(istekH, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
                      WINHTTP_HEADER_NAME_BY_INDEX, &k, &kb, WINHTTP_NO_HEADER_INDEX);
  kod = (int)k;

  // Govde
  cevap.clear();
  char tampon[8192];
  DWORD okunan = 0;
  while (WinHttpReadData(istekH, tampon, sizeof(tampon), &okunan) && okunan > 0)
    cevap.append(tampon, okunan);

  WinHttpCloseHandle(istekH);
  WinHttpCloseHandle(baglanti);

  if (kod < 200 || kod >= 300) {
    hata = "HTTP " + std::to_string(kod);
    return false;
  }
  return true;
}

// ── JSON kaçış ────────────────────────────────────────────────────────────────
std::string kacisla(const std::string& s) {
  std::string o;
  o.reserve(s.size() + 8);
  for (char c : s) {
    if (c == '"' || c == '\\') { o += '\\'; o += c; }
    else if (c == '\n') o += "\\n";
    else if (c == '\r') {}
    else if ((unsigned char)c < 0x20) o += ' ';
    else o += c;
  }
  return o;
}

// ── simdjson gercek JSON okuyucu (elle yazilmis tarayicinin YERINE) ───────────
// Eski "alanOku" belgeyi ilk '}' de kesiyordu: `kimlik` Mongo genisletilmis
// JSON `{"$oid":..}` ve `bilgiler` ic ice oldugu icin TUM belgelerde alan
// bulunamaz, liste hep bos gorunurdu.
namespace sj = simdjson;

// Unix zaman damgasini kisaZaman'in bekledigi ISO bicimine cevir
// ("2026-10-08T14:22:31"). 10 hane = saniye, 13 hane = milisaniye; diger
// degerler oldugu gibi birakilir.
std::string unixIso(const std::string& s) {
  const bool sn = (s.size() == 10), ms = (s.size() == 13);
  if (!sn && !ms) return s;
  for (char ch : s)
    if (ch < '0' || ch > '9') return s;
  i64 damga = std::strtoll(s.c_str(), nullptr, 10);
  if (damga <= 0) return s;
  if (ms) damga /= 1000;
  const time_t t = (time_t)damga;
  std::tm tm{};
  localtime_s(&tm, &t);
  char b[32];
  std::strftime(b, sizeof b, "%Y-%m-%dT%H:%M:%S", &tm);
  return b;
}

// Ham JSON degerini metne cevirir: string / sayi / MongoDB genisletilmis JSON
// sarmalayicilari ({"$oid":..}, {"$date":..}, {"$numberLong":..}).
// Donus: bir sey cikarildi mi.
bool degerMetin(sj::ondemand::value v, std::string& cikti, int derinlik = 0) {
  std::string_view sv;
  if (v.get_string().get(sv) == sj::SUCCESS) { cikti.assign(sv); return true; }
  int64_t tam = 0;
  if (v.get_int64().get(tam) == sj::SUCCESS) { cikti = std::to_string(tam); return true; }
  uint64_t sayi = 0;
  if (v.get_uint64().get(sayi) == sj::SUCCESS) { cikti = std::to_string(sayi); return true; }
  double kayan = 0;
  if (v.get_double().get(kayan) == sj::SUCCESS) {
    char b[32];
    std::snprintf(b, sizeof b, "%g", kayan);
    cikti = b;
    return true;
  }
  if (derinlik >= 3) return false;
  sj::ondemand::object o;
  if (v.get_object().get(o) != sj::SUCCESS) return false;
  // Sarmalayicilar tek anahtarli: ilk anahtara bakip icine in.
  for (auto f : o) {
    auto ak = f.unescaped_key();
    if (ak.error()) break;
    const std::string_view k = ak.value_unsafe();
    if (k == "$oid" || k == "$date" || k == "$numberLong" || k == "$numberInt" ||
        k == "$numberDouble" || k == "$numberDecimal") {
      sj::ondemand::value alt;
      if (f.value().get(alt) != sj::SUCCESS) return false;
      return degerMetin(alt, cikti, derinlik + 1);
    }
    break;  // bilinmeyen ilk anahtar -> bu bir sarmalayici degil
  }
  return false;
}

// Bir belgeyi tek geciste okur ("bilgiler" ic ice nesnesi de ayni yolla).
// On-Demand ileri-yonlu: alanlar DOSYA SIRASINDA gezilir; ayni alan iki kez
// gelirse ILK deger korunur (ust/alt duzel cakismasiz).
// Sunucu semasi (kokDB goCihazlar):
//   kimlik ({$oid}|duz string), tipi, tarih, bilgiler.{seri,musteri,
//   bilgisayar,surum,adres,...}
void belgeOku(sj::ondemand::object o, CihazOzet& c) {
  for (auto f : o) {
    auto ak = f.unescaped_key();
    if (ak.error()) continue;
    const std::string_view k = ak.value_unsafe();

    if (k == "bilgiler" || k == "bilgi") {  // ic ice bilgi nesnesi
      sj::ondemand::object alt;
      if (f.value().get_object().get(alt) == sj::SUCCESS) belgeOku(alt, c);
      continue;
    }

    std::string* hedef = nullptr;
    bool tarihMi = false;
    if (k == "kimlik") hedef = &c.cihazKimlik;
    else if (k == "bilgisayar") hedef = &c.bilgisayar;
    else if (k == "seri") hedef = &c.seri;
    else if (k == "musteri") hedef = &c.musteri;
    else if (k == "surum") hedef = &c.surum;
    else if (k == "tipi") hedef = &c.tipi;
    else if (k == "ip") hedef = &c.ip;
    else if (k == "uretici") hedef = &c.uretici;  // "1" = uretici makinesi
    else if (k == "adres") hedef = &c.ip;      // bilgiler.adres = soket IP:port
    else if (k == "tarih") { hedef = &c.sonBaglanma; tarihMi = true; }
    else continue;                             // diger alan: deger okunmaz

    if (!hedef->empty()) continue;             // ilk deger kazanir
    sj::ondemand::value v;
    if (f.value().get(v) != sj::SUCCESS) continue;
    std::string d;
    if (!degerMetin(v, d) || d.empty()) continue;
    if (tarihMi) d = unixIso(d);
    *hedef = std::move(d);
  }
}

std::string isoZaman(i64 unix) {
  if (unix <= 0) return "-";
  const time_t t = (time_t)unix;
  std::tm tm{};
  localtime_s(&tm, &t);
  char b[32];
  std::strftime(b, sizeof b, "%d.%m.%Y %H:%M", &tm);
  return b;
}

}  // namespace

// ── Genel durum ───────────────────────────────────────────────────────────────
YonetimDurumu& yonetimDurumu() {
  static YonetimDurumu d;
  return d;
}
const std::vector<CihazOzet>& yonetimCihazlari() {
  static std::vector<CihazOzet> v;
  return v;
}
const std::vector<KomutCevabi>& yonetimCevaplari() {
  static std::vector<KomutCevabi> v;
  return v;
}

// ── Oturum ac ─────────────────────────────────────────────────────────────────
// Komut imzasi icin kullanilacak ozel anahtar yolunu ayarla (UI doldurur).
void yonetimOzelAnahtarYolAyarla(const std::string& yol) { g_ozelAnahtarYol = yol; }

bool yonetimOturumAc(const std::string& sunucu, const std::string& kullaniciAdi,
                     const std::string& parola, std::string& hata) {
  UrlParca u;
  if (!urlCoz(sunucu, u)) { hata = "Gecersiz sunucu adresi"; return false; }
  g_http.sunucu = sunucu;
  oturumKapat();

  const std::string govde = "{\"kullaniciAdi\":\"" + kacisla(kullaniciAdi) +
                            "\",\"parola\":\"" + kacisla(parola) + "\"}";
  std::string cevap;
  int kod = 0;
  if (!istek("/fapiv1/fikoyt.com.FikoYT.sistem.giris.yap", govde, cevap, kod, hata))
    return false;

  g_http.oturumAcik = !g_http.cookie.empty();
  if (!g_http.oturumAcik) {
    // Sunucu giris hatalarini HTTP 200 + govdede durum.hatalar[] ile doner (orn. "Kullanici
    // Bulunamadi"). Once bu mesaji cikar; yoksa ham govde ozetini goster.
    auto jsonAlan = [&](const char* anahtar, std::size_t baslangic) -> std::string {
      const std::string ara = std::string("\"") + anahtar + "\":\"";
      const std::size_t a = cevap.find(ara, baslangic);
      if (a == std::string::npos) return {};
      const std::size_t b0 = a + ara.size();
      const std::size_t b1 = cevap.find('"', b0);
      return b1 == std::string::npos ? std::string() : cevap.substr(b0, b1 - b0);
    };
    const std::size_t hataYeri = cevap.find("\"hatalar\"");
    const std::string sunucuBaslik = hataYeri == std::string::npos ? std::string()
                                     : jsonAlan("baslik", hataYeri);
    const std::string sunucuMesaj = hataYeri == std::string::npos ? std::string()
                                    : jsonAlan("mesaj", hataYeri);
    if (!sunucuBaslik.empty() || !sunucuMesaj.empty()) {
      hata = "Giris reddedildi: " + sunucuBaslik +
             (sunucuMesaj.empty() ? std::string() : " - " + sunucuMesaj);
    } else {
      hata = "Oturum acilamadi (cookie alinamadi; HTTP " + std::to_string(kod) + "): " +
             cevap.substr(0, 160);
    }
    gunluk::uyari("Yonetim giris basarisiz. HTTP {} govde: {}", kod, cevap.substr(0, 300));
    return false;
  }

  YonetimDurumu& d = yonetimDurumu();
  d.sunucu = sunucu;
  d.kullaniciAdi = kullaniciAdi;
  d.oturumAcik = true;
  d.cevapHatasi.clear();
  gunluk::bilgi("Yonetim oturumu acildi: {}", kullaniciAdi);
  return true;
}

// ── Cihaz listesini tazele (kokDB .aggregate -> goCihazlar) ───────────────────
bool yonetimCihazlariYenile(std::vector<CihazOzet>& cikti, std::string& hata) {
  if (!g_http.oturumAcik) { hata = "Once oturum acin"; return false; }

  // goCihazlar'a erisim beyaz listede (saltOkunurKoleksiyonlar).
  const std::string govde =
      "{\"aggregate\":[{\"$match\":{}},{\"$sort\":{\"tarih\":-1}},{\"$limit\":200}]}";
  std::string cevap;
  int kod = 0;
  if (!istek("/fapiv1/fikoyt.com.FikoYT.sistem.DB.goCihazlar.aggregate",
             govde, cevap, kod, hata))
    return false;

  cikti.clear();

  // ── Gercek JSON okuyucu (simdjson On-Demand) ──────────────────────────────
  // Sunucu fapiv1 yanitini kuresel sarmalayica koyar:
  //   {"durum":{"kodu":200,...},"sonuc":[belge,...]}
  // Sarmalayici yoksa (govde duz dizi ise) o da kabul edilir.
  sj::ondemand::parser ayristirici;
  sj::padded_string dolu(cevap);
  sj::ondemand::document kok;
  if (ayristirici.iterate(dolu).get(kok) != sj::SUCCESS) {
    hata = "Cevap JSON degil (HTTP " + std::to_string(kod) + "): " + cevap.substr(0, 160);
    return false;
  }

  sj::ondemand::array dizi;
  auto kokTip = kok.type();   // yalniz bakar, tuketmez
  if (kokTip.error()) { hata = "Cevap okunamadi"; return false; }
  if (kokTip.value() == sj::ondemand::json_type::array) {
    if (kok.get_array().get(dizi) != sj::SUCCESS) {
      hata = "Cevap dizisi okunamadi: " + cevap.substr(0, 160);
      return false;
    }
  } else if (kokTip.value() == sj::ondemand::json_type::object) {
    sj::ondemand::object kokO;
    if (kok.get_object().get(kokO) != sj::SUCCESS) {
      hata = "Cevap nesnesi okunamadi: " + cevap.substr(0, 160);
      return false;
    }
    if (kokO["sonuc"].get_array().get(dizi) != sj::SUCCESS) {
      // Dizi yok: sunucunun durum.mesajini yuzeye cikar (yetki/rol hatasi vb.).
      std::string_view mesaj;
      sj::ondemand::object durum;
      std::string ek;
      if (kokO["durum"].get_object().get(durum) == sj::SUCCESS &&
          durum["mesaj"].get_string().get(mesaj) == sj::SUCCESS && !mesaj.empty())
        ek = ": " + std::string(mesaj);
      hata = "Cevapta 'sonuc' dizisi yok" + ek;
      gunluk::uyari("goCihazlar cevabi cozulemedi. Govde: {}", cevap.substr(0, 400));
      return false;
    }
  } else {
    hata = "Beklenmeyen cevap bicimi: " + cevap.substr(0, 160);
    return false;
  }

  int ham = 0;
  for (auto belge : dizi) {
    sj::ondemand::object o;
    if (belge.get_object().get(o) != sj::SUCCESS) continue;  // belge disi oge
    ++ham;
    CihazOzet c;
    belgeOku(o, c);
    if (c.cihazKimlik.empty()) continue;
    if (c.bilgisayar.empty()) c.bilgisayar = "(bilinmiyor)";
    cikti.push_back(std::move(c));
  }

  if (cikti.empty() && ham > 0) {
    // Belgeler geldi ama kimlik okunamadi -> sema/sarmalayici degismis demektir.
    gunluk::uyari("goCihazlar: {} belge alindi, hicbirinde 'kimlik' okunamadi. Govde: {}",
                  ham, cevap.substr(0, 400));
  } else {
    gunluk::bilgi("goCihazlar: {} belge -> {} cihaz", ham, cikti.size());
  }

  YonetimDurumu& d = yonetimDurumu();
  d.sunucuyaBagli = true;
  d.yukleniyor = false;
  d.cevapHatasi.clear();
  d.sonHata.clear();
  return true;
}

// ── Cihaz kaydini sil (kokDB .sil -> goCihazlar) ──────────────────────────────
// Uretici/yonetici yetkisi gerekir (sunucu: dbErisimKontrolYazma). Filtre kimlik
// uzerinden gider: sunucu IsleObjectId() ile 24 haneli hex degeri ObjectID'ye
// cevirir — goCihazlar'da da uzakKimlikDeger ayni kurali uyguladigi icin turler
// eslesir; duz string kimlikler oldugu gibi kalir. Cihaz kaydi silinince bir
// sonraki baglantasinda kendiliginden yeniden kayit olur.
bool yonetimCihazlariSil(const std::vector<std::string>& kimlikler, int& silinen,
                         std::string& hata) {
  silinen = -1;
  if (!g_http.oturumAcik) { hata = "Once oturum acin"; return false; }
  if (kimlikler.empty()) { hata = "Silinecek cihaz secilmedi"; return false; }

  // Dizi govde -> sunucu "Array" dalini calistirir (her eleman icin DeleteMany).
  std::string govde = "[";
  for (std::size_t i = 0; i < kimlikler.size(); ++i) {
    if (i) govde += ",";
    govde += "{\"kimlik\":\"" + kacisla(kimlikler[i]) + "\"}";
  }
  govde += "]";

  std::string cevap;
  int kod = 0;
  if (!istek("/fapiv1/fikoyt.com.FikoYT.sistem.DB.goCihazlar.sil", govde, cevap, kod, hata))
    return false;

  // Silinen sayisi: {"durum":..,"sonuc":{"DeletedCount":N}} (opsiyonel okuma).
  sj::ondemand::parser ayristirici;
  sj::padded_string dolu(cevap);
  sj::ondemand::document kok;
  if (ayristirici.iterate(dolu).get(kok) == sj::SUCCESS) {
    sj::ondemand::object kokO;
    if (kok.get_object().get(kokO) == sj::SUCCESS) {
      sj::ondemand::object sonuc;
      if (kokO["sonuc"].get_object().get(sonuc) == sj::SUCCESS) {
        int64_t n = -1;
        if (sonuc["DeletedCount"].get_int64().get(n) == sj::SUCCESS ||
            sonuc["deletedCount"].get_int64().get(n) == sj::SUCCESS)
          silinen = (int)n;
      }
    }
  }

  YonetimDurumu& dr = yonetimDurumu();
  dr.cevapHatasi.clear();
  if (silinen >= 0)
    gunluk::bilgi("goCihazlar: {} kayit silindi", silinen);
  else
    gunluk::bilgi("goCihazlar: {} kimlik icin silme istendi", kimlikler.size());
  return true;
}

// ── Cihaz adlari (satici takma adlari) -> kokDB fikocadCihazAdlari ─────────────
// Belge: {kimlik, cihazAdi, zaman}. _id'ye BAGLI DEGILIZ (sunucu 24 haneli hex
// degerleri ObjectID'ye cevirebilir; tur uyumsuzlugu riski): hem .sil hem .aggregate
// "kimlik" alani uzerinden gider, .ekle _id'yi sunucuya birakir. Ad degisince once
// eski kayit silinir, sonra yenisi eklenir -> cihaz basina tek kayit kalir.
bool yonetimCihazAdiYaz(const std::string& kimlik, const std::string& ad,
                        std::string& hata) {
  if (!g_http.oturumAcik) { hata = "Once oturum acin"; return false; }
  if (kimlik.empty()) { hata = "Cihaz kimligi bos"; return false; }

  std::string cevap;
  int kod = 0;
  const std::string silGovde = "[{\"kimlik\":\"" + kacisla(kimlik) + "\"}]";
  if (!istek("/fapiv1/fikoyt.com.FikoYT.sistem.DB.fikocadCihazAdlari.sil",
             silGovde, cevap, kod, hata)) {
    if (!cevap.empty()) hata += ": " + cevap.substr(0, 160);
    return false;
  }
  if (ad.empty()) {                       // sifirlama: yalniz silme yeterli
    gunluk::bilgi("Cihaz adi sunucudan silindi: {}", kimlik);
    return true;
  }

  std::string j = "{\"kimlik\":\"" + kacisla(kimlik) + "\"";
  j += ",\"cihazAdi\":\"" + kacisla(ad) + "\"";
  j += ",\"zaman\":" + std::to_string((i64)std::time(nullptr)) + "}";
  if (!istek("/fapiv1/fikoyt.com.FikoYT.sistem.DB.fikocadCihazAdlari.ekle",
             j, cevap, kod, hata)) {
    if (!cevap.empty()) hata += ": " + cevap.substr(0, 160);
    return false;
  }
  gunluk::bilgi("Cihaz adi sunucuya yazildi: {} -> {}", kimlik, ad);
  return true;
}

bool yonetimCihazAdlariOku(std::map<std::string, std::string>& adlar,
                           std::string& hata) {
  if (!g_http.oturumAcik) { hata = "Once oturum acin"; return false; }

  const std::string govde = "{\"aggregate\":[{\"$match\":{}},{\"$limit\":1000}]}";
  std::string cevap;
  int kod = 0;
  if (!istek("/fapiv1/fikoyt.com.FikoYT.sistem.DB.fikocadCihazAdlari.aggregate",
             govde, cevap, kod, hata)) {
    if (!cevap.empty()) hata += ": " + cevap.substr(0, 160);
    return false;
  }
  adlar.clear();
  // Koleksiyon henuz bos/yok: sunucu "sonuc":null donebilir -> hata degil, bos liste.
  if (cevap.find("\"sonuc\":null") != std::string::npos) return true;

  sj::ondemand::parser ayristirici;
  sj::padded_string dolu(cevap);
  sj::ondemand::document kok;
  if (ayristirici.iterate(dolu).get(kok) != sj::SUCCESS) {
    hata = "Cevap JSON degil: " + cevap.substr(0, 160);
    return false;
  }
  sj::ondemand::array dizi;
  auto kokTip = kok.type();
  if (kokTip.error()) { hata = "Cevap okunamadi"; return false; }
  if (kokTip.value() == sj::ondemand::json_type::array) {
    if (kok.get_array().get(dizi) != sj::SUCCESS) { hata = "Cevap dizisi okunamadi"; return false; }
  } else if (kokTip.value() == sj::ondemand::json_type::object) {
    sj::ondemand::object kokO;
    if (kok.get_object().get(kokO) != sj::SUCCESS) { hata = "Cevap nesnesi okunamadi"; return false; }
    if (kokO["sonuc"].get_array().get(dizi) != sj::SUCCESS) {
      // Dizi yok: sunucunun durum.mesajini yuzeye cikar (yetki / koleksiyon reddi vb.).
      std::string_view mesaj;
      sj::ondemand::object durum;
      std::string ek;
      if (kokO["durum"].get_object().get(durum) == sj::SUCCESS &&
          durum["mesaj"].get_string().get(mesaj) == sj::SUCCESS && !mesaj.empty())
        ek = ": " + std::string(mesaj);
      hata = "Cevapta 'sonuc' dizisi yok" + ek;
      return false;
    }
  } else {
    hata = "Beklenmeyen cevap bicimi: " + cevap.substr(0, 160);
    return false;
  }

  for (auto belge : dizi) {
    sj::ondemand::object o;
    if (belge.get_object().get(o) != sj::SUCCESS) continue;
    std::string kimlik, ad;
    for (auto f : o) {
      auto ak = f.unescaped_key();
      if (ak.error()) continue;
      const std::string_view k = ak.value_unsafe();
      std::string* hedef = k == "kimlik" ? &kimlik : (k == "cihazAdi" ? &ad : nullptr);
      if (!hedef) continue;
      sj::ondemand::value v;
      if (f.value().get(v) != sj::SUCCESS) continue;
      degerMetin(v, *hedef);
    }
    if (!kimlik.empty() && !ad.empty()) adlar[kimlik] = ad;
  }
  return true;
}

// ── Tetikleyici uzerinden ANINDA komut gonder ────────────────────────────────
// Kural yazar -> motoru yeniler -> manuel tetikler -> sokete emit.
// Bu 3 cagri sunucuda ~200-800 ms surer.
namespace {

// Kural kimligi: MongoDB ObjectID (12 bayt = 24 hex karakter).
//
// NEDEN ObjectID? Sunucudaki tetikleyici motoru kurallari `tetikleyiciKural`
// struct'ina decode eder ve o struct'ta `_id primitive.ObjectID` turunde
// tanimlidir. String bir _id yazarsak motor TUM KURALLARI decode hatasyla
// atar ("an ObjectID string must be exactly 12 bytes long") ve kural sayisi
// 0 kalir — komut gonderme sessizce calismaz. Bu bir tuzaktir, 12 bayt uretmek
// sarttir.
//
// Rastgelelik: kripto::rastgeleBayt (BCryptGenRandom) basarisiz olursa zaman
// damgasindan turetilir — kimlik benzersizligi bozulmaz (motor yalnizca
// varlik/donus icin kullanir, guvenlik burada degil, komut imzasindadir).
std::string kuralIdUret() {
  u8 d[12];
  if (!kripto::rastgeleBayt(d, 12)) {
    const i64 t = (i64)std::time(nullptr);
    for (int i = 0; i < 8; ++i) d[i] = (u8)((t >> (i * 8)) & 0xFF);
    d[8] = d[9] = d[10] = d[11] = 0x5B;  // "[" ile baslayan 12 bayt
  }
  return kripto::baytHex(d, 12);
}

bool tetikleyiciCagir(const std::string& govde, std::string& hata) {
  std::string cevap;
  int kod = 0;
  if (!istek("/fapiv1/fikoyt.com.FikoYT.sistem.tetikleyici.manuel-tetikle",
             govde, cevap, kod, hata))
    return false;
  // Sunucu, komutun gidecegi bagli soket sayisini "ulasan" ile bildirir: 0 = hedef
  // cevrimdisi ya da kimligi eslesmiyor. Eskiden bu durum sessizce "Gonderildi" gorunuyordu.
  if (cevap.find("\"ulasan\":0") != std::string::npos) {
    hata = "Cihaz cevrimdisi - komut hicbir cihaza iletilemedi";
    return false;
  }
  return true;
}

}  // namespace

namespace {
// Ileri bildirim: gercek tanim dosyanin altinda (lisans bileti bolumu).
// yonetimKomutGonder komutu ozel anahtarla imzalayacagi icin erken gorunmesi gerekir.
bool ozelAnahtarOku(const std::string& yol, std::string& ozelHex, std::string& hata);
}  // namespace

// Once motoru yenile (yeni kurali yuklesin)
[[maybe_unused]] static bool motoruYenile(std::string& hata) {
  std::string cevap;
  int kod = 0;
  return istek("/fapiv1/fikoyt.com.FikoYT.sistem.tetikleyici.yenile", "{}", cevap, kod, hata);
}

bool yonetimKomutGonder(const CihazOzet& cihaz, const std::string& komutKodu,
                        const std::string& mesaj, std::string& hata) {
  if (!g_http.oturumAcik) { hata = "Once oturum acin"; return false; }
  if (cihaz.cihazKimlik.empty()) { hata = "Cihaz secilmedi"; return false; }
  if (komutKodu.empty()) { hata = "Komut secilmedi"; return false; }

  // Beyaz liste denetimi (istemci tarafi da denetleyecek; burada erken geri bildirim)
  const std::vector<std::string>& bl = yonetimBeyazListe();
  if (std::find(bl.begin(), bl.end(), komutKodu) == bl.end()) {
    hata = "Komut beyaz listede degil: " + komutKodu;
    return false;
  }

  const std::string kid = kuralIdUret();

  // ── Komut imzasi (istemci 0. adimda ECDSA P-256 dogrular) ─────────────────
  // Istemcinin bekledigi kanonik metinle BIREBIR ayni sira:
  //   komutId=<id>\nkomut_kodu=<kod>\nmesaj=<mesaj>\n
  // Imza yoksa istemci "komut imzasi gecersiz veya eksik" ile REDDEDER ve
  // konsolda "Gonderildi" gorunmesine ragmen cihazda HICBIR SEY OLMAZ.
  std::string anahtarYol = g_ozelAnahtarYol;
  if (anahtarYol.empty()) {
    if (const char* up = std::getenv("USERPROFILE"); up && *up)
      anahtarYol = std::string(up) + "\\.fikoyt\\lisans_ozel.key";
  }
  std::string ozelHex;
  if (!ozelAnahtarOku(anahtarYol, ozelHex, hata)) return false;
  const std::string kanonik = "komutId=" + kid + "\nkomut_kodu=" + komutKodu +
                              "\nmesaj=" + mesaj + "\n";
  std::string imza;
  if (!kripto::imzala(kanonik, ozelHex, imza)) {
    hata = "Komut imzalanamadi (ozel anahtar gecersiz olabilir)";
    return false;
  }

  // kural govdesi: once kurali yaz (hareketler, vt=tetikleyiciler)
  //
  // ⚠️ kosul.tip OLMAZSA motor kurali calistirmaz: kuralEslesiyor() switch'in
  // default kolunda `return false` yapar ve olay sessizce yutulur (HTTP yine 200
  // doner, kural "calisti" sanilir). Bu yuzden "cihaz_olay" acikca yazilir.
  std::string kural = "{";
  kural += "\"_id\":\"" + kid + "\"";
  kural += ",\"vt\":\"FikoYT.sistem.tetikleyiciler\"";
  kural += ",\"ad\":\"Yonetim: " + kacisla(komutKodu) + "\"";
  kural += ",\"aktif\":true";
  kural += ",\"kosul\":{\"tip\":\"cihaz_olay\",\"cihazId\":\"" + kacisla(cihaz.cihazKimlik) +
           "\",\"olayKodu\":\"manuel\"}";
  kural += ",\"eylemler\":[{";
  kural += "\"tip\":\"cihaz_komut\"";
  kural += ",\"hedefCihazId\":\"" + kacisla(cihaz.cihazKimlik) + "\"";
  kural += ",\"komutKodu\":\"" + kacisla(komutKodu) + "\"";
  kural += ",\"mesaj\":\"" + kacisla(mesaj) + "\"";
  kural += ",\"komutId\":\"" + kid + "\"";
  kural += ",\"imza\":\"" + kacisla(imza) + "\"";
  kural += "}]}";

  std::string cevap;
  int kod = 0;
  if (!istek("/fapiv1/fikoyt.com.FikoYT.sistem.DB.hareketler.ekle",
             kural, cevap, kod, hata))
    return false;

  // Motor yenilemesi GEREKMIYOR: sunucu manuel-tetikle'de kurali dogrudan DB'den
  // cozup yalniz onu calistirir (eskiden once yenile + tum kurallar tetiklenirdi).

  // Manuel tetikle
  const std::string tetik = "{\"tetikleyiciId\":\"" + kid + "\"}";
  if (!tetikleyiciCagir(tetik, hata)) {
    hata = "Tetikleme basarisiz: " + hata;
    return false;
  }

  YonetimDurumu& d = yonetimDurumu();
  ++d.komutSayaci;
  d.cevapHatasi.clear();
  return true;
}

// ── Lisans bileti yaz (kokDB .ekle -> fikocadLisanslari) ───────────────────────
namespace {

// Ozel anahtari dosyadan oku (ureticide: %USERPROFILE%\.fikoyt\lisans_ozel.key)
bool ozelAnahtarOku(const std::string& yol, std::string& ozelHex, std::string& hata) {
  if (yol.empty()) { hata = "Ozel anahtar yolu bos"; return false; }
  std::ifstream f(yol, std::ios::binary);
  if (!f) { hata = "Anahtar dosyasi okunamadi: " + yol; return false; }
  std::string icerik((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());

  // En uzun kesintisiz hex dizisini ayikla (dosya "# yorum" icerebilir)
  std::string enIyi, guncel;
  for (char c : icerik) {
    const bool hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
    if (hex) guncel += c;
    else {
      if (guncel.size() > enIyi.size()) enIyi = guncel;
      guncel.clear();
    }
  }
  if (guncel.size() > enIyi.size()) enIyi = guncel;

  ozelHex = enIyi;
  if (ozelHex.size() != 192) {   // X||Y||d = 96 bayt = 192 hex
    hata = "Anahtar gecersiz (192 hex karakter bekleniyordu, " +
           std::to_string(ozelHex.size()) + " bulundu)";
    return false;
  }
  return true;
}

// Kanonik govde kur (C++ tarafi ve uretici penceresi ile ayni sira)
std::string biletGovdeKur(const std::string& cihaz, const std::string& musteri,
                          const std::string& seri, const std::vector<std::string>& komutlar,
                          i64 bitis) {
  std::string g = "belge=bilet\n";
  g += "cihaz=" + cihaz + "\n";
  g += "musteri=" + musteri + "\n";
  g += "seri=" + seri + "\n";
  g += "bitis=" + std::to_string(bitis) + "\n";
  for (const std::string& k : komutlar) g += "komut=" + k + "\n";
  return g;
}

}  // namespace

bool yonetimLisansYaz(const std::string& ozelAnahtarYolu, const CihazOzet& cihaz,
                      const std::string& musteri, const std::string& seri,
                      const std::vector<std::string>& komutlar, i64 bitisUnix,
                      std::string& hata) {
  if (!g_http.oturumAcik) { hata = "Once oturum acin"; return false; }
  if (cihaz.cihazKimlik.empty()) { hata = "Cihaz secilmedi"; return false; }

  std::string ozelHex;
  if (!ozelAnahtarOku(ozelAnahtarYolu, ozelHex, hata)) return false;

  // Kanonik govde + ECDSA imzasi
  const std::string govde = biletGovdeKur(cihaz.cihazKimlik, musteri, seri, komutlar, bitisUnix);
  std::string imza;
  if (!kripto::imzala(govde, ozelHex, imza)) {
    hata = "Imzalama basarisiz (anahtar gecersiz olabilir)";
    return false;
  }

  // kokDB .ekle -> fikocadLisanslari (cihaz bazinda tek kayit: _id=cihaz)
  std::string j = "{";
  j += "\"_id\":\"" + kacisla(cihaz.cihazKimlik) + "\"";
  j += ",\"cihaz\":\"" + kacisla(cihaz.cihazKimlik) + "\"";
  j += ",\"musteri\":\"" + kacisla(musteri) + "\"";
  j += ",\"seri\":\"" + kacisla(seri) + "\"";
  j += ",\"bitis\":" + std::to_string(bitisUnix);
  j += ",\"komutlar\":[";
  for (std::size_t i = 0; i < komutlar.size(); ++i) {
    if (i) j += ",";
    j += "\"" + kacisla(komutlar[i]) + "\"";
  }
  j += "]";
  j += ",\"govde\":\"" + kacisla(govde) + "\"";
  j += ",\"imza\":\"" + kacisla(imza) + "\"";
  j += ",\"aktif\":true";
  j += ",\"uretimZamani\":" + std::to_string((i64)std::time(nullptr));
  // Kopma suresi: sunucu belirler (varsayilan 30 sn)
  j += ",\"kopmaSuresi\":30,\"uyari1\":5,\"uyari2\":15";
  j += "}";

  std::string cevap;
  int kod = 0;
  if (!istek("/fapiv1/fikoyt.com.FikoYT.sistem.DB.fikocadLisanslari.ekle",
             j, cevap, kod, hata))
    return false;

  // Fikocad'in ANINDA fark etmesi icin lisans yenile komutu gonder
  YonetimDurumu& d = yonetimDurumu();
  d.cevapHatasi.clear();
  std::string uyari;
  yonetimKomutGonder(cihaz, "lisans.yenile", "Lisans guncellendi", uyari);

  gunluk::bilgi("Uzak lisans yazildi: {} ({} komut)", seri, komutlar.size());
  return true;
}

bool yonetimKomutYetkilendir(const CihazOzet& cihaz,
                             const std::vector<std::string>& komutlar,
                             std::string& hata) {
  if (komutlar.empty()) { hata = "Komut secilmedi"; return false; }
  std::string mesaj;
  for (std::size_t i = 0; i < komutlar.size(); ++i) {
    if (i) mesaj += ",";
    mesaj += komutlar[i];
  }
  return yonetimKomutGonder(cihaz, "lisans.komutlari", mesaj, hata);
}

bool yonetimKilitle(const CihazOzet& cihaz, bool kilitli, const std::string& mesaj,
                    std::string& hata) {
  return yonetimKomutGonder(cihaz, kilitli ? "lisans.kilitle" : "lisans.kilitac",
                            mesaj, hata);
}

// ── Beyaz liste: uzaktan calistirilabilecek komutlar ───────────────────────────
const std::vector<std::string>& yonetimBeyazListe() {
  static const std::vector<std::string> v = {
      // Lisans yonetimi
      "lisans.yenile", "lisans.komutlari", "lisans.kilitle", "lisans.kilitac",
      "lisans.uzat", "lisans.kota.sifirla",
      // Gorus (guvenli - salt okuma/degistirme)
      "gorunum.tumunu_sigdir", "gorunum.secimi_cercevele", "gorunum.izometrik",
      "gorunum.ust", "gorunum.on", "gorunum.ortografik",
      "gorunum.iziklar", "gorunum.malzeme",
      // Secim kipleri
      "secim.kip_govde", "secim.kip_yuz", "secim.kip_kenar",
      // Analiz (salt okuma)
      "dosya.fark", "analiz.cekme_acisi", "analiz.kalınligi", "analiz.kurvatur",
      // Araclar
      "arac.guncelleme", "arac.geri_bildirim", "sistem.temizle",
  };
  return v;
}

}  // namespace fiko



