package auth

import (
	"context"
	"net/netip"
	"sort"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	authdomain "hyapp/services/user-service/internal/domain/auth"
)

const (
	loginRiskIPWhitelistCacheTTL   = 30 * time.Second
	loginRiskUserWhitelistCacheTTL = 5 * time.Minute
)

// LoginRiskWhitelistCacheSnapshot 是后台保存配置后主动刷新缓存的结果。
// IP 白名单仍沿用原短 TTL；用户白名单按运营要求缓存 5 分钟，避免每个风控任务回源 MySQL。
type LoginRiskWhitelistCacheSnapshot struct {
	Refreshed            bool
	IPWhitelistCount     int
	UserWhitelistCount   int
	UserCacheExpiresAtMs int64
}

// ListLoginRiskBlockedCountries 返回当前 App 实际生效的登录地区风控词表。
// 静态配置和后台动态配置都下发给 App；App 可异步做本地风险提示，最终封禁仍以服务端 worker 为准。
func (s *Service) ListLoginRiskBlockedCountries(ctx context.Context) ([]authdomain.LoginRiskCountryBlock, error) {
	if !s.loginRiskPolicy.Enabled {
		return nil, nil
	}
	appCode := appcode.FromContext(ctx)
	blocks := make([]authdomain.LoginRiskCountryBlock, 0, len(s.loginRiskPolicy.UnsupportedCountries))
	seen := map[string]struct{}{}
	for _, country := range s.loginRiskPolicy.UnsupportedCountries {
		countryCode := normalizeCountryCode(country)
		if countryCode == "" {
			continue
		}
		block := authdomain.LoginRiskCountryBlock{
			AppCode:     appCode,
			Keyword:     countryCode,
			CountryCode: countryCode,
			Enabled:     true,
		}
		blocks = appendUniqueCountryBlock(blocks, seen, block)
	}
	if s.authRepository != nil {
		dynamicBlocks, err := s.authRepository.ListEnabledLoginRiskCountryBlocks(ctx)
		if err != nil {
			return nil, err
		}
		for _, block := range dynamicBlocks {
			block.AppCode = appcode.Normalize(block.AppCode)
			block.Keyword = strings.TrimSpace(block.Keyword)
			block.CountryCode = normalizeCountryCode(block.CountryCode)
			block.Enabled = true
			if block.Keyword == "" {
				continue
			}
			blocks = appendUniqueCountryBlock(blocks, seen, block)
		}
	}
	sort.SliceStable(blocks, func(i, j int) bool {
		if blocks[i].CountryCode != blocks[j].CountryCode {
			return blocks[i].CountryCode < blocks[j].CountryCode
		}
		return strings.ToLower(blocks[i].Keyword) < strings.ToLower(blocks[j].Keyword)
	})
	return blocks, nil
}

// CheckLoginRiskIPWhitelisted 判断入口 IP 是否命中后台白名单。
// 白名单是“跳过登录地区屏蔽”的明确运营例外，只做标准化后的单 IP 精确匹配，不扩展为网段或地区推断。
func (s *Service) CheckLoginRiskIPWhitelisted(ctx context.Context, clientIP string) (bool, error) {
	ip := normalizeLoginRiskIP(clientIP)
	if ip == "" {
		return false, nil
	}
	ips, err := s.listLoginRiskIPWhitelist(ctx)
	if err != nil {
		return false, err
	}
	for _, item := range ips {
		if item == ip {
			return true, nil
		}
	}
	return false, nil
}

// CheckLoginRiskUserWhitelisted 判断登录用户是否命中后台白名单。
// 该白名单只在 user-service 登录后风控 worker 中使用；gateway 登录前没有可信 user_id，不能用它跳过入口快拦。
func (s *Service) CheckLoginRiskUserWhitelisted(ctx context.Context, userID int64) (bool, error) {
	if userID <= 0 {
		return false, nil
	}
	userIDs, err := s.listLoginRiskUserWhitelist(ctx)
	if err != nil {
		return false, err
	}
	for _, item := range userIDs {
		if item == userID {
			return true, nil
		}
	}
	return false, nil
}

// RefreshLoginRiskWhitelistCache 从 MySQL 读取当前 App 白名单并立即写入 Redis。
// 后台保存直接调用该方法，避免新增白名单后仍等 5 分钟 TTL 自然过期才生效。
func (s *Service) RefreshLoginRiskWhitelistCache(ctx context.Context) (LoginRiskWhitelistCacheSnapshot, error) {
	if s.authRepository == nil {
		return LoginRiskWhitelistCacheSnapshot{}, nil
	}
	ips, err := s.loadLoginRiskIPWhitelist(ctx)
	if err != nil {
		return LoginRiskWhitelistCacheSnapshot{}, err
	}
	userIDs, err := s.loadLoginRiskUserWhitelist(ctx)
	if err != nil {
		return LoginRiskWhitelistCacheSnapshot{}, err
	}
	snapshot := LoginRiskWhitelistCacheSnapshot{
		IPWhitelistCount:     len(ips),
		UserWhitelistCount:   len(userIDs),
		UserCacheExpiresAtMs: s.now().Add(loginRiskUserWhitelistCacheTTL).UnixMilli(),
	}
	if s.ipDecisionCache == nil {
		return snapshot, nil
	}
	// IP 和用户白名单共用一次刷新入口；任一缓存写失败都返回错误，让后台保存页能提示重试刷新。
	if err := s.ipDecisionCache.SetIPWhitelist(ctx, appcode.FromContext(ctx), ips, loginRiskIPWhitelistCacheTTL); err != nil {
		return LoginRiskWhitelistCacheSnapshot{}, err
	}
	if err := s.ipDecisionCache.SetUserWhitelist(ctx, appcode.FromContext(ctx), userIDs, loginRiskUserWhitelistCacheTTL); err != nil {
		return LoginRiskWhitelistCacheSnapshot{}, err
	}
	snapshot.Refreshed = true
	return snapshot, nil
}

func (s *Service) listLoginRiskIPWhitelist(ctx context.Context) ([]string, error) {
	appCode := appcode.FromContext(ctx)
	if s.ipDecisionCache != nil {
		ips, ok, err := s.ipDecisionCache.GetIPWhitelist(ctx, appCode)
		if err == nil && ok {
			return normalizeLoginRiskIPList(ips), nil
		}
	}
	ips, err := s.loadLoginRiskIPWhitelist(ctx)
	if err != nil {
		return nil, err
	}
	if s.ipDecisionCache != nil {
		// 白名单读取结果按 App 整体缓存 30 秒；空列表也缓存，避免无配置时每次登录都回源 DB。
		_ = s.ipDecisionCache.SetIPWhitelist(ctx, appCode, ips, loginRiskIPWhitelistCacheTTL)
	}
	return ips, nil
}

func (s *Service) listLoginRiskUserWhitelist(ctx context.Context) ([]int64, error) {
	appCode := appcode.FromContext(ctx)
	if s.ipDecisionCache != nil {
		userIDs, ok, err := s.ipDecisionCache.GetUserWhitelist(ctx, appCode)
		if err == nil && ok {
			return normalizeLoginRiskUserIDList(userIDs), nil
		}
	}
	userIDs, err := s.loadLoginRiskUserWhitelist(ctx)
	if err != nil {
		return nil, err
	}
	if s.ipDecisionCache != nil {
		// 用户白名单按运营要求缓存 5 分钟；空列表也缓存，减少无配置时的任务回源。
		_ = s.ipDecisionCache.SetUserWhitelist(ctx, appCode, userIDs, loginRiskUserWhitelistCacheTTL)
	}
	return userIDs, nil
}

func (s *Service) loadLoginRiskIPWhitelist(ctx context.Context) ([]string, error) {
	if s.authRepository == nil {
		return nil, nil
	}
	items, err := s.authRepository.ListEnabledLoginRiskIPWhitelist(ctx)
	if err != nil {
		return nil, err
	}
	ips := make([]string, 0, len(items))
	for _, item := range items {
		if ip := normalizeLoginRiskIP(item.IPAddress); ip != "" {
			ips = append(ips, ip)
		}
	}
	return normalizeLoginRiskIPList(ips), nil
}

func (s *Service) loadLoginRiskUserWhitelist(ctx context.Context) ([]int64, error) {
	if s.authRepository == nil {
		return nil, nil
	}
	items, err := s.authRepository.ListEnabledLoginRiskUserWhitelist(ctx)
	if err != nil {
		return nil, err
	}
	userIDs := make([]int64, 0, len(items))
	for _, item := range items {
		if item.UserID > 0 {
			userIDs = append(userIDs, item.UserID)
		}
	}
	return normalizeLoginRiskUserIDList(userIDs), nil
}

func appendUniqueCountryBlock(blocks []authdomain.LoginRiskCountryBlock, seen map[string]struct{}, block authdomain.LoginRiskCountryBlock) []authdomain.LoginRiskCountryBlock {
	key := strings.ToLower(block.CountryCode + "\x00" + block.Keyword)
	if _, ok := seen[key]; ok {
		return blocks
	}
	seen[key] = struct{}{}
	return append(blocks, block)
}

func (s *Service) countryBlockedByStaticPolicy(countryCode string) bool {
	for _, country := range s.loginRiskPolicy.UnsupportedCountries {
		if normalizeCountryCode(country) == countryCode {
			return true
		}
	}
	return false
}

func (s *Service) countryBlockedByDynamicPolicy(ctx context.Context, countryCode string) (bool, error) {
	if s.authRepository == nil {
		return false, nil
	}
	blocks, err := s.authRepository.ListEnabledLoginRiskCountryBlocks(ctx)
	if err != nil {
		return false, err
	}
	for _, block := range blocks {
		if normalizeCountryCode(block.CountryCode) == countryCode || normalizeCountryCode(block.Keyword) == countryCode {
			return true, nil
		}
	}
	return false, nil
}

func normalizeLoginRiskIP(raw string) string {
	addr, err := netip.ParseAddr(strings.TrimSpace(raw))
	if err != nil {
		return ""
	}
	return addr.String()
}

func normalizeLoginRiskIPList(raw []string) []string {
	seen := make(map[string]struct{}, len(raw))
	ips := make([]string, 0, len(raw))
	for _, item := range raw {
		ip := normalizeLoginRiskIP(item)
		if ip == "" {
			continue
		}
		if _, ok := seen[ip]; ok {
			continue
		}
		seen[ip] = struct{}{}
		ips = append(ips, ip)
	}
	sort.Strings(ips)
	return ips
}

func normalizeLoginRiskUserIDList(raw []int64) []int64 {
	seen := make(map[int64]struct{}, len(raw))
	userIDs := make([]int64, 0, len(raw))
	for _, userID := range raw {
		if userID <= 0 {
			continue
		}
		if _, ok := seen[userID]; ok {
			continue
		}
		seen[userID] = struct{}{}
		userIDs = append(userIDs, userID)
	}
	sort.Slice(userIDs, func(i, j int) bool {
		return userIDs[i] < userIDs[j]
	})
	return userIDs
}
