package http

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"
	"log/slog"
	"net/http"
	"regexp"
	"strings"
	"sync"
	"time"

	"github.com/redis/go-redis/v9"
	userv1 "hyapp.local/api/proto/user/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/logx"
)

const (
	loginRiskBlockLanguage = "language"
	loginRiskBlockTimezone = "timezone"
	loginRiskBlockIPCache  = "ip_cache"
)

var languageTagPattern = regexp.MustCompile(`^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8}){0,3}$`)

// LoginRiskConfig 描述 gateway 登录入口快拦策略。
type LoginRiskConfig struct {
	Enabled   bool
	KeyPrefix string
	FastGuard LoginRiskFastGuardConfig
}

// LoginRiskFastGuardConfig 保存弱信号阻断列表；命中才拒绝，缺失或非法只审计不推断。
type LoginRiskFastGuardConfig struct {
	BlockedLanguages           []string
	BlockedLanguagePrimaryTags []string
	BlockedTimezones           []string
	BlockedTimezonePrefixes    []string
}

type loginRiskInput struct {
	Channel  string
	Platform string
	Language string
	Timezone string
}

type loginRiskDecision struct {
	Decision    string `json:"decision"`
	Country     string `json:"country"`
	Source      string `json:"source"`
	CheckedAtMs int64  `json:"checked_at_ms"`
	ExpiresAtMs int64  `json:"expires_at_ms"`
}

type loginRiskCache interface {
	GetIPDecision(ctx context.Context, appCode string, clientIPHash string) (loginRiskDecision, bool, error)
	RevokedSessionExists(ctx context.Context, appCode string, sessionID string) (bool, error)
}

type redisLoginRiskCache struct {
	client    *redis.Client
	keyPrefix string
}

type memoryLoginRiskCache struct {
	mu         sync.Mutex
	ip         map[string]loginRiskDecision
	revoked    map[string]bool
	revokedErr error
}

func defaultLoginRiskConfig() LoginRiskConfig {
	return LoginRiskConfig{
		Enabled:   true,
		KeyPrefix: "login_risk:ip:",
	}
}

func normalizeLoginRiskConfig(config LoginRiskConfig) LoginRiskConfig {
	defaults := defaultLoginRiskConfig()
	if strings.TrimSpace(config.KeyPrefix) == "" {
		config.KeyPrefix = defaults.KeyPrefix
	}
	return config
}

func NewLoginRiskRedisClient(ctx context.Context, addr string, password string, db int) (*redis.Client, error) {
	client := redis.NewClient(&redis.Options{
		Addr:     strings.TrimSpace(addr),
		Password: password,
		DB:       db,
	})
	if err := client.Ping(ctx).Err(); err != nil {
		_ = client.Close()
		return nil, err
	}
	return client, nil
}

func NewRedisLoginRiskCache(client *redis.Client, config LoginRiskConfig) loginRiskCache {
	config = normalizeLoginRiskConfig(config)
	return &redisLoginRiskCache{client: client, keyPrefix: config.KeyPrefix}
}

func newMemoryLoginRiskCache() *memoryLoginRiskCache {
	return &memoryLoginRiskCache{
		ip:      make(map[string]loginRiskDecision),
		revoked: make(map[string]bool),
	}
}

func (c *redisLoginRiskCache) GetIPDecision(ctx context.Context, appCode string, clientIPHash string) (loginRiskDecision, bool, error) {
	raw, err := c.client.Get(ctx, ipDecisionKey(c.keyPrefix, appCode, clientIPHash)).Result()
	if errors.Is(err, redis.Nil) {
		return loginRiskDecision{}, false, nil
	}
	if err != nil {
		return loginRiskDecision{}, false, err
	}
	var decision loginRiskDecision
	if err := json.Unmarshal([]byte(raw), &decision); err != nil {
		return loginRiskDecision{}, false, err
	}
	return decision, true, nil
}

func (c *redisLoginRiskCache) RevokedSessionExists(ctx context.Context, appCode string, sessionID string) (bool, error) {
	result, err := c.client.Exists(ctx, revokedSessionKey(appCode, sessionID)).Result()
	if err != nil {
		return false, err
	}
	return result > 0, nil
}

func (c *memoryLoginRiskCache) GetIPDecision(_ context.Context, appCode string, clientIPHash string) (loginRiskDecision, bool, error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	decision, ok := c.ip[ipDecisionKey(defaultLoginRiskConfig().KeyPrefix, appCode, clientIPHash)]
	return decision, ok, nil
}

func (c *memoryLoginRiskCache) RevokedSessionExists(_ context.Context, appCode string, sessionID string) (bool, error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.revokedErr != nil {
		return false, c.revokedErr
	}
	return c.revoked[revokedSessionKey(appCode, sessionID)], nil
}

func (c *memoryLoginRiskCache) setIPDecision(appCode string, clientIP string, decision loginRiskDecision) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.ip[ipDecisionKey(defaultLoginRiskConfig().KeyPrefix, appCode, hashLoginRiskIP(clientIP))] = decision
}

func (c *memoryLoginRiskCache) setRevokedSession(appCode string, sessionID string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.revoked[revokedSessionKey(appCode, sessionID)] = true
}

func ipDecisionKey(prefix string, appCode string, clientIPHash string) string {
	return strings.TrimSpace(prefix) + appcode.Normalize(appCode) + ":" + strings.TrimSpace(clientIPHash)
}

func revokedSessionKey(appCode string, sessionID string) string {
	return "auth:revoked_session:" + appcode.Normalize(appCode) + ":" + strings.TrimSpace(sessionID)
}

func hashLoginRiskIP(ip string) string {
	sum := sha256.Sum256([]byte(strings.TrimSpace(ip)))
	return hex.EncodeToString(sum[:])
}

func (h *Handler) SetLoginRisk(config LoginRiskConfig) {
	h.loginRiskConfig = normalizeLoginRiskConfig(config)
}

func (h *Handler) SetLoginRiskCache(config LoginRiskConfig, cache loginRiskCache) {
	h.loginRiskConfig = normalizeLoginRiskConfig(config)
	h.loginRiskCache = cache
}

func (h *Handler) allowLoginRisk(writer http.ResponseWriter, request *http.Request, input loginRiskInput) bool {
	config := normalizeLoginRiskConfig(h.loginRiskConfig)
	if !config.Enabled {
		return true
	}
	if h.loginRiskIPWhitelisted(request, input) {
		return true
	}
	if reason := config.FastGuard.blockReason(input.Language, input.Timezone); reason != "" {
		h.recordLoginBlocked(request, input, reason)
		httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodeAuthLoginBlocked, "login blocked")
		return false
	}
	if h.loginRiskCache == nil {
		return true
	}

	clientIPHash := hashLoginRiskIP(clientIP(request))
	decision, ok, err := h.loginRiskCache.GetIPDecision(request.Context(), appcode.FromContext(request.Context()), clientIPHash)
	if err != nil {
		logx.Warn(request.Context(), "login_ip_decision_cache_read_failed", slog.String("component", "gateway_login_risk"), slog.String("client_ip_hash", clientIPHash), slog.String("error", err.Error()))
		return true
	}
	if ok && strings.EqualFold(decision.Decision, "blocked") {
		h.recordLoginBlocked(request, input, loginRiskBlockIPCache)
		httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodeAuthLoginBlocked, "login blocked")
		return false
	}
	return true
}

func (h *Handler) loginRiskIPWhitelisted(request *http.Request, input loginRiskInput) bool {
	if h.userClient == nil {
		return false
	}
	ip := strings.TrimSpace(clientIP(request))
	if ip == "" {
		return false
	}
	ctx, cancel := context.WithTimeout(request.Context(), 500*time.Millisecond)
	defer cancel()
	resp, err := h.userClient.CheckLoginRiskIPWhitelist(ctx, &userv1.CheckLoginRiskIPWhitelistRequest{
		Meta:     authRequestMetaWithLoginContext(request, "", input.Platform, input.Language, input.Timezone, "", ""),
		ClientIp: ip,
	})
	if err != nil {
		// 白名单读取失败不能制造额外放行；继续执行原地区屏蔽链路，并保留日志用于排查 Redis/DB 回源异常。
		logx.Warn(request.Context(), "login_ip_whitelist_check_failed", slog.String("component", "gateway_login_risk"), slog.String("client_ip_hash", hashLoginRiskIP(ip)), slog.String("error", err.Error()))
		return false
	}
	return resp.GetWhitelisted()
}

func (h *Handler) recordLoginBlocked(request *http.Request, input loginRiskInput, reason string) {
	if h.userClient == nil {
		return
	}
	ctx, cancel := context.WithTimeout(request.Context(), 500*time.Millisecond)
	defer cancel()
	_, err := h.userClient.RecordLoginBlocked(ctx, &userv1.RecordLoginBlockedRequest{
		Meta:        authRequestMetaWithLoginContext(request, "", input.Platform, input.Language, input.Timezone, "", ""),
		Channel:     input.Channel,
		Platform:    input.Platform,
		Language:    input.Language,
		Timezone:    input.Timezone,
		BlockReason: reason,
	})
	if err != nil {
		logx.Warn(request.Context(), "login_fast_guard_audit_failed", slog.String("component", "gateway_login_risk"), slog.String("block_reason", reason), slog.String("error", err.Error()))
	}
}

func (g LoginRiskFastGuardConfig) blockReason(language string, timezone string) string {
	if blockedLanguage(language, g.BlockedLanguages, g.BlockedLanguagePrimaryTags) {
		return loginRiskBlockLanguage
	}
	if blockedTimezone(timezone, g.BlockedTimezones, g.BlockedTimezonePrefixes) {
		return loginRiskBlockTimezone
	}
	return ""
}

func blockedLanguage(language string, exact []string, primaryTags []string) bool {
	language = strings.TrimSpace(language)
	if language == "" || !languageTagPattern.MatchString(language) {
		return false
	}
	for _, blocked := range exact {
		if strings.EqualFold(strings.TrimSpace(blocked), language) {
			return true
		}
	}
	primary := strings.ToLower(strings.Split(language, "-")[0])
	for _, blocked := range primaryTags {
		if primary == strings.ToLower(strings.TrimSpace(blocked)) {
			return true
		}
	}
	return false
}

func blockedTimezone(timezoneName string, exact []string, prefixes []string) bool {
	timezoneName = strings.TrimSpace(timezoneName)
	if timezoneName == "" {
		return false
	}
	if _, err := time.LoadLocation(timezoneName); err != nil {
		return false
	}
	for _, blocked := range exact {
		if strings.EqualFold(strings.TrimSpace(blocked), timezoneName) {
			return true
		}
	}
	for _, prefix := range prefixes {
		if strings.HasPrefix(timezoneName, strings.TrimSpace(prefix)) {
			return true
		}
	}
	return false
}
