package cli

import (
	"bufio"
	"context"
	"flag"
	"fmt"
	"io"
	"math"
	"os"
	"os/signal"
	"slices"
	"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.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.IntVar(&config.CPIYear, "cpiy", 0, "use cpi to convert usd to this year's value")
	if err := fs.Parse(os.Args[1:]); err != nil {
		panic(err)
	}

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

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

	q, err := BuildQuery(config, positional[1:])
	if err != nil {
		panic(err)
	}

	deltas, err := ledgerFiles.Deltas()
	if err != nil {
		panic(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 {
			panic(err)
		}
		bpis = b
	}

	cpiNormalizer := ana.NewNormalizer()
	if config.CPI != "" && config.CPIYear > 0 {
		c, err := ledger.NewBPIs(config.CPI)
		if err != nil {
			panic(err)
		}
		cpi := c["CPI"]
		cpiy := cpi.Lookup(fmt.Sprintf("%d-06-01", config.CPIYear))
		if cpiy == nil {
			panic(fmt.Errorf("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 := deltas.Balances().
			WithBPIs(bpis).
			KindaLike(q).
			KindaGroup(group).
			Nonzero().
			Normalize(cpiNormalizer, "9")
		FPrintBalances(w, "", balances, nil, config.Query.USDOnly, config.Query.Normalize, time.Now().Format("2006-01-02"), false, maxAccW)
	case "gra":
		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":
		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 {
			panic(err)
		}
		client := cache.New(teller)

		accounts, err := client.Accounts(ctx)
		if err != nil {
			panic(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 {
				panic(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 {
					panic(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":
		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)
			}
		}
	default:
		panic("unknown command " + positional[0])
	}
}

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

func FPrintBalances(w io.Writer, linePrefix string, balances, cumulatives ledger.Balances, usdOnly, normalized bool, date string, fullKey bool, max int) {
	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()

	format := fmt.Sprintf("%s%%-%ds\t%%s%%.2f (%%s%%.2f)\n", linePrefix, max)
	if normalized {
		format = fmt.Sprintf("%s%%-%ds\t%%s%%.2f (%%s%%.2f (%%.2f @%%.2f (%%s%%.0f)))\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
			}

			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)
			}
		}
	}
}
