package main

// Socket.IO (Engine.IO v4) istemcisi. Her giris yapmis profil, sunucudaki fikoyt-sunucu
// socket'ine kendi cihaz kimligiyle ayri bir baglanti acar.
//   - 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))
}

// profilSoket: bir profilin sunucu socket durumu. Her profilin kendi baglantisi ve hazir bayragi vardir.
type profilSoket struct {
	anahtar  string
	mu       sync.Mutex
	baglanti *sokBaglanti
	hazir    bool
}

var (
	soketlerMu  sync.Mutex
	soketler    = map[string]*profilSoket{}
	soketDurdur = map[string]chan struct{}{} // profil -> dongu durdurma kanali
)

// soketAl: profilin socket kaydini dondurur; yoksa olusturur.
func soketAl(anahtar string) *profilSoket {
	soketlerMu.Lock()
	defer soketlerMu.Unlock()
	s, ok := soketler[anahtar]
	if !ok {
		s = &profilSoket{anahtar: anahtar}
		soketler[anahtar] = s
	}
	return s
}

func (s *profilSoket) baglantiAyarla(b *sokBaglanti) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.baglanti = b
}

func (s *profilSoket) baglantiAl() *sokBaglanti {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.baglanti
}

func (s *profilSoket) hazirAyarla(d bool) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.hazir = d
}

func (s *profilSoket) hazirMi() bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.hazir
}

// sokProfildeBaglanti: verilen profilin acik baglantisi (yoksa nil).
func sokProfildeBaglanti(profil string) *sokBaglanti {
	return soketAl(profil).baglantiAl()
}

// sokProfildeVeHazir: verilen profilin baglantisi ve hazir durumu.
func sokProfildeVeHazir(profil string) (*sokBaglanti, bool) {
	s := soketAl(profil)
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.baglanti, s.hazir
}

// sokHerhangiVeHazir: etkin hesaplardan hazir olan ilk baglantiyi dondurur. Guncelleme gibi tum
// bilgisayar icin tek islemler bunu kullanir; hicbir hesap aktif sayilmaz.
func sokHerhangiVeHazir() (*sokBaglanti, bool) {
	for _, a := range etkinHesaplar() {
		if b, hazir := sokProfildeVeHazir(a); b != nil && hazir {
			return b, true
		}
	}
	return nil, false
}

// bekleDurdur: d kadar bekler; durdurma kanali kapanirsa false doner.
func bekleDurdur(durdur <-chan struct{}, d time.Duration) bool {
	t := time.NewTimer(d)
	defer t.Stop()
	select {
	case <-durdur:
		return false
	case <-t.C:
		return true
	}
}

// profilSoketDongusu: bir profilin socket'i surekli calisir; kopunca kendi geri cekilmesiyle
// yeniden baglanir. Durdurma kanali kapaninca doner.
func profilSoketDongusu(anahtar string, durdur <-chan struct{}) {
	s := soketAl(anahtar)
	bekleme := 2 * time.Second
	for {
		select {
		case <-durdur:
			return
		default:
		}
		if _, err := oturumOkuProfil(anahtar); err != nil {
			if !bekleDurdur(durdur, 3*time.Second) {
				return
			}
			continue
		}
		kimlik, kErr := cihazKimligiProfil(anahtar)
		if kErr != nil {
			if !bekleDurdur(durdur, 3*time.Second) {
				return
			}
			continue
		}
		s.hazirAyarla(false)
		err := profilSoketOturumu(anahtar, kimlik)
		if s.hazirMi() {
			bekleme = 2 * time.Second // basarili oturumdan sonra geri cekilmeyi sifirla
		}
		olayLog("socket (%s): kesildi (%v), %v sonra yeniden denenecek", anahtar, err, bekleme)
		if !bekleDurdur(durdur, bekleme) {
			return
		}
		if bekleme < 30*time.Second {
			bekleme *= 2
		}
	}
}

// profilSoketBaslat: profilin socket dongusunu baslatir. Zaten calisiyorsa hicbir sey yapmaz.
func profilSoketBaslat(anahtar string) {
	soketlerMu.Lock()
	defer soketlerMu.Unlock()
	if _, ok := soketDurdur[anahtar]; ok {
		return
	}
	d := make(chan struct{})
	soketDurdur[anahtar] = d
	go profilSoketDongusu(anahtar, d)
}

// profilSoketDurdur: profilin socket dongusunu durdurur ve baglantisini kapatir.
func profilSoketDurdur(anahtar string) {
	soketlerMu.Lock()
	d, ok := soketDurdur[anahtar]
	if ok {
		delete(soketDurdur, anahtar)
		close(d)
	}
	s := soketler[anahtar]
	soketlerMu.Unlock()
	if s != nil {
		if b := s.baglantiAl(); b != nil {
			_ = b.conn.Close()
		}
	}
}

// profilSocketleriBaslat: oturumu olan tum profiller icin socket dongusu baslatir.
func profilSocketleriBaslat() {
	liste, err := profilListesi()
	if err != nil {
		olayLog("profil listesi okunamadi: %v", err)
		return
	}
	for _, p := range liste {
		profilSoketBaslat(p.Anahtar)
	}
}

// profilSoketOturumu: tek bir baglantinin tum yasam dongusu (kimlik, el sikisma, olaylar).
func profilSoketOturumu(anahtar, kimlik string) error {
	gizli, err := cihazGizliOkuProfil(anahtar)
	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}
	s := soketAl(anahtar)
	s.baglantiAyarla(b)
	defer s.baglantiAyarla(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
		}
		str := string(msg)
		switch {
		case str == "2": // ping -> pong
			if err := b.yaz("3"); err != nil {
				return err
			}
		case strings.HasPrefix(str, "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(str, "41"):
			return errors.New("sunucu baglantiyi kapatti")
		case strings.HasPrefix(str, "42"):
			olay, veri, err := sokOlayAyir(str[2:])
			if err != nil {
				continue
			}
			if !sokOlayIzinli(olay, anahtar) {
				olayLog("socket (%s): etkin olmayan hesabin komutu yok sayildi: %s", anahtar, olay)
				continue
			}
			switch olay {
			case "FikoYT.bridge.cihaz.hazir.ok":
				s.hazirAyarla(true)
				olayLog("socket (%s): cihaz odasina katildi", anahtar)
				go eklentiDurumGonderProfil(anahtar)
				go izinDurumGonderProfil(anahtar)
				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(anahtar, veri)
			case "FikoYT.bridge.guncelle.istegi":
				go guncellemeIstegiIsle()
			case "FikoYT.bridge.surec.istegi":
				go surecIstegiIsle(anahtar, veri)
			case "FikoYT.bridge.cihaz.kaldirildi":
				go eslesmeKaldirildi(anahtar)
			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
}

// sokGunlukGonder: olayi aktif profilin sunucu baglantisina iletir. Baglanti yoksa sessizce gecer.
func sokGunlukGonder(k olayKaydi) {
	b, hazir := sokHerhangiVeHazir()
	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) + `]`)
}
