package i18n

import (
	"embed"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"github.com/nicksnyder/go-i18n/v2/i18n"
	"golang.org/x/text/language"
)

// embedded default locales
//
//go:embed locales/*.json
var localeFS embed.FS

var (
	translator *i18n.Localizer
	initOnce   sync.Once
)

// defaultLanguageVariants maps language codes without regions to their default regional variants.
// This is used when a language without a base file is requested.
var defaultLanguageVariants = map[string]string{
	"pt": "pt-BR", // Portuguese defaults to Brazilian Portuguese for backward compatibility
	// Note: We currently have base files for these languages, but if we add regional variants
	// in the future, these defaults will be used:
	// "de": "de-DE", // German would default to Germany German
	// "en": "en-US", // English would default to US English
	// "es": "es-ES", // Spanish would default to Spain Spanish
	// "fa": "fa-IR", // Persian would default to Iran Persian
	// "fr": "fr-FR", // French would default to France French
	// "it": "it-IT", // Italian would default to Italy Italian
	// "ja": "ja-JP", // Japanese would default to Japan Japanese
	// "zh": "zh-CN", // Chinese would default to Simplified Chinese
}

// Init initializes the i18n bundle and localizer. It loads the specified locale
// and falls back to English if loading fails.
// Translation files are searched in the user config directory and downloaded
// from GitHub if missing.
//
// If locale is empty, it will attempt to detect the system locale from
// environment variables (LC_ALL, LC_MESSAGES, LANG) following POSIX standards.
func Init(locale string) (*i18n.Localizer, error) {
	// Use preferred locale detection if no explicit locale provided
	locale = getPreferredLocale(locale)
	// Normalize the locale to BCP 47 format (with hyphens)
	locale = normalizeToBCP47(locale)
	if locale == "" {
		locale = "en"
	}

	bundle := i18n.NewBundle(language.English)
	bundle.RegisterUnmarshalFunc("json", json.Unmarshal)

	// Build a list of locale candidates to try
	locales := getLocaleCandidates(locale)

	// Try to load embedded translations for each candidate
	embedded := false
	for _, candidate := range locales {
		if data, err := localeFS.ReadFile("locales/" + candidate + ".json"); err == nil {
			_, _ = bundle.ParseMessageFileBytes(data, candidate+".json")
			embedded = true
			locale = candidate // Update locale to what was actually loaded
			break
		}
	}

	// Fall back to English if nothing was loaded
	if !embedded {
		if data, err := localeFS.ReadFile("locales/en.json"); err == nil {
			_, _ = bundle.ParseMessageFileBytes(data, "en.json")
		}
	}

	// load locale from disk or download when not embedded
	path := filepath.Join(userLocaleDir(), locale+".json")
	if _, err := os.Stat(path); os.IsNotExist(err) && !embedded {
		if err := downloadLocale(path, locale); err != nil {
			// if download fails, still continue with embedded translations
			fmt.Fprintf(os.Stderr, "%s\n", fmt.Sprintf(getErrorMessage("i18n_download_failed", "Failed to download translation for language '%s': %v"), locale, err))
		}
	}
	if _, err := os.Stat(path); err == nil {
		if _, err := bundle.LoadMessageFile(path); err != nil {
			fmt.Fprintf(os.Stderr, "%s\n", fmt.Sprintf(getErrorMessage("i18n_load_failed", "Failed to load translation file: %v"), err))
		}
	}

	translator = i18n.NewLocalizer(bundle, locale)
	return translator, nil
}

// T returns the localized string for the given message id.
// If the translator is not initialized, it will automatically initialize
// with system locale detection.
func T(messageID string) string {
	initOnce.Do(func() {
		if translator == nil {
			Init("") // Empty string triggers system locale detection
		}
	})
	return translator.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID})
}

func userLocaleDir() string {
	dir, err := os.UserConfigDir()
	if err != nil {
		dir = "."
	}
	path := filepath.Join(dir, "fabric", "locales")
	os.MkdirAll(path, 0o755)
	return path
}

func downloadLocale(path, locale string) error {
	url := fmt.Sprintf("https://raw.githubusercontent.com/danielmiessler/Fabric/main/internal/i18n/locales/%s.json", locale)
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("unexpected status: %s", resp.Status)
	}
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer f.Close()
	_, err = io.Copy(f, resp.Body)
	return err
}

// getErrorMessage tries to get a translated error message, falling back to system locale
// and then to the provided fallback message. This is used during initialization when
// the translator may not be fully ready.
func getErrorMessage(messageID, fallback string) string {
	// Try to get system locale for error messages
	systemLocale := getPreferredLocale("")
	if systemLocale == "" {
		systemLocale = "en"
	}

	// First try the system locale
	if msg := tryGetMessage(systemLocale, messageID); msg != "" {
		return msg
	}

	// Fall back to English
	if systemLocale != "en" {
		if msg := tryGetMessage("en", messageID); msg != "" {
			return msg
		}
	}

	// Final fallback to hardcoded message
	return fallback
}

// tryGetMessage attempts to get a message from embedded locale files
func tryGetMessage(locale, messageID string) string {
	if data, err := localeFS.ReadFile("locales/" + locale + ".json"); err == nil {
		var messages map[string]string
		if json.Unmarshal(data, &messages) == nil {
			if msg, exists := messages[messageID]; exists {
				return msg
			}
		}
	}
	return ""
}

// normalizeToBCP47 normalizes a locale string to BCP 47 format.
// Converts underscores to hyphens and ensures proper casing (language-REGION).
func normalizeToBCP47(locale string) string {
	if locale == "" {
		return ""
	}

	// Replace underscores with hyphens
	locale = strings.ReplaceAll(locale, "_", "-")

	// Split into parts
	parts := strings.Split(locale, "-")
	if len(parts) == 1 {
		// Language only, lowercase it
		return strings.ToLower(parts[0])
	} else if len(parts) >= 2 {
		// Language and region (and possibly more)
		// Lowercase language, uppercase region
		parts[0] = strings.ToLower(parts[0])
		parts[1] = strings.ToUpper(parts[1])
		return strings.Join(parts[:2], "-") // Return only language-REGION
	}

	return locale
}

// getLocaleCandidates returns a list of locale candidates to try, in order of preference.
// For example, for "pt-PT" it returns ["pt-PT", "pt", "pt-BR"] (where pt-BR is the default for pt).
func getLocaleCandidates(locale string) []string {
	candidates := []string{}

	if locale == "" {
		return candidates
	}

	// First candidate is always the requested locale
	candidates = append(candidates, locale)

	// If it's a regional variant, add the base language as a candidate
	if strings.Contains(locale, "-") {
		baseLang := strings.Split(locale, "-")[0]
		candidates = append(candidates, baseLang)

		// Also check if the base language has a default variant
		if defaultVariant, exists := defaultLanguageVariants[baseLang]; exists {
			// Only add if it's different from what we already have
			if defaultVariant != locale {
				candidates = append(candidates, defaultVariant)
			}
		}
	} else {
		// If this is a base language without a region, check for default variant
		if defaultVariant, exists := defaultLanguageVariants[locale]; exists {
			candidates = append(candidates, defaultVariant)
		}
	}

	return candidates
}
