package s3api

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/seaweedfs/seaweedfs/weed/glog"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
)

// S3PresignedURLManager handles IAM integration for presigned URLs
type S3PresignedURLManager struct {
	s3iam *S3IAMIntegration
}

// NewS3PresignedURLManager creates a new presigned URL manager with IAM integration
func NewS3PresignedURLManager(s3iam *S3IAMIntegration) *S3PresignedURLManager {
	return &S3PresignedURLManager{
		s3iam: s3iam,
	}
}

// PresignedURLRequest represents a request to generate a presigned URL
type PresignedURLRequest struct {
	Method       string            `json:"method"`        // HTTP method (GET, PUT, POST, DELETE)
	Bucket       string            `json:"bucket"`        // S3 bucket name
	ObjectKey    string            `json:"object_key"`    // S3 object key
	Expiration   time.Duration     `json:"expiration"`    // URL expiration duration
	SessionToken string            `json:"session_token"` // JWT session token for IAM
	Headers      map[string]string `json:"headers"`       // Additional headers to sign
	QueryParams  map[string]string `json:"query_params"`  // Additional query parameters
}

// PresignedURLResponse represents the generated presigned URL
type PresignedURLResponse struct {
	URL            string            `json:"url"`             // The presigned URL
	Method         string            `json:"method"`          // HTTP method
	Headers        map[string]string `json:"headers"`         // Required headers
	ExpiresAt      time.Time         `json:"expires_at"`      // URL expiration time
	SignedHeaders  []string          `json:"signed_headers"`  // List of signed headers
	CanonicalQuery string            `json:"canonical_query"` // Canonical query string
}

// ValidatePresignedURLWithIAM validates a presigned URL request using IAM policies
func (iam *IdentityAccessManagement) ValidatePresignedURLWithIAM(r *http.Request, identity *Identity) s3err.ErrorCode {
	if iam.iamIntegration == nil {
		// Fall back to standard validation
		return s3err.ErrNone
	}

	// Extract bucket and object from request
	bucket, object := s3_constants.GetBucketAndObject(r)

	// Determine the S3 action from HTTP method and path
	action := determineS3ActionFromRequest(r, bucket, object)

	// Check if the user has permission for this action
	ctx := r.Context()
	sessionToken := extractSessionTokenFromPresignedURL(r)
	if sessionToken == "" {
		// No session token in presigned URL - use standard auth
		return s3err.ErrNone
	}

	// Parse JWT token to extract role and session information
	tokenClaims, err := parseJWTToken(sessionToken)
	if err != nil {
		glog.V(3).Infof("Failed to parse JWT token in presigned URL: %v", err)
		return s3err.ErrAccessDenied
	}

	// Extract role information from token claims
	roleName, ok := tokenClaims["role"].(string)
	if !ok || roleName == "" {
		glog.V(3).Info("No role found in JWT token for presigned URL")
		return s3err.ErrAccessDenied
	}

	sessionName, ok := tokenClaims["snam"].(string)
	if !ok || sessionName == "" {
		sessionName = "presigned-session" // Default fallback
	}

	// Use the principal ARN directly from token claims, or build it if not available
	principalArn, ok := tokenClaims["principal"].(string)
	if !ok || principalArn == "" {
		// Fallback: extract role name from role ARN and build principal ARN
		roleNameOnly := roleName
		if strings.Contains(roleName, "/") {
			parts := strings.Split(roleName, "/")
			roleNameOnly = parts[len(parts)-1]
		}
		principalArn = fmt.Sprintf("arn:seaweed:sts::assumed-role/%s/%s", roleNameOnly, sessionName)
	}

	// Create IAM identity for authorization using extracted information
	iamIdentity := &IAMIdentity{
		Name:         identity.Name,
		Principal:    principalArn,
		SessionToken: sessionToken,
		Account:      identity.Account,
	}

	// Authorize using IAM
	errCode := iam.iamIntegration.AuthorizeAction(ctx, iamIdentity, action, bucket, object, r)
	if errCode != s3err.ErrNone {
		glog.V(3).Infof("IAM authorization failed for presigned URL: principal=%s action=%s bucket=%s object=%s",
			iamIdentity.Principal, action, bucket, object)
		return errCode
	}

	glog.V(3).Infof("IAM authorization succeeded for presigned URL: principal=%s action=%s bucket=%s object=%s",
		iamIdentity.Principal, action, bucket, object)
	return s3err.ErrNone
}

// GeneratePresignedURLWithIAM generates a presigned URL with IAM policy validation
func (pm *S3PresignedURLManager) GeneratePresignedURLWithIAM(ctx context.Context, req *PresignedURLRequest, baseURL string) (*PresignedURLResponse, error) {
	if pm.s3iam == nil || !pm.s3iam.enabled {
		return nil, fmt.Errorf("IAM integration not enabled")
	}

	// Validate session token and get identity
	// Use a proper ARN format for the principal
	principalArn := fmt.Sprintf("arn:seaweed:sts::assumed-role/PresignedUser/presigned-session")
	iamIdentity := &IAMIdentity{
		SessionToken: req.SessionToken,
		Principal:    principalArn,
		Name:         "presigned-user",
		Account:      &AccountAdmin,
	}

	// Determine S3 action from method
	action := determineS3ActionFromMethodAndPath(req.Method, req.Bucket, req.ObjectKey)

	// Check IAM permissions before generating URL
	authRequest := &http.Request{
		Method: req.Method,
		URL:    &url.URL{Path: "/" + req.Bucket + "/" + req.ObjectKey},
		Header: make(http.Header),
	}
	authRequest.Header.Set("Authorization", "Bearer "+req.SessionToken)
	authRequest = authRequest.WithContext(ctx)

	errCode := pm.s3iam.AuthorizeAction(ctx, iamIdentity, action, req.Bucket, req.ObjectKey, authRequest)
	if errCode != s3err.ErrNone {
		return nil, fmt.Errorf("IAM authorization failed: user does not have permission for action %s on resource %s/%s", action, req.Bucket, req.ObjectKey)
	}

	// Generate presigned URL with validated permissions
	return pm.generatePresignedURL(req, baseURL, iamIdentity)
}

// generatePresignedURL creates the actual presigned URL
func (pm *S3PresignedURLManager) generatePresignedURL(req *PresignedURLRequest, baseURL string, identity *IAMIdentity) (*PresignedURLResponse, error) {
	// Calculate expiration time
	expiresAt := time.Now().Add(req.Expiration)

	// Build the base URL
	urlPath := "/" + req.Bucket
	if req.ObjectKey != "" {
		urlPath += "/" + req.ObjectKey
	}

	// Create query parameters for AWS signature v4
	queryParams := make(map[string]string)
	for k, v := range req.QueryParams {
		queryParams[k] = v
	}

	// Add AWS signature v4 parameters
	queryParams["X-Amz-Algorithm"] = "AWS4-HMAC-SHA256"
	queryParams["X-Amz-Credential"] = fmt.Sprintf("seaweedfs/%s/us-east-1/s3/aws4_request", expiresAt.Format("20060102"))
	queryParams["X-Amz-Date"] = expiresAt.Format("20060102T150405Z")
	queryParams["X-Amz-Expires"] = strconv.Itoa(int(req.Expiration.Seconds()))
	queryParams["X-Amz-SignedHeaders"] = "host"

	// Add session token if available
	if identity.SessionToken != "" {
		queryParams["X-Amz-Security-Token"] = identity.SessionToken
	}

	// Build canonical query string
	canonicalQuery := buildCanonicalQuery(queryParams)

	// For now, we'll create a mock signature
	// In production, this would use proper AWS signature v4 signing
	mockSignature := generateMockSignature(req.Method, urlPath, canonicalQuery, identity.SessionToken)
	queryParams["X-Amz-Signature"] = mockSignature

	// Build final URL
	finalQuery := buildCanonicalQuery(queryParams)
	fullURL := baseURL + urlPath + "?" + finalQuery

	// Prepare response
	headers := make(map[string]string)
	for k, v := range req.Headers {
		headers[k] = v
	}

	return &PresignedURLResponse{
		URL:            fullURL,
		Method:         req.Method,
		Headers:        headers,
		ExpiresAt:      expiresAt,
		SignedHeaders:  []string{"host"},
		CanonicalQuery: canonicalQuery,
	}, nil
}

// Helper functions

// determineS3ActionFromRequest determines the S3 action based on HTTP request
func determineS3ActionFromRequest(r *http.Request, bucket, object string) Action {
	return determineS3ActionFromMethodAndPath(r.Method, bucket, object)
}

// determineS3ActionFromMethodAndPath determines the S3 action based on method and path
func determineS3ActionFromMethodAndPath(method, bucket, object string) Action {
	switch method {
	case "GET":
		if object == "" {
			return s3_constants.ACTION_LIST // ListBucket
		} else {
			return s3_constants.ACTION_READ // GetObject
		}
	case "PUT", "POST":
		return s3_constants.ACTION_WRITE // PutObject
	case "DELETE":
		if object == "" {
			return s3_constants.ACTION_DELETE_BUCKET // DeleteBucket
		} else {
			return s3_constants.ACTION_WRITE // DeleteObject (uses WRITE action)
		}
	case "HEAD":
		if object == "" {
			return s3_constants.ACTION_LIST // HeadBucket
		} else {
			return s3_constants.ACTION_READ // HeadObject
		}
	default:
		return s3_constants.ACTION_READ // Default to read
	}
}

// extractSessionTokenFromPresignedURL extracts session token from presigned URL query parameters
func extractSessionTokenFromPresignedURL(r *http.Request) string {
	// Check for X-Amz-Security-Token in query parameters
	if token := r.URL.Query().Get("X-Amz-Security-Token"); token != "" {
		return token
	}

	// Check for session token in other possible locations
	if token := r.URL.Query().Get("SessionToken"); token != "" {
		return token
	}

	return ""
}

// buildCanonicalQuery builds a canonical query string for AWS signature
func buildCanonicalQuery(params map[string]string) string {
	var keys []string
	for k := range params {
		keys = append(keys, k)
	}

	// Sort keys for canonical order
	for i := 0; i < len(keys); i++ {
		for j := i + 1; j < len(keys); j++ {
			if keys[i] > keys[j] {
				keys[i], keys[j] = keys[j], keys[i]
			}
		}
	}

	var parts []string
	for _, k := range keys {
		parts = append(parts, fmt.Sprintf("%s=%s", url.QueryEscape(k), url.QueryEscape(params[k])))
	}

	return strings.Join(parts, "&")
}

// generateMockSignature generates a mock signature for testing purposes
func generateMockSignature(method, path, query, sessionToken string) string {
	// This is a simplified signature for demonstration
	// In production, use proper AWS signature v4 calculation
	data := fmt.Sprintf("%s\n%s\n%s\n%s", method, path, query, sessionToken)
	hash := sha256.Sum256([]byte(data))
	return hex.EncodeToString(hash[:])[:16] // Truncate for readability
}

// ValidatePresignedURLExpiration validates that a presigned URL hasn't expired
func ValidatePresignedURLExpiration(r *http.Request) error {
	query := r.URL.Query()

	// Get X-Amz-Date and X-Amz-Expires
	dateStr := query.Get("X-Amz-Date")
	expiresStr := query.Get("X-Amz-Expires")

	if dateStr == "" || expiresStr == "" {
		return fmt.Errorf("missing required presigned URL parameters")
	}

	// Parse date (always in UTC)
	signedDate, err := time.Parse("20060102T150405Z", dateStr)
	if err != nil {
		return fmt.Errorf("invalid X-Amz-Date format: %v", err)
	}

	// Parse expires
	expires, err := strconv.Atoi(expiresStr)
	if err != nil {
		return fmt.Errorf("invalid X-Amz-Expires format: %v", err)
	}

	// Check expiration - compare in UTC
	expirationTime := signedDate.Add(time.Duration(expires) * time.Second)
	now := time.Now().UTC()
	if now.After(expirationTime) {
		return fmt.Errorf("presigned URL has expired")
	}

	return nil
}

// PresignedURLSecurityPolicy represents security constraints for presigned URL generation
type PresignedURLSecurityPolicy struct {
	MaxExpirationDuration time.Duration `json:"max_expiration_duration"` // Maximum allowed expiration
	AllowedMethods        []string      `json:"allowed_methods"`         // Allowed HTTP methods
	RequiredHeaders       []string      `json:"required_headers"`        // Headers that must be present
	IPWhitelist           []string      `json:"ip_whitelist"`            // Allowed IP addresses/ranges
	MaxFileSize           int64         `json:"max_file_size"`           // Maximum file size for uploads
}

// DefaultPresignedURLSecurityPolicy returns a default security policy
func DefaultPresignedURLSecurityPolicy() *PresignedURLSecurityPolicy {
	return &PresignedURLSecurityPolicy{
		MaxExpirationDuration: 7 * 24 * time.Hour, // 7 days max
		AllowedMethods:        []string{"GET", "PUT", "POST", "HEAD"},
		RequiredHeaders:       []string{},
		IPWhitelist:           []string{},             // Empty means no IP restrictions
		MaxFileSize:           5 * 1024 * 1024 * 1024, // 5GB default
	}
}

// ValidatePresignedURLRequest validates a presigned URL request against security policy
func (policy *PresignedURLSecurityPolicy) ValidatePresignedURLRequest(req *PresignedURLRequest) error {
	// Check expiration duration
	if req.Expiration > policy.MaxExpirationDuration {
		return fmt.Errorf("expiration duration %v exceeds maximum allowed %v", req.Expiration, policy.MaxExpirationDuration)
	}

	// Check HTTP method
	methodAllowed := false
	for _, allowedMethod := range policy.AllowedMethods {
		if req.Method == allowedMethod {
			methodAllowed = true
			break
		}
	}
	if !methodAllowed {
		return fmt.Errorf("HTTP method %s is not allowed", req.Method)
	}

	// Check required headers
	for _, requiredHeader := range policy.RequiredHeaders {
		if _, exists := req.Headers[requiredHeader]; !exists {
			return fmt.Errorf("required header %s is missing", requiredHeader)
		}
	}

	return nil
}
