// Package v1 provides MessageFormat v1 (ICU MessageFormat) implementation for Go
package v1

import (
	"fmt"
	"regexp"
)

// MessageData represents hierarchical message object
// TypeScript original code:
//
//	export interface MessageData {
//	  [key: string]: MessageData | MessageFunction | string;
//	}
type MessageData map[string]any // MessageData | MessageFunction | string

// Messages provides accessor class for compiled message functions generated by compileModule
// TypeScript original code:
//
//	export default class Messages {
//	  /** @internal */
//	  _data: { [key: string]: MessageData } = {};
//	  /** @internal */
//	  _fallback: { [key: string]: string[] | null } = {};
//	  /** @internal */
//	  _defaultLocale: string | null = null;
//	  /** @internal */
//	  _locale: string | null = null;
//	  constructor(msgData: { [key: string]: MessageData }, defaultLocale?: string)
//	  get availableLocales(): string[]
//	  get locale(): string | null
//	  set locale(locale)
//	  get defaultLocale(): string | null
//	  set defaultLocale(locale: string | null)
//	  addMessages(data: MessageData | MessageFunction, locale?: string, keypath?: string[])
//	  resolveLocale(locale: string | null)
//	  getFallback(locale?: string | null)
//	  setFallback(lc: string, fallback: string[] | null)
//	  hasMessage(key: string | string[], locale?: string, fallback?: boolean)
//	  hasObject(key: string | string[], locale?: string, fallback?: boolean)
//	  get(key: string | string[], props?: Record<string, unknown>, locale?: string)
//	}
type Messages struct {
	_data          map[string]MessageData
	_fallback      map[string][]string // null represented as nil slice
	_defaultLocale *string
	_locale        *string
}

// NewMessages creates a new Messages instance
// TypeScript original code:
//
//	constructor(msgData: { [key: string]: MessageData }, defaultLocale?: string) {
//	  Object.keys(msgData).forEach(lc => {
//	    if (lc !== 'toString') {
//	      this._data[lc] = msgData[lc];
//	      if (defaultLocale === undefined) defaultLocale = lc;
//	    }
//	  });
//	  this.locale = defaultLocale || null;
//	  this._defaultLocale = this.locale;
//	}
func NewMessages(msgData map[string]MessageData, defaultLocale ...string) *Messages {
	m := &Messages{
		_data:     make(map[string]MessageData),
		_fallback: make(map[string][]string),
	}

	var defaultLc *string
	if len(defaultLocale) > 0 && defaultLocale[0] != "" {
		defaultLc = &defaultLocale[0]
	}

	// Copy message data, excluding 'toString' key
	for lc, data := range msgData {
		if lc != "toString" {
			m._data[lc] = data
			if defaultLc == nil {
				defaultLc = &lc
			}
		}
	}

	if defaultLc != nil {
		resolved := m.resolveLocale(*defaultLc)
		m._locale = resolved
		m._defaultLocale = resolved
	}

	return m
}

// AvailableLocales returns read-only list of available locales
// TypeScript original code:
//
//	get availableLocales(): string[] {
//	  return Object.keys(this._data);
//	}
func (m *Messages) AvailableLocales() []string {
	locales := make([]string, 0, len(m._data))
	for lc := range m._data {
		locales = append(locales, lc)
	}
	return locales
}

// Locale returns current locale
// TypeScript original code:
//
//	get locale(): string | null {
//	  return this._locale;
//	}
func (m *Messages) Locale() *string {
	return m._locale
}

// SetLocale sets current locale with partial matches support
// TypeScript original code:
//
//	set locale(locale) {
//	  this._locale = this.resolveLocale(locale);
//	}
func (m *Messages) SetLocale(locale string) {
	m._locale = m.resolveLocale(locale)
}

// DefaultLocale returns default fallback locale
// TypeScript original code:
//
//	get defaultLocale(): string | null {
//	  return this._defaultLocale;
//	}
func (m *Messages) DefaultLocale() *string {
	return m._defaultLocale
}

// SetDefaultLocale sets default fallback locale with partial matches support
// TypeScript original code:
//
//	set defaultLocale(locale: string | null) {
//	  this._defaultLocale = this.resolveLocale(locale);
//	}
func (m *Messages) SetDefaultLocale(locale *string) {
	if locale == nil {
		m._defaultLocale = nil
	} else {
		m._defaultLocale = m.resolveLocale(*locale)
	}
}

// AddMessages adds new messages to the accessor; useful if loading data dynamically
// TypeScript original code:
// addMessages(
//
//	data: MessageData | MessageFunction,
//	locale?: string,
//	keypath?: string[]
//
//	) {
//	  const lc = locale || String(this.locale);
//	  if (typeof data !== 'function') {
//	    data = Object.keys(data).reduce<MessageData>((map, key) => {
//	      if (key !== 'toString') map[key] = (data as MessageData)[key];
//	      return map;
//	    }, {});
//	  }
//	  if (Array.isArray(keypath) && keypath.length > 0) {
//	    let parent = this._data[lc] as MessageData;
//	    for (let i = 0; i < keypath.length - 1; ++i) {
//	      const key = keypath[i];
//	      if (!parent[key]) parent[key] = {};
//	      parent = parent[key] as MessageData;
//	    }
//	    parent[keypath[keypath.length - 1]] = data;
//	  } else {
//	    this._data[lc] = data as MessageData;
//	  }
//	  return this;
//	}
func (m *Messages) AddMessages(data any, locale string, keypath []string) *Messages {
	lc := locale
	if lc == "" {
		if m._locale != nil {
			lc = *m._locale
		} else {
			lc = ""
		}
	}

	// Handle different data types
	var processedData any
	switch v := data.(type) {
	case MessageFunction:
		processedData = v
	case map[string]any:
		// Filter out 'toString' key
		filtered := make(map[string]any)
		for key, value := range v {
			if key != "toString" {
				filtered[key] = value
			}
		}
		processedData = filtered
	default:
		processedData = data
	}

	// Initialize locale data if it doesn't exist
	if m._data[lc] == nil {
		m._data[lc] = make(MessageData)
	}

	// Add data at specified keypath or root
	if len(keypath) > 0 {
		parent := m._data[lc]

		// Navigate to parent of target location
		for i := 0; i < len(keypath)-1; i++ {
			key := keypath[i]
			if parent[key] == nil {
				parent[key] = make(MessageData)
			}
			if parentData, ok := parent[key].(MessageData); ok {
				parent = parentData
			} else if parentMap, ok := parent[key].(map[string]any); ok {
				parent = MessageData(parentMap)
			} else {
				// Convert to MessageData
				parent[key] = make(MessageData)
				parent = parent[key].(MessageData)
			}
		}

		// Set data at final key
		parent[keypath[len(keypath)-1]] = processedData
	} else {
		// Set entire data
		if msgData, ok := processedData.(MessageData); ok {
			m._data[lc] = msgData
		} else if msgMap, ok := processedData.(map[string]any); ok {
			m._data[lc] = MessageData(msgMap)
		}
	}

	return m
}

// ResolveLocale resolves lc to the key of an available locale or null, allowing for partial matches
// TypeScript original code:
//
//	resolveLocale(locale: string | null) {
//	  let lc = String(locale);
//	  if (this._data[lc]) return locale;
//	  if (locale) {
//	    while ((lc = lc.replace(/[-_]?[^-_]*$/, ''))) {
//	      if (this._data[lc]) return lc;
//	    }
//	    const ll = this.availableLocales;
//	    const re = new RegExp('^' + locale + '[-_]');
//	    for (let i = 0; i < ll.length; ++i) {
//	      if (re.test(ll[i])) return ll[i];
//	    }
//	  }
//	  return null;
//	}
func (m *Messages) resolveLocale(locale string) *string {
	if locale == "" {
		return nil
	}

	// Direct match
	if _, exists := m._data[locale]; exists {
		return &locale
	}

	// Try progressive truncation
	lc := locale
	for {
		// Remove last component (after last - or _)
		re := regexp.MustCompile(`[-_]?[^-_]*$`)
		newLc := re.ReplaceAllString(lc, "")
		if newLc == lc || newLc == "" {
			break
		}
		lc = newLc
		if _, exists := m._data[lc]; exists {
			return &lc
		}
	}

	// Try forward matching (locale is prefix of available locale)
	availableLocales := m.AvailableLocales()
	pattern := fmt.Sprintf("^%s[-_]", regexp.QuoteMeta(locale))
	re := regexp.MustCompile(pattern)
	for _, availableLc := range availableLocales {
		if re.MatchString(availableLc) {
			return &availableLc
		}
	}

	return nil
}

// GetFallback returns the list of fallback locales
// TypeScript original code:
//
//	getFallback(locale?: string | null) {
//	  const lc = locale || String(this.locale);
//	  return (
//	    this._fallback[lc] ||
//	    (lc === this.defaultLocale || !this.defaultLocale
//	      ? []
//	      : [this.defaultLocale])
//	  );
//	}
func (m *Messages) GetFallback(locale *string) []string {
	var lc string
	switch {
	case locale != nil:
		lc = *locale
	case m._locale != nil:
		lc = *m._locale
	default:
		lc = ""
	}

	// Return explicitly set fallback or default fallback
	if fallback, exists := m._fallback[lc]; exists && fallback != nil {
		return fallback
	}

	// Default fallback logic
	if m._defaultLocale == nil || (m._defaultLocale != nil && lc == *m._defaultLocale) {
		return []string{}
	}

	return []string{*m._defaultLocale}
}

// SetFallback sets the fallback locale or locales for lc
// TypeScript original code:
//
//	setFallback(lc: string, fallback: string[] | null) {
//	  this._fallback[lc] = Array.isArray(fallback) ? fallback : null;
//	  return this;
//	}
func (m *Messages) SetFallback(lc string, fallback []string) *Messages {
	if fallback == nil {
		m._fallback[lc] = nil
	} else {
		m._fallback[lc] = fallback
	}
	return m
}

// HasMessage checks if key is a message function for the locale
// TypeScript original code:
//
//	hasMessage(key: string | string[], locale?: string, fallback?: boolean) {
//	  const lc = locale || String(this.locale);
//	  const fb = fallback ? this.getFallback(lc) : null;
//	  return _has(this._data, lc, key, fb, 'function');
//	}
func (m *Messages) HasMessage(key any, locale *string, fallback ...bool) bool {
	var lc string
	switch {
	case locale != nil:
		lc = *locale
	case m._locale != nil:
		lc = *m._locale
	default:
		lc = ""
	}

	var fb []string
	if len(fallback) > 0 && fallback[0] {
		fb = m.GetFallback(&lc)
	}

	return m.has(lc, key, fb, "function")
}

// HasObject checks if key is a message object for the locale
// TypeScript original code:
//
//	hasObject(key: string | string[], locale?: string, fallback?: boolean) {
//	  const lc = locale || String(this.locale);
//	  const fb = fallback ? this.getFallback(lc) : null;
//	  return _has(this._data, lc, key, fb, 'object');
//	}
func (m *Messages) HasObject(key any, locale *string, fallback ...bool) bool {
	var lc string
	switch {
	case locale != nil:
		lc = *locale
	case m._locale != nil:
		lc = *m._locale
	default:
		lc = ""
	}

	var fb []string
	if len(fallback) > 0 && fallback[0] {
		fb = m.GetFallback(&lc)
	}

	return m.has(lc, key, fb, "object")
}

// Get returns the message or object corresponding to key
// TypeScript original code:
// get(
//
//	key: string | string[],
//	props?: Record<string, unknown>,
//	locale?: string
//
//	) {
//	  const lc = locale || String(this.locale);
//	  let msg = _get(this._data[lc], key);
//	  if (msg) return typeof msg == 'function' ? msg(props) : msg;
//	  const fb = this.getFallback(lc);
//	  for (let i = 0; i < fb.length; ++i) {
//	    msg = _get(this._data[fb[i]], key);
//	    if (msg) return typeof msg == 'function' ? msg(props) : msg;
//	  }
//	  return key;
//	}
func (m *Messages) Get(key any, props map[string]any, locale *string) (any, error) {
	var lc string
	switch {
	case locale != nil:
		lc = *locale
	case m._locale != nil:
		lc = *m._locale
	default:
		lc = ""
	}

	// Try primary locale
	if msg := m.get(m._data[lc], key); msg != nil {
		if msgFunc, ok := msg.(MessageFunction); ok {
			return msgFunc(props)
		}
		// Also try generic function type that might be MessageFunction compatible
		if genericFunc, ok := msg.(func(map[string]any) (any, error)); ok {
			return genericFunc(props)
		}
		return msg, nil
	}

	// Try fallback locales
	fb := m.GetFallback(&lc)
	for _, fallbackLc := range fb {
		if msg := m.get(m._data[fallbackLc], key); msg != nil {
			if msgFunc, ok := msg.(MessageFunction); ok {
				return msgFunc(props)
			}
			// Also try generic function type that might be MessageFunction compatible
			if genericFunc, ok := msg.(func(map[string]any) (any, error)); ok {
				return genericFunc(props)
			}
			return msg, nil
		}
	}

	// Return key as fallback
	return key, nil
}

// get retrieves value from MessageData by key or key path
// TypeScript original code:
// function _get(
//
//	obj: MessageData | MessageFunction | string,
//	key: string | string[]
//
//	) {
//	  if (!obj) return null;
//	  let res: MessageData | MessageFunction | string = obj;
//	  if (Array.isArray(key)) {
//	    for (let i = 0; i < key.length; ++i) {
//	      if (typeof res !== 'object') return null;
//	      res = res[key[i]];
//	      if (!res) return null;
//	    }
//	    return res;
//	  }
//	  return typeof res === 'object' ? res[key] : null;
//	}
func (m *Messages) get(obj MessageData, key any) any {
	if obj == nil {
		return nil
	}

	switch k := key.(type) {
	case []string:
		// Handle key path
		current := obj
		for _, keyPart := range k {
			if current == nil {
				return nil
			}
			value, exists := current[keyPart]
			if !exists {
				return nil
			}

			// Convert value to MessageData if it's a map
			if mapValue, ok := value.(map[string]any); ok {
				current = MessageData(mapValue)
			} else if msgData, ok := value.(MessageData); ok {
				current = msgData
			} else {
				// If not the last key and not a map, return nil
				if keyPart != k[len(k)-1] {
					return nil
				}
				return value
			}
		}
		return current
	case string:
		// Handle single key
		if value, exists := obj[k]; exists {
			return value
		}
		return nil
	default:
		// Convert other types to string
		keyStr := fmt.Sprintf("%v", key)
		if value, exists := obj[keyStr]; exists {
			return value
		}
		return nil
	}
}

// has checks if key exists and is of specified type in data or fallback locales
// TypeScript original code:
// function _has(
//
//	data: MessageData,
//	lc: string,
//	key: string | string[],
//	fallback: string[] | null,
//	type: 'function' | 'object'
//
//	) {
//	  let msg = _get(data[lc], key);
//	  if (msg) return typeof msg === type;
//	  if (fallback) {
//	    for (let i = 0; i < fallback.length; ++i) {
//	      msg = _get(data[fallback[i]], key);
//	      if (msg) return typeof msg === type;
//	    }
//	  }
//	  return false;
//	}
func (m *Messages) has(lc string, key any, fallback []string, expectedType string) bool {
	// Check primary locale
	if msg := m.get(m._data[lc], key); msg != nil {
		return m.isType(msg, expectedType)
	}

	// Check fallback locales
	for _, fallbackLc := range fallback {
		if msg := m.get(m._data[fallbackLc], key); msg != nil {
			return m.isType(msg, expectedType)
		}
	}

	return false
}

// isType checks if value matches expected type
func (m *Messages) isType(value any, expectedType string) bool {
	switch expectedType {
	case "function":
		// Check for MessageFunction or any function type
		if _, ok := value.(MessageFunction); ok {
			return true
		}
		// Also check for generic function types that might be MessageFunction compatible
		if _, ok := value.(func(map[string]any) (any, error)); ok {
			return true
		}
		return false
	case "object":
		switch value.(type) {
		case MessageData, map[string]any:
			return true
		default:
			return false
		}
	default:
		return false
	}
}
