/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package translator

import (
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/google/wire"
	myTran "github.com/segmentfault/pacman/contrib/i18n"
	"github.com/segmentfault/pacman/i18n"
	"github.com/segmentfault/pacman/log"
	"gopkg.in/yaml.v3"
)

// ProviderSet is providers.
var ProviderSet = wire.NewSet(NewTranslator)
var GlobalTrans i18n.Translator

// LangOption language option
type LangOption struct {
	Label string `json:"label"`
	Value string `json:"value"`
	// Translation completion percentage
	Progress int `json:"progress"`
}

// DefaultLangOption default language option. If user config the language is default, the language option is admin choose.
const DefaultLangOption = "Default"

var (
	// LanguageOptions language
	LanguageOptions []*LangOption
)

// NewTranslator new a translator
func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
	entries, err := os.ReadDir(c.BundleDir)
	if err != nil {
		return nil, err
	}

	// read the Bundle resources file from entries
	for _, file := range entries {
		// ignore directory
		if file.IsDir() {
			continue
		}
		// ignore non-YAML file
		if filepath.Ext(file.Name()) != ".yaml" && file.Name() != "i18n.yaml" {
			continue
		}
		log.Debugf("try to read file: %s", file.Name())
		buf, err := os.ReadFile(filepath.Join(c.BundleDir, file.Name()))
		if err != nil {
			return nil, fmt.Errorf("read file failed: %s %s", file.Name(), err)
		}

		// parse the backend translation
		originalTr := struct {
			Backend map[string]map[string]any `yaml:"backend"`
			UI      map[string]any            `yaml:"ui"`
			Plugin  map[string]any            `yaml:"plugin"`
		}{}
		if err = yaml.Unmarshal(buf, &originalTr); err != nil {
			return nil, err
		}
		translation := make(map[string]any, 0)
		for k, v := range originalTr.Backend {
			translation[k] = v
		}
		translation["backend"] = originalTr.Backend
		translation["ui"] = originalTr.UI
		translation["plugin"] = originalTr.Plugin

		content, err := yaml.Marshal(translation)
		if err != nil {
			log.Debugf("marshal translation content failed: %s %s", file.Name(), err)
			continue
		}

		// add translator use backend translation
		if err = myTran.AddTranslator(content, file.Name()); err != nil {
			log.Debugf("add translator failed: %s %s", file.Name(), err)
			reportTranslatorFormatError(file.Name(), buf)
			continue
		}
	}
	GlobalTrans = myTran.GlobalTrans

	i18nFile, err := os.ReadFile(filepath.Join(c.BundleDir, "i18n.yaml"))
	if err != nil {
		return nil, fmt.Errorf("read i18n file failed: %s", err)
	}

	s := struct {
		LangOption []*LangOption `yaml:"language_options"`
	}{}
	err = yaml.Unmarshal(i18nFile, &s)
	if err != nil {
		return nil, fmt.Errorf("i18n file parsing failed: %s", err)
	}
	LanguageOptions = s.LangOption
	for _, option := range LanguageOptions {
		option.Label = fmt.Sprintf("%s (%d%%)", option.Label, option.Progress)
	}
	return GlobalTrans, err
}

// CheckLanguageIsValid check user input language is valid
func CheckLanguageIsValid(lang string) bool {
	if lang == DefaultLangOption {
		return true
	}
	for _, option := range LanguageOptions {
		if option.Value == lang {
			return true
		}
	}
	return false
}

// Tr use language to translate data. If this language translation is not available, return default english translation.
func Tr(lang i18n.Language, data string) string {
	if GlobalTrans == nil {
		return data
	}
	translation := GlobalTrans.Tr(lang, data)
	if translation == data {
		return GlobalTrans.Tr(i18n.DefaultLanguage, data)
	}
	return translation
}

// TrWithData translate key with template data, it will replace the template data {{ .PlaceHolder }} in the translation.
func TrWithData(lang i18n.Language, key string, templateData any) string {
	if GlobalTrans == nil {
		return key
	}
	translation := GlobalTrans.TrWithData(lang, key, templateData)
	if translation == key {
		return GlobalTrans.TrWithData(i18n.DefaultLanguage, key, templateData)
	}
	return translation
}

// reportTranslatorFormatError re-parses the YAML file to locate the invalid entry
// when go-i18n fails to add the translator.
func reportTranslatorFormatError(fileName string, content []byte) {
	var raw any
	if err := yaml.Unmarshal(content, &raw); err != nil {
		log.Errorf("parse translator file %s failed when diagnosing format error: %s", fileName, err)
		return
	}
	if err := inspectTranslatorNode(raw, nil, true); err != nil {
		log.Errorf("translator file %s invalid: %s", fileName, err)
	}
}

func inspectTranslatorNode(node any, path []string, isRoot bool) error {
	switch data := node.(type) {
	case nil:
		if isRoot {
			return fmt.Errorf("root value is empty")
		}
		return fmt.Errorf("%s contains an empty value", formatTranslationPath(path))
	case string:
		if isRoot {
			return fmt.Errorf("root value must be an object but found string")
		}
		return nil
	case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
		if isRoot {
			return fmt.Errorf("root value must be an object but found %T", data)
		}
		return fmt.Errorf("%s expects a string translation but found %T", formatTranslationPath(path), data)
	case map[string]any:
		if isMessageMap(data) {
			return nil
		}
		keys := make([]string, 0, len(data))
		for key := range data {
			keys = append(keys, key)
		}
		sort.Strings(keys)
		for _, key := range keys {
			if err := inspectTranslatorNode(data[key], append(path, key), false); err != nil {
				return err
			}
		}
		return nil
	case map[string]string:
		mapped := make(map[string]any, len(data))
		for k, v := range data {
			mapped[k] = v
		}
		return inspectTranslatorNode(mapped, path, isRoot)
	case map[any]any:
		if isMessageMap(data) {
			return nil
		}
		type kv struct {
			key string
			val any
		}
		items := make([]kv, 0, len(data))
		for key, val := range data {
			strKey, ok := key.(string)
			if !ok {
				return fmt.Errorf("%s uses a non-string key %#v", formatTranslationPath(path), key)
			}
			items = append(items, kv{key: strKey, val: val})
		}
		sort.Slice(items, func(i, j int) bool {
			return items[i].key < items[j].key
		})
		for _, item := range items {
			if err := inspectTranslatorNode(item.val, append(path, item.key), false); err != nil {
				return err
			}
		}
		return nil
	case []any:
		for idx, child := range data {
			if err := inspectTranslatorNode(child, append(path, fmt.Sprintf("[%d]", idx)), false); err != nil {
				return err
			}
		}
		return nil
	case []map[string]any:
		for idx, child := range data {
			if err := inspectTranslatorNode(child, append(path, fmt.Sprintf("[%d]", idx)), false); err != nil {
				return err
			}
		}
		return nil
	default:
		if isRoot {
			return fmt.Errorf("root value must be an object but found %T", data)
		}
		return fmt.Errorf("%s contains unsupported value type %T", formatTranslationPath(path), data)
	}
}

var translatorReservedKeys = []string{
	"id", "description", "hash", "leftdelim", "rightdelim",
	"zero", "one", "two", "few", "many", "other",
}

func isMessageMap(data any) bool {
	switch v := data.(type) {
	case map[string]any:
		for _, key := range translatorReservedKeys {
			val, ok := v[key]
			if !ok {
				continue
			}
			if _, ok := val.(string); ok {
				return true
			}
		}
	case map[string]string:
		for _, key := range translatorReservedKeys {
			val, ok := v[key]
			if !ok {
				continue
			}
			if val != "" {
				return true
			}
		}
	case map[any]any:
		for _, key := range translatorReservedKeys {
			val, ok := v[key]
			if !ok {
				continue
			}
			if _, ok := val.(string); ok {
				return true
			}
		}
	}
	return false
}

func formatTranslationPath(path []string) string {
	if len(path) == 0 {
		return "root"
	}
	var b strings.Builder
	for _, part := range path {
		if part == "" {
			continue
		}
		if part[0] == '[' {
			b.WriteString(part)
			continue
		}
		if b.Len() > 0 {
			b.WriteByte('.')
		}
		b.WriteString(part)
	}
	return b.String()
}
