// Package lisans_kontrol — FikoCAD canlı lisans yönetimi (kill-switch).
//
// İstemci (fikocad.exe) açılışta /lisans-ws ham WebSocket'ine bağlanır,
// {"tip":"merhaba","seri":...,"musteri":...,"bilgisayar":...,"makine":[...]}
// gönderir. Yönetim paneli (/lisans-kontrol) bağlı TÜM fikocad'leri tabloda
// gösterir; Engelle/İzin ver düğmesi kararı ECDSA P-256 ile İMZALAYIP istemciye
// yollar. İstemci gömülü açık anahtarla imzayı doğrular → sahte sunucu/MITM
// karar enjekte edemez (politika .pol dosyasıyla aynı güven modeli).
//
// İmzalanan kanonik gövde (istemci canliKararKanonik ile AYNI sıra):
//
//	engelle=<1|0>
//	seri=<seri>
//	mesaj=<mesaj>
//	zaman=<unix saniye>
//
// Kararlar sistem/lisans_kararlar.json'a yazılır → sunucu yeniden başlasa da
// yeniden bağlanan istemci son kararını hello'da anında alır.
package lisans_kontrol

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"log"
	"math/big"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/gofiber/fiber/v3"
	"github.com/gofiber/fiber/v3/middleware/adaptor"
	"github.com/google/uuid"
	socket "github.com/zishang520/socket.io/v2/socket"
	socket_types "github.com/zishang520/engine.io/v2/types"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo/options"

	fiko_mongo "server/sistem/fikoyt_mongo"
)

// ── Yapılandırma (ortam değişkenleri) ────────────────────────────────────────
// LISANS_OZEL_PEM   : ECDSA P-256 özel anahtar PEM (lisans anahtarıyla aynı çift!)
// LISANS_KARAR_DOSYA: karar kalıcılığı (varsayılan sistem/lisans_kararlar.json)
// LISANS_ADMIN_TOKEN: doluysa panel/API bu token ister (?token= veya X-Token)

type Istemci struct {
	Kimlik    string    `json:"kimlik"`
	Seri      string    `json:"seri"`
	Musteri   string    `json:"musteri"`
	Bilgisayar string   `json:"bilgisayar"`
	Surum     string    `json:"surum"`
	Pid       int64     `json:"pid"`
	Makine    []string  `json:"makine"`
	Ip        string    `json:"ip"`
	Baglanma  time.Time `json:"baglanma"`

	socket *socket.Socket  // Socket.IO istemci bağlantısı
	yaz    sync.Mutex
}

type Karar struct {
	Seri    string `json:"seri"`
	Engelli bool   `json:"engelli"`
	Mesaj   string `json:"mesaj"`
	Zaman   int64  `json:"zaman"`
	Imza    string `json:"imza"`
}

var (
	mu        sync.RWMutex
	istemciler = map[string]*Istemci{} // kimlik → istemci
	kararlar   = map[string]Karar{}    // seri → son karar
	ozelAnahtar *ecdsa.PrivateKey
	kararDosya = "sistem/lisans_kararlar.json"
	adminToken string
)

// ── UZAK BİLET (kopma süresi + uyarı eşikleri) ────────────────────────────
// Sunucu belirlediği süreyi İMZALI biletle cihaza bildirir. Cihaz, bağlantı
// koptuktan sonra bu süre dolana kadar çalışır, sonra kendini kilitler.
//
// ⚠️ KANONİK METİN KURALI (istemciyle BİREBİR aynı olmalı):
//   "belge=bilet" satırı imzaya DAHİLDİR. İstemci (lisans_uzaktan_bilet.cpp)
//    yalnızca yorum (#) ve boş satırları düşer; belge= satırını da düşerse
//    geçerli bileti reddeder. Buradaki sıra ve içerik birebir aynı olmalıdır.
const (
	biletKopmaVarsayilan = 30 // saniye — bağlantı koptuktan sonra tolerans
	biletUyari1Varsayilan = 15 // 1. uyarı (bilgilendirme)
	biletUyari2Varsayilan = 25 // 2. uyarı (son uyarı)
)

type Bilet struct {
	Cihaz       string   `json:"cihaz"`
	Belge       string   `json:"belge"`
	Surum       string   `json:"surum"`
	KopmaSuresi int64    `json:"kopmaSuresi"`
	Uyari1      int64    `json:"uyari1"`
	Uyari2      int64    `json:"uyari2"`
	Yetkiler    []string `json:"yetkiler"`
	Zaman       int64    `json:"zaman"`
	Imza        string   `json:"imza"`
}

// Kanonik metin: her satır "anahtar=deger\n". Alan sırası SABİTTİR.
func biletKanonik(b *Bilet) string {
	yetkiler := ""
	for i, y := range b.Yetkiler {
		if i > 0 {
			yetkiler += ","
		}
		yetkiler += y
	}
	s := ""
	s += "belge=bilet\n"
	s += "cihaz=" + b.Cihaz + "\n"
	s += "surum=" + b.Surum + "\n"
	s += "kopmaSuresi=" + strconv.FormatInt(b.KopmaSuresi, 10) + "\n"
	s += "uyari1=" + strconv.FormatInt(b.Uyari1, 10) + "\n"
	s += "uyari2=" + strconv.FormatInt(b.Uyari2, 10) + "\n"
	s += "yetkiler=" + yetkiler + "\n"
	s += "zaman=" + strconv.FormatInt(b.Zaman, 10) + "\n"
	return s
}

func biletImzala(b *Bilet) error {
	if ozelAnahtar == nil {
		return errors.New("özel anahtar yüklenmemiş (LISANS_OZEL_PEM)")
	}
	h := sha256.Sum256([]byte(biletKanonik(b)))
	der, err := ecdsa.SignASN1(rand.Reader, ozelAnahtar, h[:])
	if err != nil {
		return err
	}
	raw, err := derToRaw(der)
	if err != nil {
		return err
	}
	b.Imza = base64.StdEncoding.EncodeToString(raw)
	return nil
}

// Bilet üret (ve istenirse diske yaz).
func biletUret(cihaz, surum string, yetkiler []string) (*Bilet, error) {
	b := &Bilet{
		Cihaz:       cihaz,
		Belge:       "bilet",
		Surum:       surum,
		KopmaSuresi: biletKopmaVarsayilan,
		Uyari1:      biletUyari1Varsayilan,
		Uyari2:      biletUyari2Varsayilan,
		Yetkiler:    yetkiler,
		Zaman:       time.Now().Unix(),
	}
	if err := biletImzala(b); err != nil {
		return nil, err
	}
	return b, nil
}

// ── Lisans Socket.IO sunucusu (izole — merkezi modülden bağımsız) ──────────
// Lisans olayları /isans namespace'i altında akar. Bu /socket.io/ endpoint'ini
// paylaşmaz — kendi ayrı sunucusudur.
var FikoLisansSocketIo *socket.Server
var lisansNamespace socket.Namespace
var lisansSettings *socket.ServerOptions

const lisansNamespaceYolu = "/isans"
const lisansSocketIOYol = "/isans.socket.io/"

// ── UZAK KOMUT Socket.IO (genel /socket.io/ endpoint'i) ────────────────────
// Cihaz listeleme, anlık komut gönderme ve komut-cevap akışı bu kanalda olur.
// C++ istemci (lisans_uzaktan_soket.cpp) şu adreslere bağlanır:
//   endpoint  : /socket.io/
//   namespace : /socket.io/  (istemci "0" + yol gönderir → yani bu namespace)
//   olay      : FikoYT.sistem.io.cihaz.komut  (sunucu → cihaz)
//               FikoYT.sistem.io.cihaz.komut-cevap (cihaz → sunucu)
//
// ⚠️ ÖNCEKİ HATA: bu endpoint hiç KAYIT EDİLMEMİŞTI. C++ istemci WebSocket'i
//    101 ile tamamlıyor, "0/socket.io/" CONNECT gönderiyor, sunucu cevabı
//    vermeden TCP'yi kapatıyordu → istemci "WinHttp 12030 CONNECTION_CLOSED"
//    (1 ms sonra) alıp saniyede bir yeniden bağlanıyordu.
var FikoUzakSocketIo *socket.Server
var uzakNamespace socket.Namespace
var uzakSettings *socket.ServerOptions

const uzakSocketIOYol = "/socket.io/"

// ── Özel anahtar yükleme ─────────────────────────────────────────────────────

// hexAnahtar: fiko_lisans "anahtar" çıktısını (96 bayt = X||Y||d, hex, tek satır)
// P-256 özel anahtarına çevirir. Biçim bozuksa ya da d·G ≠ (X,Y) ise nil döner.
func hexAnahtar(veri []byte) *ecdsa.PrivateKey {
	b, err := hex.DecodeString(strings.TrimSpace(string(veri)))
	if err != nil || len(b) != 96 {
		return nil
	}
	egri := elliptic.P256()
	k := &ecdsa.PrivateKey{}
	k.Curve = egri
	k.X = new(big.Int).SetBytes(b[0:32])
	k.Y = new(big.Int).SetBytes(b[32:64])
	k.D = new(big.Int).SetBytes(b[64:96])
	// Sıra/eşleşme doğrulaması: d·G genel noktayla aynı olmalı.
	x, y := egri.ScalarBaseMult(b[64:96])
	if x.Cmp(k.X) != 0 || y.Cmp(k.Y) != 0 {
		return nil
	}
	return k
}

func anahtarYukle() {
	yol := os.Getenv("LISANS_OZEL_PEM")
	if yol == "" {
		yol = "sistem/lisans_ozel.pem"
	}
	if d := os.Getenv("LISANS_KARAR_DOSYA"); d != "" {
		kararDosya = d
	}
	adminToken = os.Getenv("LISANS_ADMIN_TOKEN")

	veri, err := os.ReadFile(yol)
	if err != nil {
		log.Printf("[LISANS-KONTROL] UYARI: özel anahtar okunamadı (%s): %v — karar imzalama KAPALI", yol, err)
		return
	}
	blok, _ := pem.Decode(veri)
	if blok == nil {
		// fiko_lisans "anahtar" çıktısı: 96 bayt (X||Y||d) hex, tek satır.
		if k := hexAnahtar(veri); k != nil {
			ozelAnahtar = k
			log.Printf("[LISANS-KONTROL] Özel anahtar yüklendi (fiko_lisans hex): %s", yol)
			return
		}
		log.Printf("[LISANS-KONTROL] UYARI: %s ne PEM ne fiko_lisans hex — karar imzalama KAPALI", yol)
		return
	}
	if k, err := x509.ParseECPrivateKey(blok.Bytes); err == nil {
		ozelAnahtar = k
	} else if genel, err2 := x509.ParsePKCS8PrivateKey(blok.Bytes); err2 == nil {
		if k2, ok := genel.(*ecdsa.PrivateKey); ok {
			ozelAnahtar = k2
		}
	}
	if ozelAnahtar != nil && ozelAnahtar.Curve != elliptic.P256() {
		log.Printf("[LISANS-KONTROL] UYARI: anahtar P-256 değil — karar imzalama KAPALI")
		ozelAnahtar = nil
		return
	}
	if ozelAnahtar != nil {
		log.Printf("[LISANS-KONTROL] Özel anahtar yüklendi: %s", yol)
	}
}

// ── İmzalama: kanonik gövde → SHA256 → ECDSA → ham r||s base64 ──────────────

func kanonikKur(k Karar) string {
	e := 0
	if k.Engelli {
		e = 1
	}
	return fmt.Sprintf("engelle=%d\nseri=%s\nmesaj=%s\nzaman=%d\n", e, k.Seri, k.Mesaj, k.Zaman)
}

// DER (ASN.1) imzasını ham r||s (64 bayt) biçimine çevirir — istemcinin
// kripto::imzaDogrula'sı ham r||s bekler.
func derToRaw(der []byte) ([]byte, error) {
	if len(der) < 8 || der[0] != 0x30 {
		return nil, errors.New("bozuk DER imzası")
	}
	idx := 2
	if der[1]&0x80 != 0 {
		n := int(der[1] & 0x7f)
		if n == 0 || idx+n > len(der) {
			return nil, errors.New("bozuk DER uzunluk")
		}
		idx += n
	}
	okunanTamsayi := func() ([]byte, error) {
		if idx+2 > len(der) || der[idx] != 0x02 {
			return nil, errors.New("bozuk DER tamsayı")
		}
		uz := int(der[idx+1])
		if idx+2+uz > len(der) {
			return nil, errors.New("bozuk DER tamsayı uzunluğu")
		}
		v := der[idx+2 : idx+2+uz]
		idx += 2 + uz
		return v, nil
	}
	r, err := okunanTamsayi()
	if err != nil {
		return nil, err
	}
	s, err := okunanTamsayi()
	if err != nil {
		return nil, err
	}
	raw := make([]byte, 64)
	for i, v := range [][]byte{r, s} {
		if len(v) > 32 {
			v = v[len(v)-32:] // öndeki sıfırları at
		}
		copy(raw[i*32+(32-len(v)):], v) // sağa hizala
	}
	return raw, nil
}

func kararImzala(k *Karar) error {
	if ozelAnahtar == nil {
		return errors.New("özel anahtar yüklenmemiş (LISANS_OZEL_PEM)")
	}
	h := sha256.Sum256([]byte(kanonikKur(*k)))
	der, err := ecdsa.SignASN1(rand.Reader, ozelAnahtar, h[:])
	if err != nil {
		return err
	}
	raw, err := derToRaw(der)
	if err != nil {
		return err
	}
	k.Imza = base64.StdEncoding.EncodeToString(raw)
	return nil
}

// ── Karar kalıcılığı ─────────────────────────────────────────────────────────

func kararlariKaydet() {
	mu.RLock()
	veri, err := json.MarshalIndent(kararlar, "", "  ")
	mu.RUnlock()
	if err != nil {
		return
	}
	if err := os.MkdirAll(filepath.Dir(kararDosya), 0o755); err != nil {
		return
	}
	_ = os.WriteFile(kararDosya, veri, 0o644)
}

func kararlariYukle() {
	veri, err := os.ReadFile(kararDosya)
	if err != nil {
		return
	}
	var yuklenen map[string]Karar
	if json.Unmarshal(veri, &yuklenen) == nil {
		mu.Lock()
		kararlar = yuklenen
		mu.Unlock()
		log.Printf("[LISANS-KONTROL] %d karar diskten yüklendi", len(yuklenen))
	}
}

// ── Lisans Socket.IO bağlantısı ──────────────────────────────────────────────
// Fikocad istemcisi bağlandığında /isans namespace'ine bağlanır ve makine
// bilgisini gönderir. Bu bilgilerle istemci map'ine kaydedilir.
// "connection" olayı ilk argüman olarak socket'i alır.
func lisansBaglantiIsle(clients ...interface{}) {
	client := clients[0].(*socket.Socket)

	ist := &Istemci{
		Kimlik:     uuid.New().String(),
		Baglanma:   time.Now(),
		socket:     client,
	}

	// Cihaz kimliği handshake'teki "Cihaz_kimlik" başlığından; yoksa soket id.
	cihazKimlik := ""
	if hs := client.Handshake(); hs != nil {
		for anahtar, degerler := range hs.Headers {
			if strings.EqualFold(anahtar, "Cihaz_kimlik") && len(degerler) > 0 {
				cihazKimlik = degerler[0]
				break
			}
		}
	}
	soketId := string(client.Id())
	adres := ""
	if hs := client.Handshake(); hs != nil {
		adres = hs.Address
	}
	// Başlık yoksa kimlik "merhaba" gelince makine parmak izinden türetilir
	// (soket id her bağlantıda değiştiği için kalıcı kimlik olamaz).
	var kimlikMu sync.Mutex
	kimlikOku := func() string {
		kimlikMu.Lock()
		defer kimlikMu.Unlock()
		return cihazKimlik
	}
	if cihazKimlik != "" {
		go uzakCihazDBYaz(cihazKimlik, soketId, adres)
	}

	// Makine bilgisi merhaba mesajıyla gelir.
	client.On("merhaba", func(args ...interface{}) {
		if len(args) < 1 {
			return
		}
		govde, ok := args[0].(map[string]interface{})
		if !ok {
			return
		}
		ist.Seri, _ = govde["seri"].(string)
		ist.Musteri, _ = govde["musteri"].(string)
		ist.Bilgisayar, _ = govde["bilgisayar"].(string)
		ist.Surum, _ = govde["surum"].(string)
		if p, ok := govde["pid"].(float64); ok {
			ist.Pid = int64(p)
		}
		if dizi, ok := govde["makine"].([]interface{}); ok {
			ist.Makine = ist.Makine[:0]
			for _, m := range dizi {
				if s, ok := m.(string); ok {
					ist.Makine = append(ist.Makine, s)
				}
			}
		}
		mu.Lock()
		istemciler[ist.Kimlik] = ist
		mevcut, varMi := kararlar[ist.Seri]
		mu.Unlock()
		log.Printf("[LISANS-KONTROL] baglandi: %s | %s | %s | pid=%d", ist.Seri, ist.Musteri, ist.Bilgisayar, ist.Pid)
		// Kalıcı kimlik: başlık yoksa makine parmak izi (yoksa bilgisayar adı) özeti.
		kimlikMu.Lock()
		if cihazKimlik == "" {
			kaynak := strings.Join(ist.Makine, "|")
			if kaynak == "" {
				kaynak = "pc:" + ist.Bilgisayar
			}
			ozet := sha256.Sum256([]byte(kaynak))
			cihazKimlik = "fikocad-" + hex.EncodeToString(ozet[:])[:24]
			uzakCihazDBYaz(cihazKimlik, soketId, adres) // senkron: bilgi güncellemesi bundan sonra gelsin
		}
		k := cihazKimlik
		kimlikMu.Unlock()
		go uzakCihazBilgiYaz(k, ist.Seri, ist.Musteri, ist.Bilgisayar, ist.Surum)
		// Son kararı anında ilet (yeniden bağlanan istemci için).
		if varMi {
			ist.gonder(mevcut)
		}
	})

	// Disconnect: map'ten sil ve logla.
	client.On("disconnect", func(reason ...interface{}) {
		mu.Lock()
		delete(istemciler, ist.Kimlik)
		mu.Unlock()
		go uzakCihazDBSil(kimlikOku(), soketId)
		log.Printf("[LISANS-KONTROL] kopdu: %s (%s) — %v", ist.Seri, ist.Bilgisayar, reason)
	})
}

// ── Uzak komut kanalı: bağlantı işleyicisi ──────────────────────────────────
// Cihaz kimliği handshake sırasında "Cihaz_kimlik" HTTP başlığıyla gelir
// (C++ tarafı lisans_uzaktan_soket.cpp: cihazKimlikUret()). Cihaz listesinde
// görünmesi için bu kimlik DB'ye (goSoketler) yazılır.
var uzakCihazlar = map[string]*socket.Socket{} // cihaz_kimlik → soket
var uzakCihazBilgi = map[string]fiber.Map{}   // cihaz_kimlik → teşhis bilgisi
var uzakMu sync.RWMutex

// uzakKimlikDeger: cihaz kimliği ObjectID hex ise ObjectID, değilse düz string.
// (soketBaglandi ile aynı kayıt şeması: goCihazlar.kimlik / goSoketler.cihaz)
func uzakKimlikDeger(kimlik string) interface{} {
	if oid, err := primitive.ObjectIDFromHex(kimlik); err == nil {
		return oid
	}
	return kimlik
}

// uzakCihazDBYaz: bağlanan cihazı goCihazlar + goSoketler'e yazar → cihaz listesinde görünür.
func uzakCihazDBYaz(kimlik, soketId, adres string) {
	ctx, iptal := context.WithTimeout(context.Background(), 5*time.Second)
	defer iptal()
	db := fiko_mongo.GetMongoDBClient().Database("fikoyt")
	kd := uzakKimlikDeger(kimlik)

	_, err := db.Collection("goCihazlar").UpdateMany(ctx,
		bson.M{"kimlik": kd},
		bson.M{"$set": bson.M{
			"tarih":                 time.Now(),
			"kimlik":                kd,
			"tipi":                  "fikocad",
			"bilgiler.adres":        adres,
			"bilgiler.kaynak":       "lisans_kontrol",
		}},
		options.Update().SetUpsert(true))
	if err != nil {
		log.Printf("[UZAK-KOMUT] goCihazlar yazilamadi (%s): %v", kimlik, err)
	}

	_, err = db.Collection("goSoketler").ReplaceOne(ctx,
		bson.M{"soket": soketId, "cihaz": kd},
		bson.M{"tarih": time.Now(), "soket": soketId, "cihaz": kd, "kullanici": nil},
		options.Replace().SetUpsert(true))
	if err != nil {
		log.Printf("[UZAK-KOMUT] goSoketler yazilamadi (%s): %v", kimlik, err)
	}
}

// uzakCihazBilgiYaz: merhaba ile gelen seri/müşteri/bilgisayar/sürüm bilgisini goCihazlar'a işler.
func uzakCihazBilgiYaz(kimlik, seri, musteri, bilgisayar, surum string) {
	ctx, iptal := context.WithTimeout(context.Background(), 5*time.Second)
	defer iptal()
	_, err := fiko_mongo.GetMongoDBClient().Database("fikoyt").Collection("goCihazlar").UpdateMany(ctx,
		bson.M{"kimlik": uzakKimlikDeger(kimlik)},
		bson.M{"$set": bson.M{
			"bilgiler.seri":       seri,
			"bilgiler.musteri":    musteri,
			"bilgiler.bilgisayar": bilgisayar,
			"bilgiler.surum":      surum,
		}})
	if err != nil {
		log.Printf("[UZAK-KOMUT] goCihazlar bilgi yazilamadi (%s): %v", kimlik, err)
	}
}

// uzakCihazDBSil: kopan sokete ait goSoketler kaydını siler.
func uzakCihazDBSil(kimlik, soketId string) {
	ctx, iptal := context.WithTimeout(context.Background(), 5*time.Second)
	defer iptal()
	_, err := fiko_mongo.GetMongoDBClient().Database("fikoyt").Collection("goSoketler").
		DeleteMany(ctx, bson.M{"soket": soketId, "cihaz": uzakKimlikDeger(kimlik)})
	if err != nil {
		log.Printf("[UZAK-KOMUT] goSoketler silinemedi (%s): %v", kimlik, err)
	}
}

func uzakBaglantiIsle(clients ...interface{}) {
	client := clients[0].(*socket.Socket)

	// Cihaz kimligi: Socket.IO handshake header'larindan.
	// ⚠️ Handshake().Headers tipi map[string][]string (http.Header gibi) —
	//    ".Get" metodu YOK; diziden okunmali. Go harita case-sensitive'dir,
	//    bu yuzden EqualFold ile karsilastiriyoruz.
	kimlik := ""
	if hs := client.Handshake(); hs != nil {
		for anahtar, degerler := range hs.Headers {
			if strings.EqualFold(anahtar, "Cihaz_kimlik") && len(degerler) > 0 {
				kimlik = degerler[0]
				break
			}
		}
	}
	soketId := string(client.Id())
	if kimlik == "" {
		kimlik = soketId
	}

	uzakMu.Lock()
	uzakCihazlar[kimlik] = client
	uzakCihazBilgi[kimlik] = fiber.Map{
		"cihaz_kimlik": kimlik,
		"soket_id":     soketId,
		"baglanma":     time.Now(),
		"uzak":         client.Handshake().Address,
	}
	uzakMu.Unlock()

	log.Printf("[UZAK-KOMUT] cihaz baglandi: %s (soket %s)", kimlik, soketId)

	// Cihaz listesinde görünmesi için DB'ye yaz (bağlantıyı bloklamasın).
	go uzakCihazDBYaz(kimlik, soketId, client.Handshake().Address)

	// Cihaz, kendini tanıtan "merhaba" olayını gönderebilir (seri/surum vb.).
	client.On("merhaba", func(args ...interface{}) {
		if len(args) < 1 {
			return
		}
		govde, ok := args[0].(map[string]interface{})
		if !ok {
			return
		}
		uzakMu.Lock()
		if bilgi, varMi := uzakCihazBilgi[kimlik]; varMi {
			bilgi["seri"] = govde["seri"]
			bilgi["musteri"] = govde["musteri"]
			bilgi["bilgisayar"] = govde["bilgisayar"]
			bilgi["surum"] = govde["surum"]
		}
		uzakMu.Unlock()
	})

	// Komut cevabı: {"basarili":bool, ...}
	client.On("FikoYT.sistem.io.cihaz.komut-cevap", func(args ...interface{}) {
		if len(args) < 1 {
			return
		}
		log.Printf("[UZAK-KOMUT] cevap %s: %v", kimlik, args[0])
	})

	client.On("disconnect", func(reason ...interface{}) {
		uzakMu.Lock()
		delete(uzakCihazlar, kimlik)
		delete(uzakCihazBilgi, kimlik)
		uzakMu.Unlock()
		go uzakCihazDBSil(kimlik, soketId)
		log.Printf("[UZAK-KOMUT] cihaz koptu: %s (%v)", kimlik, reason)
	})
}

// Cihaza komut gönder. Erişim bu dosyadaki uzakMu korumalı.
func uzakKomutGonder(kimlik string, komut map[string]interface{}) error {
	uzakMu.RLock()
	sok, varMi := uzakCihazlar[kimlik]
	uzakMu.RUnlock()
	if !varMi || sok == nil {
		return fmt.Errorf("cihaz çevrimdışı: %s", kimlik)
	}
	return sok.Emit("FikoYT.sistem.io.cihaz.komut", komut)
}

// ── İmzalı kararı Socket.IO üzerinden istemciye ilet ─────────────────────────
func (i *Istemci) gonder(k Karar) {
	if i.socket == nil {
		return
	}
	// {"tip":"karar","seri":...,"engelli":...,"mesaj":...,"zaman":...,"imza":...}
	gonder := map[string]interface{}{
		"tip":     "karar",
		"seri":    k.Seri,
		"engelli": k.Engelli,
		"mesaj":   k.Mesaj,
		"zaman":   k.Zaman,
		"imza":    k.Imza,
	}
	if err := i.socket.Emit("lisans-karar", gonder); err != nil {
		log.Printf("[LISANS-KONTROL] karar iletilemedi: %v", err)
	}
}

// ── Panel (tek sayfa) ────────────────────────────────────────────────────────

const panelHTML = `<!doctype html>
<html lang="tr"><head><meta charset="utf-8">
<title>FikoCAD Canlı Lisans Kontrolü</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body{font-family:system-ui,sans-serif;background:#0e1116;color:#dbe4ee;margin:24px}
h1{font-size:20px} table{border-collapse:collapse;width:100%;margin-top:12px}
th,td{padding:8px 10px;border-bottom:1px solid #232b36;text-align:left;font-size:14px}
th{color:#8aa0b8;font-weight:600} tr.engelli td{background:#3a1418}
button{border:0;border-radius:6px;padding:6px 12px;margin:2px;cursor:pointer;font-size:13px}
.b-engel{background:#c0392b;color:#fff}.b-izin{background:#27ae60;color:#fff}
input{background:#161c25;border:1px solid #2a3442;color:#dbe4ee;border-radius:6px;padding:6px;width:220px}
.kucuk{color:#8aa0b8;font-size:12px}.durum{margin-left:auto;font-size:13px;color:#8aa0b8}
</style></head><body>
<h1>🔒 FikoCAD Canlı Lisans Kontrolü <span class="durum" id="durum"></span></h1>
<div class="kucuk">Bağlı tüm fikocad süreçleri. Kararlar ECDSA ile imzalanır — istemci doğrulamadan uygulamaz.</div>
<table><thead><tr><th>Seri</th><th>Müşteri</th><th>Bilgisayar</th><th>Sürüm</th><th>PID</th><th>IP</th><th>Bağlanma</th><th>Durum</th><th>İşlem</th></tr></thead>
<tbody id="govde"></tbody></table>
<script>
async function tazele(){
 try{
  const r=await fetch('/lisans-kontrol/api/istemciler'+location.search);
  const liste=await r.json();
  document.getElementById('durum').textContent=liste.length+' bağlı';
  const g=document.getElementById('govde');g.innerHTML='';
  for(const i of liste){
   const tr=document.createElement('tr');
   if(i.engelli)tr.className='engelli';
   tr.innerHTML='<td>'+esc(i.seri||'(lisanssız)')+'</td><td>'+esc(i.musteri||'-')+'</td>'+
    '<td>'+esc(i.bilgisayar||'-')+'</td><td>'+esc(i.surum||'-')+'</td><td>'+i.pid+'</td>'+
    '<td>'+esc(i.ip)+'</td><td>'+new Date(i.baglanma).toLocaleString('tr')+'</td>'+
    '<td>'+(i.engelli?'⛔ ENGELLİ'+(i.mesaj?'<div class=kucuk>'+esc(i.mesaj)+'</div>':''):'✅ izinli')+'</td>';
   const td=document.createElement('td');
   const be=document.createElement('button');be.className='b-engel';be.textContent='Engelle';
   be.onclick=()=>kararVer(i,true);
   const bz=document.createElement('button');bz.className='b-izin';bz.textContent='İzin ver';
   bz.onclick=()=>kararVer(i,false);
   td.append(be,bz);tr.append(td);g.append(tr);
  }
 }catch(e){document.getElementById('durum').textContent='sunucuya erişilemiyor'}
 setTimeout(tazele,2000);
}
async function kararVer(i,engelli){
 const mesaj=prompt('Kullanıcıya gösterilecek mesaj (opsiyonel):','')||'';
 await fetch('/lisans-kontrol/api/karar'+location.search,{method:'POST',
  headers:{'Content-Type':'application/json'},
  body:JSON.stringify({kimlik:i.kimlik,engelli,mesaj})});
 tazele();
}
function esc(s){const d=document.createElement('div');d.textContent=String(s);return d.innerHTML}
tazele();
</script></body></html>`

// ── Fiber kurulumu ───────────────────────────────────────────────────────────

func tokenTamam(c fiber.Ctx) bool {
	if adminToken == "" {
		return true
	}
	if c.Query("token") == adminToken {
		return true
	}
	if c.Get("X-Token") == adminToken {
		return true
	}
	return false
}

// Ayarla — main.go'dan çağrılır: lisans_kontrol.Ayarla(app)
func Ayarla(app *fiber.App) {
	anahtarYukle()
	kararlariYukle()

	// Lisans için ayrı Socket.IO sunucusu (izole — merkezi modülden bağımsız).
	lisansSettings = socket.DefaultServerOptions()
	lisansSettings.SetServeClient(true)
	lisansSettings.SetAllowEIO3(true)
	lisansSettings.SetAllowUpgrades(true)
	lisansSettings.SetTransports(socket_types.NewSet("polling", "websocket", "webtransport"))
	lisansSettings.SetPingInterval(25000 * time.Millisecond) // 25 saniye
	lisansSettings.SetPingTimeout(20000 * time.Millisecond)  // 20 saniye
	lisansSettings.SetMaxHttpBufferSize(1000000)
	lisansSettings.SetConnectTimeout(15000 * time.Millisecond)
	lisansSettings.SetCors(&socket_types.Cors{
		Origin:      "*",
		Credentials: true,
	})

	FikoLisansSocketIo = socket.NewServer(nil, lisansSettings)

	// Lisans olayları /isans namespace'i altında.
	lisansNamespace = FikoLisansSocketIo.Of(lisansNamespaceYolu, nil)
	lisansNamespace.On("connection", lisansBaglantiIsle)

	// Lisans Socket.IO endpoint'ini bağla.
	app.Get(lisansSocketIOYol, adaptor.HTTPHandler(FikoLisansSocketIo.ServeHandler(lisansSettings)))
	app.Post(lisansSocketIOYol, adaptor.HTTPHandler(FikoLisansSocketIo.ServeHandler(lisansSettings)))

	// ── Uzak komut kanalı: genel /socket.io/ endpoint'i ─────────────────────
	// C++ istemci bu adrese bağlanır; bağlanınca cihaz kaydı (goSoketler)
	// doldurulur ve "FikoYT.sistem.io.cihaz.komut" olayı dinlenir.
	uzakSettings = socket.DefaultServerOptions()
	uzakSettings.SetServeClient(true)
	uzakSettings.SetAllowEIO3(true) // C++ istemci EIO=3 gönderiyor
	uzakSettings.SetAllowUpgrades(true)
	uzakSettings.SetTransports(socket_types.NewSet("polling", "websocket"))
	uzakSettings.SetPingInterval(25000 * time.Millisecond)
	uzakSettings.SetPingTimeout(20000 * time.Millisecond)
	uzakSettings.SetMaxHttpBufferSize(1000000)
	uzakSettings.SetConnectTimeout(15000 * time.Millisecond)
	uzakSettings.SetCors(&socket_types.Cors{Origin: "*", Credentials: true})

	FikoUzakSocketIo = socket.NewServer(nil, uzakSettings)
	// İstemci "0" + endpoint yolu gönderiyor; yani bu namespace.
	uzakNamespace = FikoUzakSocketIo.Of(uzakSocketIOYol, nil)
	uzakNamespace.On("connection", uzakBaglantiIsle)

	app.Get(uzakSocketIOYol, adaptor.HTTPHandler(FikoUzakSocketIo.ServeHandler(uzakSettings)))
	app.Post(uzakSocketIOYol, adaptor.HTTPHandler(FikoUzakSocketIo.ServeHandler(uzakSettings)))
	log.Printf("[UZAK-KOMUT] hazır: %s", uzakSocketIOYol)

	// Yönetim paneli.
	app.Get("/lisans-kontrol", func(c fiber.Ctx) error {
		c.Set("Content-Type", "text/html; charset=utf-8")
		return c.SendString(panelHTML)
	})

	g := app.Group("/lisans-kontrol/api")

	// Bağlı istemciler (+ o serinin güncel kararı).
	g.Get("/istemciler", func(c fiber.Ctx) error {
		if !tokenTamam(c) {
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"hata": "token"})
		}
		mu.RLock()
		liste := make([]fiber.Map, 0, len(istemciler))
		for _, i := range istemciler {
			k, varMi := kararlar[i.Seri]
			engelli := varMi && k.Engelli
			mesaj := ""
			if varMi {
				mesaj = k.Mesaj
			}
			liste = append(liste, fiber.Map{
				"kimlik": i.Kimlik, "seri": i.Seri, "musteri": i.Musteri,
				"bilgisayar": i.Bilgisayar, "surum": i.Surum, "pid": i.Pid,
				"makine": i.Makine, "ip": i.Ip, "baglanma": i.Baglanma,
				"engelli": engelli, "mesaj": mesaj,
			})
		}
		mu.RUnlock()
		return c.JSON(liste)
	})

	// Karar ver: {kimlik, engelli, mesaj} → imzala + kaydet + Socket.IO ile yolla.
	g.Post("/karar", func(c fiber.Ctx) error {
		if !tokenTamam(c) {
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"hata": "token"})
		}
		var govde struct {
			Kimlik  string `json:"kimlik"`
			Engelli bool   `json:"engelli"`
			Mesaj   string `json:"mesaj"`
		}
		if err := c.Bind().Body(&govde); err != nil {
			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"hata": "gövde"})
		}
		mu.RLock()
		ist, varMi := istemciler[govde.Kimlik]
		mu.RUnlock()
		if !varMi {
			return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"hata": "istemci bağlı değil"})
		}
		k := Karar{Seri: ist.Seri, Engelli: govde.Engelli, Mesaj: govde.Mesaj, Zaman: time.Now().Unix()}
		if err := kararImzala(&k); err != nil {
			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"hata": err.Error()})
		}
		mu.Lock()
		kararlar[k.Seri] = k
		mu.Unlock()
		kararlariKaydet()
		ist.gonder(k)
		log.Printf("[LISANS-KONTROL] karar: %s → engelli=%v (%s)", k.Seri, k.Engelli, k.Mesaj)
		return c.JSON(fiber.Map{"tamam": true})
	})

	// ── Uzak komut API'si (üretici paneli) ──────────────────────────────────
	ug := app.Group("/lisans-kontrol/api/cihaz")

	// Anlık bağlı cihaz listesi.
	ug.Get("/liste", func(c fiber.Ctx) error {
		if !tokenTamam(c) {
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"hata": "token"})
		}
		uzakMu.RLock()
		liste := make([]fiber.Map, 0, len(uzakCihazBilgi))
		for _, b := range uzakCihazBilgi {
			liste = append(liste, b)
		}
		uzakMu.RUnlock()
		return c.JSON(liste)
	})

	// Komut gönder: {cihaz_kimlik, tip, ...} → "FikoYT.sistem.io.cihaz.komut".
	ug.Post("/komut", func(c fiber.Ctx) error {
		if !tokenTamam(c) {
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"hata": "token"})
		}
		var govde map[string]interface{}
		if err := c.Bind().Body(&govde); err != nil {
			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"hata": "gövde"})
		}
		kimlik, _ := govde["cihaz_kimlik"].(string)
		if kimlik == "" {
			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"hata": "cihaz_kimlik zorunlu"})
		}
		// Güvenlik: komut gövdesi üretici imzası olmadan ÇALIŞTIRILMAZ.
		// İstemci (lisans_uzaktan_komut.cpp) gömülü açık anahtarla doğrular.
		komut := map[string]interface{}{}
		for k, v := range govde {
			komut[k] = v
		}
		// Kanonik metin — istemciyle BİREBİR aynı olmalı:
		//   komutId=<id>\nkomut_kodu=<kod>\nmesaj=<mesaj>\n
		komutId, _ := govde["komutId"].(string)
		if komutId == "" {
			komutId = uuid.New().String()
			komut["komutId"] = komutId
		}
		mesajMetni, _ := govde["mesaj"].(string)
		tip, _ := govde["tip"].(string)
		if tip == "" {
			tip, _ = govde["komut_kodu"].(string)
		}
		kanonik := "komutId=" + komutId + "\n" +
			"komut_kodu=" + tip + "\n" +
			"mesaj=" + mesajMetni + "\n"
		if ozelAnahtar == nil {
			return c.Status(fiber.StatusInternalServerError).
				JSON(fiber.Map{"hata": "özel anahtar yüklenmemiş"})
		}
		ozet := sha256.Sum256([]byte(kanonik))
		der, err := ecdsa.SignASN1(rand.Reader, ozelAnahtar, ozet[:])
		if err != nil {
			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"hata": err.Error()})
		}
		raw, err := derToRaw(der)
		if err != nil {
			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"hata": err.Error()})
		}
		komut["imza"] = base64.StdEncoding.EncodeToString(raw)

		if err := uzakKomutGonder(kimlik, komut); err != nil {
			return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"hata": err.Error()})
		}
		log.Printf("[UZAK-KOMUT] komut gonderildi: %s tip=%v (imzali)", kimlik, tip)
		return c.JSON(fiber.Map{"tamam": true, "komutId": komutId})
	})

	// Bilet üret (üretici panelinden veya otomatik).
	bg := app.Group("/lisans-kontrol/api/bilet")

	bg.Get("/uret", func(c fiber.Ctx) error {
		if !tokenTamam(c) {
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"hata": "token"})
		}
		cihaz := c.Query("cihaz")
		if cihaz == "" {
			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"hata": "cihaz zorunlu"})
		}
		surum := c.Query("surum", "1.0")
		yetkiler := []string{"lisans_yenile", "cihaz_komut", "uzak_bilet"}
		b, err := biletUret(cihaz, surum, yetkiler)
		if err != nil {
			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"hata": err.Error()})
		}
		return c.JSON(b)
	})

	log.Printf("[LISANS-KONTROL] hazır: %s + /lisans-kontrol", lisansSocketIOYol)
}
