package userctx

import (
	"context"
	"strings"
	"sync"
	"time"

	"wucher/internal/domain/user"
)

// UserNameGetter is the interface used by handlers to resolve user IDs to names.
type UserNameGetter interface {
	GetLastName(ctx context.Context, userID []byte) (string, error)
}

// UserFullNameGetter is an optional extension for getters that can resolve
// full names (first + last). Code should prefer this when available.
type UserFullNameGetter interface {
	GetFullName(ctx context.Context, userID []byte) (string, error)
}

// UserShortNameGetter is an optional extension for getters that can resolve a
// user's short name (profile 3-letter code, e.g. "GAN"). Code should prefer
// this when available, falling back to initials from the display name.
type UserShortNameGetter interface {
	GetShortName(ctx context.Context, userID []byte) (string, error)
}

// shortNameRepo is the optional repository capability used by UserNameCache to
// look up a profile short name. Only the concrete MySQL repo implements it;
// keeping it out of user.Repository avoids churning every mock.
type shortNameRepo interface {
	GetShortNameByID(ctx context.Context, id []byte) (string, error)
}

// initialsFromFullName derives an uppercase initials short name from a full
// name: "System Admin" → "SA", "Gantner" → "G". Empty when there is no name.
func initialsFromFullName(fullName string) string {
	parts := strings.Fields(strings.TrimSpace(fullName))
	if len(parts) == 0 {
		return ""
	}
	if len(parts) == 1 {
		r := []rune(parts[0])
		if len(r) == 0 {
			return ""
		}
		return strings.ToUpper(string(r[0]))
	}
	first := []rune(parts[0])
	last := []rune(parts[len(parts)-1])
	if len(first) == 0 || len(last) == 0 {
		return ""
	}
	return strings.ToUpper(string(first[0]) + string(last[0]))
}

// GlobalUserNameGetter is set once at application startup via WireUserNameGetter.
// It is used by uuidStringOrEmpty helpers across packages.
var GlobalUserNameGetter UserNameGetter

// WireUserNameGetter configures the global user name getter.
// Call this once during application startup (e.g. in main.go).
func WireUserNameGetter(getter UserNameGetter) {
	GlobalUserNameGetter = getter
}

// contextKey for storing the getter in context.
type contextKey string

const getterKey contextKey = "userctx-getter"

// WithGetter stores a UserNameGetter in the context.
func WithGetter(ctx context.Context, getter UserNameGetter) context.Context {
	return context.WithValue(ctx, getterKey, getter)
}

// GetGetter retrieves the UserNameGetter from context.
func GetGetter(ctx context.Context) UserNameGetter {
	if ctx == nil {
		return nil
	}
	if getter, ok := any(ctx).(interface{ UserValue(any) any }); ok {
		raw := getter.UserValue(string(getterKey))
		if raw != nil {
			return raw.(UserNameGetter)
		}
	}
	v := ctx.Value(getterKey)
	if v == nil {
		return nil
	}
	g, _ := v.(UserNameGetter)
	return g
}

// GetDisplayName resolves a user ID to a display name.
// Priority:
// 1) full name from context getter (if supported)
// 2) last name from context getter
// 3) full name from global getter (if supported)
// 4) last name from global getter
func GetDisplayName(ctx context.Context, userID []byte) string {
	if len(userID) == 0 {
		return ""
	}
	type userNameGetter interface {
		GetLastName(context.Context, []byte) (string, error)
	}
	type userFullNameGetter interface {
		GetFullName(context.Context, []byte) (string, error)
	}
	// Backward compatibility: some call sites pass a context-like object that
	// implements getter methods directly.
	if fullGetter, ok := any(ctx).(userFullNameGetter); ok {
		if name, err := fullGetter.GetFullName(ctx, userID); err == nil && name != "" {
			return name
		}
	}
	if getter, ok := any(ctx).(userNameGetter); ok {
		if name, err := getter.GetLastName(ctx, userID); err == nil && name != "" {
			return name
		}
	}
	if getter := GetGetter(ctx); getter != nil {
		if fullGetter, ok := getter.(UserFullNameGetter); ok {
			if name, err := fullGetter.GetFullName(ctx, userID); err == nil && name != "" {
				return name
			}
		}
		if name, err := getter.GetLastName(ctx, userID); err == nil && name != "" {
			return name
		}
	}
	if GlobalUserNameGetter != nil {
		if fullGetter, ok := GlobalUserNameGetter.(UserFullNameGetter); ok {
			if name, err := fullGetter.GetFullName(ctx, userID); err == nil && name != "" {
				return name
			}
		}
		if name, err := GlobalUserNameGetter.GetLastName(ctx, userID); err == nil && name != "" {
			return name
		}
	}
	return ""
}

// GetShortName resolves a user ID to a short name (profile 3-letter code, e.g.
// "GAN"), falling back to initials derived from the user's display name.
// Priority mirrors GetDisplayName: context getter, then global getter, then a
// display-name-initials fallback so it always returns something short.
func GetShortName(ctx context.Context, userID []byte) string {
	if len(userID) == 0 {
		return ""
	}
	type shortGetter interface {
		GetShortName(context.Context, []byte) (string, error)
	}
	// Backward compatibility: a context-like object implementing the getter.
	if g, ok := any(ctx).(shortGetter); ok {
		if s, err := g.GetShortName(ctx, userID); err == nil && s != "" {
			return s
		}
	}
	if getter := GetGetter(ctx); getter != nil {
		if sg, ok := getter.(UserShortNameGetter); ok {
			if s, err := sg.GetShortName(ctx, userID); err == nil && s != "" {
				return s
			}
		}
	}
	if GlobalUserNameGetter != nil {
		if sg, ok := GlobalUserNameGetter.(UserShortNameGetter); ok {
			if s, err := sg.GetShortName(ctx, userID); err == nil && s != "" {
				return s
			}
		}
	}
	// Universal fallback: initials from whatever display name we can resolve.
	return initialsFromFullName(GetDisplayName(ctx, userID))
}

// GetLastName resolves a user ID to their last name using the getter from context.
// Returns an empty string if no getter is available or if userID is nil/empty.
func GetLastName(ctx context.Context, userID []byte) string {
	if len(userID) == 0 {
		return ""
	}
	getter := GetGetter(ctx)
	if getter == nil {
		return ""
	}
	name, err := getter.GetLastName(ctx, userID)
	if err != nil {
		return ""
	}
	return name
}

// cacheEntry holds a cached last name with expiry time.
type cacheEntry struct {
	lastName string
	expires  time.Time
}

// UserNameCache is an in-memory TTL cache backed by a user.Repository.
type UserNameCache struct {
	repo       user.Repository
	cache      map[string]cacheEntry
	shortCache map[string]cacheEntry
	mu         sync.RWMutex
	ttl        time.Duration
}

// NewUserNameCache creates a UserNameCache with the given TTL (default 5 minutes).
func NewUserNameCache(repo user.Repository, ttl time.Duration) *UserNameCache {
	if ttl <= 0 {
		ttl = 5 * time.Minute
	}
	c := &UserNameCache{
		repo:       repo,
		cache:      make(map[string]cacheEntry),
		shortCache: make(map[string]cacheEntry),
		ttl:        ttl,
	}
	go c.cleanupLoop()
	return c
}

// GetShortName resolves a user ID to their profile short name (3-letter code),
// falling back to initials derived from their full name. Cached with the same
// TTL as names. (The cacheEntry.lastName field stores the short name here.)
func (c *UserNameCache) GetShortName(ctx context.Context, userID []byte) (string, error) {
	if len(userID) == 0 {
		return "", nil
	}

	key := string(userID)

	c.mu.RLock()
	entry, ok := c.shortCache[key]
	c.mu.RUnlock()

	if ok && time.Now().Before(entry.expires) {
		return entry.lastName, nil
	}

	short := ""
	if sr, ok := c.repo.(shortNameRepo); ok {
		s, err := sr.GetShortNameByID(ctx, userID)
		if err != nil {
			return "", err
		}
		short = strings.TrimSpace(s)
	}
	if short == "" {
		// Fallback: initials derived from the full name.
		if full, err := c.GetFullName(ctx, userID); err == nil {
			short = initialsFromFullName(full)
		}
	}

	c.mu.Lock()
	c.shortCache[key] = cacheEntry{lastName: short, expires: time.Now().Add(c.ttl)}
	c.mu.Unlock()

	return short, nil
}

func (c *UserNameCache) GetLastName(ctx context.Context, userID []byte) (string, error) {
	if len(userID) == 0 {
		return "", nil
	}

	key := string(userID)

	c.mu.RLock()
	entry, ok := c.cache[key]
	c.mu.RUnlock()

	if ok && time.Now().Before(entry.expires) {
		return entry.lastName, nil
	}

	lastName, err := c.repo.GetLastNameByID(ctx, userID)
	if err != nil {
		return "", err
	}
	if lastName == "" {
		lastName = "(Unknown)"
	}

	c.mu.Lock()
	c.cache[key] = cacheEntry{lastName: lastName, expires: time.Now().Add(c.ttl)}
	c.mu.Unlock()

	return lastName, nil
}

func (c *UserNameCache) GetFullName(ctx context.Context, userID []byte) (string, error) {
	if len(userID) == 0 {
		return "", nil
	}
	u, err := c.repo.GetByID(ctx, userID)
	if err != nil {
		return "", err
	}
	if u == nil {
		return "", nil
	}
	first := ""
	last := ""
	if u.FirstName != "" {
		first = u.FirstName
	}
	if u.LastName != "" {
		last = u.LastName
	}
	if first == "" {
		return last, nil
	}
	if last == "" {
		return first, nil
	}
	return first + " " + last, nil
}

func (c *UserNameCache) cleanupLoop() {
	ticker := time.NewTicker(c.ttl * 2)
	for range ticker.C {
		c.mu.Lock()
		now := time.Now()
		for k, v := range c.cache {
			if now.After(v.expires) {
				delete(c.cache, k)
			}
		}
		for k, v := range c.shortCache {
			if now.After(v.expires) {
				delete(c.shortCache, k)
			}
		}
		c.mu.Unlock()
	}
}
