package user

import (
	"bytes"
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"image/png"
	"math/rand"
	"regexp"
	"strconv"
	"time"

	"bindbox-game/configs"
	"bindbox-game/internal/pkg/sms"
	"bindbox-game/internal/repository/mysql/dao"
	"bindbox-game/internal/repository/mysql/model"

	randomname "github.com/DanPlayer/randomname"
	identicon "github.com/issue9/identicon/v2"
	"github.com/redis/go-redis/v9"
	"go.uber.org/zap"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

// Redis key patterns
const (
	smsCodeKey    = "sms:code:%s"    // 验证码存储
	smsLimitKey   = "sms:limit:%s"   // 发送频率限制（60秒）
	smsDailyKey   = "sms:daily:%s"   // 每日发送计数
	smsAttemptKey = "sms:attempt:%s" // 验证尝试次数
)

// 配置常量
const (
	codeLength    = 6        // 验证码长度
	codeExpireSec = 300      // 验证码有效期（秒）
	sendLimitSec  = 60       // 发送间隔限制（秒）
	dailyLimit    = 10       // 每日发送上限
	attemptLimit  = 5        // 验证尝试上限
	testCode      = "202511" // 测试验证码，任何手机号都可以使用
)

// SmsLoginInput 短信登录输入参数
type SmsLoginInput struct {
	Mobile     string
	Code       string
	InviteCode string
}

// SmsLoginOutput 短信登录输出结果
type SmsLoginOutput struct {
	User      *model.Users
	Token     string
	IsNewUser bool
	InviterID int64
}

// 手机号正则
var mobileRegex = regexp.MustCompile(`^1[3-9]\d{9}$`)

// SendSmsCode 发送短信验证码
func (s *service) SendSmsCode(ctx context.Context, mobile string) error {
	// 1. 校验手机号格式
	if !mobileRegex.MatchString(mobile) {
		return errors.New("手机号格式不正确")
	}

	rdb := getRedisClient()

	// 2. 检查发送频率限制（60秒内不可重复发送）
	limitKey := fmt.Sprintf(smsLimitKey, mobile)
	ttl, err := rdb.TTL(ctx, limitKey).Result()
	if err == nil && ttl > 0 {
		return fmt.Errorf("请等待 %d 秒后再次发送", int(ttl.Seconds()))
	}

	// 3. 检查每日发送上限
	dailyKey := fmt.Sprintf(smsDailyKey, mobile)
	count, _ := rdb.Get(ctx, dailyKey).Int()
	if count >= dailyLimit {
		return errors.New("今日发送次数已达上限，请明日再试")
	}

	// 4. 生成6位验证码
	code := generateCode(codeLength)

	// 5. 发送短信
	cfg := configs.Get().AliyunSMS
	smsClient, err := sms.NewClient(sms.Config{
		AccessKeyID:     cfg.AccessKeyID,
		AccessKeySecret: cfg.AccessKeySecret,
		SignName:        cfg.SignName,
		TemplateCode:    cfg.TemplateCode,
	})
	if err != nil {
		s.logger.Error("创建短信客户端失败", zap.Error(err))
		return errors.New("短信服务暂不可用，请稍后重试")
	}

	if err := smsClient.SendCode(mobile, code); err != nil {
		s.logger.Error("发送短信失败", zap.String("mobile", mobile), zap.Error(err))
		return errors.New("短信发送失败，请稍后重试")
	}

	// 6. 存储验证码到Redis（5分钟有效期）
	codeKey := fmt.Sprintf(smsCodeKey, mobile)
	if err := rdb.Set(ctx, codeKey, code, time.Duration(codeExpireSec)*time.Second).Err(); err != nil {
		s.logger.Error("存储验证码失败", zap.Error(err))
		return errors.New("系统错误，请稍后重试")
	}

	// 7. 设置发送频率限制（60秒）
	rdb.Set(ctx, limitKey, "1", time.Duration(sendLimitSec)*time.Second)

	// 8. 增加每日发送计数
	rdb.Incr(ctx, dailyKey)
	// 设置每日计数的过期时间（到当天23:59:59）
	endOfDay := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 23, 59, 59, 0, time.Local)
	rdb.ExpireAt(ctx, dailyKey, endOfDay)

	// 9. 清除之前的验证尝试计数
	attemptKey := fmt.Sprintf(smsAttemptKey, mobile)
	rdb.Del(ctx, attemptKey)

	s.logger.Info("短信验证码发送成功", zap.String("mobile", mobile))
	return nil
}

// LoginByCode 通过验证码登录
func (s *service) LoginByCode(ctx context.Context, in SmsLoginInput) (*SmsLoginOutput, error) {
	// 1. 校验手机号格式
	if !mobileRegex.MatchString(in.Mobile) {
		return nil, errors.New("手机号格式不正确")
	}

	if in.Code == "" {
		return nil, errors.New("验证码不能为空")
	}

	rdb := getRedisClient()

	// 2. 检查验证尝试次数
	attemptKey := fmt.Sprintf(smsAttemptKey, in.Mobile)
	attempts, _ := rdb.Get(ctx, attemptKey).Int()
	if attempts >= attemptLimit {
		return nil, errors.New("验证次数过多，请重新获取验证码")
	}

	// 3. 获取存储的验证码（测试验证码跳过此检查）
	codeKey := fmt.Sprintf(smsCodeKey, in.Mobile)
	storedCode, err := rdb.Get(ctx, codeKey).Result()

	// 使用测试验证码时跳过Redis检查
	if in.Code == testCode {
		s.logger.Info("使用测试验证码登录", zap.String("mobile", in.Mobile))
		storedCode = testCode // 设置为测试验证码以通过后续比对
		err = nil
	}

	if err == redis.Nil {
		return nil, errors.New("验证码已过期，请重新获取")
	}
	if err != nil {
		s.logger.Error("获取验证码失败", zap.Error(err))
		return nil, errors.New("系统错误，请稍后重试")
	}

	// 4. 验证码比对（支持测试验证码）
	if storedCode != in.Code && in.Code != testCode {
		// 增加验证尝试次数
		rdb.Incr(ctx, attemptKey)
		rdb.Expire(ctx, attemptKey, time.Duration(codeExpireSec)*time.Second)
		remaining := attemptLimit - attempts - 1
		if remaining > 0 {
			return nil, fmt.Errorf("验证码错误，还剩 %d 次尝试机会", remaining)
		}
		return nil, errors.New("验证码错误")
	}

	// 5. 验证成功，删除验证码（防止重放攻击）
	rdb.Del(ctx, codeKey)
	rdb.Del(ctx, attemptKey)

	// 6. 查找或创建用户
	var user *model.Users
	isNewUser := false

	err = s.writeDB.Transaction(func(tx *dao.Query) error {
		var txErr error

		// 通过手机号查找用户
		user, txErr = tx.Users.WithContext(ctx).
			Clauses(clause.Locking{Strength: "UPDATE"}).
			Where(tx.Users.Mobile.Eq(in.Mobile)).
			First()

		if txErr != nil && !errors.Is(txErr, gorm.ErrRecordNotFound) {
			return txErr
		}

		if user == nil {
			// 用户不存在，创建新用户
			isNewUser = true
			inviteCode := s.generateInviteCode(ctx)
			nickname := randomname.GenerateName()

			// 生成默认头像
			img := identicon.S2(128).Make([]byte(in.Mobile))
			var buf bytes.Buffer
			_ = png.Encode(&buf, img)
			avatar := "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())

			user = &model.Users{
				Nickname:   nickname,
				Avatar:     avatar,
				Mobile:     in.Mobile,
				InviteCode: inviteCode,
				Status:     1,
			}

			if txErr = tx.Users.WithContext(ctx).Create(user); txErr != nil {
				return txErr
			}

			s.logger.Info("短信登录创建新用户", zap.Int64("user_id", user.ID), zap.String("mobile", in.Mobile))
		}

		// 处理邀请码逻辑（仅在真正的首次账户创建时触发，防止重复领奖）
		if in.InviteCode != "" && isNewUser {
			existed, _ := tx.UserInvites.WithContext(ctx).Where(tx.UserInvites.InviteeID.Eq(user.ID)).First()
			if existed == nil {
				inviter, _ := tx.Users.WithContext(ctx).Where(tx.Users.InviteCode.Eq(in.InviteCode)).First()
				if inviter != nil && inviter.ID != user.ID {
					reward := int64(10)
					inv := &model.UserInvites{
						InviterID:    inviter.ID,
						InviteeID:    user.ID,
						InviteCode:   in.InviteCode,
						RewardPoints: reward,
						RewardedAt:   time.Now(),
					}
					if txErr = tx.UserInvites.WithContext(ctx).Create(inv); txErr != nil {
						return txErr
					}

					// 更新被邀请人的 inviter_id
					if user.InviterID == 0 {
						tx.Users.WithContext(ctx).Where(tx.Users.ID.Eq(user.ID)).Updates(map[string]any{"inviter_id": inviter.ID})
					}

					// 为邀请人增加积分
					points, _ := tx.UserPoints.WithContext(ctx).
						Clauses(clause.Locking{Strength: "UPDATE"}).
						Where(tx.UserPoints.UserID.Eq(inviter.ID)).
						Where(tx.UserPoints.Kind.Eq("invite")).
						First()

					if points == nil {
						points = &model.UserPoints{
							UserID:     inviter.ID,
							Kind:       "invite",
							Points:     reward,
							ValidStart: time.Now(),
						}
						do := tx.UserPoints.WithContext(ctx)
						if points.ValidEnd.IsZero() {
							do = do.Omit(tx.UserPoints.ValidEnd)
						}
						if txErr = do.Create(points); txErr != nil {
							return txErr
						}
					} else {
						tx.UserPoints.WithContext(ctx).
							Where(tx.UserPoints.ID.Eq(points.ID)).
							Updates(map[string]any{"points": points.Points + reward})
					}

					// 记录积分流水
					ledger := &model.UserPointsLedger{
						UserID:   inviter.ID,
						Action:   "invite_reward",
						Points:   reward,
						RefTable: "user_invites",
						RefID:    strconv.FormatInt(inv.ID, 10),
						Remark:   "invite_reward",
					}
					tx.UserPointsLedger.WithContext(ctx).Create(ledger)
				}
			}
		}

		return nil
	})

	if err != nil {
		s.logger.Error("短信登录数据库操作失败", zap.Error(err))
		return nil, errors.New("登录失败，请稍后重试")
	}

	s.logger.Info("短信登录成功", zap.Int64("user_id", user.ID), zap.String("mobile", in.Mobile), zap.Bool("is_new", isNewUser))

	return &SmsLoginOutput{
		User:      user,
		IsNewUser: isNewUser,
	}, nil
}

// generateCode 生成指定长度的数字验证码
func generateCode(length int) string {
	rand.Seed(time.Now().UnixNano())
	code := ""
	for i := 0; i < length; i++ {
		code += strconv.Itoa(rand.Intn(10))
	}
	return code
}

// getRedisClient 获取Redis客户端
func getRedisClient() *redis.Client {
	// 动态导入，避免循环依赖
	return redis.NewClient(&redis.Options{
		Addr:     configs.Get().Redis.Addr,
		Password: configs.Get().Redis.Pass,
		DB:       configs.Get().Redis.DB,
	})
}
