package ledger

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"regexp"
	"slices"
	"strconv"
	"strings"
	"unicode"
)

type Transaction Deltas

func (t Transaction) Deltas() Deltas {
	return Deltas(slices.Clone(t))
}

type Transactions []Transaction

func (transactions Transactions) Lookup(delta Delta) Transaction {
	for i := range transactions {
		if transactions[i][0].Transaction == delta.Transaction {
			return slices.Clone(transactions[i])
		}
	}
	return nil
}

func (transactions Transactions) Deltas() Deltas {
	result := make(Deltas, 0, len(transactions))
	for _, transaction := range transactions {
		result = append(result, transaction...)
	}
	return result
}

func (deltas Deltas) Transactions() Transactions {
	m := make(map[string]Transaction)
	for _, d := range deltas {
		m[d.Transaction] = append(m[d.Transaction], d)
	}

	result := make(Transactions, 0, len(m))
	for _, v := range m {
		result = append(result, v)
	}

	slices.SortFunc(result, func(a, b Transaction) int {
		if a[0].Date == b[0].Date {
			if a[0].Description == b[0].Description {
				return strings.Compare(a[0].Transaction, b[0].Transaction)
			}
			return strings.Compare(a[0].Description, b[0].Description)
		}
		return strings.Compare(a[0].Date, b[0].Date)
	})

	return result
}

func (transactions Transactions) NotLike(like ...Like) Transactions {
	result := make(Transactions, 0, len(transactions))
	for _, transaction := range transactions {
		if matching := (Deltas)(transaction).Like(like...); len(matching) == 0 {
			result = append(result, transaction)
		}
	}
	return result
}

func (transactions Transactions) Like(like ...Like) Transactions {
	result := make(Transactions, 0, len(transactions))
	for _, transaction := range transactions {
		if matching := (Deltas)(transaction).Like(like...); len(matching) > 0 {
			result = append(result, transaction)
		}
	}
	return result
}

func (transaction Transaction) Payee() string {
	balances := Deltas(transaction).Balances()

	candidates := []string{}

	for name, balance := range balances {
		deltas := Deltas(transaction).Like(LikeName(`^` + name + `$`))
		if len(deltas) != 1 {
			continue
		}

		everyoneElse := balances.NotLike(`^` + name + `$`).Group(`^`)[""]
		matches := true
		for currency, value := range balance {
			matches = matches && everyoneElse[currency]*-1 == value
		}
		if matches {
			candidates = append(candidates, name)
		}
	}
	slices.Sort(candidates)

	if len(candidates) == 0 {
		panic(balances)
	}

	for _, candidate := range candidates {
		if strings.HasPrefix(candidate, "Withdrawal") {
			return candidate
		}
	}
	return candidates[len(candidates)-1]
}

type transaction struct {
	date        string
	otherDates  []string
	description string
	payee       string
	recipients  []transactionRecipient
	name        string

	fileName string
	lineNo   int
}

func (t transaction) empty() bool {
	return fmt.Sprint(t) == fmt.Sprint(transaction{})
}

type transactionRecipient struct {
	name     string
	value    float64
	currency string
	isSet    bool
}

func (t transaction) deltas() Deltas {
	result := []Delta{}
	sums := map[string]float64{}
	for i, recipient := range t.recipients {
		sums[recipient.currency] += recipient.value
		result = append(result, newDelta(
			t.name,
			true,
			t.date,
			t.description,
			recipient.name,
			recipient.value,
			recipient.currency,
			recipient.isSet,
			t.fileName,
			t.lineNo+i,
			t.otherDates,
		))
	}
	for currency, value := range sums {
		if value == 0 {
			continue
		}
		if t.payee == "" {
			//return nil, fmt.Errorf("didnt find net zero and no dumping ground payee set: %+v", transaction)
		} else {
			result = append(result, newDelta(
				t.name,
				false,
				t.date,
				t.description,
				t.payee,
				-1.0*value,
				currency,
				false,
				t.fileName,
				t.lineNo,
				t.otherDates,
			))
		}
	}

	for i := range result {
		for j := range result {
			if i != j {
				result[i] = result[i].withWith(result[j])
			}
		}
	}

	return result
}

func (t transactionRecipient) empty() bool {
	return t == (transactionRecipient{})
}

func (files Files) transactions() ([]transaction, error) {
	result := make([]transaction, 0)
	for _, path := range files.paths() {
		some, err := files._transactions(path)
		if err != nil {
			return nil, err
		}
		result = append(result, some...)
	}
	return result, nil
}

func (files Files) _transactions(file string) ([]transaction, error) {
	f, err := os.Open(string(file))
	if os.IsNotExist(err) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	defer f.Close()

	r := bufio.NewReaderSize(f, 2048)

	result := make([]transaction, 0)
	for {
		name := fmt.Sprintf("%s/%d", file, len(result))
		one, err := readTransaction(name, r)
		if !one.empty() {
			one.fileName = file
			one.lineNo = len(result)
			result = append(result, one)
		}
		if err == io.EOF {
			return result, nil
		}
		if err != nil {
			return result, err
		}
	}
}

func readTransaction(name string, r *bufio.Reader) (transaction, error) {
	result, err := _readTransaction(name, r)
	if err != nil && !errors.Is(err, io.EOF) {
		return result, err
	}
	if result.empty() {
		return result, err
	}
	if result.payee != "" && len(result.recipients) < 1 {
		return result, fmt.Errorf("found a transaction with payee but no recipeints: %+v", result)
	}
	if result.payee == "" {
		if len(result.recipients) < 2 {
			return result, fmt.Errorf("found a transaction with no payee and less than 2 recipeints: %+v", result)
		}
		func() {
			sumPerRecipient := map[string]float64{}
			recipients := []string{}
			for _, recipient := range result.recipients {
				recipients = append(recipients, recipient.name)
				sumPerRecipient[recipient.name] += recipient.value
			}
			slices.Sort(recipients)

			for _, k := range recipients {
				n := 0
				for i := range result.recipients {
					if result.recipients[i].name == k {
						n += 1
					}
				}
				if n != 1 {
					continue
				}

				v := sumPerRecipient[k]
				everyoneElse := 0.0
				for j := range sumPerRecipient {
					if k != j {
						everyoneElse += sumPerRecipient[j]
					}
				}
				if -1.0*v == everyoneElse {
					result.payee = k
					result.recipients = slices.DeleteFunc(result.recipients, func(recipient transactionRecipient) bool {
						return recipient.name == k
					})
					return
				}
			}
			return
		}()
	}
	return result, err
}

func _readTransaction(name string, r *bufio.Reader) (transaction, error) {
	readTransactionLeadingWhitespace(r)

	firstLine, err := readTransactionLine(r)
	if len(bytes.TrimSpace(firstLine)) == 0 {
		return transaction{}, err
	}

	dateDescriptionPattern := regexp.MustCompile(`^([0-9]+-[0-9]+-[0-9]+)((=[0-9]+-[0-9]+-[0-9]+)*)\s+(.*)$`)
	dateDescriptionMatches := dateDescriptionPattern.FindAllStringSubmatch(string(firstLine), 4)
	if len(dateDescriptionMatches) != 1 {
		return transaction{}, fmt.Errorf("bad first line: %v matches: %q", len(dateDescriptionMatches), firstLine)
	} else if len(dateDescriptionMatches[0]) != 5 {
		return transaction{}, fmt.Errorf("bad first line: %v submatches: %q", len(dateDescriptionMatches[0]), firstLine)
	}
	result := transaction{
		date:        dateDescriptionMatches[0][1],
		otherDates:  strings.Split(strings.Trim(dateDescriptionMatches[0][2], "="), "="),
		description: dateDescriptionMatches[0][4],
		name:        name,
	}

	for {
		name, value, currency, isSet, err := readTransactionName(r)
		if name != "" {
			if currency == "" {
				result.payee = name
			} else {
				result.recipients = append(result.recipients, transactionRecipient{
					name:     name,
					value:    value,
					currency: currency,
					isSet:    isSet,
				})
			}
		}
		if name == "" || err != nil {
			return result, err
		}
	}
}

func readTransactionLeadingWhitespace(r *bufio.Reader) {
	b, err := r.Peek(2048)
	if err != nil && err != io.EOF {
		return
	}

	i := 0
	for i < len(b) {
		if len(bytes.TrimSpace(b[:i])) != 0 {
			break
		}
		i++
	}

	if i > 0 {
		r.Read(make([]byte, i-1))
	}
}

func readTransactionLine(r *bufio.Reader) ([]byte, error) {
	for {
		b, err := _readTransactionLine(r)
		if err != nil || (len(bytes.TrimSpace(b)) > 0 && bytes.TrimSpace(b)[0] != '#') {
			return b, err
		}
	}
}

func _readTransactionLine(r *bufio.Reader) ([]byte, error) {
	b, err := r.Peek(2048)
	if len(b) == 0 {
		return nil, err
	}

	endOfLine := len(b)
	if idx := bytes.Index(b, []byte{'\n'}); idx > -1 {
		endOfLine = idx
	}

	b2 := make([]byte, endOfLine)
	n, err := r.Read(b2)
	if err == io.EOF {
		err = nil
	}

	if check, _ := r.Peek(1); len(check) == 1 && check[0] == '\n' {
		r.Read(make([]byte, 1))
	}

	return b2[:n], err
}

func readTransactionName(r *bufio.Reader) (string, float64, string, bool, error) {
	line, err := readTransactionLine(r)
	if err != nil {
		return "", 0, "", false, err
	}

	if len(line) > 0 && !unicode.IsSpace(rune(line[0])) {
		r2 := *r
		*r = *bufio.NewReader(io.MultiReader(bytes.NewReader(append(line, '\n')), &r2))
		return "", 0, "", false, nil
	}

	line = bytes.Split(line, []byte(";"))[0] // comment-free
	fields := bytes.Fields(line)

	isSet := false
	if len(fields) > 2 && string(fields[1]) == "=" {
		isSet = true
		fields = append(fields[:1], fields[2:]...)
	}

	switch len(fields) {
	case 1: // payee
		return string(fields[0]), 0, "", false, nil
	case 2: // payee $00.00
		b := bytes.TrimLeft(fields[1], "$")
		value, err := strconv.ParseFloat(string(b), 64)
		if err != nil {
			return "", 0, "", isSet, fmt.Errorf("failed to parse value from $XX.YY from %q (%q): %w", line, fields[1], err)
		}
		return string(fields[0]), value, string(USD), isSet, nil
	case 3: // payee 00.00 XYZ
		value, err := strconv.ParseFloat(string(fields[1]), 64)
		if err != nil {
			return "", 0, "", false, fmt.Errorf("failed to parse value from XX.YY XYZ from %q (%q): %w", line, fields[1], err)
		}
		return string(fields[0]), value, string(fields[2]), isSet, nil
	default:
		return "", 0, "", isSet, fmt.Errorf("cannot interpret %q", line)
	}
}
