package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"runtime"
	"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)
	}()
}
