package main

// Sunucu yöneticisi: proxy'yi başlatır, ayar değişince yeniden kurar ve
// ayar sayfasını (/ayar) token korumasıyla sunar.

import (
	"crypto/rand"
	_ "embed"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"
)

//go:embed ayar.html
var ayarHTML []byte

type sunucu struct {
	mu    sync.Mutex
	srv   *http.Server
	cfg   Config
	token string
}

var proxySunucu = &sunucu{token: yeniToken()}

func yeniToken() string {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return ""
	}
	return hex.EncodeToString(b)
}

// ayarURL: ayar sayfasının tam adresi (token dahil). Yalnızca yerelde kullanılır.
func (s *sunucu) ayarURL() string {
	s.mu.Lock()
	defer s.mu.Unlock()
	return "http://" + s.cfg.Dinle + "/ayar?t=" + s.token
}

// calistirLocked: dinleyiciyi verilen ayarlarla servis eder. s.mu tutulmalı.
func (s *sunucu) calistirLocked(ln net.Listener, cfg Config) {
	srv := &http.Server{Handler: s.mux(cfg), ReadHeaderTimeout: 10 * time.Second}
	s.srv = srv
	s.cfg = cfg
	go func() {
		if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Printf("sunucu durdu: %v", err)
		}
	}()
}

// ilkBaslat: uygulama açılırken sunucuyu başlatır.
func (s *sunucu) ilkBaslat(cfg Config) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	ln, err := net.Listen("tcp", cfg.Dinle)
	if err != nil {
		s.cfg = cfg
		return err
	}
	s.calistirLocked(ln, cfg)
	return nil
}

// uygula: yeni ayarlarla sunucuyu yeniden kurar. Yeni port açılamazsa
// eski ayarlarla geri döner ve hata döner.
// Not: Bu çağrı, ayar isteğini yanıtlayan sunucuyu da kapatabilir; bu yüzden
// ayarApi bunu yanıt gönderildikten sonra, ayrı bir goroutine'de çağırır.
func (s *sunucu) uygula(yeni Config) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	eski := s.cfg
	if s.srv != nil {
		_ = s.srv.Close()
		s.srv = nil
	}
	ln, err := net.Listen("tcp", yeni.Dinle)
	if err != nil {
		if ln2, err2 := net.Listen("tcp", eski.Dinle); err2 == nil {
			s.calistirLocked(ln2, eski)
		}
		return fmt.Errorf("port %s açılamadı (başka bir program kullanıyor olabilir): %v", yeni.Dinle, err)
	}
	s.calistirLocked(ln, yeni)
	return nil
}

// yetkiliIstek: DNS yeniden bağlama saldırısına karşı Host kontrolü.
func (s *sunucu) yetkiliIstek(r *http.Request) bool {
	s.mu.Lock()
	cfg := s.cfg
	s.mu.Unlock()
	_, port, err := net.SplitHostPort(cfg.Dinle)
	if err != nil {
		return false
	}
	return r.Host == cfg.Dinle || r.Host == "localhost:"+port
}

// mux: proxy uçları + ayar uçları. Ayar uçlarında CORS başlığı yoktur.
func (s *sunucu) mux(cfg Config) http.Handler {
	kokler := cfg.Kokler
	if len(kokler) == 0 {
		kokler = surucuKokleri()
	}
	hedef, err := url.Parse(cfg.Everything)
	if err != nil {
		hedef = &url.URL{Scheme: "http", Host: "127.0.0.1:8888"}
	}
	istemci := &http.Client{Timeout: 10 * time.Second}

	m := http.NewServeMux()
	m.HandleFunc("/ayar", s.ayarSayfa)
	m.HandleFunc("/ayar/api/", s.ayarApi)
	m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if origin := r.Header.Get("Origin"); origin != "" && origin == cfg.IzinliKoken {
			w.Header().Set("Access-Control-Allow-Origin", origin)
			w.Header().Set("Vary", "Origin")
			w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization, X-Auth")
			w.Header().Set("Access-Control-Allow-Private-Network", "true")
		}

		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusNoContent)
			return
		}
		if strings.HasPrefix(r.URL.Path, "/usb") {
			if usbIslemi(w, r) {
				return
			}
			http.Error(w, "bilinmeyen uç", http.StatusNotFound)
			return
		}

		if r.Method != http.MethodGet {
			http.Error(w, "yalnızca GET", http.StatusMethodNotAllowed)
			return
		}

		if r.URL.Path == "/dosya" {
			dosyaSun(w, r, kokler)
			return
		}

		girdi := r.URL.Query()
		cikti := url.Values{}
		for anahtar, degerler := range girdi {
			if izinliParametreler[strings.ToLower(anahtar)] {
				cikti[anahtar] = degerler
			}
		}
		if cikti.Get("search") == "" {
			http.Error(w, "search parametresi gerekli", http.StatusBadRequest)
			return
		}

		istek := *hedef
		istek.RawQuery = cikti.Encode()
		yanit, err := istemci.Get(istek.String())
		if err != nil {
			log.Printf("everything'e ulaşılamadı: %v", err)
			http.Error(w, "Everything HTTP sunucusuna ulaşılamadı. Everything açık ve HTTP Server etkin mi?", http.StatusBadGateway)
			return
		}
		defer yanit.Body.Close()

		w.Header().Set("Content-Type", "application/json; charset=utf-8")
		w.Header().Set("Cache-Control", "no-store")
		w.WriteHeader(yanit.StatusCode)
		io.Copy(w, yanit.Body)
	})
	return m
}

func (s *sunucu) ayarSayfa(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "yalnızca GET", http.StatusMethodNotAllowed)
		return
	}
	if !s.yetkiliIstek(r) || r.URL.Query().Get("t") != s.token {
		http.Error(w, "yetkisiz", http.StatusForbidden)
		return
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Header().Set("Cache-Control", "no-store")
	w.Header().Set("Content-Security-Policy", "default-src 'self' 'unsafe-inline'")
	w.Write(ayarHTML)
}

func (s *sunucu) ayarApi(w http.ResponseWriter, r *http.Request) {
	if !s.yetkiliIstek(r) || r.Header.Get("X-Ayar-Token") != s.token {
		http.Error(w, "yetkisiz", http.StatusForbidden)
		return
	}
	switch r.URL.Path {
	case "/ayar/api/config":
		if r.Method != http.MethodGet {
			http.Error(w, "yalnızca GET", http.StatusMethodNotAllowed)
			return
		}
		s.mu.Lock()
		cfg := s.cfg
		s.mu.Unlock()
		yazJSON(w, http.StatusOK, cfg)

	case "/ayar/api/kaydet":
		if r.Method != http.MethodPost {
			http.Error(w, "yalnızca POST", http.StatusMethodNotAllowed)
			return
		}
		var yeni Config
		if err := json.NewDecoder(io.LimitReader(r.Body, 64<<10)).Decode(&yeni); err != nil {
			yazHata(w, http.StatusBadRequest, "geçersiz istek")
			return
		}
		if err := yeni.dogrula(); err != nil {
			yazHata(w, http.StatusBadRequest, err.Error())
			return
		}

		// Yeni port farklıysa, sunucuyu kapatmadan önce boş mu diye bak.
		s.mu.Lock()
		eskiDinle := s.cfg.Dinle
		s.mu.Unlock()
		portDegisti := yeni.Dinle != eskiDinle
		if portDegisti {
			ln, err := net.Listen("tcp", yeni.Dinle)
			if err != nil {
				yazHata(w, http.StatusConflict, fmt.Sprintf("port %s açılamadı (başka bir program kullanıyor olabilir)", yeni.Dinle))
				return
			}
			ln.Close()
		}

		if err := configYaz(yeni); err != nil {
			log.Printf("ayar dosyaya yazılamadı: %v", err)
		}
		if err := otomatikBaslatAyarla(yeni.WindowsAcilis); err != nil {
			log.Printf("Windows açılış ayarı yapılamadı: %v", err)
		}
		trayDurumAyarla("Çalışıyor: " + yeni.Dinle)

		yanit := map[string]any{"ok": true}
		if portDegisti {
			yanit["yeniAdres"] = "http://" + yeni.Dinle + "/ayar?t=" + s.token
		}
		yazJSON(w, http.StatusOK, yanit)

		// Yanıt tarayıcıya gitsin diye sunucuyu biraz sonra yeniden kur.
		go func() {
			time.Sleep(300 * time.Millisecond)
			if err := s.uygula(yeni); err != nil {
				log.Printf("sunucu yeniden kurulamadı: %v", err)
				trayDurumAyarla("Başlatılamadı: " + yeni.Dinle)
			}
		}()

	default:
		http.NotFound(w, r)
	}
}

func yazJSON(w http.ResponseWriter, kod int, v any) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.Header().Set("Cache-Control", "no-store")
	w.WriteHeader(kod)
	json.NewEncoder(w).Encode(v)
}

func yazHata(w http.ResponseWriter, kod int, mesaj string) {
	yazJSON(w, kod, map[string]string{"hata": mesaj})
}
