package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"time"

	humanize "github.com/dustin/go-humanize"
)

// fetchCards fetches cards from the API using the provided limit, cursor, and set type.
func fetchCards(limit, cursor int, setType string) ([]*Card, error) {
	// Define the input data structure
	input := Input{
		Part0: struct {
			JSON JSONPayload `json:"json"`
		}{
			JSON: JSONPayload{
				Query:           "",
				Sort:            "relevance",
				Set:             setType,
				Filters:         []interface{}{},
				Limit:           limit,
				VariantLimit:    false,
				CollectionLimit: false,
				Cursor:          cursor,
				Direction:       "forward",
			},
		},
	}

	// Serialize the input data structure to JSON
	jsonData, err := json.Marshal(input)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal input data: %w", err)
	}

	// URL encode the JSON data
	urlEncodedInput := url.QueryEscape(string(jsonData))

	// Construct the full URL
	apiURL := fmt.Sprintf("https://curiosa.io/api/trpc/card.search?batch=1&input=%s", urlEncodedInput)

	// Create a new HTTP request
	req, err := http.NewRequest("GET", apiURL, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	// Set essential headers
	setHeaders(req)

	// Perform the HTTP request
	start := time.Now()
	client := &http.Client{}
	resp, err := client.Do(req)
	duration := time.Since(start)
	if err != nil {
		return nil, fmt.Errorf("failed to perform request: %w", err)
	}
	defer resp.Body.Close()

	// Check for non-200 status codes
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("non-200 response: %s", resp.Status)
	}

	// Read and decode the JSON response
	var result []struct {
		Result struct {
			Data struct {
				JSON struct {
					Cards []*Card `json:"cards"`
				} `json:"json"`
			} `json:"data"`
		} `json:"result"`
	}
	err = json.NewDecoder(resp.Body).Decode(&result)
	if err != nil {
		return nil, fmt.Errorf("failed to decode response body: %w", err)
	}

	// Extract cards from the result
	var cards []*Card
	if len(result) > 0 {
		cards = result[0].Result.Data.JSON.Cards
	}

	fmt.Printf("Cursor: %d, Limit: %d, Duration: %s, Status: %d, Cards: %d\n",
		cursor, limit, duration, resp.StatusCode, len(cards))

	return cards, nil
}

// fetchAllCards fetches all cards by making multiple requests with different cursor values.
func fetchAllCards(limit int, setType string) ([]*Card, error) {
	var allCards []*Card
	cursor := 0

	for {
		cards, err := fetchCards(limit, cursor, setType)
		if err != nil {
			return nil, err
		}

		allCards = append(allCards, cards...)

		// If the number of returned cards is less than the limit, we've fetched all cards.
		if len(cards) < limit {
			break
		}

		// Update cursor for the next batch
		cursor += limit
	}

	return allCards, nil
}

// generateImageFilename generates the image filename for each card variant.
func generateImageFilename(variant Variant) string {
	setName := strings.ToLower(variant.SetCard.SetDetails.Name)
	if setName == "" {
		setName = "other"
	}
	ext := filepath.Ext(variant.Src)
	if ext == "" {
		ext = ".png" // Default to .png if no extension found
	}
	return fmt.Sprintf("./images/%s/%s.%s%s", setName, strings.ToLower(variant.ID), strings.ToLower(variant.Slug), ext)
}

// downloadImage downloads an image from the given URL and saves it to the specified path.
func downloadImage(url, filepath string) error {
	if _, err := os.Stat(filepath); err == nil {
		fmt.Printf("Skipping download, file already exists: %s\n", filepath)
		return nil
	}

	tempFilePath := filepath + ".tmp"

	start := time.Now()
	resp, err := http.Get(url)
	duration := time.Since(start)
	if err != nil {
		return fmt.Errorf("failed to fetch image: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("non-200 response: %s", resp.Status)
	}

	file, err := os.Create(tempFilePath)
	if err != nil {
		return fmt.Errorf("failed to create image file: %w", err)
	}
	defer file.Close()

	bodySize, err := io.Copy(file, resp.Body)
	if err != nil {
		return fmt.Errorf("failed to save image: %w", err)
	}

	err = os.Rename(tempFilePath, filepath)
	if err != nil {
		return fmt.Errorf("failed to rename temp file: %w", err)
	}

	fmt.Printf("Downloaded %s | Status: %d | Size: %s | Duration: %s\n",
		filepath, resp.StatusCode, humanize.Bytes(uint64(bodySize)), duration)
	return nil
}

func main() {
	const limit = 100
	const dataDir = "./data"
	const imageDir = "./images"

	// Ensure the data and images directories exist
	err := os.MkdirAll(dataDir, os.ModePerm)
	if err != nil {
		logErrorAndExit(fmt.Errorf("failed to create data directory: %w", err))
	}

	err = os.MkdirAll(imageDir, os.ModePerm)
	if err != nil {
		logErrorAndExit(fmt.Errorf("failed to create images directory: %w", err))
	}

	// Fetch all cards for both alpha and beta sets
	alphaCards, err := fetchAllCards(limit, "alp")
	if err != nil {
		logErrorAndExit(err)
	}

	betaCards, err := fetchAllCards(limit, "bet")
	if err != nil {
		logErrorAndExit(err)
	}

	// Save alpha and beta cards separately and write combined cards.json
	allCards := append(alphaCards, betaCards...)
	saveCards(alphaCards, "alpha", dataDir, imageDir)
	saveCards(betaCards, "beta", dataDir, imageDir)
	saveCards(allCards, "all", dataDir, imageDir)
}

// saveCards saves all cards for the given set type and writes to files and images.
func saveCards(cards []*Card, filePrefix, dataDir, imageDir string) {
	// Remove duplicates
	uniqueCards := removeDuplicateCards(cards)

	// Write all cards to cards.json in the data directory
	filename := fmt.Sprintf("%s/%s_cards.json", dataDir, filePrefix)
	writeCardsToFile(filename, uniqueCards)

	// Download images for each variant and save to the appropriate directory
	for _, card := range uniqueCards {
		for _, variant := range card.Variants {
			imagePath := generateImageFilename(variant)

			err := os.MkdirAll(filepath.Dir(imagePath), os.ModePerm)
			if err != nil {
				logErrorAndExit(fmt.Errorf("failed to create image directory: %w", err))
			}

			err = downloadImage(variant.Src, imagePath)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Failed to download image %s: %v\n", imagePath, err)
				continue
			}
		}
	}

	// Group cards by element and write to files
	groupedByElement := groupCardsByElement(uniqueCards)
	for element, elementCards := range groupedByElement {
		filename := fmt.Sprintf("%s/%s_%s_cards.json", dataDir, filePrefix, strings.ToLower(element))
		writeCardsToFile(filename, elementCards)
	}

	// Group cards by element, rarity, and type and write to files
	groupedByElementRarityType := groupCardsByElementRarityType(uniqueCards)
	for key, keyCards := range groupedByElementRarityType {
		filename := fmt.Sprintf("%s/%s_%s_cards.json", dataDir, filePrefix, strings.ToLower(key))
		writeCardsToFile(filename, keyCards)
	}

	fmt.Printf("All cards have been written to %s_cards.json. Total cards: %d\n", filePrefix, len(uniqueCards))
}

// removeDuplicateCards removes duplicate cards by checking the 'id' field.
func removeDuplicateCards(cards []*Card) []*Card {
	cardMap := make(map[string]*Card)
	for _, card := range cards {
		cardMap[card.ID] = card
	}

	uniqueCards := make([]*Card, 0, len(cardMap))
	for _, card := range cardMap {
		uniqueCards = append(uniqueCards, card)
	}

	return uniqueCards
}

// setHeaders sets the necessary headers for the HTTP request.
func setHeaders(req *http.Request) {
	req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0")
	req.Header.Set("Accept", "*/*")
	req.Header.Set("content-type", "application/json")
}

// logErrorAndExit logs the error to stderr and exits with a non-zero status code.
func logErrorAndExit(err error) {
	fmt.Fprintln(os.Stderr, err)
	os.Exit(1)
}

// writeCardsToFile writes the given cards to a JSON file with the given filename.
func writeCardsToFile(filename string, cards []*Card) {
	file, err := os.Create(filename)
	if err != nil {
		logErrorAndExit(fmt.Errorf("failed to create %s: %w", filename, err))
	}
	defer file.Close()

	prettyData, err := json.MarshalIndent(cards, "", "  ")
	if err != nil {
		logErrorAndExit(fmt.Errorf("failed to marshal pretty JSON: %w", err))
	}

	_, err = file.Write(prettyData)
	if err != nil {
		logErrorAndExit(fmt.Errorf("failed to write to %s: %w", filename, err))
	}

	fmt.Printf("%s has been written. Total cards: %d\n", filename, len(cards))
}

// groupCardsByElement groups the given cards by their element.
func groupCardsByElement(cards []*Card) map[string][]*Card {
	grouped := make(map[string][]*Card)
	for _, card := range cards {
		for _, element := range card.Elements {
			grouped[element.Name] = append(grouped[element.Name], card)
		}
	}
	return grouped
}

// groupCardsByElementRarityType groups the given cards by their element, rarity, and type.
func groupCardsByElementRarityType(cards []*Card) map[string][]*Card {
	grouped := make(map[string][]*Card)
	for _, card := range cards {
		for _, element := range card.Elements {
			key := fmt.Sprintf("%s_%s_%s", strings.ToLower(element.Name), strings.ToLower(card.Guardian.Rarity), strings.ToLower(card.Guardian.Type))
			grouped[key] = append(grouped[key], card)
		}
	}
	return grouped
}
