package countryregion

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

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

	mysqlDriver "github.com/go-sql-driver/mysql"
)

const countryRegionCaller = "hyapp-admin-server"
const globalRegionCode = "GLOBAL"
const regionStatusActive = "active"
const regionStatusDisabled = "disabled"
const regionRebuildTaskStatusPending = "pending"
const regionRebuildTaskStatusRunning = "running"
const retiredRegionCodeSQLList = "'Australia and New Zealand', 'Caribbean', 'Melanesia', 'Micronesia', 'Polynesia', 'Southern Africa', 'UNSPECIFIED'"
const maxCountryNameLength = 128
const maxCountryDisplayNameLength = 128
const maxCountryFlagLength = 16
const maxRegionNameLength = 128

var (
	countryCodePattern           = regexp.MustCompile(`^[A-Z]{2,3}$`)
	isoAlpha3Pattern             = regexp.MustCompile(`^[A-Z]{3}$`)
	isoNumericPattern            = regexp.MustCompile(`^[0-9]{3}$`)
	phoneCountryCodePattern      = regexp.MustCompile(`^\+[1-9][0-9]{0,2}$`)
	shortRegionCodePattern       = regexp.MustCompile(`^[A-Za-z0-9_]{2,32}$`)
	descriptiveRegionCodePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9 _-]{1,63}$`)

	errCountryNotFound       = errors.New("country not found")
	errCountryInvalid        = errors.New("country payload invalid")
	errCountryConflict       = errors.New("country already exists")
	errRegionNotFound        = errors.New("region not found")
	errRegionInvalid         = errors.New("region payload invalid")
	errRegionConflict        = errors.New("region already exists")
	errRegionCountryMapping  = errors.New("country already belongs to another active region")
	errGlobalRegionProtected = errors.New("global region is protected")
)

type Service struct {
	userClient userclient.Client
	userDB     *sql.DB
	jobStore   countryCodeRenameJobStore
	roomClient countryCodeRoomClient
}

func NewService(userClient userclient.Client, userDB *sql.DB, jobStore countryCodeRenameJobStore, roomClient countryCodeRoomClient) *Service {
	return &Service{userClient: userClient, userDB: userDB, jobStore: jobStore, roomClient: roomClient}
}

// ListCountries 只编排 user-service 已有 ListCountries RPC，不直接读 user-service 数据库。
func (s *Service) ListCountries(ctx context.Context, requestID string, enabled *bool) ([]*userclient.Country, error) {
	return s.userClient.ListCountries(ctx, userclient.ListCountriesRequest{
		RequestID: strings.TrimSpace(requestID),
		Caller:    countryRegionCaller,
		Enabled:   enabled,
	})
}

// GetCountry 复用国家列表做详情查询，避免为了后台详情新增 user-service 契约。
func (s *Service) GetCountry(ctx context.Context, requestID string, countryID int64) (*userclient.Country, error) {
	// user-service 当前已有列表 RPC、没有单国家详情 RPC；国家主数据规模很小，后台详情复用列表结果避免新增服务契约。
	countries, err := s.ListCountries(ctx, requestID, nil)
	if err != nil {
		return nil, err
	}
	for _, country := range countries {
		if country != nil && country.CountryID == countryID {
			return country, nil
		}
	}
	return nil, errCountryNotFound
}

// CreateCountry 是后台管理创建国家主数据能力，直接写 user DB，不新增 user-service 后台 RPC。
func (s *Service) CreateCountry(ctx context.Context, actorID int64, requestID string, req createCountryRequest) (*userclient.Country, error) {
	if s.userDB == nil {
		return nil, errors.New("user db is not configured")
	}
	req = normalizeCreateCountryRequest(req)
	if !validCountryCreateRequest(req) || strings.TrimSpace(requestID) == "" {
		return nil, errCountryInvalid
	}

	enabled := true
	if req.Enabled != nil {
		enabled = *req.Enabled
	}
	nowMs := time.Now().UnixMilli()
	appCode := appctx.FromContext(ctx)
	result, err := s.userDB.ExecContext(ctx, `
		INSERT INTO countries (
			app_code, country_name, country_code, iso_alpha3, iso_numeric, country_display_name,
			phone_country_code, flag, enabled, sort_order,
			created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
		)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
	`, appCode, req.CountryName, req.CountryCode, nullableString(req.ISOAlpha3), nullableString(req.ISONumeric), req.CountryDisplayName, nullableString(req.PhoneCountryCode), req.Flag, enabled, req.SortOrder, nullableInt64(actorID), nullableInt64(actorID), nowMs, nowMs)
	if err != nil {
		return nil, mapCountryWriteError(err)
	}
	countryID, err := result.LastInsertId()
	if err != nil {
		return nil, err
	}
	return queryCountry(ctx, s.userDB, countryID)
}

// UpdateCountry 不转发 countryCode，保持“国家码创建后不可变”的 app 后端规则。
func (s *Service) UpdateCountry(ctx context.Context, actorID int64, countryID int64, requestID string, req updateCountryRequest) (*userclient.Country, error) {
	return s.userClient.UpdateCountry(ctx, userclient.UpdateCountryRequest{
		RequestID:          strings.TrimSpace(requestID),
		Caller:             countryRegionCaller,
		CountryID:          countryID,
		CountryName:        strings.TrimSpace(req.CountryName),
		CountryDisplayName: strings.TrimSpace(req.CountryDisplayName),
		SortOrder:          req.SortOrder,
		OperatorUserID:     actorID,
		ISOAlpha3:          strings.TrimSpace(req.ISOAlpha3),
		ISONumeric:         strings.TrimSpace(req.ISONumeric),
		PhoneCountryCode:   strings.TrimSpace(req.PhoneCountryCode),
		Flag:               strings.TrimSpace(req.Flag),
	})
}

// EnableCountry 是后台管理状态开关，只恢复 App 可选入口，不改历史用户国家和区域快照。
func (s *Service) EnableCountry(ctx context.Context, actorID int64, countryID int64, requestID string) (*userclient.Country, error) {
	return s.setCountryEnabled(ctx, actorID, countryID, strings.TrimSpace(requestID), true)
}

// DisableCountry 是国家删除接口背后的后台软删除语义：禁用 App 可选状态，不硬删主数据。
func (s *Service) DisableCountry(ctx context.Context, actorID int64, countryID int64, requestID string) (*userclient.Country, error) {
	return s.setCountryEnabled(ctx, actorID, countryID, strings.TrimSpace(requestID), false)
}

// ListRegions 只编排 user-service 已有 ListRegions RPC，不在 admin-server 维护区域副本。
func (s *Service) ListRegions(ctx context.Context, requestID string, status string) ([]*userclient.Region, error) {
	return s.userClient.ListRegions(ctx, userclient.ListRegionsRequest{
		RequestID: strings.TrimSpace(requestID),
		Caller:    countryRegionCaller,
		Status:    strings.TrimSpace(status),
	})
}

// GetRegion 按 regionID 读取区域和 active 国家码列表。
func (s *Service) GetRegion(ctx context.Context, requestID string, regionID int64) (*userclient.Region, error) {
	return s.userClient.GetRegion(ctx, userclient.GetRegionRequest{
		RequestID: strings.TrimSpace(requestID),
		Caller:    countryRegionCaller,
		RegionID:  regionID,
	})
}

// CreateRegion 是后台管理创建区域能力，直接写 user DB 并创建历史用户区域重算任务。
func (s *Service) CreateRegion(ctx context.Context, actorID int64, requestID string, req createRegionRequest) (*userclient.Region, error) {
	if s.userDB == nil {
		return nil, errors.New("user db is not configured")
	}
	req = normalizeCreateRegionRequest(req)
	if !validRegionCreateRequest(req) || strings.TrimSpace(requestID) == "" {
		return nil, errRegionInvalid
	}

	tx, err := s.userDB.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()

	nowMs := time.Now().UnixMilli()
	countries := []string{}
	if !isGlobalRegionCode(req.RegionCode) {
		countries, err = canonicalCountryCodes(ctx, tx, req.Countries)
		if err != nil {
			return nil, err
		}
		if err := releaseRetiredCountryMappings(ctx, tx, countries, actorID, nowMs); err != nil {
			return nil, err
		}
		if err := ensureCountriesAssignableToNewRegion(ctx, tx, countries); err != nil {
			return nil, err
		}
	}
	appCode := appctx.FromContext(ctx)
	result, err := tx.ExecContext(ctx, `
		INSERT INTO regions (app_code, region_code, name, status, sort_order, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
	`, appCode, req.RegionCode, req.Name, regionStatusActive, req.SortOrder, nullableInt64(actorID), nullableInt64(actorID), nowMs, nowMs)
	if err != nil {
		return nil, mapRegionWriteError(err)
	}
	regionID, err := result.LastInsertId()
	if err != nil {
		return nil, err
	}
	for _, country := range countries {
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO region_countries (app_code, region_id, country_code, status, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?)
		`, appCode, regionID, country, regionStatusActive, nullableInt64(actorID), nullableInt64(actorID), nowMs, nowMs); err != nil {
			return nil, mapRegionCountryWriteError(err)
		}
		if err := insertRegionRebuildTask(ctx, tx, regionID, country, regionID, nowMs); err != nil {
			return nil, err
		}
	}
	region := &userclient.Region{
		RegionID:    regionID,
		RegionCode:  req.RegionCode,
		Name:        req.Name,
		Status:      regionStatusActive,
		Countries:   append([]string(nil), countries...),
		SortOrder:   req.SortOrder,
		CreatedAtMs: nowMs,
		UpdatedAtMs: nowMs,
	}
	if err := insertRegionChangeLog(ctx, tx, regionID, "create_region", nil, region, actorID, strings.TrimSpace(requestID), nowMs); err != nil {
		return nil, err
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	return queryRegion(ctx, s.userDB, regionID)
}

// UpdateRegion 只修改区域展示字段，不改变 countries，避免编辑表单误触发归属重算。
func (s *Service) UpdateRegion(ctx context.Context, actorID int64, regionID int64, requestID string, req updateRegionRequest) (*userclient.Region, error) {
	return s.userClient.UpdateRegion(ctx, userclient.UpdateRegionRequest{
		RequestID:      strings.TrimSpace(requestID),
		Caller:         countryRegionCaller,
		RegionID:       regionID,
		RegionCode:     strings.TrimSpace(req.RegionCode),
		Name:           strings.TrimSpace(req.Name),
		SortOrder:      req.SortOrder,
		OperatorUserID: actorID,
	})
}

// ReplaceRegionCountries 单独替换国家归属；user-service 会负责释放旧映射并创建用户区域重算任务。
func (s *Service) ReplaceRegionCountries(ctx context.Context, actorID int64, regionID int64, requestID string, req replaceRegionCountriesRequest) (*userclient.Region, error) {
	return s.userClient.ReplaceRegionCountries(ctx, userclient.ReplaceRegionCountriesRequest{
		RequestID:      strings.TrimSpace(requestID),
		Caller:         countryRegionCaller,
		RegionID:       regionID,
		Countries:      append([]string(nil), req.Countries...),
		OperatorUserID: actorID,
	})
}

// EnableRegion 是后台管理状态开关，只更新 user DB 区域状态，不新增 user-service 契约。
func (s *Service) EnableRegion(ctx context.Context, actorID int64, regionID int64, requestID string) (*userclient.Region, error) {
	return s.setRegionEnabled(ctx, actorID, regionID, strings.TrimSpace(requestID), true)
}

// DisableRegion 是后台管理状态开关：停用区域并释放 active 国家归属。
func (s *Service) DisableRegion(ctx context.Context, actorID int64, regionID int64, requestID string) (*userclient.Region, error) {
	return s.setRegionEnabled(ctx, actorID, regionID, strings.TrimSpace(requestID), false)
}

func (s *Service) setRegionEnabled(ctx context.Context, actorID int64, regionID int64, requestID string, enabled bool) (*userclient.Region, error) {
	if s.userDB == nil {
		return nil, errors.New("user db is not configured")
	}
	tx, err := s.userDB.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()

	before, err := queryRegionForUpdate(ctx, tx, regionID)
	if err != nil {
		return nil, err
	}
	if !enabled && isGlobalRegionCode(before.RegionCode) {
		return nil, errGlobalRegionProtected
	}

	nowMs := time.Now().UnixMilli()
	nextStatus := regionStatusDisabled
	operation := "disable_region"
	if enabled {
		nextStatus = regionStatusActive
		operation = "enable_region"
	}

	if _, err := tx.ExecContext(ctx, `
		UPDATE regions
		SET status = ?, updated_by_user_id = ?, updated_at_ms = ?
		WHERE app_code = ? AND region_id = ?
	`, nextStatus, nullableInt64(actorID), nowMs, appctx.FromContext(ctx), regionID); err != nil {
		return nil, err
	}

	after := *before
	after.Status = nextStatus
	after.UpdatedAtMs = nowMs
	if !enabled {
		if _, err := tx.ExecContext(ctx, `
			UPDATE region_countries
			SET status = ?, updated_by_user_id = ?, updated_at_ms = ?
			WHERE app_code = ? AND region_id = ? AND status = ?
		`, regionStatusDisabled, nullableInt64(actorID), nowMs, appctx.FromContext(ctx), regionID, regionStatusActive); err != nil {
			return nil, err
		}
		for _, country := range before.Countries {
			if err := insertRegionRebuildTask(ctx, tx, regionID, country, 0, nowMs); err != nil {
				return nil, err
			}
		}
		after.Countries = nil
	}

	if err := insertRegionChangeLog(ctx, tx, regionID, operation, before, &after, actorID, requestID, nowMs); err != nil {
		return nil, err
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	return queryRegion(ctx, s.userDB, regionID)
}

func (s *Service) setCountryEnabled(ctx context.Context, actorID int64, countryID int64, requestID string, enabled bool) (*userclient.Country, error) {
	if s.userDB == nil {
		return nil, errors.New("user db is not configured")
	}
	if countryID <= 0 || strings.TrimSpace(requestID) == "" {
		return nil, errCountryInvalid
	}

	result, err := s.userDB.ExecContext(ctx, `
		UPDATE countries
		SET enabled = ?, updated_by_user_id = ?, updated_at_ms = ?
		WHERE app_code = ? AND country_id = ?
	`, enabled, nullableInt64(actorID), time.Now().UnixMilli(), appctx.FromContext(ctx), countryID)
	if err != nil {
		return nil, err
	}
	if affected, err := result.RowsAffected(); err != nil {
		return nil, err
	} else if affected == 0 {
		return nil, errCountryNotFound
	}

	return queryCountry(ctx, s.userDB, countryID)
}

type regionQueryer interface {
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}

func queryCountry(ctx context.Context, q regionQueryer, countryID int64) (*userclient.Country, error) {
	var country userclient.Country
	if err := q.QueryRowContext(ctx, `
		SELECT country_id, country_name, country_code,
		       COALESCE(iso_alpha3, ''), COALESCE(iso_numeric, ''),
		       country_display_name, COALESCE(phone_country_code, ''), COALESCE(flag, ''), enabled,
		       sort_order, created_at_ms, updated_at_ms
		FROM countries
		WHERE app_code = ? AND country_id = ?
	`, appctx.FromContext(ctx), countryID).Scan(
		&country.CountryID,
		&country.CountryName,
		&country.CountryCode,
		&country.ISOAlpha3,
		&country.ISONumeric,
		&country.CountryDisplayName,
		&country.PhoneCountryCode,
		&country.Flag,
		&country.Enabled,
		&country.SortOrder,
		&country.CreatedAtMs,
		&country.UpdatedAtMs,
	); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, errCountryNotFound
		}
		return nil, err
	}
	return &country, nil
}

func queryRegionForUpdate(ctx context.Context, tx *sql.Tx, regionID int64) (*userclient.Region, error) {
	return scanRegion(ctx, tx, `
		SELECT region_id, region_code, name, status, sort_order, created_at_ms, updated_at_ms
		FROM regions
		WHERE app_code = ? AND region_id = ?
		FOR UPDATE
	`, appctx.FromContext(ctx), regionID)
}

func queryRegion(ctx context.Context, q regionQueryer, regionID int64) (*userclient.Region, error) {
	return scanRegion(ctx, q, `
		SELECT region_id, region_code, name, status, sort_order, created_at_ms, updated_at_ms
		FROM regions
		WHERE app_code = ? AND region_id = ?
	`, appctx.FromContext(ctx), regionID)
}

func scanRegion(ctx context.Context, q regionQueryer, query string, args ...any) (*userclient.Region, error) {
	var region userclient.Region
	if err := q.QueryRowContext(ctx, query, args...).Scan(
		&region.RegionID,
		&region.RegionCode,
		&region.Name,
		&region.Status,
		&region.SortOrder,
		&region.CreatedAtMs,
		&region.UpdatedAtMs,
	); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, errRegionNotFound
		}
		return nil, err
	}
	countries, err := activeCountriesForRegion(ctx, q, region.RegionID)
	if err != nil {
		return nil, err
	}
	region.Countries = countries
	return &region, nil
}

func activeCountriesForRegion(ctx context.Context, q regionQueryer, regionID int64) ([]string, error) {
	rows, err := q.QueryContext(ctx, `
		SELECT country_code
		FROM region_countries
		WHERE app_code = ? AND region_id = ? AND status = ?
		ORDER BY country_code ASC
	`, appctx.FromContext(ctx), regionID, regionStatusActive)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	countries := []string{}
	for rows.Next() {
		var country string
		if err := rows.Scan(&country); err != nil {
			return nil, err
		}
		countries = append(countries, country)
	}
	return countries, rows.Err()
}

func insertRegionRebuildTask(ctx context.Context, tx *sql.Tx, regionID int64, countryCode string, targetRegionID int64, nowMs int64) error {
	_, err := tx.ExecContext(ctx, `
		INSERT INTO user_region_rebuild_tasks (app_code, region_id, country_code, target_region_id, mapping_revision, status, cursor_user_id, created_at_ms, updated_at_ms)
		VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
	`, appctx.FromContext(ctx), nullableInt64(regionID), countryCode, nullableInt64(targetRegionID), nowMs, regionRebuildTaskStatusPending, nowMs, nowMs)
	return err
}

func insertRegionChangeLog(ctx context.Context, tx *sql.Tx, regionID int64, operation string, before *userclient.Region, after *userclient.Region, actorID int64, requestID string, nowMs int64) error {
	beforeJSON, err := nullableJSON(before)
	if err != nil {
		return err
	}
	afterJSON, err := nullableJSON(after)
	if err != nil {
		return err
	}
	_, err = tx.ExecContext(ctx, `
		INSERT INTO region_change_logs (app_code, region_id, operation, before_json, after_json, operator_user_id, request_id, created_at_ms)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?)
	`, appctx.FromContext(ctx), nullableInt64(regionID), operation, beforeJSON, afterJSON, nullableInt64(actorID), requestID, nowMs)
	return err
}

func nullableJSON(value any) (any, error) {
	if value == nil {
		return nil, nil
	}
	body, err := json.Marshal(value)
	if err != nil {
		return nil, err
	}
	return string(body), nil
}

func nullableInt64(value int64) any {
	if value <= 0 {
		return nil
	}
	return value
}

func nullableString(value string) any {
	if strings.TrimSpace(value) == "" {
		return nil
	}
	return value
}

func normalizeCreateCountryRequest(req createCountryRequest) createCountryRequest {
	req.CountryName = strings.TrimSpace(req.CountryName)
	req.CountryCode = strings.ToUpper(strings.TrimSpace(req.CountryCode))
	req.ISOAlpha3 = strings.ToUpper(strings.TrimSpace(req.ISOAlpha3))
	req.ISONumeric = strings.TrimSpace(req.ISONumeric)
	req.CountryDisplayName = strings.TrimSpace(req.CountryDisplayName)
	req.PhoneCountryCode = strings.TrimSpace(req.PhoneCountryCode)
	req.Flag = strings.TrimSpace(req.Flag)
	return req
}

func validCountryCreateRequest(req createCountryRequest) bool {
	if !countryCodePattern.MatchString(req.CountryCode) {
		return false
	}
	if req.CountryName == "" || utf8.RuneCountInString(req.CountryName) > maxCountryNameLength {
		return false
	}
	if req.CountryDisplayName == "" || utf8.RuneCountInString(req.CountryDisplayName) > maxCountryDisplayNameLength {
		return false
	}
	if req.ISOAlpha3 != "" && !isoAlpha3Pattern.MatchString(req.ISOAlpha3) {
		return false
	}
	if req.ISONumeric != "" && !isoNumericPattern.MatchString(req.ISONumeric) {
		return false
	}
	if req.PhoneCountryCode != "" && !phoneCountryCodePattern.MatchString(req.PhoneCountryCode) {
		return false
	}
	return utf8.RuneCountInString(req.Flag) <= maxCountryFlagLength
}

func mapCountryWriteError(err error) error {
	var mysqlErr *mysqlDriver.MySQLError
	if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
		return errCountryConflict
	}
	return err
}

func normalizeCreateRegionRequest(req createRegionRequest) createRegionRequest {
	req.RegionCode = normalizeRegionCode(req.RegionCode)
	req.Name = strings.TrimSpace(req.Name)
	req.Countries = append([]string(nil), req.Countries...)
	return req
}

func normalizeRegionCode(code string) string {
	code = strings.TrimSpace(code)
	if shortRegionCodePattern.MatchString(code) {
		return strings.ToUpper(code)
	}
	return strings.Join(strings.Fields(code), " ")
}

func validRegionCreateRequest(req createRegionRequest) bool {
	if !validRegionCode(req.RegionCode) {
		return false
	}
	if isRetiredRegionCode(req.RegionCode) {
		return false
	}
	if req.Name == "" || utf8.RuneCountInString(req.Name) > maxRegionNameLength {
		return false
	}
	if isGlobalRegionCode(req.RegionCode) {
		return len(req.Countries) == 0
	}
	return len(req.Countries) > 0
}

func validRegionCode(code string) bool {
	return shortRegionCodePattern.MatchString(code) || descriptiveRegionCodePattern.MatchString(code)
}

func isGlobalRegionCode(code string) bool {
	return normalizeRegionCode(code) == globalRegionCode
}

func isRetiredRegionCode(code string) bool {
	switch strings.ToUpper(normalizeRegionCode(code)) {
	case "AUSTRALIA AND NEW ZEALAND", "CARIBBEAN", "MELANESIA", "MICRONESIA", "POLYNESIA", "SOUTHERN AFRICA", "UNSPECIFIED":
		return true
	default:
		return false
	}
}

func canonicalCountryCodes(ctx context.Context, q regionQueryer, countries []string) ([]string, error) {
	if len(countries) == 0 {
		return nil, errRegionInvalid
	}
	seen := make(map[string]struct{}, len(countries))
	result := make([]string, 0, len(countries))
	for _, input := range countries {
		countryCode := strings.ToUpper(strings.TrimSpace(input))
		if !countryCodePattern.MatchString(countryCode) {
			return nil, errRegionInvalid
		}
		var canonical string
		err := q.QueryRowContext(ctx, `
			SELECT country_code
			FROM countries
			WHERE app_code = ? AND country_code = ?
		`, appctx.FromContext(ctx), countryCode).Scan(&canonical)
		if errors.Is(err, sql.ErrNoRows) {
			return nil, errRegionInvalid
		}
		if err != nil {
			return nil, err
		}
		if _, exists := seen[canonical]; exists {
			return nil, errRegionInvalid
		}
		seen[canonical] = struct{}{}
		result = append(result, canonical)
	}
	return result, nil
}

func ensureCountriesAssignableToNewRegion(ctx context.Context, q regionQueryer, countries []string) error {
	for _, country := range countries {
		var existingRegionID int64
		err := q.QueryRowContext(ctx, `
			SELECT region_id
			FROM region_countries
			WHERE app_code = ? AND country_code = ? AND status = ?
			LIMIT 1
		`, appctx.FromContext(ctx), country, regionStatusActive).Scan(&existingRegionID)
		if errors.Is(err, sql.ErrNoRows) {
			continue
		}
		if err != nil {
			return err
		}
		return errRegionCountryMapping
	}
	return nil
}

func releaseRetiredCountryMappings(ctx context.Context, tx *sql.Tx, countries []string, actorID int64, nowMs int64) error {
	for _, country := range countries {
		if _, err := tx.ExecContext(ctx, `
			UPDATE region_countries rc
			INNER JOIN regions rg ON rg.app_code = rc.app_code AND rg.region_id = rc.region_id
			SET rc.status = ?, rc.updated_by_user_id = ?, rc.updated_at_ms = ?
			WHERE rc.app_code = ? AND rc.country_code = ? AND rc.status = ?
			  AND rg.region_code IN (`+retiredRegionCodeSQLList+`)
		`, regionStatusDisabled, nullableInt64(actorID), nowMs, appctx.FromContext(ctx), country, regionStatusActive); err != nil {
			return err
		}
	}
	return nil
}

func mapRegionWriteError(err error) error {
	var mysqlErr *mysqlDriver.MySQLError
	if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
		return errRegionConflict
	}
	return err
}

func mapRegionCountryWriteError(err error) error {
	var mysqlErr *mysqlDriver.MySQLError
	if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
		return errRegionCountryMapping
	}
	return err
}
