package traefikoidc

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"strings"
	"time"

	"github.com/lukaszraczylo/traefikoidc/internal/utils"
)

// newUUIDv4 returns an RFC 4122 v4 UUID string (e.g.
// "f47ac10b-58cc-4372-a567-0e02b2c3d479") backed by crypto/rand. Used for CSRF
// tokens and other opaque random identifiers — replaces github.com/google/uuid
// to keep the plugin stdlib-only on the production path.
func newUUIDv4() (string, error) {
	var b [16]byte
	if _, err := rand.Read(b[:]); err != nil {
		return "", fmt.Errorf("could not generate UUID: %w", err)
	}
	b[6] = (b[6] & 0x0f) | 0x40 // version 4
	b[8] = (b[8] & 0x3f) | 0x80 // RFC 4122 variant
	return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
		b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]), nil
}

// generateNonce creates a cryptographically secure random nonce for OIDC flows.
// The nonce is used to prevent replay attacks and associate client sessions with ID tokens.
// Returns:
//   - A base64 URL-encoded nonce string (43 characters)
//   - An error if the random byte generation fails
func generateNonce() (string, error) {
	nonceBytes := make([]byte, 32)
	_, err := rand.Read(nonceBytes)
	if err != nil {
		return "", fmt.Errorf("could not generate nonce: %w", err)
	}
	return base64.URLEncoding.EncodeToString(nonceBytes), nil
}

// generateCodeVerifier creates a PKCE code verifier according to RFC 7636.
// The code verifier is a cryptographically random string used for the PKCE flow
// to prevent authorization code interception attacks.
// Returns:
//   - A base64 raw URL-encoded code verifier string (43 characters)
//   - An error if the random byte generation fails
func generateCodeVerifier() (string, error) {
	verifierBytes := make([]byte, 32)
	_, err := rand.Read(verifierBytes)
	if err != nil {
		return "", fmt.Errorf("could not generate code verifier: %w", err)
	}
	return base64.RawURLEncoding.EncodeToString(verifierBytes), nil
}

// deriveCodeChallenge creates a PKCE code challenge from the code verifier.
// It computes the SHA-256 hash of the code verifier and base64 URL-encodes it
// according to RFC 7636 specification.
// Parameters:
//   - codeVerifier: The code verifier string
//
// Returns:
//   - The base64 URL encoded SHA-256 hash of the code verifier (code challenge)
func deriveCodeChallenge(codeVerifier string) string {
	hasher := sha256.New()
	hasher.Write([]byte(codeVerifier))
	hash := hasher.Sum(nil)

	return base64.RawURLEncoding.EncodeToString(hash)
}

// TokenResponse represents the standard OAuth 2.0/OIDC token response.
// It contains the tokens and metadata returned by the authorization server during
// code exchange or token refresh operations.
type TokenResponse struct {
	// IDToken contains the OpenID Connect identity token (JWT)
	IDToken string `json:"id_token"`
	// AccessToken is the OAuth 2.0 access token for API access
	AccessToken string `json:"access_token"`
	// RefreshToken allows obtaining new tokens when the access token expires
	RefreshToken string `json:"refresh_token"`
	// TokenType specifies the token type (typically "Bearer")
	TokenType string `json:"token_type"`
	// ExpiresIn indicates token lifetime in seconds
	ExpiresIn int `json:"expires_in"`
}

// exchangeTokens performs OAuth 2.0 token exchange with the authorization server.
// It supports both authorization code and refresh token grant types with PKCE support.
// Parameters:
//   - ctx: Context for request timeout and cancellation
//   - grantType: OAuth grant type ("authorization_code" or "refresh_token")
//   - codeOrToken: Authorization code or refresh token depending on grant type
//   - redirectURL: Redirect URI used in authorization (required for code exchange)
//   - codeVerifier: PKCE code verifier (optional, used with PKCE flow)
//
// Returns:
//   - *TokenResponse: Parsed token response from the authorization server
//   - An error if the token exchange fails (e.g., network error, provider error, invalid grant)
func (t *TraefikOidc) exchangeTokens(ctx context.Context, grantType string, codeOrToken string, redirectURL string, codeVerifier string) (*TokenResponse, error) {
	data := url.Values{
		"grant_type": {grantType},
	}
	// client_id is sent in the body for every method except client_secret_basic,
	// where it is carried in the Authorization header per RFC 6749 §2.3.1.
	if t.clientAuthMethod != "client_secret_basic" || t.clientAssertion != nil {
		data.Set("client_id", t.clientID)
	}

	if grantType == "authorization_code" {
		data.Set("code", codeOrToken)
		data.Set("redirect_uri", redirectURL)

		if codeVerifier != "" {
			data.Set("code_verifier", codeVerifier)
		}
	} else if grantType == "refresh_token" {
		data.Set("refresh_token", codeOrToken)
	}

	client := t.tokenHTTPClient
	if client == nil {
		// Use shared transport pool to prevent memory leaks
		jar, _ := cookiejar.New(nil) // Safe to ignore: cookiejar creation with nil options rarely fails
		pooledClient := CreateTokenHTTPClient()
		client = &http.Client{
			Transport: pooledClient.Transport,
			Timeout:   pooledClient.Timeout,
			CheckRedirect: func(req *http.Request, via []*http.Request) error {
				if len(via) >= 50 {
					return fmt.Errorf("stopped after 50 redirects")
				}
				return nil
			},
			Jar: jar,
		}
	}

	// Read tokenURL with RLock — needed as audience for private_key_jwt (RFC 7523 §3).
	t.metadataMu.RLock()
	tokenURL := t.tokenURL
	t.metadataMu.RUnlock()

	useBasicAuth := false
	if t.clientAssertion != nil {
		assertion, err := t.clientAssertion.Sign(tokenURL, t.clientID)
		if err != nil {
			return nil, fmt.Errorf("failed to sign client assertion: %w", err)
		}
		data.Set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
		data.Set("client_assertion", assertion)
	} else if t.clientAuthMethod == "client_secret_basic" {
		useBasicAuth = true
	} else {
		data.Set("client_secret", t.clientSecret)
	}

	req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, strings.NewReader(data.Encode()))
	if err != nil {
		return nil, fmt.Errorf("failed to create token request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	if useBasicAuth {
		setOAuthBasicAuth(req, t.clientID, t.clientSecret)
	}

	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to exchange tokens: %w", err)
	}
	defer func() {
		_, _ = io.Copy(io.Discard, resp.Body) // Safe to ignore: draining response body on defer
		_ = resp.Body.Close()                 // Safe to ignore: closing body on defer
	}()

	if resp.StatusCode != http.StatusOK {
		limitReader := io.LimitReader(resp.Body, 1024*10)
		bodyBytes, _ := io.ReadAll(limitReader) // Safe to ignore: reading error body for diagnostics
		return nil, fmt.Errorf("token endpoint returned status %d: %s", resp.StatusCode, string(bodyBytes))
	}

	var tokenResponse TokenResponse
	if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
		return nil, fmt.Errorf("failed to decode token response: %w", err)
	}

	return &tokenResponse, nil
}

// getNewTokenWithRefreshToken refreshes access and ID tokens using a refresh token.
// This is used when the current tokens are expired but the refresh token is still valid.
// It now uses the TokenResilienceManager for circuit breaker and retry logic.
// Parameters:
//   - refreshToken: The refresh token to exchange for new tokens
//
// Returns:
//   - *TokenResponse: New token set from the authorization server
//   - An error if the refresh operation fails
func (t *TraefikOidc) getNewTokenWithRefreshToken(refreshToken string) (*TokenResponse, error) {
	ctx := context.Background()

	// Use token resilience manager if available, otherwise fall back to direct call
	if t.tokenResilienceManager != nil {
		return t.tokenResilienceManager.ExecuteTokenRefresh(ctx, t, refreshToken)
	}

	// Fallback for backward compatibility
	tokenResponse, err := t.exchangeTokens(ctx, "refresh_token", refreshToken, "", "")
	if err != nil {
		return nil, fmt.Errorf("failed to refresh token: %w", err)
	}

	t.logger.Debugf("Token response: %+v", tokenResponse)
	return tokenResponse, nil
}

// extractClaims extracts and parses claims from a JWT token without signature verification.
// This is a utility function for quickly accessing token payload data when signature
// verification is not required or has already been performed.
// Parameters:
//   - tokenString: The JWT token string to parse
//
// Returns:
//   - map[string]interface{}: Parsed claims from the token payload
//   - An error if the token format is invalid, decoding fails, or JSON unmarshaling fails
func extractClaims(tokenString string) (map[string]interface{}, error) {
	parts := strings.Split(tokenString, ".")
	if len(parts) != 3 {
		return nil, fmt.Errorf("invalid token format")
	}

	payload, err := base64.RawURLEncoding.DecodeString(parts[1])
	if err != nil {
		return nil, fmt.Errorf("failed to decode token payload: %w", err)
	}

	var claims map[string]interface{}
	if err := json.Unmarshal(payload, &claims); err != nil {
		return nil, fmt.Errorf("failed to unmarshal claims: %w", err)
	}

	return claims, nil
}

// TokenCache provides a specialized cache for JWT tokens and their parsed claims.
// It wraps the UniversalCache with token-specific operations.
type TokenCache struct {
	// cache is the underlying universal cache implementation
	cache *UniversalCache
}

// NewTokenCache creates and initializes a new TokenCache.
// It uses the global cache manager to ensure singleton behavior.
func NewTokenCache() *TokenCache {
	manager := GetUniversalCacheManager(nil)
	return &TokenCache{
		cache: manager.GetTokenCache(),
	}
}

// Set stores parsed token claims in the cache with expiration.
// The token is prefixed to prevent collisions with other cache entries.
// Parameters:
//   - token: The JWT token string (used as cache key)
//   - claims: Parsed claims from the token
//   - expiration: The duration for which the cache entry should be valid
func (tc *TokenCache) Set(token string, claims map[string]interface{}, expiration time.Duration) {
	token = "t-" + token
	_ = tc.cache.Set(token, claims, expiration) // Safe to ignore: cache failures are non-critical
}

// Get retrieves cached claims for a token.
// Parameters:
//   - token: The JWT token string to look up
//
// Returns:
//   - map[string]interface{}: The cached claims if found
//   - A boolean indicating whether the token was found in the cache (true if found, false otherwise)
func (tc *TokenCache) Get(token string) (map[string]interface{}, bool) {
	token = "t-" + token
	value, found := tc.cache.Get(token)
	if !found {
		return nil, false
	}
	claims, ok := value.(map[string]interface{})
	return claims, ok
}

// Delete removes a token from the cache.
// Parameters:
//   - token: The raw token string to remove from the cache
func (tc *TokenCache) Delete(token string) {
	token = "t-" + token
	tc.cache.Delete(token)
}

// Cleanup removes expired entries from the token cache.
// This is a no-op as cleanup is handled internally by UniversalCache.
func (tc *TokenCache) Cleanup() {
	// Cleanup is handled internally by UniversalCache
}

// Close stops the cleanup goroutine and releases resources.
// This is a no-op as the cache is managed globally.
func (tc *TokenCache) Close() {
	// Cache is managed globally by UniversalCacheManager
}

// Clear removes all items from the cache
func (tc *TokenCache) Clear() {
	tc.cache.Clear()
}

// exchangeCodeForToken exchanges an authorization code for tokens.
// This implements the OAuth 2.0 authorization code flow with optional PKCE support.
// It now uses the TokenResilienceManager for circuit breaker and retry logic.
// Parameters:
//   - code: The authorization code received from the authorization server
//   - redirectURL: The redirect URI used in the authorization request
//   - codeVerifier: PKCE code verifier (used if PKCE is enabled)
//
// Returns:
//   - *TokenResponse: The token response containing access, refresh, and ID tokens
//   - An error if the code exchange fails
func (t *TraefikOidc) exchangeCodeForToken(code string, redirectURL string, codeVerifier string) (*TokenResponse, error) {
	ctx := context.Background()

	effectiveCodeVerifier := ""
	if t.enablePKCE && codeVerifier != "" {
		effectiveCodeVerifier = codeVerifier
	}

	// Use token resilience manager if available, otherwise fall back to direct call
	if t.tokenResilienceManager != nil {
		return t.tokenResilienceManager.ExecuteTokenExchange(ctx, t, "authorization_code", code, redirectURL, effectiveCodeVerifier)
	}

	// Fallback for backward compatibility
	tokenResponse, err := t.exchangeTokens(ctx, "authorization_code", code, redirectURL, effectiveCodeVerifier)
	if err != nil {
		return nil, fmt.Errorf("failed to exchange code for token: %w", err)
	}
	return tokenResponse, nil
}

// createStringMap converts a slice of strings to a set-like map for fast lookups.
// This is a utility function for creating efficient membership tests.
// Parameters:
//   - keys: Slice of strings to convert to a map
//
// Returns:
//   - A map where the keys are the strings from the input slice and the values are empty structs
func createStringMap(keys []string) map[string]struct{} {
	result := make(map[string]struct{})
	for _, key := range keys {
		result[key] = struct{}{}
	}
	return result
}

// handleLogout processes user logout requests and performs proper session cleanup.
// It retrieves the ID token for logout URL construction, clears the session,
// and redirects to the provider's logout endpoint or configured post-logout URI.
// It handles potential errors during session retrieval or clearing.
func (t *TraefikOidc) handleLogout(rw http.ResponseWriter, req *http.Request) {
	t.logger.Debug("Processing logout request")
	session, err := t.sessionManager.GetSession(req)
	if err != nil {
		t.logger.Errorf("Error getting session: %v", err)
		http.Error(rw, "Session error", http.StatusInternalServerError)
		return
	}

	idToken := session.GetIDToken()

	if err := session.Clear(req, rw); err != nil {
		t.logger.Errorf("Error clearing session: %v", err)
		http.Error(rw, "Session error", http.StatusInternalServerError)
		return
	}

	host := utils.DetermineHost(req)
	scheme := utils.DetermineScheme(req, t.forceHTTPS)
	baseURL := fmt.Sprintf("%s://%s", scheme, host)

	postLogoutRedirectURI := t.postLogoutRedirectURI
	// localRedirect is used when there is no provider end-session endpoint and
	// the plugin redirects the browser itself. It must never be an absolute URL
	// derived from the request host (X-Forwarded-Host is client-controllable and
	// would be an open redirect); use a host-relative path, or the operator's
	// own configured absolute URL, instead.
	localRedirect := "/"
	if postLogoutRedirectURI == "" {
		postLogoutRedirectURI = fmt.Sprintf("%s/", baseURL)
	} else if !strings.HasPrefix(postLogoutRedirectURI, "http") {
		localRedirect = normalizeLogoutPath(postLogoutRedirectURI)
		postLogoutRedirectURI = fmt.Sprintf("%s%s", baseURL, postLogoutRedirectURI)
	} else {
		localRedirect = postLogoutRedirectURI
	}

	// Read endSessionURL with RLock
	t.metadataMu.RLock()
	endSessionURL := t.endSessionURL
	t.metadataMu.RUnlock()

	if endSessionURL != "" && idToken != "" {
		logoutURL, err := BuildLogoutURL(endSessionURL, idToken, postLogoutRedirectURI)
		if err != nil {
			t.logger.Errorf("Failed to build logout URL: %v", err)
			http.Error(rw, "Logout error", http.StatusInternalServerError)
			return
		}
		http.Redirect(rw, req, logoutURL, http.StatusFound)
		return
	}

	http.Redirect(rw, req, localRedirect, http.StatusFound)
}

// BuildLogoutURL constructs a logout URL for the OIDC provider's end session endpoint.
// It includes the ID token hint and post-logout redirect URI according to OIDC specifications.
// Parameters:
//   - endSessionURL: The provider's logout/end session endpoint
//   - idToken: The ID token to include as a hint
//   - postLogoutRedirectURI: Where to redirect after logout
//
// Returns:
//   - The complete logout URL with query parameters
//   - An error if the provided endSessionURL is invalid
func BuildLogoutURL(endSessionURL, idToken, postLogoutRedirectURI string) (string, error) {
	u, err := url.Parse(endSessionURL)
	if err != nil {
		return "", fmt.Errorf("failed to parse end session URL: %w", err)
	}

	q := u.Query()
	q.Set("id_token_hint", idToken)
	if postLogoutRedirectURI != "" {
		q.Set("post_logout_redirect_uri", postLogoutRedirectURI)
	}
	u.RawQuery = q.Encode()

	return u.String(), nil
}

// setOAuthBasicAuth sets the Authorization header per RFC 6749 §2.3.1: the
// client_id and client_secret are form-urlencoded individually, joined with a
// colon, then base64-encoded. This differs from http.Request.SetBasicAuth,
// which skips the form-urlencode step — that matters for credentials with
// reserved characters (`:`, `@`, `+`, `%`, etc.) where the wire format would
// otherwise diverge from what the spec mandates.
func setOAuthBasicAuth(req *http.Request, clientID, clientSecret string) {
	user := url.QueryEscape(clientID)
	pass := url.QueryEscape(clientSecret)
	auth := base64.StdEncoding.EncodeToString([]byte(user + ":" + pass))
	req.Header.Set("Authorization", "Basic "+auth)
}

// deduplicateScopes removes duplicate scopes from a slice while preserving order.
// This ensures that OAuth scope parameters don't contain duplicates which could
// cause issues with some authorization servers.
// The first occurrence of each scope is kept.
func deduplicateScopes(scopes []string) []string {
	if len(scopes) == 0 {
		return []string{}
	}
	seen := make(map[string]struct{})
	result := []string{}
	for _, scope := range scopes {
		if _, ok := seen[scope]; !ok {
			seen[scope] = struct{}{}
			result = append(result, scope)
		}
	}
	return result
}
