// Package i18nerr implements errors with internationalization using gotext
package i18nerr

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"sync"

	"codeberg.org/eduVPN/eduvpn-common/internal/httpwrap"

	"golang.org/x/text/language"
	"golang.org/x/text/message"
)

var (
	printers sync.Map
	once     sync.Once
)

// TranslatedInner defines errors that are used as inner causes but are still translated because they can happen frequently
func TranslatedInner(inner error) (string, bool) {
	unwrapped := inner
	for errors.Unwrap(unwrapped) != nil {
		unwrapped = errors.Unwrap(unwrapped)
	}

	var tErr *httpwrap.TimeoutError
	switch {
	case errors.As(inner, &tErr):
		return printerOrNew(language.English).Sprintf("Timeout reached contacting URL: '%s'", tErr.URL), false
	case errors.Is(inner, context.Canceled):
		return unwrapped.Error(), true
	}
	return unwrapped.Error(), false
}

// Error wraps an actual error with the translation key
// This translation key is later used to lookup translation
// The inner error always consists of the translation key and some formatting
type Error struct {
	key     message.Reference
	args    []any
	wrapped *Error
	Misc    bool
}

func (e *Error) translated(t language.Tag) string {
	once.Do(func() {
		inititializeLangs()
	})
	msg := printerOrNew(t).Sprintf(e.key, e.args...)
	if e.wrapped != nil {
		return printerOrNew(t).Sprintf("%s. The cause of the error is: %s.", msg, e.wrapped.Error())
	}
	return msg
}

// Error gets the error string
// it does this by simply forwarding the error method from the actual inner error
func (e *Error) Error() string {
	return e.translated(language.English)
}

// Translations returns all the translations for the error including the source translation (english)
func (e *Error) Translations() map[string]string {
	translations := make(map[string]string)
	// add the source transltaion first
	source := e.Error()
	translations[language.English.String()] = source
	for _, t := range message.DefaultCatalog.Languages() {
		// already added
		if t == language.English {
			continue
		}
		// get the final translation string for the tag
		// and add it if it's not equal to the english version
		f := e.translated(t)
		if f != source {
			translations[t.String()] = f
		}
	}
	return translations
}

// Unwrap returns the unwrapped error
// it does this by unwrapping the inner error
func (e *Error) Unwrap() error {
	if e.wrapped == nil {
		return nil
	}
	return e.wrapped.Unwrap()
}

// printerOrNew gets a message printer from the global printers map using the tag 'tag'
// If the printer cannot be found in the sync map, we return a new printer
func printerOrNew(tag language.Tag) *message.Printer {
	v, ok := printers.Load(tag)
	if !ok {
		slog.Debug("i18n could not load printer from map", "tag", tag)
		return message.NewPrinter(tag)
	}
	p, ok := v.(*message.Printer)
	if !ok {
		slog.Debug("i18n could not load printer from map with incorrect type", "tag", tag, "type", fmt.Sprintf("%T", p))
		return message.NewPrinter(tag)
	}
	return p
}

// New creates a new i18n error using a message reference
func New(key message.Reference) *Error {
	_ = printerOrNew(language.English).Sprint(key)
	return &Error{key: key}
}

// Newf creates a new i18n error using a message reference and arguments.
// It formats this with fmt.Errorf
func Newf(key message.Reference, args ...any) *Error {
	_ = printerOrNew(language.English).Sprintf(key, args...)
	return &Error{key: key, args: args}
}

// Wrap creates a new i18n error using an error to be wrapped 'err' and a prefix message reference 'key'.
// It formats this with fmt.Errorf
func Wrap(err error, key message.Reference) *Error {
	_ = printerOrNew(language.English).Sprintf(key)
	t, misc := TranslatedInner(err)
	return &Error{key: key, wrapped: &Error{key: t, Misc: misc}, Misc: misc}
}

// Wrapf creates a new i18n error using an error to be wrapped 'err' and a prefix message reference 'key' with format arguments 'args'.
// It formats this with fmt.Errorf
func Wrapf(err error, key message.Reference, args ...any) *Error {
	_ = printerOrNew(language.English).Sprintf(key, args...)
	t, misc := TranslatedInner(err)
	return &Error{key: key, args: args, wrapped: &Error{key: t, Misc: misc}, Misc: misc}
}

// NewInternal creates an internal localised error from a display string
func NewInternal(disp string) *Error {
	return Wrap(errors.New(disp), "An internal error occurred")
}

// NewInternalf creates an internal localised error from a display string and arguments
func NewInternalf(disp string, args ...any) *Error {
	return NewInternal(fmt.Sprintf(disp, args...))
}

// WrapInternal wraps an error and a display string into a localised internal error
func WrapInternal(err error, disp string) *Error {
	return Wrap(fmt.Errorf("%s with internal cause: %w", disp, err), "An internal error occurred")
}

// WrapInternalf wraps an error and a display string with args into a localised internal error
func WrapInternalf(err error, disp string, args ...any) *Error {
	return WrapInternal(err, fmt.Sprintf(disp, args...))
}

// initializeLangs initializes the printers from the default catalog into the sync map
// we cannot do this in init() because this is too early
func inititializeLangs() {
	slog.Debug("i18n initializing languages")
	for _, t := range message.DefaultCatalog.Languages() {
		printers.Store(t, message.NewPrinter(t))
	}
}
