package main

import (
	"encoding/json"
	"fmt"
	"log"
	"math"
	"strconv"
)

type SimState struct {
	SessionID string
	Score     int
	Ticks     int
	PosY      float64
	VelY      float64
	IsDead    bool

	// Objekte
	Obstacles []ActiveObstacle
	Platforms []ActivePlatform

	// Powerups
	GodLives  int
	HasBat    bool
	BootTicks int

	// Spawning & RNG
	NextSpawnTick int
	RNG           *PseudoRNG

	// Anti-Cheat
	LastJumpDist   float64
	SuspicionScore int

	Chunks []ChunkDef
}

func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle, []ActivePlatform, PowerUpState, int, int, uint32) {

	state := loadSimState(sessionID, vals)

	if isBotSpamming(inputs) {
		log.Printf("🤖 BOT-ALARM [%s]: Spammt Sprünge", sessionID)
		state.IsDead = true
		return packResponse(&state)
	}

	// 3. Game Loop
	for i := 0; i < totalTicks; i++ {
		state.Ticks++

		currentSpeed := calculateSpeed(state.Ticks)
		didJump, isCrouching := parseInput(inputs, i)

		updatePhysics(&state, didJump, isCrouching, currentSpeed)

		checkCollisions(&state, isCrouching, currentSpeed)
		if state.IsDead {
			break
		}

		moveWorld(&state, currentSpeed)
		handleSpawning(&state, currentSpeed)

		state.Score++
	}

	if state.SuspicionScore > 15 {
		log.Printf("🤖 BOT-ALARM [%s]: Zu perfekte Sprünge (Heuristik)", sessionID)
		state.IsDead = true
	}

	saveSimState(&state)
	return packResponse(&state)
}

func updatePhysics(s *SimState, didJump, isCrouching bool, currentSpeed float64) {

	jumpPower := JumpPower
	if s.BootTicks > 0 {
		jumpPower = HighJumpPower
		s.BootTicks--
	}

	isGrounded := checkGrounded(s)

	if isCrouching {

		if !isGrounded {
			s.VelY += 2.0
		}
	}

	if didJump && isGrounded && !isCrouching {
		s.VelY = jumpPower
		isGrounded = false
	}

	// 5. Schwerkraft anwenden
	s.VelY += Gravity

	oldY := s.PosY
	newY := s.PosY + s.VelY

	landed := false

	if s.VelY > 0 {

		playerFeetOld := oldY + PlayerHeight
		playerFeetNew := newY + PlayerHeight

		pLeft := 50.0
		pRight := 50.0 + 30.0

		for _, p := range s.Platforms {

			if (pRight-5.0 > p.X) && (pLeft+5.0 < p.X+p.Width) {

				if playerFeetOld <= p.Y && playerFeetNew >= p.Y {

					newY = p.Y - PlayerHeight
					s.VelY = 0
					landed = true
					isGrounded = true
					break
				}
			}
		}
	}

	if !landed {
		if newY >= PlayerYBase {
			newY = PlayerYBase
			s.VelY = 0
			isGrounded = true
		}
	}

	s.PosY = newY
}

func checkCollisions(s *SimState, isCrouching bool, currentSpeed float64) {
	hitboxH := PlayerHeight
	hitboxY := s.PosY
	if isCrouching {
		hitboxH = PlayerHeight / 2
		hitboxY = s.PosY + (PlayerHeight - hitboxH)
	}

	activeObs := []ActiveObstacle{}

	for _, obs := range s.Obstacles {

		paddingX := 10.0
		if obs.X+obs.Width-paddingX < 55.0 {
			activeObs = append(activeObs, obs)
			continue
		}

		pLeft, pRight := 50.0+paddingX, 50.0+30.0-paddingX
		pTop, pBottom := hitboxY+10.0, hitboxY+hitboxH-5.0
		if obs.Type == "teacher" {
			pTop = hitboxY + 25.0
		}

		oLeft, oRight := obs.X+paddingX, obs.X+obs.Width-paddingX
		oTop, oBottom := obs.Y+10.0, obs.Y+obs.Height-5.0

		isHit := pRight > oLeft && pLeft < oRight && pBottom > oTop && pTop < oBottom

		if isHit {
			if obs.Type == "coin" {
				s.Score += 2000
				continue
			}
			if obs.Type == "powerup" {
				applyPowerup(s, obs.ID)
				continue
			}
			if s.HasBat && obs.Type == "teacher" {
				s.HasBat = false
				log.Printf("[%s] ⚾ Bat used on %s", s.SessionID, obs.ID)
				continue
			}
			if s.GodLives > 0 {
				s.GodLives--
				log.Printf("[%s] 🛡️ Godmode saved life", s.SessionID)
				continue
			}

			_, pRight := 50.0+10.0, 50.0+30.0-10.0 // Player X ist fest
			// Player Y/Height ausrechnen (für Log)
			pH := PlayerHeight
			if isCrouching {
				pH = PlayerHeight / 2
			}
			pTopLog := s.PosY + 10.0
			pBottomLog := s.PosY + pH - 5.0

			log.Printf("\n💀 --- DEATH REPORT [%s] ---\n"+
				"⏱️  Tick:   %d (Speed: %.2f)\n"+
				"🏃  Player: Y=%.2f (Top: %.2f, Bottom: %.2f) | VelY=%.2f | Duck=%v\n"+
				"🧱  Killer: ID='%s' (Type=%s)\n"+
				"📍  Object: X=%.2f (Left: %.2f, Right: %.2f)\n"+
				"            Y=%.2f (Top: %.2f, Bottom: %.2f)\n"+
				"💥  Overlap: X-Diff=%.2f, Y-Diff=%.2f\n"+
				"------------------------------------------\n",
				s.SessionID,
				s.Ticks, currentSpeed,
				s.PosY, pTopLog, pBottomLog, s.VelY, isCrouching,
				obs.ID, obs.Type,
				obs.X, oLeft, oRight,
				obs.Y, oTop, oBottom,
				(oLeft - pRight), (oTop - pBottomLog), // Wenn negativ, überlappen sie
			)

			log.Printf("💀 DEATH [%s]: Hit %s at Tick %d", s.SessionID, obs.ID, s.Ticks)
			s.IsDead = true
		}
		activeObs = append(activeObs, obs)
	}
	s.Obstacles = activeObs
}

func moveWorld(s *SimState, speed float64) {
	nextObs := []ActiveObstacle{}
	for _, o := range s.Obstacles {
		o.X -= speed
		if o.X+o.Width > -200.0 {
			nextObs = append(nextObs, o)
		}
	}
	s.Obstacles = nextObs

	nextPlats := []ActivePlatform{}
	for _, p := range s.Platforms {
		p.X -= speed
		if p.X+p.Width > -200.0 {
			nextPlats = append(nextPlats, p)
		}
	}
	s.Platforms = nextPlats
}

func handleSpawning(s *SimState, speed float64) {
	if s.NextSpawnTick == 0 {
		s.NextSpawnTick = s.Ticks + 50
	}

	if s.Ticks >= s.NextSpawnTick {
		spawnX := GameWidth + 3200.0

		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 {
				s.Platforms = append(s.Platforms, ActivePlatform{
					X: spawnX + p.X, Y: p.Y, Width: p.Width, Height: p.Height,
				})
			}
			for _, o := range chunk.Obstacles {

				s.Obstacles = append(s.Obstacles, ActiveObstacle{
					ID: o.ID, Type: o.Type, X: spawnX + o.X, Y: o.Y, Width: o.Width, Height: o.Height,
				})
			}

			width := chunk.TotalWidth
			if width == 0 {
				width = 2000
			}

			s.NextSpawnTick = s.Ticks + int(float64(width)/speed)

		} else {
			spawnRandomObstacle(s, speed, spawnX)
		}
	}
}

func loadSimState(sid string, vals map[string]string) SimState {
	rngState, _ := strconv.ParseInt(vals["rng_state"], 10, 64)

	s := SimState{
		SessionID:      sid,
		Score:          int(parseOr(vals["score"], 0)),
		Ticks:          int(parseOr(vals["total_ticks"], 0)),
		PosY:           parseOr(vals["pos_y"], PlayerYBase),
		VelY:           parseOr(vals["vel_y"], 0.0),
		NextSpawnTick:  int(parseOr(vals["next_spawn_tick"], 0)),
		GodLives:       int(parseOr(vals["p_god_lives"], 0)),
		BootTicks:      int(parseOr(vals["p_boot_ticks"], 0)),
		HasBat:         vals["p_has_bat"] == "1",
		LastJumpDist:   parseOr(vals["ac_last_dist"], 0.0),
		SuspicionScore: int(parseOr(vals["ac_suspicion"], 0)),
		RNG:            NewRNG(rngState),
	}

	if v, ok := vals["obstacles"]; ok && v != "" {
		json.Unmarshal([]byte(v), &s.Obstacles)
	}
	if v, ok := vals["platforms"]; ok && v != "" {
		json.Unmarshal([]byte(v), &s.Platforms)
	}
	return s
}

func saveSimState(s *SimState) {
	obsJson, _ := json.Marshal(s.Obstacles)
	platJson, _ := json.Marshal(s.Platforms)
	batStr := "0"
	if s.HasBat {
		batStr = "1"
	}

	rdb.HSet(ctx, "session:"+s.SessionID, map[string]interface{}{
		"score":           s.Score,
		"total_ticks":     s.Ticks,
		"next_spawn_tick": s.NextSpawnTick,
		"pos_y":           fmt.Sprintf("%f", s.PosY),
		"vel_y":           fmt.Sprintf("%f", s.VelY),
		"rng_state":       s.RNG.State,
		"obstacles":       string(obsJson),
		"platforms":       string(platJson),
		"p_god_lives":     s.GodLives,
		"p_has_bat":       batStr,
		"p_boot_ticks":    s.BootTicks,
		"ac_last_dist":    fmt.Sprintf("%f", s.LastJumpDist),
		"ac_suspicion":    s.SuspicionScore,
	})
}

func packResponse(s *SimState) (bool, int, []ActiveObstacle, []ActivePlatform, PowerUpState, int, int, uint32) {
	pState := PowerUpState{
		GodLives:  s.GodLives,
		HasBat:    s.HasBat,
		BootTicks: s.BootTicks,
	}
	return s.IsDead, s.Score, s.Obstacles, s.Platforms, pState, s.Ticks, s.NextSpawnTick, s.RNG.State
}

func checkPlatformLanding(pX, pY, pW, playerX, oldPlayerY, newPlayerY, velY float64) (bool, float64) {
	if velY < 0 {
		return false, 0
	}
	const pTolerance = 10.0
	playerW := 30.0

	if (playerX+playerW-pTolerance > pX) && (playerX+pTolerance < pX+pW) {
		playerFeetOld := oldPlayerY + PlayerHeight
		playerFeetNew := newPlayerY + PlayerHeight
		if playerFeetOld <= pY && playerFeetNew >= pY {
			return true, pY - PlayerHeight
		}
	}
	return false, 0
}

func spawnRandomObstacle(s *SimState, speed, spawnX float64) {
	gapPixel := 400 + int(s.RNG.NextRange(0, 500))
	ticksToWait := int(float64(gapPixel) / speed)
	s.NextSpawnTick = s.Ticks + ticksToWait

	isBossPhase := (s.Ticks % 1500) > 1200
	var possibleDefs []ObstacleDef

	for _, d := range defaultConfig.Obstacles {
		if isBossPhase {
			if d.ID == "principal" || d.ID == "trashcan" {
				possibleDefs = append(possibleDefs, d)
			}
		} else {
			if d.ID == "principal" {
				continue
			}
			if d.ID == "eraser" && s.Ticks < 3000 {
				continue
			}
			possibleDefs = append(possibleDefs, d)
		}
	}

	def := s.RNG.PickDef(possibleDefs)

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

	if def != nil {
		spawnY := GroundY - def.Height - def.YOffset
		s.Obstacles = append(s.Obstacles, ActiveObstacle{
			ID: def.ID, Type: def.Type, X: spawnX, Y: spawnY, Width: def.Width, Height: def.Height,
		})
	}
}

func applyPowerup(s *SimState, id string) {
	if id == "p_god" {
		s.GodLives = 3
	}
	if id == "p_bat" {
		s.HasBat = true
	}
	if id == "p_boot" {
		s.BootTicks = 600
	}
}

func checkGrounded(s *SimState) bool {
	if s.VelY != 0 {
		return false
	}
	if s.PosY >= PlayerYBase-0.1 {
		return true
	}

	for _, p := range s.Platforms {
		if math.Abs((s.PosY+PlayerHeight)-p.Y) < 0.5 {
			if 50+30 > p.X && 50 < p.X+p.Width {
				return true
			}
		}
	}
	return false
}

func isBotSpamming(inputs []Input) bool {
	jumpCount := 0
	for _, inp := range inputs {
		if inp.Act == "JUMP" {
			jumpCount++
		}
	}
	return jumpCount > 10
}

func parseInput(inputs []Input, currentTick int) (bool, bool) {
	jump := false
	duck := false
	for _, inp := range inputs {
		if inp.Tick == currentTick {
			if inp.Act == "JUMP" {
				jump = true
			}
			if inp.Act == "DUCK" {
				duck = true
			}
		}
	}
	return jump, duck
}

func calculateSpeed(ticks int) float64 {
	speed := BaseSpeed + (float64(ticks)/1000.0)*1.5
	if speed > 36.0 {
		return 36.0
	}
	return speed
}

func checkJumpSuspicion(s *SimState) {
	var distToObs float64 = -1.0
	for _, o := range s.Obstacles {
		if o.X > 50.0 {
			distToObs = o.X - 50.0
			break
		}
	}
	if distToObs > 0 {
		diff := math.Abs(distToObs - s.LastJumpDist)
		if diff < 1.0 {
			s.SuspicionScore++
		} else if s.SuspicionScore > 0 {
			s.SuspicionScore--
		}
		s.LastJumpDist = distToObs
	}
}

func parseOr(s string, def float64) float64 {
	if s == "" {
		return def
	}
	v, err := strconv.ParseFloat(s, 64)
	if err != nil {
		return def
	}
	return v
}
