package main

// Socket.IO (Engine.IO v4) istemcisi. Sunucudaki fikoyt-sunucu socket'ine bagli kalir.
//   - Baglanti kurulunca cihaz kimligi + gizli anahtar ile cihaz odasina katilir.
//   - Sunucu "FikoYT.bridge.cihaz.degisti" gonderince senkron hemen tetiklenir.
//   - Cevrimici durumu sunucuda bu soketin varligindan turetilir.
//   - Kopunca ustel geri cekilme (2sn -> 30sn) ile yeniden baglanir.

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/gorilla/websocket"
)

const sokAdres = "wss://fikoyt.com:9000/socket.io/?EIO=4&transport=websocket"

type sokBaglanti struct {
	conn  *websocket.Conn
	yazMu sync.Mutex
}

func (b *sokBaglanti) yaz(paket string) error {
	b.yazMu.Lock()
	defer b.yazMu.Unlock()
	return b.conn.WriteMessage(websocket.TextMessage, []byte(paket))
}

// cihazSocketDongusu: arka planda surekli calisir; kopunca yeniden baglanir.
func cihazSocketDongusu(kimlik string) {
	bekleme := 2 * time.Second
	for {
		if _, err := oturumOku(); err != nil {
			time.Sleep(3 * time.Second)
			continue
		}
		sokHazirAyarla(false)
		err := soketOturumu(kimlik)
		if sokHazirMi() {
			bekleme = 2 * time.Second // basarili oturumdan sonra geri cekilmeyi sifirla
		}
		olayLog("socket: kesildi (%v), %v sonra yeniden denenecek", err, bekleme)
		time.Sleep(bekleme)
		if bekleme < 30*time.Second {
			bekleme *= 2
		}
	}
}

// sokHazirOldu: sunucu cihaz odasina katilmayi onayladi mi. Her erisim sokAktifMu altindadir
// (yazma sokHazirAyarla, okuma sokHazirMi / sokAktifMu ile birlikte). Kilitsiz erisim veri yarisidir.
var sokHazirOldu bool

func sokHazirAyarla(d bool) {
	sokAktifMu.Lock()
	defer sokAktifMu.Unlock()
	sokHazirOldu = d
}

func sokHazirMi() bool {
	sokAktifMu.Lock()
	defer sokAktifMu.Unlock()
	return sokHazirOldu
}

func soketOturumu(kimlik string) error {
	gizli, err := cihazGizliOku()
	if err != nil {
		return err
	}
	dialer := websocket.Dialer{HandshakeTimeout: 10 * time.Second}
	conn, _, err := dialer.Dial(sokAdres, nil)
	if err != nil {
		return err
	}
	defer conn.Close()
	b := &sokBaglanti{conn: conn}
	sokAktifAyarla(b)
	defer sokAktifAyarla(nil)

	// Acilis paketi: "0{...pingInterval, pingTimeout...}"
	_, ilk, err := conn.ReadMessage()
	if err != nil {
		return err
	}
	if len(ilk) < 2 || ilk[0] != '0' {
		return errors.New("beklenmeyen acilis paketi")
	}
	var acilis struct {
		PingInterval int `json:"pingInterval"`
		PingTimeout  int `json:"pingTimeout"`
	}
	_ = json.Unmarshal(ilk[1:], &acilis)
	if acilis.PingInterval == 0 {
		acilis.PingInterval = 25000
	}
	if acilis.PingTimeout == 0 {
		acilis.PingTimeout = 20000
	}
	zamanAsimi := time.Duration(acilis.PingInterval+acilis.PingTimeout) * time.Millisecond

	if err := b.yaz("40"); err != nil { // kok namespace'e baglan
		return err
	}
	for {
		_ = conn.SetReadDeadline(time.Now().Add(zamanAsimi))
		_, msg, err := conn.ReadMessage()
		if err != nil {
			return err
		}
		s := string(msg)
		switch {
		case s == "2": // ping -> pong
			if err := b.yaz("3"); err != nil {
				return err
			}
		case strings.HasPrefix(s, "40"): // namespace baglandi: cihazi tanit
			govde, _ := json.Marshal(map[string]string{"cihazKimlik": kimlik, "gizli": gizli})
			if err := b.yaz(`42["FikoYT.bridge.cihaz.hazir",` + string(govde) + `]`); err != nil {
				return err
			}
		case strings.HasPrefix(s, "41"):
			return errors.New("sunucu baglantiyi kapatti")
		case strings.HasPrefix(s, "42"):
			olay, veri, err := sokOlayAyir(s[2:])
			if err != nil {
				continue
			}
			switch olay {
			case "FikoYT.bridge.cihaz.hazir.ok":
				sokHazirAyarla(true)
				olayLog("socket: cihaz odasina katildi")
				go eklentiDurumGonder()
				go izinDurumGonder()
				go bridgeSurumGonder()
			case "FikoYT.bridge.cihaz.hazir.hata":
				return errors.New("cihaz dogrulanamadi (kayit henuz yok olabilir)")
			case "FikoYT.bridge.eklenti.istegi":
				// Onay beklerken okuma dongusu bloklanmasin diye ayri surecte islenir.
				go eklentiIstegiIsle(veri)
			case "FikoYT.bridge.guncelle.istegi":
				go guncellemeIstegiIsle()
			case "FikoYT.bridge.surec.istegi":
				go surecIstegiIsle(veri)
			case "FikoYT.bridge.cihaz.kaldirildi":
				go eslesmeKaldirildi()
			case "FikoYT.bridge.cihaz.degisti", "FikoYT.bridge.izin.istegi":
				_ = veri
				senkronTetikle()
			}
		}
	}
}

// sokOlayAyir: `["olay", {...}]` govdesini olay adi ve verisine ayirir.
func sokOlayAyir(govde string) (string, json.RawMessage, error) {
	var parcalar []json.RawMessage
	if err := json.Unmarshal([]byte(govde), &parcalar); err != nil || len(parcalar) == 0 {
		return "", nil, fmt.Errorf("olay cozulemedi")
	}
	var ad string
	if err := json.Unmarshal(parcalar[0], &ad); err != nil {
		return "", nil, err
	}
	var veri json.RawMessage
	if len(parcalar) > 1 {
		veri = parcalar[1]
	}
	return ad, veri, nil
}

// Aktif soket: olay gunlugu bu baglanti uzerinden sunucuya gider (yoksa yalnizca yerel dosyada kalir).
var (
	sokAktifMu sync.Mutex
	sokAktif   *sokBaglanti
)

func sokAktifAyarla(b *sokBaglanti) {
	sokAktifMu.Lock()
	defer sokAktifMu.Unlock()
	sokAktif = b
}

// sokGunlukGonder: olayi sunucuya iletir. Baglanti yoksa veya hata olursa sessizce gecer.
func sokGunlukGonder(k olayKaydi) {
	sokAktifMu.Lock()
	b := sokAktif
	hazir := sokHazirOldu
	sokAktifMu.Unlock()
	if b == nil || !hazir {
		return
	}
	govde, err := json.Marshal(map[string]string{"zaman": k.Zaman, "seviye": k.Seviye, "mesaj": k.Mesaj})
	if err != nil {
		return
	}
	_ = b.yaz(`42["FikoYT.bridge.log",` + string(govde) + `]`)
}
