package main

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"math/rand"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/heroiclabs/nakama-common/runtime"
)

// --- Constants & Enums ---

const (
	OpCodeGameStart   = 1
	OpCodeUpdateState = 2
	OpCodeMove        = 3
	OpCodeGameEvent   = 5 // Special game events (item use, character ability)
	OpCodeGameOver    = 6
	OpCodeGetState    = 100 // New opcode for requesting current state

	MaxPlayers = 2 // Default value, will be overridden by config
)

var (
	BackendBaseURL = "http://host.docker.internal:9991/api/internal" // Default
	InternalAPIKey = "bindbox-internal-secret-2024"                  // Must match backend
)

var httpClient = &http.Client{
	Timeout: 5 * time.Second,
}

// Helper function to make authenticated internal API requests
func makeInternalRequest(method, url string, body []byte) (*http.Response, error) {
	req, err := http.NewRequest(method, url, bytes.NewBuffer(body))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Internal-Key", InternalAPIKey)
	return httpClient.Do(req)
}

type MinesweeperConfig struct {
	GridSize         int             `json:"grid_size"`
	BombCount        int             `json:"bomb_count"`
	ItemMin          int             `json:"item_min"`
	ItemMax          int             `json:"item_max"`
	HPInit           int             `json:"hp_init"`
	MatchPlayerCount int             `json:"match_player_count"` // Required players to start match
	EnabledItems     map[string]bool `json:"enabled_items"`
	ItemWeights      map[string]int  `json:"item_weights"`
}

func getMinesweeperConfig(logger runtime.Logger) *MinesweeperConfig {
	url := BackendBaseURL + "/game/minesweeper/config"
	logger.Info("Fetching minesweeper config from: %s", url)

	resp, err := makeInternalRequest("GET", url, nil)
	if err != nil {
		logger.Error("Network error fetching config (check BackendBaseURL): %v", err)
		return nil
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		logger.Error("Backend config API returned status: %d (URL: %s)", resp.StatusCode, url)
		return nil
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		logger.Error("Failed to read config response body: %v", err)
		return nil
	}

	var config MinesweeperConfig
	if err := json.Unmarshal(body, &config); err != nil {
		logger.Error("Failed to parse minesweeper config: %v. Raw body: %s", err, string(body))
		return nil
	}

	return &config
}

type VerifyTicketResponse struct {
	Valid          bool   `json:"valid"`
	UserID         string `json:"user_id"`
	RemainingTimes int    `json:"remaining_times"`
}

type SettleGameResponse struct {
	Success bool   `json:"success"`
	Reward  string `json:"reward"`
}

// GameTokenInfo contains validated token information from backend
type GameTokenInfo struct {
	Valid    bool   `json:"valid"`
	UserID   int64  `json:"user_id"`
	Username string `json:"username"`
	Avatar   string `json:"avatar"`
	GameType string `json:"game_type"`
	Ticket   string `json:"ticket"`
	Error    string `json:"error"`
}

// validateGameToken validates a game token with the backend and returns user info
func validateGameToken(logger runtime.Logger, gameToken string) *GameTokenInfo {
	logger.Info("Validating game token with backend")

	reqBody, _ := json.Marshal(map[string]string{
		"game_token": gameToken,
	})

	resp, err := makeInternalRequest("POST", BackendBaseURL+"/game/validate-token", reqBody)
	if err != nil {
		logger.Error("Failed to call backend validate-token API: %v", err)
		return nil
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		logger.Error("Backend validate-token returned non-200 status: %d", resp.StatusCode)
		return nil
	}

	body, _ := ioutil.ReadAll(resp.Body)
	var result GameTokenInfo
	if err := json.Unmarshal(body, &result); err != nil {
		logger.Error("Failed to parse validate-token response: %v", err)
		return nil
	}

	if !result.Valid {
		logger.Warn("Game token invalid: %s", result.Error)
		return nil
	}

	return &result
}

func verifyTicketWithBackend(logger runtime.Logger, userID string, ticket string) bool {
	logger.Info("Verifying ticket with backend: %s for user %s", ticket, userID)

	reqBody, _ := json.Marshal(map[string]string{
		"user_id": userID,
		"ticket":  ticket,
	})

	resp, err := makeInternalRequest("POST", BackendBaseURL+"/game/verify", reqBody)
	if err != nil {
		logger.Error("Failed to call backend verify API: %v", err)
		return false // Fail safe
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		logger.Error("Backend returned non-200 status: %d", resp.StatusCode)
		return false
	}

	body, _ := ioutil.ReadAll(resp.Body)
	var result VerifyTicketResponse
	if err := json.Unmarshal(body, &result); err != nil {
		logger.Error("Failed to parse backend response: %v", err)
		return false
	}

	return result.Valid
}

// settleGameWithBackend settles the game with the backend using the real user ID
func settleGameWithBackend(logger runtime.Logger, realUserID int64, ticket, matchID string, win bool, score int) {
	logger.Info("Settling game with backend for realUserID %d (Win: %v)", realUserID, win)

	reqBody, _ := json.Marshal(map[string]interface{}{
		"user_id":  fmt.Sprintf("%d", realUserID), // Backend expects string
		"ticket":   ticket,
		"match_id": matchID,
		"win":      win,
		"score":    score,
	})

	// Async call to not block game loop too much, or use goroutine
	go func() {
		resp, err := makeInternalRequest("POST", BackendBaseURL+"/game/settle", reqBody)
		if err != nil {
			logger.Error("Failed to call backend settle API: %v", err)
			return
		}
		defer resp.Body.Close()

		if resp.StatusCode != 200 {
			logger.Error("Backend settle returned non-200: %d", resp.StatusCode)
		} else {
			logger.Info("Game settled successfully with backend")
		}
	}()
}

var (
	ItemTypes = []string{
		"medkit", "bomb_timer", "poison", "shield", "skip",
		"magnifier", "knife", "revive", "lightning", "chest", "curse",
	}
	CharacterTypes = []string{
		"elephant", "cat", "dog", "monkey", "chicken", "sloth", "hippo", "tiger",
	}

	CharacterData = map[string]struct {
		MaxHP  int
		Avatar string
		Desc   string
	}{
		"elephant": {MaxHP: 5, Avatar: "🐘", Desc: "High HP, can't use some items"},
		"cat":      {MaxHP: 3, Avatar: "🐱", Desc: "Max dmg taken is 1"},
		"dog":      {MaxHP: 4, Avatar: "🐶", Desc: "Periodic magnifier"},
		"monkey":   {MaxHP: 4, Avatar: "🐒", Desc: "Get banana (heal) chance"},
		"chicken":  {MaxHP: 4, Avatar: "🐔", Desc: "Get item on dmg chance"},
		"sloth":    {MaxHP: 4, Avatar: "🦥", Desc: "Immune poison, bomb dmg reduced"},
		"hippo":    {MaxHP: 4, Avatar: "🦛", Desc: "Cant pick items, resist death chance"},
		"tiger":    {MaxHP: 4, Avatar: "🐯", Desc: "Stronger knife"},
	}
)

// --- Structs ---

type GridCell struct {
	Type          string `json:"type"` // "empty" | "bomb" | "item"
	ItemID        string `json:"itemId,omitempty"`
	Revealed      bool   `json:"revealed"`
	NeighborBombs int    `json:"neighborBombs"` // Count of adjacent bombs
}

type Player struct {
	UserID     string   `json:"userId"`     // Nakama user ID
	RealUserID int64    `json:"realUserId"` // Backend user ID (for settlements)
	SessionID  string   `json:"sessionId"`
	Username   string   `json:"username"`
	Avatar     string   `json:"avatar"`
	HP         int      `json:"hp"`
	MaxHP      int      `json:"maxHp"`
	Status     []string `json:"status"` // Visual status tags
	Character  string   `json:"character"`
	Ticket     string   `json:"ticket"` // Store the ticket used to join

	// Status Flags
	Shield        bool `json:"shield"`
	SkipTurn      bool `json:"skipTurn"`
	Poisoned      bool `json:"poisoned"`
	PoisonSteps   int  `json:"poisonSteps"` // Steps taken since poisoned
	Revive        bool `json:"revive"`
	Curse         bool `json:"curse"`
	TimeBombTurns int  `json:"timeBombTurns"` // Countdown for time bomb (0 = no bomb)

	// Character ability usage counters (for limits)
	MonkeyBananaCount int  `json:"monkeyBananaCount"` // Max 2 per game
	ChickenItemCount  int  `json:"chickenItemCount"`  // Max 2 per game
	HippoDeathImmune  bool `json:"hippoDeathImmune"`  // Used once = true

	// Magnifier reveals (cell index -> cell type)
	RevealedCells map[int]string `json:"revealedCells"`
}

type GameState struct {
	Players           map[string]*Player `json:"players"`
	Grid              []*GridCell        `json:"grid"`
	GridSize          int                `json:"gridSize"` // Side length of the square grid
	TurnOrder         []string           `json:"turnOrder"`
	CurrentTurnIndex  int                `json:"currentTurnIndex"`
	Round             int                `json:"round"`
	GlobalTurnCount   int                `json:"globalTurnCount"` // Total turns taken (for Dog ability)
	WinnerID          string             `json:"winnerId"`
	GameStarted       bool               `json:"gameStarted"`
	LastMoveTimestamp int64              `json:"lastMoveTimestamp"` // Unix timestamp in seconds
}

type MoveMessage struct {
	Index int `json:"index"`
}

type GetStateMessage struct {
	Action string `json:"action"`
}

type MatchState struct {
	State            *GameState
	HPInit           int
	MatchPlayerCount int                       // Dynamic player count for matching
	ValidatedPlayers map[string]*GameTokenInfo // Cache: NakamaUserID -> validated token info
}

// GameEvent represents a special event to be displayed in client logs
type GameEvent struct {
	Type       string `json:"type"` // "ability", "item", "damage", "heal", "status"
	PlayerID   string `json:"playerId"`
	PlayerName string `json:"playerName"`
	TargetID   string `json:"targetId,omitempty"`
	TargetName string `json:"targetName,omitempty"`
	ItemID     string `json:"itemId,omitempty"`
	Value      int    `json:"value,omitempty"`
	Message    string `json:"message"`
}

func broadcastEvent(dispatcher runtime.MatchDispatcher, event GameEvent) {
	data, _ := json.Marshal(event)
	dispatcher.BroadcastMessage(OpCodeGameEvent, data, nil, nil, true)
}

// --- Match Handler Methods ---
type MatchHandler struct{}

func (m *MatchHandler) MatchInit(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, params map[string]interface{}) (interface{}, int, string) {
	logger.Error("MatchInit called (Go) - Dynamic Config Version")

	// Fetch dynamic config
	apiConfig := getMinesweeperConfig(logger)

	// Set defaults if fetch fails
	gridSizeSide := 10
	totalCells := 100
	bombCount := 30
	itemCountMin := 5
	itemCountMax := 10
	hpInit := 4

	if apiConfig != nil {
		if apiConfig.GridSize > 0 && apiConfig.GridSize <= 30 {
			gridSizeSide = apiConfig.GridSize
		}
		totalCells = gridSizeSide * gridSizeSide
		if apiConfig.BombCount > 0 && apiConfig.BombCount < totalCells {
			bombCount = apiConfig.BombCount
		}
		if apiConfig.ItemMax >= apiConfig.ItemMin && apiConfig.ItemMin >= 0 {
			itemCountMin = apiConfig.ItemMin
			itemCountMax = apiConfig.ItemMax
		}
		if apiConfig.HPInit > 0 {
			hpInit = apiConfig.HPInit
		}
		logger.Info("Using dynamic config: %+v (Final: Grid=%d, Bombs=%d, HP=%d)", apiConfig, gridSizeSide, bombCount, hpInit)
	}

	// Dynamic match player count
	matchPlayerCount := MaxPlayers // Default
	if apiConfig != nil && apiConfig.MatchPlayerCount >= 2 && apiConfig.MatchPlayerCount <= 10 {
		matchPlayerCount = apiConfig.MatchPlayerCount
		logger.Info("Using dynamic match_player_count: %d", matchPlayerCount)
	}

	// Generate Grid
	grid := make([]*GridCell, totalCells)
	for i := 0; i < totalCells; i++ {
		grid[i] = &GridCell{Type: "empty", Revealed: false}
	}

	// Place Bombs
	bombsPlaced := 0
	for bombsPlaced < bombCount && bombsPlaced < totalCells {
		idx := rand.Intn(totalCells)
		if grid[idx].Type == "empty" {
			grid[idx].Type = "bomb"
			bombsPlaced++
		}
	}

	// Filter enabled items and calculate weights
	var pool []string
	if apiConfig != nil && len(apiConfig.EnabledItems) > 0 {
		for _, it := range ItemTypes {
			if enabled, ok := apiConfig.EnabledItems[it]; ok && enabled {
				weight := 10
				if w, ok := apiConfig.ItemWeights[it]; ok && w > 0 {
					weight = w
				}
				for i := 0; i < weight; i++ {
					pool = append(pool, it)
				}
			}
		}
	}

	// Fallback pool if empty
	if len(pool) == 0 {
		pool = ItemTypes
	}

	// Place Items
	itemCount := rand.Intn(itemCountMax-itemCountMin+1) + itemCountMin
	itemsPlaced := 0
	for itemsPlaced < itemCount && (bombsPlaced+itemsPlaced) < totalCells {
		idx := rand.Intn(totalCells)
		if grid[idx].Type == "empty" {
			grid[idx].Type = "item"
			grid[idx].ItemID = pool[rand.Intn(len(pool))]
			itemsPlaced++
		}
	}

	// Calculate Neighbor Bombs for all cells
	for i := 0; i < totalCells; i++ {
		if grid[i].Type == "bomb" {
			continue
		}

		count := 0
		row := i / gridSizeSide
		col := i % gridSizeSide

		// Check all 8 neighbors
		for r := row - 1; r <= row+1; r++ {
			for c := col - 1; c <= col+1; c++ {
				// Skip valid check
				if r >= 0 && r < gridSizeSide && c >= 0 && c < gridSizeSide {
					// Skip self
					if r == row && c == col {
						continue
					}
					neighborIdx := r*gridSizeSide + c
					if grid[neighborIdx].Type == "bomb" {
						count++
					}
				}
			}
		}
		grid[i].NeighborBombs = count
	}

	state := &GameState{
		Players:          make(map[string]*Player),
		Grid:             grid,
		GridSize:         gridSizeSide,
		TurnOrder:        make([]string, 0),
		CurrentTurnIndex: 0,
		Round:            1,
		WinnerID:         "",
		GameStarted:      false,
	}

	// Store hpInit in params or just use local, but MatchJoin needs it
	// We can store it in MatchState for future joins
	tickRate := 10
	label := "Animal Minesweeper"

	return &MatchState{
		State:            state,
		HPInit:           hpInit,
		MatchPlayerCount: matchPlayerCount,
		ValidatedPlayers: make(map[string]*GameTokenInfo),
	}, tickRate, label
}

func (m *MatchHandler) MatchJoinAttempt(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presence runtime.Presence, metadata map[string]string) (interface{}, bool, string) {
	matchState := state.(*MatchState)
	gameState := matchState.State

	// 1. Validate GameToken (REQUIRED - prevents free play and spoofing)
	gameToken, ok := metadata["game_token"]
	if !ok || gameToken == "" {
		logger.Warn("MatchJoinAttempt: No game_token provided for user %s, rejecting", presence.GetUserId())
		return state, false, "Game token required to join"
	}

	// Validate token with backend and get real user info
	tokenInfo := validateGameToken(logger, gameToken)
	if tokenInfo == nil {
		logger.Warn("MatchJoinAttempt: Invalid game_token for user %s", presence.GetUserId())
		return state, false, "Invalid or expired game token"
	}

	// Cache the validated info for use in MatchJoin
	matchState.ValidatedPlayers[presence.GetUserId()] = tokenInfo
	logger.Info("MatchJoinAttempt: Validated user %s (RealUserID: %d, Username: %s)",
		presence.GetUserId(), tokenInfo.UserID, tokenInfo.Username)

	if gameState.GameStarted {
		return state, false, "Game already started"
	}
	if len(gameState.Players) >= matchState.MatchPlayerCount {
		return state, false, "Match full"
	}

	return state, true, ""
}

func (m *MatchHandler) MatchJoin(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presences []runtime.Presence) interface{} {
	matchState := state.(*MatchState)
	gameState := matchState.State

	for _, presence := range presences {
		if _, exists := gameState.Players[presence.GetUserId()]; exists {
			continue
		}

		// Get validated token info from cache
		tokenInfo := matchState.ValidatedPlayers[presence.GetUserId()]
		if tokenInfo == nil {
			// This should not happen if MatchJoinAttempt worked correctly
			logger.Error("MatchJoin: No cached token info for user %s, rejecting", presence.GetUserId())
			continue
		}

		character := CharacterTypes[rand.Intn(len(CharacterTypes))]
		charData := CharacterData[character]
		initialHP := charData.MaxHP
		if matchState.HPInit > 0 {
			initialHP = matchState.HPInit
		}

		// Use real user info from validated token
		username := tokenInfo.Username
		if username == "" {
			username = presence.GetUsername() // Fallback to Nakama username
		}

		player := &Player{
			UserID:        presence.GetUserId(),
			RealUserID:    tokenInfo.UserID, // Backend user ID for settlements
			SessionID:     presence.GetSessionId(),
			Username:      username,
			Avatar:        charData.Avatar,
			HP:            initialHP,
			MaxHP:         initialHP,
			Status:        make([]string, 0),
			Character:     character,
			RevealedCells: make(map[int]string),
			Ticket:        tokenInfo.Ticket,
		}

		gameState.Players[presence.GetUserId()] = player
		gameState.TurnOrder = append(gameState.TurnOrder, presence.GetUserId())
		logger.Info("Player joined: %s (RealUserID: %d, Username: %s)",
			presence.GetUserId(), tokenInfo.UserID, username)
	}

	// Check if full to start game
	if len(gameState.Players) >= matchState.MatchPlayerCount && !gameState.GameStarted {
		gameState.GameStarted = true
		gameState.LastMoveTimestamp = time.Now().Unix()
		logger.Info("Game Started! TurnOrder: %v, First player: %s", gameState.TurnOrder, gameState.TurnOrder[0])

		// Consume game tickets for all players on match success
		for _, player := range gameState.Players {
			go func(p *Player) {
				consumeReqBody, _ := json.Marshal(map[string]string{
					"user_id":   fmt.Sprintf("%d", p.RealUserID),
					"game_code": "minesweeper",
					"ticket":    p.Ticket,
				})
				resp, err := makeInternalRequest("POST", BackendBaseURL+"/game/consume-ticket", consumeReqBody)
				if err != nil {
					logger.Error("Failed to consume ticket for user %d: %v", p.RealUserID, err)
					return
				}
				defer resp.Body.Close()
				if resp.StatusCode != 200 {
					logger.Error("Backend consume-ticket returned non-200 for user %d: %d", p.RealUserID, resp.StatusCode)
				} else {
					logger.Info("Successfully consumed ticket for user %d on match success", p.RealUserID)
				}
			}(player)
		}

		data, _ := json.Marshal(gameState)
		dispatcher.BroadcastMessage(OpCodeGameStart, data, nil, nil, true)
	} else {
		logger.Info("Player count: %d. Broadcasting UPDATE_STATE", len(gameState.Players))
		data, _ := json.Marshal(gameState)
		dispatcher.BroadcastMessage(OpCodeUpdateState, data, nil, nil, true)
	}

	return matchState
}

func (m *MatchHandler) MatchLeave(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presences []runtime.Presence) interface{} {
	matchState := state.(*MatchState)
	gameState := matchState.State

	for _, presence := range presences {
		delete(gameState.Players, presence.GetUserId())

		// Remove from turn order
		for i, uid := range gameState.TurnOrder {
			if uid == presence.GetUserId() {
				// Remove element
				gameState.TurnOrder = append(gameState.TurnOrder[:i], gameState.TurnOrder[i+1:]...)

				// Adjust turn index
				if i < gameState.CurrentTurnIndex {
					gameState.CurrentTurnIndex--
				}
				if len(gameState.TurnOrder) > 0 && gameState.CurrentTurnIndex >= len(gameState.TurnOrder) {
					gameState.CurrentTurnIndex = 0
				}
				break
			}
		}
	}

	// Broadcast update if game is running
	if gameState.GameStarted && len(gameState.Players) > 0 {
		data, _ := json.Marshal(gameState)
		dispatcher.BroadcastMessage(OpCodeUpdateState, data, nil, nil, true)
	}

	// End game if less than 2 players (always need at least 2 to continue)
	if gameState.GameStarted && len(gameState.Players) < 2 {
		winnerID := ""
		if len(gameState.Players) == 1 {
			for uid := range gameState.Players {
				winnerID = uid
				break
			}
		}
		gameState.WinnerID = winnerID

		endData, _ := json.Marshal(map[string]string{"winnerId": winnerID})
		dispatcher.BroadcastMessage(OpCodeGameOver, endData, nil, nil, true)
		return nil // End match
	}

	return matchState
}

func (m *MatchHandler) MatchLoop(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, messages []runtime.MatchData) interface{} {
	matchState := state.(*MatchState)
	gameState := matchState.State

	// Only log when there are messages to process
	if len(messages) > 0 {
		logger.Info("MatchLoop processing %d messages", len(messages))
	}

	for _, message := range messages {
		logger.Info("Processing message with op_code: %d, sender: %s", message.GetOpCode(), message.GetUserId())
		switch message.GetOpCode() {
		case OpCodeMove:
			if !gameState.GameStarted {
				logger.Info("MatchLoop: Game not started ignoring move. GameStarted=%v", gameState.GameStarted)
				continue // Skip move messages if game hasn't started
			}
			logger.Info("MatchLoop: Processing OpCodeMove")
			var move MoveMessage
			if err := json.Unmarshal(message.GetData(), &move); err != nil {
				logger.Error("Failed to parse move data: %v", err)
				continue
			}
			handleMove(gameState, message.GetUserId(), move.Index, logger, dispatcher)

		case OpCodeGetState:
			// Handle get state request - send current state to requesting player
			logger.Debug("Sending current game state to player: %s", message.GetUserId())
			data, _ := json.Marshal(gameState)
			dispatcher.BroadcastMessage(OpCodeUpdateState, data, []runtime.Presence{message}, nil, true)
		}
	}

	// Inactivity Timeout Check (15 seconds)
	if gameState.GameStarted && gameState.WinnerID == "" {
		now := time.Now().Unix()
		if now-gameState.LastMoveTimestamp >= 15 {
			currentUserID := gameState.TurnOrder[gameState.CurrentTurnIndex]
			logger.Info("Inactivity timeout for player %s (15s). Deducting 1 HP and advancing turn.", currentUserID)

			player := gameState.Players[currentUserID]
			if player != nil {
				// Deduct 1 HP
				applyDamage(gameState, player, 1)

				// Advance Turn
				advanceTurn(gameState, logger)

				// Update last move timestamp
				gameState.LastMoveTimestamp = time.Now().Unix()

				// Check Game Over
				if checkGameOver(gameState, dispatcher, logger) {
					return matchState
				}

				// Broadcast update
				data, _ := json.Marshal(gameState)
				dispatcher.BroadcastMessage(OpCodeUpdateState, data, nil, nil, true)
			}
		}
	}

	return matchState
}

func (m *MatchHandler) MatchTerminate(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, graceSeconds int) interface{} {
	return state
}

func (m *MatchHandler) MatchSignal(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, data string) (interface{}, string) {
	return state, data
}

// --- Helper Functions ---

func handleMove(state *GameState, userID string, cellIndex int, logger runtime.Logger, dispatcher runtime.MatchDispatcher) {
	logger.Info("handleMove: userID=%s, cellIndex=%d", userID, cellIndex)

	if len(state.TurnOrder) == 0 {
		return
	}

	currentUserID := state.TurnOrder[state.CurrentTurnIndex]
	if userID != currentUserID {
		logger.Info("handleMove: Not your turn. Expected=%s, Got=%s", currentUserID, userID)
		return
	}

	if cellIndex < 0 || cellIndex >= len(state.Grid) {
		return
	}

	cell := state.Grid[cellIndex]
	if cell.Revealed {
		return
	}

	// Reveal
	cell.Revealed = true
	player := state.Players[userID]

	// Increment global turn counter
	state.GlobalTurnCount++

	// 狗狗天赋: 定期触发放大镜效果
	// 4人局每6回合触发一次，6人局每9回合触发一次，自动透视一个随机未翻开的格子
	if player.Character == "dog" {
		interval := 6
		if len(state.Players) >= 6 {
			interval = 9
		}
		if state.GlobalTurnCount%interval == 0 {
			// 随机选择一个未翻开的格子透视给玩家
			for i := 0; i < 100; i++ {
				idx := rand.Intn(len(state.Grid))
				if !state.Grid[idx].Revealed {
					logger.Info("Dog %s activates magnifier! Cell %d is %s", player.UserID, idx, state.Grid[idx].Type)
					broadcastEvent(dispatcher, GameEvent{
						Type: "ability", PlayerID: player.UserID, PlayerName: player.Username,
						Message: "🐶 狗狗触发嗅觉天赋，获得了一个格子的信息！",
					})
					break
				}
			}
		}
	}

	// 猴子天赋: 香蕉概率回复
	// 每次行动有15%概率获得香蕉（回复1点HP），每局游戏最多生效2次
	if player.Character == "monkey" && player.MonkeyBananaCount < 2 && rand.Float32() < 0.15 {
		healPlayer(player, 1)
		player.MonkeyBananaCount++
		logger.Info("Monkey %s found a banana! (%d/2)", player.UserID, player.MonkeyBananaCount)
		broadcastEvent(dispatcher, GameEvent{
			Type: "ability", PlayerID: player.UserID, PlayerName: player.Username,
			Value: 1, Message: "🍌 猴子发现了香蕉，回复1点血量！",
		})
	}

	// Handle Cell Content
	if cell.Type == "bomb" {
		dmg := 2
		if player.Character == "sloth" {
			dmg = 1
		}
		logger.Info("Player %s stepped on bomb (dmg=%d)", player.UserID, dmg)
		broadcastEvent(dispatcher, GameEvent{
			Type: "damage", PlayerID: player.UserID, PlayerName: player.Username,
			Value: dmg, Message: fmt.Sprintf("💣 踩到炸弹，受到%d点伤害！", dmg),
		})
		applyDamage(state, player, dmg)

	} else if cell.Type == "item" {
		if player.Character == "hippo" {
			logger.Info("Hippo %s cannot pick up items", player.UserID)
			broadcastEvent(dispatcher, GameEvent{
				Type: "ability", PlayerID: player.UserID, PlayerName: player.Username,
				ItemID: cell.ItemID, Message: "🦛 河马无法拾取道具！",
			})
		} else {
			resolveItem(state, player, cell.ItemID, logger, dispatcher)
		}
	}

	// Check Game Over
	if checkGameOver(state, dispatcher, logger) {
		return
	}

	// Advance Turn
	advanceTurn(state, logger)

	// Update last move timestamp
	state.LastMoveTimestamp = time.Now().Unix()

	// Double check Game Over (poison might have killed someone)
	if checkGameOver(state, dispatcher, logger) {
		return
	}

	// Broadcast
	data, _ := json.Marshal(state)
	dispatcher.BroadcastMessage(OpCodeUpdateState, data, nil, nil, true)
}

// resolveItem 处理道具效果
// 道具被拾取后自动使用，根据不同道具类型触发对应效果
func resolveItem(state *GameState, player *Player, item string, logger runtime.Logger, dispatcher runtime.MatchDispatcher) {
	logger.Info("Player %s used item: %s", player.UserID, item)

	// 大象角色限制: 无法使用医疗包、好人卡、复活甲
	if player.Character == "elephant" && (item == "medkit" || item == "skip" || item == "revive") {
		logger.Info("Elephant refused item %s", item)
		broadcastEvent(dispatcher, GameEvent{
			Type: "ability", PlayerID: player.UserID, PlayerName: player.Username,
			ItemID: item, Message: "🐘 大象无法使用该道具！",
		})
		return
	}

	switch item {
	case "medkit":
		player.Poisoned = false
		player.PoisonSteps = 0
		healPlayer(player, 1)
		broadcastEvent(dispatcher, GameEvent{
			Type: "item", PlayerID: player.UserID, PlayerName: player.Username,
			ItemID: item, Value: 1, Message: "💊 使用医疗包，回复1血并解除中毒！",
		})
	case "bomb_timer":
		player.TimeBombTurns = 3
		logger.Info("Player %s has a time bomb! Explodes in 3 turns", player.UserID)
		broadcastEvent(dispatcher, GameEvent{
			Type: "item", PlayerID: player.UserID, PlayerName: player.Username,
			ItemID: item, Value: 3, Message: "⏰ 定时炸弹启动，3回合后爆炸！",
		})
	case "poison":
		target := getRandomAliveTarget(state, player.UserID)
		if target != nil {
			if target.Character == "sloth" {
				logger.Info("Sloth resisted poison")
				broadcastEvent(dispatcher, GameEvent{
					Type: "item", PlayerID: player.UserID, PlayerName: player.Username,
					TargetID: target.UserID, TargetName: target.Username,
					ItemID: item, Message: "🦥 树懒免疫了毒药！",
				})
			} else {
				target.Poisoned = true
				broadcastEvent(dispatcher, GameEvent{
					Type: "item", PlayerID: player.UserID, PlayerName: player.Username,
					TargetID: target.UserID, TargetName: target.Username,
					ItemID: item, Message: fmt.Sprintf("☠️ %s 中毒了！", target.Username),
				})
			}
		}
	case "shield":
		player.Shield = true
		broadcastEvent(dispatcher, GameEvent{
			Type: "item", PlayerID: player.UserID, PlayerName: player.Username,
			ItemID: item, Message: "🛡️ 获得护盾，可抵挡一次伤害！",
		})
	case "skip":
		player.SkipTurn = true
		player.Shield = true
		broadcastEvent(dispatcher, GameEvent{
			Type: "item", PlayerID: player.UserID, PlayerName: player.Username,
			ItemID: item, Message: "⏭️ 好人卡：跳过回合并获得护盾！",
		})
	case "magnifier":
		for i := 0; i < 100; i++ {
			idx := rand.Intn(len(state.Grid))
			if !state.Grid[idx].Revealed {
				cellType := state.Grid[idx].Type
				if state.Grid[idx].Type == "item" {
					cellType = state.Grid[idx].ItemID
				}
				player.RevealedCells[idx] = cellType
				logger.Info("Magnifier: Player %s can now see cell %d (%s)", player.UserID, idx, cellType)
				broadcastEvent(dispatcher, GameEvent{
					Type: "item", PlayerID: player.UserID, PlayerName: player.Username,
					ItemID: item, Message: "🔍 放大镜：透视了一个隐藏格子！",
				})
				break
			}
		}
	case "knife":
		dmg := 1
		isAOE := false
		if player.Character == "tiger" {
			dmg = 2
			isAOE = true
		}

		if isAOE {
			broadcastEvent(dispatcher, GameEvent{
				Type: "item", PlayerID: player.UserID, PlayerName: player.Username,
				ItemID: item, Value: dmg, Message: fmt.Sprintf("🐯🔪 老虎的飞刀对所有敌人造成%d点伤害！", dmg),
			})
			for _, p := range state.Players {
				if p.UserID != player.UserID && p.HP > 0 {
					applyDamage(state, p, dmg)
				}
			}
		} else {
			target := getRandomAliveTarget(state, player.UserID)
			if target != nil {
				broadcastEvent(dispatcher, GameEvent{
					Type: "item", PlayerID: player.UserID, PlayerName: player.Username,
					TargetID: target.UserID, TargetName: target.Username,
					ItemID: item, Value: dmg, Message: fmt.Sprintf("🔪 飞刀命中 %s，造成%d点伤害！", target.Username, dmg),
				})
				applyDamage(state, target, dmg)
			}
		}
	case "revive":
		player.Revive = true
		broadcastEvent(dispatcher, GameEvent{
			Type: "item", PlayerID: player.UserID, PlayerName: player.Username,
			ItemID: item, Message: "💖 获得复活甲，可免疫一次死亡！",
		})
	case "lightning":
		broadcastEvent(dispatcher, GameEvent{
			Type: "item", PlayerID: player.UserID, PlayerName: player.Username,
			ItemID: item, Value: 1, Message: "⚡ 闪电对所有玩家造成1点伤害！",
		})
		for _, p := range state.Players {
			applyDamage(state, p, 1)
		}
	case "chest":
		// Meta reward, logic ignored
	case "curse":
		player.Curse = true
	}
}

func applyDamage(state *GameState, target *Player, amount int) {
	if target.HP <= 0 {
		return
	}

	// 护盾优先抵挡伤害（消耗护盾，不受伤）
	if target.Shield {
		target.Shield = false
		return // 完全格挡
	}

	// 猫咪天赋: 所有伤害强制为1点（诅咒加成也无效）
	if target.Character == "cat" {
		amount = 1
		target.Curse = false // 诅咒被消耗但不生效
	} else if target.Curse {
		// 非猫咪角色: 诅咒使伤害翻倍
		amount *= 2
		target.Curse = false
	}

	// Apply damage
	target.HP -= amount

	// 坤坤天赋: 受伤时有概率获得道具
	// 8%概率获得好人卡/护盾/放大镜之一，每局最多触发2次
	if target.Character == "chicken" && target.HP > 0 && target.ChickenItemCount < 2 {
		if rand.Float32() < 0.08 {
			target.ChickenItemCount++
			// 随机获得: 好人卡(skip), 护盾(shield), 放大镜(magnifier)
			items := []string{"skip", "shield", "magnifier"}
			item := items[rand.Intn(len(items))]
			switch item {
			case "skip":
				target.SkipTurn = true
				target.Shield = true // 好人卡附带护盾效果
			case "shield":
				target.Shield = true
			case "magnifier":
				// 放大镜效果在其他地方处理
			}
		}
	}

	// Death Check
	if target.HP <= 0 {
		if target.Revive {
			target.Revive = false
			target.HP = 1
		} else if target.Character == "hippo" && !target.HippoDeathImmune {
			// 55% chance to survive death (once per game)
			if rand.Float32() < 0.55 {
				target.HP = 1
				target.HippoDeathImmune = true // Mark as used
			}
		}
	}

	if target.HP < 0 {
		target.HP = 0
	}
}

func healPlayer(p *Player, amount int) {
	if p.HP < p.MaxHP {
		p.HP += amount
		if p.HP > p.MaxHP {
			p.HP = p.MaxHP
		}
	}
}

func getRandomAliveTarget(state *GameState, excludeID string) *Player {
	candidates := []*Player{}
	for _, p := range state.Players {
		if p.UserID != excludeID && p.HP > 0 {
			candidates = append(candidates, p)
		}
	}
	if len(candidates) == 0 {
		return nil
	}

	return candidates[rand.Intn(len(candidates))]
}

func advanceTurn(state *GameState, logger runtime.Logger) {
	scanCount := 0

	for {
		state.CurrentTurnIndex = (state.CurrentTurnIndex + 1) % len(state.TurnOrder)
		scanCount++

		// Prevent infinite loop if everyone skips/is dead
		if scanCount > len(state.TurnOrder)*2 {
			break
		}

		nextUID := state.TurnOrder[state.CurrentTurnIndex]
		nextPlayer := state.Players[nextUID]

		if nextPlayer.HP <= 0 {
			continue
		}

		// Handle Time Bomb countdown
		if nextPlayer.TimeBombTurns > 0 {
			nextPlayer.TimeBombTurns--
			if nextPlayer.TimeBombTurns == 0 {
				// BOOM! Time bomb explodes
				dmg := 2
				if nextPlayer.Character == "sloth" {
					dmg = 1 // Sloth takes reduced bomb damage
				}
				logger.Info("Time bomb exploded on player %s! Taking %d damage", nextPlayer.UserID, dmg)
				applyDamage(state, nextPlayer, dmg)
				if nextPlayer.HP <= 0 {
					continue // Died from bomb, skip turn
				}
			}
		}

		// Handle Poison
		if nextPlayer.Poisoned {
			nextPlayer.PoisonSteps++
			if nextPlayer.PoisonSteps%2 == 0 {
				applyDamage(state, nextPlayer, 1)
				if nextPlayer.HP <= 0 {
					continue // Died from poison, skip turn
				}
			}
		}

		// Handle Skip
		if nextPlayer.SkipTurn {
			nextPlayer.SkipTurn = false
			logger.Info("Player %s skipped turn", nextPlayer.UserID)
			continue
		}

		// Found valid player
		break
	}

	// Game Over check should happen outside
}

func checkGameOver(state *GameState, dispatcher runtime.MatchDispatcher, logger runtime.Logger) bool {
	alive := []string{}
	for _, p := range state.Players {
		if p.HP > 0 {
			alive = append(alive, p.UserID)
		}
	}

	if len(alive) <= 1 {
		winnerID := ""
		if len(alive) == 1 {
			winnerID = alive[0]
		}
		state.WinnerID = winnerID
		state.GameStarted = false

		// 2. Settle Game with Backend using RealUserID
		if winnerID != "" {
			winnerPlayer := state.Players[winnerID]
			if winnerPlayer != nil && winnerPlayer.RealUserID > 0 {
				settleGameWithBackend(logger, winnerPlayer.RealUserID, winnerPlayer.Ticket, "", true, 100)
			} else {
				logger.Error("Winner player has no RealUserID, cannot settle")
			}
		}

		endData, _ := json.Marshal(map[string]interface{}{
			"winnerId":  winnerID,
			"gameState": state,
		})
		dispatcher.BroadcastMessage(OpCodeGameOver, endData, nil, nil, true)
		return true
	}
	return false
}

// Presence helper for broadcast
type UserIDPresence struct {
	UserID    string
	SessionID string
	Username  string
}

func (p *UserIDPresence) GetUserId() string                 { return p.UserID }
func (p *UserIDPresence) GetSessionId() string              { return p.SessionID }
func (p *UserIDPresence) GetNodeId() string                 { return "" }
func (p *UserIDPresence) GetHidden() bool                   { return false }
func (p *UserIDPresence) GetPersistence() bool              { return false }
func (p *UserIDPresence) GetUsername() string               { return p.Username }
func (p *UserIDPresence) GetStatus() string                 { return "" }
func (p *UserIDPresence) GetReason() runtime.PresenceReason { return runtime.PresenceReasonUnknown }

// --- Init Module ---

func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
	logger.Error("Nakama Go Module Loaded - Dynamic Config Version")

	// Seed random
	rand.Seed(time.Now().UnixNano())

	// Initialize Backend URL from Env if exists
	envURL := os.Getenv("MINESWEEPER_BACKEND_URL")
	if envURL != "" {
		BackendBaseURL = strings.TrimSuffix(envURL, "/")
		logger.Info("Setting BackendBaseURL from environment: %s", BackendBaseURL)
	}

	if err := initializer.RegisterMatch("animal_minesweeper", func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule) (runtime.Match, error) {
		logger.Error("Creating new MatchHandler")
		return &MatchHandler{}, nil
	}); err != nil {
		logger.Error("Unable to register match: %v", err)
		return err
	}

	// Register matchmaker matched hook - when 4 players are matched, create an authoritative match
	if err := initializer.RegisterMatchmakerMatched(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, entries []runtime.MatchmakerEntry) (string, error) {
		logger.Info("Matchmaker matched! Creating authoritative match for %d players", len(entries))

		// Create an authoritative match using our custom handler
		matchId, err := nk.MatchCreate(ctx, "animal_minesweeper", nil)
		if err != nil {
			logger.Error("Failed to create match: %v", err)
			return "", err
		}

		logger.Info("Created authoritative match: %s", matchId)
		return matchId, nil
	}); err != nil {
		logger.Error("Unable to register matchmaker matched hook: %v", err)
		return err
	}

	logger.Error("Match registration completed successfully")
	return nil
}
