package api

import (
	"crypto/rand"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"prsi/internal/engine"
	"sort"
	"sync"

	"github.com/gorilla/websocket"
)

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

// Hub manages all active game rooms.
type Hub struct {
	mu      sync.RWMutex
	games   map[string]*engine.Game        // gameID → game
	conns   map[string][]*playerConn       // gameID → connected sockets
	scripts map[string]string              // gameID → accumulated JS rules for this room
	rules   map[string][]engine.ActiveRule // gameID → human-readable rule history
}

// playerConn pairs a WebSocket connection with the player ID that opened it.
type playerConn struct {
	conn     *websocket.Conn
	playerID string
}

func NewHub() *Hub {
	return &Hub{
		games:   make(map[string]*engine.Game),
		conns:   make(map[string][]*playerConn),
		scripts: make(map[string]string),
		rules:   make(map[string][]engine.ActiveRule),
	}
}

// ──────────────────────────────────────────────────────────────────────────────
// WebSocket handler
// ──────────────────────────────────────────────────────────────────────────────

type wsMessage struct {
	Type       string `json:"type"`
	PlayerID   string `json:"player_id,omitempty"`
	PlayerName string `json:"player_name,omitempty"`
	GameID     string `json:"game_id,omitempty"`
	// CardIndices carries selected card index; CardIdx is kept for backwards compat.
	CardIndices []int       `json:"card_indices,omitempty"`
	CardIdx     *int        `json:"card_idx,omitempty"`
	ChosenSuit  engine.Suit `json:"chosen_suit,omitempty"`
	Choice      string      `json:"choice,omitempty"`
	Script      string      `json:"script,omitempty"`
	Description string      `json:"description,omitempty"`
}

func (h *Hub) HandleWS(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println("ws upgrade:", err)
		return
	}
	defer conn.Close()

	for {
		_, raw, err := conn.ReadMessage()
		if err != nil {
			h.removeConn(conn)
			break
		}
		var m wsMessage
		if err := json.Unmarshal(raw, &m); err != nil {
			continue
		}
		h.handle(conn, m)
	}
}

// ──────────────────────────────────────────────────────────────────────────────
// Message dispatch
// ──────────────────────────────────────────────────────────────────────────────

func (h *Hub) handle(conn *websocket.Conn, m wsMessage) {
	h.mu.Lock()
	defer h.mu.Unlock()

	switch m.Type {

	case "create":
		id := generateID()
		game, err := engine.NewGame(id, engine.DefaultScript)
		if err != nil {
			writeError(conn, err.Error())
			return
		}
		h.games[id] = game
		h.scripts[id] = engine.DefaultScript
		conn.WriteJSON(map[string]string{"type": "created", "game_id": id})

	case "join":
		game, ok := h.games[m.GameID]
		if !ok {
			writeError(conn, "game not found")
			return
		}
		if err := game.AddPlayer(m.PlayerID, m.PlayerName); err != nil {
			writeError(conn, err.Error())
			return
		}
		// Register this connection.
		h.conns[m.GameID] = append(h.conns[m.GameID], &playerConn{conn: conn, playerID: m.PlayerID})
		h.broadcast(m.GameID)

	case "start":
		game, ok := h.games[m.GameID]
		if !ok {
			writeError(conn, "game not found")
			return
		}
		// If the previous game ended, recreate with accumulated script but keep players.
		if game.State == engine.Ended {
			players := game.Players
			script := h.scripts[m.GameID]
			activeRules := h.rules[m.GameID]
			newGame, err := engine.NewGame(m.GameID, script)
			if err != nil {
				writeError(conn, "invalid rules script: "+err.Error())
				return
			}
			newGame.ActiveRules = activeRules
			h.games[m.GameID] = newGame
			game = newGame
			for _, p := range players {
				game.AddPlayer(p.ID, p.Name) //nolint
			}
		}
		if err := game.Start(); err != nil {
			writeError(conn, err.Error())
			return
		}
		h.broadcast(m.GameID)

	case "play":
		game, ok := h.games[m.GameID]
		if !ok {
			return
		}
		indices := m.CardIndices
		if len(indices) == 0 && m.CardIdx != nil {
			indices = []int{*m.CardIdx}
		}
		if err := game.PlayCards(m.PlayerID, indices, m.ChosenSuit); err != nil {
			writeError(conn, err.Error())
			return
		}
		h.broadcast(m.GameID)

	case "draw":
		game, ok := h.games[m.GameID]
		if !ok {
			return
		}
		if err := game.Draw(m.PlayerID); err != nil {
			writeError(conn, err.Error())
			return
		}
		h.broadcast(m.GameID)

	case "choice":
		game, ok := h.games[m.GameID]
		if !ok {
			return
		}
		if err := game.ApplyChoice(m.PlayerID, m.Choice); err != nil {
			writeError(conn, err.Error())
			return
		}
		h.broadcast(m.GameID)

	// set_rule is sent by the winner after the LLM has produced and the client has
	// confirmed a new script. The hub validates it, stores it, and broadcasts.
	case "set_rule":
		game, ok := h.games[m.GameID]
		if !ok {
			writeError(conn, "game not found")
			return
		}
		// Only the winner may set a rule.
		if game.Winner == nil || game.Winner.ID != m.PlayerID {
			writeError(conn, "only the winner can propose a new rule")
			return
		}
		// Validate the script compiles.
		if _, err := engine.NewScriptRuntime(m.Script); err != nil {
			writeError(conn, "invalid script: "+err.Error())
			return
		}
		h.scripts[m.GameID] = m.Script
		rule := engine.ActiveRule{
			Description: m.Description,
			ProposedBy:  game.Winner.Name,
		}
		h.rules[m.GameID] = append(h.rules[m.GameID], rule)
		// Broadcast so clients can display the new rule list.
		h.broadcastRules(m.GameID)
	}
}

// ──────────────────────────────────────────────────────────────────────────────
// HTTP handler — exposes script source for the editor panel
// ──────────────────────────────────────────────────────────────────────────────

func (h *Hub) HandleGetScript(w http.ResponseWriter, r *http.Request) {
	gameID := r.URL.Query().Get("game_id")
	h.mu.RLock()
	script, ok := h.scripts[gameID]
	h.mu.RUnlock()
	if !ok {
		http.Error(w, "game not found", http.StatusNotFound)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"script": script})
}

type lobbyInfo struct {
	GameID      string           `json:"game_id"`
	PlayerCount int              `json:"player_count"`
	Players     []string         `json:"players"`
	State       engine.GameState `json:"state"`
	HasRules    bool             `json:"has_rules"`
}

// HandleListLobbies returns waiting rooms that can be joined.
func (h *Hub) HandleListLobbies(w http.ResponseWriter, r *http.Request) {
	h.mu.RLock()
	defer h.mu.RUnlock()

	rooms := make([]lobbyInfo, 0, len(h.games))
	for id, g := range h.games {
		g.Mu.RLock()
		if g.State == engine.Waiting && len(g.Players) < 4 {
			names := make([]string, 0, len(g.Players))
			for _, p := range g.Players {
				names = append(names, p.Name)
			}
			rooms = append(rooms, lobbyInfo{
				GameID:      id,
				PlayerCount: len(g.Players),
				Players:     names,
				State:       g.State,
				HasRules:    len(g.ActiveRules) > 0,
			})
		}
		g.Mu.RUnlock()
	}

	sort.Slice(rooms, func(i, j int) bool {
		if rooms[i].PlayerCount != rooms[j].PlayerCount {
			return rooms[i].PlayerCount > rooms[j].PlayerCount
		}
		return rooms[i].GameID < rooms[j].GameID
	})

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{"lobbies": rooms})
}

// ──────────────────────────────────────────────────────────────────────────────
// Broadcast helpers
// ──────────────────────────────────────────────────────────────────────────────

func (h *Hub) broadcast(gameID string) {
	game := h.games[gameID]
	conns := h.conns[gameID]

	// Build per-player views so each player only sees their own hand.
	for _, pc := range conns {
		view := buildView(game, pc.playerID)
		if err := pc.conn.WriteJSON(map[string]interface{}{
			"type": "update",
			"game": view,
		}); err != nil {
			log.Println("ws write:", err)
		}
	}
}

func (h *Hub) broadcastRules(gameID string) {
	rules := h.rules[gameID]
	script := h.scripts[gameID]
	for _, pc := range h.conns[gameID] {
		pc.conn.WriteJSON(map[string]interface{}{
			"type":   "rules_updated",
			"rules":  rules,
			"script": script,
		})
	}
}

// gameView is what we send to each client — identical to Game but Hand only
// contains the *receiving* player's cards; others get a count.
type gameView struct {
	ID             string                 `json:"id"`
	State          engine.GameState       `json:"state"`
	CurrentPlayer  int                    `json:"current_player_index"`
	TurnNumber     int                    `json:"turn_number"`
	RoundNumber    int                    `json:"round_number"`
	LastActionType string                 `json:"last_action_type,omitempty"`
	LastActorIndex int                    `json:"last_actor_index,omitempty"`
	ActiveSuit     engine.Suit            `json:"active_suit"`
	PenaltyCards   int                    `json:"penalty_cards"`
	SkipNext       bool                   `json:"skip_next"`
	DiscardPile    []engine.Card          `json:"discard_pile"`
	Winner         *engine.Player         `json:"winner"`
	PendingChoice  *engine.PendingChoice  `json:"pending_choice,omitempty"`
	ChoiceQueue    []engine.PendingChoice `json:"choice_queue,omitempty"`
	ActiveRules    []engine.ActiveRule    `json:"active_rules"`
	Players        []playerView           `json:"players"`
}

type playerView struct {
	ID        string        `json:"id"`
	Name      string        `json:"name"`
	HandCount int           `json:"hand_count"`
	Hand      []engine.Card `json:"hand"` // non-nil only for the receiving player
}

func buildView(g *engine.Game, receiverID string) gameView {
	g.Mu.RLock()
	defer g.Mu.RUnlock()

	players := make([]playerView, len(g.Players))
	for i, p := range g.Players {
		pv := playerView{
			ID:        p.ID,
			Name:      p.Name,
			HandCount: len(p.Hand),
		}
		if p.ID == receiverID {
			pv.Hand = p.Hand
		}
		players[i] = pv
	}

	// Only send the top 1 card of the discard pile to save bandwidth.
	var discardTop []engine.Card
	if len(g.DiscardPile) > 0 {
		discardTop = g.DiscardPile[len(g.DiscardPile)-1:]
	}

	return gameView{
		ID:             g.ID,
		State:          g.State,
		CurrentPlayer:  g.CurrentPlayer,
		TurnNumber:     g.TurnNumber,
		RoundNumber:    g.RoundNumber,
		LastActionType: g.LastActionType,
		LastActorIndex: g.LastActorIndex,
		ActiveSuit:     g.ActiveSuit,
		PenaltyCards:   g.PenaltyCards,
		SkipNext:       g.SkipNext,
		DiscardPile:    discardTop,
		Winner:         g.Winner,
		PendingChoice:  g.PendingChoice,
		ChoiceQueue:    g.ChoiceQueue,
		ActiveRules:    g.ActiveRules,
		Players:        players,
	}
}

// ──────────────────────────────────────────────────────────────────────────────
// Utility
// ──────────────────────────────────────────────────────────────────────────────

func (h *Hub) removeConn(conn *websocket.Conn) {
	h.mu.Lock()
	defer h.mu.Unlock()
	for gameID, list := range h.conns {
		for i, pc := range list {
			if pc.conn == conn {
				h.conns[gameID] = append(list[:i], list[i+1:]...)
				return
			}
		}
	}
}

func writeError(conn *websocket.Conn, msg string) {
	conn.WriteJSON(map[string]string{"type": "error", "message": msg})
}

func generateID() string {
	b := make([]byte, 4)
	rand.Read(b)
	return fmt.Sprintf("%x", b)
}
