package serra

import (
	"errors"
	"fmt"
	"math"
	"os"
	"strconv"
	"time"
	"unicode"

	"github.com/charmbracelet/log"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
)

type Rarities struct {
	Rares, Uncommons, Commons, Mythics float64
}

var (
	Icon        = "\U0001F9D9\U0001F3FC"
	Reset       = "\033[0m"
	Background  = "\033[38;5;59m"
	CurrentLine = "\033[38;5;60m"
	Foreground  = "\033[38;5;231m"
	Comment     = "\033[38;5;103m"
	Cyan        = "\033[38;5;159m"
	Green       = "\033[38;5;120m"
	Orange      = "\033[38;5;222m"
	Pink        = "\033[38;5;212m"
	Purple      = "\033[38;5;183m"
	Red         = "\033[38;5;210m"
	Yellow      = "\033[38;5;229m"
)

func Logger() *log.Logger {

	l := log.New(os.Stderr)
	l.SetReportTimestamp(false)
	return l
}

func getStoredCard(coll *Collection, c *Card) (Card, error) {
	// find already existing card
	sort := bson.D{{"_id", 1}}
	searchFilter := bson.D{{"_id", c.ID}}
	storedCards, err := coll.storageFind(searchFilter, sort, 0, 0)
	if err != nil {
		return Card{}, err
	}

	return storedCards[0], nil
}

func modifyCardCount(coll *Collection, c *Card, amount int64, foil bool) error {
	l := Logger()

	storedCard, err := getStoredCard(coll, c)
	if err != nil {
		return err
	}

	// update card amount
	var update bson.M
	if foil {
		update = bson.M{
			"$set": bson.M{"serra_count_foil": storedCard.SerraCountFoil + amount},
		}
	} else {
		update = bson.M{
			"$set": bson.M{"serra_count": storedCard.SerraCount + amount},
		}
	}

	coll.storageUpdate(bson.M{"_id": bson.M{"$eq": c.ID}}, update)

	var total int64
	if foil {
		total = storedCard.SerraCountFoil + amount
		if amount < 0 {
			l.Warnf("Reduced card amount of \"%s\" (%.2f%s, foil) from %d to %d", storedCard.Name, storedCard.getValue(true), getCurrency(), storedCard.SerraCountFoil, total)
		} else {
			l.Warnf("Increased card amount of \"%s\" (%.2f%s, foil) from %d to %d", storedCard.Name, storedCard.getValue(true), getCurrency(), storedCard.SerraCountFoil, total)
		}
	} else {
		total = storedCard.SerraCount + amount
		if amount < 0 {
			l.Warnf("Reduced card amount of \"%s\" (%.2f%s) from %d to %d", storedCard.Name, storedCard.getValue(false), getCurrency(), storedCard.SerraCount, total)
		} else {
			l.Warnf("Increased card amount of \"%s\" (%.2f%s) from %d to %d", storedCard.Name, storedCard.getValue(false), getCurrency(), storedCard.SerraCount, total)
		}
	}

	return nil
}

func modifyCardDeckCount(coll *Collection, c *Card, amount int64, foil bool) error {
	l := Logger()

	storedCard, err := getStoredCard(coll, c)
	if err != nil {
		return err
	}

	// update card amount
	var update bson.M
	if foil {
		newAmount := min(max(storedCard.SerraCountFoilDeck + amount, 0), storedCard.SerraCountFoil)
		l.Infof("%d / %d available", storedCard.SerraCountFoil - newAmount, storedCard.SerraCountFoil)
		update = bson.M{
			"$set": bson.M{"serra_count_foil_deck": newAmount},
		}
	} else {
		newAmount := min(max(storedCard.SerraCountDeck + amount, 0), storedCard.SerraCount)
		l.Infof("%d / %d available", storedCard.SerraCount - newAmount, storedCard.SerraCount)
		update = bson.M{
			"$set": bson.M{"serra_count_deck": newAmount},
		}
	}

	coll.storageUpdate(bson.M{"_id": bson.M{"$eq": c.ID}}, update)

	return nil
}

func findCardByCollectorNumber(coll *Collection, setCode string, collectorNumber string) (*Card, error) {
	sort := bson.D{{"_id", 1}}
	searchFilter := bson.D{{"set", setCode}, {"collectornumber", collectorNumber}}
	storedCards, err := coll.storageFind(searchFilter, sort, 0, 0)
	if err != nil {
		return &Card{}, err
	}

	if len(storedCards) < 1 {
		return &Card{}, errors.New("Card not found")
	}

	return &storedCards[0], nil
}

func stringToTime(s primitive.DateTime) string {
	return time.UnixMilli(int64(s)).Format("2006-01-02")
}

// missing compares two slices and returns slice of differences
func missing(a, b []string) []string {
	type void struct{}
	// create map with length of the 'a' slice
	ma := make(map[string]void, len(a))
	diffs := []string{}
	// Convert first slice to map with empty struct (0 bytes)
	for _, ka := range a {
		ma[ka] = void{}
	}
	// find missing values in a
	for _, kb := range b {
		if _, ok := ma[kb]; !ok {
			diffs = append(diffs, kb)
		}
	}
	return diffs
}

func findSetByCode(coll *Collection, setcode string) (*Set, error) {
	storedSets, err := coll.storageFindSet(bson.D{{"code", setcode}}, bson.D{{"_id", 1}})
	if err != nil {
		return &Set{}, err
	}

	if len(storedSets) < 1 {
		return &Set{}, errors.New("Set not found")
	}

	return &storedSets[0], nil
}

func convertManaSymbols(sym []interface{}) string {
	var mana string

	if len(sym) == 0 {
		mana = mana + "None" //probibited sign for lands
	}

	for _, v := range sym {
		switch v {
		case "B":
			mana = mana + "Black" //black
		case "R":
			mana = mana + "Red" //red
		case "G":
			mana = mana + "Green" //green
		case "U":
			mana = mana + "Blue" //blue
		case "W":
			mana = mana + "White" //white
		}
	}
	return mana

}

func convertRarities(rar []primitive.M) Rarities {

	// this is maybe the ugliest way someone could choose to verify, if a rarity type is missing
	// [
	// { _id: { rarity: 'common' }, count: 20 },
	// { _id: { rarity: 'uncommon' }, count: 2 }
	// ]
	// if a result like this is there, 1 rarity type "rare" is not in the array. and needs to be
	// initialized with 0, otherwise we get a panic

	var ri Rarities
	for _, r := range rar {
		switch r["_id"] {
		case "rare":
			ri.Rares = r["count"].(float64)
		case "uncommon":
			ri.Uncommons = r["count"].(float64)
		case "common":
			ri.Commons = r["count"].(float64)
		case "mythic":
			ri.Mythics = r["count"].(float64)
		}
	}
	return ri

}

func showPriceHistory(prices []PriceEntry, prefix string, total bool) {

	var before float64
	for _, e := range prices {

		var value float64
		if total {
			if getCurrency() == EUR {
				value = e.Eur + e.EurFoil
			} else {
				value = e.Usd + e.UsdFoil
			}
		} else {
			if getCurrency() == EUR {
				value = e.Eur
			} else {
				value = e.Usd
			}
		}

		if value > before && before != 0 {
			fmt.Printf("%s%s%s %.2f%s%s (%+.2f%%, %+.2f%s)\n", prefix, stringToTime(e.Date), Green, value, getCurrency(), Reset, (value/before*100)-100, value-before, getCurrency())
		} else if value < before {
			fmt.Printf("%s%s%s %.2f%s%s (%+.2f%%, %+.2f%s)\n", prefix, stringToTime(e.Date), Red, value, getCurrency(), Reset, (value/before*100)-100, value-before, getCurrency())
		} else {
			fmt.Printf("%s%s %.2f%s%s\n", prefix, stringToTime(e.Date), value, getCurrency(), Reset)
		}
		before = value
	}
}

func filterForDigits(str string) int {
	var numStr string
	for _, c := range str {
		if unicode.IsDigit(c) {
			numStr += string(c)
		}
	}
	s, _ := strconv.Atoi(numStr)
	return s
}

func getFloat64(unknown interface{}) (float64, error) {
	switch i := unknown.(type) {
	case float64:
		return i, nil
	case float32:
		return float64(i), nil
	case int64:
		return float64(i), nil
	case int32:
		return float64(i), nil
	case int:
		return float64(i), nil
	case uint64:
		return float64(i), nil
	case uint32:
		return float64(i), nil
	case uint:
		return float64(i), nil
	default:
		return math.NaN(), errors.New("non-numeric type could not be converted to float")
	}
}

func coloredValue(value float64) string {

	outputColor := Reset

	if value > 1 {
		outputColor = Green
	}
	if value > 5 {
		outputColor = Yellow
	}
	if value > 10 {
		outputColor = Red
	}

	return outputColor
}

func askConfirmation(card *Card) bool {
	drawImage(card)

	response := "some invalid response"
	for response != "y" && response != "n" {
		fmt.Println("Is this correct (y/n)?")
		fmt.Scanln(&response)
	}

	return response == "y"
}
