package repository

import (
	"context"
	"encoding/base64"
	"errors"
	"strconv"
	"time"

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

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

// NewRedisTOTPEnrollStore builds the staged-secret cache used during TOTP
// enrollment. Secrets are stored as opaque cipher blobs base64-encoded.
func NewRedisTOTPEnrollStore(client *redislib.Client) domrepo.TOTPEnrollStore {
	if client == nil || client.Zero() == nil {
		panic("member: redis client is required for totp enroll store")
	}
	return &redisTOTPEnrollStore{client: client.Zero()}
}

type redisTOTPEnrollStore struct {
	client *redis.Redis
}

func (s *redisTOTPEnrollStore) Save(ctx context.Context, tenantID, uid string, cipher []byte, ttl time.Duration) error {
	seconds := int(ttl.Seconds())
	if seconds < 1 {
		seconds = 1
	}
	encoded := base64.RawStdEncoding.EncodeToString(cipher)
	return s.client.SetexCtx(ctx, member.GetTOTPEnrollRedisKey(tenantID, uid), encoded, seconds)
}

func (s *redisTOTPEnrollStore) Get(ctx context.Context, tenantID, uid string) ([]byte, error) {
	val, err := s.client.GetCtx(ctx, member.GetTOTPEnrollRedisKey(tenantID, uid))
	if errors.Is(err, redis.Nil) || val == "" {
		return nil, member.ErrTOTPEnrollMissing
	}
	if err != nil {
		return nil, err
	}
	blob, err := base64.RawStdEncoding.DecodeString(val)
	if err != nil {
		return nil, err
	}
	return blob, nil
}

func (s *redisTOTPEnrollStore) Delete(ctx context.Context, tenantID, uid string) error {
	_, err := s.client.DelCtx(ctx, member.GetTOTPEnrollRedisKey(tenantID, uid))
	return err
}

// NewRedisTOTPReplayStore prevents replay of a successful TOTP code within
// its allowed window via SETNX on the (tenant,uid,timestep) key.
func NewRedisTOTPReplayStore(client *redislib.Client) domrepo.TOTPReplayStore {
	if client == nil || client.Zero() == nil {
		panic("member: redis client is required for totp replay store")
	}
	return &redisTOTPReplayStore{client: client.Zero()}
}

type redisTOTPReplayStore struct {
	client *redis.Redis
}

func (s *redisTOTPReplayStore) MarkUsed(ctx context.Context, tenantID, uid string, timestep uint64, ttl time.Duration) (bool, error) {
	seconds := int(ttl.Seconds())
	if seconds < 1 {
		seconds = 1
	}
	key := member.GetTOTPUsedRedisKey(tenantID, uid, strconv.FormatUint(timestep, 10))
	return s.client.SetnxExCtx(ctx, key, "1", seconds)
}
