package acme

import (
	"crypto/rand"
	"encoding/base64"
	"errors"
	"sync"
	"time"
)

const nonceLifetime = 10 * time.Minute

// NonceStore is a thread-safe single-use nonce store with expiry.
// Nonces are short-lived per RFC 8555 §7.2.
type NonceStore struct {
	nonces map[string]time.Time
	issued int
	mu     sync.Mutex
}

// NewNonceStore creates a new nonce store.
func NewNonceStore() *NonceStore {
	return &NonceStore{
		nonces: make(map[string]time.Time),
	}
}

// Issue generates, stores, and returns a new base64url-encoded nonce.
func (s *NonceStore) Issue() (string, error) {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	nonce := base64.RawURLEncoding.EncodeToString(b)

	s.mu.Lock()
	defer s.mu.Unlock()
	s.nonces[nonce] = time.Now().Add(nonceLifetime)
	s.issued++
	// Purge expired nonces every 100 issues to bound memory.
	if s.issued%100 == 0 {
		s.purgeExpiredLocked()
	}
	return nonce, nil
}

// Consume validates that the nonce exists and has not expired, then removes it.
// Returns an error if the nonce is unknown, expired, or already consumed.
func (s *NonceStore) Consume(nonce string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	exp, ok := s.nonces[nonce]
	if !ok {
		return errors.New("unknown or already-consumed nonce")
	}
	delete(s.nonces, nonce)
	if time.Now().After(exp) {
		return errors.New("nonce expired")
	}
	return nil
}

// purgeExpiredLocked removes all expired nonces. Caller must hold s.mu.
func (s *NonceStore) purgeExpiredLocked() {
	now := time.Now()
	for n, exp := range s.nonces {
		if now.After(exp) {
			delete(s.nonces, n)
		}
	}
}
