package s3api

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"github.com/seaweedfs/seaweedfs/weed/glog"
	"github.com/seaweedfs/seaweedfs/weed/iam/integration"
	"github.com/seaweedfs/seaweedfs/weed/iam/providers"
	"github.com/seaweedfs/seaweedfs/weed/iam/sts"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
	"github.com/seaweedfs/seaweedfs/weed/security"
)

// privateNetworks contains pre-parsed private IP ranges for efficient lookups
var privateNetworks []*net.IPNet

func init() {
	// Private IPv4 ranges (RFC1918) and IPv6 Unique Local Addresses (ULA)
	privateRanges := []string{
		"10.0.0.0/8",     // IPv4 private
		"172.16.0.0/12",  // IPv4 private
		"192.168.0.0/16", // IPv4 private
		"fc00::/7",       // IPv6 Unique Local Addresses (ULA)
	}

	for _, cidr := range privateRanges {
		_, network, err := net.ParseCIDR(cidr)
		if err == nil {
			privateNetworks = append(privateNetworks, network)
		}
	}
}

// IAMIntegration defines the interface for IAM integration
type IAMIntegration interface {
	AuthenticateJWT(ctx context.Context, r *http.Request) (*IAMIdentity, s3err.ErrorCode)
	AuthorizeAction(ctx context.Context, identity *IAMIdentity, action Action, bucket string, objectKey string, r *http.Request) s3err.ErrorCode
	ValidateSessionToken(ctx context.Context, token string) (*sts.SessionInfo, error)
	ValidateTrustPolicyForPrincipal(ctx context.Context, roleArn, principalArn string) error
	DefaultAllow() bool
}

// IAMManagerProvider exposes the IAMManager backing an IAM integration.
type IAMManagerProvider interface {
	GetIAMManager() *integration.IAMManager
}

// S3IAMIntegration provides IAM integration for S3 API
type S3IAMIntegration struct {
	iamManager   *integration.IAMManager
	stsService   *sts.STSService
	filerAddress string
	enabled      bool
}

// NewS3IAMIntegration creates a new S3 IAM integration
func NewS3IAMIntegration(iamManager *integration.IAMManager, filerAddress string) *S3IAMIntegration {
	var stsService *sts.STSService
	if iamManager != nil {
		stsService = iamManager.GetSTSService()
	}

	return &S3IAMIntegration{
		iamManager:   iamManager,
		stsService:   stsService,
		filerAddress: filerAddress,
		enabled:      iamManager != nil,
	}
}

// GetIAMManager returns the IAMManager backing this integration.
func (s3iam *S3IAMIntegration) GetIAMManager() *integration.IAMManager {
	return s3iam.iamManager
}

// AuthenticateJWT authenticates JWT tokens using our STS service
func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Request) (*IAMIdentity, s3err.ErrorCode) {

	if !s3iam.enabled {
		return nil, s3err.ErrNotImplemented
	}

	// Extract bearer token from Authorization header
	authHeader := r.Header.Get("Authorization")
	if !strings.HasPrefix(authHeader, security.BearerPrefix) {
		return nil, s3err.ErrAccessDenied
	}

	sessionToken := strings.TrimPrefix(authHeader, security.BearerPrefix)
	if sessionToken == "" {
		return nil, s3err.ErrAccessDenied
	}

	// Basic token format validation - reject obviously invalid tokens
	if sessionToken == "invalid-token" || len(sessionToken) < 10 {
		glog.V(3).Info("Session token format is invalid")
		return nil, s3err.ErrAccessDenied
	}

	// SECURITY NOTE: ParseJWTToken parses without cryptographic verification
	// This is SAFE because we only use the unverified claims to route to the correct
	// verification method. All code paths below perform full cryptographic verification:
	// - OIDC tokens: validated via validateExternalOIDCToken (line 98)
	// - STS tokens: validated via ValidateSessionToken (line 156)
	// The unverified issuer claim is only used for routing, never for authorization.
	tokenClaims, err := ParseUnverifiedJWTToken(sessionToken)
	if err != nil {
		glog.V(3).Infof("Failed to parse JWT token: %v", err)
		return nil, s3err.ErrAccessDenied
	}

	// Determine token type by issuer claim (more robust than checking role claim)
	// We use the unverified claims ONLY for routing to the correct verification method.
	// We DO NOT use these claims for building the identity.
	issuer, issuerOk := tokenClaims["iss"].(string)
	if !issuerOk {
		glog.V(3).Infof("Token missing issuer claim - invalid JWT")
		return nil, s3err.ErrAccessDenied
	}

	// Check if this is an STS-issued token by examining the issuer
	if !s3iam.isSTSIssuer(issuer) {

		// Not an STS session token, try to validate as OIDC token with timeout
		// Create a context with a reasonable timeout to prevent hanging
		ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
		defer cancel()

		identity, err := s3iam.validateExternalOIDCToken(ctx, sessionToken)

		if err != nil {
			return nil, s3err.ErrAccessDenied
		}

		// Extract role from OIDC identity
		if identity.RoleArn == "" {
			return nil, s3err.ErrAccessDenied
		}

		// Same trust-policy gate as AssumeRoleWithWebIdentity: the role mapping
		// alone must not grant a role that STS would refuse for this token.
		if err := s3iam.iamManager.ValidateTrustPolicyForWebIdentity(ctx, identity.RoleArn, sessionToken, nil); err != nil {
			glog.V(3).Infof("OIDC bearer token rejected by trust policy of role %s: %v", identity.RoleArn, err)
			return nil, s3err.ErrAccessDenied
		}

		// Create claims map and populate with standard claims and attributes
		claims := make(map[string]interface{}, len(identity.Attributes)+5)

		// Add all attributes from the identity to the claims
		// This makes attributes like "preferred_username" available for policy substitution
		for k, v := range identity.Attributes {
			claims[k] = v
		}

		// Add standard OIDC fields to claims so they are available as variables
		// This ensures ${jwt:email}, ${jwt:name}, etc. work as documented in the wiki
		if identity.Email != "" {
			claims["email"] = identity.Email
		}
		if identity.DisplayName != "" {
			claims["name"] = identity.DisplayName
		}
		if len(identity.Groups) > 0 {
			claims["groups"] = identity.Groups
		}

		// Set critical claims explicitly, overwriting any from attributes to ensure correctness.
		claims["sub"] = identity.UserID
		claims["role"] = identity.RoleArn

		// Use real email address if available
		emailAddress := identity.UserID + "@oidc.local"
		if identity.Email != "" {
			emailAddress = identity.Email
		}

		displayName := identity.UserID
		if identity.DisplayName != "" {
			displayName = identity.DisplayName
		}

		// Return IAM identity for OIDC token
		return &IAMIdentity{
			Name:         identity.UserID,
			Principal:    identity.RoleArn,
			SessionToken: sessionToken,
			Account: &Account{
				DisplayName:  displayName,
				EmailAddress: emailAddress,
				Id:           identity.UserID,
			},
			Claims: claims,
		}, s3err.ErrNone
	}

	// This is an STS-issued token - validate with STS service
	// ValidateSessionToken performs cryptographic verification and extraction of trusted claims
	sessionInfo, err := s3iam.stsService.ValidateSessionToken(ctx, sessionToken)
	if err != nil {
		glog.V(3).Infof("STS session validation failed: %v", err)
		return nil, s3err.ErrAccessDenied
	}

	// Create claims map starting with request context (which holds custom claims)
	claims := make(map[string]interface{})
	if sessionInfo.RequestContext != nil {
		for k, v := range sessionInfo.RequestContext {
			claims[k] = v
		}
	}

	// Add standard claims
	claims["sub"] = sessionInfo.Subject
	claims["role"] = sessionInfo.RoleArn
	claims["principal"] = sessionInfo.Principal
	claims["snam"] = sessionInfo.SessionName

	// Create IAM identity from VALIDATED session info
	// We use the trusted data returned by the STS service, not the unverified token claims
	identity := &IAMIdentity{
		Name:         sessionInfo.Subject,
		Principal:    sessionInfo.Principal,
		SessionToken: sessionToken,
		Account: &Account{
			DisplayName:  sessionInfo.SessionName,
			EmailAddress: sessionInfo.Subject + "@seaweedfs.local",
			Id:           sessionInfo.Subject,
		},
		Claims: claims,
	}

	glog.V(3).Infof("JWT authentication successful for principal: %s", identity.Principal)
	return identity, s3err.ErrNone
}

// ValidateSessionToken checks the validity of an STS session token
func (s3iam *S3IAMIntegration) ValidateSessionToken(ctx context.Context, token string) (*sts.SessionInfo, error) {
	if s3iam.stsService == nil {
		return nil, fmt.Errorf("STS service not available")
	}
	return s3iam.stsService.ValidateSessionToken(ctx, token)
}

// AuthorizeAction authorizes actions using our policy engine
func (s3iam *S3IAMIntegration) AuthorizeAction(ctx context.Context, identity *IAMIdentity, action Action, bucket string, objectKey string, r *http.Request) s3err.ErrorCode {
	if !s3iam.enabled {
		return s3err.ErrNone // Fallback to existing authorization
	}

	if identity == nil || identity.Principal == "" {
		return s3err.ErrAccessDenied
	}

	// Extract request context for policy conditions
	requestContext := extractRequestContext(r)

	// For list operations, populate the s3:prefix condition key and ensure the
	// resource ARN stays at bucket level (matching AWS ListBucket semantics).
	// See https://github.com/seaweedfs/seaweedfs/issues/8969
	resourceObjectKey := objectKey
	if action == "List" {
		listPrefix := r.URL.Query().Get("prefix")
		if listPrefix != "" {
			requestContext["s3:prefix"] = listPrefix
		} else if objectKey != "" && objectKey != "/" {
			requestContext["s3:prefix"] = objectKey
		} else {
			requestContext["s3:prefix"] = ""
		}
		resourceObjectKey = ""
	}

	// Build resource ARN for the S3 operation
	resourceArn := buildS3ResourceArn(bucket, resourceObjectKey)

	// Add identity claims to request context for policy variables
	// Only add claim keys if they don't already exist (to avoid overwriting request-derived context)
	if identity.Claims != nil {
		for k, v := range identity.Claims {
			// Only add the claim if this key doesn't already exist in request context
			if _, exists := requestContext[k]; !exists {
				requestContext[k] = v
			}

			// If the claim doesn't have a namespace prefix (e.g. "email"), add "jwt:" prefix
			// This allows ${jwt:email} or ${jwt:preferred_username} to work
			// Only add namespaced version if it doesn't already exist
			if !strings.Contains(k, ":") {
				jwtKey := "jwt:" + k
				if _, exists := requestContext[jwtKey]; !exists {
					requestContext[jwtKey] = v
				}
			}
		}
	}

	// Determine the specific S3 action based on the HTTP request details
	specificAction := ResolveS3Action(r, string(action), bucket, objectKey)

	// Create action request
	actionRequest := &integration.ActionRequest{
		Principal: identity.Principal,
		Action:    specificAction,
		Resource:  resourceArn,
		// Static SigV4 IAM users do not carry a session token. IAMManager
		// evaluates their attached policies directly and only validates STS/OIDC
		// session state when a token is actually present.
		SessionToken:   identity.SessionToken,
		RequestContext: requestContext,
		PolicyNames:    identity.PolicyNames,
	}

	// Check if action is allowed using our policy engine
	allowed, err := s3iam.iamManager.IsActionAllowed(ctx, actionRequest)
	if err != nil {
		return s3err.ErrAccessDenied
	}

	if !allowed {
		return s3err.ErrAccessDenied
	}

	return s3err.ErrNone
}

// ValidateTrustPolicyForPrincipal delegates to IAMManager to validate trust policy
func (s3iam *S3IAMIntegration) ValidateTrustPolicyForPrincipal(ctx context.Context, roleArn, principalArn string) error {
	if s3iam.iamManager == nil {
		return fmt.Errorf("IAM manager not available")
	}
	return s3iam.iamManager.ValidateTrustPolicyForPrincipal(ctx, roleArn, principalArn)
}

// DefaultAllow returns whether access is allowed by default when no policy is found
func (s3iam *S3IAMIntegration) DefaultAllow() bool {
	if s3iam.iamManager == nil {
		return true // Default to true if IAM is not enabled
	}
	return s3iam.iamManager.DefaultAllow()
}

// IAMIdentity represents an authenticated identity with session information
type IAMIdentity struct {
	Name         string
	Principal    string
	SessionToken string
	Account      *Account
	PolicyNames  []string
	Claims       map[string]interface{}
}

// IsAdmin checks if the identity has admin privileges
func (identity *IAMIdentity) IsAdmin() bool {
	// In our IAM system, admin status is determined by policies, not identity
	// This is handled by the policy engine during authorization
	return false
}

// Mock session structures for validation
type MockSessionInfo struct {
	AssumedRoleUser MockAssumedRoleUser
}

type MockAssumedRoleUser struct {
	AssumedRoleId string
	Arn           string
}

// Helper functions

// buildS3ResourceArn builds an S3 resource ARN from bucket and object
func buildS3ResourceArn(bucket string, objectKey string) string {
	// If bucket is already an ARN, return it as-is
	if strings.HasPrefix(bucket, "arn:") {
		return bucket
	}

	if bucket == "" {
		return "arn:aws:s3:::*"
	}

	if objectKey == "" || objectKey == "/" {
		return "arn:aws:s3:::" + bucket
	}

	// Remove leading slash from object key if present
	objectKey = strings.TrimPrefix(objectKey, "/")

	return "arn:aws:s3:::" + bucket + "/" + objectKey
}

// extractRequestContext extracts request context for policy conditions
func extractRequestContext(r *http.Request) map[string]interface{} {
	context := make(map[string]interface{})

	// Extract source IP for IP-based conditions
	// Use AWS-compatible key name for policy variable substitution
	sourceIP := extractSourceIP(r)
	if sourceIP != "" {
		context["aws:SourceIp"] = sourceIP
	}

	// Extract user agent
	if userAgent := r.Header.Get("User-Agent"); userAgent != "" {
		context["userAgent"] = userAgent
	}

	// Extract request time
	context["requestTime"] = r.Context().Value("requestTime")

	// Extract additional headers that might be useful for conditions
	if referer := r.Header.Get("Referer"); referer != "" {
		context["referer"] = referer
	}

	return context
}

// extractSourceIP extracts the real source IP from the request
// SECURITY: Prioritizes RemoteAddr over client-controlled headers to prevent spoofing
// Only trusts X-Forwarded-For/X-Real-IP if RemoteAddr appears to be from a trusted proxy
func extractSourceIP(r *http.Request) string {
	// Always start with RemoteAddr as the most trustworthy source
	remoteIP := r.RemoteAddr
	if ip, _, err := net.SplitHostPort(remoteIP); err == nil {
		remoteIP = ip
	}

	// NOTE: The current heuristic of using isPrivateIP assumes reverse proxies are on a
	// private/local network. This may be insufficient for some cloud, CDN, or multi-tier
	// proxy deployments where proxies terminate connections from public IPs. In such
	// environments, deployment-specific controls (e.g., network ACLs or proxy configs)
	// should be used to ensure only trusted components can set forwarding headers.
	// Future enhancements may introduce an explicit, configurable trusted proxy CIDR list.
	isTrustedProxy := isPrivateIP(remoteIP)

	if isTrustedProxy {
		// Check X-Real-IP header first (single IP, more reliable than X-Forwarded-For)
		if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
			return strings.TrimSpace(realIP)
		}

		// Check X-Forwarded-For header (can contain multiple IPs, take the first one)
		if forwardedFor := r.Header.Get("X-Forwarded-For"); forwardedFor != "" {
			if ips := strings.Split(forwardedFor, ","); len(ips) > 0 {
				return strings.TrimSpace(ips[0])
			}
		}
	}

	// Fall back to RemoteAddr (most secure)
	return remoteIP
}

// isPrivateIP checks if an IP is in a private range (localhost or RFC1918)
func isPrivateIP(ipStr string) bool {
	ip := net.ParseIP(ipStr)
	if ip == nil {
		return false
	}

	// Check for localhost and link-local addresses (IPv4/IPv6)
	if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
		return true
	}

	// Check against pre-parsed private CIDR ranges
	for _, network := range privateNetworks {
		if network.Contains(ip) {
			return true
		}
	}

	return false
}

// ParseUnverifiedJWTToken parses a JWT token and returns its claims WITHOUT cryptographic verification
//
// SECURITY WARNING: This function does NOT validate the token signature!
// It should ONLY be used for:
// 1. Routing tokens to the appropriate verification method (e.g., checking issuer to determine STS vs OIDC)
// 2. Extracting claims for logging/debugging AFTER the token has been cryptographically verified
//
// NEVER use the returned claims for authorization decisions without first calling a proper
// verification function like ValidateSessionToken() or validateExternalOIDCToken().
func ParseUnverifiedJWTToken(tokenString string) (jwt.MapClaims, error) {
	// Parse token without verification to get claims
	// This token IS NOT VERIFIED at this stage.
	// It is only used to peek at claims (like issuer) to determine which verification key/strategy to use.
	token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
	if err != nil {
		return nil, err
	}

	if claims, ok := token.Claims.(jwt.MapClaims); ok {
		return claims, nil
	}

	return nil, fmt.Errorf("invalid token claims")
}

// SetIAMIntegration adds advanced IAM integration to the S3ApiServer
func (s3a *S3ApiServer) SetIAMIntegration(iamManager *integration.IAMManager) {
	if s3a.iam != nil {
		s3a.iam.iamIntegration = NewS3IAMIntegration(iamManager, "localhost:8888")
		glog.V(1).Infof("IAM integration successfully set on S3ApiServer")
	} else {
		glog.Errorf("Cannot set IAM integration: s3a.iam is nil")
	}
}

// OIDCIdentity represents an identity validated through OIDC
type OIDCIdentity struct {
	UserID      string
	RoleArn     string
	Provider    string
	Email       string
	DisplayName string
	Groups      []string
	Attributes  map[string]string
}

// validateExternalOIDCToken validates an external OIDC token using the STS service's secure issuer-based lookup
// This method delegates to the STS service's validateWebIdentityToken for better security and efficiency
func (s3iam *S3IAMIntegration) validateExternalOIDCToken(ctx context.Context, token string) (*OIDCIdentity, error) {

	if s3iam.iamManager == nil {
		return nil, fmt.Errorf("IAM manager not available")
	}

	// Get STS service for secure token validation
	stsService := s3iam.iamManager.GetSTSService()
	if stsService == nil {
		return nil, fmt.Errorf("STS service not available")
	}

	// Use the STS service's secure validateWebIdentityToken method
	// This method uses issuer-based lookup to select the correct provider, which is more secure and efficient
	externalIdentity, provider, err := stsService.ValidateWebIdentityToken(ctx, token)
	if err != nil {
		return nil, fmt.Errorf("token validation failed: %w", err)
	}

	if externalIdentity == nil {
		return nil, fmt.Errorf("authentication succeeded but no identity returned")
	}

	// Extract role from external identity attributes
	rolesAttr, exists := externalIdentity.Attributes["roles"]
	if !exists || rolesAttr == "" {
		glog.V(3).Infof("No roles found in external identity")
		return nil, fmt.Errorf("no roles found in external identity")
	}

	// Parse roles (stored as comma-separated string)
	rolesStr := strings.TrimSpace(rolesAttr)
	roles := strings.Split(rolesStr, ",")

	// Clean up role names
	var cleanRoles []string
	for _, role := range roles {
		cleanRole := strings.TrimSpace(role)
		if cleanRole != "" {
			cleanRoles = append(cleanRoles, cleanRole)
		}
	}

	if len(cleanRoles) == 0 {
		glog.V(3).Infof("Empty roles list after parsing")
		return nil, fmt.Errorf("no valid roles found in token")
	}

	// Determine the primary role using intelligent selection
	roleArn := s3iam.selectPrimaryRole(cleanRoles, externalIdentity)

	return &OIDCIdentity{
		UserID:      externalIdentity.UserID,
		RoleArn:     roleArn,
		Provider:    fmt.Sprintf("%T", provider), // Use provider type as identifier
		Email:       externalIdentity.Email,
		DisplayName: externalIdentity.DisplayName,
		Groups:      externalIdentity.Groups,
		Attributes:  externalIdentity.Attributes,
	}, nil
}

// selectPrimaryRole simply picks the first role from the list
// The OIDC provider should return roles in priority order (most important first)
func (s3iam *S3IAMIntegration) selectPrimaryRole(roles []string, externalIdentity *providers.ExternalIdentity) string {
	if len(roles) == 0 {
		return ""
	}

	// Just pick the first one - keep it simple
	selectedRole := roles[0]
	return selectedRole
}

// isSTSIssuer determines if an issuer belongs to the STS service
// Uses exact match against configured STS issuer for security and correctness
func (s3iam *S3IAMIntegration) isSTSIssuer(issuer string) bool {
	if s3iam.stsService == nil || s3iam.stsService.Config == nil {
		return false
	}

	// Directly compare with the configured STS issuer for exact match
	// This prevents false positives from external OIDC providers that might
	// contain STS-related keywords in their issuer URLs
	return issuer == s3iam.stsService.Config.Issuer
}
