package ws

import (
	"encoding/json"
	"log/slog"
	"sync"
	"time"

	"github.com/gorilla/websocket"
)

const (
	writeWait      = 10 * time.Second
	pongWait       = 60 * time.Second
	pingPeriod     = (pongWait * 9) / 10
	maxMessageSize = 4096
	sendBuffer     = 32
)

// Client یک اتصال WebSocket یک بازیکن.
type Client struct {
	hub    *Hub
	conn   *websocket.Conn
	send   chan []byte
	UserID int64
	Name   string

	// done هنگام قطع اتصال بسته می‌شود تا writePump خارج شده و
	// trySend دیگر تلاش به ارسال نکند (send هیچ‌گاه close نمی‌شود تا panic رخ ندهد).
	done      chan struct{}
	closeOnce sync.Once

	// توسط goroutine هاب ست می‌شوند (تک‌نویسنده) و فقط توسط آن خوانده می‌شوند.
	room      *Room
	seat      int
	tier      string // نوع میزی که در صفش است (برای تسویه)
	tableCode string // کدِ میز خصوصیِ در انتظار (پیش از شروع بازی)
}

// close اتصال را یک‌بار به‌صورت امن می‌بندد.
func (c *Client) close() {
	c.closeOnce.Do(func() {
		close(c.done)
		c.conn.Close()
	})
}

// readPump پیام‌های ورودی را خوانده و به هاب می‌فرستد.
func (c *Client) readPump() {
	defer func() {
		c.hub.unregister <- c
		c.close()
	}()
	c.conn.SetReadLimit(maxMessageSize)
	_ = c.conn.SetReadDeadline(time.Now().Add(pongWait))
	c.conn.SetPongHandler(func(string) error {
		return c.conn.SetReadDeadline(time.Now().Add(pongWait))
	})

	for {
		_, raw, err := c.conn.ReadMessage()
		if err != nil {
			return
		}
		var msg inboundMsg
		if err := json.Unmarshal(raw, &msg); err != nil {
			c.trySend(mustJSON(errorMsg{Type: "error", Message: "invalid message"}))
			continue
		}
		c.hub.inbound <- inbound{client: c, msg: msg}
	}
}

// writePump پیام‌های خروجی و ping را به سوکت می‌نویسد.
func (c *Client) writePump() {
	ticker := time.NewTicker(pingPeriod)
	defer func() {
		ticker.Stop()
		c.close()
	}()
	for {
		select {
		case <-c.done:
			return
		case msg := <-c.send:
			_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
			if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil {
				return
			}
		case <-ticker.C:
			_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
			if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
				return
			}
		}
	}
}

// trySend ارسال غیرمسدودکننده؛ اگر اتصال بسته یا بافر پر بود پیام را می‌اندازد.
// send هیچ‌گاه close نمی‌شود؛ پس از done صرفاً پیام دور ریخته می‌شود (بدون panic).
func (c *Client) trySend(b []byte) {
	select {
	case <-c.done:
	case c.send <- b:
	default:
		slog.Warn("client send buffer full, dropping message", "user", c.UserID)
	}
}

func mustJSON(v any) []byte {
	b, _ := json.Marshal(v)
	return b
}
