package main

// Bridge kendi kendini gunceller. Akis: manifest -> surum karsilastirma -> PC'de onay
// (kullanici "Her zaman otomatik guncelle" derse sonraki guncellemeler sorulmadan kurulur)
// -> indirme (yalnizca fikoyt.com) -> eklenti ile ayni imza ve ozet dogrulamasi ->
// calisan dosya yeniden adlandirilir, yeni dosya yerine konur, yeni surec baslatilip
// eski surec kapanir. Dogrulama basarisizsa hicbir dosya degismez.
// Web arayuzu guncelleme isteyebilir (sunucu uzerinden); izni yine bu bilgisayarda verilir.
// Asamalar sunucuya "FikoYT.bridge.guncelleme.durum" olayiyla bildirilir.

import (
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

const guncellemeManifestAdres = "https://fikoyt.com:9000/bridge/manifest.json"
const guncellemeIlkGecikme = 30 * time.Second
const guncellemeAralik = 6 * time.Hour
const guncellemeMaksManifest = 16 << 10
const guncellemeBayrak = "FIKOYT_GUNCELLEME"

type guncellemeManifest struct {
	Anahtar string `json:"anahtar"`
	Surum   string `json:"surum"`
	Sha256  string `json:"sha256"`
	Imza    string `json:"imza"`
	Bicim   string `json:"bicim"`
	Acilis  bool   `json:"acilis"`
	Url     string `json:"url"`
}

type guncellemeAyar struct {
	Otomatik bool `json:"otomatik"`
}

func guncellemeAyarDosyasi() (string, error) {
	klasor, err := veriKlasoru()
	if err != nil {
		return "", err
	}
	return filepath.Join(klasor, "guncelleme.json"), nil
}

func guncellemeAyarOku() guncellemeAyar {
	var a guncellemeAyar
	yol, err := guncellemeAyarDosyasi()
	if err != nil {
		return a
	}
	veri, err := os.ReadFile(yol)
	if err != nil {
		return a
	}
	_ = json.Unmarshal(veri, &a)
	return a
}

func guncellemeAyarYaz(a guncellemeAyar) error {
	yol, err := guncellemeAyarDosyasi()
	if err != nil {
		return err
	}
	veri, err := json.Marshal(a)
	if err != nil {
		return err
	}
	return os.WriteFile(yol, veri, 0o600)
}

// surumKarsilastir: a > b ise 1, esitse 0, kucukse -1. On surumlu "0.1.0-dev", "0.1.0"dan kucuktur.
func surumKarsilastir(a, b string) int {
	an, ap := surumParcala(a)
	bn, bp := surumParcala(b)
	for i := 0; i < 3; i++ {
		if an[i] != bn[i] {
			if an[i] > bn[i] {
				return 1
			}
			return -1
		}
	}
	switch {
	case ap == bp:
		return 0
	case ap == "":
		return 1
	case bp == "":
		return -1
	case ap > bp:
		return 1
	default:
		return -1
	}
}

func surumParcala(s string) ([3]int, string) {
	var n [3]int
	on := ""
	if i := strings.IndexAny(s, "-+"); i >= 0 {
		on = s[i+1:]
		s = s[:i]
	}
	for i, p := range strings.SplitN(s, ".", 3) {
		n[i], _ = strconv.Atoi(p)
	}
	return n, on
}

// sokOlayGonder: sunucuya adli bir olay yollar. Baglanti yoksa sessizce gecer.
func sokOlayGonder(olay string, veri map[string]any) {
	sokAktifMu.Lock()
	b := sokAktif
	hazir := sokHazirOldu
	sokAktifMu.Unlock()
	if b == nil || !hazir {
		return
	}
	govde, err := json.Marshal(veri)
	if err != nil {
		return
	}
	_ = b.yaz(`42["` + olay + `",` + string(govde) + `]`)
}

// bridgeSurumGonder: baglanti kurulunca sunucuya calisan surumu ve otomatik tercihi bildirir.
func bridgeSurumGonder() {
	sokOlayGonder("FikoYT.bridge.surum", map[string]any{"surum": surum, "otomatik": guncellemeAyarOku().Otomatik})
}

// guncellemeDurumGonder: guncelleme asamasini sunucuya bildirir (web arayuzu bu olayi dinler).
func guncellemeDurumGonder(durum, hedef string) {
	sokOlayGonder("FikoYT.bridge.guncelleme.durum", map[string]any{"durum": durum, "surum": surum, "hedefSurum": hedef})
}

// guncellemeIstegiIsle: web'den gelen "simdi guncelle" istegi. Yeni surum yoksa bunu bildirir.
func guncellemeIstegiIsle() {
	olayLog("guncelleme istegi web'den alindi")
	guncellemeKontrol(true)
}

// bekleyenOnayiYenidenAc: baska bir onay ekrani aciksa onu yeniden acar. Acik istek varsa true doner.
// Her tekrar istegi yeni bir sekme acmasin diye en fazla 15 saniyede bir yeniden acilir.
func bekleyenOnayiYenidenAc() bool {
	eslesMu.Lock()
	id := bekleyenID
	yeniden := id != "" && time.Since(sonOnayAcilis) > onayYenidenAcmaAraligi
	if yeniden {
		sonOnayAcilis = time.Now()
	}
	eslesMu.Unlock()
	if id == "" {
		return false
	}
	if yeniden {
		_ = tarayiciAc("http://127.0.0.1:7878/onay/" + id)
	}
	return true
}

// guncellemeManifestOku: manifesti yalnizca izinli adresten ceker (yonlendirmeler dahil).
func guncellemeManifestOku() (guncellemeManifest, error) {
	var m guncellemeManifest
	if err := izinliIndirmeAdresi(guncellemeManifestAdres); err != nil {
		return m, err
	}
	istemci := &http.Client{
		Timeout: 20 * time.Second,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return izinliIndirmeAdresi(req.URL.String())
		},
	}
	yanit, err := istemci.Get(guncellemeManifestAdres)
	if err != nil {
		return m, err
	}
	defer yanit.Body.Close()
	if yanit.StatusCode != http.StatusOK {
		return m, errors.New("guncelleme manifesti alinamadi")
	}
	veri, err := io.ReadAll(io.LimitReader(yanit.Body, guncellemeMaksManifest+1))
	if err != nil {
		return m, err
	}
	if len(veri) > guncellemeMaksManifest {
		return m, errors.New("guncelleme manifesti cok buyuk")
	}
	err = json.Unmarshal(veri, &m)
	return m, err
}

// guncellemeKontrol: yeni surum varsa onay ister (ya da otomatik moddaysa dogrudan kurar).
// zorla=true: web'den istenmistir; sonuc (guncel / hata / red) her durumda bildirilir.
func guncellemeKontrol(zorla bool) {
	m, err := guncellemeManifestOku()
	if err != nil {
		olayLog("guncelleme kontrolu basarisiz: %v", err)
		if zorla {
			guncellemeDurumGonder("hata", "")
		}
		return
	}
	if m.Anahtar != "bridge" {
		return
	}
	if surumKarsilastir(m.Surum, surum) <= 0 {
		if zorla {
			guncellemeDurumGonder("guncel", m.Surum)
		}
		return
	}
	p := paketBilgisi{Anahtar: m.Anahtar, Surum: m.Surum, Sha256: m.Sha256, Imza: m.Imza, Bicim: m.Bicim, Acilis: m.Acilis}
	if err := paketAlanlariGecerli(p); err != nil {
		olayLog("guncelleme manifesti gecersiz: %v", err)
		if zorla {
			guncellemeDurumGonder("hata", m.Surum)
		}
		return
	}
	if !guncellemeAyarOku().Otomatik {
		guncellemeDurumGonder("onay-bekleniyor", m.Surum)
		bilgi := onayBilgi{
			baslik:   "Köprü güncellensin mi?",
			aciklama: "Yeni bir sürüm hazır: " + surum + " → " + m.Surum + ".",
			ayrinti: []string{
				"Mevcut sürüm: " + surum,
				"Yeni sürüm: " + m.Surum,
				"Köprü kısa süreliğine yeniden başlar. Eklentiler de yeniden açılır.",
				"Dosya yalnızca fikoyt.com'dan indirilir ve imzası doğrulanmadan kurulmaz.",
			},
			guncelleme: true,
		}
		izin, sure, err := onayBekleSecenekli(bilgi)
		if err != nil {
			if errors.Is(err, errMesgul) {
				// Acik onay ekrani zaten yeniden acildi (onayBekleSecenekli); karar oradan verilir.
				olayLog("guncelleme: acik onay ekrani yeniden acildi")
				guncellemeDurumGonder("onay-bekleniyor", m.Surum)
				return
			}
			olayLog("guncelleme onayi alinamadi: %v", err)
			guncellemeDurumGonder("hata", m.Surum)
			return
		}
		if !izin {
			olayLog("guncelleme ertelendi: %s", m.Surum)
			guncellemeDurumGonder("ertelendi", m.Surum)
			return
		}
		if sure == sureOtomatik {
			if err := guncellemeAyarYaz(guncellemeAyar{Otomatik: true}); err != nil {
				olayLog("otomatik guncelleme tercihi kaydedilemedi: %v", err)
			}
		}
	}
	if err := guncellemeUygula(p, m.Url); err != nil {
		olayLog("guncelleme kurulamadi: %v", err)
		guncellemeDurumGonder("hata", m.Surum)
	}
}

// guncellemeUygula: dogrulanmis paketi calisan dosyanin yerine koyar ve yeni surec baslatip cikar.
func guncellemeUygula(p paketBilgisi, adres string) error {
	if err := izinliIndirmeAdresi(adres); err != nil {
		return err
	}
	guncellemeDurumGonder("indiriliyor", p.Surum)
	veri, err := paketIndir(adres)
	if err != nil {
		return err
	}
	if err := paketDogrula(p, paketAcikAnahtar, veri); err != nil {
		return err
	}
	calisan, err := os.Executable()
	if err != nil {
		return err
	}
	yeni := calisan + ".yeni"
	eski := calisan + ".eski"
	guncellemeDurumGonder("kuruluyor", p.Surum)
	if err := os.WriteFile(yeni, veri, 0o700); err != nil {
		return err
	}
	_ = os.Remove(eski)
	if err := os.Rename(calisan, eski); err != nil {
		_ = os.Remove(yeni)
		return err
	}
	if err := os.Rename(yeni, calisan); err != nil {
		_ = os.Rename(eski, calisan)
		_ = os.Remove(yeni)
		return err
	}
	cmd := exec.Command(calisan, os.Args[1:]...)
	surecPenceresizGizle(cmd)
	cmd.Env = append(os.Environ(), guncellemeBayrak+"=1")
	if err := cmd.Start(); err != nil {
		// Yeni surec baslamadi: eski dosyayi geri koy, eski surec calismaya devam etsin.
		_ = os.Rename(calisan, yeni)
		_ = os.Rename(eski, calisan)
		_ = os.Remove(yeni)
		return err
	}
	olayLog("köprü %s kuruldu, yeniden baslatiliyor", p.Surum)
	guncellemeDurumGonder("yeniden-baslatiliyor", p.Surum)
	time.Sleep(700 * time.Millisecond) // olayin sunucuya ulasmasi icin kisa bekleme
	// Calisan eklenti surecleri ve oturumlari duzgunce kapatilir; plugin.exe kilitli kalmaz, surec oksuz kalmaz.
	eklentileriKapat()
	os.Exit(0)
	return nil
}

// guncellemeBaslangic: guncellemeden sonra acilan surec, eski surecin kapanmasi icin kisa bekler
// ve eski dosyayi temizler. Normal acilista hicbir sey yapmaz.
func guncellemeBaslangic() {
	if os.Getenv(guncellemeBayrak) == "1" {
		time.Sleep(3 * time.Second)
	}
	if calisan, err := os.Executable(); err == nil {
		_ = os.Remove(calisan + ".eski")
	}
}

// guncellemeDongusu: ilk kontrolden sonra belirli aralikla yeni surum arar.
func guncellemeDongusu() {
	time.Sleep(guncellemeIlkGecikme)
	for {
		guncellemeKontrol(false)
		time.Sleep(guncellemeAralik)
	}
}
