/*
 * Bot Babayev Farhad təfərindən 2020 ci ildə yaradılmışdır.
 * Copyright (C) 2019  Viktor
 *
 */

package main

import (
	"fmt"
	"html"
	"io/ioutil"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/go-redsync/redsync"
	"github.com/gomodule/redigo/redis"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	tb "gopkg.in/tucnak/telebot.v2"

	"github.com/nuetoban/crocodile-game-bot/crocodile"
	"github.com/nuetoban/crocodile-game-bot/model"
	"github.com/nuetoban/crocodile-game-bot/storage"
	"github.com/nuetoban/crocodile-game-bot/utils"
)

var (
	mutexFabric  *redsync.Redsync
	lockForLocks *sync.Mutex // ну а хули нам
	locks        map[int64]*sync.Mutex
	machines     map[int64]*crocodile.Machine
	fabric       *crocodile.MachineFabric
	bot          *tb.Bot
	redisPool    *redis.Pool

	textUpdatesRecieved float64
	startTotal          float64
	ratingTotal         float64
	globalRatingTotal   float64
	cstatTotal          float64
	chatsRatingTotal    float64

	updatesProcessed int

	wordsInlineKeys   [][]tb.InlineButton
	newGameInlineKeys [][]tb.InlineButton
	ratingGetter      RatingGetter
	statisticsGetter  StatisticsGetter

	rateLimiter *RateLimiter

	DEBUG = false
)

func init() {
	loadEnv(".env")
	lockForLocks = &sync.Mutex{}
	locks = make(map[int64]*sync.Mutex)

	redisHost := os.Getenv("REDIS_HOST")
	if redisHost == "" {
		redisHost = ":6379"
	}
	redisPool = newPool(redisHost)
	mutexFabric = redsync.New([]redsync.Pool{redisPool})
	cleanupHook()
}

// https://github.com/pete911/examples-redigo
func newPool(server string) *redis.Pool {
	return &redis.Pool{
		MaxIdle:     200,
		MaxActive:   10000,
		IdleTimeout: 240 * time.Second,
		Dial: func() (redis.Conn, error) {
			c, err := redis.Dial("tcp", server)
			if err != nil {
				panic(err)
			}
			return c, err
		},

		TestOnBorrow: func(c redis.Conn, t time.Time) error {
			_, err := c.Do("PING")
			if err != nil {
				panic(err)
			}
			return err
		},
	}
}

// https://github.com/pete911/examples-redigo
func cleanupHook() {
	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt)
	signal.Notify(c, syscall.SIGTERM)
	signal.Notify(c, syscall.SIGKILL)
	go func() {
		<-c
		redisPool.Close()
		os.Exit(0)
	}()
}

type RatingGetter interface {
	GetRating(chatID int64) ([]model.UserInChat, error)
	GetGlobalRating() ([]model.UserInChat, error)
	GetChatsRating() ([]model.ChatStatistics, error)
	GetUserStats(userID int, chatID int64) (model.UserInChat, model.UserInChat, error)
	ResetStats(chatID int64) error
}

type StatisticsGetter interface {
	GetStatistics() (model.Statistics, error)
}

type dbCredentials struct {
	Host,
	User,
	Pass,
	Name string

	Port int
	KW   storage.KW
}

func loggerMiddlewarePoller(upd *tb.Update) bool {
	if upd.Message != nil && upd.Message.Chat != nil && upd.Message.Sender != nil {
		log.Debugf(
			"Received update, chat: %d, chatTitle: \"%s\", user: %d",
			upd.Message.Chat.ID,
			upd.Message.Chat.Title,
			upd.Message.Sender.ID,
		)
	}
	updatesProcessed++
	return true
}

func getDbCredentialsFromEnv() (dbCredentials, error) {
	prefix := "CROCODILE_GAME_DB_"
	ret := dbCredentials{}
	ret.KW = storage.KW{}
	envList := os.Environ()
	env := make(map[string]string)
	for _, v := range envList {
		kv := strings.Split(v, "=")
		env[kv[0]] = kv[1]
	}
	var err error

	ret.Port, err = strconv.Atoi(env[prefix+"PORT"])
	if err != nil {
		return ret, err
	}
	delete(env, prefix+"PORT")

	ret.Host = env[prefix+"HOST"]
	ret.User = env[prefix+"USER"]
	ret.Pass = env[prefix+"PASS"]
	ret.Name = env[prefix+"NAME"]
	delete(env, prefix+"HOST")
	delete(env, prefix+"USER")
	delete(env, prefix+"PASS")
	delete(env, prefix+"NAME")

	for k, v := range env {
		if strings.HasPrefix(k, prefix) {
			ret.KW[strings.ToLower(strings.TrimPrefix(k, prefix))] = v
		}
	}

	return ret, nil
}

func main() {
	logInit()
	if os.Getenv("CROCODILE_GAME_DEV") != "" {
		DEBUG = true
		setLogLevel("TRACE")
	}

	if os.Getenv("CROCODILE_GAME_LOGLEVEL") != "" {
		setLogLevel(os.Getenv("CROCODILE_GAME_LOGLEVEL"))
	}

	log.Info("Loading words")
	f, err := os.Open("dictionaries/az.txt")
	if err != nil {
		log.Fatalf("Cannot open dictionary: %v", err)
	}
	wordsProvider, _ := crocodile.NewWordsProviderReader(f)

	log.Info("Readind DB env variables")
	creds, err := getDbCredentialsFromEnv()
	if err != nil {
		log.Fatalf("Cannot get database credentials from ENV: %v", err)
	}

	log.Info("Connecting to the database")
	pg, err := storage.NewStorage(storage.NewConnString(
		creds.Host, creds.User,
		creds.Pass, creds.Name,
		creds.Port, creds.KW,
	), redisPool, storage.WrapLogrus(log))
	if err != nil {
		log.Fatalf("Cannot connect to database (%s, %s) on host %s: %v", creds.User, creds.Name, creds.Host, err)
	}

	ratingGetter = pg
	statisticsGetter = pg

	log.Info("Creating games fabric")
	fabric = crocodile.NewMachineFabric(pg, wordsProvider, log)
	machines = make(map[int64]*crocodile.Machine)

	rateLimiter = NewRateLimiter(redisPool)

	log.Info("Connecting to Telegram API")
	var poller tb.Poller
	if os.Getenv("CROCODILE_GAME_WEBHOOK") != "" {
		poller = &tb.Webhook{
			Endpoint: &tb.WebhookEndpoint{
				PublicURL: os.Getenv("CROCODILE_GAME_WEBHOOK"),
			},
			Listen: "0.0.0.0:9999",
		}
	} else {
		poller = &tb.LongPoller{Timeout: 5 * time.Second}
	}

	mp := tb.NewMiddlewarePoller(poller, loggerMiddlewarePoller)
	mp.Capacity = 10000

	settings := tb.Settings{
		Token:   os.Getenv("CROCODILE_GAME_BOT_TOKEN"),
		Poller:  mp,
		Updates: 10000,
	}
	bot, err = tb.NewBot(settings)
	if err != nil {
		log.Fatalf("Cannot connect to Telegram API: %v", err)
	}
	// pg.SetBotID(bot.Me.ID)

	log.Info("Binding handlers")
	bot.Handle(tb.OnText, logDuration(mustLock(textHandler)))
	bot.Handle("/start", logDuration(mustLock(startNewGameHandler)))
	bot.Handle("/game", logDuration(mustLock(startNewGameHandler)))
	bot.Handle("/cancel", logDuration(mustLock(cancelHandler)))
	bot.Handle("/stop", logDuration(mustLock(cancelHandler)))
	bot.Handle("/rating", logDuration(ratingHandler))
	bot.Handle("/globalrating", logDuration(globalRatingHandler))
	bot.Handle("/bstat", logDuration(statsHandler))
	bot.Handle("/chatrating", logDuration(chatsRatingHandler))
	bot.Handle("/rules", logDuration(rulesHandler))
	bot.Handle("/info", logDuration(infoHandler))
	bot.Handle("/help", logDuration(helpHandler))
	bot.Handle("/mystats", logDuration(myStatsHandler))
	bot.Handle("/resetstats", logDuration(resetStatsHandler))
	bindButtonsHandlers(bot)

	collector := newMetricsCollector(pg)
	prometheus.MustRegister(collector)

	http.Handle("/metrics", promhttp.Handler())

	log.Info("Starting metrics exporter server")
	go http.ListenAndServe(":8080", nil)

	go func() {
		var err error
		for {
			time.Sleep(time.Second * 15)

			if updatesProcessed == 0 {
				err = ioutil.WriteFile("/tmp/crocostatus", []byte("BAD"), 0644)
			} else {
				err = ioutil.WriteFile("/tmp/crocostatus", []byte("GOOD"), 0644)
			}

			updatesProcessed = 0
			if err != nil {
				panic(err)
			}
		}
	}()

	log.Info("Starting the bot")
	bot.Start()
}

// Decorator for logging duration of function execution
func logDuration(f func(*tb.Message)) func(*tb.Message) {
	return func(m *tb.Message) {
		start := time.Now()
		f(m)
		diff := time.Now().Sub(start)
		if diff.Seconds() > 1 {
			log.Warnf("Took %s time to complete update processing", time.Time{}.Add(diff).Format("04:05.000"))
		} else {
			log.Tracef("Took %s time to complete update processing", time.Time{}.Add(diff).Format("04:05.000"))
		}
	}
}

// Decorator for logging duration of function execution (callback handlers)
func logDurationCallback(f func(*tb.Callback)) func(*tb.Callback) {
	return func(c *tb.Callback) {
		start := time.Now()
		f(c)
		diff := time.Now().Sub(start)
		if diff.Seconds() > 1 {
			log.Warnf("Took %s time to complete update processing (cb)", time.Time{}.Add(diff).Format("04:05.000"))
		} else {
			log.Tracef("Took %s time to complete update processing (cb)", time.Time{}.Add(diff).Format("04:05.000"))
		}
	}
}

// Decorator for distributed lock for chat (messages handlers)
func mustLock(f func(*tb.Message)) func(*tb.Message) {
	return func(m *tb.Message) {
		go func() {
			m := m
			log.Tracef("Locking chat %d", m.Chat.ID)
			lockChat(m.Chat.ID)

			f(m)

			log.Tracef("Unlocking chat %d", m.Chat.ID)
			unlockChat(m.Chat.ID)
		}()
	}
}

// Decorator for distributed lock for chat (callback handlers)
func mustLockCallback(f func(*tb.Callback)) func(*tb.Callback) {
	return func(c *tb.Callback) {
		go func() {
			c := c
			log.Tracef("Locking chat %d", c.Message.Chat.ID)
			lockChat(c.Message.Chat.ID)

			f(c)

			log.Tracef("Unlocking chat %d", c.Message.Chat.ID)
			unlockChat(c.Message.Chat.ID)
		}()
	}
}

func globalRatingHandler(m *tb.Message) {
	globalRatingTotal++
	rating, err := ratingGetter.GetGlobalRating()
	if err != nil {
		log.Errorf("globalRatingHandler: cannot get rating %v:", err)
		return
	}

	ratingString := buildRating("TOP-25 <b>oyuncularr</b> Tüm gruplarda 💡", rating)

	err = sendMessage(m.Chat, m.Chat.ID, ratingString)
	if err != nil {
		log.Errorf("globalRatingHandler: cannot send rating: %v", err)
	}
}

func buildRating(header string, data []model.UserInChat) string {
	if len(data) < 1 {
		return "Derecelendirme henüz belirlenmedi!"
	}

	out := header + "\n\n"
	for k, v := range data {
		out += fmt.Sprintf(
			"<b>%d</b>. %s — %d %s.\n",
			k+1,
			html.EscapeString(v.Name),
			v.Guessed,
			utils.DetectCaseAnswers(v.Guessed),
		)
	}

	return out
}

func buildRatingChatStatistics(header string, data []model.ChatStatistics) string {
	if len(data) < 1 {
		return "Derecelendirme henüz belirlenmedi!"
	}

	out := header + "\n\n"
	for k, v := range data {
		out += fmt.Sprintf(
			"<b>%d</b>. %s — %d %s.\n",
			k+1,
			html.EscapeString(v.Title),
			v.Guessed,
			utils.DetectCaseForGames(v.Guessed),
		)
	}

	return out
}

func ratingHandler(m *tb.Message) {
	ratingTotal++
	rating, err := ratingGetter.GetRating(m.Chat.ID)
	if err != nil {
		log.Errorf("ratingHandler: cannot get rating %v:", err)
		return
	}

	ratingString := buildRating("TOP-25 <b>oyuncular</b> 💡", rating)

	err = sendMessage(m.Chat, m.Chat.ID, ratingString)
	if err != nil {
		log.Errorf("ratingHandler: cannot send rating: %v", err)
	}
}

func sendMessage(s tb.Recipient, chatID int64, text string) error {
	err := rateLimiter.Limit(chatID,
		func() error { _, err := bot.Send(s, text, tb.ModeHTML, tb.NoPreview); return err },
		func() error {
			_, err := bot.Send(s, "FlodWait denemesi saptandı. 15 Saniye engel. ")
			return err
		},
		func() error { return nil })
	return err
}

func statsHandler(m *tb.Message) {
	cstatTotal++
	stats, err := statisticsGetter.GetStatistics()
	if err != nil {
		log.Errorf("statsHandler: cannot get stats %v:", err)
		return
	}

	outString := "<b>♻️ Botun istastikleri: Sürüm- geliştirme </b> \n\n"
	outString += fmt.Sprintf("Grup sayısı: %d\n", stats.Chats)
	outString += fmt.Sprintf("Oyuncu sayısı: %d\n", stats.Users)
	outString += fmt.Sprintf("Oyun sayısı: %d\n", stats.GamesPlayed)

	err = sendMessage(m.Chat, m.Chat.ID, outString)
	if err != nil {
		log.Errorf("statsHandler: cannot send stats: %v", err)
	}
}

func lockChat(chatID int64) error {
	lockForLocks.Lock()
	defer lockForLocks.Unlock()
	if _, ok := locks[chatID]; ok {
		if locks[chatID] != nil {
			locks[chatID].Lock()
		} else {
			locks[chatID] = &sync.Mutex{}
			locks[chatID].Lock()
		}
	} else {
		locks[chatID] = &sync.Mutex{}
		locks[chatID].Lock()
	}
	return nil
}

func unlockChat(chatID int64) {
	// mutexFabric.NewMutex("mutex/" + strconv.Itoa(int(chatID))).Unlock()
	locks[chatID].Unlock()
}

func startNewGameHandler(m *tb.Message) {
	if m.Private() {
		menu := &tb.ReplyMarkup{ResizeReplyKeyboard: true}
		r := &tb.ReplyMarkup{}
		menu.Inline(
			menu.Row(r.URL("🤖 Botu Grubuna Davet Et", "https://t.me/TubidyKelimeOyunuBot?startgroup=a")),
			menu.Row(r.URL("🎮 Oyun Grubumuz", "https://t.me/keyfialemsohbet")),
			menu.Row(r.URL("📣 Bot Destek Kanalı", "https://t.me/Tubidybotdestek")),
			menu.Row(r.URL("🎶 Paylaşım kanalı", "https://t.me/keyfialemkanal")),
			menu.Row(r.URL("Yapımcı", "https://t.me/dnztrmn")),
			
		)

		bot.Send(m.Sender, "👋🏻 Merhaba, Ben gruplarınızda eğlenceli vakit geçirmeniz için tasarlanmış bir kelime botuyum beni grubunuza ekleyerek keyifli vakit geçirebilirsiniz.", tb.ModeHTML, tb.NoPreview, menu)
		bot.Send(m.Sender, "Bot hakkında destek almanız icin /help komutunu kullanabilirsiniz ", tb.ModeHTML, tb.NoPreview)
		return
	}

	startTotal++

	machine := fabric.NewMachine(m.Chat.ID, m.ID)

	username := strings.TrimSpace(m.Sender.FirstName + " " + m.Sender.LastName)

	_, err := machine.StartNewGameAndReturnWord(m.Sender.ID, username, m.Chat.Title)

	if err != nil {
		if err.Error() == crocodile.ErrGameAlreadyStarted {
			_, ms, _ := utils.CalculateTimeDiff(time.Now(), machine.GetStartedTime())

			if ms < 2 {
				sendMessage(m.Chat, m.Chat.ID, "Oyun çoktan başladî :) ")
				return
			} else {
				machine.StopGame()
				_, err = machine.StartNewGameAndReturnWord(m.Sender.ID, username, m.Chat.Title)
				if err != nil {
					log.Println(err)
				}
			}
		} else {
			log.Println(err)
			return
		}
	}

	bot.Send(
		m.Chat,
		fmt.Sprintf(
			`🎄 <a href="tg://user?id=%d">%s</a> Kelimeyi anlatıyor! ❄️`,
			m.Sender.ID, html.EscapeString(m.Sender.FirstName)),
		tb.ModeHTML,
		&tb.ReplyMarkup{InlineKeyboard: wordsInlineKeys},
	)
}

func startNewGameHandlerCallback(c *tb.Callback) {
	m := c.Message

	// If machine for this chat has been created already
	ma := fabric.NewMachine(m.Chat.ID, m.ID)

	username := strings.TrimSpace(c.Sender.FirstName + " " + c.Sender.LastName)
	_, err := ma.StartNewGameAndReturnWord(c.Sender.ID, username, m.Chat.Title)

	if err != nil {
		if err.Error() == crocodile.ErrGameAlreadyStarted {
			_, ms, _ := utils.CalculateTimeDiff(time.Now(), ma.GetStartedTime())

			if ms < 2 {
				bot.Respond(c, &tb.CallbackResponse{Text: "oyun çoktan başladı :)"})
				return
			} else {
				ma.StopGame()
				_, err = ma.StartNewGameAndReturnWord(c.Sender.ID, username, m.Chat.Title)
				if err != nil {
					log.Println(err)
				}
				bot.Respond(c, &tb.CallbackResponse{
					Text:      fmt.Sprintf("🌬 Sen — sunuyorsun, işte kelimen — %s", ma.GetWord()),
					ShowAlert: true,
				})
			}
		} else if err.Error() == crocodile.ErrWaitingForWinnerRespond {
			bot.Respond(c, &tb.CallbackResponse{Text: "Kelimeyi bulan oyuncunun 5 Saniyesi var! ❄️"})
			return
		} else {
			log.Println(err)
			bot.Respond(c, &tb.CallbackResponse{Text: "."})
			return
		}
	}

	bot.Respond(c, &tb.CallbackResponse{
		Text:      fmt.Sprintf("❄️ Sen — sunucusun, işte kelimen — %s", ma.GetWord()),
		ShowAlert: true,
	})
	bot.Send(
		m.Chat,
		fmt.Sprintf(
			`🎄 <b> <a href="tg://user?id=%d">%s</a> Kelimeyi anlatıyor! ❄️</b>.
			🇹🇷 Sohbet ve oyun grubumuz - @keyfialemsohbet`,
			c.Sender.ID, html.EscapeString(c.Sender.FirstName)),
		tb.ModeHTML,
		&tb.ReplyMarkup{InlineKeyboard: wordsInlineKeys},
	)
}

func textHandler(m *tb.Message) {
	textUpdatesRecieved++

	ma := fabric.NewMachine(m.Chat.ID, m.ID)

	if ma.GetHost() != m.Sender.ID || DEBUG {
		username := strings.TrimSpace(m.Sender.FirstName + " " + m.Sender.LastName)
		if word, ok := ma.CheckWordAndSetWinner(m.Text, m.Sender.ID, username); ok {
			bot.Send(
				m.Chat,
				fmt.Sprintf(
					"%s Kelimeyi buldu! <b>%s</b> 🌲",
					username, word,
				),
				tb.ModeHTML,
				&tb.ReplyMarkup{InlineKeyboard: newGameInlineKeys},
			)
		}
	}
}

func seeWordCallbackHandler(c *tb.Callback) {
	m := fabric.NewMachine(c.Message.Chat.ID, c.Message.ID)
	var message string

	if c.Sender.ID != m.GetHost() {
		message = "Bu kelime senin için değil! 🌨"
	} else {
		message = m.GetWord()
	}

	bot.Respond(c, &tb.CallbackResponse{Text: message, ShowAlert: true})
}

func nextWordCallbackHandler(c *tb.Callback) {
	m := fabric.NewMachine(c.Message.Chat.ID, c.Message.ID)
	var message string
	var err error

	if c.Sender.ID != m.GetHost() {
		message = "Bu kelime senin için değil! 🌨"
	} else {
		message, err = m.SetNewRandomWord()
		if err != nil {
			log.Errorf("nextWordCallbackHandler: cannot get word: %v", err)
			bot.Respond(c, &tb.CallbackResponse{Text: message, ShowAlert: true})
			return
		}
	}

	bot.Respond(c, &tb.CallbackResponse{Text: message, ShowAlert: true})
}

func returnCallbackHandler(c *tb.Callback) {
	ma := fabric.NewMachine(c.Message.Chat.ID, c.Message.ID)
	var message string
	m := c.Message

	if c.Sender.ID != ma.GetHost() {
		message = "Bu kelime senin için değil! ❌"
	} else {
		bot.Send(
			m.Chat,
			fmt.Sprintf("%s Sunucu olmak istemiyor!", c.Sender.FirstName),
			&tb.ReplyMarkup{InlineKeyboard: newGameInlineKeys},
		)
		ma.StopGame()
	}

	bot.Respond(c, &tb.CallbackResponse{Text: message, ShowAlert: true})
}

func bindButtonsHandlers(bot *tb.Bot) {
	seeWord := tb.InlineButton{Unique: "see_word", Text: "Kelimeye Bak 📜"}
	nextWord := tb.InlineButton{Unique: "next_word", Text: "Sonraki kelime ➡️"}
	ret := tb.InlineButton{Unique: "ret_game", Text: "Sunucu olmak istemiyorum ⛔️"}
	newGame := tb.InlineButton{Unique: "new_game", Text: "Sunucu olmak istiyorum! 🙋🏻‍♂️"}

	wordsInlineKeys = [][]tb.InlineButton{{seeWord}, {nextWord}, {ret}}
	newGameInlineKeys = [][]tb.InlineButton{{newGame}}

	bot.Handle(&newGame, logDurationCallback(mustLockCallback(startNewGameHandlerCallback)))
	bot.Handle(&seeWord, logDurationCallback(mustLockCallback(seeWordCallbackHandler)))
	bot.Handle(&nextWord, logDurationCallback(mustLockCallback(nextWordCallbackHandler)))
	bot.Handle(&ret, logDurationCallback(mustLockCallback(returnCallbackHandler)))
}

func rulesHandler(m *tb.Message) {
	sendMessage(m.Chat, m.Chat.ID, `
<b>Oyunun kuralları! 📒 </b>

Oyun 2 rolden ibarettir Sunucu (kelimeyi anlatır) ve Oyuncu (kelimeyi tahmin etmeye çalışır).

 /start komutu ilen verilen kelimeyi — kelimeyi söylemeden - anlatır ve oyuncular tahmin etmeye çalışırlar anlatılamayan veyahut bilinmeyen bir kelime olursa değiştirilebilir.
Resmi sohbet ve oyun grubumuza bekleriz @keyfialemsohbet

Bot ile alakalı problemleriniz ve önerilerinizi bu hesaptan iletebilirsiniz. - @dnztrmn
`)
}

func infoHandler(m *tb.Message) {
	sendMessage(m.Chat, m.Chat.ID, `
<b>Düymələr və bot haqqında məlumat!</b>

• /start - Yeni oyun başlatmak
• /cancel - Oyunu sonlandırmaq
• /help - yardım
• /info - Bot hakkında bilgi
• /rating - Grup içi TOP-25 oyuncar
• /globalrating - Bütün gruplarda TOP-25 oyuncular
• /rules - Oyunun kuralları
• /chatrating - TOP-25 gruplar
`)
}

func helpHandler(m *tb.Message) {
	sendMessage(m.Chat, m.Chat.ID, `
<b>✅Qruplar/Grublar/Groups/Группы:
🇹🇷 - @keyfialemsohbet
👨 - @Dnztrmn
📣 - @tubidybotdestek

🇦🇿 Botu öz qrupuna əlavə et: https://t.me/CrocodileGameAz_bot?startgroup=a
🇺🇸 Add bot to chat: https://t.me/CrocodileGameEn_bot?startgroup=a
🇹🇷 Botu grubuna ekle: https://t.me/CrocodileGameTR_bot?startgroup=a
🇷🇺 Добавить бота в группу: https://t.me/CrocodileGameRU_bot?startgroup=a</b>
`)
}

func chatsRatingHandler(m *tb.Message) {
	chatsRatingTotal++
	rating, err := ratingGetter.GetChatsRating()
	if err != nil {
		log.Errorf("chatsRatingHandler: cannot get rating %v:", err)
		return
	}

	ratingString := buildRatingChatStatistics("TOP 25 <b>qruplar</b>💡", rating)

	err = sendMessage(m.Chat, m.Chat.ID, ratingString)
	if err != nil {
		log.Errorf("chatsRatingHandler: cannot send rating: %v", err)
	}
}

func cancelHandler(m *tb.Message) {
	// Get chat member info
	chatMember, err := bot.ChatMemberOf(m.Chat, m.Sender)
	if err != nil {
		log.Errorf("cancelHandler error: %v", err)
		return
	}

	// Get game
	ma := fabric.NewMachine(m.Chat.ID, m.ID)

	// Check role
	if !(chatMember.Role == tb.Creator || chatMember.Role == tb.Administrator || ma.GetHost() == m.Sender.ID) {
		log.Debugf("cancelHandler: this user cannot cancel the game")
		err = sendMessage(m.Chat, m.Chat.ID, "Bu komut, sunum yapan kişi veya yönetici için tasarlanmıştır.❌")
		if err != nil {
			log.Errorf("cancelHandler: cannot send notification: %v", err)
		}
		return
	}

	// Stop game
	err = ma.StopGame()
	if err != nil {
		log.Errorf("cancelHandler: cannot cancel game: %v", err)
		return
	}

	// Notify users
	err = sendMessage(m.Chat, m.Chat.ID, fmt.Sprintf("Oyun sonlandırıldı.🛑 /start@%s, düğmesine basarak yeni oyun başlatabilirsiniz.", "TubidyKelimeOyunuBot"))
	if err != nil {
		log.Errorf("cancelHandler: cannot send message: %v", err)
	}
}

func myStatsHandler(m *tb.Message) {
	forCurrentChat, forAllChats, err := ratingGetter.GetUserStats(m.Sender.ID, m.Chat.ID)
	if err != nil {
		log.Errorf("myStatsHandler: cannot get rating %v:", err)
		return
	}

	ratingString := strings.TrimSpace(fmt.Sprintf(`
 📈 <b>Oyuncunun skoru <a href="tg://user?id=%d">%s</a></b>

 <i>Bu çatda</i>
 Aparıcı olub: %d
 Başa salıb: %d
 Tapdığı sözlər: %d

 <i>Bütün çatlarda</i>
 Aparıcı olub: %d
 Başa salıb: %d
 Tapdığı sözlər: %d
	 `,
		m.Sender.ID, strings.TrimSpace(m.Sender.FirstName+" "+m.Sender.LastName),
		forCurrentChat.WasHost, forCurrentChat.Success, forCurrentChat.Guessed,
		forAllChats.WasHost, forAllChats.Success, forAllChats.Guessed,
	))

	err = sendMessage(m.Chat, m.Chat.ID, ratingString)
	if err != nil {
		log.Errorf("myStatsHandler: cannot send rating: %v", err)
	}
}

func resetStatsHandler(m *tb.Message) {
	// Get chat member info
	chatMember, err := bot.ChatMemberOf(m.Chat, m.Sender)
	if err != nil {
		log.Errorf("resetStatsHandler error: %v", err)
		return
	}

	// Check role
	if !(chatMember.Role == tb.Creator) {
		log.Debugf("cancelHandler: this user cannot cancel the game")
		err = sendMessage(m.Chat, m.Chat.ID, "Bu komut, sunum yapan kişi veya yönetici için tasarlanmıştır. ❌")
		if err != nil {
			log.Errorf("resetStatsHandler: cannot send notification: %v", err)
		}
		return
	}

	if err := ratingGetter.ResetStats(m.Chat.ID); err != nil {
		err = sendMessage(m.Chat, m.Chat.ID, "Bir hata oluştu")
		if err != nil {
			log.Errorf("resetStatsHandler: cannot send notification: %v", err)
		}
	}

	err = sendMessage(m.Chat, m.Chat.ID, "✅ Reytinq sıfırlandı.")
	if err != nil {
		log.Errorf("resetStatsHandler: cannot send notification: %v", err)
	}
}
