package uyar

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"regexp"
	"strings"
	"sync"
	"time"

	fiko_mongo "server/sistem/fikoyt_mongo"
	mail "server/sistem/mail"
	push "server/sistem/push"
	socket_ortak "server/sistem/socket_ortak"

	"github.com/dop251/goja"
	"github.com/zishang520/socket.io/v2/socket"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

// TetikleyiciMotor sunucu taraflı kural tabanlı olay işleme motorudur.
type TetikleyiciMotor struct {
	kurallar    []tetikleyiciKural
	kurallarMu  sync.RWMutex
	rateLimitMu sync.Mutex
	rateLimit   map[string]time.Time // kuralId -> son tetiklenme
	dedupMu     sync.Mutex
	dedup       map[string]time.Time // "cihazKimlik:olayKodu" -> son görülme
	firmaCacheMu sync.RWMutex
	firmaCache   map[string]string // firmaId -> firma adı
}

type tetikleyiciKural struct {
	Id                 primitive.ObjectID `bson:"_id"`
	Ad                 string             `bson:"ad"`
	Aktif              bool               `bson:"aktif"`
	FirmaId            interface{}        `bson:"firmaId"`
	Kosul              tetikleyiciKosul   `bson:"kosul"`
	Eylemler           []bson.M           `bson:"eylemler"`
	BeklemeMs          int64              `bson:"beklemeMs"`
	GunlukLimit        int                `bson:"gunlukLimit"`
	TetiklenmeSayisi   int64              `bson:"tetiklenmeSayisi"`
	SonTetiklenmeTarihi string            `bson:"sonTetiklenmeTarihi"`
}

type tetikleyiciKosul struct {
	Tip      string `bson:"tip"`
	CihazId  string `bson:"cihazId"`
	OlayKodu string `bson:"olayKodu"`
}

// Singleton motor örneği.
var TetikleyiciMotorOrnek = &TetikleyiciMotor{
	rateLimit:  make(map[string]time.Time),
	dedup:      make(map[string]time.Time),
	firmaCache: make(map[string]string),
}

// Baslat motoru MongoDB'den kuralları yükleyerek başlatır ve periyodik yenileme goroutine'ini açar.
func (m *TetikleyiciMotor) Baslat() {
	m.kurallariYukle()
	go func() {
		ticker := time.NewTicker(60 * time.Second)
		defer ticker.Stop()
		for range ticker.C {
			m.kurallariYukle()
		}
	}()
	log.Printf("[TetikleyiciMotor] Motor başlatıldı")
}

// KurallariYenile dışarıdan çağrılabilir — tetikleyici eklendiğinde/güncellendiğinde/silindiğinde anında yeniler.
func (m *TetikleyiciMotor) KurallariYenile() {
	m.kurallariYukle()
}

func (m *TetikleyiciMotor) kurallariYukle() {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	col := fiko_mongo.GetMongoDBClient().Database("fikoyt").Collection("hareketler")
	cursor, err := col.Find(ctx,
		bson.M{"vt": "FikoYT.sistem.tetikleyiciler", "aktif": true},
		options.Find().SetSort(bson.M{"_id": 1}),
	)
	if err != nil {
		log.Printf("[TetikleyiciMotor] Kural yükleme hatası: %v", err)
		return
	}
	defer cursor.Close(ctx)

	var kurallar []tetikleyiciKural
	if err := cursor.All(ctx, &kurallar); err != nil {
		log.Printf("[TetikleyiciMotor] Kural decode hatası: %v", err)
		return
	}

	m.kurallarMu.Lock()
	m.kurallar = kurallar
	m.kurallarMu.Unlock()

	log.Printf("[TetikleyiciMotor] %d aktif kural yüklendi", len(kurallar))
}

// OlayIsle gelen olay verisini kurallara göre değerlendirir ve eşleşen eylemleri tetikler.
func (m *TetikleyiciMotor) OlayIsle(data map[string]interface{}) {
	cihazKimlik, _ := data["cihaz_kimlik"].(string)
	olayKodu := m.olayKodunuCikar(data)

	if cihazKimlik == "" || olayKodu == "" {
		return
	}

	// Deduplikasyon: aynı cihaz+olay 500ms içinde tekrar gelirse atla (HMI çift emit koruması)
	dedupKey := cihazKimlik + ":" + olayKodu
	now := time.Now()
	m.dedupMu.Lock()
	if son, ok := m.dedup[dedupKey]; ok && now.Sub(son) < 500*time.Millisecond {
		m.dedupMu.Unlock()
		return
	}
	m.dedup[dedupKey] = now
	m.dedupMu.Unlock()

	m.kurallarMu.RLock()
	kurallar := make([]tetikleyiciKural, len(m.kurallar))
	copy(kurallar, m.kurallar)
	m.kurallarMu.RUnlock()

	for _, kural := range kurallar {
		if m.kuralEslesiyor(kural, cihazKimlik, olayKodu) {
			go m.kuraliCalistir(kural, cihazKimlik, olayKodu, data)
		}
	}
}

// olayKodunuCikar data'dan olay_kodu çıkarır; durum alanı varsa dönüştürür.
func (m *TetikleyiciMotor) olayKodunuCikar(data map[string]interface{}) string {
	if ok, _ := data["olay_kodu"].(string); ok != "" {
		return ok
	}
	// durum -> olay_kodu dönüşümü
	durum, _ := data["durum"].(string)
	switch durum {
	case "kart_var", "baglandı", "okutuldu":
		return "kart_koyuldu"
	case "kart_yok", "bos", "çekildi":
		return "kart_cekildi"
	default:
		return durum
	}
}

func (m *TetikleyiciMotor) kuralEslesiyor(kural tetikleyiciKural, cihazKimlik, olayKodu string) bool {
	switch kural.Kosul.Tip {
	case "cihaz_olay":
		if kural.Kosul.CihazId != "" && kural.Kosul.CihazId != cihazKimlik {
			return false
		}
		if kural.Kosul.OlayKodu != "" && kural.Kosul.OlayKodu != olayKodu {
			return false
		}
		return true
	case "mail_gelen":
		return olayKodu == "mail_geldi"
	default:
		return false
	}
}

func (m *TetikleyiciMotor) kuraliCalistir(kural tetikleyiciKural, cihazKimlik, olayKodu string, data map[string]interface{}) {
	kuralIdStr := kural.Id.Hex()

	// Rate limit kontrolü (beklemeMs=0 ise rate limit yok)
	beklemeMs := kural.BeklemeMs
	if beklemeMs > 0 {
		m.rateLimitMu.Lock()
		if son, ok := m.rateLimit[kuralIdStr]; ok && time.Since(son) < time.Duration(beklemeMs)*time.Millisecond {
			m.rateLimitMu.Unlock()
			log.Printf("[TetikleyiciMotor] Rate limit: %s (%dms)", kural.Ad, beklemeMs)
			return
		}
		m.rateLimit[kuralIdStr] = time.Now()
		m.rateLimitMu.Unlock()
	}

	log.Printf("[TetikleyiciMotor] Kural tetiklendi: %s (%s -> %s)", kural.Ad, cihazKimlik, olayKodu)

	for _, eylem := range kural.Eylemler {
		tip, _ := eylem["tip"].(string)
		switch tip {
		case "bildirim":
			m.eylemBildirim(eylem, kural, cihazKimlik, olayKodu, data)
		case "mail_gonder":
			m.eylemMail(eylem, kural, cihazKimlik, olayKodu, data)
		case "webhook":
			m.eylemWebhook(eylem, kural, cihazKimlik, olayKodu, data)
		case "cihaz_komut":
			m.eylemCihazKomut(eylem, kural, cihazKimlik)
		case "push_gonder":
			m.eylemPush(eylem, kural, cihazKimlik, olayKodu, data)
		default:
			log.Printf("[TetikleyiciMotor] Bilinmeyen eylem tipi: %s", tip)
		}
	}

	m.gecmisKaydet(kural, cihazKimlik, olayKodu)
	m.tetiklenmeSayisiArttir(kural.Id)
}

// --- Yardımcılar ---

// veriAlaniBul data ve data.veri içinde birden fazla alan adını dener, ilk bulunanı döner
func (m *TetikleyiciMotor) veriAlaniBul(data map[string]interface{}, alanlar ...string) string {
	for _, alan := range alanlar {
		if v, ok := data[alan].(string); ok && v != "" {
			return v
		}
		// float64 olarak gelebilir (JSON number)
		if v, ok := data[alan].(float64); ok {
			return fmt.Sprintf("%.0f", v)
		}
	}
	// data.veri alt objesinde ara
	if veri, ok := data["veri"].(map[string]interface{}); ok {
		for _, alan := range alanlar {
			if v, ok := veri[alan].(string); ok && v != "" {
				return v
			}
			if v, ok := veri[alan].(float64); ok {
				return fmt.Sprintf("%.0f", v)
			}
		}
	}
	// istatistik alt objesinde ara
	if ist, ok := data["istatistik"].(map[string]interface{}); ok {
		for _, alan := range alanlar {
			if v, ok := ist[alan].(string); ok && v != "" {
				return v
			}
			if v, ok := ist[alan].(float64); ok {
				return fmt.Sprintf("%.0f", v)
			}
		}
	}
	return ""
}

// personelBulKartUid kart UID'den personel adını MongoDB'den bulur
func (m *TetikleyiciMotor) personelBulKartUid(kartUid string) string {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	col := fiko_mongo.GetMongoDBClient().Database("fikoyt").Collection("hareketler")

	// 1. Önce cihaz olay loglarından personelIsim bul (en hızlı)
	var olay bson.M
	col.FindOne(ctx, bson.M{
		"kartUid":      kartUid,
		"personelIsim": bson.M{"$exists": true, "$ne": ""},
	}, &options.FindOneOptions{
		Sort:       bson.M{"_id": -1},
		Projection: bson.M{"personelIsim": 1},
	}).Decode(&olay)
	if pi, ok := olay["personelIsim"].(string); ok && pi != "" {
		return pi
	}

	// 2. Personel kaydından ad+soyad bul
	var personel bson.M
	col.FindOne(ctx, bson.M{
		"kartUid": kartUid,
		"vt":      "FikoYT.sistem.firma.personeller",
	}, &options.FindOneOptions{
		Projection: bson.M{"ad": 1, "soyad": 1, "isim": 1, "soyIsim": 1},
	}).Decode(&personel)
	if personel != nil {
		ad, _ := personel["ad"].(string)
		soyad, _ := personel["soyad"].(string)
		isim, _ := personel["isim"].(string)
		soyIsim, _ := personel["soyIsim"].(string)
		if ad != "" {
			return strings.TrimSpace(ad + " " + soyad)
		}
		if isim != "" {
			return strings.TrimSpace(isim + " " + soyIsim)
		}
	}

	return ""
}

// --- Şablon Değişkenleri ---

func (m *TetikleyiciMotor) sablonUygula(sablon string, kural tetikleyiciKural, cihazKimlik, olayKodu string, data map[string]interface{}) string {
	if sablon == "" {
		return ""
	}

	personelIsim := m.veriAlaniBul(data, "personelIsim", "personel_isim", "isim")
	kartUid := m.veriAlaniBul(data, "kartUid", "kart_kimlik", "kartKimlik", "son_kart_uid")
	aciklama, _ := data["aciklama"].(string)
	if kartUid == "" && strings.HasPrefix(aciklama, "UID: ") {
		kartUid = strings.TrimPrefix(aciklama, "UID: ")
	}
	if personelIsim == "" && kartUid != "" {
		personelIsim = m.personelBulKartUid(kartUid)
	}
	zaman := time.Now().Format("15:04:05")
	tarih := time.Now().Format("02.01.2006")

	// Cihaz bilgileri
	cihazTipi := m.veriAlaniBul(data, "cihazTipi", "cihaz_tipi", "tip")
	cihazIsmi := m.veriAlaniBul(data, "cihazAdi", "cihaz_adi", "cihazIsmi")
	kaynak := m.veriAlaniBul(data, "kaynak", "source")

	// Firma adı (cache'li DB lookup)
	firmaId := m.veriAlaniBul(data, "firmaId", "firma_id")
	firmaAdi := ""
	if firmaId != "" {
		firmaAdi = m.firmaAdiBul(firmaId)
	}

	// Personel detay (departman, unvan)
	departman := m.veriAlaniBul(data, "departman", "bolum")
	unvan := m.veriAlaniBul(data, "unvan", "gorev")
	if (departman == "" || unvan == "") && kartUid != "" {
		d, u := m.personelDetayBul(kartUid)
		if departman == "" {
			departman = d
		}
		if unvan == "" {
			unvan = u
		}
	}

	// ESP telemetri
	ip := m.veriAlaniBul(data, "ip", "ipAdresi", "ip_adresi")
	mac := m.veriAlaniBul(data, "mac", "macAdresi", "mac_adresi")
	firmware := m.veriAlaniBul(data, "firmware", "fw_surum", "firmwareSurum")
	uptime := m.veriAlaniBul(data, "uptime", "calisma_suresi")
	sinyal := m.veriAlaniBul(data, "rssi", "sinyal", "wifi_sinyal")
	batarya := m.veriAlaniBul(data, "batarya", "battery", "pil")

	// Tetiklenme sayısı
	tetiklenmeSayisi := fmt.Sprintf("%d", kural.TetiklenmeSayisi)

	// Mail değişkenleri
	gonderen, _ := data["gonderen"].(string)
	konu, _ := data["konu"].(string)
	mailTarih, _ := data["mailTarih"].(string)
	mailAlici := m.veriAlaniBul(data, "mailAlici", "mail_alici", "alici")
	mailIcerik := m.veriAlaniBul(data, "mailIcerik", "mail_icerik", "icerik")
	if len(mailIcerik) > 200 {
		mailIcerik = mailIcerik[:200]
	}

	// Dosya bilgileri
	dosyaSayisi := m.veriAlaniBul(data, "dosyaSayisi", "dosya_sayisi", "ekSayisi")
	dosyaIsimleri := m.veriAlaniBul(data, "dosyaIsimleri", "dosya_isimleri", "ekIsimleri")

	// Tüm veri JSON
	tumVeri := ""
	if jsonBytes, err := json.Marshal(data); err == nil {
		tumVeri = string(jsonBytes)
	}

	degiskenMap := map[string]interface{}{
		"cihaz":             cihazKimlik,
		"olay":              olayKodu,
		"kural":             kural.Ad,
		"personel":          personelIsim,
		"kart":              kartUid,
		"aciklama":          aciklama,
		"saat":              zaman,
		"tarih":             tarih,
		"cihaz_tipi":        cihazTipi,
		"cihaz_ismi":        cihazIsmi,
		"kaynak":            kaynak,
		"firma":             firmaAdi,
		"departman":         departman,
		"unvan":             unvan,
		"ip":                ip,
		"mac":               mac,
		"firmware":          firmware,
		"uptime":            uptime,
		"sinyal":            sinyal,
		"batarya":           batarya,
		"tetiklenme_sayisi": kural.TetiklenmeSayisi,
		"dosya_sayisi":      dosyaSayisi,
		"dosya_isimleri":    dosyaIsimleri,
		"gonderen":          gonderen,
		"konu":              konu,
		"mailTarih":         mailTarih,
		"mail_alici":        mailAlici,
		"mail_icerik":       mailIcerik,
		"icerik":            mailIcerik,
	}
	// event data alanlarını ekle (mevcut anahtarları ezmez)
	for k, v := range data {
		if _, exists := degiskenMap[k]; !exists {
			degiskenMap[k] = v
		}
	}
	if veri, ok := data["veri"].(map[string]interface{}); ok {
		for k, v := range veri {
			if _, exists := degiskenMap[k]; !exists {
				degiskenMap[k] = v
			}
		}
	}

	r := strings.NewReplacer(
		"{{cihaz}}", cihazKimlik,
		"{{olay}}", olayKodu,
		"{{kural}}", kural.Ad,
		"{{personel}}", personelIsim,
		"{{kart}}", kartUid,
		"{{aciklama}}", aciklama,
		"{{saat}}", zaman,
		"{{tarih}}", tarih,
		"{{cihaz_tipi}}", cihazTipi,
		"{{cihaz_ismi}}", cihazIsmi,
		"{{kaynak}}", kaynak,
		"{{firma}}", firmaAdi,
		"{{departman}}", departman,
		"{{unvan}}", unvan,
		"{{ip}}", ip,
		"{{mac}}", mac,
		"{{firmware}}", firmware,
		"{{uptime}}", uptime,
		"{{sinyal}}", sinyal,
		"{{batarya}}", batarya,
		"{{tetiklenme_sayisi}}", tetiklenmeSayisi,
		"{{gonderen}}", gonderen,
		"{{konu}}", konu,
		"{{mailTarih}}", mailTarih,
		"{{mail_alici}}", mailAlici,
		"{{mail_icerik}}", mailIcerik,
		"{{dosya_sayisi}}", dosyaSayisi,
		"{{dosya_isimleri}}", dosyaIsimleri,
		"{{tum_veri}}", tumVeri,
		// Eski format uyumluluğu
		"@cihaz", cihazKimlik,
		"@olay", olayKodu,
		"@kural", kural.Ad,
		"@personel", personelIsim,
		"@kart", kartUid,
	)
	sonuc := r.Replace(sablon)
	sonuc = m.ifadeCozumle(sonuc, degiskenMap)
	return sonuc
}

var yasakliRe = regexp.MustCompile(`require|import|eval|fetch|XMLHttp|process\.env|global\.|__proto__|constructor\[`)

// ifadeCozumle şablonu işler:
//   - "return" içeriyorsa → goja fonksiyon body olarak çalıştırır (params objesi üzerinden)
//   - içermiyorsa → ${...} / {{...}} expression replace yapar
func (m *TetikleyiciMotor) ifadeCozumle(sablon string, degiskenler map[string]interface{}) string {
	if sablon == "" {
		return sablon
	}

	if yasakliRe.MatchString(sablon) {
		log.Printf("[TetikleyiciMotor] Güvenlik: yasak ifade engellendi")
		return "[YASAK]"
	}

	vm := goja.New()
	timer := time.AfterFunc(150*time.Millisecond, func() {
		vm.Interrupt("zaman asimi")
	})
	defer timer.Stop()

	m.gojaYardimcilarYukle(vm)

	// Fonksiyon modu: kullanıcı "return ..." içeren bir body yazdı
	if strings.Contains(sablon, "return ") {
		jsKod := "(function(params) {\n" + sablon + "\n})"
		fnVal, err := vm.RunString(jsKod)
		if err != nil {
			log.Printf("[TetikleyiciMotor] JS derleme hatası: %v", err)
			return sablon
		}
		callable, ok := goja.AssertFunction(fnVal)
		if !ok {
			return sablon
		}
		result, err := callable(goja.Undefined(), vm.ToValue(degiskenler))
		if err != nil {
			log.Printf("[TetikleyiciMotor] JS çalıştırma hatası: %v", err)
			return sablon
		}
		out := result.String()
		if out == "undefined" || out == "null" {
			return ""
		}
		return out
	}

	// Expression modu: ${...} ve {{...}} pattern'lerini değerlendir
	ifadeRe := regexp.MustCompile(`\$\{(.+?)\}|\{\{(.+?)\}\}`)
	if !ifadeRe.MatchString(sablon) {
		return sablon
	}

	for k, v := range degiskenler {
		vm.Set(k, v)
	}

	return ifadeRe.ReplaceAllStringFunc(sablon, func(match string) string {
		var ifade string
		if strings.HasPrefix(match, "${") {
			ifade = strings.TrimSpace(match[2 : len(match)-1])
		} else {
			ifade = strings.TrimSpace(match[2 : len(match)-2])
		}
		if ifade == "" {
			return match
		}
		val, err := vm.RunString(ifade)
		if err != nil {
			return match
		}
		result := val.String()
		if result == "undefined" || result == "null" {
			return ""
		}
		return result
	})
}

// gojaYardimcilarYukle VM'e Türkçe yardımcı fonksiyonları ekler.
func (m *TetikleyiciMotor) gojaYardimcilarYukle(vm *goja.Runtime) {
	vm.Set("kisalt", func(call goja.FunctionCall) goja.Value {
		s := call.Argument(0).String()
		n := int(call.Argument(1).ToInteger())
		r := []rune(s)
		if len(r) <= n {
			return vm.ToValue(s)
		}
		return vm.ToValue(string(r[:n]) + "...")
	})
	vm.Set("buyukHarf", func(call goja.FunctionCall) goja.Value {
		return vm.ToValue(strings.ToUpper(call.Argument(0).String()))
	})
	vm.Set("kucukHarf", func(call goja.FunctionCall) goja.Value {
		return vm.ToValue(strings.ToLower(call.Argument(0).String()))
	})
	vm.Set("parcala", func(call goja.FunctionCall) goja.Value {
		s := call.Argument(0).String()
		sep := call.Argument(1).String()
		return vm.ToValue(strings.Split(s, sep))
	})
	vm.Set("birles", func(call goja.FunctionCall) goja.Value {
		parts := call.Argument(0).Export()
		sep := call.Argument(1).String()
		var strs []string
		if arr, ok := parts.([]interface{}); ok {
			for _, p := range arr {
				strs = append(strs, fmt.Sprintf("%v", p))
			}
		}
		return vm.ToValue(strings.Join(strs, sep))
	})
}

func (m *TetikleyiciMotor) firmaAdiBul(firmaId string) string {
	m.firmaCacheMu.RLock()
	if ad, ok := m.firmaCache[firmaId]; ok {
		m.firmaCacheMu.RUnlock()
		return ad
	}
	m.firmaCacheMu.RUnlock()

	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	col := fiko_mongo.GetMongoDBClient().Database("fikoyt").Collection("hareketler")

	filter := bson.M{"vt": "FikoYT.sistem.firmalar"}
	if oid, err := primitive.ObjectIDFromHex(firmaId); err == nil {
		filter["_id"] = oid
	} else {
		filter["_id"] = firmaId
	}

	var firma bson.M
	col.FindOne(ctx, filter, &options.FindOneOptions{
		Projection: bson.M{"firmaAdi": 1, "isim": 1},
	}).Decode(&firma)

	ad := ""
	if firma != nil {
		if v, ok := firma["firmaAdi"].(string); ok && v != "" {
			ad = v
		} else if v, ok := firma["isim"].(string); ok && v != "" {
			ad = v
		}
	}

	m.firmaCacheMu.Lock()
	m.firmaCache[firmaId] = ad
	m.firmaCacheMu.Unlock()
	return ad
}

func (m *TetikleyiciMotor) personelDetayBul(kartUid string) (departman, unvan string) {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	col := fiko_mongo.GetMongoDBClient().Database("fikoyt").Collection("hareketler")

	var personel bson.M
	col.FindOne(ctx, bson.M{
		"kartUid": kartUid,
		"vt":      "FikoYT.sistem.firma.personeller",
	}, &options.FindOneOptions{
		Projection: bson.M{"departman": 1, "bolum": 1, "unvan": 1, "gorev": 1},
	}).Decode(&personel)

	if personel != nil {
		if v, ok := personel["departman"].(string); ok && v != "" {
			departman = v
		} else if v, ok := personel["bolum"].(string); ok && v != "" {
			departman = v
		}
		if v, ok := personel["unvan"].(string); ok && v != "" {
			unvan = v
		} else if v, ok := personel["gorev"].(string); ok && v != "" {
			unvan = v
		}
	}
	return
}

// --- Eylemler ---

func (m *TetikleyiciMotor) eylemBildirim(eylem bson.M, kural tetikleyiciKural, cihazKimlik, olayKodu string, data map[string]interface{}) {
	mesajSablon, _ := eylem["bildirimMesaj"].(string)
	mesaj := m.sablonUygula(mesajSablon, kural, cihazKimlik, olayKodu, data)

	alicilarRaw, _ := eylem["bildirimAlicilari"].(primitive.A)
	if len(alicilarRaw) == 0 {
		return
	}

	for _, raw := range alicilarRaw {
		var aliciId primitive.ObjectID
		switch v := raw.(type) {
		case primitive.ObjectID:
			aliciId = v
		case string:
			oid, err := primitive.ObjectIDFromHex(v)
			if err != nil {
				continue
			}
			aliciId = oid
		default:
			continue
		}

		bildirim := bson.M{
			"vt":            "FikoYT.sistem.bildirimler",
			"aliciId":       aliciId,
			"gondericIsim":  "Tetikleyici Motor",
			"olay":          olayKodu,
			"mesaj":         mesaj,
			"okudu":         false,
			"tarih":         time.Now(),
			"sunucu_zamani": time.Now().Format(time.RFC3339),
			"kaynakKural":   kural.Id,
			"kaynakKuralAd": kural.Ad,
		}

		ctx := context.TODO()
		col := fiko_mongo.GetMongoDBClient().Database("fikoyt").Collection("hareketler")
		sonuc, err := col.InsertOne(ctx, bildirim)
		if err != nil {
			log.Printf("[TetikleyiciMotor] Bildirim DB kayıt hatası: %v", err)
			continue
		}
		bildirim["_id"] = sonuc.InsertedID

		go bildirimSocketPush(aliciId, bildirim)
		m.pushBildirimGonder(aliciId, mesaj)
	}
}

func (m *TetikleyiciMotor) eylemMail(eylem bson.M, kural tetikleyiciKural, cihazKimlik, olayKodu string, data map[string]interface{}) {
	alicilar := m.stringDizisiCikar(eylem["mailAlicilari"])
	if len(alicilar) == 0 {
		return
	}

	konu, _ := eylem["mailKonu"].(string)
	if konu == "" {
		konu = fmt.Sprintf("FikoYT Tetikleyici: %s", kural.Ad)
	}
	icerik, _ := eylem["mailIcerik"].(string)
	if icerik == "" {
		icerik = fmt.Sprintf("<p>Kural: <strong>%s</strong><br>Cihaz: <strong>%s</strong><br>Olay: <strong>%s</strong></p>",
			kural.Ad, cihazKimlik, olayKodu)
	} else {
		icerik = m.sablonUygula(icerik, kural, cihazKimlik, olayKodu, data)
	}
	konu = m.sablonUygula(konu, kural, cihazKimlik, olayKodu, data)

	mailSvc := mail.YeniMailService(func(kol string) *mongo.Collection {
		return fiko_mongo.GetMongoDBClient().Database("fikoyt").Collection(kol)
	})
	if err := mailSvc.TopluGonder(alicilar, konu, icerik); err != nil {
		log.Printf("[TetikleyiciMotor] Mail gönderim hatası (kural: %s): %v", kural.Ad, err)
	}
}

func (m *TetikleyiciMotor) eylemWebhook(eylem bson.M, kural tetikleyiciKural, cihazKimlik, olayKodu string, data map[string]interface{}) {
	url, _ := eylem["webhookUrl"].(string)
	if url == "" {
		return
	}

	payload := fmt.Sprintf(`{"kural":"%s","kuralId":"%s","cihaz_kimlik":"%s","olay_kodu":"%s","zaman":"%s"}`,
		kural.Ad, kural.Id.Hex(), cihazKimlik, olayKodu, time.Now().Format(time.RFC3339))

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Post(url, "application/json", bytes.NewBufferString(payload))
	if err != nil {
		log.Printf("[TetikleyiciMotor] Webhook hatası (kural: %s, url: %s): %v", kural.Ad, url, err)
		return
	}
	defer resp.Body.Close()
	io.Copy(io.Discard, resp.Body)
}

func (m *TetikleyiciMotor) eylemCihazKomut(eylem bson.M, kural tetikleyiciKural, cihazKimlik string) {
	if socket_ortak.FikoSocketIo == nil {
		return
	}

	hedefCihaz, _ := eylem["hedefCihazId"].(string)
	if hedefCihaz == "" {
		hedefCihaz = cihazKimlik
	}
	komutKodu, _ := eylem["komutKodu"].(string)

	// Üretici imzası + kanonik alanlar: istemci (uygulama/fikocad/
	// lisans_uzaktan_komut.cpp, uzakKomutUygula 0. adım) komutu ECDSA P-256
	// ile doğrular. Konsol eyleme yazar, burada yalnızca
	// "FikoYT.sistem.io.cihaz.komut" yüküne TAŞINIR — taşınmazsa istemci
	// "komut imzasi gecersiz veya eksik" ile REDDEDER.
	// NOT: mesaj/komutId KANONİK İMZANIN parçasıdır; eksik taşımak doğrulamayı
	// bozar (istemci boş değerle kurar, konsol dolusuyla imzalanır).
	mesaj, _ := eylem["mesaj"].(string)
	komutId, _ := eylem["komutId"].(string)
	imza, _ := eylem["imza"].(string)

	komutData := map[string]interface{}{
		"cihaz_kimlik": hedefCihaz,
		"komut_kodu":   komutKodu,
		"mesaj":        mesaj,
		"komutId":      komutId,
		"imza":         imza,
		"kaynak":       "tetikleyici",
		"kural_ad":     kural.Ad,
		"zaman":        time.Now().Format(time.RFC3339),
	}

	socket_ortak.FikoSocketIo.FetchSockets()(func(sockets []*socket.RemoteSocket, err error) {
		if err != nil {
			return
		}
		for _, s := range sockets {
			s.Emit("FikoYT.sistem.io.cihaz.komut", komutData)
		}
	})
}

func (m *TetikleyiciMotor) eylemPush(eylem bson.M, kural tetikleyiciKural, cihazKimlik, olayKodu string, data map[string]interface{}) {
	mesajSablon, _ := eylem["pushMesaj"].(string)
	if mesajSablon == "" {
		mesajSablon = fmt.Sprintf("%s — %s", kural.Ad, olayKodu)
	}
	mesaj := m.sablonUygula(mesajSablon, kural, cihazKimlik, olayKodu, data)

	baslik, _ := eylem["pushBaslik"].(string)
	if baslik == "" {
		baslik = "FikoYT Tetikleyici"
	}

	alicilarRaw, _ := eylem["pushAlicilari"].(primitive.A)
	if len(alicilarRaw) == 0 {
		return
	}

	pushSvc := push.YeniPushService(func(kol string) *mongo.Collection {
		return fiko_mongo.GetMongoDBClient().Database("fikoyt").Collection(kol)
	})

	for _, raw := range alicilarRaw {
		var aliciId primitive.ObjectID
		switch v := raw.(type) {
		case primitive.ObjectID:
			aliciId = v
		case string:
			oid, err := primitive.ObjectIDFromHex(v)
			if err != nil {
				continue
			}
			aliciId = oid
		default:
			continue
		}
		id := aliciId
		go func() {
			if err := pushSvc.Gonder(id, push.PushMesaj{
				Baslik: baslik,
				Mesaj:  mesaj,
				Tag:    "tetikleyici",
			}); err != nil {
				log.Printf("[TetikleyiciMotor] Push gönderim hatası (kural: %s, alici: %s): %v", kural.Ad, id.Hex(), err)
			}
		}()
	}
}

// --- Yardımcı ---

func (m *TetikleyiciMotor) gecmisKaydet(kural tetikleyiciKural, cihazKimlik, olayKodu string) {
	ctx := context.TODO()
	col := fiko_mongo.GetMongoDBClient().Database("fikoyt").Collection("hareketler")
	_, err := col.InsertOne(ctx, bson.M{
		"vt":            "FikoYT.sistem.tetikleyici_gecmis",
		"kuralId":       kural.Id,
		"kuralAd":       kural.Ad,
		"cihazKimlik":   cihazKimlik,
		"olayKodu":      olayKodu,
		"tarih":         time.Now(),
		"sunucu_zamani": time.Now().Format(time.RFC3339),
	})
	if err != nil {
		log.Printf("[TetikleyiciMotor] Geçmiş kayıt hatası: %v", err)
	}
}

func (m *TetikleyiciMotor) tetiklenmeSayisiArttir(kuralId primitive.ObjectID) {
	ctx := context.TODO()
	col := fiko_mongo.GetMongoDBClient().Database("fikoyt").Collection("hareketler")
	_, err := col.UpdateOne(ctx,
		bson.M{"_id": kuralId},
		bson.M{
			"$inc": bson.M{"tetiklenmeSayisi": 1},
			"$set": bson.M{"sonTetiklenmeTarihi": time.Now().Format(time.RFC3339)},
		},
	)
	if err != nil {
		log.Printf("[TetikleyiciMotor] Sayı güncelleme hatası: %v", err)
	}
}

func (m *TetikleyiciMotor) stringDizisiCikar(raw interface{}) []string {
	var sonuc []string
	switch v := raw.(type) {
	case primitive.A:
		for _, item := range v {
			if s, ok := item.(string); ok && s != "" {
				sonuc = append(sonuc, s)
			}
		}
	case []interface{}:
		for _, item := range v {
			if s, ok := item.(string); ok && s != "" {
				sonuc = append(sonuc, s)
			}
		}
	}
	return sonuc
}

// PushBildirimGonder tetikleyici motorunun web push göndermesi için yardımcı.
func (m *TetikleyiciMotor) pushBildirimGonder(aliciId primitive.ObjectID, mesaj string) {
	go func() {
		pushSvc := push.YeniPushService(func(kol string) *mongo.Collection {
			return fiko_mongo.GetMongoDBClient().Database("fikoyt").Collection(kol)
		})
		pushSvc.Gonder(aliciId, push.PushMesaj{
			Baslik: "FikoYT Tetikleyici",
			Mesaj:  mesaj,
			Tag:    "tetikleyici",
		})
	}()
}
