package regionblock

import (
	"context"
	"database/sql"
	"errors"
	"net/netip"
	"regexp"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"

	"hyapp-admin-server/internal/appctx"
	"hyapp-admin-server/internal/integration/userclient"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

const (
	maxRegionBlockKeywords       = 200
	maxRegionBlockKeywordLength  = 128
	maxIPWhitelistItems          = 500
	maxUserWhitelistItems        = 500
	maxUserWhitelistLookupLength = 64
)

var (
	countryCodePattern       = regexp.MustCompile(`^[A-Za-z]{2,3}$`)
	errInvalidPayload        = errors.New("region block payload invalid")
	errWhitelistCacheRefresh = errors.New("login risk whitelist cache refresh failed")
)

type Service struct {
	userDB *sql.DB
	user   userclient.Client
}

type RegionBlock struct {
	BlockID     int64  `json:"block_id"`
	Keyword     string `json:"keyword"`
	CountryCode string `json:"country_code"`
	Enabled     bool   `json:"enabled"`
	CreatedAtMS int64  `json:"created_at_ms"`
	UpdatedAtMS int64  `json:"updated_at_ms"`
}

type IPWhitelistItem struct {
	WhitelistID int64  `json:"whitelist_id"`
	IPAddress   string `json:"ip_address"`
	Enabled     bool   `json:"enabled"`
	CreatedAtMS int64  `json:"created_at_ms"`
	UpdatedAtMS int64  `json:"updated_at_ms"`
}

type UserWhitelistItem struct {
	WhitelistID   int64  `json:"whitelist_id"`
	UserID        int64  `json:"user_id,string"`
	DisplayUserID string `json:"display_user_id"`
	Enabled       bool   `json:"enabled"`
	CreatedAtMS   int64  `json:"created_at_ms"`
	UpdatedAtMS   int64  `json:"updated_at_ms"`
}

type Config struct {
	Items              []RegionBlock       `json:"items"`
	Total              int                 `json:"total"`
	WhitelistItems     []IPWhitelistItem   `json:"whitelist_items"`
	WhitelistTotal     int                 `json:"whitelist_total"`
	WhitelistUserItems []UserWhitelistItem `json:"whitelist_user_items"`
	WhitelistUserTotal int                 `json:"whitelist_user_total"`
}

type normalizedKeyword struct {
	Keyword     string
	CountryCode string
}

type countryCodeQuerier interface {
	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}

func NewService(userDB *sql.DB, user userclient.Client) *Service {
	return &Service{userDB: userDB, user: user}
}

// List 返回当前 App 已启用的登录地区屏蔽词、IP 白名单和用户白名单。
// 表属于 user-service 登录风控事实，admin-server 只作为后台维护入口直接写 user DB。
func (s *Service) List(ctx context.Context) (Config, error) {
	if s.userDB == nil {
		return Config{}, errors.New("user db is not configured")
	}
	appCode := appctx.FromContext(ctx)
	blocks, err := queryEnabledBlocks(ctx, s.userDB, appCode)
	if err != nil {
		return Config{}, err
	}
	whitelist, err := queryEnabledIPWhitelist(ctx, s.userDB, appCode)
	if err != nil {
		return Config{}, err
	}
	userWhitelist, err := queryEnabledUserWhitelist(ctx, s.userDB, appCode)
	if err != nil {
		return Config{}, err
	}
	return Config{Items: blocks, Total: len(blocks), WhitelistItems: whitelist, WhitelistTotal: len(whitelist), WhitelistUserItems: userWhitelist, WhitelistUserTotal: len(userWhitelist)}, nil
}

// Replace 用页面提交的词表整体替换当前 App 的生效配置。
// 整体替换比逐条增删更适合配置页保存语义，也能避免客户端和服务端出现半旧半新的屏蔽词集合。
func (s *Service) Replace(ctx context.Context, requestID string, actorID int64, keywords []string, whitelistIPs *[]string, whitelistUserIDs *[]string) (Config, error) {
	if s.userDB == nil {
		return Config{}, errors.New("user db is not configured")
	}
	normalized, err := normalizeKeywords(ctx, s.userDB, appctx.FromContext(ctx), keywords)
	if err != nil {
		return Config{}, err
	}
	var normalizedIPs []string
	if whitelistIPs != nil {
		normalizedIPs, err = normalizeIPWhitelist(*whitelistIPs)
		if err != nil {
			return Config{}, err
		}
	}
	var normalizedUserIDs []int64
	if whitelistUserIDs != nil {
		normalizedUserIDs, err = s.normalizeUserIDWhitelist(ctx, requestID, *whitelistUserIDs)
		if err != nil {
			return Config{}, err
		}
	}
	tx, err := s.userDB.BeginTx(ctx, nil)
	if err != nil {
		return Config{}, err
	}
	defer tx.Rollback()

	nowMS := time.Now().UTC().UnixMilli()
	appCode := appctx.FromContext(ctx)
	if _, err := tx.ExecContext(ctx, `
		UPDATE login_risk_country_blocks
		SET enabled = 0, updated_by_user_id = ?, updated_at_ms = ?
		WHERE app_code = ? AND enabled = 1
	`, nullableActor(actorID), nowMS, appCode); err != nil {
		return Config{}, err
	}
	for _, keyword := range normalized {
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO login_risk_country_blocks (
				app_code, keyword, country_code, enabled,
				created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
			)
			VALUES (?, ?, ?, 1, ?, ?, ?, ?)
			ON DUPLICATE KEY UPDATE
				country_code = VALUES(country_code),
				enabled = 1,
				updated_by_user_id = VALUES(updated_by_user_id),
				updated_at_ms = VALUES(updated_at_ms)
		`, appCode, keyword.Keyword, keyword.CountryCode, nullableActor(actorID), nullableActor(actorID), nowMS, nowMS); err != nil {
			return Config{}, err
		}
	}
	if whitelistIPs != nil {
		if _, err := tx.ExecContext(ctx, `
			UPDATE login_risk_ip_whitelist
			SET enabled = 0, updated_by_user_id = ?, updated_at_ms = ?
			WHERE app_code = ? AND enabled = 1
		`, nullableActor(actorID), nowMS, appCode); err != nil {
			return Config{}, err
		}
		for _, ip := range normalizedIPs {
			if _, err := tx.ExecContext(ctx, `
				INSERT INTO login_risk_ip_whitelist (
					app_code, ip_address, enabled,
					created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
				)
				VALUES (?, ?, 1, ?, ?, ?, ?)
				ON DUPLICATE KEY UPDATE
					enabled = 1,
					updated_by_user_id = VALUES(updated_by_user_id),
					updated_at_ms = VALUES(updated_at_ms)
			`, appCode, ip, nullableActor(actorID), nullableActor(actorID), nowMS, nowMS); err != nil {
				return Config{}, err
			}
		}
	}
	if whitelistUserIDs != nil {
		if _, err := tx.ExecContext(ctx, `
			UPDATE login_risk_user_whitelist
			SET enabled = 0, updated_by_user_id = ?, updated_at_ms = ?
			WHERE app_code = ? AND enabled = 1
		`, nullableActor(actorID), nowMS, appCode); err != nil {
			return Config{}, err
		}
		for _, userID := range normalizedUserIDs {
			if _, err := tx.ExecContext(ctx, `
				INSERT INTO login_risk_user_whitelist (
					app_code, user_id, enabled,
					created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
				)
				VALUES (?, ?, 1, ?, ?, ?, ?)
				ON DUPLICATE KEY UPDATE
					enabled = 1,
					updated_by_user_id = VALUES(updated_by_user_id),
					updated_at_ms = VALUES(updated_at_ms)
			`, appCode, userID, nullableActor(actorID), nullableActor(actorID), nowMS, nowMS); err != nil {
				return Config{}, err
			}
		}
	}
	if err := tx.Commit(); err != nil {
		return Config{}, err
	}
	if whitelistIPs != nil || whitelistUserIDs != nil {
		if err := s.refreshWhitelistCache(ctx, requestID); err != nil {
			return Config{}, err
		}
	}
	return s.List(ctx)
}

func normalizeKeywords(ctx context.Context, querier countryCodeQuerier, appCode string, raw []string) ([]normalizedKeyword, error) {
	if len(raw) > maxRegionBlockKeywords {
		return nil, errInvalidPayload
	}
	result := make([]normalizedKeyword, 0, len(raw))
	seen := map[string]struct{}{}
	for _, item := range raw {
		keyword := strings.TrimSpace(item)
		if keyword == "" || utf8.RuneCountInString(keyword) > maxRegionBlockKeywordLength {
			return nil, errInvalidPayload
		}
		if countryCodePattern.MatchString(keyword) {
			keyword = strings.ToUpper(keyword)
		}
		key := strings.ToLower(keyword)
		if _, ok := seen[key]; ok {
			continue
		}
		countryCode, err := resolveCountryCode(ctx, querier, appCode, keyword)
		if err != nil {
			return nil, err
		}
		seen[key] = struct{}{}
		result = append(result, normalizedKeyword{Keyword: keyword, CountryCode: countryCode})
	}
	return result, nil
}

func normalizeIPWhitelist(raw []string) ([]string, error) {
	if len(raw) > maxIPWhitelistItems {
		return nil, errInvalidPayload
	}
	result := make([]string, 0, len(raw))
	seen := map[string]struct{}{}
	for _, item := range raw {
		addr, err := netip.ParseAddr(strings.TrimSpace(item))
		if err != nil {
			return nil, errInvalidPayload
		}
		ip := addr.String()
		if _, ok := seen[ip]; ok {
			continue
		}
		seen[ip] = struct{}{}
		result = append(result, ip)
	}
	return result, nil
}

func (s *Service) normalizeUserIDWhitelist(ctx context.Context, requestID string, raw []string) ([]int64, error) {
	if len(raw) > maxUserWhitelistItems {
		return nil, errInvalidPayload
	}
	result := make([]int64, 0, len(raw))
	seen := map[int64]struct{}{}
	for _, item := range raw {
		value := strings.TrimSpace(item)
		if value == "" {
			return nil, errInvalidPayload
		}
		userID, err := s.resolveWhitelistUserID(ctx, requestID, value)
		if err != nil {
			return nil, err
		}
		if userID <= 0 {
			return nil, errInvalidPayload
		}
		if _, ok := seen[userID]; ok {
			continue
		}
		seen[userID] = struct{}{}
		result = append(result, userID)
	}
	return result, nil
}

func (s *Service) resolveWhitelistUserID(ctx context.Context, requestID string, raw string) (int64, error) {
	value := strings.TrimSpace(raw)
	if value == "" || utf8.RuneCountInString(value) > maxUserWhitelistLookupLength {
		return 0, errInvalidPayload
	}
	if s.user == nil {
		// 本地隔离测试没有 user-service client 时只接受真实 user_id，避免把短号误写成未知用户。
		numericID, numericOK := parsePositiveInt64(value)
		if !numericOK {
			return 0, errInvalidPayload
		}
		return numericID, nil
	}

	// 后台输入优先按当前 active 展示号解析，默认短号和靓号都由 user-service 统一处理；
	// 保存时只落真实 user_id，后续用户改短号或靓号过期也不会影响登录风控白名单。
	if identity, err := s.user.ResolveDisplayUserID(ctx, userclient.ResolveDisplayUserIDRequest{
		RequestID:     requestID,
		Caller:        "admin-server",
		DisplayUserID: value,
	}); err != nil {
		if !isGRPCNotFound(err) && !isGRPCInvalidArgument(err) {
			return 0, err
		}
	} else if identity != nil && identity.UserID > 0 {
		return identity.UserID, nil
	}

	// 展示号没有命中时，纯数字输入再兜底按内部 user_id 校验；非数字靓号未命中就直接视为无效配置。
	numericID, numericOK := parsePositiveInt64(value)
	if !numericOK {
		return 0, errInvalidPayload
	}
	if found, err := s.userExists(ctx, requestID, numericID); err != nil {
		return 0, err
	} else if found {
		return numericID, nil
	}
	return 0, errInvalidPayload
}

func (s *Service) userExists(ctx context.Context, requestID string, userID int64) (bool, error) {
	user, err := s.user.GetUser(ctx, userclient.GetUserRequest{
		RequestID: requestID,
		Caller:    "admin-server",
		UserID:    userID,
	})
	if err != nil {
		if isGRPCNotFound(err) {
			return false, nil
		}
		return false, err
	}
	return user != nil && user.UserID > 0, nil
}

func parsePositiveInt64(text string) (int64, bool) {
	parsed, err := strconv.ParseInt(strings.TrimSpace(text), 10, 64)
	return parsed, err == nil && parsed > 0
}

func isGRPCNotFound(err error) bool {
	return status.Code(err) == codes.NotFound
}

func isGRPCInvalidArgument(err error) bool {
	return status.Code(err) == codes.InvalidArgument
}

func (s *Service) refreshWhitelistCache(ctx context.Context, requestID string) error {
	if s.user == nil {
		return nil
	}
	_, err := s.user.RefreshLoginRiskWhitelistCache(ctx, userclient.RefreshLoginRiskWhitelistCacheRequest{
		RequestID: requestID,
		Caller:    "admin-server",
	})
	if err != nil {
		return errWhitelistCacheRefresh
	}
	return nil
}

func resolveCountryCode(ctx context.Context, querier countryCodeQuerier, appCode string, keyword string) (string, error) {
	if countryCodePattern.MatchString(keyword) {
		return strings.ToUpper(keyword), nil
	}
	upperKeyword := strings.ToUpper(keyword)
	var countryCode string
	err := querier.QueryRowContext(ctx, `
		SELECT country_code
		FROM countries
		WHERE app_code = ?
		  AND (
			country_name = ?
			OR country_display_name = ?
			OR UPPER(country_code) = ?
			OR UPPER(COALESCE(iso_alpha3, '')) = ?
		  )
		ORDER BY enabled DESC, sort_order DESC, country_id ASC
		LIMIT 1
	`, appCode, keyword, keyword, upperKeyword, upperKeyword).Scan(&countryCode)
	if errors.Is(err, sql.ErrNoRows) {
		return "", nil
	}
	if err != nil {
		return "", err
	}
	return strings.ToUpper(strings.TrimSpace(countryCode)), nil
}

func queryEnabledBlocks(ctx context.Context, db *sql.DB, appCode string) ([]RegionBlock, error) {
	rows, err := db.QueryContext(ctx, `
		SELECT block_id, keyword, country_code, enabled, created_at_ms, updated_at_ms
		FROM login_risk_country_blocks
		WHERE app_code = ? AND enabled = 1
		ORDER BY country_code ASC, keyword ASC, block_id ASC
	`, appCode)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := make([]RegionBlock, 0)
	for rows.Next() {
		var item RegionBlock
		if err := rows.Scan(&item.BlockID, &item.Keyword, &item.CountryCode, &item.Enabled, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

func queryEnabledIPWhitelist(ctx context.Context, db *sql.DB, appCode string) ([]IPWhitelistItem, error) {
	rows, err := db.QueryContext(ctx, `
		SELECT whitelist_id, ip_address, enabled, created_at_ms, updated_at_ms
		FROM login_risk_ip_whitelist
		WHERE app_code = ? AND enabled = 1
		ORDER BY ip_address ASC, whitelist_id ASC
	`, appCode)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := make([]IPWhitelistItem, 0)
	for rows.Next() {
		var item IPWhitelistItem
		if err := rows.Scan(&item.WhitelistID, &item.IPAddress, &item.Enabled, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

func queryEnabledUserWhitelist(ctx context.Context, db *sql.DB, appCode string) ([]UserWhitelistItem, error) {
	rows, err := db.QueryContext(ctx, `
		SELECT w.whitelist_id, w.user_id, COALESCE(u.current_display_user_id, ''), w.enabled, w.created_at_ms, w.updated_at_ms
		FROM login_risk_user_whitelist w
		LEFT JOIN users u
		  ON u.app_code = w.app_code
		 AND u.user_id = w.user_id
		WHERE w.app_code = ? AND w.enabled = 1
		ORDER BY w.user_id ASC, w.whitelist_id ASC
	`, appCode)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := make([]UserWhitelistItem, 0)
	for rows.Next() {
		var item UserWhitelistItem
		if err := rows.Scan(&item.WhitelistID, &item.UserID, &item.DisplayUserID, &item.Enabled, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
			return nil, err
		}
		if item.UserID > 0 {
			items = append(items, item)
		}
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

func nullableActor(actorID int64) any {
	if actorID <= 0 {
		return nil
	}
	return actorID
}
