package main

// Dosya izinleri (faz 1): klasor bazli izin ve tam erisim. Izinler yalnizca bu bilgisayarda,
// ayar sayfasindan ya da onay ekranindan verilir. Yasak yollar her durumda kapali kalir.
// Eklenti (dosya-yoneticisi) islem yapmadan once bu kontrolden gecer; kontrol bridge'de yapilir.

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

var dosyaIslemleri = map[string]bool{"oku": true, "yaz": true, "kopyala": true, "sil": true}

var dosyaIslemAdlari = map[string]string{"oku": "okuma", "yaz": "yazma", "kopyala": "kopyalama", "sil": "silme"}

// DosyaKlasorIzni: bir klasor ve o klasorde izinli islemler. Bitis: unix saniyesi.
type DosyaKlasorIzni struct {
	Kok      string   `json:"kok"`
	Islemler []string `json:"islemler"`
	Bitis    int64    `json:"bitis"`
}

// DosyaIzinleri: klasor izinleri ve tam erisim (TamErisim: unix bitis; 0 = kapali).
type DosyaIzinleri struct {
	Klasorler []DosyaKlasorIzni `json:"klasorler"`
	TamErisim int64             `json:"tamErisim"`
}

var (
	dosyaIzinMu sync.RWMutex
	dosyaIzin   DosyaIzinleri
)

func dosyaIzinDosyasi() (string, error) {
	klasor, err := veriKlasoru()
	if err != nil {
		return "", err
	}
	return filepath.Join(klasor, "bridge-dosya-izin.json"), nil
}

// dosyaIzinleriYukle: kayitli izinleri okur. Dosya yoksa hepsi kapali kabul edilir.
func dosyaIzinleriYukle() error {
	yol, err := dosyaIzinDosyasi()
	if err != nil {
		return err
	}
	veri, err := os.ReadFile(yol)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err != nil {
		return err
	}
	var y DosyaIzinleri
	if err := json.Unmarshal(veri, &y); err != nil {
		return err
	}
	dosyaIzinMu.Lock()
	dosyaIzin = y
	dosyaIzinMu.Unlock()
	return nil
}

func dosyaIzinleriKaydet(y DosyaIzinleri) error {
	yol, err := dosyaIzinDosyasi()
	if err != nil {
		return err
	}
	veri, err := json.MarshalIndent(y, "", "  ")
	if err != nil {
		return err
	}
	if err := os.WriteFile(yol, veri, 0o600); err != nil {
		return err
	}
	dosyaIzinMu.Lock()
	dosyaIzin = y
	dosyaIzinMu.Unlock()
	return nil
}

func dosyaIzinOku() DosyaIzinleri {
	dosyaIzinMu.RLock()
	defer dosyaIzinMu.RUnlock()
	return dosyaIzin
}

// sureBitisi: secilen sureye gore bitis zamani. "bu-sefer" kaydedilmez (ok=false).
func sureBitisi(sure string, simdi time.Time) (int64, bool) {
	switch sure {
	case sureSaat:
		return simdi.Add(time.Hour).Unix(), true
	case sureGun:
		return simdi.Add(24 * time.Hour).Unix(), true
	case sureKalici:
		return simdi.Add(kaliciSure).Unix(), true
	}
	return 0, false
}

// secimBitisi: onay ekranindan gelen sureyi bitis zamanina cevirir. "ozel:<unix>" kullanicinin sectigi tarihtir.
func secimBitisi(sure string, simdi time.Time) (int64, bool) {
	if strings.HasPrefix(sure, "ozel:") {
		v, err := strconv.ParseInt(strings.TrimPrefix(sure, "ozel:"), 10, 64)
		if err != nil {
			return 0, false
		}
		return ozelBitis("ozel", v, simdi)
	}
	return sureBitisi(sure, simdi)
}

// normYol: mutlak, temiz ve sembolik baglantilari cozulmus yol. Dosya yoksa ust klasor cozulur.
func normYol(yol string) (string, error) {
	mutlak, err := filepath.Abs(filepath.Clean(yol))
	if err != nil {
		return "", err
	}
	if gercek, err := filepath.EvalSymlinks(mutlak); err == nil {
		return filepath.Clean(gercek), nil
	}
	gercekUst, err := filepath.EvalSymlinks(filepath.Dir(mutlak))
	if err != nil {
		return "", err
	}
	return filepath.Join(gercekUst, filepath.Base(mutlak)), nil
}

// altYolMu: yol, kok ile ayni ya da kok'un altindaysa true (buyuk-kucuk harf duyarsiz).
func altYolMu(yol, kok string) bool {
	ayirici := string(filepath.Separator)
	y := strings.ToLower(strings.TrimRight(yol, ayirici))
	k := strings.ToLower(strings.TrimRight(kok, ayirici))
	return y == k || strings.HasPrefix(y, k+ayirici)
}

// yasakYollar: her durumda erisime kapali klasorler (sistem, program klasorleri, bridge'in kendi klasorleri).
func yasakYollar() []string {
	liste := []string{}
	for _, k := range []string{"SystemRoot", "ProgramFiles", "ProgramFiles(x86)", "ProgramW6432"} {
		if v := os.Getenv(k); v != "" {
			liste = append(liste, v)
		}
	}
	if v, err := veriKlasoru(); err == nil {
		liste = append(liste, v)
	}
	if exe, err := os.Executable(); err == nil {
		liste = append(liste, filepath.Dir(exe))
	}
	return liste
}

func yasakMi(yol string, yasak []string) bool {
	for _, y := range yasak {
		if altYolMu(yol, y) {
			return true
		}
	}
	return false
}

// dosyaIzinliMi: izin durumuna gore islem yapilabilir mi. Yol normalize edilmis olmali.
func dosyaIzinliMi(y DosyaIzinleri, yol, islem string, simdi time.Time, yasak []string) bool {
	if !dosyaIslemleri[islem] || yasakMi(yol, yasak) {
		return false
	}
	if y.TamErisim > simdi.Unix() {
		return true
	}
	for _, k := range y.Klasorler {
		if k.Bitis <= simdi.Unix() || !altYolMu(yol, k.Kok) {
			continue
		}
		for _, i := range k.Islemler {
			if i == islem {
				return true
			}
		}
	}
	return false
}

// dosyaIslemIzinliMiGercek: yolu normalize edip mevcut izinlere gore kontrol eder.
func dosyaIslemIzinliMiGercek(yol, islem string) bool {
	n, err := normYol(yol)
	if err != nil {
		return false
	}
	return dosyaIzinliMi(dosyaIzinOku(), n, islem, time.Now(), yasakYollar())
}

// klasorIzniEkle: ayni klasor varsa islemleri birlestirir ve bitisi uzatir.
func klasorIzniEkle(liste []DosyaKlasorIzni, kok string, islemler []string, bitis int64) []DosyaKlasorIzni {
	for i, k := range liste {
		if strings.EqualFold(k.Kok, kok) {
			for _, yeni := range islemler {
				var var_ bool
				for _, mevcut := range k.Islemler {
					if mevcut == yeni {
						var_ = true
					}
				}
				if !var_ {
					liste[i].Islemler = append(liste[i].Islemler, yeni)
				}
			}
			if bitis > liste[i].Bitis {
				liste[i].Bitis = bitis
			}
			return liste
		}
	}
	return append(liste, DosyaKlasorIzni{Kok: kok, Islemler: islemler, Bitis: bitis})
}

func klasorIzniKaldir(liste []DosyaKlasorIzni, kok string) []DosyaKlasorIzni {
	kalan := []DosyaKlasorIzni{}
	for _, k := range liste {
		if !strings.EqualFold(k.Kok, kok) {
			kalan = append(kalan, k)
		}
	}
	return kalan
}

// dosyaIslemIzniIste: izin yoksa kullanicidan sorar. Secilen sure klasor izni olarak kaydedilir;
// "bu sefer" yalnizca bu islem icindir. Donus: izin verildi mi.
func dosyaIslemIzniIste(site, yol, islem string) (bool, error) {
	if !dosyaIslemleri[islem] {
		return false, errors.New("bilinmeyen dosya islemi")
	}
	n, err := normYol(yol)
	if err != nil {
		return false, err
	}
	yasak := yasakYollar()
	if yasakMi(n, yasak) {
		return false, errors.New("bu klasor koruma altinda")
	}
	if dosyaIzinliMi(dosyaIzinOku(), n, islem, time.Now(), yasak) {
		return true, nil
	}
	kok := filepath.Dir(n)
	if bilgi, err := os.Stat(n); err == nil && bilgi.IsDir() {
		kok = n
	}
	izin, sure, err := onayBekleSecenekli(onayBilgi{
		baslik:   "Dosya işlemine izin verilsin mi?",
		aciklama: "Bir web sitesi bu bilgisayarda bir dosya üzerinde " + dosyaIslemAdlari[islem] + " işlemi yapmak istiyor.",
		ayrinti: []string{
			"Yol: " + n,
			"İşlem: " + dosyaIslemAdlari[islem],
		},
		kapsam: "1 saat ya da her zaman seçilirse bu klasör için verilir ve Ayarlar'dan geri alınabilir. Bu sefer seçilirse yalnızca bu işlem için izin verilir.",
	})
	if err != nil || !izin {
		return false, err
	}
	if strings.HasPrefix(sure, "ozel:") {
		if _, ok := secimBitisi(sure, time.Now()); !ok {
			return false, errors.New("gecersiz bitis tarihi")
		}
	}
	if bitis, kaydet := secimBitisi(sure, time.Now()); kaydet {
		y := dosyaIzinOku()
		y.Klasorler = klasorIzniEkle(y.Klasorler, kok, []string{islem}, bitis)
		if err := dosyaIzinleriKaydet(y); err != nil {
			olayLog("dosya izni kaydedilemedi: %v", err)
		}
	}
	olayLog("dosya izni verildi: %s %s (%s)", site, dosyaIslemAdlari[islem], sure)
	return true, nil
}

type dosyaIzinIstegi struct {
	Islem    string   `json:"islem"`
	Kok      string   `json:"kok"`
	Islemler []string `json:"islemler"`
	Sure     string   `json:"sure"`
	Acik     bool     `json:"acik"`
	Onay     bool     `json:"onay"`
	Bitis    int64    `json:"bitis"`
}

// ozelBitis: "ozel" surede kullanicinin sectigi bitis (simdiden sonra, en fazla 1 yil); digerleri sureBitisi ile.
func ozelBitis(sure string, bitis int64, simdi time.Time) (int64, bool) {
	if sure == "ozel" {
		if bitis <= simdi.Unix() || bitis > simdi.Add(365*24*time.Hour).Unix() {
			return 0, false
		}
		return bitis, true
	}
	return sureBitisi(sure, simdi)
}

// dosyaIzinHandler: GET izinleri okur; POST (yerel, CSRF korumali) izni degistirir.
func dosyaIzinHandler(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		yazJSON(w, http.StatusOK, dosyaIzinOku())
	case http.MethodPost:
		if !yerelIstek(w, r) || !csrfDogrula(w, r) {
			return
		}
		var istek dosyaIzinIstegi
		if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&istek); err != nil {
			http.Error(w, "gecersiz istek", http.StatusBadRequest)
			return
		}
		simdi := time.Now()
		y := dosyaIzinOku()
		switch istek.Islem {
		case "ekle":
			n, err := normYol(istek.Kok)
			if err != nil {
				http.Error(w, "gecersiz klasor", http.StatusBadRequest)
				return
			}
			bilgi, err := os.Stat(n)
			if err != nil || !bilgi.IsDir() {
				http.Error(w, "klasor bulunamadi", http.StatusBadRequest)
				return
			}
			if yasakMi(n, yasakYollar()) {
				http.Error(w, "bu klasor koruma altinda", http.StatusBadRequest)
				return
			}
			if len(istek.Islemler) == 0 {
				http.Error(w, "en az bir islem secin", http.StatusBadRequest)
				return
			}
			for _, i := range istek.Islemler {
				if !dosyaIslemleri[i] {
					http.Error(w, "bilinmeyen islem", http.StatusBadRequest)
					return
				}
			}
			bitis, ok := ozelBitis(istek.Sure, istek.Bitis, simdi)
			if !ok {
				http.Error(w, "sure secin", http.StatusBadRequest)
				return
			}
			y.Klasorler = klasorIzniEkle(y.Klasorler, n, istek.Islemler, bitis)
		case "kaldir":
			n, err := normYol(istek.Kok)
			if err != nil {
				http.Error(w, "gecersiz klasor", http.StatusBadRequest)
				return
			}
			y.Klasorler = klasorIzniKaldir(y.Klasorler, n)
		case "tam-erisim":
			if !istek.Acik {
				y.TamErisim = 0
				break
			}
			if !istek.Onay {
				http.Error(w, "tam erisim icin onay gerekli", http.StatusBadRequest)
				return
			}
			bitis, ok := sureBitisi(istek.Sure, simdi)
			if !ok {
				http.Error(w, "sure secin", http.StatusBadRequest)
				return
			}
			y.TamErisim = bitis
		default:
			http.Error(w, "bilinmeyen islem", http.StatusBadRequest)
			return
		}
		if err := dosyaIzinleriKaydet(y); err != nil {
			http.Error(w, "kaydedilemedi", http.StatusInternalServerError)
			return
		}
		olayLog("dosya izni guncellendi: %s", istek.Islem)
		yazJSON(w, http.StatusOK, dosyaIzinOku())
	default:
		http.Error(w, "yalnizca GET veya POST", http.StatusMethodNotAllowed)
	}
}
