package bundle

import (
	"encoding/json"
	"fmt"
	"io/fs"
	"path"
	"strings"

	"code.justin.tv/cplat/twitchling/localization/language"
	"code.justin.tv/cplat/twitchling/localization/message"
	"code.justin.tv/cplat/twitchling/project"
)

type Variant string

const (
	Variant_Default Variant = "default"
)

type MessageID string

type bundleMap map[language.Tag]map[MessageID]map[Variant]message.Message

type Bundle struct {
	messages bundleMap
}

// Creates a message bundle from the specified directory.
// This function may return a partial bundle along with an error indicating what failed while loading the bundle.
func New(bundleDir fs.FS) (*Bundle, error) {
	translatedMessages, tErr := loadTranslations(bundleDir)
	messages, mErr := parseBundle(translatedMessages)

	err := tErr.combine(mErr)
	if err != nil {
		return &Bundle{messages}, err
	}

	return &Bundle{messages}, nil
}

func (m *Bundle) Get(language language.Tag, messageID MessageID, variant Variant) (*message.Message, error) {
	message, ok := m.messages[language][messageID][variant]
	if !ok {
		return nil, fmt.Errorf("no message %s:%s found for language %s", messageID, variant, language)
	}

	return &message, nil
}

func parseBundle(translatedMessages map[language.Tag]project.MessageSet) (bundleMap, *MessageBundleError) {
	var errors []error
	bundle := bundleMap{}

	for lang, messageSet := range translatedMessages {
		for _, messages := range messageSet.Messages {
			for variantID, variant := range messages.Variants {
				messageFormat, err := message.New(lang, variant)
				if err != nil {
					errors = append(errors, err)
					continue
				}

				messageID := MessageID(messages.ID)

				if bundle[lang] == nil {
					bundle[lang] = map[MessageID]map[Variant]message.Message{}
				}

				if bundle[lang][messageID] == nil {
					bundle[lang][messageID] = map[Variant]message.Message{}
				}

				bundle[lang][MessageID(messages.ID)][Variant(variantID)] = *messageFormat
			}
		}
	}

	return bundle, newMessageBundleError(errors)
}

// This can still return an error even if it manages to load other translations in order to assist in catching issues with loading some translations.
func loadTranslations(bundleDir fs.FS) (map[language.Tag]project.MessageSet, *MessageBundleError) {
	translations := map[language.Tag]project.MessageSet{}
	var errors []error

	var messageFiles map[language.Tag]string = map[language.Tag]string{}
	var knownBundleFileNames map[string]language.Tag = map[string]language.Tag{}

	// Create a map of expected files to help speed things up!
	for _, lang := range language.SupportedLanguages {
		knownBundleFileNames[fmt.Sprintf("%s.json", lang)] = lang
	}

	err := fs.WalkDir(bundleDir, ".", func(filePath string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}

		base := path.Base(filePath)
		if lang, ok := knownBundleFileNames[base]; ok {
			messageFiles[lang] = filePath
		}

		return nil
	})

	if err != nil {
		errors = append(errors, err)
		return nil, newMessageBundleError(errors)
	}

	for _, lang := range language.SupportedLanguages {
		messageFile, ok := messageFiles[lang]
		if !ok {
			errors = append(errors, fmt.Errorf("no translations for %s found", lang))
			continue
		}

		data, err := fs.ReadFile(bundleDir, messageFile)

		if err != nil {
			errors = append(errors, err)
			continue
		}

		var messages project.MessageSet
		err = json.Unmarshal(data, &messages)
		if err != nil {
			errors = append(errors, err)
			continue
		}

		translations[lang] = messages
	}

	return translations, newMessageBundleError(errors)
}

type MessageBundleError struct {
	errors []error
}

func newMessageBundleError(errors []error) *MessageBundleError {
	if errors == nil {
		return nil
	}

	return &MessageBundleError{errors}
}

func (e *MessageBundleError) Errors() []error {
	return e.errors
}

func (e *MessageBundleError) combine(other *MessageBundleError) *MessageBundleError {
	if e == nil && other == nil {
		return nil
	}

	var selfErr MessageBundleError
	if e != nil {
		selfErr = *e
	}

	var otherErr MessageBundleError
	if other != nil {
		otherErr = *other
	}

	return &MessageBundleError{append(selfErr.errors, otherErr.errors...)}
}

func (e *MessageBundleError) Error() string {
	buf := strings.Builder{}

	fmt.Fprintln(&buf, "aggregate loading error:")

	for i, err := range e.errors {
		fmt.Fprintf(&buf, "\t%d: %s", i, err)
	}

	return buf.String()
}
