package repository

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"time"

	redislib "gateway/internal/library/redis"
	member "gateway/internal/model/member/domain"
	domrepo "gateway/internal/model/member/domain/repository"

	"github.com/zeromicro/go-zero/core/stores/redis"
)

type redisOTPChallengeStore struct {
	client *redis.Redis
}

func NewRedisOTPChallengeStore(client *redislib.Client) domrepo.OTPChallengeStore {
	if client == nil || client.Zero() == nil {
		panic("member: redis client is required for otp store")
	}
	return &redisOTPChallengeStore{client: client.Zero()}
}

func (s *redisOTPChallengeStore) Save(ctx context.Context, challengeID string, ch *domrepo.OTPChallenge, ttl time.Duration) error {
	payload := *ch
	payload.Attempts = 0
	raw, err := json.Marshal(&payload)
	if err != nil {
		return fmt.Errorf("member: marshal otp challenge: %w", err)
	}
	seconds := int(ttl.Seconds())
	if seconds < 1 {
		seconds = 1
	}
	chKey := member.GetOTPChallengeRedisKey(challengeID)
	attKey := member.GetOTPAttemptsRedisKey(challengeID)
	if err := s.client.SetexCtx(ctx, chKey, string(raw), seconds); err != nil {
		return err
	}
	return s.client.SetexCtx(ctx, attKey, "0", seconds)
}

func (s *redisOTPChallengeStore) Get(ctx context.Context, challengeID string) (*domrepo.OTPChallenge, error) {
	val, err := s.client.GetCtx(ctx, member.GetOTPChallengeRedisKey(challengeID))
	if errors.Is(err, redis.Nil) {
		return nil, member.ErrChallengeNotFound
	}
	if err != nil {
		return nil, err
	}
	var ch domrepo.OTPChallenge
	if err := json.Unmarshal([]byte(val), &ch); err != nil {
		return nil, fmt.Errorf("member: unmarshal otp challenge: %w", err)
	}
	attKey := member.GetOTPAttemptsRedisKey(challengeID)
	if attVal, attErr := s.client.GetCtx(ctx, attKey); attErr == nil && attVal != "" {
		if n, parseErr := strconv.Atoi(attVal); parseErr == nil {
			ch.Attempts = n
		}
	}
	return &ch, nil
}

func (s *redisOTPChallengeStore) IncrementAttempts(ctx context.Context, challengeID string) (int, error) {
	chKey := member.GetOTPChallengeRedisKey(challengeID)
	exists, err := s.client.ExistsCtx(ctx, chKey)
	if err != nil {
		return 0, err
	}
	if !exists {
		return 0, member.ErrChallengeNotFound
	}
	attKey := member.GetOTPAttemptsRedisKey(challengeID)
	count, err := s.client.IncrCtx(ctx, attKey)
	if err != nil {
		return 0, err
	}
	if ttl, ttlErr := s.client.TtlCtx(ctx, chKey); ttlErr == nil && ttl > 0 {
		if expErr := s.client.ExpireCtx(ctx, attKey, ttl); expErr != nil {
			return int(count), expErr
		}
	}
	return int(count), nil
}

func (s *redisOTPChallengeStore) Delete(ctx context.Context, challengeID string) error {
	_, err := s.client.DelCtx(ctx, member.GetOTPChallengeRedisKey(challengeID), member.GetOTPAttemptsRedisKey(challengeID))
	return err
}

type redisVerifyRateStore struct {
	client *redis.Redis
}

func NewRedisVerifyRateStore(client *redislib.Client) domrepo.VerifyRateStore {
	if client == nil || client.Zero() == nil {
		panic("member: redis client is required for verify rate store")
	}
	return &redisVerifyRateStore{client: client.Zero()}
}

func (s *redisVerifyRateStore) TryResendLock(ctx context.Context, key string, ttl time.Duration) (bool, error) {
	seconds := int(ttl.Seconds())
	if seconds < 1 {
		seconds = 1
	}
	ok, err := s.client.SetnxExCtx(ctx, key, "1", seconds)
	if err != nil {
		return false, err
	}
	return ok, nil
}

func (s *redisVerifyRateStore) IncrDaily(ctx context.Context, key string, ttl time.Duration) (int64, error) {
	count, err := s.client.IncrCtx(ctx, key)
	if err != nil {
		return 0, err
	}
	seconds := int(ttl.Seconds())
	if seconds < 1 {
		seconds = 1
	}
	if err := s.client.ExpireCtx(ctx, key, seconds); err != nil {
		return count, err
	}
	return count, nil
}
