package user

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

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

func (r *Repository) GetUser(ctx context.Context, userID int64) (userdomain.User, error) {
	// 普通查询不加锁，靓号过期恢复由 service 层判断后调用 identity 事务。
	user, err := QueryUser(ctx, r.db, "WHERE user_id = ?", userID)
	if err == sql.ErrNoRows {
		// users 表无记录时返回稳定 NotFound。
		return userdomain.User{}, xerr.New(xerr.NotFound, "user not found")
	}
	if err != nil {
		return userdomain.User{}, err
	}
	if err := r.hydrateInviteOverview(ctx, &user); err != nil {
		return userdomain.User{}, err
	}

	return user, nil
}

// GetMyProfileStats 只读取预聚合统计表，不能实时扫描访客、关注或好友明细。
func (r *Repository) GetMyProfileStats(ctx context.Context, userID int64) (userdomain.ProfileStats, error) {
	if r == nil || r.db == nil {
		return userdomain.ProfileStats{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	appCode := appcode.FromContext(ctx)
	stats := userdomain.ProfileStats{AppCode: appCode, UserID: userID}
	err := r.db.QueryRowContext(ctx, `
		SELECT visitors_count, following_count, friends_count, updated_at_ms
		FROM user_profile_stats
		WHERE app_code = ? AND user_id = ?`,
		appCode, userID,
	).Scan(&stats.VisitorsCount, &stats.FollowingCount, &stats.FriendsCount, &stats.UpdatedAtMs)
	if err == sql.ErrNoRows {
		return stats, nil
	}
	if err != nil {
		return userdomain.ProfileStats{}, err
	}
	return stats, nil
}

// BatchGetUsers 批量读取用户主状态，缺失用户不会出现在返回 map 中。

func (r *Repository) BatchGetUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.User, error) {
	// 调用方已校验 userIDs 非空且合法，这里直接构造 IN 查询。
	placeholders := make([]string, 0, len(userIDs))
	args := make([]any, 0, len(userIDs)+1)
	args = append(args, appcode.FromContext(ctx))
	for _, userID := range userIDs {
		placeholders = append(placeholders, "?")
		args = append(args, userID)
	}

	rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
		SELECT %s
		FROM users
		WHERE app_code = ? AND user_id IN (%s)
	`, userSelectColumns, strings.Join(placeholders, ",")), args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	result := make(map[int64]userdomain.User, len(userIDs))
	for rows.Next() {
		// 缺失用户不会返回，调用方按 map key 判断存在性。
		user, err := ScanUser(rows)
		if err != nil {
			return nil, err
		}
		result[user.UserID] = user
	}

	return result, rows.Err()
}

// ListUserIDs returns active user IDs in ascending order for cursor-based backend fanout.
func (r *Repository) ListUserIDs(ctx context.Context, filter userdomain.UserIDPageFilter) ([]int64, error) {
	conditions := []string{"app_code = ?", "status = ?", "user_id > ?"}
	args := []any{appcode.FromContext(ctx), string(userdomain.StatusActive), filter.CursorUserID}
	switch filter.TargetScope {
	case userdomain.UserIDTargetRegion:
		conditions = append(conditions, "region_id = ?")
		args = append(args, filter.RegionID)
	case userdomain.UserIDTargetCountry:
		conditions = append(conditions, "country = ?")
		args = append(args, filter.Country)
	}
	args = append(args, filter.PageSize)

	rows, err := r.db.QueryContext(ctx, `
		SELECT user_id
		FROM users
		WHERE `+strings.Join(conditions, " AND ")+`
		ORDER BY user_id ASC
		LIMIT ?`, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	userIDs := make([]int64, 0, filter.PageSize)
	for rows.Next() {
		var userID int64
		if err := rows.Scan(&userID); err != nil {
			return nil, err
		}
		userIDs = append(userIDs, userID)
	}
	return userIDs, rows.Err()
}

// CreateUserWithIdentity 是用户创建的事务入口。
// 它同时写 users 和默认短号 active 记录，避免出现没有默认门牌号的用户。

func (r *Repository) CreateUserWithIdentity(ctx context.Context, user userdomain.User, identity userdomain.Identity) error {
	// 用户和默认短号必须在一个事务里创建，避免出现孤立用户。
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	if err := InsertUserIdentity(ctx, tx, user, identity); err != nil {
		// 唯一键冲突转换成 display_user_id exists，供 service 有限重试。
		return shared.MapDisplayDuplicateError(err)
	}
	if _, err := invitestorage.EnsurePrimaryCodeForUser(ctx, tx, user.AppCode, user.UserID, user.CreatedAtMs); err != nil {
		return err
	}

	return tx.Commit()
}

// UpdateUserProfile 修改用户基础资料并返回更新后的 users 快照。

func (r *Repository) UpdateUserProfile(ctx context.Context, command userdomain.ProfileUpdateCommand) (userdomain.User, error) {
	// 资料更新锁定 users 行，避免和国家修改、短号懒过期覆盖 updated_at_ms。
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return userdomain.User{}, err
	}
	defer tx.Rollback()

	user, err := QueryUser(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.UserID)
	if err == sql.ErrNoRows {
		return userdomain.User{}, xerr.New(xerr.NotFound, "user not found")
	}
	if err != nil {
		return userdomain.User{}, err
	}
	if command.Username != nil {
		user.Username = *command.Username
	}
	if command.Avatar != nil {
		user.Avatar = *command.Avatar
	}
	if command.Gender != nil {
		user.Gender = *command.Gender
	}
	if command.BirthDate != nil {
		user.BirthDate = *command.BirthDate
	}
	user.UpdatedAtMs = command.UpdatedAtMs

	_, err = tx.ExecContext(ctx, `
		UPDATE users
		SET username = ?, avatar = ?, gender = ?, birth_date = ?, updated_at_ms = ?
		WHERE app_code = ? AND user_id = ?
	`, shared.NullableString(user.Username), shared.NullableString(user.Avatar), shared.NullableString(user.Gender), shared.NullableString(user.BirthDate), user.UpdatedAtMs, appcode.Normalize(command.AppCode), command.UserID)
	if err != nil {
		return userdomain.User{}, err
	}
	if err := tx.Commit(); err != nil {
		return userdomain.User{}, err
	}

	return user, nil
}

// CompleteOnboarding 是注册页资料固化的事务入口。
// 该方法不写 user_country_change_logs，确保首次国家选择不消耗改国家冷却窗口。

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

	user, err := QueryUser(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.UserID)
	if err == sql.ErrNoRows {
		return userdomain.User{}, xerr.New(xerr.NotFound, "user not found")
	}
	if err != nil {
		return userdomain.User{}, err
	}
	if user.ProfileCompleted {
		// 已完成用户不能用注册页接口重写资料；提交入口保持幂等只返回当前事实。
		if err := tx.Commit(); err != nil {
			return userdomain.User{}, err
		}
		return r.GetUser(ctx, command.UserID)
	}
	var inviteBindingErr error
	inviteSnapshot := command.InviteCode
	if command.InviteCode != "" {
		user.InviteBinding, inviteBindingErr = invitestorage.BindRelation(ctx, tx, invitestorageBindCommand(command))
		if inviteBindingErr != nil {
			return userdomain.User{}, inviteBindingErr
		}
		if user.InviteBinding.InviteCode != "" {
			inviteSnapshot = user.InviteBinding.InviteCode
		}
	}

	_, err = tx.ExecContext(ctx, `
		UPDATE users
		SET username = ?,
			avatar = ?,
			gender = ?,
			country = ?,
			region_id = ?,
			invite_code = CASE WHEN ? = '' THEN invite_code ELSE ? END,
			profile_completed = ?,
			profile_completed_at_ms = ?,
			onboarding_status = ?,
			updated_at_ms = ?
		WHERE user_id = ?
		  AND app_code = ?
	`, command.Username, command.Avatar, command.Gender, command.Country, shared.NullableRegionID(command.RegionID), inviteSnapshot, inviteSnapshot, true, command.CompletedAtMs, string(userdomain.OnboardingStatusCompleted), command.CompletedAtMs, command.UserID, appcode.Normalize(command.AppCode))
	if err != nil {
		return userdomain.User{}, err
	}
	if err := tx.Commit(); err != nil {
		return userdomain.User{}, err
	}

	updated, err := r.GetUser(ctx, command.UserID)
	updated.InviteBinding = user.InviteBinding
	return updated, err
}

// ChangeUserCountry 修改国家并写日志；冷却期检查和日志写入必须在同一事务内完成。

func (r *Repository) ChangeUserCountry(ctx context.Context, command userdomain.CountryChangeCommand) (userdomain.User, int64, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return userdomain.User{}, 0, err
	}
	defer tx.Rollback()

	user, err := QueryUser(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.UserID)
	if err == sql.ErrNoRows {
		return userdomain.User{}, 0, xerr.New(xerr.NotFound, "user not found")
	}
	if err != nil {
		return userdomain.User{}, 0, err
	}
	lastChangedAt, err := latestCountryChangedAt(ctx, tx, command.UserID)
	if err != nil {
		return userdomain.User{}, 0, err
	}
	nextAllowedAt := int64(0)
	if lastChangedAt > 0 && command.CooldownMs > 0 {
		nextAllowedAt = lastChangedAt + command.CooldownMs
	}
	if user.Country == command.NewCountry {
		// 修改为相同国家不写日志，也不消耗冷却期；映射变化时允许修正 stale region_id。
		if user.RegionID != command.NewRegionID {
			user.RegionID = command.NewRegionID
			user.UpdatedAtMs = command.ChangedAtMs
			if _, err := tx.ExecContext(ctx, `
				UPDATE users
				SET region_id = ?, updated_at_ms = ?
				WHERE app_code = ? AND user_id = ?
			`, shared.NullableRegionID(user.RegionID), user.UpdatedAtMs, appcode.Normalize(command.AppCode), command.UserID); err != nil {
				return userdomain.User{}, 0, err
			}
		}
		if err := tx.Commit(); err != nil {
			return userdomain.User{}, 0, err
		}
		updated, err := r.GetUser(ctx, command.UserID)
		return updated, nextAllowedAt, err
	}
	if nextAllowedAt > 0 && command.ChangedAtMs < nextAllowedAt {
		return userdomain.User{}, nextAllowedAt, xerr.New(xerr.CountryChangeCooldown, "country change is cooling down")
	}

	oldCountry := user.Country
	user.Country = command.NewCountry
	user.RegionID = command.NewRegionID
	user.UpdatedAtMs = command.ChangedAtMs
	_, err = tx.ExecContext(ctx, `
		UPDATE users
		SET country = ?, region_id = ?, updated_at_ms = ?
		WHERE app_code = ? AND user_id = ?
	`, shared.NullableString(user.Country), shared.NullableRegionID(user.RegionID), user.UpdatedAtMs, appcode.Normalize(command.AppCode), command.UserID)
	if err != nil {
		return userdomain.User{}, 0, err
	}
	_, err = tx.ExecContext(ctx, `
		INSERT INTO user_country_change_logs (app_code, user_id, old_country, new_country, request_id, changed_at_ms)
		VALUES (?, ?, ?, ?, ?, ?)
	`, appcode.Normalize(command.AppCode), command.UserID, shared.NullableString(oldCountry), command.NewCountry, command.RequestID, command.ChangedAtMs)
	if err != nil {
		return userdomain.User{}, 0, err
	}
	if err := tx.Commit(); err != nil {
		return userdomain.User{}, 0, err
	}

	updated, err := r.GetUser(ctx, command.UserID)
	return updated, command.ChangedAtMs + command.CooldownMs, err
}

func (r *Repository) hydrateInviteOverview(ctx context.Context, user *userdomain.User) error {
	if user == nil || user.UserID <= 0 {
		return nil
	}
	overview, err := invitestorage.New(r.db).GetOverview(ctx, user.AppCode, user.UserID, user.RegionID, timeNowMs())
	if err != nil {
		return err
	}
	user.InviteOverview = overview
	return nil
}

func invitestorageBindCommand(command userdomain.CompleteOnboardingCommand) invitedomain.BindCommand {
	return invitedomain.BindCommand{
		AppCode:         command.AppCode,
		InvitedUserID:   command.UserID,
		InvitedRegionID: command.RegionID,
		InviteCode:      command.InviteCode,
		Source:          "complete_onboarding",
		RequestID:       command.RequestID,
		BoundAtMs:       command.CompletedAtMs,
	}
}

func timeNowMs() int64 {
	return time.Now().UnixMilli()
}

// QueryUser 读取 users 快照字段；既服务普通查询，也服务短号事务中的 FOR UPDATE 读。
