package steam

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"slices"
	"strconv"

	"github.com/chenasraf/stimvisor/common"
	"github.com/chenasraf/stimvisor/dirs"
	"github.com/chenasraf/stimvisor/logger"

	"github.com/Goldziher/go-utils/sliceutils"
)

// GameInfo represents the information about a game.
type GameInfo struct {
	Id               string   `json:"id"`
	Name             string   `json:"name"`
	InstallDir       string   `json:"installDir"`
	Description      string   `json:"description"`
	ShortDescription string   `json:"shortDescription"`
	Website          string   `json:"website"`
	BackgroundImage  string   `json:"backgroundImage"`
	CapsuleImage     string   `json:"capsuleImage"`
	Categories       []string `json:"categories"`
}

// GetGameInfo retrieves the information of a game given its ID.
func GetGameInfo(gameId string) (GameInfo, error) {
	gameDir, err := GetGameDir(gameId)
	if err != nil {
		return GameInfo{}, err
	}
	raw, err := loadGameInfo(gameId)
	if err != nil {
		return GameInfo{}, err
	}

	website := ""
	if raw["website"] != nil {
		website = raw["website"].(string)
	}

	categories := []string{}
	if raw["categories"] != nil {
		categories = sliceutils.Map(
			raw["categories"].([]interface{}),
			func(v interface{}, i int, l []interface{}) string {
				val, ok := v.(map[string]interface{})
				if !ok {
					return ""
				}
				return val["description"].(string)
			})
	}

	return GameInfo{
		Id:               gameId,
		Name:             raw["name"].(string),
		InstallDir:       gameDir,
		Description:      raw["detailed_description"].(string),
		ShortDescription: raw["short_description"].(string),
		Website:          website,
		BackgroundImage:  raw["background"].(string),
		CapsuleImage:     raw["capsule_image"].(string),
		Categories:       categories,
	}, nil
}

// GetGameInfoCacheDir returns the directory path for caching game information.
func GetGameInfoCacheDir() string {
	cacheDir := common.GetCacheDir()
	return filepath.Join(cacheDir, "gameinfo")
}

// loadGameInfo loads the game information from the cache or fetches it if not available.
func loadGameInfo(gameId string) (map[string]interface{}, error) {
	info := make(map[string]interface{})
	cachePath := filepath.Join(GetGameInfoCacheDir(), gameId+".json")
	var err error
	if _, err = os.Stat(cachePath); os.IsNotExist(err) {
		info, err = fetchGameInfo(gameId)
		if err == nil {
			return info, nil
		}
	}
	if err != nil {
		return info, err
	}
	f, err := os.ReadFile(cachePath)
	if err != nil {
		return info, err
	}
	json.Unmarshal(f, &info)
	logger.Debug("Loaded game info for %s from cache", gameId)
	return info, nil
}

const STEAM_API_URL = "https://store.steampowered.com/api/appdetails?appids=%s"

// fetchGameInfo fetches the game information from the Steam API and caches it.
func fetchGameInfo(gameId string) (map[string]interface{}, error) {
	os.MkdirAll(GetGameInfoCacheDir(), 0755)
	cachePath := filepath.Join(GetGameInfoCacheDir(), gameId+".json")
	url := fmt.Sprintf(STEAM_API_URL, gameId)
	logger.Info("Fetching game info for %s from %s", gameId, url)
	resp, err := http.Get(url)
	if err != nil {
		logger.FatalErr(err)
		panic(err)
	}
	defer resp.Body.Close()

	respBytes, err := io.ReadAll(resp.Body)
	respJson := make(map[string]interface{})
	json.Unmarshal(respBytes, &respJson)

	if respJson[gameId] == nil {
		logger.Error("Failed to fetch game info for %s", gameId)
		return map[string]interface{}{}, fmt.Errorf("Failed to fetch game info for %s", gameId)
	}
	// extract result->gameId->data
	respGame := respJson[gameId].(map[string]interface{})

	if respGame["success"] == false || respGame["data"] == nil {
		logger.Error("Failed to fetch game info for %s", gameId)
		return map[string]interface{}{}, fmt.Errorf("Failed to fetch game info for %s", gameId)
	}
	respGameData := respGame["data"].(map[string]interface{})
	partBytes, _ := json.Marshal(respGameData)

	cacheFile, err := os.Create(cachePath)
	if err != nil {
		logger.FatalErr(err)
		panic(err)
	}
	defer cacheFile.Close()

	logger.Info("Caching game info for %s", gameId)
	cacheFile.WriteString(string(partBytes))

	return respGameData, nil
}

// GetAllDirs returns a list of directories for all games of a specific Steam user by user ID.
func GetAllDirs(userId string) ([]string, error) {
	userDir, err := dirs.GetUserDirectory(userId)
	if err != nil {
		return nil, err
	}

	entries, err := os.ReadDir(userDir)
	if err != nil {
		return nil, err
	}

	gameDirs := []string{}
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}
		if slices.Contains(common.STEAM_INTERNAL_IDS, entry.Name()) {
			continue
		}
		if _, err := strconv.Atoi(entry.Name()); err != nil {
			continue
		}
		gameDir := fmt.Sprintf("%s/%s", userDir, entry.Name())
		gameDirs = append(gameDirs, gameDir)
	}
	return gameDirs, nil
}

// GetGameDir returns the directory path for a specific game by game ID.
func GetGameDir(gameId string) (string, error) {
	userDir, err := dirs.GetSteamUserDirectory()
	if err != nil {
		return "", err
	}

	return fmt.Sprintf("%s/%s", userDir, gameId), nil
}
