package xmlmodels

import (
	"fmt"
	"html/template"
	"log/slog"
	"maps"
	"path/filepath"
	"slices"
	"strconv"
	"strings"
	"sync"

	gitpkg "github.com/Theodor-Springmann-Stiftung/lenz-web/git"
	"github.com/Theodor-Springmann-Stiftung/lenz-web/xmlparsing"
)

const (
	REFERENCES_PATH = "data/xml/references.xml"
	LETTERS_PATH    = "data/xml/briefe.xml"
	META_PATH       = "data/xml/meta.xml"
	TRADITIONS_PATH = "data/xml/traditions.xml"
)

type Library struct {
	Commit gitpkg.Commit

	Persons *xmlparsing.XMLParser[PersonDef]
	Places  *xmlparsing.XMLParser[LocationDef]
	AppDefs *xmlparsing.XMLParser[AppDef]

	Letters    *xmlparsing.XMLParser[Letter]
	Traditions *xmlparsing.XMLParser[Tradition]
	Metas      *xmlparsing.XMLParser[Meta]

	cache sync.Map
}

func (l *Library) String() string {
	sb := strings.Builder{}

	sb.WriteString("Persons: ")
	sb.WriteString(strconv.Itoa(l.Persons.Count()))
	sb.WriteString("\n")

	sb.WriteString("Places: ")
	sb.WriteString(strconv.Itoa(l.Places.Count()))
	sb.WriteString("\n")

	sb.WriteString("AppDefs: ")
	sb.WriteString(strconv.Itoa(l.AppDefs.Count()))
	sb.WriteString("\n")

	sb.WriteString("Letters: ")
	sb.WriteString(strconv.Itoa(l.Letters.Count()))
	filter := func(item Letter) bool {
		return len(item.Hands) > 0
	}
	hands := 0
	for l := range l.Letters.Filter(filter) {
		hands += 1
		sb.WriteString("\n")
		sb.WriteString(strconv.Itoa(l.Letter) + ": ")
		sb.WriteString(strconv.Itoa(len(l.Hands)) + " hands, No " + strconv.Itoa(hands))
	}
	sb.WriteString("\n")

	sb.WriteString("Traditions: ")
	sb.WriteString(strconv.Itoa(l.Traditions.Count()))
	sb.WriteString("\n")

	sb.WriteString("Metas: ")
	sb.WriteString(strconv.Itoa(l.Metas.Count()))
	sb.WriteString("\n")

	return sb.String()
}

func NewLibrary(baseDir string, commit *gitpkg.Commit) (*Library, error) {
	lib := &Library{
		Persons:    xmlparsing.NewXMLParser[PersonDef](),
		Places:     xmlparsing.NewXMLParser[LocationDef](),
		AppDefs:    xmlparsing.NewXMLParser[AppDef](),
		Letters:    xmlparsing.NewXMLParser[Letter](),
		Traditions: xmlparsing.NewXMLParser[Tradition](),
		Metas:      xmlparsing.NewXMLParser[Meta](),
	}
	if commit != nil {
		lib.Commit = *commit
	}

	if err := lib.parse(baseDir); err != nil {
		return nil, err
	}

	return lib, nil
}

func (l *Library) parse(baseDir string) error {
	l.cache.Clear()

	wg := sync.WaitGroup{}
	failedPaths := make([]string, 0)
	failedMu := sync.Mutex{}

	parse := func(fn func() error, path string, label string) {
		wg.Add(1)
		go func() {
			defer wg.Done()
			if err := fn(); err != nil {
				failedMu.Lock()
				slog.Error("Failed to serialize "+label+":", "error", err)
				failedPaths = append(failedPaths, filepath.Join(baseDir, path))
				failedMu.Unlock()
			}
		}()
	}

	// References must be ready before dependent documents (hands etc.) resolve correctly.
	parse(func() error {
		return l.Persons.Serialize(&PersonDefs{}, filepath.Join(baseDir, REFERENCES_PATH))
	}, REFERENCES_PATH, "persons")
	parse(func() error {
		return l.Places.Serialize(&LocationDefs{}, filepath.Join(baseDir, REFERENCES_PATH))
	}, REFERENCES_PATH, "places")
	parse(func() error {
		return l.AppDefs.Serialize(&AppDefs{}, filepath.Join(baseDir, REFERENCES_PATH))
	}, REFERENCES_PATH, "appdefs")
	wg.Wait()

	// Remaining documents can be parsed once references are available.
	parse(func() error {
		return l.Letters.Serialize(&DocumentsRoot{}, filepath.Join(baseDir, LETTERS_PATH))
	}, LETTERS_PATH, "letters")
	parse(func() error {
		return l.Traditions.Serialize(&TraditionsRoot{}, filepath.Join(baseDir, TRADITIONS_PATH))
	}, TRADITIONS_PATH, "traditions")
	parse(func() error {
		return l.Metas.Serialize(&MetaRoot{}, filepath.Join(baseDir, META_PATH))
	}, META_PATH, "meta")

	wg.Wait()

	if len(failedPaths) > 0 {
		return fmt.Errorf("parsing encountered errors: failed paths: %v", failedPaths)
	}

	return nil
}

type NextPrev struct {
	Next, Prev *Meta
}

func (l *Library) NextPrev(meta *Meta) *NextPrev {
	year := meta.Earliest().Sort().Year
	years, yearmap := l.Years()
	var next, prev *Meta
	for i, y := range yearmap[year] {
		if y.Letter == meta.Letter {
			if i > 0 {
				prev = &yearmap[year][i-1]
			} else {
				index := slices.Index(years, year)
				if index > 0 {
					prev = &yearmap[years[index-1]][len(yearmap[years[index-1]])-1]
				}
			}
			if i < len(yearmap[year])-1 {
				next = &yearmap[year][i+1]
			} else {
				index := slices.Index(years, year)
				if index < len(years)-1 {
					next = &yearmap[years[index+1]][0]
				}
			}
			break
		}
	}

	return &NextPrev{Next: next, Prev: prev}
}

func (l *Library) Years() ([]int, map[int][]Meta) {
	if years, ok := l.cache.Load("years"); ok {
		if yearmap, ok := l.cache.Load("yearmap"); ok {
			return years.([]int), yearmap.(map[int][]Meta)
		}
	}

	mapYears := make(map[int][]Meta)
	for item := range l.Metas.Iterate() {
		earliest := item.Earliest()
		if earliest != nil {
			mapYears[earliest.Sort().Year] = append(mapYears[earliest.Sort().Year], item)
		}
	}

	ret := slices.Collect(maps.Keys(mapYears))
	slices.Sort(ret)

	for _, items := range mapYears {
		slices.SortFunc(items, func(a, b Meta) int {
			return a.Earliest().Sort().Compare(b.Earliest().Sort())
		})
	}
	l.cache.Store("years", ret)
	l.cache.Store("yearmap", mapYears)
	return ret, mapYears
}

func (l *Library) LettersForYear(year int) (ret []Meta) {
	for l := range l.Metas.Filter(func(item Meta) bool {
		return item.Earliest().Sort().Year == year
	}) {
		ret = append(ret, l)
	}
	return
}

func (l *Library) Person(id int) (ret *PersonDef) {
	ret = l.Persons.Item(id)
	return
}

func (l *Library) App(id int) (ret *AppDef) {
	ret = l.AppDefs.Item(id)
	return
}

func (l *Library) Place(id int) (ret *LocationDef) {
	ret = l.Places.Item(id)
	return
}

func (l *Library) Tradition(letter int) (ret []App) {
	item := l.Traditions.Item(letter)
	if item == nil {
		return []App{}
	}

	return item.Apps
}

func (l *Library) GetPersons(id []int) (ret []*PersonDef) {
	for _, i := range id {
		ret = append(ret, l.Person(i))
	}
	return
}

func (l *Library) GetPlaces(id []int) (ret []*LocationDef) {
	for _, i := range id {
		ret = append(ret, l.Place(i))
	}
	return
}

func (l *Library) FuncMap() template.FuncMap {
	return template.FuncMap{
		"Person":    l.Person,
		"Place":     l.Place,
		"Persons":   l.GetPersons,
		"Places":    l.GetPlaces,
		"App":       l.App,
		"Tradition": l.Tradition,
	}
}
