package region

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"slices"
	"sort"
	"strings"

	mysqldriver "github.com/go-sql-driver/mysql"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	userdomain "hyapp/services/user-service/internal/domain/user"
	"hyapp/services/user-service/internal/storage/mysql/shared"
)

const retiredRegionCodeSQLList = "'Australia and New Zealand', 'Caribbean', 'Melanesia', 'Micronesia', 'Polynesia', 'Southern Africa', 'UNSPECIFIED'"

func (r *Repository) ResolveActiveRegionByCountry(ctx context.Context, countryCode string) (userdomain.Region, bool, error) {
	country, ok, err := resolveCountryByCode(ctx, r.db, countryCode, true)
	if err != nil || !ok {
		return userdomain.Region{}, false, err
	}
	var region userdomain.Region
	err = r.db.QueryRowContext(ctx, `
		SELECT rg.app_code, rg.region_id, rg.region_code, rg.name, rg.status, rg.sort_order,
		       COALESCE(rg.created_by_user_id, 0), COALESCE(rg.updated_by_user_id, 0), rg.created_at_ms, rg.updated_at_ms
		FROM region_countries rc
		INNER JOIN regions rg ON rg.app_code = rc.app_code AND rg.region_id = rc.region_id
		WHERE rc.app_code = ? AND rc.country_code = ? AND rc.status = ? AND rg.status = ?
		  AND rg.region_code NOT IN (`+retiredRegionCodeSQLList+`)
		LIMIT 1
	`, appcode.FromContext(ctx), country.CountryCode, userdomain.RegionStatusActive, userdomain.RegionStatusActive).Scan(
		&region.AppCode,
		&region.RegionID,
		&region.RegionCode,
		&region.Name,
		&region.Status,
		&region.SortOrder,
		&region.CreatedByUserID,
		&region.UpdatedByUserID,
		&region.CreatedAtMs,
		&region.UpdatedAtMs,
	)
	if err == sql.ErrNoRows {
		return globalRegionFromContext(ctx), true, nil
	}
	if err != nil {
		return userdomain.Region{}, false, err
	}
	countries, err := activeCountriesForRegion(ctx, r.db, region.RegionID)
	if err != nil {
		return userdomain.Region{}, false, err
	}
	region.Countries = countries

	return region, true, nil
}

// UpdateRegion 修改区域展示字段，不改变国家归属。

func (r *Repository) UpdateRegion(ctx context.Context, command userdomain.UpdateRegionCommand) (userdomain.Region, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return userdomain.Region{}, err
	}
	defer tx.Rollback()

	before, err := queryRegion(ctx, tx, "WHERE app_code = ? AND region_id = ? FOR UPDATE", appcode.Normalize(command.AppCode), command.RegionID)
	if err == sql.ErrNoRows {
		return userdomain.Region{}, xerr.New(xerr.RegionNotFound, "region not found")
	}
	if err != nil {
		return userdomain.Region{}, err
	}
	if userdomain.IsRetiredRegionCode(before.RegionCode) {
		return userdomain.Region{}, xerr.New(xerr.RegionNotFound, "region not found")
	}
	if userdomain.IsGlobalRegionCode(before.RegionCode) && !userdomain.IsGlobalRegionCode(command.RegionCode) {
		return userdomain.Region{}, xerr.New(xerr.InvalidArgument, "global region code cannot be changed")
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE regions
		SET region_code = ?, name = ?, sort_order = ?, updated_by_user_id = ?, updated_at_ms = ?
		WHERE app_code = ? AND region_id = ?
	`, command.RegionCode, command.Name, command.SortOrder, shared.NullableOperator(command.OperatorUserID), command.NowMs, appcode.Normalize(command.AppCode), command.RegionID); err != nil {
		return userdomain.Region{}, mapRegionDuplicateError(err)
	}
	after := before
	after.RegionCode = command.RegionCode
	after.Name = command.Name
	after.SortOrder = command.SortOrder
	after.UpdatedByUserID = command.OperatorUserID
	after.UpdatedAtMs = command.NowMs
	if err := insertRegionChangeLog(ctx, tx, command.RegionID, "update_region", before, after, command.OperatorUserID, command.RequestID, command.NowMs); err != nil {
		return userdomain.Region{}, err
	}
	if err := tx.Commit(); err != nil {
		return userdomain.Region{}, err
	}

	return r.GetRegion(ctx, command.RegionID)
}

// ListRegions 返回区域主数据列表，countries 只包含 active 映射。

func (r *Repository) ListRegions(ctx context.Context, filter userdomain.RegionFilter) ([]userdomain.Region, error) {
	query := `
		SELECT app_code, region_id, region_code, name, status, sort_order,
		       COALESCE(created_by_user_id, 0), COALESCE(updated_by_user_id, 0), created_at_ms, updated_at_ms
		FROM regions`
	args := []any{appcode.FromContext(ctx)}
	conditions := []string{"app_code = ?"}
	if filter.Status != "" {
		conditions = append(conditions, "status = ?")
		args = append(args, filter.Status)
	}
	conditions = append(conditions, "region_code <> ?")
	args = append(args, userdomain.GlobalRegionCode)
	conditions = append(conditions, "region_code NOT IN ("+retiredRegionCodeSQLList+")")
	query += " WHERE " + strings.Join(conditions, " AND ")
	query += " ORDER BY sort_order DESC, region_code ASC"
	rows, err := r.db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	regions := []userdomain.Region{}
	for rows.Next() {
		region, err := scanRegion(rows)
		if err != nil {
			return nil, err
		}
		countries, err := activeCountriesForRegion(ctx, r.db, region.RegionID)
		if err != nil {
			return nil, err
		}
		region.Countries = countries
		regions = append(regions, region)
	}

	if err := rows.Err(); err != nil {
		return nil, err
	}

	return regions, nil
}

// GetRegion 返回单个区域及其 active 国家列表。

func (r *Repository) GetRegion(ctx context.Context, regionID int64) (userdomain.Region, error) {
	region, err := queryRegion(ctx, r.db, "WHERE app_code = ? AND region_id = ?", appcode.FromContext(ctx), regionID)
	if err == sql.ErrNoRows {
		return userdomain.Region{}, xerr.New(xerr.RegionNotFound, "region not found")
	}
	if err != nil {
		return userdomain.Region{}, err
	}
	if userdomain.IsRetiredRegionCode(region.RegionCode) {
		return userdomain.Region{}, xerr.New(xerr.RegionNotFound, "region not found")
	}

	return region, nil
}

// ReplaceRegionCountries 原子替换区域 active 国家归属，并为受影响国家创建 rebuild task。

func (r *Repository) ReplaceRegionCountries(ctx context.Context, command userdomain.ReplaceRegionCountriesCommand) (userdomain.Region, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return userdomain.Region{}, err
	}
	defer tx.Rollback()

	before, err := queryRegion(ctx, tx, "WHERE app_code = ? AND region_id = ? FOR UPDATE", appcode.Normalize(command.AppCode), command.RegionID)
	if err == sql.ErrNoRows {
		return userdomain.Region{}, xerr.New(xerr.RegionNotFound, "region not found")
	}
	if err != nil {
		return userdomain.Region{}, err
	}
	if userdomain.IsRetiredRegionCode(before.RegionCode) {
		return userdomain.Region{}, xerr.New(xerr.RegionNotFound, "region not found")
	}
	if userdomain.IsGlobalRegionCode(before.RegionCode) {
		return userdomain.Region{}, xerr.New(xerr.InvalidArgument, "global region countries are implicit")
	}
	if before.Status != userdomain.RegionStatusActive {
		return userdomain.Region{}, xerr.New(xerr.RegionDisabled, "region is disabled")
	}
	countries, err := canonicalCountryCodes(ctx, tx, command.Countries)
	if err != nil {
		return userdomain.Region{}, err
	}
	if err := releaseRetiredCountryMappings(ctx, tx, appcode.Normalize(command.AppCode), countries, command.OperatorUserID, command.NowMs); err != nil {
		return userdomain.Region{}, err
	}
	if err := ensureCountriesAssignableToRegion(ctx, tx, countries, command.RegionID); err != nil {
		return userdomain.Region{}, err
	}
	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 = ?
	`, userdomain.RegionStatusDisabled, shared.NullableOperator(command.OperatorUserID), command.NowMs, appcode.Normalize(command.AppCode), command.RegionID, userdomain.RegionStatusActive); err != nil {
		return userdomain.Region{}, 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.Normalize(command.AppCode), command.RegionID, country, userdomain.RegionStatusActive, shared.NullableOperator(command.OperatorUserID), shared.NullableOperator(command.OperatorUserID), command.NowMs, command.NowMs); err != nil {
			return userdomain.Region{}, mapRegionCountryDuplicateError(err)
		}
	}
	after := before
	after.Countries = append([]string(nil), countries...)
	after.UpdatedByUserID = command.OperatorUserID
	after.UpdatedAtMs = command.NowMs
	for _, country := range unionCountryCodes(before.Countries, countries) {
		targetRegionID := int64(0)
		if stringSliceContains(countries, country) {
			targetRegionID = command.RegionID
		}
		if err := insertRebuildTask(ctx, tx, appcode.Normalize(command.AppCode), command.RegionID, country, targetRegionID, command.NowMs); err != nil {
			return userdomain.Region{}, err
		}
	}
	if err := insertRegionChangeLog(ctx, tx, command.RegionID, "replace_region_countries", before, after, command.OperatorUserID, command.RequestID, command.NowMs); err != nil {
		return userdomain.Region{}, err
	}
	if err := tx.Commit(); err != nil {
		return userdomain.Region{}, err
	}

	return r.GetRegion(ctx, command.RegionID)
}

// ProcessNextRegionRebuildTask 先提交 task claim，再按 user_id cursor 分页刷新 users.region_id。

func queryRegion(ctx context.Context, q queryer, clause string, args ...any) (userdomain.Region, error) {
	row := q.QueryRowContext(ctx, `
		SELECT app_code, region_id, region_code, name, status, sort_order,
		       COALESCE(created_by_user_id, 0), COALESCE(updated_by_user_id, 0), created_at_ms, updated_at_ms
		FROM regions
		`+clause, args...)
	region, err := scanRegion(row)
	if err != nil {
		return userdomain.Region{}, err
	}
	countries, err := activeCountriesForRegion(ctx, q, region.RegionID)
	if err != nil {
		return userdomain.Region{}, err
	}
	region.Countries = countries

	return region, nil
}

func scanRegion(scanner interface{ Scan(dest ...any) error }) (userdomain.Region, error) {
	var region userdomain.Region
	err := scanner.Scan(
		&region.AppCode,
		&region.RegionID,
		&region.RegionCode,
		&region.Name,
		&region.Status,
		&region.SortOrder,
		&region.CreatedByUserID,
		&region.UpdatedByUserID,
		&region.CreatedAtMs,
		&region.UpdatedAtMs,
	)
	return region, err
}

func globalRegionFromContext(ctx context.Context) userdomain.Region {
	return userdomain.Region{
		AppCode:    appcode.FromContext(ctx),
		RegionID:   0,
		RegionCode: userdomain.GlobalRegionCode,
		Name:       userdomain.GlobalRegionCode,
		Status:     userdomain.RegionStatusActive,
		Countries:  []string{},
	}
}

func activeCountriesForRegion(ctx context.Context, q queryer, regionID int64) ([]string, error) {
	rows, err := queryRows(ctx, q, `
		SELECT country_code
		FROM region_countries
		WHERE app_code = ? AND region_id = ? AND status = ?
		ORDER BY country_code ASC
	`, appcode.FromContext(ctx), regionID, userdomain.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 releaseRetiredCountryMappings(ctx context.Context, tx *sql.Tx, appCode string, countries []string, operatorUserID 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+`)
		`, userdomain.RegionStatusDisabled, shared.NullableOperator(operatorUserID), nowMs, appcode.Normalize(appCode), country, userdomain.RegionStatusActive); err != nil {
			return err
		}
	}
	return nil
}

func insertRegionChangeLog(ctx context.Context, tx *sql.Tx, regionID int64, operation string, before any, after any, operatorUserID int64, requestID string, nowMs int64) error {
	beforeJSON, err := marshalNullableJSON(before)
	if err != nil {
		return err
	}
	afterJSON, err := marshalNullableJSON(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 (?, ?, ?, ?, ?, ?, ?, ?)
	`, appcode.FromContext(ctx), shared.NullableRegionID(regionID), operation, beforeJSON, afterJSON, shared.NullableOperator(operatorUserID), requestID, nowMs)
	return err
}

func marshalNullableJSON(value any) (any, error) {
	if value == nil {
		return nil, nil
	}
	payload, err := json.Marshal(value)
	if err != nil {
		return nil, err
	}

	return string(payload), nil
}

func mapRegionDuplicateError(err error) error {
	var mysqlErr *mysqldriver.MySQLError
	if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
		return xerr.New(xerr.Conflict, "region_code already exists")
	}
	return err
}

func unionCountryCodes(left []string, right []string) []string {
	seen := make(map[string]struct{}, len(left)+len(right))
	result := make([]string, 0, len(left)+len(right))
	for _, country := range append(append([]string(nil), left...), right...) {
		if _, exists := seen[country]; exists {
			continue
		}
		seen[country] = struct{}{}
		result = append(result, country)
	}
	sort.Strings(result)

	return result
}

func stringSliceContains(values []string, target string) bool {
	return slices.Contains(values, target)
}
