package ws

import (
	"log/slog"
	"time"

	"hakemsho/internal/game"
)

const targetScore = 7 // امتیاز لازم برای برد بازی

// actionKind نوع پیام ورودی به goroutine میز.
type actionKind int

const (
	akInput      actionKind = iota // ورودی بازیکن (choose_trump/play_card/leave)
	akTimeout                      // سررسید تایمر نوبت
	akDisconnect                   // قطع اتصال یک بازیکن
	akReconnect                    // اتصال مجدد یک بازیکن
	akCollect                      // جمع‌آوری دستِ کامل پس از نمایش
)

// roomAction یک رویداد برای پردازش در goroutine میز.
type roomAction struct {
	kind   actionKind
	seat   int
	client *Client    // برای akReconnect و تأیید فرستنده در akInput/akDisconnect
	gen    int        // برای akTimeout: نسل تایمر (تشخیص تایمر کهنه)
	msg    inboundMsg // برای akInput
}

// seatInfo وضعیت یک جایگاه؛ فقط توسط goroutine میز خوانده/نوشته می‌شود (single-writer).
type seatInfo struct {
	client    *Client
	isBot     bool
	userID    int64
	name      string
	connected bool
	left      bool   // خروج دائمی داوطلبانه (در تسویه از بازگشت ورودی محروم)
	coins     int64  // برای نمایش سرِ میز
	rankTier  string // نشانِ رتبه برای نمایش سرِ میز
	avatar    string // seed آواتارِ انتخابیِ کاربر (برای نمایش سرِ میز)
}

// auto مشخص می‌کند جایگاه باید خودکار بازی شود (بات یا قطع‌شده).
func (s *seatInfo) auto() bool { return s.isBot || !s.connected }

// Room یک میز بازی؛ تنها goroutine خودش روی state بازی و seatها می‌نویسد.
type Room struct {
	ID          string
	hub         *Hub
	tier          string
	private       bool  // میز دورهمی: بدون ورودی/جایزه‌ی tier (فقط آمار و شرطِ سکه)
	handsOverride int   // تعداد دستِ سفارشیِ میز خصوصی (۰ یعنی از tier)
	stake         int64 // شرطِ سکه‌ایِ هر بازیکن در میز خصوصی (۰ یعنی رایگان)
	pot           int64 // مجموعِ سکه‌های جمع‌آوری‌شده برای برنده
	paid          [4]bool // جایگاه‌هایی که شرط را پرداخته‌اند (برای بازگشت هنگام لغو)
	settler     Settler
	seats       [4]*seatInfo
	game        *game.Game
	actions     chan roomAction
	ended       bool
	turnGen     int
	timer          *time.Timer // تایمر فعال (نوبت یا جمع‌آوری)؛ فقط goroutine میز به آن دست می‌زند
	turnTimeout    time.Duration
	botDelay       time.Duration
	trickHold      time.Duration
	firstHandDelay time.Duration // پنجره‌ی بُر زدن+پخشِ ۵ کارت (ابتدای هَند)
	dealDelay      time.Duration // پنجره‌ی پخشِ بقیه‌ی کارت‌ها (پس از انتخابِ حکم)
	// graceUntil: تا این لحظه هیچ حرکتِ خودکاری انجام نمی‌شود (هماهنگ با انیمیشن‌های کلاینت).
	graceUntil time.Time
	chatAt     [4]time.Time // آخرین زمانِ چتِ هر جایگاه (ضدِ اسپم)
}

func newRoom(id string, seats [4]*seatInfo, hub *Hub) *Room {
	return &Room{
		ID:             id,
		hub:            hub,
		settler:        hub.settler,
		seats:          seats,
		actions:        make(chan roomAction, 64),
		turnTimeout:    hub.turnTimeout,
		botDelay:       hub.botDelay,
		trickHold:      hub.trickHold,
		firstHandDelay: hub.firstHandDelay,
		dealDelay:      hub.dealDelay,
	}
}

// run حلقه اصلی میز.
func (r *Room) run() {
	r.start()
	for act := range r.actions {
		if r.ended {
			continue
		}
		switch act.kind {
		case akInput:
			r.handleInput(act)
		case akTimeout:
			if act.gen == r.turnGen {
				r.autoMove(act.seat)
			}
		case akDisconnect:
			r.handleDisconnect(act.seat, act.client)
		case akReconnect:
			r.handleReconnect(act.seat, act.client)
		case akCollect:
			r.collect()
		}
		if r.ended {
			return // بازی تمام شد ⇒ خروج از goroutine
		}
	}
}

// start بازی را ساخته، matched را برای انسان‌ها فرستاده و وضعیت اولیه را پخش می‌کند.
func (r *Room) start() {
	// تعداد هَندِ لازم برای برد بازی از روی نوعِ میز (tier) تعیین می‌شود.
	target := r.settler.TargetHands(r.tier)
	if r.handsOverride > 0 {
		target = r.handsOverride // میز خصوصی: تعداد دستِ انتخابیِ میزبان
	}
	if target <= 0 {
		target = targetScore
	}
	r.game = game.NewGame(target, game.FirstHakem())
	r.graceUntil = time.Now().Add(r.firstHandDelay) // بُر زدن + پخشِ ۵ کارت
	// شرطِ سکه‌ایِ میزِ خصوصی: از هر بازیکنِ انسان کسر و به pot اضافه می‌شود
	// (هر که موجودی نداشت، رایگان بازی می‌کند و به pot اضافه نمی‌شود).
	if r.stake > 0 {
		for seat, s := range r.seats {
			if s.isBot || s.userID == 0 {
				continue
			}
			if err := r.settler.ChargeCoins(s.userID, r.stake, r.ID); err == nil {
				r.pot += r.stake
				r.paid[seat] = true
			}
		}
	}
	// اطلاعاتِ نمایشیِ بازیکنان (سکه و نشانِ رتبه) را یک‌بار هنگام شروع می‌گیریم.
	for _, s := range r.seats {
		if s.isBot || s.userID == 0 {
			continue
		}
		s.coins, s.rankTier, s.avatar = r.settler.PlayerProfile(s.userID)
	}
	for seat, s := range r.seats {
		if s.client != nil {
			s.client.trySend(mustJSON(r.matchedFor(seat)))
		}
	}
	r.react()
}

func (r *Room) matchedFor(seat int) matchedMsg {
	return matchedMsg{Type: "matched", Room: r.ID, Seat: seat, Players: r.playersInfo()}
}

// react پس از هر تغییر state: نتیجه را اعلام و تایمر بازیگر بعدی را مسلح می‌کند.
func (r *Room) react() {
	// دستِ کامل (۴ کارت) را نشان بده، کمی نگه دار، سپس جمع کن.
	if r.game.TrickDone {
		r.broadcastState()
		r.scheduleCollect()
		return
	}
	switch r.game.Phase {
	case game.PhaseHandOver:
		res := r.game.LastResult
		r.recordHandStats(res)
		r.broadcast(mustJSON(handOverMsg{
			Type: "hand_over", WinnerTeam: res.WinnerTeam, Kot: res.Kot,
			HakemKot: res.HakemKot, Points: res.Points, Scores: r.game.Scores,
		}))
		if err := r.game.NextHand(); err != nil {
			slog.Error("next hand", "room", r.ID, "err", err)
			r.refundAll()
			r.endRoom()
			return
		}
		r.graceUntil = time.Now().Add(r.firstHandDelay) // هَندِ جدید ⇒ بُر زدن + پخشِ ۵ کارت
		r.broadcastState()
		r.armTimer()
	case game.PhaseGameOver:
		res := r.game.LastResult
		r.recordHandStats(res)
		r.broadcast(mustJSON(gameOverMsg{
			Type: "game_over", WinnerTeam: res.WinnerTeam, Scores: r.game.Scores,
		}))
		r.settleGameOver(res)
		r.endRoom()
	default: // choose_trump یا playing
		r.broadcastState()
		r.armTimer()
	}
}

// stopTimer تایمر فعال را لغو می‌کند تا پیام‌های کهنه کانال را پر نکنند.
func (r *Room) stopTimer() {
	if r.timer != nil {
		r.timer.Stop()
		r.timer = nil
	}
}

// scheduleCollect پس از نمایش دستِ کامل، جمع‌آوری را زمان‌بندی می‌کند.
func (r *Room) scheduleCollect() {
	r.stopTimer()
	r.turnGen++ // هر تایمر نوبتِ کهنه را بی‌اثر کن
	r.timer = time.AfterFunc(r.trickHold, func() {
		select {
		case r.actions <- roomAction{kind: akCollect}:
		default:
		}
	})
}

// collect دستِ کامل را جمع کرده و به وضعیت بعدی می‌رود.
func (r *Room) collect() {
	if err := r.game.CollectTrick(); err != nil {
		return // قبلاً جمع شده یا نامعتبر
	}
	r.react()
}

// armTimer برای بازیگر فعلی (حاکم در انتخاب حکم، یا نوبت‌دار در بازی) تایمر می‌گذارد.
func (r *Room) armTimer() {
	r.stopTimer()
	if r.game.TrickDone {
		return // در زمان نمایش دستِ کامل تایمر نوبت نگذار
	}
	var actor int
	switch r.game.Phase {
	case game.PhaseChooseTrump:
		actor = r.game.Hakem
	case game.PhasePlaying:
		actor = r.game.Turn
	default:
		return
	}
	r.turnGen++
	gen := r.turnGen
	delay := r.turnTimeout
	if r.seats[actor].auto() {
		delay = r.botDelay
		// تا پایانِ پنجره‌ی انیمیشنِ کلاینت (بُر زدن یا پخشِ کارت)، حرکتِ خودکار را
		// عقب می‌اندازیم تا کارت‌ها وسطِ انیمیشن روی میز نیفتند. نوبتِ انسان دست‌نخورده می‌ماند.
		if grace := time.Until(r.graceUntil); grace > delay {
			delay = grace
		}
	}
	r.timer = time.AfterFunc(delay, func() {
		select {
		case r.actions <- roomAction{kind: akTimeout, seat: actor, gen: gen}:
		default:
		}
	})
}

// handleInput یک ورودی معتبر از بازیکن را اعمال می‌کند.
func (r *Room) handleInput(act roomAction) {
	s := r.seats[act.seat]
	if s.client == nil || s.client != act.client {
		return // فرستنده دیگر مالک این جایگاه نیست (کهنه)
	}
	switch act.msg.Type {
	case "leave":
		r.handleLeave(act.seat)
	case "chat":
		r.handleChat(act.seat, act.msg.Text, act.msg.Emoji)
	case "choose_trump":
		suit, err := game.ParseSuit(act.msg.Suit)
		if err != nil {
			r.sendError(act.seat, "invalid suit")
			return
		}
		if err := r.game.ChooseTrump(act.seat, suit); err != nil {
			r.sendError(act.seat, err.Error())
			return
		}
		r.graceUntil = time.Now().Add(r.dealDelay) // انیمیشنِ پخشِ بقیه‌ی کارت‌ها
		r.react()
	case "reshuffle":
		// بُرِ مجدد با مصرفِ یک بلیت: فقط حاکم و فقط پیش از انتخابِ حکم.
		if r.game.Phase != game.PhaseChooseTrump || act.seat != r.game.Hakem {
			r.sendError(act.seat, "بُر مجدد مجاز نیست")
			return
		}
		if s.isBot || s.userID == 0 {
			return
		}
		if err := r.settler.ChargeTicket(s.userID); err != nil {
			r.sendError(act.seat, "بلیت کافی ندارید")
			return
		}
		if err := r.game.Reshuffle(act.seat); err != nil {
			r.sendError(act.seat, err.Error())
			return
		}
		r.graceUntil = time.Now().Add(r.firstHandDelay) // انیمیشنِ بُر زدنِ مجدد
		r.react()
	case "play_card":
		card, err := game.ParseCard(act.msg.Card)
		if err != nil {
			r.sendError(act.seat, "invalid card")
			return
		}
		if err := r.playCard(act.seat, card); err != nil {
			r.sendError(act.seat, err.Error())
			return
		}
		r.react()
	}
}

// autoMove حرکت خودکار برای جایگاه (بات/قطع‌شده/AFK) را انجام می‌دهد.
func (r *Room) autoMove(seat int) {
	switch r.game.Phase {
	case game.PhaseChooseTrump:
		if seat != r.game.Hakem {
			return
		}
		r.autoChooseTrump(seat)
		r.react()
	case game.PhasePlaying:
		if seat != r.game.Turn {
			return
		}
		r.autoPlayCard(seat)
		r.react()
	}
}

// autoChooseTrump حکم را با راهبردِ باتِ هوشمند (طول + قدرتِ خال) انتخاب می‌کند.
func (r *Room) autoChooseTrump(seat int) {
	hand := r.game.Hand(seat)
	if len(hand) == 0 {
		return
	}
	_ = r.game.ChooseTrump(seat, botChooseTrump(hand))
	r.graceUntil = time.Now().Add(r.dealDelay) // انیمیشنِ پخشِ بقیه‌ی کارت‌ها
}

// autoPlayCard با هوشِ باتِ اکتشافی یک کارتِ مجاز و راهبردی بازی می‌کند.
func (r *Room) autoPlayCard(seat int) {
	hand := r.game.Hand(seat)
	if len(hand) == 0 {
		return
	}
	choice := botPlay(hand, r.game.Trick, r.game.Trump, r.game.LeadSuit)
	_ = r.playCard(seat, choice)
}

// playCard کارت را بازی کرده و در صورت بُرِش با حکم، آن را برای آمار ثبت می‌کند.
func (r *Room) playCard(seat int, card game.Card) error {
	wasFirst := len(r.game.Trick) == 0
	if err := r.game.PlayCard(seat, card); err != nil {
		return err
	}
	// بُرِش: کارتِ غیرِاولِ دست، از خالِ حکم، در حالی که خالِ زمینه حکم نبوده
	// ⇒ بازیکن خالِ زمینه را نداشته و با حکم بریده است.
	if !wasFirst && card.Suit == r.game.Trump && r.game.LeadSuit != r.game.Trump {
		if s := r.seats[seat]; !s.isBot && s.userID != 0 {
			r.settler.RecordCut(s.userID)
		}
	}
	return nil
}

// recordHandStats آمار هر بازیکنِ انسان را برای هَندِ پایان‌یافته ثبت می‌کند.
func (r *Room) recordHandStats(res *game.HandResult) {
	if res == nil {
		return
	}
	for seat, s := range r.seats {
		if s.isBot || s.userID == 0 {
			continue
		}
		won := game.Team(seat) == res.WinnerTeam
		r.settler.RecordHand(s.userID, won, res.Kot, seat == res.Hakem)
	}
}

// handleLeave خروج دائمی یک بازیکن؛ جایگاهش به بات تبدیل می‌شود.
func (r *Room) handleLeave(seat int) {
	s := r.seats[seat]
	s.isBot = true
	s.connected = false
	s.left = true
	s.client = nil
	r.broadcast(mustJSON(playerLeftMsg{Type: "player_left", Seat: seat}))
	if r.connectedHumans() == 0 {
		r.refundAll()
		r.endRoom()
		return
	}
	r.broadcastState()
	r.armTimer()
}

// handleDisconnect قطع موقت یک بازیکن؛ جایگاه برای بازگشت باز می‌ماند و خودکار بازی می‌شود.
func (r *Room) handleDisconnect(seat int, client *Client) {
	s := r.seats[seat]
	if s.client != client {
		return // قبلاً با کلاینت جدید جایگزین شده (کهنه)
	}
	s.connected = false
	s.client = nil
	r.broadcast(mustJSON(playerStatusMsg{Type: "player_disconnected", Seat: seat}))
	if r.connectedHumans() == 0 {
		r.refundAll()
		r.endRoom()
		return
	}
	r.broadcastState()
	r.armTimer()
}

// handleReconnect اتصال مجدد یک بازیکن به جایگاه قبلی.
func (r *Room) handleReconnect(seat int, client *Client) {
	s := r.seats[seat]
	s.client = client
	s.connected = true
	r.broadcast(mustJSON(playerStatusMsg{Type: "player_reconnected", Seat: seat}))
	// وضعیت کامل را برای بازیکن بازگشته بفرست
	client.trySend(mustJSON(r.matchedFor(seat)))
	client.trySend(mustJSON(buildState(r.game, r.ID, seat, r.playersInfo(), int(r.turnTimeout.Milliseconds()))))
	r.armTimer() // مهلت نوبت برای انسانِ بازگشته دوباره بلند می‌شود
}

// endRoom میز را بسته و فهرست انسان‌ها را برای پاک‌سازی به هاب می‌دهد.
func (r *Room) endRoom() {
	if r.ended {
		return
	}
	r.ended = true
	var ids []int64
	var clients []*Client
	for _, s := range r.seats {
		if s.isBot {
			continue
		}
		ids = append(ids, s.userID)
		if s.client != nil {
			clients = append(clients, s.client)
		}
	}
	r.hub.endRoom <- endInfo{room: r, humanIDs: ids, clients: clients}
}

// settleGameOver جایزه را به برندگانِ انسان می‌دهد و بازی را ثبت می‌کند.
func (r *Room) settleGameOver(res *game.HandResult) {
	// برندگانِ انسان (برای تقسیمِ potِ میزِ خصوصی).
	var winners []int64
	for seat, s := range r.seats {
		if s.isBot || s.userID == 0 {
			continue
		}
		won := game.Team(seat) == res.WinnerTeam
		if won {
			winners = append(winners, s.userID)
			if !r.private {
				r.settler.AwardWinner(s.userID, r.tier)
			}
		}
		if !r.private {
			r.settler.AwardRank(s.userID, r.tier, won) // امتیازِ رتبه (اگر میزِ رتبه‌بندی باشد)
		}
		r.settler.RecordGameResult(s.userID, won)
	}
	// تقسیمِ potِ شرطِ خصوصی بینِ برندگانِ انسان (باقی‌مانده به نفرِ اول).
	if r.private && r.pot > 0 && len(winners) > 0 {
		share := r.pot / int64(len(winners))
		rem := r.pot - share*int64(len(winners))
		for i, id := range winners {
			amt := share
			if i == 0 {
				amt += rem
			}
			r.settler.AwardCoins(id, amt, r.ID)
		}
		r.pot = 0
	}
	r.settler.RecordGame(r.ID, r.playersJSON(res.WinnerTeam), res.WinnerTeam, res.Kot)
}

// refundAll ورودی را به بازیکنانِ انسان که داوطلبانه خارج نشده‌اند بازمی‌گرداند.
func (r *Room) refundAll() {
	if r.private {
		// میزِ خصوصی: فقط شرطِ سکه‌ایِ پرداخت‌شده بازمی‌گردد (اگر شرطی بوده).
		if r.stake > 0 {
			for seat, s := range r.seats {
				if s.isBot || s.left || s.userID == 0 || !r.paid[seat] {
					continue
				}
				r.settler.AwardCoins(s.userID, r.stake, r.ID+":refund")
				r.paid[seat] = false
			}
			r.pot = 0
		}
		return
	}
	for _, s := range r.seats {
		if s.isBot || s.left || s.userID == 0 {
			continue
		}
		r.settler.Refund(s.userID, r.tier)
	}
}

// playersJSON خلاصه‌ی بازیکنان را برای ثبت تاریخچه می‌سازد.
func (r *Room) playersJSON(winnerTeam int) string {
	type rec struct {
		Seat int    `json:"seat"`
		Name string `json:"name"`
		Bot  bool   `json:"bot"`
		Won  bool   `json:"won"`
	}
	out := make([]rec, 4)
	for i, s := range r.seats {
		out[i] = rec{Seat: i, Name: s.name, Bot: s.isBot, Won: game.Team(i) == winnerTeam}
	}
	return string(mustJSON(out))
}

func (r *Room) connectedHumans() int {
	n := 0
	for _, s := range r.seats {
		if !s.isBot && s.connected && s.client != nil {
			n++
		}
	}
	return n
}

func (r *Room) playersInfo() []PlayerInfo {
	out := make([]PlayerInfo, 4)
	for i, s := range r.seats {
		out[i] = PlayerInfo{
			Seat: i, Name: s.name, Bot: s.isBot,
			Connected: s.isBot || s.connected,
			Coins:     s.coins, RankTier: s.rankTier, Avatar: s.avatar,
		}
	}
	return out
}

// broadcastState نمای اختصاصی هر بازیکنِ انسانِ متصل را برای او ارسال می‌کند.
func (r *Room) broadcastState() {
	players := r.playersInfo()
	for seat, s := range r.seats {
		if s.client != nil {
			s.client.trySend(mustJSON(buildState(r.game, r.ID, seat, players, int(r.turnTimeout.Milliseconds()))))
		}
	}
}

// broadcast یک پیام یکسان را برای همه بازیکنانِ انسانِ متصل ارسال می‌کند.
func (r *Room) broadcast(b []byte) {
	for _, s := range r.seats {
		if s.client != nil {
			s.client.trySend(b)
		}
	}
}

// handleChat یک پیام/شکلکِ بازیکن را با کنترلِ نرخ (ضدِ اسپم) و طولِ مجاز برای
// همه‌ی میز پخش می‌کند تا کنارِ آواتارِ فرستنده نمایش داده شود.
func (r *Room) handleChat(seat int, text, emoji string) {
	now := time.Now()
	if now.Sub(r.chatAt[seat]) < 1200*time.Millisecond {
		return // فاصله‌ی کمتر از ~۱.۲ ثانیه ⇒ نادیده (ضدِ اسپم)
	}
	clip := func(s string, n int) string {
		r := []rune(s)
		if len(r) > n {
			return string(r[:n])
		}
		return s
	}
	text, emoji = clip(text, 60), clip(emoji, 16)
	if text == "" && emoji == "" {
		return
	}
	r.chatAt[seat] = now
	r.broadcast(mustJSON(chatMsg{Type: "chat", Seat: seat, Text: text, Emoji: emoji}))
}

func (r *Room) sendError(seat int, msg string) {
	if c := r.seats[seat].client; c != nil {
		c.trySend(mustJSON(errorMsg{Type: "error", Message: msg}))
	}
}
