package sdk

import (
	"bufio"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"strconv"
	"sync"
	"time"
)

const (
	// varsayilanHavuz: ayni anda calisan islemci sayisi (Havuz ile degistirilebilir).
	varsayilanHavuz = 4
	// gunlukButcesi: stderr'e yazilacak toplam bayt siniri (surec omru boyunca).
	gunlukButcesi = 64 * 1024
	// onayZamanAsimi: bridge kullanici onayini bekleyecegi en uzun sure.
	onayZamanAsimi = 60 * time.Second

	// KodIslemYok: istenen islem kayitli degil (JSON-RPC: method not found).
	KodIslemYok = -32601
	// KodIslemHatasi: islemci hata dondurdu ya da coktu.
	KodIslemHatasi = -32000
)

var (
	// ErrZamanAsimi: bridge'den cevap gelmeden sure doldu.
	ErrZamanAsimi = errors.New("sdk: zaman asimi")
)

// Isleyici: bir islem icin is mantigi. Donen deger JSON olarak kodlanip bridge'e gider.
type Isleyici func(c *Cagri) (any, error)

// Cagri: tek bir islem cagrisinin baglami.
type Cagri struct {
	// Ctx: islem baglami. Bridge zaman asimini ayrica uygular.
	Ctx context.Context
	// Veri: bridge'in gonderdigi islem verisi (JSON).
	Veri json.RawMessage
	// Ayar: cagri anindaki ayar (fikoyt/ayar ile gelen son deger).
	Ayar json.RawMessage

	e *Eklenti
}

// Eklenti: bir eklentinin calisma zamani. Kayit cagrisi Calistir'dan once yapilmalidir.
type Eklenti struct {
	ad          string
	havuz       int
	isleyiciler map[string]Isleyici

	yazMu sync.Mutex
	out   io.Writer

	ayarMu sync.RWMutex
	ayar   json.RawMessage

	istekMu   sync.Mutex
	sonrakiId int64
	bekleyen  map[string]chan Mesaj

	// bekle: calisan islemci goroutine'leri. Girdi kapaninca bu bitene kadar cevaplar yazilir.
	bekle sync.WaitGroup

	// acilis: Calistir cikisi ayarladiktan sonra, stdin okumadan once bir kez calisir (olay gonderen goroutine'ler icin).
	acilis func()

	gunlukMu         sync.Mutex
	gunlukKullanilan int
	gunlukAsildi     bool
}

// Yeni: ad ile bir eklenti calisma zamani olusturur.
func Yeni(ad string) *Eklenti {
	return &Eklenti{
		ad:          ad,
		havuz:       varsayilanHavuz,
		isleyiciler: map[string]Isleyici{},
		bekleyen:    map[string]chan Mesaj{},
	}
}

// Havuz: ayni anda calisan islemci sayisini ayarlar (n > 0).
func (e *Eklenti) Havuz(n int) *Eklenti {
	if n > 0 {
		e.havuz = n
	}
	return e
}

// Kayit: bir islem adina isleyici baglar. Calistir'dan once cagrilmalidir.
func (e *Eklenti) Kayit(islem string, f Isleyici) {
	e.isleyiciler[islem] = f
}

// Acilis: Calistir icinde, stdin okumadan once bir kez calisacak islevi ayarlar.
// Olay ureten arka plan goroutine'lerini (dinleyici, izle) burada baslatilir.
func (e *Eklenti) Acilis(f func()) *Eklenti {
	e.acilis = f
	return e
}

// Calistir: stdin'den cerceveleri okur, stdout'a cerceve yazar. Girdi kapanana kadar bloklar.
// ping ve merhaba cevaplari okuma dongusunde verilir; islemciler ayri goroutine'lerde calisir,
// bu yuzden havuz dolu olsa bile ping cevabi gecikmez.
func (e *Eklenti) Calistir(in io.Reader, out io.Writer) error {
	e.yazMu.Lock()
	e.out = out
	e.yazMu.Unlock()
	if e.acilis != nil {
		e.acilis()
	}
	r := bufio.NewReader(in)
	sem := make(chan struct{}, e.havuz)
	for {
		m, err := oku(r)
		if err != nil {
			e.bekle.Wait()
			return err
		}
		if m.Method == "" && len(m.Id) > 0 {
			e.cevapYonlendir(m)
			continue
		}
		switch m.Method {
		case YontemMerhaba:
			e.yanitla(m.Id, map[string]any{"protokol": ProtokolSurumu, "eklenti": e.ad}, nil)
		case YontemPing:
			e.yanitla(m.Id, map[string]any{}, nil)
		case YontemAyar:
			e.ayarAyarla(m.Params)
		default:
			isl, ok := e.isleyiciler[m.Method]
			if !ok {
				e.yanitla(m.Id, nil, &Hata{Kod: KodIslemYok, Mesaj: "islem desteklenmiyor"})
				continue
			}
			e.bekle.Add(1)
			go e.calistirIsleyici(m, isl, sem)
		}
	}
}

func (e *Eklenti) calistirIsleyici(m Mesaj, isl Isleyici, sem chan struct{}) {
	defer e.bekle.Done()
	sem <- struct{}{}
	defer func() { <-sem }()
	defer func() {
		if r := recover(); r != nil {
			e.yanitla(m.Id, nil, &Hata{Kod: KodIslemHatasi, Mesaj: fmt.Sprintf("islem cokustu: %v", r)})
		}
	}()
	c := &Cagri{Ctx: context.Background(), Veri: m.Params, Ayar: e.ayarOku(), e: e}
	sonuc, err := isl(c)
	if err != nil {
		e.yanitla(m.Id, nil, &Hata{Kod: KodIslemHatasi, Mesaj: err.Error()})
		return
	}
	e.yanitla(m.Id, sonuc, nil)
}

// yanitla: id'si olan istege cevap yazar. id yoksa (bildirim) hicbir sey yazilmaz.
func (e *Eklenti) yanitla(id json.RawMessage, v any, hata *Hata) {
	if len(id) == 0 {
		return
	}
	m := Mesaj{Id: id}
	if hata != nil {
		m.Error = hata
	} else {
		raw, err := json.Marshal(v)
		if err != nil {
			m.Error = &Hata{Kod: KodIslemHatasi, Mesaj: "sonuc kodlanamadi"}
		} else {
			m.Result = raw
		}
	}
	_ = e.yaz(m)
}

func (e *Eklenti) yaz(m Mesaj) error {
	b, err := cerceve(m)
	if err != nil {
		return err
	}
	e.yazMu.Lock()
	defer e.yazMu.Unlock()
	if e.out == nil {
		return errors.New("sdk: cikis henuz baslatilmadi (Calistir cagrilmadi)")
	}
	_, err = e.out.Write(b)
	return err
}

// cevapYonlendir: bridge'in bizim istegimize verdigi cevabi bekleyen cagriya iletir.
func (e *Eklenti) cevapYonlendir(m Mesaj) {
	key := string(m.Id)
	e.istekMu.Lock()
	ch, ok := e.bekleyen[key]
	if ok {
		delete(e.bekleyen, key)
	}
	e.istekMu.Unlock()
	if ok {
		ch <- m
	}
}

// istekGonder: bridge'e id'li istek yollar ve cevabi bekler.
func (e *Eklenti) istekGonder(ctx context.Context, metot string, params any) (json.RawMessage, error) {
	pb, err := json.Marshal(params)
	if err != nil {
		return nil, err
	}
	e.istekMu.Lock()
	e.sonrakiId++
	key := strconv.FormatInt(e.sonrakiId, 10)
	ch := make(chan Mesaj, 1)
	e.bekleyen[key] = ch
	e.istekMu.Unlock()

	vazgec := func() {
		e.istekMu.Lock()
		delete(e.bekleyen, key)
		e.istekMu.Unlock()
	}
	if err := e.yaz(Mesaj{Id: json.RawMessage(key), Method: metot, Params: pb}); err != nil {
		vazgec()
		return nil, err
	}
	select {
	case m := <-ch:
		if m.Error != nil {
			return nil, m.Error
		}
		return m.Result, nil
	case <-ctx.Done():
		vazgec()
		return nil, ErrZamanAsimi
	}
}

func (e *Eklenti) ayarAyarla(p json.RawMessage) {
	e.ayarMu.Lock()
	defer e.ayarMu.Unlock()
	e.ayar = append(json.RawMessage(nil), p...)
}

func (e *Eklenti) ayarOku() json.RawMessage {
	e.ayarMu.RLock()
	defer e.ayarMu.RUnlock()
	return append(json.RawMessage(nil), e.ayar...)
}

// Ayar: son fikoyt/ayar ile gelen ayarin ham JSON'unu dondurur (kopya). Bos ise nil.
// Olay ureten goroutine'ler (Cagri baglami olmadan) anlik ayari okumak icin kullanilir.
func (e *Eklenti) Ayar() json.RawMessage {
	return e.ayarOku()
}

// OnayIste: bridge'e izin turu icin onay ister. Metni bridge yazar; eklenti metin gonderemez.
// Donen deger kullanicinin karari; bridge tabloda olmayan bir tur icin hata doner.
func (c *Cagri) OnayIste(tur string) (bool, error) {
	ctx, iptal := context.WithTimeout(c.Ctx, onayZamanAsimi)
	defer iptal()
	sonuc, err := c.e.istekGonder(ctx, YontemOnayIste, map[string]string{"tur": tur})
	if err != nil {
		return false, err
	}
	var s struct {
		Izin bool `json:"izin"`
	}
	if err := json.Unmarshal(sonuc, &s); err != nil {
		return false, ErrCerceve
	}
	return s.Izin, nil
}

// MaksOlay: tek bir olay bildiriminin govde ust siniri. Bridge olay akisinda daha kucuk
// bir sinir uygular (surecOlayBoyutLimit); asan olay bridge tarafinda dusurulur.
const MaksOlay = 64 * 1024

// ParcaGonder: buyuk bir sonucun bir parcasini bridge'e bildirim olarak yollar. Parcalar sirali
// gonderilmelidir; son sonuc (parcaId tasiyan) ayrica islemci tarafindan dondurulur.
func (c *Cagri) ParcaGonder(parcaId string, sira, toplam int, veri string) error {
	if len(veri) > MaksParca {
		return errors.New("sdk: parca boyutu sinirini asiyor")
	}
	pb, err := json.Marshal(map[string]any{"id": parcaId, "sira": sira, "toplam": toplam, "veri": veri})
	if err != nil {
		return err
	}
	return c.e.yaz(Mesaj{Method: YontemParca, Params: pb})
}

// Gunlukle: stderr'e sinirli gunluk yazar. Surec omru boyunca gunlukButcesi asilirsa susar.
func (c *Cagri) Gunlukle(bicim string, args ...any) {
	c.e.Gunlukle(bicim, args...)
}

// OlayGonder: bridge'e (ve sunucuya) bir olay bildirimi yollar. Cevap beklemez.
// veri JSON'a cevrulup fikoyt/olay bildiriminin params'i olarak gider. Bridge bunu
// anahtar bilgisiyle sunucuya iletir (FikoYT.bridge.eklenti.olay). islemci goroutine'i
// disindan (ornegin arka plan izleme dongusunden) de cagrilabilir.
func (c *Cagri) OlayGonder(veri any) error {
	return c.e.olayGonder(veri)
}

// OlayGonder: eklenti duzeyinden olay bildirimi (islemci baglami disindan da kullanilabilir).
func (e *Eklenti) OlayGonder(veri any) error {
	return e.olayGonder(veri)
}

func (e *Eklenti) olayGonder(veri any) error {
	pb, err := json.Marshal(veri)
	if err != nil {
		return err
	}
	if len(pb) > MaksOlay {
		return errors.New("sdk: olay boyutu sinirini asiyor")
	}
	return e.yaz(Mesaj{Method: YontemOlay, Params: pb})
}

// Gunlukle: stderr'e sinirli gunluk yazar.
func (e *Eklenti) Gunlukle(bicim string, args ...any) {
	e.gunlukMu.Lock()
	defer e.gunlukMu.Unlock()
	if e.gunlukAsildi {
		return
	}
	satir := fmt.Sprintf(bicim, args...) + "\n"
	if e.gunlukKullanilan+len(satir) > gunlukButcesi {
		e.gunlukAsildi = true
		_, _ = io.WriteString(os.Stderr, "sdk: gunluk butcesi asildi, sonraki satirlar yazilmayacak\n")
		return
	}
	e.gunlukKullanilan += len(satir)
	_, _ = io.WriteString(os.Stderr, satir)
}
