package ledger

import (
	"fmt"
	"maps"
	"regexp"
	"sort"
	"strings"
	"time"
)

type Normalizer interface {
	NormalizeFactor(string, string) float64
}

type Balances map[string]Balance

type Balance map[Currency]float64

func (balances Balances) Sub(other Balances) Balances {
	result := make(Balances)
	for k, v := range balances {
		result[k] = v.Sub(other[k])
	}
	for k, v := range other {
		if _, ok := balances[k]; ok {
			continue
		}
		result[k] = v.Invert()
	}
	return result
}

func (balances Balances) Nonzero() Balances {
	result := make(Balances)
	for k, v := range balances {
		if nonzero := v.Nonzero(); len(nonzero) > 0 {
			result[k] = nonzero
		}
	}
	return result
}

func (balances Balances) NotLike(pattern string) Balances {
	result := make(Balances)
	p := regexp.MustCompile(pattern)
	for k, v := range balances {
		if !p.MatchString(k) {
			result[k] = maps.Clone(v)
		}
	}
	return result
}

func (balances Balances) KindaLike(like Like) Balances {
	ks := []string{}
	for k := range balances {
		ks = append(ks, k)
	}
	sort.Strings(ks)

	result := make(Balances)
	for _, k := range ks {
		v := balances[k]
		if like(v.kinda(k)) {
			result[k] = maps.Clone(v)
		}
	}
	return result
}

func (b Balance) kinda(k string) Delta {
	return Delta{
		Date:        time.Now().Format("2006-01-02"),
		Name:        k,
		Value:       b[USD],
		Description: k,
		Transaction: k,
		Payee:       false,
	}
}

func (balances Balances) Like(pattern string) Balances {
	result := make(Balances)
	p := regexp.MustCompile(pattern)
	for k, v := range balances {
		if p.MatchString(k) {
			result[k] = maps.Clone(v)
		}
	}
	return result
}

func (balances Balances) Group(pattern string) Balances {
	result := make(Balances)
	p := regexp.MustCompile(pattern)
	for k, v := range balances {
		k2 := k
		if p.MatchString(k) {
			k2 = p.FindString(k)
		}

		was := result[k2]
		if was == nil {
			was = make(Balance)
		}

		for k3, v3 := range v {
			was[k3] += v3
		}

		result[k2] = was
	}
	return result
}

func (balances Balances) KindaGroup(group Group) Balances {
	ks := []string{}
	for k := range balances {
		ks = append(ks, k)
	}
	sort.Strings(ks)

	result := make(Balances)
	for _, k := range ks {
		v := balances[k]
		k2 := k
		if k3 := group(v.kinda(k)).Name; k2 != k3 {
			k2 = k3
		}

		if _, ok := result[k2]; !ok {
			result[k2] = make(Balance)
		}

		for k3, v3 := range v {
			result[k2][k3] += v3
		}
	}
	return result
}

func (balances Balances) WithBPIs(bpis BPIs) Balances {
	return balances.WithBPIsAt(bpis, "9")
}

func (balances Balances) WithBPIsAt(bpis BPIs, date string) Balances {
	ks := []string{}
	for k := range balances {
		ks = append(ks, k)
	}
	sort.Strings(ks)

	result := make(Balances)
	for _, k := range ks {
		v := balances[k]
		if _, ok := result[k]; !ok {
			result[k] = make(Balance)
		}
		for k2, v2 := range v {
			if k2 == USD {
				result[k][USD] = result[k][USD] + v2
			} else if scalar := bpis[k2].Lookup(date); scalar != nil {
				result[k][USD] = result[k][USD] + v2*(*scalar)
			} else {
				result[k][k2] = result[k][k2] + v2
			}
		}
	}
	return result
}

func (balances Balances) PushAll(other Balances) {
	for k, v := range other {
		if _, ok := balances[k]; !ok {
			balances[k] = make(Balance)
		}
		for k2, v2 := range v {
			if _, ok := balances[k][k2]; !ok {
				balances[k][k2] = 0
			}
			balances[k][k2] += v2
		}
	}
}

func (balances Balances) Push(d Delta) {
	if _, ok := balances[d.Name]; !ok {
		balances[d.Name] = make(Balance)
	}
	balances[d.Name].Push(d)
}

func (balance Balance) Sub(other Balance) Balance {
	return balance.Sum(other.Invert())
}

func (balance Balance) Sum(other Balance) Balance {
	result := make(Balance)
	for k, v := range balance {
		result[k] += v
	}
	for k, v := range other {
		result[k] += v
	}
	return result
}

func (balance Balance) Nonzero() Balance {
	result := make(Balance)
	for k, v := range balance {
		if v != 0 {
			result[k] = v
		}
	}
	return result
}

func (balance Balance) Invert() Balance {
	result := make(Balance)
	for k, v := range balance {
		result[k] = v * -1.0
	}
	return result
}

func (balance Balance) Push(d Delta) {
	if _, ok := balance[d.Currency]; !ok {
		balance[d.Currency] = 0
	}
	balance[d.Currency] += d.Value
}

func (balances Balances) Debug() string {
	result := []string{}
	for k, v := range balances {
		result = append(result, fmt.Sprintf("%s:[%s]", k, v.Debug()))
	}
	return strings.Join(result, " ")
}

func (balance Balance) Debug() string {
	result := []string{}
	for k, v := range balance {
		if k == USD {
			result = append(result, fmt.Sprintf("%s%.2f", k, v))
		} else {
			result = append(result, fmt.Sprintf("%.3f %s", v, k))
		}
	}
	return strings.Join(result, " + ")
}

func (balances Balances) Normalize(n Normalizer, date string) Balances {
	result := make(Balances)
	for name, balance := range balances {
		result[name] = make(Balance)
		for currency, value := range balance {
			result[name][currency] = value / n.NormalizeFactor(name, date)
		}
	}
	return result
}
