package cli

import (
	"bufio"
	"context"
	"encoding/csv"
	"flag"
	"fmt"
	"io"
	"log"
	"maps"
	"math"
	"os"
	"os/signal"
	"slices"
	"strconv"
	"strings"
	"syscall"
	"time"

	"gogs.inhome.blapointe.com/ana-ledger/src/ana"
	"gogs.inhome.blapointe.com/ana-ledger/src/bank"
	"gogs.inhome.blapointe.com/ana-ledger/src/bank/cache"
	"gogs.inhome.blapointe.com/ana-ledger/src/bank/teller"
	"gogs.inhome.blapointe.com/ana-ledger/src/ledger"

	"github.com/guptarohit/asciigraph"
	"golang.org/x/crypto/ssh/terminal"
)

func Main() {
	var config Config

	fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
	fs.Var(&config.Files, "f", "paths to files")
	fs.Var(&config.Query.Period, "period", "period can be YYYY, YYYY-mm, YYYY-mm-dd, x..y")
	fs.StringVar(&config.Query.Sort, "S", "", "sort ie date")
	fs.BoolVar(&config.Query.NoRounding, "no-rounding", false, "no rounding")
	fs.BoolVar(&config.Compact, "c", false, "reg entries oneline")
	fs.StringVar(&config.Query.With, "w", "", "regexp for transactions")
	fs.StringVar(&config.CSV, "csv", "", "if csv then the csv")
	fs.IntVar(&config.Query.Depth, "depth", 0, "depth grouping")
	fs.BoolVar(&config.Query.Normalize, "n", false, "normalize with default normalizer")
	fs.BoolVar(&config.Query.USDOnly, "usd", false, "filter to usd")
	fs.BoolVar(&config.Query.NoExchanging, "no-exchanging", true, "omit currency exchanges")
	fs.StringVar(&config.BPI, "bpi", "", "path to bpi")
	fs.StringVar(&config.CPI, "cpi", "", "path to cpi")
	fs.StringVar(&config.GroupDate, "group-date", "^....-..-..", "date grouping")
	fs.IntVar(&config.CPIYear, "cpiy", 0, "use cpi to convert usd to this year's value")
	fs.BoolVar(&config.NoPercent, "no-percent", false, "compute percent")
	if err := fs.Parse(os.Args[1:]); err != nil {
		panic(err)
	}

	files := config.Files.Strings()
	if len(files) == 0 {
		log.Fatalf("must specify at least one file")
	}
	ledgerFiles, err := ledger.NewFiles(files[0], files[1:]...)
	if err != nil {
		log.Fatalf("%v", err)
	}

	positional := fs.Args()
	if len(positional) == 0 || len(positional[0]) < 3 {
		log.Fatalf("positional arguments required, ie bal|reg PATTERN MATCHING")
	}
	cmd := positional[0]

	q, err := BuildQuery(config, positional[1:])
	if err != nil {
		log.Fatalf("%v", err)
	}

	deltas, err := ledgerFiles.Deltas()
	if err != nil {
		log.Fatalf("%v", err)
	}

	if period := config.Query.Period; !period.Empty() {
		after := period.Start.Format("2006-01-02")
		before := period.Stop.Format("2006-01-02")
		deltas = deltas.Like(
			ledger.LikeAfter(after),
			ledger.LikeBefore(before),
		)
	}

	pattern := ".*"
	if depth := config.Query.Depth; depth > 0 {
		patterns := []string{}
		pattern = ""
		for i := 0; i < depth; i++ {
			pattern += "[^:]*:"
			patterns = append([]string{strings.Trim(pattern, ":")}, patterns...)
		}
		pattern = strings.Join(patterns, "|")
	}
	group := ledger.GroupName(pattern)

	bpis := make(ledger.BPIs)
	if config.BPI != "" {
		b, err := ledger.NewBPIs(config.BPI)
		if err != nil {
			log.Fatalf("%v", err)
		}
		bpis = b
		if period := config.Query.Period; !period.Empty() {
			before := period.Stop.Format("2006-01-02")
			for _, timeToValue := range bpis {
				maps.DeleteFunc(timeToValue, func(k string, _ float64) bool {
					return k > before
				})
			}
		}
	}

	cpiNormalizer := ana.NewNormalizer()
	if config.CPI != "" && config.CPIYear > 0 {
		c, err := ledger.NewBPIs(config.CPI)
		if err != nil {
			log.Fatalf("%v", err)
		}
		cpi := c["CPI"]
		cpiy := cpi.Lookup(fmt.Sprintf("%d-06-01", config.CPIYear))
		if cpiy == nil {
			log.Fatalf("no cpi for year %d", config.CPIYear)
		}

		for date, value := range cpi {
			cpiNormalizer = cpiNormalizer.With(".*", date, value/(*cpiy))
		}
	}

	if config.Query.Normalize {
		deltas = ana.NewDefaultNormalizer().Normalize(deltas)
	}

	if config.Query.With != "" {
		deltas = deltas.Like(ledger.LikeWith(config.Query.With))
	}

	w := bufio.NewWriter(os.Stdout)
	defer w.Flush()

	maxAccW := 0
	for _, delta := range deltas.Like(q).Group(group) {
		if accW := len(delta.Name); accW > maxAccW {
			maxAccW = accW
		}
	}

	switch cmd[:3] {
	case "bal": // balances
		balances := deltas.Group(ledger.GroupDate(config.GroupDate)).Balances().
			WithBPIs(bpis).
			KindaLike(q).
			KindaGroup(group).
			Nonzero().
			Normalize(cpiNormalizer, "9")

		cumulatives := make(ledger.Balances)
		cumulativesFormat := "%s%.2f"
		if !config.NoPercent {
			var sum float64
			for key := range balances {
				if _, ok := cumulatives[key]; !ok {
					cumulatives[key] = make(ledger.Balance)
				}
				for currency, val := range balances[key] {
					if currency == ledger.USD {
						cumulatives[key][currency] = val
						sum += val
					} else {
						cumulatives[key][currency] = 0
					}
				}
			}
			for key := range cumulatives {
				cumulatives[key][ledger.USD] = 100 * cumulatives[key][ledger.USD] / sum
			}
			cumulativesFormat = "%.0f%%"
		}

		FPrintBalances(w, "", balances, cumulatives, config.Query.USDOnly, config.Query.Normalize, time.Now().Format("2006-01-02"), false, maxAccW, cumulativesFormat)
	case "gra": // graph
		dateGrouping := "^[0-9]{4}-[0-9]{2}"
		if period := config.Query.Period; !period.Empty() {
			day := time.Hour * 24
			year := day * 365
			r := period.Stop.Sub(period.Start)
			if r > 10*year {
				dateGrouping = "^[0-9]{4}"
			} else if r > 5*year {
			} else if r > year {
				dateGrouping = "^[0-9]{4}-[0-9]{2}-[0-9]"
			} else {
				dateGrouping = "^[0-9]{4}-[0-9]{2}-[0-9]{2}"
			}
		}
		deltas = deltas.Group(ledger.GroupDate(dateGrouping))

		transactions := deltas.Transactions()
		cumulative := make(ledger.Balances)
		data := map[string][]float64{}
		pushData := func() {
			soFar := 0
			for _, v := range data {
				soFar = len(v)
			}
			for k, balance := range cumulative {
				for curr, v := range balance {
					if curr != ledger.USD {
						continue
					}
					if _, ok := data[k]; !ok {
						data[k] = make([]float64, soFar)
					}
					data[k] = append(data[k], v)
				}
			}
		}
		for i, transaction := range transactions {
			if i > 0 && transactions[i-1][0].Date != transaction[0].Date {
				pushData()
			}
			balances := ledger.Deltas(transaction).
				Like(q).
				Group(group).
				Balances().
				WithBPIsAt(bpis, transaction[0].Date).
				Nonzero().
				Normalize(cpiNormalizer, transaction[0].Date)
			cumulative.PushAll(balances)
		}
		pushData()

		labels := []string{}
		for k := range data {
			labels = append(labels, k)
		}
		slices.Sort(labels)

		points := [][]float64{}
		for _, k := range labels {
			points = append(points, data[k])
		}
		for i := range points {
			for j := range points[i] {
				points[i][j] /= 1000.0
			}
		}

		options := []asciigraph.Option{asciigraph.Precision(0)}
		if _, h, err := terminal.GetSize(0); err == nil && h > 4 {
			options = append(options, asciigraph.Height(h-4))
		}
		if len(labels) < 256 {
			seriesColors := make([]asciigraph.AnsiColor, len(labels))
			for i := range seriesColors {
				seriesColors[i] = asciigraph.AnsiColor(i)
			}
			options = append(options, asciigraph.SeriesLegends(labels...))
			options = append(options, asciigraph.SeriesColors(seriesColors...))
		}
		fmt.Println(asciigraph.PlotMany(points, options...))
	case "rec": // reconcile via teller // DEAD
		log.Fatalf("dead and bad")
		byDate := map[string]ledger.Deltas{}
		for _, delta := range deltas {
			delta := delta
			byDate[delta.Date] = append(byDate[delta.Date], delta)
		}

		ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
		defer can()

		teller, err := teller.New()
		if err != nil {
			log.Fatalf("%v", err)
		}
		client := cache.New(teller)

		accounts, err := client.Accounts(ctx)
		if err != nil {
			log.Fatalf("%v", err)
		}

		inDay := func(date string, transaction bank.Transaction) bool {
			return slices.ContainsFunc(byDate[date], func(d ledger.Delta) bool {
				v := fmt.Sprintf("%.2f", d.Value)
				nv := fmt.Sprintf("%.2f", -1.0*d.Value)
				a := fmt.Sprintf("%.2f", transaction.Amount)
				return v == a || nv == a
			})
		}

		for _, acc := range accounts {
			transactions, err := client.Transactions(ctx, acc)
			if err != nil {
				log.Fatalf("%v", err)
			}

			for _, transaction := range transactions {
				if inDay(transaction.Date, transaction) {
					continue
				}

				msg := "missing"

				ts, err := time.ParseInLocation("2006-01-02", transaction.Date, time.Local)
				if err != nil {
					log.Fatalf("%v", err)
				}
				dayBefore := ts.Add(-24 * time.Hour).Format("2006-01-02")
				dayAfter := ts.Add(24 * time.Hour).Format("2006-01-02")
				if inDay(dayBefore, transaction) || inDay(dayAfter, transaction) {
					msg = "1dayoff"
				}

				prefix := "  "
				if transaction.Status != "posted" {
					prefix = "! "
				}
				fmt.Printf("[%s] %s  $%7.2f  %s%s (%s)\n", msg, transaction.Date, transaction.Amount, prefix, transaction.Details.CounterParty.Name, transaction.Description)
			}
		}
	case "reg": // reg
		deltas = deltas.Group(ledger.GroupDate(config.GroupDate))
		transactions := deltas.Transactions()
		cumulative := make(ledger.Balances)
		for _, transaction := range transactions {
			balances := ledger.Deltas(transaction).Like(q).Group(group).Balances().WithBPIsAt(bpis, transaction[0].Date).Nonzero().Normalize(cpiNormalizer, transaction[0].Date)
			shouldPrint := false
			shouldPrint = shouldPrint || len(balances) > 2
			if config.Query.NoExchanging {
				shouldPrint = shouldPrint || len(balances) > 1
				for _, v := range balances {
					shouldPrint = shouldPrint || len(v) == 1
				}
			} else {
				shouldPrint = shouldPrint || len(balances) > 0
			}
			if shouldPrint {
				cumulative.PushAll(balances)
				cumulative = cumulative.Nonzero()
				FPrintBalancesFor(transaction[0].Description, w, "\t\t", balances, cumulative, config.Query.USDOnly, config.Query.Normalize, transaction[0].Date, config.Compact, maxAccW, "%s%.2f")
			}
		}
	case "csv": // reconcile with given csv
		deltas := deltas.Group(group)

		if config.CSV == "" {
			log.Fatalf("missing required -csv")
		}
		f, err := os.Open(config.CSV)
		if err != nil {
			log.Fatalf("cannot open csv %q: %v", config.CSV, err)
		}
		defer f.Close()
		reader := csv.NewReader(f)
		fields, err := reader.Read()
		if err != nil {
			log.Fatal(err)
		}

		dateIdxs := []int{}
		for i := range fields {
			if strings.Contains(strings.ToLower(fields[i]), "date") {
				dateIdxs = append(dateIdxs, i)
			}
		}

		descIdx := slices.IndexFunc(fields, func(field string) bool {
			return strings.Contains(strings.ToLower(field), "desc")
		})

		amountIdx := slices.IndexFunc(fields, func(field string) bool {
			return strings.Contains(strings.ToLower(field), "amount")
		})

		csvDeltas := make(ledger.Deltas, 0)
		for {
			line, err := reader.Read()
			if err == io.EOF {
				break
			}
			if err != nil {
				log.Fatalf("failed to read csv line: %v", err)
			}

			dates := []string{}
			for _, dateIdx := range dateIdxs {
				dates = append(dates, normalizeDate(line[dateIdx]))
			}
			dates = slices.Compact(dates)
			desc := line[descIdx]
			amount := line[amountIdx]

			amountF, err := strconv.ParseFloat(amount, 32)
			if err != nil {
				log.Fatalf("non-float amount %q: %v", amount, err)
			}
			csvDeltas = append(csvDeltas, ledger.Delta{
				Date:        dates[0],
				OtherDates:  dates[1:],
				Name:        desc,
				Value:       amountF,
				Currency:    ledger.USD,
				Description: desc,
				Payee:       true,
			})
		}

		matched := map[string]struct{}{}
		matchies := func(deltas ledger.Deltas, delta ledger.Delta) (ledger.Deltas, []string) {
			deltas = slices.Clone(deltas)
			dates := append([]string{delta.Date}, delta.OtherDates...)
			matches := deltas.Like(func(delta ledger.Delta) bool {
				return delta.Date >= slices.Min(dates)
			})
			matches = matches.Like(func(delta ledger.Delta) bool {
				return delta.Date <= slices.Max(dates)
			})
			matches = matches.Like(func(delta ledger.Delta) bool {
				return delta.Currency == ledger.USD
			})
			matches = matches.Like(func(d2 ledger.Delta) bool {
				return fmt.Sprintf("%.2f", d2.Value) == fmt.Sprintf("%.2f", delta.Value)
			})
			for _, match := range matches {
				if _, ok := matched[match.ID()]; !ok {
					matched[match.ID()] = struct{}{}
					break
				}
			}
			return matches, dates
		}

		datesMatched := []string{}
		namesMatched := []string{}
		nonAssetNamesMatched := []string{}
		for _, csvDelta := range csvDeltas {
			matches, dates := matchies(deltas, csvDelta)
			if len(matches) == 0 {
				fmt.Printf("unique to csv    | %s   %s   %.2f\n", strings.Join(dates, "="), csvDelta.Name, csvDelta.Value)
			}
			for _, match := range matches {
				datesMatched = append(datesMatched, match.Date)
				datesMatched = append(datesMatched, match.OtherDates...)
				namesMatched = append(namesMatched, match.Name)
			}
		}
		nonAssetNamesMatched = slices.DeleteFunc(slices.Clone(namesMatched), func(name string) bool {
			return strings.Contains(name, "Asset")
		})
		datesMatched = slices.DeleteFunc(datesMatched, func(a string) bool { return strings.TrimSpace(a) == "" })
		datesMatched = slices.Compact(datesMatched)
		namesMatched = slices.Compact(namesMatched)

		transactions := deltas.Transactions()
		deltas = deltas.Like(func(delta ledger.Delta) bool {
			return delta.Date >= slices.Min(datesMatched)
		})
		deltas = deltas.Like(func(delta ledger.Delta) bool {
			return delta.Date <= slices.Max(datesMatched)
		})
		deltas = deltas.Like(func(delta ledger.Delta) bool {
			return slices.Contains(namesMatched, delta.Name)
		})
		deltas = deltas.Like(func(delta ledger.Delta) bool {
			xaction := transactions.Lookup(delta)
			if noXactionFound := len(xaction) == 0; noXactionFound {
				return false
			}

			names := []string{}
			for _, delta := range xaction.Deltas() {
				names = append(names, delta.Name)
			}
			slices.Sort(names)
			_ = names

			allTheSame := true
			for i := range xaction {
				for j := i + 1; j < len(xaction); j++ {
					allTheSame = allTheSame && (xaction[i].Name == xaction[j].Name)
				}
			}
			if allTheSame {
				return false
			}

			if anyNameNotRelevant := slices.ContainsFunc(xaction.Deltas(), func(some ledger.Delta) bool {
				return !slices.Contains(namesMatched, delta.Name)
			}); anyNameNotRelevant {
				return false
			}

			if hasNonAssetNameMatch := slices.ContainsFunc(xaction.Deltas(), func(some ledger.Delta) bool {
				return slices.Contains(nonAssetNamesMatched, delta.Name)
			}); !hasNonAssetNameMatch {
				return false
			}

			return true
		})

		deltasSum := deltas.Group(ledger.GroupDate(""), ledger.GroupName("")).Balances()[""][ledger.USD]
		csvSum := csvDeltas.Group(ledger.GroupDate(""), ledger.GroupName("")).Balances()[""][ledger.USD]
		if deltasSum != csvSum {
			log.Printf("csv sum %.2f but deltas sum %.2f", csvSum, deltasSum)
		}

		for _, delta := range deltas {
			matches, dates := matchies(csvDeltas, delta)
			if len(matches) == 0 {
				fmt.Printf("unique to ledger | %s   %s   %.2f\n", strings.Join(dates, "="), delta.Description, delta.Value)
			}
		}

		for _, delta := range deltas {
			if _, ok := matched[delta.ID()]; !ok {
				fmt.Printf("unmatched ledger | %s   %s   %.2f\n", delta.Date, delta.Description, delta.Value)
			}
		}
		for _, delta := range csvDeltas {
			if _, ok := matched[delta.ID()]; !ok {
				fmt.Printf("unmatched csv    | %s   %s   %.2f\n", delta.Date, delta.Description, delta.Value)
			}
		}
	default:
		log.Fatalf("unknown command %q", positional[0])
	}
}

func FPrintBalancesFor(description string, w io.Writer, linePrefix string, balances, cumulatives ledger.Balances, usdOnly, normalized bool, date string, compact bool, keyW int, cumulativeFormat string) {
	if compact {
		FPrintBalances(w, date+"\t", balances, cumulatives, usdOnly, normalized, date, compact, keyW, cumulativeFormat)
	} else {
		fmt.Fprintf(w, "%s\t%s\n", date, description)
		FPrintBalances(w, linePrefix, balances, cumulatives, usdOnly, normalized, date, compact, keyW, cumulativeFormat)
	}
}

func FPrintBalances(w io.Writer, linePrefix string, balances, cumulatives ledger.Balances, usdOnly, normalized bool, date string, fullKey bool, max int, cumulativeFormat string) {
	maxes := map[ledger.Currency]float64{}
	keys := []string{}
	for k, v := range balances {
		keys = append(keys, k)
		for k2, v2 := range v {
			if math.Abs(v2) > math.Abs(maxes[k2]) {
				maxes[k2] = math.Abs(v2)
			}
		}
	}
	slices.Sort(keys)

	normalizer := ana.NewDefaultNormalizer()

	cumulativeFormat = strings.ReplaceAll(cumulativeFormat, "%", "%%")
	format := fmt.Sprintf("%s%%-%ds\t%%s%%.2f ("+cumulativeFormat+")\n", linePrefix, max)
	if normalized {
		format = fmt.Sprintf("%s%%-%ds\t%%s%%.2f (%%s%%.2f (%%.2f @%%.2f ("+cumulativeFormat+")))\n", linePrefix, max)
	}
	for i, key := range keys {
		printableKey := key
		if fullKey {
		} else if i == 0 {
		} else {
			commonPrefixLen := func() int {
				j := 0
				n := len(keys[i])
				if n2 := len(keys[i-1]); n2 < n {
					n = n2
				}
				for j = 0; j < n; j++ {
					if keys[i-1][j] != keys[i][j] {
						break
					}
				}
				for keys[i][j] != ':' && j > 0 {
					j -= 1
				}
				return j
			}()
			printableKey = strings.Repeat(" ", commonPrefixLen) + keys[i][commonPrefixLen:]
		}

		currencies := []ledger.Currency{}
		for currency := range balances[key] {
			currencies = append(currencies, currency)
		}
		slices.Sort(currencies)

		for _, currency := range currencies {
			printableCurrency := currency
			format := format
			if printableCurrency != "$" {
				printableCurrency += " "
				format = strings.ReplaceAll(format, "%.2f", "%.3f")
			}
			if usdOnly && printableCurrency != "$" {
				continue
			}

			cumulative := balances[key][currency]
			if balance, ok := cumulatives[key]; !ok {
			} else if value, ok := balance[currency]; !ok {
			} else {
				cumulative = value
			}

			printingPercents := strings.Contains(cumulativeFormat, "%%%%")
			if !printingPercents {
				if !normalized {
					fmt.Fprintf(w, format,
						printableKey,
						printableCurrency, balances[key][currency],
						printableCurrency, cumulative,
					)
				} else {
					factor := normalizer.NormalizeFactor(key, date)
					trailingMax := maxes[currency] - math.Abs(balances[key][currency])
					fmt.Fprintf(w, format, printableKey, printableCurrency, balances[key][currency], printableCurrency, cumulative, cumulative*factor, factor, printableCurrency, factor*trailingMax)
				}
			} else {
				if !normalized {
					fmt.Fprintf(w, format, printableKey, printableCurrency, balances[key][currency], cumulative)
				} else {
					factor := normalizer.NormalizeFactor(key, date)
					trailingMax := maxes[currency] - math.Abs(balances[key][currency])
					fmt.Fprintf(w, format, printableKey, printableCurrency, balances[key][currency], printableCurrency, cumulative, cumulative*factor, factor, factor*trailingMax)
				}
			}
		}
	}
}

func normalizeDate(date string) string {
	for _, layout := range []string{
		"01/02/2006",
	} {
		if t, err := time.Parse(layout, date); err == nil {
			return t.Format("2006-01-02")
		}
	}
	log.Fatalf("cannot normalize date %q", date)
	return ""
}
