package main

// Cerceveli protokol 2 ile calisan eklenti oturumlari (Asama 2).
//  - Her eklenti icin tek kalici surec; ilk cagrida lazy baslar ve merhaba ile el sikisir.
//  - Ping her ekPingAraligi'nda atilir; ardisik hatalar surec yeniden baslatilir.
//  - Ayar yalnizca degistiginde gonderilir (eklenti bellekte tutar).
//  - Buyuk sonuclar "fikoyt/parca" bildirimleriyle gelir; bridge bunlari birlestirmeden sunucuya iletir (sunucu birlestirir).
//  - Eklentinin onay istegi yalnizca bridge'in kendi metin tablosundaki izin turleri icin gosterilir.

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os/exec"
	"strings"
	"sync"
	"syscall"
	"time"

	"fikoyt-local-bridge/eklenti_protokol"
)

var (
	ekPingAraligi     = 5 * time.Second
	ekPingZamanAsimi  = 2 * time.Second
	ekPingHataSiniri  = 3
	ekCagriZamanAsimi = 10 * time.Second
	// ekElSikisZamanAsimi: merhaba cevabi icin bekleme (protokol 2).
	ekElSikisZamanAsimi = 3 * time.Second
)

// errEklentiElSikis: eklenti surec olarak basladi ama protokol 2 ile el sikismadi.
// Surecin hic baslamamasi bu hata degildir.
var errEklentiElSikis = errors.New("eklenti protokol 2 el sikismasi basarisiz")

var errEklentiYok = errors.New("eklenti calismiyor")

// ekOnayMetni: bridge'in kendi yazdigi onay metni. Eklenti metin gonderemez.
type ekOnayMetni struct {
	Baslik   string
	Aciklama string
}

// ekOnayMetinleri: eklenti anahtari -> izin turu -> bridge metni. Bir eklenti yalnizca kendi
// turlerini isteyebilir; baska eklentinin turu bu tabloda aranmaz. Eklenti metin gonderemez.
var ekOnayMetinleri = map[string]map[string]ekOnayMetni{}

// ekOnayMetniBul: anahtar ve tur ikilisi tabloda yoksa reddedilir.
func ekOnayMetniBul(anahtar, tur string) (ekOnayMetni, bool) {
	m, ok := ekOnayMetinleri[anahtar][tur]
	return m, ok
}

type ekOturum struct {
	tablo   string // profil|eklenti: ekOturumlar anahtari
	profil  string // bu surecin ait oldugu hesap (olaylar bu hesabin soketinden gider)
	anahtar string
	cmd     *exec.Cmd
	istemci *protokol.Istemci
	kapali  chan struct{}

	mu       sync.Mutex
	sonAyar  []byte
	pingHata int

	// Ping zamanlamasi oturum acilirken kopyalanir; ping dongusu paket degiskenlerini okumaz (veri yarisi olmaz).
	pingAraligi    time.Duration
	pingZamanAsimi time.Duration
	pingHataSiniri int
}

var (
	ekMu        sync.Mutex
	ekOturumlar = map[string]*ekOturum{}
)

// ekOturumAlProfil: verilen profil icin calisan oturumu dondurur. Yoksa yol/args ile yeni surec baslatir.
// Surec baslatma ve el sikisma global kilit disinda yapilir; yavas bir eklenti digerlerini bekletmez.
func ekOturumAlProfil(profil, anahtar, yol string, args []string) (*ekOturum, error) {
	tablo := ekTabloAnahtari(profil, anahtar)
	ekMu.Lock()
	if o, ok := ekOturumlar[tablo]; ok {
		select {
		case <-o.kapali:
			delete(ekOturumlar, tablo)
		default:
			ekMu.Unlock()
			return o, nil
		}
	}
	ekMu.Unlock()

	o, err := ekOturumBaslatProfil(profil, anahtar, yol, args)
	if err != nil {
		return nil, err
	}

	ekMu.Lock()
	defer ekMu.Unlock()
	if mevcut, ok := ekOturumlar[tablo]; ok {
		// Bu arada baska bir cagri ayni eklentiyi baslatmis: yenisini kapat, mevcudu kullan.
		o.istemci.Kapat()
		return mevcut, nil
	}
	ekOturumlar[tablo] = o
	return o, nil
}

// ekOturumBaslatProfil: verilen profil icin eklenti surecini baslatir ve el sikisir.
func ekOturumBaslatProfil(profil, anahtar, yol string, args []string) (*ekOturum, error) {
	cmd := exec.Command(yol, args...)
	cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
	cmd.Stderr = io.Discard
	stdin, err := cmd.StdinPipe()
	if err != nil {
		return nil, err
	}
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, err
	}
	if err := cmd.Start(); err != nil {
		return nil, err
	}
	surecIsNesnesineBagla(cmd.Process.Pid)

	o := &ekOturum{
		tablo:          ekTabloAnahtari(profil, anahtar),
		profil:         profil,
		anahtar:        anahtar,
		cmd:            cmd,
		kapali:         make(chan struct{}),
		pingAraligi:    ekPingAraligi,
		pingZamanAsimi: ekPingZamanAsimi,
		pingHataSiniri: ekPingHataSiniri,
	}
	o.istemci = protokol.Yeni(stdout, stdin, o.gelen)
	go o.izle()

	ctx, iptal := context.WithTimeout(context.Background(), ekElSikisZamanAsimi)
	defer iptal()
	if _, err := protokol.ElSikis(ctx, o.istemci, surum); err != nil {
		o.istemci.Kapat()
		return nil, fmt.Errorf("%w: %w", errEklentiElSikis, err)
	}
	go o.pingDongusu()
	return o, nil
}

// izle: baglanti kapaninca sureci oldurur, bekler ve oturumu listeden cikarir.
// Surec tek yerde (burada) beklenir; okuyucu ile yaris olmaz.
func (o *ekOturum) izle() {
	<-o.istemci.Bitti()
	if o.cmd.Process != nil {
		_ = o.cmd.Process.Kill()
	}
	_ = o.cmd.Wait()
	ekMu.Lock()
	if ekOturumlar[o.tablo] == o {
		delete(ekOturumlar, o.tablo)
	}
	ekMu.Unlock()
	close(o.kapali)
}

func (o *ekOturum) pingDongusu() {
	t := time.NewTicker(o.pingAraligi)
	defer t.Stop()
	for {
		select {
		case <-o.kapali:
			return
		case <-t.C:
			ctx, iptal := context.WithTimeout(context.Background(), o.pingZamanAsimi)
			_, err := o.istemci.Cagir(ctx, protokol.YontemPing, nil)
			iptal()
			if err == nil {
				o.mu.Lock()
				o.pingHata = 0
				o.mu.Unlock()
				continue
			}
			o.mu.Lock()
			o.pingHata++
			asildi := o.pingHata >= o.pingHataSiniri
			o.mu.Unlock()
			if asildi {
				olayLog("eklenti yanit vermiyor, yeniden baslatilacak: %s", o.anahtar)
				o.istemci.Kapat()
				return
			}
		}
	}
}

// gelen: eklentinin bildirim ve isteklerini isler. Okuyucu goroutine'inde calisir; uzun is yapmaz.
func (o *ekOturum) gelen(m protokol.Mesaj) {
	switch m.Method {
	case protokol.YontemParca:
		// Birlestirme sunucuda yapilir; burada yalnizca iletilir. Sira korunur (ayni okuyucu goroutine'i).
		eklentiParcaGonder(o.profil, m.Params)
	case protokol.YontemOlay:
		// Surekli calisan eklentinin (ornegin usb) urettigi olay bildirimi; ayni hesabin soketinden iletilir.
		eklentiOlayIlet(o.profil, o.anahtar, m.Params)
	case protokol.YontemOnayIste:
		go o.onayIste(m)
	}
}

// onayIste: izin turu bridge tablosunda yoksa reddeder. Metni eklenti degil bridge yazar.
func (o *ekOturum) onayIste(m protokol.Mesaj) {
	var p struct {
		Tur string `json:"tur"`
	}
	if json.Unmarshal(m.Params, &p) != nil {
		_ = o.istemci.Yanitla(m.Id, nil, &protokol.Hata{Kod: -32602, Mesaj: "gecersiz istek"})
		return
	}
	metin, ok := ekOnayMetniBul(o.anahtar, p.Tur)
	if !ok {
		_ = o.istemci.Yanitla(m.Id, nil, &protokol.Hata{Kod: -32602, Mesaj: "izin turu desteklenmiyor"})
		return
	}
	onay, err := pcOnayi(hesapBaslik(o.profil, metin.Baslik), metin.Aciklama)
	izin := err == nil && onay
	_ = o.istemci.Yanitla(m.Id, map[string]bool{"izin": izin}, nil)
}

// ayarGonder: ayar degismediyse hicbir sey yollamaz.
func (o *ekOturum) ayarGonder(ayar json.RawMessage) error {
	o.mu.Lock()
	defer o.mu.Unlock()
	if bytes.Equal(o.sonAyar, ayar) {
		return nil
	}
	if err := o.istemci.Bildir(protokol.YontemAyar, ayar); err != nil {
		return err
	}
	o.sonAyar = append([]byte(nil), ayar...)
	return nil
}

// Cagir: once ayari (gerekiyorsa) gonderir, sonra islemi yapar. Sonuc parcali ise
// {"parcaId":...} olarak doner; birlestirme sunucuda yapilir.
func (o *ekOturum) Cagir(ctx context.Context, islem string, veri, ayar json.RawMessage) (json.RawMessage, error) {
	if err := o.ayarGonder(ayar); err != nil {
		return nil, err
	}
	return o.istemci.Cagir(ctx, islem, veri)
}

// ekCagirProfil: verilen profil icin oturumu acar ve islemi calistirir.
func ekCagirProfil(ctx context.Context, profil, anahtar, yol string, args []string, islem string, veri, ayar json.RawMessage) (json.RawMessage, error) {
	o, err := ekOturumAlProfil(profil, anahtar, yol, args)
	if err != nil {
		return nil, err
	}
	return o.Cagir(ctx, islem, veri, ayar)
}

// ekOturumAcikMi: oturum listeden silinmemis ve kapatilmamis mi.
func ekOturumAcikMi(o *ekOturum) bool {
	select {
	case <-o.kapali:
		return false
	default:
		return true
	}
}

// ekOturumVarProfil: verilen profil icin su an acik protokol 2 oturumu var mi.
func ekOturumVarProfil(profil, anahtar string) bool {
	ekMu.Lock()
	o, ok := ekOturumlar[ekTabloAnahtari(profil, anahtar)]
	ekMu.Unlock()
	return ok && ekOturumAcikMi(o)
}

// ekSurecBaslatProfil: verilen profil icin surekli eklentiyi protokol 2 oturumu olarak baslatir.
func ekSurecBaslatProfil(profil, anahtar, yol string) error {
	_, err := ekOturumAlProfil(profil, anahtar, yol, nil)
	return err
}

// ekEklentiDurdurProfil: verilen profil icin acik protokol 2 oturumunu durdurur.
func ekEklentiDurdurProfil(profil, anahtar string) error {
	ekMu.Lock()
	o, ok := ekOturumlar[ekTabloAnahtari(profil, anahtar)]
	ekMu.Unlock()
	if ok && ekOturumAcikMi(o) {
		o.istemci.Kapat()
		return nil
	}
	return errEklentiYok
}

// ekSurecCalisiyorProfil: verilen profil icin surekli eklenti calisiyor mu.
func ekSurecCalisiyorProfil(profil, anahtar string) bool {
	return ekOturumVarProfil(profil, anahtar)
}

// ekSurecTaraProfil: verilen profil icin calisan eklentiden surucu listesi ister.
func ekSurecTaraProfil(ctx context.Context, profil, anahtar string, veri json.RawMessage, ayar json.RawMessage) error {
	ekMu.Lock()
	o, ok := ekOturumlar[ekTabloAnahtari(profil, anahtar)]
	ekMu.Unlock()
	if ok && ekOturumAcikMi(o) {
		_, err := o.Cagir(ctx, "tara", veri, ayar)
		return err
	}
	return errEklentiYok
}

// ekTumunuDurdur: calisan tum eklenti surecleri kapatilir. Profil degisince cagrilir; boylece
// bir hesabin surecleri (olaylari aktif soket uzerinden gider) baska hesapta calismaz.
func ekTumunuDurdur() {
	ekMu.Lock()
	liste := make([]*ekOturum, 0, len(ekOturumlar))
	for _, o := range ekOturumlar {
		liste = append(liste, o)
	}
	ekMu.Unlock()
	for _, o := range liste {
		o.istemci.Kapat()
	}
}

// ekSurecDurdurProfil: verilen profiline ait calisan tum eklenti sureclerini durdurur.
// Diger profillerin surecleri etkilenmez.
func ekSurecDurdurProfil(profil string) {
	prefix := profil + "|"
	ekMu.Lock()
	var kapatilacak []*ekOturum
	for _, o := range ekOturumlar {
		if strings.HasPrefix(o.tablo, prefix) && ekOturumAcikMi(o) {
			kapatilacak = append(kapatilacak, o)
		}
	}
	ekMu.Unlock()
	for _, o := range kapatilacak {
		o.istemci.Kapat()
	}
}

// ekSurecAyarProfil: verilen profil icin ayari calisan eklentiye iletir (yalnizca degisince).
func ekSurecAyarProfil(profil, anahtar string, ayar json.RawMessage) error {
	ekMu.Lock()
	o, ok := ekOturumlar[ekTabloAnahtari(profil, anahtar)]
	ekMu.Unlock()
	if ok && ekOturumAcikMi(o) {
		return o.ayarGonder(ayar)
	}
	return errEklentiYok
}
