package main

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

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

type HypixelPlayerBedwarsStats struct {
	Wins int `json:"wins_bedwars"`
}

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

type HypixelPlayer struct {
	Achievements HypixelPlayerAchievements `json:"achievements"`
	Stats        HypixelPlayerStats        `json:"stats"`
}

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

const URL_ = "https://api.hypixel.net/player?key=5039a811-1b27-4db6-a7f0-c8dd28eeebcd&uuid="

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

type Cache struct {
	mu            sync.RWMutex
	uuidMap       map[string]*CachedUuid
	cleanUuidMap  map[string]*CachedUuid
	playerNameMap map[string]*CachedUuid
}

func NewCache() *Cache {
	return &Cache{
		uuidMap:       make(map[string]*CachedUuid),
		cleanUuidMap:  make(map[string]*CachedUuid),
		playerNameMap: make(map[string]*CachedUuid),
	}
}

func (c *Cache) 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 *Cache) GetByUuid(uuid string) (*CachedUuid, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	val, ok := c.uuidMap[uuid]
	return val, ok
}

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

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

func (c *Cache) 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)
	}
}

var UuidCache = NewCache()

type UUIDResponse struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

func GetUUIDFromName(name string) (string, error) {
	println(fmt.Sprintf("Player Name to search: '%s'", name))
	url := fmt.Sprintf("https://api.mojang.com/users/profiles/minecraft/%s", name)

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

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("UUID error: failed to fetch UUID for name %s, status code: %d", name, resp.StatusCode)
	}

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

	var uuidResponse UUIDResponse
	err = json.Unmarshal(body, &uuidResponse)
	if err != nil {
		return "", err
	}

	return uuidResponse.ID, nil
}

//func GetNameFromUUID(uuid string) ([]string, error) {
//	uuid = strings.ReplaceAll(uuid, "-", "")
//
//	url := fmt.Sprintf("https://api.mojang.com/user/profiles/%s/names", uuid)
//
//	resp, err := http.Get(url)
//	if err != nil {
//		return nil, err
//	}
//	defer resp.Body.Close()
//
//	if resp.StatusCode != http.StatusOK {
//		return nil, fmt.Errorf("error: failed to fetch name for UUID %s, status code: %d", uuid, resp.StatusCode)
//	}
//
//	body, err := io.ReadAll(resp.Body)
//	if err != nil {
//		return nil, err
//	}
//
//	var nameHistory []NameHistory
//	err = json.Unmarshal(body, &nameHistory)
//	if err != nil {
//		return nil, err
//	}
//
//	var names []string
//	for _, history := range nameHistory {
//		names = append(names, history.Name)
//	}
//
//	return names, nil
//}

func fetchHypixelData(url string, ch chan<- HypixelPlayerResponse, wg *sync.WaitGroup) {
	defer wg.Done()

	resp, err := http.Get(url)
	if err != nil {
		log.Println("Error fetching data:", err)
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		log.Printf("Error: failed to fetch data, status code: %d\n", resp.StatusCode)
		return
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Println("Error reading response body:", err)
		return
	}

	var playerResponse HypixelPlayerResponse
	err = json.Unmarshal(body, &playerResponse)
	if err != nil {
		log.Println("Error unmarshalling JSON:", err)
		return
	}

	ch <- playerResponse
}

type StatsDisplayPlayer struct {
	Name   string
	Player HypixelPlayer
}

type StatsDisplayMode int

const (
	BEDWARS StatsDisplayMode = iota
)

type StatsDisplay struct {
	Players []StatsDisplayPlayer
	Mode    StatsDisplayMode
}

func NewStatsDisplay() *StatsDisplay {
	return &StatsDisplay{
		Players: make([]StatsDisplayPlayer, 0),
		Mode:    BEDWARS,
	}
}

func (d *StatsDisplay) AddPlayer(playername string) {
	playeruuid, err := GetUUIDFromName(playername)
	if err != nil {
		fmt.Printf("Error fetching UUID for player %s: %v\n", playername, err)
		return
	}

	hypixelApiUrl := URL_ + playeruuid

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

	wg.Add(1)
	go fetchHypixelData(hypixelApiUrl, ch, &wg)

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

	for playerResponse := range ch {
		if playerResponse.Success {
			fmt.Printf("Player Bedwars Level: %d\n", playerResponse.Player.Achievements.BedwarsStar)
			fmt.Printf("Player Bedwars Wins: %d\n", playerResponse.Player.Stats.Bedwars.Wins)

			d.Players = append(d.Players, StatsDisplayPlayer{Player: playerResponse.Player, Name: playername})
		} else {
			fmt.Println("Failed to fetch player data.")
		}
	}
}

var StatsDisplayApp = NewStatsDisplay()

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 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 := 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)

			//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")
			StatsDisplayApp.AddPlayer(playerName)

			//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")
		StatsDisplayApp.AddPlayer(playerName)

		// 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 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
	}
}

func start_demo() {
	path := os.Getenv("USERPROFILE")
	path = filepath.Join(path, ".lunarclient", "offline", "multiver", "logs", "latest.log")

	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(path, 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:
			onFileEmit(replaceCorruptedRune(line))
		default:
		}

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

		yOffset := sy
		for _, player := range StatsDisplayApp.Players {
			playerName := player.Name
			bedwarsLevel := player.Player.Achievements.BedwarsStar
			bedwarsWins := player.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()
	}

	close(lineCh)

	for playerResponse := range ch {
		if playerResponse.Success {
			fmt.Printf("Player Bedwars Level: %d\n", playerResponse.Player.Achievements.BedwarsStar)
			fmt.Printf("Player Bedwars Wins: %d\n", playerResponse.Player.Stats.Bedwars.Wins)
		} else {
			fmt.Println("Failed to fetch player data.")
		}
	}
}
