package main

import (
	"encoding/json"
	"html"
	"io"
	"log"
	"math/rand"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/redis/go-redis/v9"
)

func handleEditorPage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "./secure/editor.html")
}

func handleObstacleEditorPage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "./secure/obstacle_editor.html")
}

func handleAdminChunks(w http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodPost {
		var chunk ChunkDef
		if err := json.NewDecoder(r.Body).Decode(&chunk); err != nil {
			http.Error(w, "Bad JSON", 400)
			return
		}

		chunkJson, _ := json.Marshal(chunk)
		rdb.SAdd(ctx, "config:chunks:list", chunk.ID)
		rdb.Set(ctx, "config:chunks:data:"+chunk.ID, chunkJson, 0)

		w.WriteHeader(http.StatusOK)
	}

}

func handleConfig(w http.ResponseWriter, r *http.Request) {
	conf := defaultConfig

	chunkIDs, _ := rdb.SMembers(ctx, "config:chunks:list").Result()

	sort.Strings(chunkIDs)

	var loadedChunks []ChunkDef

	// 3. Details laden
	for _, id := range chunkIDs {
		val, err := rdb.Get(ctx, "config:chunks:data:"+id).Result()
		if err == nil {
			var c ChunkDef
			json.Unmarshal([]byte(val), &c)
			c.ID = id // Sicherstellen, dass ID gesetzt ist
			loadedChunks = append(loadedChunks, c)
		}
	}
	conf.Chunks = loadedChunks

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(conf)
}

func handleStart(w http.ResponseWriter, r *http.Request) {
	sessionID := uuid.New().String()
	rawSeed := time.Now().UnixNano()
	seed32 := uint32(rawSeed)

	emptyObs, _ := json.Marshal([]ActiveObstacle{})

	err := rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{
		"seed":      seed32,
		"rng_state": seed32,
		"score":     0,
		"is_dead":   0,
		"pos_y":     PlayerYBase,
		"vel_y":     0.0,
		"obstacles": string(emptyObs),
	}).Err()

	if err != nil {
		http.Error(w, "DB Error", 500)
		return
	}
	rdb.Expire(ctx, "session:"+sessionID, 4000*time.Hour)

	json.NewEncoder(w).Encode(StartResponse{SessionID: sessionID, Seed: seed32})
}

func handleValidate(w http.ResponseWriter, r *http.Request) {
	var req ValidateRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Bad Request", 400)
		return
	}

	key := "session:" + req.SessionID
	vals, err := rdb.HGetAll(ctx, key).Result()
	if err != nil || len(vals) == 0 {
		http.Error(w, "Session invalid", 401)
		return
	}

	if vals["is_dead"] == "1" {
		json.NewEncoder(w).Encode(ValidateResponse{Status: "dead", VerifiedScore: 0})
		return
	}

	isDead, score, obstacles, platforms, powerUpState, serverTick, nextSpawnTick, finalRngState := simulateChunk(req.SessionID, req.Inputs, req.TotalTicks, vals)
	status := "alive"
	if isDead {
		status = "dead"
		rdb.HSet(ctx, key, "is_dead", 1)
	}
	rdb.Expire(ctx, key, 1*time.Hour)

	json.NewEncoder(w).Encode(ValidateResponse{
		Status:        status,
		VerifiedScore: score,
		ServerObs:     obstacles,
		ServerPlats:   platforms,
		PowerUps:      powerUpState,
		ServerTick:    serverTick,
		NextSpawnTick: nextSpawnTick,
		RngState:      finalRngState,
	})
}

func handleSubmitName(w http.ResponseWriter, r *http.Request) {
	var req SubmitNameRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Bad Request", 400)
		return
	}

	if len(req.Name) > 4 {
		http.Error(w, "Zu lang", 400)
		return
	}
	if containsBadWord(req.Name) {
		http.Error(w, "Name verboten", 400)
		return
	}

	safeName := html.EscapeString(req.Name)
	sessionKey := "session:" + req.SessionID

	scoreVal, err := rdb.HGet(ctx, sessionKey, "score").Result()
	if err != nil {
		http.Error(w, "Session expired", 404)
		return
	}
	scoreInt, _ := strconv.Atoi(scoreVal)

	claimCode := generateClaimCode()
	timestamp := time.Now().Format("02.01.2006 15:04")

	rdb.HSet(ctx, sessionKey, map[string]interface{}{
		"name":       safeName,
		"claim_code": claimCode,
		"created_at": timestamp,
	})

	// Leaderboard Eintrag
	rdb.ZAdd(ctx, "leaderboard:unverified", redis.Z{Score: float64(scoreInt), Member: req.SessionID})
	rdb.ZAdd(ctx, "leaderboard:public", redis.Z{Score: float64(scoreInt), Member: req.SessionID})

	rdb.Persist(ctx, sessionKey)

	rdb.HDel(ctx, sessionKey, "obstacles", "rng_state", "pos_y", "vel_y", "p_god_lives", "p_has_bat", "p_boot_ticks")

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(SubmitResponse{ClaimCode: claimCode})
}

func handleLeaderboard(w http.ResponseWriter, r *http.Request) {
	mySessionID := r.URL.Query().Get("sessionId")
	targetKey := "leaderboard:public"

	var entries []LeaderboardEntry
	top3, _ := rdb.ZRevRangeWithScores(ctx, targetKey, 0, 2).Result()

	for i, z := range top3 {
		rank := int64(i + 1)
		sid := z.Member.(string)
		name, _ := rdb.HGet(ctx, "session:"+sid, "name").Result()
		if name == "" {
			name = "Unbekannt"
		}
		entries = append(entries, LeaderboardEntry{
			Rank: rank, Name: name, Score: int(z.Score), IsMe: (sid == mySessionID),
		})
	}

	if mySessionID != "" {
		myRank, err := rdb.ZRevRank(ctx, targetKey, mySessionID).Result()
		if err == nil && myRank > 2 {
			start := myRank - 1
			stop := myRank + 1
			neighbors, _ := rdb.ZRevRangeWithScores(ctx, targetKey, start, stop).Result()
			for i, z := range neighbors {
				rank := start + int64(i) + 1
				sid := z.Member.(string)
				name, _ := rdb.HGet(ctx, "session:"+sid, "name").Result()
				if name == "" {
					name = "Unbekannt"
				}
				entries = append(entries, LeaderboardEntry{
					Rank: rank, Name: name, Score: int(z.Score), IsMe: (sid == mySessionID),
				})
			}
		}
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(entries)
}

func handleAdminPage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "./secure/admin.html")
}

func handleAdminList(w http.ResponseWriter, r *http.Request) {
	listType := r.URL.Query().Get("type")
	redisKey := "leaderboard:unverified"
	if listType == "public" {
		redisKey = "leaderboard:public"
	}

	vals, _ := rdb.ZRevRangeWithScores(ctx, redisKey, 0, -1).Result()
	var adminList []AdminEntry

	for _, z := range vals {
		sid := z.Member.(string)
		info, _ := rdb.HGetAll(ctx, "session:"+sid).Result()
		name := info["name"]
		if name == "" {
			name = "Unbekannt"
		}
		adminList = append(adminList, AdminEntry{
			SessionID: sid, Name: name, Score: int(z.Score),
			Code: info["claim_code"], Time: info["created_at"],
		})
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(adminList)
}

func handleAdminAction(w http.ResponseWriter, r *http.Request) {
	var req AdminActionRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Bad Request", 400)
		return
	}

	log.Printf("👮 ADMIN ACTION: %s on %s", req.Action, req.SessionID)

	if req.Action == "approve" {
		score, err := rdb.ZScore(ctx, "leaderboard:unverified", req.SessionID).Result()
		if err == nil {
			rdb.ZAdd(ctx, "leaderboard:public", redis.Z{Score: score, Member: req.SessionID})
			rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID)
		}
	} else if req.Action == "delete" {
		rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID)
		rdb.ZRem(ctx, "leaderboard:public", req.SessionID)

		rdb.Del(ctx, "session:"+req.SessionID)
	}

	w.WriteHeader(http.StatusOK)
}

func handleClaimDelete(w http.ResponseWriter, r *http.Request) {
	var req ClaimDeleteRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Bad Request", 400)
		return
	}

	sessionKey := "session:" + req.SessionID
	realCode, err := rdb.HGet(ctx, sessionKey, "claim_code").Result()

	if err != nil || realCode == "" {
		http.Error(w, "Not found", 404)
		return
	}

	if realCode != req.ClaimCode {
		http.Error(w, "Wrong Code", 403)
		return
	}

	log.Printf("🗑️ USER DELETE: Session %s deleted via code", req.SessionID)

	rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID)
	rdb.ZRem(ctx, "leaderboard:public", req.SessionID)

	rdb.Del(ctx, sessionKey)

	w.WriteHeader(http.StatusOK)
}

func generateClaimCode() string {
	const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
	b := make([]byte, 8)
	for i := range b {
		b[i] = charset[rand.Intn(len(charset))]
	}
	return string(b)
}

func handleAdminBadwords(w http.ResponseWriter, r *http.Request) {
	key := "config:badwords"

	if r.Method == http.MethodGet {
		words, _ := rdb.SMembers(ctx, key).Result()
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(words)
		return
	}

	if r.Method == http.MethodPost {

		type WordReq struct {
			Word   string `json:"word"`
			Action string `json:"action"`
		}
		var req WordReq
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			http.Error(w, "Bad Request", 400)
			return
		}

		if req.Action == "add" && req.Word != "" {
			rdb.SAdd(ctx, key, strings.ToLower(req.Word))
		} else if req.Action == "remove" && req.Word != "" {
			rdb.SRem(ctx, key, strings.ToLower(req.Word))
		}

		w.WriteHeader(http.StatusOK)
	}
}

func handleAdminUpload(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Nur POST erlaubt", 405)
		return
	}

	r.ParseMultipartForm(10 << 20)
	file, header, err := r.FormFile("file")
	if err != nil {
		http.Error(w, "Keine Datei gefunden", 400)
		return
	}
	defer file.Close()

	filename := filepath.Base(header.Filename)

	targetPath := filepath.Join("./static/assets", filename)

	// Datei erstellen
	dst, err := os.Create(targetPath)
	if err != nil {
		http.Error(w, "Konnte Datei nicht speichern (Rechte?)", 500)
		return
	}
	defer dst.Close()

	// Inhalt kopieren
	if _, err := io.Copy(dst, file); err != nil {
		http.Error(w, "Fehler beim Schreiben", 500)
		return
	}

	log.Printf("📂 UPLOAD: %s erfolgreich gespeichert.", filename)

	// JSON Antwort mit dem Pfad zurückgeben
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{
		"status":   "ok",
		"filename": filename,
	})
}

// 2. CONFIG SPEICHERN (Redis)
func handleAdminSaveConfig(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Nur POST", 405)
		return
	}

	var newConfig GameConfig
	if err := json.NewDecoder(r.Body).Decode(&newConfig); err != nil {
		http.Error(w, "Bad JSON", 400)
		return
	}

	data, _ := json.Marshal(newConfig)
	rdb.Set(ctx, "config:gamedata", data, 0)

	defaultConfig.Obstacles = newConfig.Obstacles
	defaultConfig.Backgrounds = newConfig.Backgrounds

	w.WriteHeader(http.StatusOK)
}
