package ratelimit

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"time"

	"github.com/redis/go-redis/v9"
	"go.uber.org/zap"
)

// AuthFailureRateLimiter provides specialized rate limiting for authorization failures
// to protect against privilege escalation and unauthorized access attempts
type AuthFailureRateLimiter interface {
	// CheckAuthFailure checks if the user has exceeded authorization failure limits
	// Returns: allowed (bool), remainingAttempts (int), resetTime (time.Time), error
	CheckAuthFailure(ctx context.Context, userID string, resourceID string, action string) (bool, int, time.Time, error)

	// RecordAuthFailure records an authorization failure
	RecordAuthFailure(ctx context.Context, userID string, resourceID string, action string, reason string) error

	// RecordAuthSuccess records a successful authorization (optionally resets counters)
	RecordAuthSuccess(ctx context.Context, userID string, resourceID string, action string) error

	// IsUserBlocked checks if a user is temporarily blocked from authorization attempts
	IsUserBlocked(ctx context.Context, userID string) (bool, time.Duration, error)

	// GetFailureCount returns the number of authorization failures for a user
	GetFailureCount(ctx context.Context, userID string) (int, error)

	// GetResourceFailureCount returns failures for a specific resource
	GetResourceFailureCount(ctx context.Context, userID string, resourceID string) (int, error)

	// ResetUserFailures manually resets failure counters for a user
	ResetUserFailures(ctx context.Context, userID string) error
}

// AuthFailureRateLimiterConfig holds configuration for authorization failure rate limiting
type AuthFailureRateLimiterConfig struct {
	// MaxFailuresPerUser is the maximum authorization failures per user before blocking
	MaxFailuresPerUser int
	// MaxFailuresPerResource is the maximum failures per resource per user
	MaxFailuresPerResource int
	// FailureWindow is the time window for tracking failures
	FailureWindow time.Duration
	// BlockDuration is how long to block a user after exceeding limits
	BlockDuration time.Duration
	// AlertThreshold is the number of failures before alerting (for monitoring)
	AlertThreshold int
	// KeyPrefix is the prefix for Redis keys
	KeyPrefix string
}

// DefaultAuthFailureRateLimiterConfig returns recommended configuration
// Following OWASP guidelines for authorization failure handling
func DefaultAuthFailureRateLimiterConfig() AuthFailureRateLimiterConfig {
	return AuthFailureRateLimiterConfig{
		MaxFailuresPerUser:      20,              // 20 total auth failures per user
		MaxFailuresPerResource:  5,               // 5 failures per specific resource
		FailureWindow:           15 * time.Minute, // in 15-minute window
		BlockDuration:           30 * time.Minute, // block for 30 minutes
		AlertThreshold:          10,              // alert after 10 failures
		KeyPrefix:               "auth_fail_rl",
	}
}

type authFailureRateLimiter struct {
	client *redis.Client
	config AuthFailureRateLimiterConfig
	logger *zap.Logger
}

// NewAuthFailureRateLimiter creates a new authorization failure rate limiter
func NewAuthFailureRateLimiter(client *redis.Client, config AuthFailureRateLimiterConfig, logger *zap.Logger) AuthFailureRateLimiter {
	return &authFailureRateLimiter{
		client: client,
		config: config,
		logger: logger.Named("auth-failure-rate-limiter"),
	}
}

// CheckAuthFailure checks if the user has exceeded authorization failure limits
// CWE-307: Protection against authorization brute force attacks
// OWASP A01:2021: Broken Access Control - Rate limiting authorization failures
func (r *authFailureRateLimiter) CheckAuthFailure(ctx context.Context, userID string, resourceID string, action string) (bool, int, time.Time, error) {
	// Check if user is blocked
	blocked, remaining, err := r.IsUserBlocked(ctx, userID)
	if err != nil {
		r.logger.Error("failed to check user block status",
			zap.String("user_id_hash", hashID(userID)),
			zap.Error(err))
		// Fail open on Redis error (security vs availability trade-off)
		return true, 0, time.Time{}, err
	}

	if blocked {
		resetTime := time.Now().Add(remaining)
		r.logger.Warn("blocked user attempted authorization",
			zap.String("user_id_hash", hashID(userID)),
			zap.String("resource_id_hash", hashID(resourceID)),
			zap.String("action", action),
			zap.Duration("remaining_block", remaining))
		return false, 0, resetTime, nil
	}

	// Check per-user failure count
	userFailures, err := r.GetFailureCount(ctx, userID)
	if err != nil {
		r.logger.Error("failed to get user failure count",
			zap.String("user_id_hash", hashID(userID)),
			zap.Error(err))
		// Fail open on Redis error
		return true, 0, time.Time{}, err
	}

	// Check per-resource failure count
	resourceFailures, err := r.GetResourceFailureCount(ctx, userID, resourceID)
	if err != nil {
		r.logger.Error("failed to get resource failure count",
			zap.String("user_id_hash", hashID(userID)),
			zap.String("resource_id_hash", hashID(resourceID)),
			zap.Error(err))
		// Fail open on Redis error
		return true, 0, time.Time{}, err
	}

	// Check if limits exceeded
	if userFailures >= r.config.MaxFailuresPerUser {
		r.blockUser(ctx, userID)
		resetTime := time.Now().Add(r.config.BlockDuration)
		r.logger.Warn("user exceeded authorization failure limit",
			zap.String("user_id_hash", hashID(userID)),
			zap.Int("failures", userFailures))
		return false, 0, resetTime, nil
	}

	if resourceFailures >= r.config.MaxFailuresPerResource {
		resetTime := time.Now().Add(r.config.FailureWindow)
		r.logger.Warn("user exceeded resource-specific failure limit",
			zap.String("user_id_hash", hashID(userID)),
			zap.String("resource_id_hash", hashID(resourceID)),
			zap.Int("failures", resourceFailures))
		return false, r.config.MaxFailuresPerUser - userFailures, resetTime, nil
	}

	remainingAttempts := r.config.MaxFailuresPerUser - userFailures
	resetTime := time.Now().Add(r.config.FailureWindow)

	return true, remainingAttempts, resetTime, nil
}

// RecordAuthFailure records an authorization failure
// CWE-778: Insufficient Logging of security events
func (r *authFailureRateLimiter) RecordAuthFailure(ctx context.Context, userID string, resourceID string, action string, reason string) error {
	now := time.Now()
	timestamp := now.UnixNano()

	// Record per-user failure
	userKey := r.getUserFailureKey(userID)
	pipe := r.client.Pipeline()

	// Add to sorted set with timestamp as score (for windowing)
	pipe.ZAdd(ctx, userKey, redis.Z{
		Score:  float64(timestamp),
		Member: fmt.Sprintf("%d:%s:%s", timestamp, resourceID, action),
	})
	pipe.Expire(ctx, userKey, r.config.FailureWindow)

	// Record per-resource failure
	if resourceID != "" {
		resourceKey := r.getResourceFailureKey(userID, resourceID)
		pipe.ZAdd(ctx, resourceKey, redis.Z{
			Score:  float64(timestamp),
			Member: fmt.Sprintf("%d:%s", timestamp, action),
		})
		pipe.Expire(ctx, resourceKey, r.config.FailureWindow)
	}

	// Increment total failure counter for metrics
	metricsKey := r.getMetricsKey(userID)
	pipe.Incr(ctx, metricsKey)
	pipe.Expire(ctx, metricsKey, 24*time.Hour) // Keep metrics for 24 hours

	_, err := pipe.Exec(ctx)
	if err != nil {
		r.logger.Error("failed to record authorization failure",
			zap.String("user_id_hash", hashID(userID)),
			zap.String("resource_id_hash", hashID(resourceID)),
			zap.String("action", action),
			zap.Error(err))
		return err
	}

	// Check if we should alert
	count, _ := r.GetFailureCount(ctx, userID)
	if count == r.config.AlertThreshold {
		r.logger.Error("SECURITY ALERT: User reached authorization failure alert threshold",
			zap.String("user_id_hash", hashID(userID)),
			zap.String("resource_id_hash", hashID(resourceID)),
			zap.String("action", action),
			zap.String("reason", reason),
			zap.Int("failure_count", count))
	}

	r.logger.Warn("authorization failure recorded",
		zap.String("user_id_hash", hashID(userID)),
		zap.String("resource_id_hash", hashID(resourceID)),
		zap.String("action", action),
		zap.String("reason", reason),
		zap.Int("total_failures", count))

	return nil
}

// RecordAuthSuccess records a successful authorization
func (r *authFailureRateLimiter) RecordAuthSuccess(ctx context.Context, userID string, resourceID string, action string) error {
	// Optionally, we could reset or reduce failure counts on success
	// For now, we just log the success for audit purposes
	r.logger.Debug("authorization success recorded",
		zap.String("user_id_hash", hashID(userID)),
		zap.String("resource_id_hash", hashID(resourceID)),
		zap.String("action", action))

	return nil
}

// IsUserBlocked checks if a user is temporarily blocked
func (r *authFailureRateLimiter) IsUserBlocked(ctx context.Context, userID string) (bool, time.Duration, error) {
	blockKey := r.getBlockKey(userID)

	ttl, err := r.client.TTL(ctx, blockKey).Result()
	if err != nil {
		return false, 0, err
	}

	// TTL returns -2 if key doesn't exist, -1 if no expiration
	if ttl < 0 {
		return false, 0, nil
	}

	return true, ttl, nil
}

// GetFailureCount returns the number of authorization failures for a user
func (r *authFailureRateLimiter) GetFailureCount(ctx context.Context, userID string) (int, error) {
	userKey := r.getUserFailureKey(userID)
	now := time.Now()
	windowStart := now.Add(-r.config.FailureWindow)

	// Remove old entries outside the window
	r.client.ZRemRangeByScore(ctx, userKey, "0", fmt.Sprintf("%d", windowStart.UnixNano()))

	// Count current failures in window
	count, err := r.client.ZCount(ctx, userKey,
		fmt.Sprintf("%d", windowStart.UnixNano()),
		"+inf").Result()

	if err != nil && err != redis.Nil {
		return 0, err
	}

	return int(count), nil
}

// GetResourceFailureCount returns failures for a specific resource
func (r *authFailureRateLimiter) GetResourceFailureCount(ctx context.Context, userID string, resourceID string) (int, error) {
	if resourceID == "" {
		return 0, nil
	}

	resourceKey := r.getResourceFailureKey(userID, resourceID)
	now := time.Now()
	windowStart := now.Add(-r.config.FailureWindow)

	// Remove old entries
	r.client.ZRemRangeByScore(ctx, resourceKey, "0", fmt.Sprintf("%d", windowStart.UnixNano()))

	// Count current failures
	count, err := r.client.ZCount(ctx, resourceKey,
		fmt.Sprintf("%d", windowStart.UnixNano()),
		"+inf").Result()

	if err != nil && err != redis.Nil {
		return 0, err
	}

	return int(count), nil
}

// ResetUserFailures manually resets failure counters for a user
func (r *authFailureRateLimiter) ResetUserFailures(ctx context.Context, userID string) error {
	pattern := fmt.Sprintf("%s:user:%s:*", r.config.KeyPrefix, hashID(userID))

	// Find all keys for this user
	keys, err := r.client.Keys(ctx, pattern).Result()
	if err != nil {
		return err
	}

	if len(keys) > 0 {
		pipe := r.client.Pipeline()
		for _, key := range keys {
			pipe.Del(ctx, key)
		}
		_, err = pipe.Exec(ctx)
		if err != nil {
			r.logger.Error("failed to reset user failures",
				zap.String("user_id_hash", hashID(userID)),
				zap.Error(err))
			return err
		}
	}

	r.logger.Info("user authorization failures reset",
		zap.String("user_id_hash", hashID(userID)))

	return nil
}

// blockUser blocks a user from further authorization attempts
func (r *authFailureRateLimiter) blockUser(ctx context.Context, userID string) error {
	blockKey := r.getBlockKey(userID)
	err := r.client.Set(ctx, blockKey, "blocked", r.config.BlockDuration).Err()
	if err != nil {
		r.logger.Error("failed to block user",
			zap.String("user_id_hash", hashID(userID)),
			zap.Error(err))
		return err
	}

	r.logger.Error("SECURITY: User blocked due to excessive authorization failures",
		zap.String("user_id_hash", hashID(userID)),
		zap.Duration("block_duration", r.config.BlockDuration))

	return nil
}

// Key generation helpers
func (r *authFailureRateLimiter) getUserFailureKey(userID string) string {
	return fmt.Sprintf("%s:user:%s:failures", r.config.KeyPrefix, hashID(userID))
}

func (r *authFailureRateLimiter) getResourceFailureKey(userID string, resourceID string) string {
	return fmt.Sprintf("%s:user:%s:resource:%s:failures", r.config.KeyPrefix, hashID(userID), hashID(resourceID))
}

func (r *authFailureRateLimiter) getBlockKey(userID string) string {
	return fmt.Sprintf("%s:user:%s:blocked", r.config.KeyPrefix, hashID(userID))
}

func (r *authFailureRateLimiter) getMetricsKey(userID string) string {
	return fmt.Sprintf("%s:user:%s:metrics", r.config.KeyPrefix, hashID(userID))
}

// hashID creates a consistent hash of an ID for use as a Redis key component
// CWE-532: Prevents sensitive IDs in Redis keys
func hashID(id string) string {
	if id == "" {
		return "empty"
	}
	hash := sha256.Sum256([]byte(id))
	// Return first 16 bytes of hash as hex (32 chars) for shorter keys
	return hex.EncodeToString(hash[:16])
}