package ws

import (
	"fmt"
	"log/slog"
	"math/rand"
	"net/http"
	"time"

	"github.com/gorilla/websocket"
)

// مقادیر پیش‌فرض مقاوم‌سازی (قابل بازنویسی در تست).
const (
	defaultTurnTimeout    = 30 * time.Second        // مهلت نوبت بازیکنِ انسان
	defaultBotDelay       = 800 * time.Millisecond  // تأخیر حرکت بات/قطع‌شده (حس طبیعی + فرصت بازگشت)
	defaultMatchWait      = 6 * time.Second         // مهلت پر شدن میز با انسان پیش از افزودن بات
	defaultTrickHold      = 1200 * time.Millisecond // مدت نمایش دستِ کامل پیش از جمع‌آوری
	defaultFirstHandDelay = 3000 * time.Millisecond // تأخیر اولین حرکتِ بات تا پایانِ اینترو+بُر زدنِ کلاینت
	defaultDealDelay      = 1600 * time.Millisecond  // تأخیر اولین بازی تا پایانِ انیمیشنِ پخشِ کارت‌ها
)

// AuthFunc توکن را تأیید کرده و (شناسه کاربر، نام نمایشی) را برمی‌گرداند.
type AuthFunc func(token string) (userID int64, name string, err error)

// inbound پیام ورودی یک کلاینت برای پردازش در هاب.
type inbound struct {
	client *Client
	msg    inboundMsg
}

// location محل بازیکن در حال بازی (برای reconnect).
type location struct {
	room *Room
	seat int
}

// endInfo اطلاعاتی که میز هنگام بسته‌شدن به هاب می‌دهد تا نگاشت‌ها پاک شوند.
type endInfo struct {
	room     *Room
	humanIDs []int64
	clients  []*Client
}

// pendingTable میز خصوصیِ در انتظارِ شروع (دورهمی).
// فقط توسط goroutine هاب خوانده/نوشته می‌شود.
type pendingTable struct {
	code    string
	host    int64
	clients []*Client
	hands   int  // تعداد دستِ انتخاب‌شده توسط میزبان (۳/۵/۷)
	rot     int  // چرخشِ جایگاه‌های غیرِمیزبان (۰..۲)
	stake   int  // شرطِ سکه‌ایِ هر بازیکن (۰ یعنی رایگان)
	started bool // پس از فشردن «شروع» توسط میزبان
}

// nonHostSeat جایگاهِ مطلقِ j-اُمین بازیکنِ غیرِمیزبان را با چرخشِ rot برمی‌گرداند.
// میزبان همیشه جایگاه ۰ است؛ سه جایگاهِ دیگر {۱،۲،۳} با چرخش جابه‌جا می‌شوند.
func nonHostSeat(j, rot int) int {
	seats := [3]int{1, 2, 3}
	return seats[((j+rot)%3+3)%3]
}

// Hub هماهنگ‌کننده مرکزی: اتصال‌ها، صف matchmaking و مسیریابی پیام‌ها.
// تمام state آن فقط توسط goroutine Run تغییر می‌کند (single-writer، بدون قفل).
// پس از ساخت یک میز، هاب دیگر به seatInfo دست نمی‌زند (مالک آن goroutine میز است).
type Hub struct {
	auth     AuthFunc
	upgrader websocket.Upgrader

	clients     map[*Client]bool
	queues      map[string][]*Client    // صف انتظار به ازای هر tier
	locations   map[int64]location      // userID → محل بازی (برای reconnect)
	tables      map[string]*pendingTable // کد میز → میز خصوصیِ در انتظار
	roomSeq     int
	botSeq      int
	fillPending map[string]bool
	settler     Settler

	turnTimeout    time.Duration
	botDelay       time.Duration
	matchWait      time.Duration
	trickHold      time.Duration
	firstHandDelay time.Duration // تأخیر اولین حرکتِ بات (هماهنگ با انیمیشنِ بُر زدنِ کلاینت)
	dealDelay      time.Duration // تأخیر اولین بازی پس از انتخابِ حکم (انیمیشنِ پخشِ کارت‌ها)
	countdownDelay time.Duration // مهلت شمارش معکوسِ میز خصوصی پیش از شروع

	register   chan *Client
	unregister chan *Client
	inbound    chan inbound
	endRoom    chan endInfo
	fill       chan string // tier برای پر کردن با بات
	startTbl   chan string // کد میز خصوصی برای شروع (پس از شمارش معکوس)
}

func NewHub(auth AuthFunc) *Hub {
	return &Hub{
		auth: auth,
		upgrader: websocket.Upgrader{
			ReadBufferSize:  1024,
			WriteBufferSize: 1024,
			// در پروداکشن مبدأ را محدود کنید؛ فعلاً برای توسعه باز است.
			CheckOrigin: func(r *http.Request) bool { return true },
		},
		clients:     make(map[*Client]bool),
		queues:      make(map[string][]*Client),
		locations:   make(map[int64]location),
		tables:      make(map[string]*pendingTable),
		fillPending: make(map[string]bool),
		settler:     noopSettler{},
		turnTimeout:    defaultTurnTimeout,
		botDelay:       defaultBotDelay,
		matchWait:      defaultMatchWait,
		trickHold:      defaultTrickHold,
		firstHandDelay: defaultFirstHandDelay,
		dealDelay:      defaultDealDelay,
		countdownDelay: 3 * time.Second,
		register:    make(chan *Client),
		unregister:  make(chan *Client),
		inbound:     make(chan inbound, 64),
		endRoom:     make(chan endInfo),
		fill:        make(chan string, 8),
		startTbl:    make(chan string, 8),
	}
}

// SetSettler تسویه‌گر اقتصادی را تنظیم می‌کند (پیش از Run).
func (h *Hub) SetSettler(s Settler) { h.settler = s }

const defaultTier = "beginner"

// Run حلقه اصلی هاب (در یک goroutine اجرا شود).
func (h *Hub) Run() {
	for {
		select {
		case c := <-h.register:
			h.handleRegister(c)
		case c := <-h.unregister:
			h.handleDisconnect(c)
		case in := <-h.inbound:
			h.handle(in)
		case e := <-h.endRoom:
			h.closeRoom(e)
		case tier := <-h.fill:
			h.onFill(tier)
		case code := <-h.startTbl:
			h.onStartTable(code)
		}
	}
}

// handleRegister اتصال جدید را ثبت می‌کند؛ اگر کاربر در حال بازی بود، reconnect می‌شود.
func (h *Hub) handleRegister(c *Client) {
	h.clients[c] = true
	if loc, ok := h.locations[c.UserID]; ok {
		c.room = loc.room
		c.seat = loc.seat
		select {
		case loc.room.actions <- roomAction{kind: akReconnect, seat: loc.seat, client: c}:
		default:
		}
		slog.Info("player reconnecting", "user", c.UserID, "room", loc.room.ID)
	}
}

func (h *Hub) handle(in inbound) {
	c := in.client
	switch in.msg.Type {
	case "join_queue":
		if c.room == nil {
			tier := in.msg.Tier
			if tier == "" {
				tier = defaultTier
			}
			h.enqueue(c, tier)
		}
	case "choose_trump", "play_card", "reshuffle", "chat", "leave":
		if c.room != nil {
			if in.msg.Type == "leave" {
				delete(h.locations, c.UserID) // پس از خروج دائمی، reconnect نشود
			}
			select {
			case c.room.actions <- roomAction{kind: akInput, seat: c.seat, client: c, msg: in.msg}:
			default:
			}
		}
	case "create_table":
		h.createTable(c)
	case "join_table":
		h.joinTable(c, in.msg.Code)
	case "start_table":
		h.startTable(c, in.msg.Hands, in.msg.Stake)
	case "set_stake":
		h.setStake(c, in.msg.Stake)
	case "rotate_table":
		h.rotateTable(c, in.msg.Dir)
	case "leave_table":
		h.leaveTable(c)
	}
}

// --- میز خصوصی (دورهمی) ---

// genCode یک کد ۵ رقمیِ یکتا برای میز خصوصی می‌سازد.
func (h *Hub) genCode() string {
	for {
		code := fmt.Sprintf("%05d", rand.Intn(100000))
		if _, ok := h.tables[code]; !ok {
			return code
		}
	}
}

// createTable یک میز خصوصی می‌سازد (در صورت مجاز بودنِ سهمیه).
func (h *Hub) createTable(c *Client) {
	if c.room != nil || c.tableCode != "" {
		return
	}
	if err := h.settler.ChargePrivateTable(c.UserID); err != nil {
		c.trySend(mustJSON(errorMsg{Type: "error", Message: "سهمیه‌ی میزهای رایگان به پایان رسیده؛ برای میز نامحدود VIP بگیرید"}))
		return
	}
	code := h.genCode()
	t := &pendingTable{code: code, host: c.UserID, clients: []*Client{c}}
	h.tables[code] = t
	c.tableCode = code
	slog.Info("private table created", "code", code, "host", c.UserID)
	h.broadcastLobby(t)
}

// joinTable کلاینت را به میز خصوصیِ موجود می‌افزاید.
func (h *Hub) joinTable(c *Client, code string) {
	if c.room != nil || c.tableCode != "" {
		return
	}
	t := h.tables[code]
	if t == nil || t.started {
		c.trySend(mustJSON(errorMsg{Type: "error", Message: "میز یافت نشد"}))
		return
	}
	if len(t.clients) >= 4 {
		c.trySend(mustJSON(errorMsg{Type: "error", Message: "میز پر است"}))
		return
	}
	t.clients = append(t.clients, c)
	c.tableCode = code
	h.broadcastLobby(t)
}

// startTable شمارش معکوس را آغاز و پس از آن بازی را شروع می‌کند (فقط میزبان).
func (h *Hub) startTable(c *Client, hands, stake int) {
	t := h.tables[c.tableCode]
	if t == nil || t.host != c.UserID || t.started {
		return
	}
	t.hands = clampHands(hands) // تعداد دستِ انتخابیِ میزبان (۳/۵/۷)
	if stake >= 0 {
		t.stake = stake // آخرین شرطِ سکه‌ای (اگر همراهِ شروع فرستاده شد)
	}
	t.started = true
	cd := mustJSON(countdownMsg{Type: "countdown", Seconds: 3})
	for _, cl := range t.clients {
		cl.trySend(cd)
	}
	code := t.code
	time.AfterFunc(h.countdownDelay, func() {
		select {
		case h.startTbl <- code:
		default:
		}
	})
}

// onStartTable پس از شمارش معکوس، میز را از کلاینت‌های متصل می‌سازد و بازی را شروع می‌کند.
func (h *Hub) onStartTable(code string) {
	t := h.tables[code]
	if t == nil {
		return
	}
	delete(h.tables, code)

	var seats [4]*seatInfo
	humans := 0
	for i, cl := range t.clients {
		if i >= 4 || !h.clients[cl] {
			continue // قطع‌شده‌ها نادیده گرفته می‌شوند (جایشان را بات می‌گیرد)
		}
		seat := t.seatOf(i)
		seats[seat] = &seatInfo{client: cl, userID: cl.UserID, name: cl.Name, connected: true}
		humans++
	}
	if humans == 0 {
		return // همه خارج شدند
	}
	for s := 0; s < 4; s++ {
		if seats[s] == nil {
			h.botSeq++
			seats[s] = &seatInfo{isBot: true, name: fmt.Sprintf("ربات %d", h.botSeq)}
		}
	}

	h.roomSeq++
	room := newRoom(fmt.Sprintf("p%d", h.roomSeq), seats, h)
	room.tier = defaultTier // برای جایزه/ورودی بی‌اثر است (میز خصوصی)
	room.private = true
	room.handsOverride = clampHands(t.hands) // تعداد دستِ انتخابیِ میزبان
	room.stake = int64(t.stake)              // شرطِ سکه‌ایِ هر بازیکن
	for s := 0; s < 4; s++ {
		if seats[s].isBot {
			continue
		}
		cl := seats[s].client
		cl.room = room
		cl.seat = s
		cl.tableCode = ""
		h.locations[cl.UserID] = location{room: room, seat: s}
	}
	go room.run()
	slog.Info("private room started", "room", room.ID, "code", code, "humans", humans)
}

// setStake شرطِ سکه‌ایِ میز را تنظیم و برای همه پخش می‌کند (فقط میزبان، پیش از شروع).
func (h *Hub) setStake(c *Client, stake int) {
	t := h.tables[c.tableCode]
	if t == nil || t.host != c.UserID || t.started {
		return
	}
	if stake < 0 {
		stake = 0
	}
	if stake > 1_000_000 {
		stake = 1_000_000
	}
	t.stake = stake
	h.broadcastLobby(t)
}

// rotateTable جایگاهِ بازیکنانِ غیرِمیزبان را دورِ میز می‌چرخاند (فقط میزبان).
// میزبان همیشه جایگاهِ اولش را حفظ می‌کند؛ چرخش تیم‌بندی را تغییر می‌دهد.
func (h *Hub) rotateTable(c *Client, dir int) {
	t := h.tables[c.tableCode]
	if t == nil || t.host != c.UserID || t.started {
		return
	}
	if len(t.clients) < 2 {
		return // جز میزبان کسی نیست ⇒ چیزی برای چرخش نمانده
	}
	d := 1
	if dir < 0 {
		d = -1
	}
	t.rot = ((t.rot+d)%3 + 3) % 3
	h.broadcastLobby(t)
}

// leaveTable خروجِ داوطلبانه از اتاق انتظار.
func (h *Hub) leaveTable(c *Client) {
	t := h.tables[c.tableCode]
	c.tableCode = ""
	if t != nil {
		h.removeFromTable(t, c)
	}
}

// removeFromTable کلاینت را از میز حذف می‌کند؛ با خروجِ میزبان یا خالی‌شدن، میز منحل می‌شود.
func (h *Hub) removeFromTable(t *pendingTable, c *Client) {
	idx := -1
	for i, cl := range t.clients {
		if cl == c {
			idx = i
			break
		}
	}
	if idx < 0 {
		return
	}
	t.clients = append(t.clients[:idx], t.clients[idx+1:]...)
	if c.UserID == t.host || len(t.clients) == 0 {
		closed := mustJSON(tableClosedMsg{Type: "table_closed", Reason: "host_left"})
		for _, cl := range t.clients {
			cl.tableCode = ""
			cl.trySend(closed)
		}
		delete(h.tables, t.code)
		return
	}
	h.broadcastLobby(t)
}

// broadcastLobby وضعیت اتاق انتظار را برای همه‌ی اعضای میز ارسال می‌کند.
func (h *Hub) broadcastLobby(t *pendingTable) {
	players := make([]lobbyPlayer, len(t.clients))
	for i, cl := range t.clients {
		_, _, avatar := h.settler.PlayerProfile(cl.UserID)
		players[i] = lobbyPlayer{
			Name:   cl.Name,
			Avatar: avatar,
			Host:   cl.UserID == t.host,
			Seat:   t.seatOf(i),
		}
	}
	for i, cl := range t.clients {
		remaining, unlimited := h.settler.PrivateTableInfo(cl.UserID)
		cl.trySend(mustJSON(tableLobbyMsg{
			Type: "table_lobby", Code: t.code, Players: players,
			Host: cl.UserID == t.host, YouSeat: t.seatOf(i),
			Remaining: remaining, Unlimited: unlimited, Stake: t.stake,
		}))
	}
}

// seatOf جایگاهِ مطلقِ کلاینتِ شماره i در فهرستِ میز را برمی‌گرداند (میزبان=۰).
func (t *pendingTable) seatOf(i int) int {
	if i == 0 {
		return 0
	}
	return nonHostSeat(i-1, t.rot)
}

// clampHands تعداد دست را به یکی از مقادیرِ مجاز (۳/۵/۷) محدود می‌کند.
func clampHands(h int) int {
	switch h {
	case 5:
		return 5
	case 7:
		return 7
	default:
		return 3
	}
}

// enqueue کلاینت را پس از کسر ورودی به صفِ tier افزوده و میزهای کامل را می‌سازد.
func (h *Hub) enqueue(c *Client, tier string) {
	if c.room != nil {
		return
	}
	for _, q := range h.queues[tier] {
		if q == c {
			return
		}
	}
	// کسر ورودی میز؛ اگر سکه کافی نبود، اجازه ورود به صف داده نمی‌شود.
	if err := h.settler.ChargeEntry(c.UserID, tier); err != nil {
		c.trySend(mustJSON(errorMsg{Type: "error", Message: "موجودی سکه برای ورود به این میز کافی نیست"}))
		return
	}
	c.tier = tier
	h.queues[tier] = append(h.queues[tier], c)
	for len(h.queues[tier]) >= 4 {
		h.formTable(tier, 4)
	}
	h.maybeArmFill(tier)
}

// onFill با سررسید مهلت، میز ناقصِ یک tier را با بات کامل می‌کند.
func (h *Hub) onFill(tier string) {
	h.fillPending[tier] = false
	n := len(h.queues[tier])
	if n == 0 {
		return
	}
	if n > 4 {
		n = 4
	}
	h.formTable(tier, n)
	h.maybeArmFill(tier)
}

// maybeArmFill در صورت وجود بازیکن در صفِ tier، تایمر پر کردن با بات را مسلح می‌کند.
func (h *Hub) maybeArmFill(tier string) {
	if len(h.queues[tier]) > 0 && !h.fillPending[tier] {
		h.fillPending[tier] = true
		time.AfterFunc(h.matchWait, func() {
			select {
			case h.fill <- tier:
			default:
			}
		})
	}
}

// formTable یک میزِ tier با nHumans بازیکن از ابتدای صف و بات برای بقیه می‌سازد.
func (h *Hub) formTable(tier string, nHumans int) {
	var seats [4]*seatInfo
	for i := 0; i < nHumans; i++ {
		c := h.queues[tier][i]
		seats[i] = &seatInfo{client: c, userID: c.UserID, name: c.Name, connected: true}
	}
	h.queues[tier] = h.queues[tier][nHumans:]
	for i := nHumans; i < 4; i++ {
		h.botSeq++
		seats[i] = &seatInfo{isBot: true, name: fmt.Sprintf("ربات %d", h.botSeq)}
	}

	h.roomSeq++
	room := newRoom(fmt.Sprintf("r%d", h.roomSeq), seats, h)
	room.tier = tier
	// ست‌کردن اشاره‌گرها و نگاشت reconnect پیش از شروع goroutine میز (happens-before).
	for i := 0; i < nHumans; i++ {
		c := seats[i].client
		c.room = room
		c.seat = i
		h.locations[c.UserID] = location{room: room, seat: i}
	}
	go room.run()
	slog.Info("room created", "room", room.ID, "tier", tier, "humans", nHumans, "bots", 4-nHumans)
}

// handleDisconnect قطع اتصال یک کلاینت را مدیریت می‌کند (نگاشت reconnect حفظ می‌شود).
func (h *Hub) handleDisconnect(c *Client) {
	if !h.clients[c] {
		return
	}
	delete(h.clients, c)
	c.close() // writePump را خاتمه می‌دهد؛ send بسته نمی‌شود تا room بدون panic بتواند trySend کند

	// اگر در صف بود، حذف و ورودی بازگردانده شود (هنوز بازی شروع نشده).
	if q := h.queues[c.tier]; len(q) > 0 {
		for i, x := range q {
			if x == c {
				h.queues[c.tier] = append(q[:i], q[i+1:]...)
				h.settler.Refund(c.UserID, c.tier)
				break
			}
		}
	}
	// اگر در اتاق انتظارِ میز خصوصی بود، از آن حذف شود.
	if c.tableCode != "" {
		if t := h.tables[c.tableCode]; t != nil {
			h.removeFromTable(t, c)
		}
		c.tableCode = ""
	}
	if c.room != nil {
		select {
		case c.room.actions <- roomAction{kind: akDisconnect, seat: c.seat, client: c}:
		default:
		}
	}
}

// closeRoom پس از پایان بازی، نگاشت‌ها و اشاره‌گرهای میز را پاک می‌کند.
func (h *Hub) closeRoom(e endInfo) {
	for _, id := range e.humanIDs {
		if loc, ok := h.locations[id]; ok && loc.room == e.room {
			delete(h.locations, id)
		}
	}
	for _, c := range e.clients {
		if c.room == e.room {
			c.room = nil
			c.seat = 0
		}
	}
	slog.Info("room closed", "room", e.room.ID)
}

// ServeWS اتصال WebSocket را پس از تأیید توکن برقرار می‌کند.
// توکن از پارامتر کوئری ?token= یا هدر Authorization خوانده می‌شود.
func (h *Hub) ServeWS(w http.ResponseWriter, r *http.Request) {
	token := r.URL.Query().Get("token")
	if token == "" {
		if a := r.Header.Get("Authorization"); len(a) > 7 && a[:7] == "Bearer " {
			token = a[7:]
		}
	}
	userID, name, err := h.auth(token)
	if err != nil {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	conn, err := h.upgrader.Upgrade(w, r, nil)
	if err != nil {
		return // upgrader خودش پاسخ خطا را نوشته
	}

	c := &Client{
		hub:    h,
		conn:   conn,
		send:   make(chan []byte, sendBuffer),
		done:   make(chan struct{}),
		UserID: userID,
		Name:   name,
	}
	h.register <- c
	go c.writePump()
	go c.readPump()
}
