package providers

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

	"github.com/seaweedfs/seaweedfs/weed/glog"
	"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
)

// IdentityProvider defines the interface for external identity providers
type IdentityProvider interface {
	// Name returns the unique name of the provider
	Name() string

	// Initialize initializes the provider with configuration
	Initialize(config interface{}) error

	// Authenticate authenticates a user with a token and returns external identity
	Authenticate(ctx context.Context, token string) (*ExternalIdentity, error)

	// GetUserInfo retrieves user information by user ID
	GetUserInfo(ctx context.Context, userID string) (*ExternalIdentity, error)

	// ValidateToken validates a token and returns claims
	ValidateToken(ctx context.Context, token string) (*TokenClaims, error)
}

// ExternalIdentity represents an identity from an external provider
type ExternalIdentity struct {
	// UserID is the unique identifier from the external provider
	UserID string `json:"userId"`

	// Email is the user's email address
	Email string `json:"email"`

	// DisplayName is the user's display name
	DisplayName string `json:"displayName"`

	// Groups are the groups the user belongs to
	Groups []string `json:"groups,omitempty"`

	// Attributes are additional user attributes
	Attributes map[string]string `json:"attributes,omitempty"`

	// Provider is the name of the identity provider
	Provider string `json:"provider"`

	// Issuer is the OIDC `iss` claim (or equivalent) from the source token.
	// Stable per (provider, identity) and used together with UserID to derive
	// a stable parent-user hash that survives token rotation.
	Issuer string `json:"issuer,omitempty"`

	// PrincipalTags are key/value pairs extracted from the AWS principal-tags
	// namespace claim (`https://aws.amazon.com/tags/principal_tags`). They are
	// surfaced as `aws:PrincipalTag/<key>` in the policy request context
	// (subject to per-provider allowlist filtering at the STS layer).
	PrincipalTags map[string]string `json:"principalTags,omitempty"`

	// ClaimPolicies are policy names pulled from the provider-configured
	// PolicyClaim. Empty when the provider isn't running in claim-based
	// policy mode or the claim was absent.
	ClaimPolicies []string `json:"claimPolicies,omitempty"`

	// TokenExpiration is the expiration time of the source identity token
	// This is used to limit session duration to not exceed the token's exp claim
	TokenExpiration *time.Time `json:"tokenExpiration,omitempty"`
}

// Validate validates the external identity structure
func (e *ExternalIdentity) Validate() error {
	if e.UserID == "" {
		return fmt.Errorf("user ID is required")
	}

	if e.Provider == "" {
		return fmt.Errorf("provider is required")
	}

	if e.Email != "" {
		if _, err := mail.ParseAddress(e.Email); err != nil {
			return fmt.Errorf("invalid email format: %w", err)
		}
	}

	return nil
}

// TokenClaims represents claims from a validated token
type TokenClaims struct {
	// Subject (sub) - user identifier
	Subject string `json:"sub"`

	// Issuer (iss) - token issuer
	Issuer string `json:"iss"`

	// Audience (aud) - intended audience
	Audience string `json:"aud"`

	// ExpiresAt (exp) - expiration time
	ExpiresAt time.Time `json:"exp"`

	// IssuedAt (iat) - issued at time
	IssuedAt time.Time `json:"iat"`

	// NotBefore (nbf) - not valid before time
	NotBefore time.Time `json:"nbf,omitempty"`

	// Claims are additional claims from the token
	Claims map[string]interface{} `json:"claims,omitempty"`
}

// IsValid checks if the token claims are valid (not expired, etc.)
func (c *TokenClaims) IsValid() bool {
	now := time.Now()

	// Check expiration
	if !c.ExpiresAt.IsZero() && now.After(c.ExpiresAt) {
		return false
	}

	// Check not before
	if !c.NotBefore.IsZero() && now.Before(c.NotBefore) {
		return false
	}

	// Check issued at (shouldn't be in the future)
	if !c.IssuedAt.IsZero() && now.Before(c.IssuedAt) {
		return false
	}

	return true
}

// GetClaimString returns a string claim value
func (c *TokenClaims) GetClaimString(key string) (string, bool) {
	if value, exists := c.Claims[key]; exists {
		if str, ok := value.(string); ok {
			return str, true
		}
	}
	return "", false
}

// GetClaimStringSlice returns a string slice claim value
func (c *TokenClaims) GetClaimStringSlice(key string) ([]string, bool) {
	if value, exists := c.Claims[key]; exists {
		switch v := value.(type) {
		case []string:
			return v, true
		case []interface{}:
			var result []string
			for _, item := range v {
				if str, ok := item.(string); ok {
					result = append(result, str)
				}
			}
			return result, len(result) > 0
		case string:
			// Single string can be treated as slice
			return []string{v}, true
		}
	}
	return nil, false
}

// ProviderConfig represents configuration for identity providers
type ProviderConfig struct {
	// Type of provider (oidc, ldap, saml)
	Type string `json:"type"`

	// Name of the provider instance
	Name string `json:"name"`

	// Enabled indicates if the provider is active
	Enabled bool `json:"enabled"`

	// Config is provider-specific configuration
	Config map[string]interface{} `json:"config"`

	// RoleMapping defines how to map external identities to roles
	RoleMapping *RoleMapping `json:"roleMapping,omitempty"`
}

// RoleMapping defines rules for mapping external identities to roles
type RoleMapping struct {
	// Rules are the mapping rules
	Rules []MappingRule `json:"rules"`

	// DefaultRole is assigned if no rules match
	DefaultRole string `json:"defaultRole,omitempty"`
}

// MappingRule defines a single mapping rule
type MappingRule struct {
	// Claim is the claim key to check
	Claim string `json:"claim"`

	// Value is the expected claim value (supports wildcards)
	Value string `json:"value"`

	// Role is the role ARN to assign
	Role string `json:"role"`

	// Condition is additional condition logic (optional)
	Condition string `json:"condition,omitempty"`
}

// Matches checks if a rule matches the given claims
func (r *MappingRule) Matches(claims *TokenClaims) bool {
	if r.Claim == "" || r.Value == "" {
		glog.V(3).Infof("Rule invalid: claim=%s, value=%s", r.Claim, r.Value)
		return false
	}

	claimValue, exists := claims.GetClaimString(r.Claim)
	if !exists {
		glog.V(3).Infof("Claim '%s' not found as string, trying as string slice", r.Claim)
		// Try as string slice
		if claimSlice, sliceExists := claims.GetClaimStringSlice(r.Claim); sliceExists {
			glog.V(3).Infof("Claim '%s' found as string slice: %v", r.Claim, claimSlice)
			for _, val := range claimSlice {
				glog.V(3).Infof("Checking if '%s' matches rule value '%s'", val, r.Value)
				if r.matchValue(val) {
					glog.V(3).Infof("Match found: '%s' matches '%s'", val, r.Value)
					return true
				}
			}
		} else {
			glog.V(3).Infof("Claim '%s' not found in any format", r.Claim)
		}
		return false
	}

	glog.V(3).Infof("Claim '%s' found as string: '%s'", r.Claim, claimValue)
	return r.matchValue(claimValue)
}

// matchValue checks if a value matches the rule value (with wildcard support)
// Uses AWS IAM-compliant case-insensitive wildcard matching for consistency with policy engine
func (r *MappingRule) matchValue(value string) bool {
	matched := wildcard.MatchesWildcard(strings.ToLower(r.Value), strings.ToLower(value))
	glog.V(3).Infof("AWS IAM pattern match result: '%s' matches '%s' = %t", value, r.Value, matched)
	return matched
}
