package recovery

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

	"github.com/hibiken/asynq"
	"github.com/perfect-panel/server/internal/config"
	commonLogic "github.com/perfect-panel/server/internal/logic/common"
	"github.com/perfect-panel/server/internal/svc"
	"github.com/perfect-panel/server/internal/types"
	"github.com/perfect-panel/server/pkg/constant"
	"github.com/perfect-panel/server/pkg/limit"
	"github.com/perfect-panel/server/pkg/logger"
	"github.com/perfect-panel/server/pkg/random"
	"github.com/perfect-panel/server/pkg/xerr"
	queue "github.com/perfect-panel/server/queue/types"
	"github.com/pkg/errors"
)

type SendCodeLogic struct {
	logger.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext
}

func NewSendCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendCodeLogic {
	return &SendCodeLogic{
		Logger: logger.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
}

func (l *SendCodeLogic) SendCode(req *types.RecoverySendCodeRequest) (*types.SendCodeResponse, error) {
	email := strings.ToLower(strings.TrimSpace(req.Email))
	if email == "" {
		return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "email is required")
	}

	scene := constant.Security.String()
	cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, email)
	limiter := limit.NewPeriodLimit(60, 1, l.svcCtx.Redis, fmt.Sprintf("%s:%s:%s", config.SendIntervalKeyPrefix, "email", scene))
	permit, err := limiter.Take(email)
	if err != nil {
		return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to take limit")
	}
	if !limiter.ParsePermitState(permit) {
		return nil, errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "send email too many requests")
	}
	permit, err = l.svcCtx.AuthLimiter.Take(fmt.Sprintf("%s:%s:%s", "email", scene, email))
	if err != nil {
		return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to take limit")
	}
	if !l.svcCtx.AuthLimiter.ParsePermitState(permit) {
		return nil, errors.Wrapf(xerr.NewErrCode(xerr.TodaySendCountExceedsLimit), "send email too many requests")
	}

	code := random.Key(6, 0)
	expireTime := l.svcCtx.Config.VerifyCode.VerifyCodeExpireTime
	if expireTime == 0 {
		expireTime = 900
	}
	val, _ := json.Marshal(commonLogic.CacheKeyPayload{
		Code:   code,
		LastAt: time.Now().Unix(),
	})
	if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, string(val), time.Second*time.Duration(expireTime)).Err(); err != nil {
		return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to set verification code")
	}

	expireMinutes := expireTime / 60
	taskPayload := queue.SendEmailPayload{
		Type:    queue.EmailTypeVerify,
		Scene:   scene,
		Email:   email,
		Subject: "Verification code",
		Content: map[string]interface{}{
			"Type":     uint8(constant.Security),
			"SiteLogo": l.svcCtx.Config.Site.SiteLogo,
			"SiteName": l.svcCtx.Config.Site.SiteName,
			"Expire":   expireMinutes,
			"Code":     code,
		},
	}
	payload, err := json.Marshal(taskPayload)
	if err != nil {
		return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to marshal task payload")
	}
	task := asynq.NewTask(queue.ForthwithSendEmail, payload, asynq.MaxRetry(3))
	if _, err = l.svcCtx.Queue.Enqueue(task); err != nil {
		return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to enqueue task")
	}

	resp := &types.SendCodeResponse{Status: true}
	if l.svcCtx.Config.Model == constant.DevMode {
		resp.Code = code
	}
	return resp, nil
}
