package main

import (
	"log"
	"math"
	"net/http"
	"time"

	"github.com/gorilla/websocket"
)

const (
	ServerTickRate = 50 * time.Millisecond

	BufferAhead = 60
	SpawnXStart = 2000.0
)

var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool { return true },
}

// Protokoll
type WSInputMsg struct {
	Type  string  `json:"type"`
	Input string  `json:"input"`
	Tick  int     `json:"tick"`
	PosY  float64 `json:"y"`
	VelY  float64 `json:"vy"`
}

type WSServerMsg struct {
	Type         string           `json:"type"`
	Obstacles    []ActiveObstacle `json:"obstacles"`
	Platforms    []ActivePlatform `json:"platforms"`
	ServerTick   int              `json:"serverTick"`
	Score        int              `json:"score"`
	PowerUps     PowerUpState     `json:"powerups"`
	SessionID    string           `json:"sessionId"`
	Ts           int              `json:"ts,omitempty"`
	CurrentSpeed float64          `json:"currentSpeed"`
}

func handleWebSocket(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		return
	}
	defer conn.Close()

	// 1. Session Init
	sessionID := "ws-" + time.Now().Format("150405999")

	err = rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{
		"score":      0, // Startwert
		"is_dead":    0,
		"created_at": time.Now().Format("02.01.2006 15:04"),
	}).Err()

	if err != nil {
		log.Println("Redis Init Fehler:", err)
		return
	}

	rdb.Expire(ctx, "session:"+sessionID, 24*time.Hour)

	// Session ID senden
	conn.WriteJSON(WSServerMsg{Type: "init", SessionID: sessionID})

	state := SimState{
		SessionID:     sessionID,
		RNG:           NewRNG(time.Now().UnixNano()),
		Score:         0,
		Ticks:         0,
		PosY:          PlayerYBase,
		NextSpawnTick: 0,
		Chunks:        loadChunksFromRedis(),
	}

	// Channel größer machen, damit bei Lag nichts blockiert
	inputChan := make(chan WSInputMsg, 100)
	closeChan := make(chan struct{})

	// LESE-ROUTINE
	go func() {
		defer close(closeChan)
		for {
			var msg WSInputMsg
			if err := conn.ReadJSON(&msg); err != nil {
				return
			}
			inputChan <- msg
		}
	}()

	// GAME LOOP (High Performance)
	ticker := time.NewTicker(ServerTickRate)
	defer ticker.Stop()

	// Input State
	var pendingJump bool
	var isCrouching bool
	generatedHeadTick := 0

	for {
		select {
		case <-closeChan:
			return

		case <-ticker.C:
		InputLoop:
			for {
				select {
				case msg := <-inputChan:
					if msg.Type == "input" {
						if msg.Input == "JUMP" {
							pendingJump = true
						}
						if msg.Input == "DUCK_START" {
							isCrouching = true
						}
						if msg.Input == "DUCK_END" {
							isCrouching = false
						}
						if msg.Input == "DEATH" {
							state.IsDead = true
						}

					}
					if msg.Type == "ping" {
						conn.WriteJSON(WSServerMsg{Type: "pong", Ts: msg.Tick})
					}

					if msg.Type == "sync" {

						diff := math.Abs(state.PosY - msg.PosY)

						if diff < 100.0 {
							state.PosY = msg.PosY
							state.VelY = msg.VelY
						}
					}

					if msg.Type == "debug" {
						spd := calculateSpeed(state.Ticks)
						conn.WriteJSON(WSServerMsg{
							Type:         "debug_sync",
							ServerTick:   state.Ticks,
							Obstacles:    state.Obstacles,
							Platforms:    state.Platforms,
							Score:        state.Score,
							CurrentSpeed: spd,
						})
						log.Printf("🐞 Debug Snapshot an Client gesendet (Tick %d)", state.Ticks)
					}
				default:
					break InputLoop
				}
			}

			state.Ticks++
			state.Score++

			currentSpeed := calculateSpeed(state.Ticks)

			updatePhysics(&state, pendingJump, isCrouching, currentSpeed)
			pendingJump = false

			checkCollisions(&state, isCrouching, currentSpeed)

			if state.IsDead {
				rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{
					"score":   state.Score,
					"is_dead": 1,
				})
				rdb.Expire(ctx, "session:"+sessionID, 24*time.Hour)

				conn.WriteJSON(WSServerMsg{Type: "dead", Score: state.Score})
				return
			}

			moveWorld(&state, currentSpeed)

			targetTick := state.Ticks + BufferAhead
			var newObs []ActiveObstacle
			var newPlats []ActivePlatform

			loops := 0
			for generatedHeadTick < targetTick && loops < 10 {
				generatedHeadTick++
				loops++
				o, p := generateFutureObjects(&state, generatedHeadTick, currentSpeed)
				if len(o) > 0 {
					newObs = append(newObs, o...)
					state.Obstacles = append(state.Obstacles, o...)
				}
				if len(p) > 0 {
					newPlats = append(newPlats, p...)
					state.Platforms = append(state.Platforms, p...)
				}
			}

			if len(newObs) > 0 || len(newPlats) > 0 || state.Ticks%15 == 0 {
				msg := WSServerMsg{
					Type:       "chunk",
					ServerTick: state.Ticks,
					Score:      state.Score,
					Obstacles:  newObs,
					Platforms:  newPlats,
					PowerUps: PowerUpState{
						GodLives: state.GodLives, HasBat: state.HasBat, BootTicks: state.BootTicks,
					},
				}
				conn.WriteJSON(msg)
			}
		}
	}
}

func generateFutureObjects(s *SimState, tick int, speed float64) ([]ActiveObstacle, []ActivePlatform) {
	var createdObs []ActiveObstacle
	var createdPlats []ActivePlatform

	// Initialisierung beim ersten Lauf
	if s.NextSpawnTick == 0 {
		s.NextSpawnTick = tick + 50
	}

	// Ist es Zeit für etwas Neues?
	if tick >= s.NextSpawnTick {
		spawnX := SpawnXStart

		chunkCount := len(defaultConfig.Chunks)

		if chunkCount > 0 && s.RNG.NextFloat() > 0.8 {
			idx := int(s.RNG.NextRange(0, float64(chunkCount)))
			chunk := defaultConfig.Chunks[idx]

			for _, p := range chunk.Platforms {
				createdPlats = append(createdPlats, ActivePlatform{
					X:      spawnX + p.X,
					Y:      p.Y,
					Width:  p.Width,
					Height: p.Height,
				})
			}

			for _, o := range chunk.Obstacles {

				speech := ""
				for _, def := range defaultConfig.Obstacles {
					if def.ID == o.ID {
						if def.CanTalk && len(def.SpeechLines) > 0 {
							if s.RNG.NextFloat() > 0.7 {
								sIdx := int(s.RNG.NextRange(0, float64(len(def.SpeechLines))))
								speech = def.SpeechLines[sIdx]
							}
						}
						break
					}
				}

				createdObs = append(createdObs, ActiveObstacle{
					ID:     o.ID,
					Type:   o.Type,
					X:      spawnX + o.X,
					Y:      o.Y,
					Width:  o.Width,
					Height: o.Height,
					Speech: speech,
				})
			}

			width := float64(chunk.TotalWidth)
			if width == 0 {
				width = 2000.0
			}
			s.NextSpawnTick = tick + int(width/speed)

		} else {

			gap := 400 + int(s.RNG.NextRange(0, 500))
			s.NextSpawnTick = tick + int(float64(gap)/speed)

			defs := defaultConfig.Obstacles
			if len(defs) > 0 {
				isBoss := (tick % 1500) > 1200
				var pool []ObstacleDef

				for _, d := range defs {

					if d.NoRandomSpawn {
						continue
					}

					if isBoss {
						if d.ID == "principal" || d.ID == "trashcan" {
							pool = append(pool, d)
						}
					} else {
						if d.ID != "principal" {
							pool = append(pool, d)
						}
					}
				}

				def := s.RNG.PickDef(pool)

				if def != nil {
					if def.Type == "powerup" && s.RNG.NextFloat() > 0.1 {
						def = nil
					}

					if def != nil {
						// Speech Logik
						speech := ""
						if def.CanTalk && len(def.SpeechLines) > 0 {
							if s.RNG.NextFloat() > 0.7 {
								sIdx := int(s.RNG.NextRange(0, float64(len(def.SpeechLines))))
								speech = def.SpeechLines[sIdx]
							}
						}

						spawnY := GroundY - def.Height - def.YOffset

						createdObs = append(createdObs, ActiveObstacle{
							ID:     def.ID,
							Type:   def.Type,
							X:      spawnX,
							Y:      spawnY,
							Width:  def.Width,
							Height: def.Height,
							Speech: speech,
						})
					}
				}
			}
		}
	}
	return createdObs, createdPlats
}
