package game

import (
	"log"
	"sync"
	"time"

	"gitea.boner.be/bdnugget/goonscape/assets"
	"gitea.boner.be/bdnugget/goonscape/network"
	"gitea.boner.be/bdnugget/goonscape/types"
	pb "gitea.boner.be/bdnugget/goonserver/actions"
	rl "github.com/gen2brain/raylib-go/raylib"
)

type Game struct {
	// Component-based architecture
	PlayerManager *PlayerManager
	AssetManager  *AssetManager
	UIManager     *UIManager

	// Core game state
	Camera       rl.Camera3D
	quitChan     chan struct{}
	cleanupOnce  sync.Once
	frameCounter int // For periodic logging

	// Legacy fields for backward compatibility
	Player       *types.Player           // Use PlayerManager.LocalPlayer instead
	OtherPlayers map[int32]*types.Player // Use PlayerManager.OtherPlayers instead
	Models       []types.ModelAsset      // Use AssetManager.Models instead
	Music        rl.Music                // Use AssetManager.Music instead
	Chat         *Chat                   // Use UIManager.Chat instead
	MenuOpen     bool                    // Use UIManager.MenuOpen instead
	loginScreen  *LoginScreen            // Use UIManager.LoginScreen instead
	isLoggedIn   bool                    // Use UIManager.IsLoggedIn instead
}

func New() *Game {
	// Create managers
	playerManager := NewPlayerManager()
	assetManager := NewAssetManager()
	uiManager := NewUIManager()

	g := &Game{
		PlayerManager: playerManager,
		AssetManager:  assetManager,
		UIManager:     uiManager,
		Camera: rl.Camera3D{
			Position:   rl.NewVector3(0.0, 20.0, 0.0),
			Target:     rl.NewVector3(0.0, 0.0, 0.0),
			Up:         rl.NewVector3(0.0, 1.0, 0.0),
			Fovy:       45.0,
			Projection: rl.CameraPerspective,
		},
		quitChan: make(chan struct{}),
	}

	// Initialize legacy fields (for backward compatibility)
	g.Player = g.PlayerManager.LocalPlayer
	g.OtherPlayers = g.PlayerManager.OtherPlayers
	g.Models = g.AssetManager.Models
	g.Music = g.AssetManager.Music
	g.Chat = g.UIManager.Chat
	g.MenuOpen = g.UIManager.MenuOpen
	g.loginScreen = g.UIManager.LoginScreen
	g.isLoggedIn = g.UIManager.IsLoggedIn

	// Set up inter-component references
	g.Chat.userData = g // Pass game instance to chat for callbacks

	// Initialize world
	InitWorld()

	return g
}

func (g *Game) LoadAssets() error {
	return SafeExecute(func() error {
		// Load models
		var err error
		models, err := assets.LoadModels()
		if err != nil {
			log.Printf("Warning: Failed to load models: %v", err)
		}
		g.AssetManager.Models = models

		// Update legacy field
		g.Models = models

		// Try to load music
		music, err := assets.LoadMusic("resources/audio/music.mp3")
		if err != nil {
			log.Printf("Warning: Failed to load music: %v", err)
		} else {
			g.AssetManager.Music = music
			// Update legacy field
			g.Music = music
		}

		return nil
	})
}

func (g *Game) Update(deltaTime float32) {
	// Legacy code to maintain compatibility
	if !g.UIManager.IsLoggedIn {
		// Handle login
		username, password, isRegistering, doAuth := g.UIManager.LoginScreen.Update()
		// Update legacy fields
		g.isLoggedIn = g.UIManager.IsLoggedIn

		if doAuth {
			conn, playerID, err := network.ConnectToServer(username, password, isRegistering)
			if err != nil {
				g.UIManager.LoginScreen.SetError(err.Error())
				return
			}

			g.PlayerManager.LocalPlayer = &types.Player{
				Speed:      50.0,
				TargetPath: []types.Tile{},
				UserData:   g,
				QuitDone:   make(chan struct{}),
				ID:         playerID,
			}
			g.AssignModelToPlayer(g.PlayerManager.LocalPlayer)

			go network.HandleServerCommunication(conn, playerID, g.PlayerManager.LocalPlayer, g.PlayerManager.OtherPlayers, g.quitChan)
			g.UIManager.IsLoggedIn = true
		}
		return
	}

	// Handle ESC for menu
	if rl.IsKeyPressed(rl.KeyEscape) {
		g.UIManager.MenuOpen = !g.UIManager.MenuOpen
		return
	}

	// Don't process other inputs if menu is open
	if g.UIManager.MenuOpen {
		return
	}

	if message, sent := g.UIManager.Chat.Update(); sent {
		g.PlayerManager.LocalPlayer.Lock()
		g.PlayerManager.LocalPlayer.ActionQueue = append(g.PlayerManager.LocalPlayer.ActionQueue, &pb.Action{
			Type:        pb.Action_CHAT,
			ChatMessage: message,
			PlayerId:    g.PlayerManager.LocalPlayer.ID,
		})
		g.PlayerManager.LocalPlayer.Unlock()
	}

	g.HandleInput()

	if len(g.PlayerManager.LocalPlayer.TargetPath) > 0 {
		g.PlayerManager.LocalPlayer.MoveTowards(g.PlayerManager.LocalPlayer.TargetPath[0], deltaTime, GetMapGrid())
	}

	// Periodically log information about other players
	g.frameCounter++
	if g.frameCounter%300 == 0 {
		rl.TraceLog(rl.LogInfo, "There are %d other players", len(g.PlayerManager.OtherPlayers))
		for id, other := range g.PlayerManager.OtherPlayers {
			rl.TraceLog(rl.LogInfo, "Other player ID: %d, Position: (%f, %f, %f), Has model: %v",
				id, other.PosActual.X, other.PosActual.Y, other.PosActual.Z, other.Model.Meshes != nil)
		}
	}

	// Process other players
	for _, other := range g.PlayerManager.OtherPlayers {
		if other == nil {
			continue
		}

		// Make sure other players have models assigned
		if other.Model.Meshes == nil {
			g.AssignModelToPlayer(other)
		}

		// Update other player movement
		if len(other.TargetPath) > 0 {
			other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid())
		}
	}

	UpdateCamera(&g.Camera, g.PlayerManager.LocalPlayer.PosActual, deltaTime)

	// Update music if available
	if g.AssetManager.Music.Stream.Buffer != nil {
		rl.UpdateMusicStream(g.AssetManager.Music)
	}

	// Update legacy fields
	g.Player = g.PlayerManager.LocalPlayer
	g.OtherPlayers = g.PlayerManager.OtherPlayers
	g.Models = g.AssetManager.Models
	g.Music = g.AssetManager.Music
	g.MenuOpen = g.UIManager.MenuOpen
}

func (g *Game) DrawMap() {
	for x := 0; x < types.MapWidth; x++ {
		for y := 0; y < types.MapHeight; y++ {
			height := GetTileHeight(x, y)

			// Interpolate height for smoother landscape
			if x > 0 {
				height += GetTileHeight(x-1, y)
			}
			if y > 0 {
				height += GetTileHeight(x, y-1)
			}
			if x > 0 && y > 0 {
				height += GetTileHeight(x-1, y-1)
			}
			height /= 4.0

			tilePos := rl.Vector3{
				X: float32(x * types.TileSize),
				Y: height * types.TileHeight,
				Z: float32(y * types.TileSize),
			}
			color := rl.Color{R: uint8(height * 25), G: 100, B: 100, A: 64}
			rl.DrawCube(tilePos, types.TileSize, types.TileHeight, types.TileSize, color)
		}
	}
}

func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
	// No need for lock in rendering, we'll use a "take snapshot" approach
	// This avoids potential deadlocks and makes the rendering more consistent

	// Check for invalid model
	if model.Meshes == nil || model.Meshes.VertexCount <= 0 {
		// Don't try to draw invalid models
		return
	}

	grid := GetMapGrid()
	modelIndex := int(player.ID) % len(g.Models)
	if modelIndex < 0 || modelIndex >= len(g.Models) {
		// Prevent out of bounds access
		modelIndex = 0
	}
	modelAsset := g.Models[modelIndex]

	const defaultHeight = 8.0 // Default height above tile, fine tune per model in types.ModelAsset
	playerPos := rl.Vector3{
		X: player.PosActual.X,
		Y: grid[player.PosTile.X][player.PosTile.Y].Height*types.TileHeight + defaultHeight + modelAsset.YOffset,
		Z: player.PosActual.Z,
	}

	// Check if model has animations
	if modelAsset.Animations.Idle != nil || modelAsset.Animations.Walk != nil {
		if player.IsMoving && len(modelAsset.Animations.Walk) > 0 {
			anim := modelAsset.Animations.Walk[0] // Use first walk animation
			if anim.FrameCount > 0 {
				currentFrame := player.AnimationFrame % anim.FrameCount
				rl.UpdateModelAnimation(model, anim, currentFrame)
			}
		} else if len(modelAsset.Animations.Idle) > 0 {
			anim := modelAsset.Animations.Idle[0] // Use first idle animation
			if anim.FrameCount > 0 {
				currentFrame := player.AnimationFrame % anim.FrameCount
				rl.UpdateModelAnimation(model, anim, currentFrame)
			}
		}
	}

	// Use placeholder color if it's set, otherwise use white
	var drawColor rl.Color = rl.White
	if player.PlaceholderColor.A > 0 {
		drawColor = player.PlaceholderColor
	}
	rl.DrawModel(model, playerPos, 16, drawColor)

	// Draw floating messages and path indicators
	if player.FloatingMessage != nil {
		screenPos := rl.GetWorldToScreen(rl.Vector3{
			X: playerPos.X,
			Y: playerPos.Y + 24.0,
			Z: playerPos.Z,
		}, g.Camera)

		player.FloatingMessage.ScreenPos = screenPos
	}

	if len(player.TargetPath) > 0 {
		targetTile := player.TargetPath[len(player.TargetPath)-1]
		targetPos := rl.Vector3{
			X: float32(targetTile.X * types.TileSize),
			Y: grid[targetTile.X][targetTile.Y].Height * types.TileHeight,
			Z: float32(targetTile.Y * types.TileSize),
		}
		rl.DrawCubeWires(targetPos, types.TileSize, types.TileHeight, types.TileSize, rl.Green)

		nextTile := player.TargetPath[0]
		nextPos := rl.Vector3{
			X: float32(nextTile.X * types.TileSize),
			Y: grid[nextTile.X][nextTile.Y].Height * types.TileHeight,
			Z: float32(nextTile.Y * types.TileSize),
		}
		rl.DrawCubeWires(nextPos, types.TileSize, types.TileHeight, types.TileSize, rl.Yellow)
	}
}

func (g *Game) Render() {
	rl.BeginDrawing()
	defer func() {
		// This defer will catch any panics that might occur during rendering
		// and ensure EndDrawing gets called to maintain proper graphics state
		if r := recover(); r != nil {
			rl.TraceLog(rl.LogError, "Panic during rendering: %v", r)
		}
		rl.EndDrawing()
	}()

	rl.ClearBackground(rl.RayWhite)

	if !g.UIManager.IsLoggedIn {
		g.UIManager.LoginScreen.Draw()
		return
	}

	rl.BeginMode3D(g.Camera)
	g.DrawMap()

	// Draw player only if valid
	if g.PlayerManager.LocalPlayer != nil && g.PlayerManager.LocalPlayer.Model.Meshes != nil {
		g.DrawPlayer(g.PlayerManager.LocalPlayer, g.PlayerManager.LocalPlayer.Model)
	}

	// Draw other players with defensive checks
	for _, other := range g.PlayerManager.OtherPlayers {
		if other == nil {
			continue
		}

		// Make sure model is assigned
		if other.Model.Meshes == nil {
			g.AssignModelToPlayer(other)
			// Skip this frame if assignment failed
			if other.Model.Meshes == nil {
				continue
			}
		}
		g.DrawPlayer(other, other.Model)
	}
	rl.EndMode3D()

	// Draw floating messages
	drawFloatingMessage := func(msg *types.FloatingMessage) {
		if msg == nil || time.Now().After(msg.ExpireTime) {
			return
		}
		pos := msg.ScreenPos
		text := msg.Content
		textWidth := rl.MeasureText(text, 20)

		for offsetX := -2; offsetX <= 2; offsetX++ {
			for offsetY := -2; offsetY <= 2; offsetY++ {
				rl.DrawText(text,
					int32(pos.X)-textWidth/2+int32(offsetX),
					int32(pos.Y)+int32(offsetY),
					20,
					rl.Black)
			}
		}
		rl.DrawText(text, int32(pos.X)-textWidth/2, int32(pos.Y), 20, rl.Yellow)
	}

	if g.PlayerManager.LocalPlayer != nil && g.PlayerManager.LocalPlayer.FloatingMessage != nil {
		drawFloatingMessage(g.PlayerManager.LocalPlayer.FloatingMessage)
	}

	for _, other := range g.PlayerManager.OtherPlayers {
		if other != nil && other.FloatingMessage != nil {
			drawFloatingMessage(other.FloatingMessage)
		}
	}

	// Draw menu if open
	if g.UIManager.MenuOpen {
		g.DrawMenu()
	}

	// Only draw chat if menu is not open
	if !g.UIManager.MenuOpen && g.UIManager.Chat != nil {
		g.UIManager.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
	}

	rl.DrawFPS(10, 10)
}

func (g *Game) Cleanup() {
	g.cleanupOnce.Do(func() {
		// Cleanup models
		for _, model := range g.AssetManager.Models {
			rl.UnloadModel(model.Model)
			if model.Texture.ID > 0 {
				rl.UnloadTexture(model.Texture)
			}
		}

		// Unload music
		if g.AssetManager.Music.Stream.Buffer != nil {
			rl.UnloadMusicStream(g.AssetManager.Music)
		}

		close(g.quitChan)
	})
}

func (g *Game) HandleInput() {
	clickedTile, clicked := g.GetTileAtMouse()
	if clicked {
		path := FindPath(GetTile(g.PlayerManager.LocalPlayer.PosTile.X, g.PlayerManager.LocalPlayer.PosTile.Y), clickedTile)
		if len(path) > 1 {
			g.PlayerManager.LocalPlayer.Lock()
			g.PlayerManager.LocalPlayer.TargetPath = path[1:]
			g.PlayerManager.LocalPlayer.ActionQueue = append(g.PlayerManager.LocalPlayer.ActionQueue, &pb.Action{
				Type:     pb.Action_MOVE,
				X:        int32(clickedTile.X),
				Y:        int32(clickedTile.Y),
				PlayerId: g.PlayerManager.LocalPlayer.ID,
			})
			g.PlayerManager.LocalPlayer.Unlock()
		}
	}
}

func (g *Game) DrawMenu() {
	screenWidth := float32(rl.GetScreenWidth())
	screenHeight := float32(rl.GetScreenHeight())

	// Semi-transparent background
	rl.DrawRectangle(0, 0, int32(screenWidth), int32(screenHeight), rl.ColorAlpha(rl.Black, 0.7))

	// Menu title
	title := "Menu"
	titleSize := int32(40)
	titleWidth := rl.MeasureText(title, titleSize)
	rl.DrawText(title, int32(screenWidth/2)-titleWidth/2, 100, titleSize, rl.White)

	// Menu buttons
	buttonWidth := float32(200)
	buttonHeight := float32(40)
	buttonY := float32(200)
	buttonSpacing := float32(60)

	menuItems := []string{"Resume", "Settings", "Exit Game"}
	for _, item := range menuItems {
		buttonRect := rl.Rectangle{
			X:      screenWidth/2 - buttonWidth/2,
			Y:      buttonY,
			Width:  buttonWidth,
			Height: buttonHeight,
		}

		// Check mouse hover
		mousePoint := rl.GetMousePosition()
		mouseHover := rl.CheckCollisionPointRec(mousePoint, buttonRect)

		// Draw button
		if mouseHover {
			rl.DrawRectangleRec(buttonRect, rl.ColorAlpha(rl.White, 0.3))
			if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
				switch item {
				case "Resume":
					g.UIManager.MenuOpen = false
				case "Settings":
					// TODO: Implement settings
				case "Exit Game":
					g.Shutdown()
				}
			}
		}

		// Draw button text
		textSize := int32(20)
		textWidth := rl.MeasureText(item, textSize)
		textX := int32(buttonRect.X+buttonRect.Width/2) - textWidth/2
		textY := int32(buttonRect.Y + buttonRect.Height/2 - float32(textSize)/2)
		rl.DrawText(item, textX, textY, textSize, rl.White)

		buttonY += buttonSpacing
	}
}

func (g *Game) Shutdown() {
	close(g.quitChan)
}

func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
	g.UIManager.Chat.HandleServerMessages(messages)
}

func (g *Game) AssignModelToPlayer(player *types.Player) {
	if player == nil {
		return
	}

	modelAsset, found := g.AssetManager.GetModelForPlayer(player.ID)
	if !found {
		return
	}

	player.Model = modelAsset.Model
	player.PlaceholderColor = modelAsset.PlaceholderColor

	// Initialize animations if available
	if len(modelAsset.Animations.Idle) > 0 || len(modelAsset.Animations.Walk) > 0 {
		player.InitializeAnimations(modelAsset.Animations)
	}
}

func (g *Game) QuitChan() <-chan struct{} {
	return g.quitChan
}
