package user

import (
	"context"
	"database/sql"
	"strings"

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

const (
	profileVisitorPageMax       int32 = 100
	profileVisitorPageSizeMax   int32 = 100
	profileVisitorReadWindowMax int64 = 10_000
)

// RecordProfileVisit 写访问去重记录。重复访问只刷新时间和次数，不重复增加 visitors_count。
func (r *Repository) RecordProfileVisit(ctx context.Context, visitorUserID int64, targetUserID int64, nowMs int64) (bool, userdomain.ProfileStats, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return false, userdomain.ProfileStats{}, err
	}
	defer tx.Rollback()

	appCode := appcode.FromContext(ctx)
	if err := ensureSocialUser(ctx, tx, appCode, visitorUserID); err != nil {
		return false, userdomain.ProfileStats{}, err
	}
	if err := ensureSocialUser(ctx, tx, appCode, targetUserID); err != nil {
		return false, userdomain.ProfileStats{}, err
	}
	if err := ensureProfileStatsRow(ctx, tx, appCode, targetUserID, nowMs); err != nil {
		return false, userdomain.ProfileStats{}, err
	}
	result, err := tx.ExecContext(ctx, `
		INSERT INTO user_profile_visits (
			app_code, target_user_id, visitor_user_id, visit_count,
			first_visited_at_ms, last_visited_at_ms, updated_at_ms
		) VALUES (?, ?, ?, 1, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
			visit_count = visit_count + 1,
			last_visited_at_ms = VALUES(last_visited_at_ms),
			updated_at_ms = VALUES(updated_at_ms)`,
		appCode, targetUserID, visitorUserID, nowMs, nowMs, nowMs,
	)
	if err != nil {
		return false, userdomain.ProfileStats{}, err
	}
	affected, _ := result.RowsAffected()
	inserted := affected == 1
	if inserted {
		if _, err := tx.ExecContext(ctx, `
			UPDATE user_profile_stats
			SET visitors_count = visitors_count + 1, updated_at_ms = ?
			WHERE app_code = ? AND user_id = ?`,
			nowMs, appCode, targetUserID,
		); err != nil {
			return false, userdomain.ProfileStats{}, err
		}
	}
	stats, err := queryProfileStatsTx(ctx, tx, appCode, targetUserID)
	if err != nil {
		return false, userdomain.ProfileStats{}, err
	}
	return inserted, stats, tx.Commit()
}

func (r *Repository) ListProfileVisitors(ctx context.Context, userID int64, page int32, pageSize int32) ([]userdomain.ProfileVisitRecord, int64, error) {
	// gRPC 内部调用也必须受同一有界窗口保护，不能只相信 gateway 的 HTTP 参数校验。
	// page/page_size 双上限固定公开协议；window 上限再防御未来任一上限被单独调整后重新引入无界物化。
	if page <= 0 || page > profileVisitorPageMax || pageSize <= 0 || pageSize > profileVisitorPageSizeMax {
		return nil, 0, xerr.New(xerr.InvalidArgument, "profile visitor page is out of range")
	}
	offset := socialOffset(page, pageSize)
	if offset+int64(pageSize) > profileVisitorReadWindowMax {
		return nil, 0, xerr.New(xerr.InvalidArgument, "profile visitor page window is too large")
	}
	appCode := appcode.FromContext(ctx)
	// total 与两张表的分页读取放在同一个只读快照中，避免并发访问恰好落在查询之间时出现
	// total 和 records 不属于同一时点。COUNT 都命中 (app_code,target_user_id,...) 前缀，不扫全表。
	tx, err := r.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
	if err != nil {
		return nil, 0, err
	}
	defer tx.Rollback()
	var total int64
	if err := tx.QueryRowContext(ctx, `
		SELECT
			(SELECT COUNT(*) FROM user_profile_visits WHERE app_code = ? AND target_user_id = ?) +
			(SELECT COUNT(*) FROM user_profile_anonymous_visits WHERE app_code = ? AND target_user_id = ?)`,
		appCode, userID, appCode, userID,
	).Scan(&total); err != nil {
		return nil, 0, err
	}
	if total == 0 {
		return []userdomain.ProfileVisitRecord{}, 0, tx.Commit()
	}

	if offset >= total {
		// 越过末页时直接返回快照 total，避免为了一个确定为空的页面从两个分支各读取全部目标记录。
		return []userdomain.ProfileVisitRecord{}, total, tx.Commit()
	}
	fetchLimit := offset + int64(pageSize)
	if fetchLimit > total {
		fetchLimit = total
	}
	publicRecords, err := queryPublicProfileVisitors(ctx, tx, appCode, userID, fetchLimit)
	if err != nil {
		return nil, 0, err
	}
	anonymousRecords, err := queryAnonymousProfileVisitors(ctx, tx, appCode, userID, fetchLimit)
	if err != nil {
		return nil, 0, err
	}
	records := mergeProfileVisitors(publicRecords, anonymousRecords, offset, pageSize)
	if err := tx.Commit(); err != nil {
		return nil, 0, err
	}
	return records, total, nil
}

func queryPublicProfileVisitors(ctx context.Context, tx *sql.Tx, appCode string, userID int64, limit int64) ([]userdomain.ProfileVisitRecord, error) {
	rows, err := tx.QueryContext(ctx, `
		SELECT visitor_user_id, target_user_id, visit_count, last_visited_at_ms
		FROM user_profile_visits
		WHERE app_code = ? AND target_user_id = ?
		ORDER BY last_visited_at_ms DESC, visitor_user_id DESC
		LIMIT ?`,
		appCode, userID, limit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	records := make([]userdomain.ProfileVisitRecord, 0)
	for rows.Next() {
		var record userdomain.ProfileVisitRecord
		if err := rows.Scan(&record.VisitorUserID, &record.TargetUserID, &record.VisitCount, &record.LastVisitedAtMs); err != nil {
			return nil, err
		}
		records = append(records, record)
	}
	return records, rows.Err()
}

func queryAnonymousProfileVisitors(ctx context.Context, tx *sql.Tx, appCode string, userID int64, limit int64) ([]userdomain.ProfileVisitRecord, error) {
	rows, err := tx.QueryContext(ctx, `
		SELECT anonymous_visit_id, target_user_id, visit_count, last_visited_at_ms
		FROM user_profile_anonymous_visits
		WHERE app_code = ? AND target_user_id = ?
		ORDER BY last_visited_at_ms DESC, anonymous_visit_id DESC
		LIMIT ?`,
		appCode, userID, limit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	records := make([]userdomain.ProfileVisitRecord, 0)
	for rows.Next() {
		record := userdomain.ProfileVisitRecord{VisitorIdentityHidden: true}
		if err := rows.Scan(&record.AnonymousVisitID, &record.TargetUserID, &record.VisitCount, &record.LastVisitedAtMs); err != nil {
			return nil, err
		}
		records = append(records, record)
	}
	return records, rows.Err()
}

// mergeProfileVisitors 合并两个已排序分支，并在内存中应用全局 offset。
// 每个 SQL 分支最多读取 offset+page_size 行，避免 UNION ALL 为单个高访问目标物化其全部历史记录。
func mergeProfileVisitors(publicRecords []userdomain.ProfileVisitRecord, anonymousRecords []userdomain.ProfileVisitRecord, offset int64, pageSize int32) []userdomain.ProfileVisitRecord {
	records := make([]userdomain.ProfileVisitRecord, 0, pageSize)
	publicIndex, anonymousIndex := 0, 0
	var skipped int64
	for (publicIndex < len(publicRecords) || anonymousIndex < len(anonymousRecords)) && len(records) < int(pageSize) {
		var next userdomain.ProfileVisitRecord
		switch {
		case anonymousIndex >= len(anonymousRecords):
			next = publicRecords[publicIndex]
			publicIndex++
		case publicIndex >= len(publicRecords):
			next = anonymousRecords[anonymousIndex]
			anonymousIndex++
		case profileVisitBefore(publicRecords[publicIndex], anonymousRecords[anonymousIndex]):
			next = publicRecords[publicIndex]
			publicIndex++
		default:
			next = anonymousRecords[anonymousIndex]
			anonymousIndex++
		}
		if skipped < offset {
			skipped++
			continue
		}
		records = append(records, next)
	}
	return records
}

func profileVisitBefore(left userdomain.ProfileVisitRecord, right userdomain.ProfileVisitRecord) bool {
	if left.LastVisitedAtMs != right.LastVisitedAtMs {
		return left.LastVisitedAtMs > right.LastVisitedAtMs
	}
	if left.VisitorIdentityHidden != right.VisitorIdentityHidden {
		// 同毫秒内公开记录固定排在匿名记录前，避免不同查询计划造成跨页漂移。
		return !left.VisitorIdentityHidden
	}
	if left.VisitorIdentityHidden {
		return left.AnonymousVisitID > right.AnonymousVisitID
	}
	return left.VisitorUserID > right.VisitorUserID
}

func (r *Repository) FollowUser(ctx context.Context, followerUserID int64, followeeUserID int64, nowMs int64) (userdomain.ProfileStats, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return userdomain.ProfileStats{}, err
	}
	defer tx.Rollback()

	appCode := appcode.FromContext(ctx)
	if err := ensureSocialUser(ctx, tx, appCode, followerUserID); err != nil {
		return userdomain.ProfileStats{}, err
	}
	if err := ensureSocialUser(ctx, tx, appCode, followeeUserID); err != nil {
		return userdomain.ProfileStats{}, err
	}
	if err := ensureProfileStatsRows(ctx, tx, appCode, followerUserID, followeeUserID, nowMs); err != nil {
		return userdomain.ProfileStats{}, err
	}
	status, exists, err := lockFollowStatus(ctx, tx, appCode, followerUserID, followeeUserID)
	if err != nil {
		return userdomain.ProfileStats{}, err
	}
	shouldIncrement := !exists || status != userdomain.SocialStatusActive
	if exists {
		if _, err := tx.ExecContext(ctx, `
			UPDATE user_follows
			SET status = ?, updated_at_ms = ?, followed_at_ms = CASE WHEN status <> ? THEN ? ELSE followed_at_ms END
			WHERE app_code = ? AND follower_user_id = ? AND followee_user_id = ?`,
			userdomain.SocialStatusActive, nowMs, userdomain.SocialStatusActive, nowMs, appCode, followerUserID, followeeUserID,
		); err != nil {
			return userdomain.ProfileStats{}, err
		}
	} else {
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO user_follows (
				app_code, follower_user_id, followee_user_id, status, followed_at_ms, updated_at_ms
			) VALUES (?, ?, ?, ?, ?, ?)`,
			appCode, followerUserID, followeeUserID, userdomain.SocialStatusActive, nowMs, nowMs,
		); err != nil {
			return userdomain.ProfileStats{}, err
		}
	}
	if shouldIncrement {
		// 新增粉丝按事件净增统计；状态未变化的重复关注不写事件，避免客户端重试重复 +1。
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO user_follow_events (
			  app_code, follower_user_id, followee_user_id, delta, occurred_at_ms
			) VALUES (?, ?, ?, 1, ?)`,
			appCode, followerUserID, followeeUserID, nowMs,
		); err != nil {
			return userdomain.ProfileStats{}, err
		}
		if _, err := tx.ExecContext(ctx, `
			UPDATE user_profile_stats
			SET following_count = following_count + 1, updated_at_ms = ?
			WHERE app_code = ? AND user_id = ?`,
			nowMs, appCode, followerUserID,
		); err != nil {
			return userdomain.ProfileStats{}, err
		}
		// 关注关系和双边计数必须在同一事务提交；资料页只读 followee 的预聚合值，
		// 不在每次打开资料卡时反向扫描 user_follows。
		if _, err := tx.ExecContext(ctx, `
			UPDATE user_profile_stats
			SET followers_count = followers_count + 1, updated_at_ms = ?
			WHERE app_code = ? AND user_id = ?`,
			nowMs, appCode, followeeUserID,
		); err != nil {
			return userdomain.ProfileStats{}, err
		}
	}
	stats, err := queryProfileStatsTx(ctx, tx, appCode, followerUserID)
	if err != nil {
		return userdomain.ProfileStats{}, err
	}
	return stats, tx.Commit()
}

func (r *Repository) UnfollowUser(ctx context.Context, followerUserID int64, followeeUserID int64, nowMs int64) (userdomain.ProfileStats, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return userdomain.ProfileStats{}, err
	}
	defer tx.Rollback()

	appCode := appcode.FromContext(ctx)
	if err := ensureProfileStatsRows(ctx, tx, appCode, followerUserID, followeeUserID, nowMs); err != nil {
		return userdomain.ProfileStats{}, err
	}
	status, exists, err := lockFollowStatus(ctx, tx, appCode, followerUserID, followeeUserID)
	if err != nil {
		return userdomain.ProfileStats{}, err
	}
	if exists && status == userdomain.SocialStatusActive {
		if _, err := tx.ExecContext(ctx, `
			UPDATE user_follows
			SET status = ?, updated_at_ms = ?
			WHERE app_code = ? AND follower_user_id = ? AND followee_user_id = ?`,
			userdomain.SocialStatusCancelled, nowMs, appCode, followerUserID, followeeUserID,
		); err != nil {
			return userdomain.ProfileStats{}, err
		}
		// 取消关注写 -1，确保筛选区间内的新增粉丝展示为净增而不是只累计关注动作。
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO user_follow_events (
			  app_code, follower_user_id, followee_user_id, delta, occurred_at_ms
			) VALUES (?, ?, ?, -1, ?)`,
			appCode, followerUserID, followeeUserID, nowMs,
		); err != nil {
			return userdomain.ProfileStats{}, err
		}
		if _, err := tx.ExecContext(ctx, `
			UPDATE user_profile_stats
			SET following_count = GREATEST(following_count - 1, 0), updated_at_ms = ?
			WHERE app_code = ? AND user_id = ?`,
			nowMs, appCode, followerUserID,
		); err != nil {
			return userdomain.ProfileStats{}, err
		}
		// 幂等取消只在 active -> cancelled 时递减；GREATEST 防止历史脏计数被扣成负数。
		if _, err := tx.ExecContext(ctx, `
			UPDATE user_profile_stats
			SET followers_count = GREATEST(followers_count - 1, 0), updated_at_ms = ?
			WHERE app_code = ? AND user_id = ?`,
			nowMs, appCode, followeeUserID,
		); err != nil {
			return userdomain.ProfileStats{}, err
		}
	}
	stats, err := queryProfileStatsTx(ctx, tx, appCode, followerUserID)
	if err != nil {
		return userdomain.ProfileStats{}, err
	}
	return stats, tx.Commit()
}

func (r *Repository) ListFollowing(ctx context.Context, userID int64, page int32, pageSize int32, cursorUpdatedAtMS int64, cursorUserID int64) ([]userdomain.FollowRecord, int64, error) {
	appCode := appcode.FromContext(ctx)
	total, err := countRows(ctx, r.db, `
		SELECT COUNT(*) FROM user_follows
		WHERE app_code = ? AND follower_user_id = ? AND status = ?`,
		appCode, userID, userdomain.SocialStatusActive,
	)
	if err != nil {
		return nil, 0, err
	}
	where := []string{"app_code = ?", "follower_user_id = ?", "status = ?"}
	args := []any{appCode, userID, userdomain.SocialStatusActive}
	if cursorUpdatedAtMS > 0 && cursorUserID > 0 {
		// 关系流游标沿用列表排序：时间倒序，user_id 倒序；避免 OFFSET 在关系变更时漂移。
		where = append(where, "(followed_at_ms < ? OR (followed_at_ms = ? AND followee_user_id < ?))")
		args = append(args, cursorUpdatedAtMS, cursorUpdatedAtMS, cursorUserID)
	}
	args = append(args, pageSize, socialOffset(page, pageSize))
	rows, err := r.db.QueryContext(ctx, `
		SELECT follower_user_id, followee_user_id, followed_at_ms
		FROM user_follows
		WHERE `+strings.Join(where, " AND ")+`
		ORDER BY followed_at_ms DESC, followee_user_id DESC
		LIMIT ? OFFSET ?`,
		args...,
	)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()

	records := make([]userdomain.FollowRecord, 0, pageSize)
	for rows.Next() {
		var record userdomain.FollowRecord
		if err := rows.Scan(&record.FollowerUserID, &record.FolloweeUserID, &record.FollowedAtMs); err != nil {
			return nil, 0, err
		}
		records = append(records, record)
	}
	return records, total, rows.Err()
}

func (r *Repository) ApplyFriend(ctx context.Context, requesterUserID int64, targetUserID int64, nowMs int64) (userdomain.FriendApplication, bool, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return userdomain.FriendApplication{}, false, err
	}
	defer tx.Rollback()

	appCode := appcode.FromContext(ctx)
	if err := ensureSocialUser(ctx, tx, appCode, requesterUserID); err != nil {
		return userdomain.FriendApplication{}, false, err
	}
	if err := ensureSocialUser(ctx, tx, appCode, targetUserID); err != nil {
		return userdomain.FriendApplication{}, false, err
	}
	if active, err := activeFriendship(ctx, tx, appCode, requesterUserID, targetUserID); err != nil {
		return userdomain.FriendApplication{}, false, err
	} else if active {
		return userdomain.FriendApplication{RequesterUserID: requesterUserID, TargetUserID: targetUserID, Status: userdomain.FriendApplicationStatusAccepted, UpdatedAtMs: nowMs}, true, tx.Commit()
	}
	application := userdomain.FriendApplication{RequesterUserID: requesterUserID, TargetUserID: targetUserID, Status: userdomain.FriendApplicationStatusPending, CreatedAtMs: nowMs, UpdatedAtMs: nowMs}
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO user_friend_applications (
			app_code, requester_user_id, target_user_id, status, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
			status = IF(status = ?, status, VALUES(status)),
			updated_at_ms = VALUES(updated_at_ms)`,
		appCode, requesterUserID, targetUserID, application.Status, nowMs, nowMs, userdomain.FriendApplicationStatusPending,
	); err != nil {
		return userdomain.FriendApplication{}, false, err
	}
	return application, false, tx.Commit()
}

func (r *Repository) AcceptFriendApplication(ctx context.Context, accepterUserID int64, requesterUserID int64, nowMs int64) (userdomain.FriendRecord, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return userdomain.FriendRecord{}, err
	}
	defer tx.Rollback()

	friend, err := acceptFriendApplicationTx(ctx, tx, appcode.FromContext(ctx), accepterUserID, requesterUserID, nowMs)
	if err != nil {
		return userdomain.FriendRecord{}, err
	}
	return friend, tx.Commit()
}

func (r *Repository) DeleteFriend(ctx context.Context, userID int64, friendUserID int64, nowMs int64) (bool, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return false, err
	}
	defer tx.Rollback()

	appCode := appcode.FromContext(ctx)
	active, err := activeFriendship(ctx, tx, appCode, userID, friendUserID)
	if err != nil {
		return false, err
	}
	if !active {
		return false, tx.Commit()
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE user_friendships
		SET status = ?, updated_at_ms = ?
		WHERE app_code = ? AND ((user_id = ? AND friend_user_id = ?) OR (user_id = ? AND friend_user_id = ?))`,
		userdomain.SocialStatusDeleted, nowMs, appCode, userID, friendUserID, friendUserID, userID,
	); err != nil {
		return false, err
	}
	if err := adjustFriendCount(ctx, tx, appCode, userID, -1, nowMs); err != nil {
		return false, err
	}
	if err := adjustFriendCount(ctx, tx, appCode, friendUserID, -1, nowMs); err != nil {
		return false, err
	}
	return true, tx.Commit()
}

func (r *Repository) ListFriends(ctx context.Context, userID int64, page int32, pageSize int32, cursorUpdatedAtMS int64, cursorUserID int64) ([]userdomain.FriendRecord, int64, error) {
	appCode := appcode.FromContext(ctx)
	total, err := countRows(ctx, r.db, `
		SELECT COUNT(*) FROM user_friendships
		WHERE app_code = ? AND user_id = ? AND status = ?`,
		appCode, userID, userdomain.SocialStatusActive,
	)
	if err != nil {
		return nil, 0, err
	}
	where := []string{"app_code = ?", "user_id = ?", "status = ?"}
	args := []any{appCode, userID, userdomain.SocialStatusActive}
	if cursorUpdatedAtMS > 0 && cursorUserID > 0 {
		// 好友表同样按建立时间倒序、friend_user_id 倒序续扫，避免 Mine 房间流翻页漏项。
		where = append(where, "(friended_at_ms < ? OR (friended_at_ms = ? AND friend_user_id < ?))")
		args = append(args, cursorUpdatedAtMS, cursorUpdatedAtMS, cursorUserID)
	}
	args = append(args, pageSize, socialOffset(page, pageSize))
	rows, err := r.db.QueryContext(ctx, `
		SELECT user_id, friend_user_id, friended_at_ms
		FROM user_friendships
		WHERE `+strings.Join(where, " AND ")+`
		ORDER BY friended_at_ms DESC, friend_user_id DESC
		LIMIT ? OFFSET ?`,
		args...,
	)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()
	records := make([]userdomain.FriendRecord, 0, pageSize)
	for rows.Next() {
		var record userdomain.FriendRecord
		if err := rows.Scan(&record.UserID, &record.FriendUserID, &record.FriendedAtMs); err != nil {
			return nil, 0, err
		}
		records = append(records, record)
	}
	return records, total, rows.Err()
}

func (r *Repository) ListFriendApplications(ctx context.Context, userID int64, direction string, page int32, pageSize int32) ([]userdomain.FriendApplication, int64, error) {
	appCode := appcode.FromContext(ctx)
	column := "target_user_id"
	if direction == "outgoing" {
		column = "requester_user_id"
	}
	total, err := countRows(ctx, r.db, `
		SELECT COUNT(*) FROM user_friend_applications
		WHERE app_code = ? AND `+column+` = ? AND status = ?`,
		appCode, userID, userdomain.FriendApplicationStatusPending,
	)
	if err != nil {
		return nil, 0, err
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT requester_user_id, target_user_id, status, created_at_ms, updated_at_ms
		FROM user_friend_applications
		WHERE app_code = ? AND `+column+` = ? AND status = ?
		ORDER BY updated_at_ms DESC
		LIMIT ? OFFSET ?`,
		appCode, userID, userdomain.FriendApplicationStatusPending, pageSize, socialOffset(page, pageSize),
	)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()
	applications := make([]userdomain.FriendApplication, 0, pageSize)
	for rows.Next() {
		var application userdomain.FriendApplication
		if err := rows.Scan(&application.RequesterUserID, &application.TargetUserID, &application.Status, &application.CreatedAtMs, &application.UpdatedAtMs); err != nil {
			return nil, 0, err
		}
		applications = append(applications, application)
	}
	return applications, total, rows.Err()
}

func acceptFriendApplicationTx(ctx context.Context, tx *sql.Tx, appCode string, accepterUserID int64, requesterUserID int64, nowMs int64) (userdomain.FriendRecord, error) {
	pending, err := pendingFriendApplication(ctx, tx, appCode, requesterUserID, accepterUserID)
	if err != nil {
		return userdomain.FriendRecord{}, err
	}
	if !pending {
		if record, active, err := activeFriendshipRecord(ctx, tx, appCode, accepterUserID, requesterUserID); err != nil {
			return userdomain.FriendRecord{}, err
		} else if active {
			return record, nil
		}
		return userdomain.FriendRecord{}, xerr.New(xerr.NotFound, "friend application not found")
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE user_friend_applications
		SET status = ?, updated_at_ms = ?
		WHERE app_code = ? AND requester_user_id = ? AND target_user_id = ?`,
		userdomain.FriendApplicationStatusAccepted, nowMs, appCode, requesterUserID, accepterUserID,
	); err != nil {
		return userdomain.FriendRecord{}, err
	}
	insertedA, err := upsertFriendship(ctx, tx, appCode, accepterUserID, requesterUserID, nowMs)
	if err != nil {
		return userdomain.FriendRecord{}, err
	}
	insertedB, err := upsertFriendship(ctx, tx, appCode, requesterUserID, accepterUserID, nowMs)
	if err != nil {
		return userdomain.FriendRecord{}, err
	}
	if insertedA {
		if err := adjustFriendCount(ctx, tx, appCode, accepterUserID, 1, nowMs); err != nil {
			return userdomain.FriendRecord{}, err
		}
	}
	if insertedB {
		if err := adjustFriendCount(ctx, tx, appCode, requesterUserID, 1, nowMs); err != nil {
			return userdomain.FriendRecord{}, err
		}
	}
	return userdomain.FriendRecord{UserID: accepterUserID, FriendUserID: requesterUserID, FriendedAtMs: nowMs}, nil
}

func upsertFriendship(ctx context.Context, tx *sql.Tx, appCode string, userID int64, friendUserID int64, nowMs int64) (bool, error) {
	var status string
	err := tx.QueryRowContext(ctx, `
		SELECT status FROM user_friendships
		WHERE app_code = ? AND user_id = ? AND friend_user_id = ?
		FOR UPDATE`,
		appCode, userID, friendUserID,
	).Scan(&status)
	if err == sql.ErrNoRows {
		_, err = tx.ExecContext(ctx, `
			INSERT INTO user_friendships (
				app_code, user_id, friend_user_id, status, friended_at_ms, updated_at_ms
			) VALUES (?, ?, ?, ?, ?, ?)`,
			appCode, userID, friendUserID, userdomain.SocialStatusActive, nowMs, nowMs,
		)
		return true, err
	}
	if err != nil {
		return false, err
	}
	if status == userdomain.SocialStatusActive {
		return false, nil
	}
	_, err = tx.ExecContext(ctx, `
		UPDATE user_friendships
		SET status = ?, friended_at_ms = ?, updated_at_ms = ?
		WHERE app_code = ? AND user_id = ? AND friend_user_id = ?`,
		userdomain.SocialStatusActive, nowMs, nowMs, appCode, userID, friendUserID,
	)
	return true, err
}

func ensureSocialUser(ctx context.Context, tx *sql.Tx, appCode string, userID int64) error {
	var exists int
	if err := tx.QueryRowContext(ctx, `
		SELECT 1 FROM users WHERE app_code = ? AND user_id = ?`,
		appCode, userID,
	).Scan(&exists); err != nil {
		if err == sql.ErrNoRows {
			return xerr.New(xerr.NotFound, "user not found")
		}
		return err
	}
	return nil
}

func ensureProfileStatsRow(ctx context.Context, tx *sql.Tx, appCode string, userID int64, nowMs int64) error {
	_, err := tx.ExecContext(ctx, `
		INSERT IGNORE INTO user_profile_stats (
			app_code, user_id, visitors_count, following_count, followers_count, friends_count, updated_at_ms
		) VALUES (?, ?, 0, 0, 0, 0, ?)`,
		appCode, userID, nowMs,
	)
	return err
}

func ensureProfileStatsRows(ctx context.Context, tx *sql.Tx, appCode string, leftUserID int64, rightUserID int64, nowMs int64) error {
	// 双边关注计数总是按 user_id 升序触碰投影行，避免 A->B 与 B->A 并发关注时
	// 以相反顺序获取 INSERT/UPDATE 行锁形成可避免的死锁。INSERT IGNORE 只保证行存在，
	// 后续 FOR UPDATE 才把两行以固定顺序持有到事务结束。
	if leftUserID > rightUserID {
		leftUserID, rightUserID = rightUserID, leftUserID
	}
	if err := ensureProfileStatsRow(ctx, tx, appCode, leftUserID, nowMs); err != nil {
		return err
	}
	if leftUserID == rightUserID {
		var lockedUserID int64
		return tx.QueryRowContext(ctx, `
			SELECT user_id FROM user_profile_stats
			WHERE app_code = ? AND user_id = ?
			FOR UPDATE`,
			appCode, leftUserID,
		).Scan(&lockedUserID)
	}
	if err := ensureProfileStatsRow(ctx, tx, appCode, rightUserID, nowMs); err != nil {
		return err
	}
	rows, err := tx.QueryContext(ctx, `
		SELECT user_id FROM user_profile_stats
		WHERE app_code = ? AND user_id IN (?, ?)
		ORDER BY user_id
		FOR UPDATE`,
		appCode, leftUserID, rightUserID,
	)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var lockedUserID int64
		if err := rows.Scan(&lockedUserID); err != nil {
			return err
		}
	}
	return rows.Err()
}

func queryProfileStatsTx(ctx context.Context, tx *sql.Tx, appCode string, userID int64) (userdomain.ProfileStats, error) {
	var stats userdomain.ProfileStats
	stats.AppCode = appCode
	stats.UserID = userID
	err := tx.QueryRowContext(ctx, `
		SELECT visitors_count, following_count, followers_count, friends_count, updated_at_ms
		FROM user_profile_stats
		WHERE app_code = ? AND user_id = ?`,
		appCode, userID,
	).Scan(&stats.VisitorsCount, &stats.FollowingCount, &stats.FollowersCount, &stats.FriendsCount, &stats.UpdatedAtMs)
	return stats, err
}

func lockFollowStatus(ctx context.Context, tx *sql.Tx, appCode string, followerUserID int64, followeeUserID int64) (string, bool, error) {
	var status string
	err := tx.QueryRowContext(ctx, `
		SELECT status FROM user_follows
		WHERE app_code = ? AND follower_user_id = ? AND followee_user_id = ?
		FOR UPDATE`,
		appCode, followerUserID, followeeUserID,
	).Scan(&status)
	if err == sql.ErrNoRows {
		return "", false, nil
	}
	return status, err == nil, err
}

func pendingFriendApplication(ctx context.Context, tx *sql.Tx, appCode string, requesterUserID int64, targetUserID int64) (bool, error) {
	var exists int
	err := tx.QueryRowContext(ctx, `
		SELECT 1 FROM user_friend_applications
		WHERE app_code = ? AND requester_user_id = ? AND target_user_id = ? AND status = ?
		FOR UPDATE`,
		appCode, requesterUserID, targetUserID, userdomain.FriendApplicationStatusPending,
	).Scan(&exists)
	if err == sql.ErrNoRows {
		return false, nil
	}
	return err == nil, err
}

func activeFriendship(ctx context.Context, tx *sql.Tx, appCode string, userID int64, friendUserID int64) (bool, error) {
	var exists int
	err := tx.QueryRowContext(ctx, `
		SELECT 1 FROM user_friendships
		WHERE app_code = ? AND user_id = ? AND friend_user_id = ? AND status = ?
		FOR UPDATE`,
		appCode, userID, friendUserID, userdomain.SocialStatusActive,
	).Scan(&exists)
	if err == sql.ErrNoRows {
		return false, nil
	}
	return err == nil, err
}

func activeFriendshipRecord(ctx context.Context, tx *sql.Tx, appCode string, userID int64, friendUserID int64) (userdomain.FriendRecord, bool, error) {
	var record userdomain.FriendRecord
	err := tx.QueryRowContext(ctx, `
		SELECT user_id, friend_user_id, friended_at_ms
		FROM user_friendships
		WHERE app_code = ? AND user_id = ? AND friend_user_id = ? AND status = ?
		FOR UPDATE`,
		appCode, userID, friendUserID, userdomain.SocialStatusActive,
	).Scan(&record.UserID, &record.FriendUserID, &record.FriendedAtMs)
	if err == sql.ErrNoRows {
		return userdomain.FriendRecord{}, false, nil
	}
	return record, err == nil, err
}

func adjustFriendCount(ctx context.Context, tx *sql.Tx, appCode string, userID int64, delta int64, nowMs int64) error {
	if err := ensureProfileStatsRow(ctx, tx, appCode, userID, nowMs); err != nil {
		return err
	}
	expr := "friends_count + ?"
	if delta < 0 {
		expr = "GREATEST(friends_count + ?, 0)"
	}
	_, err := tx.ExecContext(ctx, `
		UPDATE user_profile_stats
		SET friends_count = `+expr+`, updated_at_ms = ?
		WHERE app_code = ? AND user_id = ?`,
		delta, nowMs, appCode, userID,
	)
	return err
}

func countRows(ctx context.Context, db *sql.DB, query string, args ...any) (int64, error) {
	var total int64
	err := db.QueryRowContext(ctx, query, args...).Scan(&total)
	return total, err
}

func socialOffset(page int32, pageSize int32) int64 {
	// 转为 int64 后再相乘，避免合法 int32 page 在高页码时先于 SQL LIMIT/OFFSET 发生整数溢出。
	return (int64(page) - 1) * int64(pageSize)
}
