package main

// Cihaz kimligi ve senkron.
//   - Her kurulumda rastgele bir cihaz kimligi uretilir (yerelde saklanir).
//   - Giris yapildiginda cihaz, kullanicinin hesabina kaydedilir (bridge.cihaz.kaydet).
//   - Periyodik olarak senkron yapilir:
//       * Web'den kapatilan izinler yerelde de kapatilir (onay gerektirmez).
//       * Web'den acilma istegi (bekleyen) gelirse YEREL onay ekrani acilir;
//         karar sunucuya bildirilir (bridge.cihaz.karar).
//   - Yerel onay olmadan hicbir izin acilmaz.

import (
	"bytes"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
)

const fikoytCihazApi = "https://fikoyt.com:9000/fapiv1/fikoyt.com.FikoYT.sistem.bridge.cihaz."

// cihazMakineDosyasi: kimligin hangi bilgisayara ait oldugunu tutar (bilgisayar adinin ozeti).
const cihazMakineDosyasi = "cihaz-makine"

func makineParmagi() string {
	ad, err := os.Hostname()
	if err != nil || ad == "" {
		ad = "bilinmeyen"
	}
	ozet := sha256.Sum256([]byte(strings.ToLower(ad)))
	return hex.EncodeToString(ozet[:])
}

// cihazKimlikleriBuBilgisayara: kimlik bu bilgisayara ait degilse (ya da hic kaydedilmemisse)
// kimlik ve gizli anahtari yeniden uretir. Baska bir bilgisayardan kopyalanan profil, bu adimda
// kendi kimligini alir; iki bilgisayar ayni odaya girip sunucu isteklerini ikisine birden almaz.
func cihazKimlikleriBuBilgisayara() error {
	klasor, err := veriKlasoru()
	if err != nil {
		return err
	}
	parmak := makineParmagi()
	mYol := filepath.Join(klasor, cihazMakineDosyasi)
	if veri, err := os.ReadFile(mYol); err == nil && string(veri) == parmak {
		return nil
	}
	for _, ad := range []string{"cihaz-kimlik", "cihaz-gizli"} {
		if err := os.Remove(filepath.Join(klasor, ad)); err != nil && !errors.Is(err, os.ErrNotExist) {
			return err
		}
	}
	olayLog("cihaz kimliği bu bilgisayara özel olarak yenilendi")
	return os.WriteFile(mYol, []byte(parmak), 0o600)
}

// cihazKimligi: yoksa uretir, varsa okur. Kimlik, bu bilgisayarin kalici tanimidir.
func cihazKimligi() (string, error) {
	if err := cihazKimlikleriBuBilgisayara(); err != nil {
		return "", err
	}
	klasor, err := veriKlasoru()
	if err != nil {
		return "", err
	}
	yol := filepath.Join(klasor, "cihaz-kimlik")
	if veri, err := os.ReadFile(yol); err == nil && len(veri) >= 16 {
		return string(veri), nil
	}
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	kimlik := hex.EncodeToString(b)
	return kimlik, os.WriteFile(yol, []byte(kimlik), 0o600)
}

// fikoytCagri: fikoyt.com cihaz ucuna oturum cerezleriyle istek atar.
// Sunucu cevabinda durum.hata true ise hata dondurur, degilse "sonuc" alanini dondurur.
func fikoytCagri(uc string, veri map[string]any) (map[string]any, error) {
	reqBody, err := json.Marshal(veri)
	if err != nil {
		return nil, err
	}
	var govde bytes.Buffer
	mw := multipart.NewWriter(&govde)
	if err := mw.WriteField("req_body", string(reqBody)); err != nil {
		return nil, err
	}
	mw.Close()

	istek, err := http.NewRequest(http.MethodPost, fikoytCihazApi+uc, &govde)
	if err != nil {
		return nil, err
	}
	istek.Header.Set("Content-Type", mw.FormDataContentType())
	istek.Header.Set("Accept", "application/json")
	if err := fikoytIstekCerezleri(istek); err != nil {
		return nil, errors.New("giriş gerekli")
	}

	istemci := &http.Client{Timeout: 15 * time.Second}
	yanit, err := istemci.Do(istek)
	if err != nil {
		return nil, errors.New("sunucuya ulaşılamadı")
	}
	defer yanit.Body.Close()
	icerik, _ := io.ReadAll(io.LimitReader(yanit.Body, 1<<20))

	var cevap map[string]any
	if json.Unmarshal(icerik, &cevap) != nil {
		return nil, fmt.Errorf("sunucu HTTP %d döndü", yanit.StatusCode)
	}
	if durum, ok := cevap["durum"].(map[string]any); ok {
		if hata, _ := durum["hata"].(bool); hata {
			mesaj, _ := durum["mesaj"].(string)
			if mesaj == "" {
				mesaj = fmt.Sprintf("sunucu hatası (HTTP %d)", yanit.StatusCode)
			}
			return nil, errors.New(mesaj)
		}
	}
	if yanit.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("sunucu HTTP %d döndü", yanit.StatusCode)
	}
	sonuc, _ := cevap["sonuc"].(map[string]any)
	if sonuc == nil {
		sonuc = map[string]any{}
	}
	return sonuc, nil
}

// cihazKaydet: cihazi giris yapan kullanicinin hesabina kaydeder.
func cihazKaydet(kimlik string) error {
	ad, err := os.Hostname()
	if err != nil || ad == "" {
		ad = "Bilgisayar"
	}
	gizliOzet, err := cihazGizliOzet()
	if err != nil {
		return err
	}
	_, err = fikoytCagri("kaydet", map[string]any{"cihazKimlik": kimlik, "ad": ad, "gizliOzet": gizliOzet})
	return err
}

// cihazSenkronDongusu: arka planda calisir. Giris yoksa sessizce bekler.
func cihazSenkronDongusu() {
	kimlik, err := cihazKimligi()
	if err != nil {
		olayLog("cihaz kimliği okunamadı: %v", err)
		return
	}
	go cihazSocketDongusu(kimlik)
	kayitli := false
	for {
		select {
		case <-senkronTetik: // sunucu degisiklik bildirdi (socket)
		case <-time.After(10 * time.Second): // yedek periyodik senkron
		}
		if _, err := oturumOku(); err != nil {
			kayitli = false
			continue
		}
		if !kayitli {
			if err := cihazKaydet(kimlik); err != nil {
				olayLog("cihaz kaydı: %v", err)
				continue
			}
			kayitli = true
			olayLog("cihaz hesaba kaydedildi")
		}
		if err := senkronBir(kimlik); err != nil {
			olayLog("senkron: %v", err)
			// Sunucuda kayit yoksa (or. eslesme web'den kaldirildi) bir sonraki turda yeniden kaydol.
			if strings.Contains(err.Error(), "bulunamad") {
				kayitli = false
			}
		}
	}
}

func senkronBir(kimlik string) error {
	sonuc, err := fikoytCagri("senkron", map[string]any{"cihazKimlik": kimlik})
	if err != nil {
		return err
	}

	// 1) Web'den kapatilan izinleri yerelde de kapat.
	sunucuIzin, _ := sonuc["izinler"].(map[string]any)
	yerel := izinOku()
	degisti := false
	if !sunucuAcik(sunucuIzin, "everythingAra") && yerel.EverythingAra {
		yerel.EverythingAra = false
		degisti = true
	}
	if !sunucuAcik(sunucuIzin, "catiaAc") && yerel.CatiaAc {
		yerel.CatiaAc = false
		degisti = true
	}
	if !sunucuAcik(sunucuIzin, "usb") && yerel.UsbBaglanti {
		yerel.UsbBaglanti = false
		degisti = true
	}
	if !sunucuAcik(sunucuIzin, "dosyaYonetimi") && yerel.Acik("dosyaYonetimi") {
		yerel.DosyaYonetimiBitis = 0
		degisti = true
	}
	if degisti {
		if err := izinleriKaydet(yerel); err != nil {
			return err
		}
		olayLog("web'den kapatılan izinler yerelde uygulandı")
		izinDurumGonder()
	}

	// 2) Bekleyen acma istegi varsa YEREL onay iste.
	bekleyen, _ := sonuc["bekleyen"].(map[string]any)
	if bekleyen == nil {
		return nil
	}
	anahtar, _ := bekleyen["anahtar"].(string)
	etiket, ok := izinEtiketleri[anahtar]
	if !ok {
		_, _ = fikoytCagri("karar", map[string]any{"cihazKimlik": kimlik, "anahtar": anahtar, "izin": false})
		return nil
	}
	if izinOku().Acik(anahtar) {
		// Zaten acik; istegi kapat.
		_, err := fikoytCagri("karar", map[string]any{"cihazKimlik": kimlik, "anahtar": anahtar, "izin": true})
		return err
	}

	onay, sure, err := izinOnayla(anahtar, etiket.baslik, etiket.aciklama)
	if errors.Is(err, errMesgul) {
		return nil // baska bir onay acik; sonraki turda tekrar denenir
	}
	if err != nil {
		return err
	}
	if onay {
		onay, err = izinKaydetOnay(anahtar, sure)
		if err != nil {
			return err
		}
	}
	_, err = fikoytCagri("karar", map[string]any{"cihazKimlik": kimlik, "anahtar": anahtar, "izin": onay})
	return err
}

func sunucuAcik(m map[string]any, anahtar string) bool {
	b, _ := m[anahtar].(bool)
	return b
}

// senkronTetik: socket'ten gelen degisiklik bildirimi icin (tampon 1; fazlasi birlesir).
var senkronTetik = make(chan struct{}, 1)

func senkronTetikle() {
	select {
	case senkronTetik <- struct{}{}:
	default:
	}
}

// cihazGizliOku: cihaz gizli anahtari (yoksa uretir). Sadece bu bilgisayarda saklanir;
// sunucuya yalnizca SHA-256 ozeti gonderilir, socket dogrulamasi icin gizli anahtar kullanilir.
func cihazGizliOku() (string, error) {
	if err := cihazKimlikleriBuBilgisayara(); err != nil {
		return "", err
	}
	klasor, err := veriKlasoru()
	if err != nil {
		return "", err
	}
	yol := filepath.Join(klasor, "cihaz-gizli")
	if veri, err := os.ReadFile(yol); err == nil && len(veri) >= 32 {
		return string(veri), nil
	}
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	gizli := hex.EncodeToString(b)
	return gizli, os.WriteFile(yol, []byte(gizli), 0o600)
}

func cihazGizliOzet() (string, error) {
	gizli, err := cihazGizliOku()
	if err != nil {
		return "", err
	}
	ozet := sha256.Sum256([]byte(gizli))
	return hex.EncodeToString(ozet[:]), nil
}
