package main

import (
	"context"
	"encoding/json"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/go-chi/httprate"
)

func main() {
	if err := initDB(envOr("KVER_DB", "kver.db")); err != nil {
		log.Fatalf("db init: %v", err)
	}
	defer db.Close()

	srv := &http.Server{
		Addr:    envOr("KVER_ADDR", ":8080"),
		Handler: routes(),
		// Timeouts gegen festhängende/langsame Verbindungen (Slowloris). Read
		// und Write großzügig genug für 16-MB-Uploads über langsame Leitungen.
		ReadHeaderTimeout: 10 * time.Second,
		ReadTimeout:       2 * time.Minute,
		WriteTimeout:      2 * time.Minute,
		IdleTimeout:       2 * time.Minute,
	}

	// Graceful Shutdown: auf SIGINT/SIGTERM laufende Requests zu Ende bedienen,
	// dann die DB sauber schließen (WAL-Checkpoint) statt hart abzubrechen.
	idle := make(chan struct{})
	go func() {
		sig := make(chan os.Signal, 1)
		signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
		<-sig
		ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
		defer cancel()
		if err := srv.Shutdown(ctx); err != nil {
			log.Printf("shutdown: %v", err)
		}
		close(idle)
	}()

	log.Printf("kver listening on %s", srv.Addr)
	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		log.Fatalf("listen: %v", err)
	}
	<-idle
}

// envOr liest eine Umgebungsvariable mit Fallback auf einen Default.
func envOr(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

// routes baut den HTTP-Handler mit allen Endpunkten. In main() und in den
// Tests identisch verwendet.
func routes() http.Handler {
	r := chi.NewRouter()
	// RealIP übernimmt X-Forwarded-For/X-Real-IP vom Reverse-Proxy, damit
	// Logging und das IP-Rate-Limit unten den Client statt den Proxy sehen.
	// Voraussetzung: die App ist nur über den Proxy erreichbar (Deployment
	// bindet an 127.0.0.1, siehe notes/deploy.md) — sonst wären die Header
	// spoofbar.
	r.Use(middleware.RealIP)
	r.Use(middleware.Logger)
	r.Use(middleware.Recoverer)
	r.Use(secHeaders)

	// --- Auth (öffentlich) ---
	// Login und Registrierung pro IP drosseln (Brute-Force-Bremse). bcrypt bremst
	// zusätzlich, aber das Limit kappt automatisiertes Durchprobieren früh.
	r.Group(func(r chi.Router) {
		r.Use(httprate.LimitByIP(20, time.Minute))
		r.Post("/auth/login", handleLogin)
		r.Post("/auth/newuser", handleNewUser)
	})
	r.Get("/auth/logout", handleLogout)
	r.Get("/auth/sessioninfo", handleSessionInfo)
	r.Get("/auth/headerbar", handleHeaderbar)

	// --- Entries ---
	r.Get("/entry/feed/{page}", handleFeed)
	r.Get("/entry/{pid}/votes", handleVotes)
	r.Get("/entry/{pid}/thread", handleThread)
	r.Get("/e/{pid}", serveEntryPage)

	// --- User (öffentlich) ---
	r.Get("/u/{username}", serveUserPage)
	r.Get("/u/{username}/info", handleUserPage)
	r.Get("/u/{username}/feed/{page}", handleUserFeed)

	// --- Geschützt (Session erforderlich) ---
	r.Group(func(r chi.Router) {
		r.Use(requireAuth)
		r.Post("/entry/create", handleCreateEntry)
		r.Post("/entry/{pid}/vote", handleVote)
		r.Post("/entry/{pid}/edit", handleEditEntry)
		r.Post("/entry/{pid}/delete", handleDeleteEntry)
		r.Get("/user/info", handleUserInfo)
		r.Post("/user/rename", handleUserRename)
		r.Post("/user/avatar", handleSetAvatar)
		r.Post("/user/delete", handleUserDelete)
	})

	// Hochgeladene Medien + alte statische Assets. nosniff (global via
	// secHeaders) verhindert, dass der Browser eine durchgereichte (nicht
	// re-kodierte) Datei als HTML/JS interpretiert -> schließt
	// Polyglot/Stored-XSS über Bild-Uploads.
	r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))

	// Neues JS-Frontend
	r.Handle("/*", http.FileServer(http.Dir("web")))

	return r
}

// secHeaders setzt Standard-Security-Header auf alle Antworten:
//   - nosniff: Browser darf den deklarierten Content-Type nicht überstimmen
//     (wichtig für durchgereichte GIF-Uploads, siehe storeImage).
//   - Frame-Verbote gegen Clickjacking.
//   - CSP als Defense-in-depth gegen XSS: das Frontend hat kein Inline-JS/CSS,
//     daher reicht 'self' ohne unsafe-inline.
func secHeaders(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		h := w.Header()
		h.Set("X-Content-Type-Options", "nosniff")
		h.Set("X-Frame-Options", "DENY")
		h.Set("Referrer-Policy", "same-origin")
		h.Set("Content-Security-Policy",
			"default-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'")
		next.ServeHTTP(w, r)
	})
}

func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}

func writeError(w http.ResponseWriter, status int, msg string) {
	writeJSON(w, status, map[string]string{"error": msg})
}
