package region

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	userdomain "hyapp/services/user-service/internal/domain/user"
	"hyapp/services/user-service/internal/storage/mysql/shared"
)

func (r *Repository) ProcessNextRegionRebuildTask(ctx context.Context, workerID string, nowMs int64, lockTTL time.Duration, batchSize int) (userdomain.RegionRebuildProcessResult, error) {
	if lockTTL <= 0 {
		lockTTL = 30 * time.Second
	}
	if batchSize <= 0 {
		batchSize = 500
	}

	// claim 必须独立提交：running/locked_until_ms/attempt_count 要对其他 worker 和运维可见。
	task, ok, err := claimRegionRebuildTask(ctx, r.db, appcode.FromContext(ctx), workerID, nowMs, lockTTL)
	if err != nil {
		return userdomain.RegionRebuildProcessResult{}, err
	}
	if !ok {
		return userdomain.RegionRebuildProcessResult{NoTask: true}, nil
	}

	return r.processClaimedRegionRebuildTask(ctx, task.TaskID, workerID, nowMs, batchSize)
}

func (r *Repository) processClaimedRegionRebuildTask(ctx context.Context, taskID int64, workerID string, nowMs int64, batchSize int) (userdomain.RegionRebuildProcessResult, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return userdomain.RegionRebuildProcessResult{}, err
	}
	defer tx.Rollback()

	task, ok, err := lockClaimedRegionRebuildTask(ctx, tx, taskID, workerID)
	if err != nil {
		return userdomain.RegionRebuildProcessResult{}, err
	}
	if !ok {
		// task 在 claim 提交后被接管或人工改动时，本轮不应继续写 users。
		return userdomain.RegionRebuildProcessResult{TaskID: taskID}, xerr.New(xerr.Conflict, "region rebuild task lock is lost")
	}
	result := userdomain.RegionRebuildProcessResult{
		AppCode:        task.AppCode,
		TaskID:         task.TaskID,
		CountryCode:    task.CountryCode,
		TargetRegionID: task.TargetRegionID,
		Status:         userdomain.RegionRebuildTaskStatusRunning,
		CursorUserID:   task.CursorUserID,
	}

	stale, err := newerRegionRebuildTaskExists(ctx, tx, task.AppCode, task.CountryCode, task.MappingRevision, task.TaskID)
	if err != nil {
		return result, err
	}
	if stale {
		// 旧任务不能覆盖新映射；直接标记 skipped，等待较新任务刷新历史用户。
		if err := finishRegionRebuildTask(ctx, tx, task.TaskID, workerID, userdomain.RegionRebuildTaskStatusSkipped, task.CursorUserID, "stale mapping revision", nowMs); err != nil {
			return result, err
		}
		if err := tx.Commit(); err != nil {
			return result, err
		}
		result.Status = userdomain.RegionRebuildTaskStatusSkipped
		return result, nil
	}

	users, err := selectRegionRebuildUsers(ctx, tx, task.AppCode, task.CountryCode, task.CursorUserID, batchSize)
	if err != nil {
		return result, err
	}
	if len(users) == 0 {
		if err := finishRegionRebuildTask(ctx, tx, task.TaskID, workerID, userdomain.RegionRebuildTaskStatusCompleted, task.CursorUserID, "", nowMs); err != nil {
			return result, err
		}
		if err := tx.Commit(); err != nil {
			return result, err
		}
		result.Status = userdomain.RegionRebuildTaskStatusCompleted
		return result, nil
	}

	userIDs := make([]int64, 0, len(users))
	for _, user := range users {
		userIDs = append(userIDs, user.UserID)
		if user.RegionID == task.TargetRegionID {
			continue
		}
		if err := insertRegionRebuildUserRegionChangedOutbox(ctx, tx, task, user, nowMs); err != nil {
			return result, err
		}
	}
	if err := updateUsersRegionByIDs(ctx, tx, task.AppCode, userIDs, task.TargetRegionID, nowMs); err != nil {
		return result, err
	}
	newCursor := userIDs[len(userIDs)-1]
	status := userdomain.RegionRebuildTaskStatusPending
	if len(userIDs) < batchSize {
		status = userdomain.RegionRebuildTaskStatusCompleted
	}
	if err := finishRegionRebuildTask(ctx, tx, task.TaskID, workerID, status, newCursor, "", nowMs); err != nil {
		return result, err
	}
	if err := tx.Commit(); err != nil {
		return result, err
	}

	result.Status = status
	result.ProcessedUsers = len(userIDs)
	result.CursorUserID = newCursor
	return result, nil
}

type regionRebuildUser struct {
	UserID   int64
	RegionID int64
}

func insertRebuildTask(ctx context.Context, tx *sql.Tx, appCode string, regionID int64, countryCode string, targetRegionID int64, revision 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, ?, ?)
	`, appcode.Normalize(appCode), shared.NullableRegionID(regionID), countryCode, shared.NullableRegionID(targetRegionID), revision, userdomain.RegionRebuildTaskStatusPending, revision, revision)
	return err
}

func claimRegionRebuildTask(ctx context.Context, db *sql.DB, appCode string, workerID string, nowMs int64, lockTTL time.Duration) (userdomain.UserRegionRebuildTask, bool, error) {
	tx, err := db.BeginTx(ctx, nil)
	if err != nil {
		return userdomain.UserRegionRebuildTask{}, false, err
	}
	defer tx.Rollback()

	task, ok, err := selectClaimableRegionRebuildTask(ctx, tx, appCode, nowMs)
	if err != nil {
		return userdomain.UserRegionRebuildTask{}, false, err
	}
	if !ok {
		return userdomain.UserRegionRebuildTask{}, false, nil
	}

	lockedUntilMs := nowMs + lockTTL.Milliseconds()
	result, err := tx.ExecContext(ctx, `
		UPDATE user_region_rebuild_tasks
		SET status = ?, locked_by = ?, locked_until_ms = ?, attempt_count = attempt_count + 1, error_message = NULL, updated_at_ms = ?
		WHERE task_id = ?
	`, userdomain.RegionRebuildTaskStatusRunning, workerID, lockedUntilMs, nowMs, task.TaskID)
	if err != nil {
		return userdomain.UserRegionRebuildTask{}, false, err
	}
	if affected, err := result.RowsAffected(); err != nil {
		return userdomain.UserRegionRebuildTask{}, false, err
	} else if affected != 1 {
		return userdomain.UserRegionRebuildTask{}, false, xerr.New(xerr.Conflict, "region rebuild task claim conflict")
	}
	if err := tx.Commit(); err != nil {
		return userdomain.UserRegionRebuildTask{}, false, err
	}
	task.Status = userdomain.RegionRebuildTaskStatusRunning
	task.LockedBy = workerID
	task.LockedUntilMs = lockedUntilMs
	task.AttemptCount++
	task.UpdatedAtMs = nowMs

	return task, true, nil
}

func selectClaimableRegionRebuildTask(ctx context.Context, tx *sql.Tx, appCode string, nowMs int64) (userdomain.UserRegionRebuildTask, bool, error) {
	row := tx.QueryRowContext(ctx, `
		SELECT app_code, task_id, region_id, country_code, target_region_id, mapping_revision, status, cursor_user_id,
		       locked_by, locked_until_ms, attempt_count, error_message, created_at_ms, updated_at_ms
		FROM user_region_rebuild_tasks
		WHERE app_code = ?
		  AND (status = ?
		   OR (status = ? AND COALESCE(locked_until_ms, 0) <= ?))
		ORDER BY updated_at_ms ASC, task_id ASC
		LIMIT 1
		FOR UPDATE SKIP LOCKED
	`, appcode.Normalize(appCode), userdomain.RegionRebuildTaskStatusPending, userdomain.RegionRebuildTaskStatusRunning, nowMs)
	task, err := scanRegionRebuildTask(row)
	if err == sql.ErrNoRows {
		return userdomain.UserRegionRebuildTask{}, false, nil
	}
	if err != nil {
		return userdomain.UserRegionRebuildTask{}, false, err
	}

	return task, true, nil
}

func lockClaimedRegionRebuildTask(ctx context.Context, tx *sql.Tx, taskID int64, workerID string) (userdomain.UserRegionRebuildTask, bool, error) {
	row := tx.QueryRowContext(ctx, `
		SELECT app_code, task_id, region_id, country_code, target_region_id, mapping_revision, status, cursor_user_id,
		       locked_by, locked_until_ms, attempt_count, error_message, created_at_ms, updated_at_ms
		FROM user_region_rebuild_tasks
		WHERE task_id = ? AND status = ? AND locked_by = ?
		FOR UPDATE
	`, taskID, userdomain.RegionRebuildTaskStatusRunning, workerID)
	task, err := scanRegionRebuildTask(row)
	if err == sql.ErrNoRows {
		return userdomain.UserRegionRebuildTask{}, false, nil
	}
	if err != nil {
		return userdomain.UserRegionRebuildTask{}, false, err
	}

	return task, true, nil
}

func scanRegionRebuildTask(scanner interface{ Scan(dest ...any) error }) (userdomain.UserRegionRebuildTask, error) {
	var task userdomain.UserRegionRebuildTask
	var regionID sql.Null[int64]
	var targetRegionID sql.Null[int64]
	var lockedBy sql.Null[string]
	var lockedUntil sql.Null[int64]
	var errorMessage sql.Null[string]
	err := scanner.Scan(
		&task.AppCode,
		&task.TaskID,
		&regionID,
		&task.CountryCode,
		&targetRegionID,
		&task.MappingRevision,
		&task.Status,
		&task.CursorUserID,
		&lockedBy,
		&lockedUntil,
		&task.AttemptCount,
		&errorMessage,
		&task.CreatedAtMs,
		&task.UpdatedAtMs,
	)
	if err != nil {
		return userdomain.UserRegionRebuildTask{}, err
	}
	if regionID.Valid {
		task.RegionID = regionID.V
	}
	if targetRegionID.Valid {
		task.TargetRegionID = targetRegionID.V
	}
	if lockedBy.Valid {
		task.LockedBy = lockedBy.V
	}
	if lockedUntil.Valid {
		task.LockedUntilMs = lockedUntil.V
	}
	if errorMessage.Valid {
		task.ErrorMessage = errorMessage.V
	}

	return task, nil
}

func newerRegionRebuildTaskExists(ctx context.Context, q queryer, appCode string, countryCode string, revision int64, taskID int64) (bool, error) {
	var one int
	err := q.QueryRowContext(ctx, `
		SELECT 1
		FROM user_region_rebuild_tasks
		WHERE app_code = ?
		  AND country_code = ?
		  AND (mapping_revision > ? OR (mapping_revision = ? AND task_id > ?))
		LIMIT 1
	`, appcode.Normalize(appCode), countryCode, revision, revision, taskID).Scan(&one)
	if err == sql.ErrNoRows {
		return false, nil
	}
	if err != nil {
		return false, err
	}

	return true, nil
}

func selectRegionRebuildUsers(ctx context.Context, q queryer, appCode string, countryCode string, cursorUserID int64, batchSize int) ([]regionRebuildUser, error) {
	rows, err := queryRows(ctx, q, `
		SELECT user_id, region_id
		FROM users
		WHERE app_code = ? AND country = ? AND user_id > ?
		ORDER BY user_id ASC
		LIMIT ?
		FOR UPDATE
	`, appcode.Normalize(appCode), countryCode, cursorUserID, batchSize)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	users := make([]regionRebuildUser, 0, batchSize)
	for rows.Next() {
		var user regionRebuildUser
		var regionID sql.Null[int64]
		if err := rows.Scan(&user.UserID, &regionID); err != nil {
			return nil, err
		}
		if regionID.Valid {
			user.RegionID = regionID.V
		}
		users = append(users, user)
	}

	return users, rows.Err()
}

func insertRegionRebuildUserRegionChangedOutbox(ctx context.Context, tx *sql.Tx, task userdomain.UserRegionRebuildTask, user regionRebuildUser, nowMs int64) error {
	payload := map[string]any{
		"user_id":       user.UserID,
		"old_country":   task.CountryCode,
		"new_country":   task.CountryCode,
		"old_region_id": user.RegionID,
		"new_region_id": task.TargetRegionID,
		"source":        "region_rebuild",
		"request_id":    fmt.Sprintf("region_rebuild_task:%d", task.TaskID),
		"changed_at_ms": nowMs,
	}
	payloadJSON, err := json.Marshal(payload)
	if err != nil {
		return err
	}
	eventID := fmt.Sprintf("UserRegionChanged:region_rebuild:%d:%d:%d:%d", user.UserID, user.RegionID, task.TargetRegionID, nowMs)
	_, err = tx.ExecContext(ctx, `
		INSERT IGNORE INTO user_outbox (app_code, event_id, event_type, aggregate_type, aggregate_id, status, payload_json, created_at_ms, updated_at_ms)
		VALUES (?, ?, 'UserRegionChanged', 'user', ?, 'pending', ?, ?, ?)
	`, appcode.Normalize(task.AppCode), eventID, user.UserID, string(payloadJSON), nowMs, nowMs)
	return err
}

func updateUsersRegionByIDs(ctx context.Context, tx *sql.Tx, appCode string, userIDs []int64, targetRegionID int64, nowMs int64) error {
	if len(userIDs) == 0 {
		return nil
	}
	placeholders := make([]string, 0, len(userIDs))
	args := make([]any, 0, len(userIDs)+3)
	args = append(args, shared.NullableRegionID(targetRegionID), nowMs, appcode.Normalize(appCode))
	for _, userID := range userIDs {
		placeholders = append(placeholders, "?")
		args = append(args, userID)
	}
	_, err := tx.ExecContext(ctx, `
		UPDATE users
		SET region_id = ?, updated_at_ms = ?
		WHERE app_code = ? AND user_id IN (`+strings.Join(placeholders, ",")+`)
	`, args...)
	return err
}

func finishRegionRebuildTask(ctx context.Context, tx *sql.Tx, taskID int64, workerID string, status string, cursorUserID int64, errorMessage string, nowMs int64) error {
	result, err := tx.ExecContext(ctx, `
		UPDATE user_region_rebuild_tasks
		SET status = ?, cursor_user_id = ?, locked_by = NULL, locked_until_ms = NULL, error_message = ?, updated_at_ms = ?
		WHERE task_id = ? AND status = ? AND locked_by = ?
	`, status, cursorUserID, shared.NullableString(errorMessage), nowMs, taskID, userdomain.RegionRebuildTaskStatusRunning, workerID)
	if err != nil {
		return err
	}
	if affected, err := result.RowsAffected(); err != nil {
		return err
	} else if affected != 1 {
		// finish 和 users 更新在同一个事务里；丢失 claim 时让事务回滚，避免旧 worker 覆盖新进度。
		return xerr.New(xerr.Conflict, "region rebuild task lock is lost")
	}

	return nil
}
