// Package traefikoidc provides OIDC authentication middleware for Traefik.
// This file implements OAuth 2.0 Token Introspection (RFC 7662) for opaque token validation.
package traefikoidc

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
)

// IntrospectionResponse represents the response from an OAuth 2.0 token introspection endpoint.
// Per RFC 7662, this contains information about the token's validity and properties.
type IntrospectionResponse struct {
	Scope     string `json:"scope,omitempty"`
	ClientID  string `json:"client_id,omitempty"`
	Username  string `json:"username,omitempty"`
	TokenType string `json:"token_type,omitempty"`
	Sub       string `json:"sub,omitempty"`
	// Aud holds the introspection audience. Per RFC 7662 it may be a single
	// string or an array of strings, so it is decoded as interface{} and
	// matched with verifyAudience (which handles both shapes).
	Aud    interface{} `json:"aud,omitempty"`
	Iss    string      `json:"iss,omitempty"`
	Jti    string      `json:"jti,omitempty"`
	Exp    int64       `json:"exp,omitempty"`
	Iat    int64       `json:"iat,omitempty"`
	Nbf    int64       `json:"nbf,omitempty"`
	Active bool        `json:"active"`
}

// introspectToken performs OAuth 2.0 Token Introspection (RFC 7662) for an opaque token.
// It queries the provider's introspection endpoint to determine token validity and properties.
// Results are cached to minimize repeated introspection requests.
//
// Parameters:
//   - token: The opaque access token to introspect
//
// Returns:
//   - *IntrospectionResponse: The introspection result
//   - error: Any error that occurred during introspection
func (t *TraefikOidc) introspectToken(token string) (*IntrospectionResponse, error) {
	// Check cache first
	if t.introspectionCache != nil {
		if cached, found := t.introspectionCache.Get(token); found {
			if response, ok := cached.(*IntrospectionResponse); ok {
				t.logger.Debugf("Using cached introspection result for token")
				return response, nil
			}
		}
	}

	// Get introspection URL
	t.metadataMu.RLock()
	introspectionURL := t.introspectionURL
	t.metadataMu.RUnlock()

	if introspectionURL == "" {
		return nil, fmt.Errorf("introspection endpoint not available from provider")
	}

	// Prepare introspection request per RFC 7662 Section 2.1
	data := url.Values{}
	data.Set("token", token)
	data.Set("token_type_hint", "access_token") // Hint that it's an access token

	// Create HTTP request
	req, err := http.NewRequestWithContext(context.Background(), "POST", introspectionURL, strings.NewReader(data.Encode()))
	if err != nil {
		return nil, fmt.Errorf("failed to create introspection request: %w", err)
	}

	// Set required headers
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("Accept", "application/json")

	// Authenticate using client credentials (per RFC 7662 Section 2.1)
	// The introspection endpoint requires authentication
	req.SetBasicAuth(t.clientID, t.clientSecret)

	// Send request with circuit breaker if available
	var resp *http.Response
	if t.errorRecoveryManager != nil {
		t.metadataMu.RLock()
		serviceName := fmt.Sprintf("token-introspection-%s", t.issuerURL)
		t.metadataMu.RUnlock()

		err = t.errorRecoveryManager.ExecuteWithRecovery(context.Background(), serviceName, func() error {
			var reqErr error
			resp, reqErr = t.httpClient.Do(req) //nolint:bodyclose // Body is closed in defer after error check
			if reqErr != nil && resp != nil && resp.Body != nil {
				_ = resp.Body.Close() // Safe to ignore: closing body on error
			}
			return reqErr
		})
	} else {
		resp, err = t.httpClient.Do(req)
	}

	if err != nil {
		if resp != nil && resp.Body != nil {
			_ = resp.Body.Close() // Safe to ignore: closing body on error
		}
		return nil, fmt.Errorf("introspection request failed: %w", err)
	}
	defer func() {
		if resp != nil && resp.Body != nil {
			_, _ = io.Copy(io.Discard, resp.Body) // Safe to ignore: draining body on defer
			_ = resp.Body.Close()                 // Safe to ignore: closing body on defer
		}
	}()

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

	// Parse response per RFC 7662 Section 2.2
	var introspectionResp IntrospectionResponse
	if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&introspectionResp); err != nil {
		return nil, fmt.Errorf("failed to decode introspection response: %w", err)
	}

	// Cache the result
	if t.introspectionCache != nil {
		// Cache for a short duration or until token expiry (whichever is shorter)
		cacheDuration := 5 * time.Minute
		// When introspection is REQUIRED, operators expect near-real-time
		// revocation; cap the positive-result cache so a token revoked at the
		// provider cannot keep passing for the full 5 minutes (rank 8).
		if t.requireTokenIntrospection && cacheDuration > 30*time.Second {
			cacheDuration = 30 * time.Second
		}
		if introspectionResp.Exp > 0 {
			expTime := time.Unix(introspectionResp.Exp, 0)
			untilExp := time.Until(expTime)
			if untilExp > 0 && untilExp < cacheDuration {
				cacheDuration = untilExp
			}
		}
		t.introspectionCache.Set(token, &introspectionResp, cacheDuration)
		t.logger.Debugf("Cached introspection result for %v", cacheDuration)
	}

	return &introspectionResp, nil
}

// validateOpaqueToken validates an opaque access token using token introspection.
// It checks if the token is active, not expired, and has the correct audience if specified.
//
// Parameters:
//   - token: The opaque access token to validate
//
// Returns:
//   - error: Validation error if token is invalid, nil if valid
func (t *TraefikOidc) validateOpaqueToken(token string) error {
	// Check if opaque tokens are allowed
	if !t.allowOpaqueTokens {
		return fmt.Errorf("opaque tokens are not enabled (set allowOpaqueTokens to true)")
	}

	// Check if introspection is required but not available
	t.metadataMu.RLock()
	introspectionURL := t.introspectionURL
	t.metadataMu.RUnlock()

	if introspectionURL == "" {
		if t.requireTokenIntrospection {
			return fmt.Errorf("token introspection required but endpoint not available")
		}
		// Allow fallback to ID token validation
		t.logger.Debugf("Introspection endpoint not available, will rely on ID token validation")
		return nil
	}

	// Perform introspection
	resp, err := t.introspectToken(token)
	if err != nil {
		return fmt.Errorf("token introspection failed: %w", err)
	}

	// Check if token is active (per RFC 7662 Section 2.2)
	if !resp.Active {
		return fmt.Errorf("token is not active (revoked or expired)")
	}

	// Validate expiration if present
	if resp.Exp > 0 {
		expTime := time.Unix(resp.Exp, 0)
		if time.Now().After(expTime) {
			return fmt.Errorf("token has expired")
		}
	}

	// Validate not-before if present
	if resp.Nbf > 0 {
		nbfTime := time.Unix(resp.Nbf, 0)
		if time.Now().Before(nbfTime) {
			return fmt.Errorf("token not yet valid (nbf)")
		}
	}

	// Validate audience if configured. When a distinct API audience is
	// configured (audience != clientID), the introspection response MUST carry
	// a matching audience. Fail closed on a missing or mismatched aud: a token
	// whose audience cannot be confirmed must not be accepted, otherwise a
	// token minted for a different audience would pass. aud may be a single
	// string or an array of strings (RFC 7662); verifyAudience handles both.
	if t.audience != "" && t.audience != t.clientID {
		if resp.Aud == nil {
			return fmt.Errorf("invalid audience: expected %s, introspection response has no audience", t.audience)
		}
		if err := verifyAudience(resp.Aud, t.audience); err != nil {
			return fmt.Errorf("invalid audience: expected %s: %w", t.audience, err)
		}
	}

	t.logger.Debugf("Opaque token validation successful via introspection")
	return nil
}
