package main

import (
	"bufio"
	"encoding/json"
	"errors"
	"fmt"
	rl "github.com/gen2brain/raylib-go/raylib"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"
)

func errorContext(skip int) string {
	pc, file, line, ok := runtime.Caller(skip)
	if !ok {
		return "unknown"
	}

	fn := runtime.FuncForPC(pc).Name()
	return fmt.Sprintf("%s:%d %s", file, line, fn)
}

func APITraceError(format string, args ...interface{}) error {
	traceInfo := errorContext(2)
	return fmt.Errorf("[%s] "+format, append([]interface{}{traceInfo}, args...)...)
}

type HypixelPlayerAchievements struct {
	BedwarsStar int `json:"bedwars_level"`
}

type HypixelPlayerBedwarsStats struct {
	Experience   int     `json:"Experience"`
	Winstreak    int     `json:"winstreak"`
	Wins         int     `json:"wins_bedwars"`
	Losses       int     `json:"losses_bedwars"`
	GamesPlayed  int     `json:"games_played_bedwars"`
	WinLossRatio float32 // wins / gamesplayed

	Kills          int     `json:"kills_bedwars"`
	Deaths         int     `json:"deaths_bedwars"`
	KillDeathRatio float32 // kills / deaths

	FinalKills          int     `json:"final_kills_bedwars"`
	FinalDeaths         int     `json:"final_deaths_bedwars"`
	FinalKillDeathRatio float32 // final kills / final deaths

	BedsBroken          int     `json:"beds_broken_bedwars"`
	BedsLost            int     `json:"beds_lost_bedwars"`
	BedsBrokenLostRatio float32 // beds broken / beds lost
}

func (stats *HypixelPlayerBedwarsStats) CalculateRatios() {
	// WLR
	if stats.GamesPlayed > 0 {
		stats.WinLossRatio = float32(stats.Wins) / float32(stats.GamesPlayed)
	} else {
		stats.WinLossRatio = float32(stats.Wins)
	}

	// KDR
	if stats.Deaths > 0 {
		stats.KillDeathRatio = float32(stats.Kills) / float32(stats.Deaths)
	} else {
		stats.KillDeathRatio = float32(stats.Kills)
	}

	// FKDR
	if stats.FinalDeaths > 0 {
		stats.FinalKillDeathRatio = float32(stats.FinalKills) / float32(stats.FinalDeaths)
	} else {
		stats.FinalKillDeathRatio = float32(stats.FinalKills)
	}

	// BBLR
	if stats.BedsLost > 0 {
		stats.BedsBrokenLostRatio = float32(stats.BedsBroken) / float32(stats.BedsLost)
	} else {
		stats.BedsBrokenLostRatio = float32(stats.BedsBroken)
	}
}

func (stats *HypixelPlayerBedwarsStats) IsWinstreakDisabled() bool {
	return stats.Winstreak == -1
}

type HypixelPlayerStats struct {
	Bedwars HypixelPlayerBedwarsStats `json:"Bedwars"`
}

type HypixelPlayer struct {
	DisplayName      string                    `json:"displayname"`
	Achievements     HypixelPlayerAchievements `json:"achievements"`
	Stats            HypixelPlayerStats        `json:"stats"`
	MonthlyRankColor string                    `json:"monthlyRankColor"`
	RankPlusColor    string                    `json:"rankPlusColor"`
	NetworkExp       json.Number               `json:"networkExp"`
}

type HypixelPlayerResponse struct {
	Success bool          `json:"success"`
	Player  HypixelPlayer `json:"player"`
}

type HypixelAPIKey struct {
	Value         string
	Uses          int
	RateLimit     time.Duration
	LimitUsesLeft int
}

func NewHypixelAPIKey(key string) *HypixelAPIKey {
	return &HypixelAPIKey{
		Value:         key,
		Uses:          0,
		RateLimit:     time.Millisecond * 1,
		LimitUsesLeft: -1,
	}
}

type HypixelAPI struct {
	BaseURL string
	APIKey  HypixelAPIKey
}

func NewHypixelAPI(apiKey string) *HypixelAPI {
	// TODO: Ensure valid key before creating
	key := NewHypixelAPIKey(apiKey)
	return &HypixelAPI{
		BaseURL: "https://api.hypixel.net/v2/",
		APIKey:  *key,
	}
}

func (h *HypixelAPI) FetchPlayer(PlayerUUID string) (HypixelPlayerResponse, error) {
	// TODO: Handle nicked players
	if len(PlayerUUID) != 32 && len(PlayerUUID) != 36 {
		return HypixelPlayerResponse{}, fmt.Errorf("invalid Player UUID! please pass in a valid UUID (Not a name!)")
	}
	RequestUrl := h.BaseURL + "player?uuid=" + PlayerUUID

	client := &http.Client{
		Timeout: 10 * time.Second,
	}

	req, err := http.NewRequest("GET", RequestUrl, nil)
	if err != nil {
		return HypixelPlayerResponse{}, APITraceError("failed to create HTTP request: %v", err)
	}
	req.Header.Set("API-Key", h.APIKey.Value)

	resp, err := client.Do(req)
	if err != nil {
		return HypixelPlayerResponse{}, APITraceError("failed to send HTTP request: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return HypixelPlayerResponse{}, APITraceError("received non-200 response: %d", resp.StatusCode)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return HypixelPlayerResponse{}, APITraceError("failed to read response body: %v", err)
	}

	var playerResponse HypixelPlayerResponse
	err = json.Unmarshal(body, &playerResponse)
	if err != nil {
		return HypixelPlayerResponse{}, APITraceError("failed to unmarshal JSON: %v", err)
	}

	if !playerResponse.Success {
		return HypixelPlayerResponse{}, APITraceError("API returned unsuccessful response")
	}

	playerResponse.Player.Stats.Bedwars.CalculateRatios()

	return playerResponse, nil
}

func (h *HypixelAPI) FetchPlayerAsync(PlayerUUID string, resultChan chan<- HypixelPlayerResponse, errorChan chan<- error) {
	go func() {
		result, err := h.FetchPlayer(PlayerUUID)
		if err != nil {
			errorChan <- err
			return
		}
		resultChan <- result
	}()
}

func (h *HypixelAPI) FetchPlayersAsync(PlayerUUIDs []string, resultChan chan<- HypixelPlayerResponse, errorChan chan<- error) {
	var wg sync.WaitGroup

	for _, uuid := range PlayerUUIDs {
		wg.Add(1)

		go func(uuid string) {
			defer wg.Done()

			result, err := h.FetchPlayer(uuid)
			if err != nil {
				errorChan <- err
				return
			}

			resultChan <- result
		}(uuid)
	}

	go func() {
		wg.Wait()
		close(resultChan)
		close(errorChan)
	}()
}

type CachedUuid struct {
	CleanUuid   string
	Uuid        string
	PlayerName  string
	TimeFetched time.Time
}

type UUIDCache struct {
	mu            sync.RWMutex
	lifetimeLimit time.Duration
	uuidMap       map[string]*CachedUuid
	cleanUuidMap  map[string]*CachedUuid
	playerNameMap map[string]*CachedUuid
}

func NewUUIDCache(lifetime time.Duration) *UUIDCache {
	return &UUIDCache{
		lifetimeLimit: lifetime,
		uuidMap:       make(map[string]*CachedUuid),
		cleanUuidMap:  make(map[string]*CachedUuid),
		playerNameMap: make(map[string]*CachedUuid),
	}
}

func (c *UUIDCache) Add(cachedUuid *CachedUuid) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.uuidMap[cachedUuid.Uuid] = cachedUuid
	c.cleanUuidMap[cachedUuid.CleanUuid] = cachedUuid
	c.playerNameMap[cachedUuid.PlayerName] = cachedUuid
}

func (c *UUIDCache) GetByUuid(uuid string) (*CachedUuid, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	val, ok := c.uuidMap[uuid]
	return val, ok
}

func (c *UUIDCache) GetByCleanUuid(cleanUuid string) (*CachedUuid, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	val, ok := c.cleanUuidMap[cleanUuid]
	return val, ok
}

func (c *UUIDCache) GetByPlayerName(playerName string) (*CachedUuid, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	val, ok := c.playerNameMap[playerName]
	return val, ok

}

func (c *UUIDCache) Get(id string) (*CachedUuid, bool) {
	val, ok := c.GetByPlayerName(id)
	if ok {
		return val, ok
	}

	val, ok = c.GetByUuid(id)
	if ok {
		return val, ok
	}

	val, ok = c.GetByCleanUuid(id)
	return val, ok
}

func (c *UUIDCache) Delete(uuid string) {
	c.mu.Lock()
	defer c.mu.Unlock()

	if cachedUuid, ok := c.uuidMap[uuid]; ok {
		delete(c.uuidMap, cachedUuid.Uuid)
		delete(c.cleanUuidMap, cachedUuid.CleanUuid)
		delete(c.playerNameMap, cachedUuid.PlayerName)
	}
}

func (c *UUIDCache) Clean() {
	c.mu.Lock()
	defer c.mu.Unlock()

	now := time.Now()

	for uuid, cachedUuid := range c.uuidMap {
		if now.Sub(cachedUuid.TimeFetched) > c.lifetimeLimit {
			delete(c.uuidMap, uuid)
			delete(c.cleanUuidMap, cachedUuid.CleanUuid)
			delete(c.playerNameMap, cachedUuid.PlayerName)
		}
	}
}

type Property struct {
	Name      string `json:"name"`
	Value     string `json:"value"`
	Signature string `json:"signature"`
}

type MCPlayer struct {
	Username    string
	UUID        string
	AvatarURL   string
	SkinTexture string
	Properties  []Property
}

func isValidUUID(id string) bool {
	r := regexp.MustCompile(`^[a-fA-F0-9]{32}$|^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$`)
	return r.MatchString(id)
}

func isValidUsername(name string) bool {
	r := regexp.MustCompile(`^[a-zA-Z0-9_]{3,16}$`)
	return r.MatchString(name)
}

func fetchFromPlayerDB(id string) (*MCPlayer, error) {
	url := fmt.Sprintf("https://playerdb.co/api/player/minecraft/%s", id)

	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var result map[string]interface{}
	err = json.Unmarshal(body, &result)
	if err != nil {
		return nil, err
	}

	if result["success"].(bool) == false {
		return nil, fmt.Errorf("player not found (playerdb): %s", id)
	}

	data := result["data"].(map[string]interface{})
	player := data["player"].(map[string]interface{})
	properties := player["properties"].([]interface{})

	mcPlayer := &MCPlayer{
		Username:    player["username"].(string),
		UUID:        player["id"].(string),
		AvatarURL:   player["avatar"].(string),
		SkinTexture: player["skin_texture"].(string),
	}

	for _, p := range properties {
		prop := p.(map[string]interface{})
		mcPlayer.Properties = append(mcPlayer.Properties, Property{
			Name:      prop["name"].(string),
			Value:     prop["value"].(string),
			Signature: prop["signature"].(string),
		})
	}

	return mcPlayer, nil
}

func FetchMCPlayer(id string) (*MCPlayer, error) {
	// TODO: Implement mojang API as the fallback
	// TODO: Catch Nicks (propagate nick catcher through to HypixelAPI)
	if isValidUUID(id) {
		player, err := fetchFromPlayerDB(id)
		if err != nil {
			return nil, err
		}
		return player, nil
	} else if isValidUsername(id) {
		player, err := fetchFromPlayerDB(id)
		if err != nil {
			return nil, err
		}
		return player, nil
	} else {
		return nil, errors.New("invalid input: not a valid UUID or username")
	}
}

type DisplayTable interface {
	AddPlayer(player HypixelPlayer)
	AddPlayerN(player string, app *App)
	Draw()
	GetPlayers() []HypixelPlayer
}

type BedwarsDisplayTable struct {
	players []HypixelPlayer
}

func (b *BedwarsDisplayTable) GetPlayers() []HypixelPlayer {
	return b.players
}

func (b *BedwarsDisplayTable) AddPlayer(player HypixelPlayer) {
	b.players = append(b.players, player)
}

func (b *BedwarsDisplayTable) AddPlayerN(player string, app *App) {
	cachedPlayer, isAlive := app.UUIDCache.Get(player)

	if isAlive {
		fetchedPlayer, err := app.API.FetchPlayer(cachedPlayer.Uuid)
		if err != nil {
			fmt.Println("Error fetching player from Hypixel API:", err)
			return
		}
		b.AddPlayer(fetchedPlayer.Player)
	} else {
		playerData, err := FetchMCPlayer(player)
		if err != nil {
			fmt.Println("Error fetching player data:", err)
			return
		}

		app.UUIDCache.Add(&CachedUuid{
			CleanUuid:   strings.ReplaceAll(playerData.UUID, "-", ""),
			Uuid:        playerData.UUID,
			PlayerName:  playerData.Username,
			TimeFetched: time.Now(),
		})

		fetchedPlayer, err := app.API.FetchPlayer(playerData.UUID)
		if err != nil {
			fmt.Println("Error fetching player from Hypixel API:", err)
			return
		}
		b.AddPlayer(fetchedPlayer.Player)
	}
}

func (b *BedwarsDisplayTable) Draw() {
	fmt.Println("=== Bedwars Player Table ===")
	for _, player := range b.players {
		fmt.Printf("Player: %s, Score: %d\n", player.DisplayName, player.Achievements.BedwarsStar)
	}
}

type App struct {
	LogPath             string
	API                 *HypixelAPI
	UUIDCache           *UUIDCache
	CurrentDisplayTable DisplayTable
}

func NewApp(apiKey string) *App {
	path := os.Getenv("USERPROFILE")
	logPath := filepath.Join(path, ".lunarclient", "offline", "multiver", "logs", "latest.log")

	return &App{
		LogPath:             logPath,
		API:                 NewHypixelAPI(apiKey),
		UUIDCache:           NewUUIDCache(120 * time.Minute),
		CurrentDisplayTable: &BedwarsDisplayTable{},
	}
}

type LogBuffer struct {
	strings []string
	size    int
}

func NewLogBuffer(size int) *LogBuffer {
	return &LogBuffer{
		strings: make([]string, 0, size),
		size:    size,
	}
}

func (l *LogBuffer) Add(s string) {
	if len(l.strings) == l.size {
		l.strings = l.strings[1:]
	}
	l.strings = append(l.strings, s)
}

func (l *LogBuffer) Get() []string {
	return l.strings
}

func (l *LogBuffer) GetLast() (string, error) {
	if len(l.strings) == 0 {
		return "", fmt.Errorf("log buffer is empty")
	}
	return l.strings[len(l.strings)-1], nil
}

func (l *LogBuffer) GetSecondToLast() (string, error) {
	if len(l.strings) < 2 {
		return "", fmt.Errorf("log buffer does not have enough lines")
	}
	return l.strings[len(l.strings)-2], nil
}

func (l *LogBuffer) GetLineStepsBack(x int) (string, error) {
	if x < 0 || x >= len(l.strings) {
		return "", fmt.Errorf("log buffer does not have enough lines to step back %d times", x)
	}
	return l.strings[len(l.strings)-1-x], nil
}

var LogBuf = NewLogBuffer(10)

func replaceCorruptedRune(msg string) string {
	runes := []rune(msg)
	for i, r := range runes {
		if r == '�' {
			runes[i] = '§'
		}
	}
	return string(runes)
}

func (a *App) onFileEmit(line string) {
	msg := strings.TrimSpace(line)
	if len(msg) < 34 {
		return
	}
	submsg := msg[33:]
	if len(submsg) != 0 {
		LogBuf.Add(submsg)
	}

	OnlinePrefix := "[CHAT] ONLINE: "
	PartyListSeparatorLinePrefix := "[CHAT] -----------------------------------------------------"
	PartyMemberCountPrefix := "[CHAT] Party Members ("
	PartyLeaderPrefix := "[CHAT] Party Leader: "
	PartyListMembersPrefix := "[CHAT] Party Members: "

	if strings.HasPrefix(submsg, OnlinePrefix) { // Online Message
		newsubmsg := strings.TrimPrefix(submsg, OnlinePrefix)
		players := strings.Split(newsubmsg, ",")
		for _, player := range players {
			playerName := strings.TrimSpace(player)
			playerUUID, err := FetchMCPlayer(playerName)
			if err != nil {
				log.Fatalf("Error fetching UUID: %v", err)
				return
			}
			fmt.Printf("UUID of player %s: %s\n", playerName, playerUUID)
			cachedPlayer := CachedUuid{playerUUID.UUID, playerName, playerName, time.Now()}
			a.UUIDCache.Add(&cachedPlayer)

			//names, err := GetNameFromUUID(playerUUID)
			//if err != nil {
			//	log.Fatalf("Error fetching names from UUID: %v", err)
			//}
			//fmt.Printf("Name history for UUID %s: %v\n", playerUUID, names)
		}

	} else if strings.HasPrefix(submsg, PartyListSeparatorLinePrefix) { // Party List
		last, _ := LogBuf.GetSecondToLast()
		// TODO: Check if moderators
		if !strings.HasPrefix(last, PartyListMembersPrefix) {
			return
		}

		PartyMembersMsg := strings.TrimPrefix(last, PartyListMembersPrefix)
		for _, player := range strings.Split(PartyMembersMsg, ",") {
			playerName := strings.TrimSpace(strings.TrimSuffix(player, " ?"))
			if strings.HasPrefix(playerName, "[") {
				playerName = strings.Split(playerName, " ")[1]
			}
			print("Found Player: '")
			print(playerName)
			print("'\n")
			a.CurrentDisplayTable.AddPlayerN(playerName, a)

			//playerName := strings.TrimSpace(player)
			//playerUUID, err := GetUUIDFromName(playerName)
			//if err != nil {
			//	log.Fatalf("Error fetching UUID: %v", err)
			//	return
			//}
			//fmt.Printf("UUID of player %s: %s\n", playerName, playerUUID)
			//cachedPlayer := CachedUuid{playerUUID, playerName, playerName, time.Now()}
			//UuidCache.Add(&cachedPlayer)
		}

		// Parse Party Leader
		leaders_msg, err := LogBuf.GetLineStepsBack(2)
		if err != nil {
			println("Unable to find party leader message")
			return
		}
		PartyLeaderMsg := strings.TrimPrefix(leaders_msg, PartyLeaderPrefix)
		playerName := strings.TrimSpace(strings.TrimSuffix(PartyLeaderMsg, " ?"))
		if strings.HasPrefix(playerName, "[") {
			playerName = strings.Split(playerName, " ")[1]
		}
		print("Found Leader: '")
		print(playerName)
		print("'\n")
		a.CurrentDisplayTable.AddPlayerN(playerName, a)

		// Parse Party Count
		party_count_msg, err := LogBuf.GetLineStepsBack(4)
		if err != nil {
			println("Unable to find party count message")
			return
		}
		PartyCountMsg := strings.TrimPrefix(party_count_msg, PartyMemberCountPrefix)
		count_str := strings.TrimSuffix(PartyCountMsg, ")")
		count, err := strconv.Atoi(count_str)
		if err != nil {
			println("Unable to parse party count message - Invalid number used")
			return
		}
		print("Expected ")
		print(count)
		print(" party members\n")
	}

	println(submsg)
}

func (a *App) Start() {

	rl.SetConfigFlags(rl.FlagWindowTransparent | rl.FlagWindowUndecorated)
	screenWidth := 800
	screenHeight := 600
	rl.InitWindow(int32(screenWidth), int32(screenHeight), "Raylib in Go")
	defer rl.CloseWindow()

	windowPosition := rl.NewVector2(500, 200)
	rl.SetWindowPosition(int(int32(windowPosition.X)), int(int32(windowPosition.Y)))

	var panOffset rl.Vector2
	var dragWindow bool = false

	const LowFps = 8
	rl.SetTargetFPS(LowFps)

	lineCh := make(chan string)
	go tailFile(a.LogPath, lineCh)

	ch := make(chan HypixelPlayerResponse)
	var wg sync.WaitGroup

	go func() {
		wg.Wait()
		close(ch)
	}()

	sx := 10
	sy := 10

	for !rl.WindowShouldClose() {
		mousePosition := rl.GetMousePosition()
		windowPosition := rl.GetWindowPosition()
		screenMousePosition := rl.Vector2{
			X: windowPosition.X + mousePosition.X,
			Y: windowPosition.Y + mousePosition.Y,
		}

		if rl.IsMouseButtonPressed(rl.MouseLeftButton) && !dragWindow {
			rl.SetTargetFPS(int32(rl.GetMonitorRefreshRate(0)))
			dragWindow = true
			panOffset.X = screenMousePosition.X - float32(windowPosition.X)
			panOffset.Y = screenMousePosition.Y - float32(windowPosition.Y)
		}

		if dragWindow {
			newWindowPositionX := int(screenMousePosition.X - panOffset.X)
			newWindowPositionY := int(screenMousePosition.Y - panOffset.Y)

			rl.SetWindowPosition(newWindowPositionX, newWindowPositionY)

			if rl.IsMouseButtonReleased(rl.MouseLeftButton) {
				dragWindow = false
				rl.SetTargetFPS(LowFps)
			}
		}

		select {
		case line := <-lineCh:
			a.onFileEmit(replaceCorruptedRune(line))
		default:
		}

		rl.BeginDrawing()
		rl.ClearBackground(rl.Color{0, 0, 0, 100})

		yOffset := sy
		for _, player := range a.CurrentDisplayTable.GetPlayers() {
			playerName := player.DisplayName
			bedwarsLevel := player.Achievements.BedwarsStar
			bedwarsWins := player.Stats.Bedwars.Wins

			rl.DrawText(fmt.Sprintf("Player: %s", playerName), int32(sx), int32(yOffset), 20, rl.Black)
			yOffset += 25
			rl.DrawText(fmt.Sprintf("Bedwars Level: %d", bedwarsLevel), int32(sx), int32(yOffset), 18, rl.DarkGray)
			yOffset += 25
			rl.DrawText(fmt.Sprintf("Bedwars Wins: %d", bedwarsWins), int32(sx), int32(yOffset), 18, rl.DarkGray)
			yOffset += 40
		}
		rl.EndDrawing()
	}
}

func tailFile(path string, lineCh chan<- string) {
	file, err := os.Open(path)
	if err != nil {
		log.Fatalf("Failed to open file: %v", err)
	}
	defer file.Close()

	file.Seek(0, 2)

	reader := bufio.NewReader(file)

	for {
		line, err := reader.ReadString('\n')
		if err != nil {
			time.Sleep(100 * time.Millisecond)
			continue
		}
		lineCh <- line
	}
}

const KEY = "5039a811-1b27-4db6-a7f0-c8dd28eeebcd"
const UUID = "5328930e-d411-49cb-90ad-4e5c7b27dd86"

func main() {
	app := NewApp(KEY)
	app.Start()
}
