// Package ratelimiter provides client-side rate limiting for sensitive operations.
//
// Security Note: This is a defense-in-depth measure. The backend MUST also implement
// rate limiting as the authoritative control. Client-side rate limiting provides:
//   - Protection against accidental rapid requests (e.g., user double-clicking)
//   - Reduced load on backend during legitimate high-frequency usage
//   - Better UX by failing fast with clear error messages
//   - Deterrent against simple automated attacks (though not a security boundary)
//
// This does NOT replace server-side rate limiting, which remains the security control.
package ratelimiter

import (
	"fmt"
	"sync"
	"time"
)

// Operation represents a rate-limited operation type
type Operation string

const (
	// OpRequestOTT is the operation for requesting a one-time token
	OpRequestOTT Operation = "request_ott"
	// OpVerifyOTT is the operation for verifying a one-time token
	OpVerifyOTT Operation = "verify_ott"
	// OpCompleteLogin is the operation for completing login
	OpCompleteLogin Operation = "complete_login"
	// OpRegister is the operation for user registration
	OpRegister Operation = "register"
	// OpVerifyEmail is the operation for email verification
	OpVerifyEmail Operation = "verify_email"
)

// RateLimitError is returned when an operation is rate limited
type RateLimitError struct {
	Operation   Operation
	RetryAfter  time.Duration
	AttemptsMade int
	MaxAttempts int
}

func (e *RateLimitError) Error() string {
	return fmt.Sprintf(
		"rate limited: %s operation exceeded %d attempts, retry after %v",
		e.Operation, e.MaxAttempts, e.RetryAfter.Round(time.Second),
	)
}

// operationLimit defines the rate limit configuration for an operation
type operationLimit struct {
	maxAttempts int           // Maximum attempts allowed in the window
	window      time.Duration // Time window for the limit
	cooldown    time.Duration // Cooldown period after hitting the limit
}

// operationState tracks the current state of rate limiting for an operation
type operationState struct {
	attempts    int       // Current attempt count
	windowStart time.Time // When the current window started
	lockedUntil time.Time // If rate limited, when the cooldown ends
}

// Service provides rate limiting functionality
type Service struct {
	mu     sync.Mutex
	limits map[Operation]operationLimit
	state  map[string]*operationState // key: operation + identifier (e.g., email)
}

// New creates a new rate limiter service with default limits.
//
// Default limits are designed to:
//   - Allow normal user behavior (typos, retries)
//   - Prevent rapid automated attempts
//   - Provide reasonable cooldown periods
func New() *Service {
	return &Service{
		limits: map[Operation]operationLimit{
			// OTT request: 3 attempts per 60 seconds, 2 minute cooldown
			// Rationale: Users might request OTT multiple times if email is slow
			OpRequestOTT: {
				maxAttempts: 3,
				window:      60 * time.Second,
				cooldown:    2 * time.Minute,
			},
			// OTT verification: 5 attempts per 60 seconds, 1 minute cooldown
			// Rationale: Users might mistype the 8-digit code
			OpVerifyOTT: {
				maxAttempts: 5,
				window:      60 * time.Second,
				cooldown:    1 * time.Minute,
			},
			// Complete login: 5 attempts per 60 seconds, 1 minute cooldown
			// Rationale: Password decryption might fail due to typos
			OpCompleteLogin: {
				maxAttempts: 5,
				window:      60 * time.Second,
				cooldown:    1 * time.Minute,
			},
			// Registration: 3 attempts per 5 minutes, 5 minute cooldown
			// Rationale: Registration is a one-time operation, limit abuse
			OpRegister: {
				maxAttempts: 3,
				window:      5 * time.Minute,
				cooldown:    5 * time.Minute,
			},
			// Email verification: 5 attempts per 60 seconds, 1 minute cooldown
			// Rationale: Users might mistype the verification code
			OpVerifyEmail: {
				maxAttempts: 5,
				window:      60 * time.Second,
				cooldown:    1 * time.Minute,
			},
		},
		state: make(map[string]*operationState),
	}
}

// ProvideService creates the rate limiter service for Wire dependency injection
func ProvideService() *Service {
	return New()
}

// Check verifies if an operation is allowed and records the attempt.
// The identifier is typically the user's email address.
// Returns nil if the operation is allowed, or a RateLimitError if rate limited.
func (s *Service) Check(op Operation, identifier string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	limit, ok := s.limits[op]
	if !ok {
		// Unknown operation, allow by default (fail open for usability)
		return nil
	}

	key := string(op) + ":" + identifier
	now := time.Now()

	state, exists := s.state[key]
	if !exists {
		// First attempt for this operation+identifier
		s.state[key] = &operationState{
			attempts:    1,
			windowStart: now,
		}
		return nil
	}

	// Check if currently in cooldown
	if now.Before(state.lockedUntil) {
		return &RateLimitError{
			Operation:    op,
			RetryAfter:   state.lockedUntil.Sub(now),
			AttemptsMade: state.attempts,
			MaxAttempts:  limit.maxAttempts,
		}
	}

	// Check if window has expired (reset if so)
	if now.Sub(state.windowStart) > limit.window {
		state.attempts = 1
		state.windowStart = now
		state.lockedUntil = time.Time{}
		return nil
	}

	// Increment attempt count
	state.attempts++

	// Check if limit exceeded
	if state.attempts > limit.maxAttempts {
		state.lockedUntil = now.Add(limit.cooldown)
		return &RateLimitError{
			Operation:    op,
			RetryAfter:   limit.cooldown,
			AttemptsMade: state.attempts,
			MaxAttempts:  limit.maxAttempts,
		}
	}

	return nil
}

// Reset clears the rate limit state for a specific operation and identifier.
// Call this after a successful operation where you want to allow fresh attempts
// (e.g., after successful OTT verification). Do NOT call this for operations
// where success shouldn't reset the limit (e.g., OTT request, registration).
func (s *Service) Reset(op Operation, identifier string) {
	s.mu.Lock()
	defer s.mu.Unlock()

	key := string(op) + ":" + identifier
	delete(s.state, key)
}

// ResetAll clears all rate limit state for an identifier (e.g., after successful login).
func (s *Service) ResetAll(identifier string) {
	s.mu.Lock()
	defer s.mu.Unlock()

	for op := range s.limits {
		key := string(op) + ":" + identifier
		delete(s.state, key)
	}
}

// GetRemainingAttempts returns the number of remaining attempts for an operation.
// Returns -1 if the operation is currently rate limited.
func (s *Service) GetRemainingAttempts(op Operation, identifier string) int {
	s.mu.Lock()
	defer s.mu.Unlock()

	limit, ok := s.limits[op]
	if !ok {
		return -1
	}

	key := string(op) + ":" + identifier
	state, exists := s.state[key]
	if !exists {
		return limit.maxAttempts
	}

	now := time.Now()

	// In cooldown
	if now.Before(state.lockedUntil) {
		return -1
	}

	// Window expired
	if now.Sub(state.windowStart) > limit.window {
		return limit.maxAttempts
	}

	remaining := limit.maxAttempts - state.attempts
	if remaining < 0 {
		return 0
	}
	return remaining
}

// Cleanup removes expired state entries to prevent memory growth.
// This should be called periodically (e.g., every hour).
func (s *Service) Cleanup() {
	s.mu.Lock()
	defer s.mu.Unlock()

	now := time.Now()
	maxAge := 24 * time.Hour // Remove entries older than 24 hours

	for key, state := range s.state {
		// Remove if window started more than maxAge ago and not in cooldown
		if now.Sub(state.windowStart) > maxAge && now.After(state.lockedUntil) {
			delete(s.state, key)
		}
	}
}
