package regionblock

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

	"hyapp-admin-server/internal/appctx"
)

const (
	maxRegionBlockKeywords      = 200
	maxRegionBlockKeywordLength = 128
)

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

type Service struct {
	userDB *sql.DB
}

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 normalizedKeyword struct {
	Keyword     string
	CountryCode string
}

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

func NewService(userDB *sql.DB) *Service {
	return &Service{userDB: userDB}
}

// List 返回当前 App 已启用的登录地区屏蔽词。
// 表属于 user-service 登录风控事实，admin-server 只作为后台维护入口直接写 user DB。
func (s *Service) List(ctx context.Context) ([]RegionBlock, error) {
	if s.userDB == nil {
		return nil, errors.New("user db is not configured")
	}
	return queryEnabledBlocks(ctx, s.userDB, appctx.FromContext(ctx))
}

// Replace 用页面提交的词表整体替换当前 App 的生效配置。
// 整体替换比逐条增删更适合配置页保存语义，也能避免客户端和服务端出现半旧半新的屏蔽词集合。
func (s *Service) Replace(ctx context.Context, actorID int64, keywords []string) ([]RegionBlock, error) {
	if s.userDB == nil {
		return nil, errors.New("user db is not configured")
	}
	normalized, err := normalizeKeywords(ctx, s.userDB, appctx.FromContext(ctx), keywords)
	if err != nil {
		return nil, err
	}
	tx, err := s.userDB.BeginTx(ctx, nil)
	if err != nil {
		return nil, 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 nil, 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 nil, err
		}
	}
	if err := tx.Commit(); err != nil {
		return nil, 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 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 ASC, 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 nullableActor(actorID int64) any {
	if actorID <= 0 {
		return nil
	}
	return actorID
}
