package providers

import (
	"net/url"
	"strings"
	"sync"
)

// ProviderRegistry manages a collection of OIDC provider implementations.
// It provides thread-safe access to provider instances and caches detection results.
type ProviderRegistry struct {
	cache     map[string]OIDCProvider
	typeMap   map[ProviderType]OIDCProvider
	providers []OIDCProvider
	mu        sync.RWMutex
	// Bounded cache configuration to prevent memory leaks
	maxCacheSize int
	cacheCount   int
}

// NewProviderRegistry creates and initializes a new ProviderRegistry.
func NewProviderRegistry() *ProviderRegistry {
	return &ProviderRegistry{
		providers:    make([]OIDCProvider, 0),
		cache:        make(map[string]OIDCProvider),
		typeMap:      make(map[ProviderType]OIDCProvider),
		maxCacheSize: 1000, // Prevent unbounded cache growth
		cacheCount:   0,
	}
}

// RegisterProvider adds a new provider to the registry.
// It maintains both a list of providers and a type-to-provider mapping for efficient lookups.
func (r *ProviderRegistry) RegisterProvider(provider OIDCProvider) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.providers = append(r.providers, provider)
	r.typeMap[provider.GetType()] = provider
}

// GetProviderByType retrieves a provider instance by its type.
// Returns nil if the provider type is not registered.
func (r *ProviderRegistry) GetProviderByType(providerType ProviderType) OIDCProvider {
	r.mu.RLock()
	defer r.mu.RUnlock()
	return r.typeMap[providerType]
}

// GetRegisteredProviders returns a slice of all registered provider types.
func (r *ProviderRegistry) GetRegisteredProviders() []ProviderType {
	r.mu.RLock()
	defer r.mu.RUnlock()

	types := make([]ProviderType, 0, len(r.typeMap))
	for providerType := range r.typeMap {
		types = append(types, providerType)
	}
	return types
}

// ClearCache removes all cached provider detection results.
// This can be useful for testing or when provider configuration changes.
func (r *ProviderRegistry) ClearCache() {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.cache = make(map[string]OIDCProvider)
	r.cacheCount = 0
}

// evictOldestCacheEntry removes the first cache entry when cache is full
// This is a simple eviction strategy - in production, LRU might be preferred
func (r *ProviderRegistry) evictOldestCacheEntry() {
	// Simple eviction: remove first entry found
	for key := range r.cache {
		delete(r.cache, key)
		r.cacheCount--
		break
	}
}

// DetectProvider identifies the appropriate OIDC provider for an issuer URL.
// Uses double-checked locking pattern to avoid race conditions while caching results.
func (r *ProviderRegistry) DetectProvider(issuerURL string) OIDCProvider {
	r.mu.RLock()
	if provider, found := r.cache[issuerURL]; found {
		r.mu.RUnlock()
		return provider
	}
	r.mu.RUnlock()

	r.mu.Lock()
	defer r.mu.Unlock()

	if provider, found := r.cache[issuerURL]; found {
		return provider
	}

	detectedProvider := r.detectProviderUnsafe(issuerURL)

	// Check if cache is full and evict if necessary
	if r.cacheCount >= r.maxCacheSize {
		r.evictOldestCacheEntry()
	}

	r.cache[issuerURL] = detectedProvider
	r.cacheCount++

	return detectedProvider
}

// detectProviderUnsafe performs the actual provider detection logic.
// This method assumes the caller holds the appropriate lock and should not be called directly.
func (r *ProviderRegistry) detectProviderUnsafe(issuerURL string) OIDCProvider {
	normalizedURL, err := url.Parse(issuerURL)
	if err != nil {
		return nil
	}

	// Check if the URL has a valid scheme and host
	if normalizedURL.Scheme == "" || normalizedURL.Host == "" {
		return nil
	}

	// Convert host to lowercase for case-insensitive matching
	host := strings.ToLower(normalizedURL.Host)

	for _, p := range r.providers {
		switch p.GetType() {
		case ProviderTypeGoogle:
			if strings.Contains(host, "accounts.google.com") {
				return p
			}
		case ProviderTypeAzure:
			if strings.Contains(host, "login.microsoftonline.com") || strings.Contains(host, "sts.windows.net") {
				return p
			}
		case ProviderTypeGitHub:
			if strings.Contains(host, "github.com") {
				return p
			}
		case ProviderTypeAuth0:
			if strings.Contains(host, ".auth0.com") {
				return p
			}
		case ProviderTypeOkta:
			if strings.Contains(host, ".okta.com") || strings.Contains(host, ".oktapreview.com") || strings.Contains(host, ".okta-emea.com") {
				return p
			}
		case ProviderTypeKeycloak:
			// Match both Keycloak <17 (`/auth/realms/`) and 17+ (`/realms/`).
			if strings.Contains(host, "keycloak") || strings.Contains(normalizedURL.Path, "/realms/") {
				return p
			}
		case ProviderTypeAWSCognito:
			if strings.Contains(host, "cognito-idp") && strings.Contains(host, ".amazonaws.com") {
				return p
			}
		case ProviderTypeGitLab:
			// Match gitlab.com, self-hosted (gitlab.*), and instances with gitlab in subdomain
			if strings.Contains(host, "gitlab.com") ||
				strings.Contains(host, "gitlab") {
				return p
			}
		}
	}

	for _, p := range r.providers {
		if p.GetType() == ProviderTypeGeneric {
			return p
		}
	}

	return nil
}
