package integration

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"github.com/seaweedfs/seaweedfs/weed/glog"
	"github.com/seaweedfs/seaweedfs/weed/iam/oidc"
	"github.com/seaweedfs/seaweedfs/weed/iam/policy"
	"github.com/seaweedfs/seaweedfs/weed/iam/providers"
	"github.com/seaweedfs/seaweedfs/weed/iam/sts"
	"github.com/seaweedfs/seaweedfs/weed/iam/utils"
	"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
)

// maxPoliciesForEvaluation defines an upper bound on the number of policies that
// will be evaluated for a single request. This protects against pathological or
// malicious inputs that attempt to create extremely large policy lists.
const maxPoliciesForEvaluation = 1024

// IAMManager orchestrates all IAM components
type IAMManager struct {
	stsService           *sts.STSService
	policyEngine         *policy.PolicyEngine
	roleStore            RoleStore
	userStore            UserStore
	oidcProviderStore    OIDCProviderStore
	oidcAuditSink        OIDCProviderAuditSink
	revocationStore      SessionRevocationStore
	filerAddressProvider func() string // Function to get current filer address
	initialized          bool
	runtimePolicyMu      sync.Mutex
	runtimePolicyNames   map[string]struct{}
}

// SetOIDCProviderAuditSink configures the lifecycle event sink. When nil
// (default), GlogAuditSink is used so events still surface in logs.
func (m *IAMManager) SetOIDCProviderAuditSink(sink OIDCProviderAuditSink) {
	m.oidcAuditSink = sink
}

// emitOIDCAudit logs a lifecycle event. Errors are swallowed: an audit
// failure must never block an IAM mutation that has already succeeded.
func (m *IAMManager) emitOIDCAudit(ctx context.Context, eventType OIDCProviderAuditEventType, arn, url string, detail map[string]string) {
	sink := m.oidcAuditSink
	if sink == nil {
		sink = GlogAuditSink{}
	}
	event := &OIDCProviderAuditEvent{
		Type:       eventType,
		ARN:        arn,
		URL:        url,
		Detail:     detail,
		OccurredAt: time.Now().UTC(),
	}
	if err := sink.Emit(ctx, event); err != nil {
		glog.Warningf("OIDC audit emit %s for %s: %v", eventType, arn, err)
	}
}

// SetSessionRevocationStore configures the per-session revocation list. When
// nil, RevokeSession returns an error and IsSessionRevoked is a no-op (every
// session is considered live until natural expiry). Operators who want
// revocation must wire a store explicitly.
func (m *IAMManager) SetSessionRevocationStore(store SessionRevocationStore) {
	m.revocationStore = store
}

// RevokeSession marks a session as revoked using its JTI (which equals the
// session ID for STS-issued tokens).
func (m *IAMManager) RevokeSession(ctx context.Context, jti string, expiresAt time.Time, reason string) error {
	if m.revocationStore == nil {
		return fmt.Errorf("session revocation store not configured")
	}
	if jti == "" {
		return fmt.Errorf("jti cannot be empty")
	}
	return m.revocationStore.Revoke(ctx, m.getFilerAddress(), &RevocationEntry{
		JTI:       jti,
		ExpiresAt: expiresAt,
		Reason:    reason,
	})
}

// IsSessionRevoked returns true if the given JTI has been revoked. Returns
// false (with nil error) when no revocation store is configured.
func (m *IAMManager) IsSessionRevoked(ctx context.Context, jti string) (bool, error) {
	if m.revocationStore == nil || jti == "" {
		return false, nil
	}
	return m.revocationStore.IsRevoked(ctx, m.getFilerAddress(), jti)
}

// PurgeRevokedSessions removes revocation entries whose underlying session
// has already expired. Safe to call on a cron schedule.
func (m *IAMManager) PurgeRevokedSessions(ctx context.Context) (int, error) {
	if m.revocationStore == nil {
		return 0, nil
	}
	return m.revocationStore.Purge(ctx, m.getFilerAddress(), time.Now().UTC())
}

// SetOIDCProviderStore configures the IAM-managed OIDC provider store. When
// nil, OIDC provider IAM actions return ServiceNotReady. The store is the
// source of truth for AssumeRoleWithWebIdentity provider resolution once
// Phase 2b lands; in Phase 2a it is read-only and populated from static
// configuration at boot.
func (m *IAMManager) SetOIDCProviderStore(store OIDCProviderStore) {
	m.oidcProviderStore = store
}

// GetOIDCProviderStore returns the configured store (may be nil).
func (m *IAMManager) GetOIDCProviderStore() OIDCProviderStore {
	return m.oidcProviderStore
}

// GetOIDCProvider returns the record for the given ARN, or an error if the
// store is not configured or the record is missing.
func (m *IAMManager) GetOIDCProvider(ctx context.Context, arn string) (*OIDCProviderRecord, error) {
	if m.oidcProviderStore == nil {
		return nil, fmt.Errorf("OIDC provider store not configured")
	}
	return m.oidcProviderStore.GetProviderByARN(ctx, m.getFilerAddress(), arn)
}

// ListOIDCProviders enumerates all configured OIDC providers.
func (m *IAMManager) ListOIDCProviders(ctx context.Context) ([]*OIDCProviderRecord, error) {
	if m.oidcProviderStore == nil {
		return nil, fmt.Errorf("OIDC provider store not configured")
	}
	return m.oidcProviderStore.ListProviders(ctx, m.getFilerAddress())
}

// CreateOIDCProvider persists a new IAM-managed OIDC provider record. Refuses
// to overwrite an existing record so callers see EntityAlreadyExists semantics.
func (m *IAMManager) CreateOIDCProvider(ctx context.Context, rec *OIDCProviderRecord) error {
	if m.oidcProviderStore == nil {
		return fmt.Errorf("OIDC provider store not configured")
	}
	if rec == nil {
		return fmt.Errorf("record cannot be nil")
	}
	if err := validateOIDCProviderRecord(rec); err != nil {
		return err
	}
	existing, err := m.oidcProviderStore.GetProviderByARN(ctx, m.getFilerAddress(), rec.ARN)
	if err == nil && existing != nil {
		return fmt.Errorf("%w: %s", ErrOIDCProviderAlreadyExists, rec.ARN)
	}
	if err != nil && !errors.Is(err, ErrOIDCProviderNotFound) {
		return fmt.Errorf("lookup existing OIDC provider %q: %w", rec.ARN, err)
	}
	now := time.Now().UTC()
	rec.CreatedAt = now
	rec.UpdatedAt = now
	if err := m.oidcProviderStore.StoreProvider(ctx, m.getFilerAddress(), rec); err != nil {
		return err
	}
	m.refreshOIDCProvidersBestEffort(ctx, "CreateOIDCProvider", rec.ARN)
	m.emitOIDCAudit(ctx, OIDCAuditEventCreated, rec.ARN, rec.URL, nil)
	return nil
}

// DeleteOIDCProvider removes the IAM-managed record. Idempotent.
func (m *IAMManager) DeleteOIDCProvider(ctx context.Context, arn string) error {
	if m.oidcProviderStore == nil {
		return fmt.Errorf("OIDC provider store not configured")
	}
	if err := m.oidcProviderStore.DeleteProvider(ctx, m.getFilerAddress(), arn); err != nil {
		return err
	}
	m.refreshOIDCProvidersBestEffort(ctx, "DeleteOIDCProvider", arn)
	m.emitOIDCAudit(ctx, OIDCAuditEventDeleted, arn, "", nil)
	return nil
}

// AddClientIDToOIDCProvider appends `clientID` to the provider's allowed
// audience list. Adding an existing client ID is a no-op (AWS-compat).
func (m *IAMManager) AddClientIDToOIDCProvider(ctx context.Context, arn, clientID string) error {
	if m.oidcProviderStore == nil {
		return fmt.Errorf("OIDC provider store not configured")
	}
	if clientID == "" {
		return fmt.Errorf("ClientID cannot be empty")
	}
	rec, err := m.oidcProviderStore.GetProviderByARN(ctx, m.getFilerAddress(), arn)
	if err != nil {
		return err
	}
	for _, existing := range rec.ClientIDs {
		if existing == clientID {
			return nil // idempotent
		}
	}
	if len(rec.ClientIDs) >= 100 {
		return fmt.Errorf("ClientIDList must contain at most 100 entries")
	}
	rec.ClientIDs = append(rec.ClientIDs, clientID)
	rec.UpdatedAt = time.Now().UTC()
	if err := m.oidcProviderStore.StoreProvider(ctx, m.getFilerAddress(), rec); err != nil {
		return err
	}
	m.refreshOIDCProvidersBestEffort(ctx, "AddClientIDToOIDCProvider", arn)
	m.emitOIDCAudit(ctx, OIDCAuditEventClientIDAdded, rec.ARN, rec.URL, map[string]string{"clientId": clientID})
	return nil
}

// RemoveClientIDFromOIDCProvider drops `clientID` from the allowed audience
// list. Removing a missing client ID is a no-op.
func (m *IAMManager) RemoveClientIDFromOIDCProvider(ctx context.Context, arn, clientID string) error {
	if m.oidcProviderStore == nil {
		return fmt.Errorf("OIDC provider store not configured")
	}
	rec, err := m.oidcProviderStore.GetProviderByARN(ctx, m.getFilerAddress(), arn)
	if err != nil {
		return err
	}
	pruned := make([]string, 0, len(rec.ClientIDs))
	for _, existing := range rec.ClientIDs {
		if existing != clientID {
			pruned = append(pruned, existing)
		}
	}
	if len(pruned) == len(rec.ClientIDs) {
		return nil // not present; no-op
	}
	rec.ClientIDs = pruned
	rec.UpdatedAt = time.Now().UTC()
	if err := m.oidcProviderStore.StoreProvider(ctx, m.getFilerAddress(), rec); err != nil {
		return err
	}
	m.refreshOIDCProvidersBestEffort(ctx, "RemoveClientIDFromOIDCProvider", arn)
	m.emitOIDCAudit(ctx, OIDCAuditEventClientIDRemoved, rec.ARN, rec.URL, map[string]string{"clientId": clientID})
	return nil
}

// UpdateOIDCProviderThumbprints replaces the entire thumbprint list. AWS
// constrains the list to 1..5 entries when non-empty; we mirror that bound.
func (m *IAMManager) UpdateOIDCProviderThumbprints(ctx context.Context, arn string, thumbprints []string) error {
	if m.oidcProviderStore == nil {
		return fmt.Errorf("OIDC provider store not configured")
	}
	if len(thumbprints) > 5 {
		return fmt.Errorf("ThumbprintList must contain at most 5 entries, got %d", len(thumbprints))
	}
	for _, tp := range thumbprints {
		if !isValidSHA1Thumbprint(tp) {
			return fmt.Errorf("invalid thumbprint %q: must be 40-character SHA-1 hex", tp)
		}
	}
	rec, err := m.oidcProviderStore.GetProviderByARN(ctx, m.getFilerAddress(), arn)
	if err != nil {
		return err
	}
	rec.Thumbprints = append([]string(nil), thumbprints...)
	rec.UpdatedAt = time.Now().UTC()
	if err := m.oidcProviderStore.StoreProvider(ctx, m.getFilerAddress(), rec); err != nil {
		return err
	}
	m.refreshOIDCProvidersBestEffort(ctx, "UpdateOIDCProviderThumbprints", arn)
	m.emitOIDCAudit(ctx, OIDCAuditEventThumbprintsSet, rec.ARN, rec.URL, map[string]string{"count": fmt.Sprintf("%d", len(thumbprints))})
	return nil
}

// TagOIDCProvider merges the supplied tags into the provider's tag set.
func (m *IAMManager) TagOIDCProvider(ctx context.Context, arn string, tags map[string]string) error {
	if m.oidcProviderStore == nil {
		return fmt.Errorf("OIDC provider store not configured")
	}
	rec, err := m.oidcProviderStore.GetProviderByARN(ctx, m.getFilerAddress(), arn)
	if err != nil {
		return err
	}
	if rec.Tags == nil {
		rec.Tags = make(map[string]string, len(tags))
	}
	for k, v := range tags {
		rec.Tags[k] = v
	}
	rec.UpdatedAt = time.Now().UTC()
	if err := m.oidcProviderStore.StoreProvider(ctx, m.getFilerAddress(), rec); err != nil {
		return err
	}
	m.emitOIDCAudit(ctx, OIDCAuditEventTagsAdded, rec.ARN, rec.URL, map[string]string{"count": fmt.Sprintf("%d", len(tags))})
	return nil
}

// UntagOIDCProvider removes the named tags from the provider's tag set.
func (m *IAMManager) UntagOIDCProvider(ctx context.Context, arn string, keys []string) error {
	if m.oidcProviderStore == nil {
		return fmt.Errorf("OIDC provider store not configured")
	}
	rec, err := m.oidcProviderStore.GetProviderByARN(ctx, m.getFilerAddress(), arn)
	if err != nil {
		return err
	}
	for _, k := range keys {
		delete(rec.Tags, k)
	}
	rec.UpdatedAt = time.Now().UTC()
	if err := m.oidcProviderStore.StoreProvider(ctx, m.getFilerAddress(), rec); err != nil {
		return err
	}
	m.emitOIDCAudit(ctx, OIDCAuditEventTagsRemoved, rec.ARN, rec.URL, map[string]string{"count": fmt.Sprintf("%d", len(keys))})
	return nil
}

// validateOIDCProviderRecord enforces the invariants AWS imposes on the
// underlying CreateOpenIDConnectProvider call.
func validateOIDCProviderRecord(rec *OIDCProviderRecord) error {
	if rec.URL == "" {
		return fmt.Errorf("Url is required")
	}
	if rec.ARN == "" {
		return fmt.Errorf("ARN is required")
	}
	if len(rec.ClientIDs) == 0 {
		return fmt.Errorf("ClientIDList must contain at least one entry")
	}
	if len(rec.ClientIDs) > 100 {
		return fmt.Errorf("ClientIDList must contain at most 100 entries")
	}
	if len(rec.Thumbprints) > 5 {
		return fmt.Errorf("ThumbprintList must contain at most 5 entries")
	}
	for _, tp := range rec.Thumbprints {
		if !isValidSHA1Thumbprint(tp) {
			return fmt.Errorf("invalid thumbprint %q: must be 40-character SHA-1 hex", tp)
		}
	}
	return nil
}

// isValidSHA1Thumbprint returns true iff `s` is exactly 40 hex characters,
// matching the SHA-1 digest format AWS expects.
func isValidSHA1Thumbprint(s string) bool {
	if len(s) != 40 {
		return false
	}
	for _, r := range s {
		switch {
		case r >= '0' && r <= '9':
		case r >= 'a' && r <= 'f':
		case r >= 'A' && r <= 'F':
		default:
			return false
		}
	}
	return true
}

// IAMConfig holds configuration for all IAM components
type IAMConfig struct {
	// STS service configuration
	STS *sts.STSConfig `json:"sts"`

	// Policy engine configuration
	Policy *policy.PolicyEngineConfig `json:"policy"`

	// Role store configuration
	Roles *RoleStoreConfig `json:"roleStore"`

	// OIDCProviders configures the IAM-managed OIDC provider store. Optional;
	// if absent the manager defaults to an in-memory store hydrated from
	// STS.Providers at boot.
	OIDCProviders *OIDCProviderStoreConfig `json:"oidcProviderStore,omitempty"`
}

// OIDCProviderStoreConfig holds OIDC provider store configuration.
type OIDCProviderStoreConfig struct {
	StoreType   string                 `json:"storeType"` // memory, filer
	StoreConfig map[string]interface{} `json:"storeConfig,omitempty"`
}

// RoleStoreConfig holds role store configuration
type RoleStoreConfig struct {
	// StoreType specifies the role store backend (memory, filer, etc.)
	StoreType string `json:"storeType"`

	// StoreConfig contains store-specific configuration
	StoreConfig map[string]interface{} `json:"storeConfig,omitempty"`
}

// UserStore defines the interface for retrieving IAM user policy attachments.
type UserStore interface {
	GetUser(ctx context.Context, username string) (*iam_pb.Identity, error)
}

// RoleDefinition defines a role with its trust policy and attached policies
type RoleDefinition struct {
	// RoleName is the name of the role
	RoleName string `json:"roleName"`

	// RoleArn is the full ARN of the role
	RoleArn string `json:"roleArn"`

	// TrustPolicy defines who can assume this role
	TrustPolicy *policy.PolicyDocument `json:"trustPolicy"`

	// AttachedPolicies lists the policy names attached to this role
	AttachedPolicies []string `json:"attachedPolicies"`

	// Description is an optional description of the role
	Description string `json:"description,omitempty"`

	// MaxSessionDuration is the upper bound (in seconds) on session length when
	// callers assume this role. Zero means "use the global STS default". When
	// set it must satisfy AWS bounds: 3600 ≤ MaxSessionDuration ≤ 43200.
	// Honoured by AssumeRole, AssumeRoleWithWebIdentity, AssumeRoleWithCredentials.
	MaxSessionDuration int64 `json:"maxSessionDuration,omitempty"`
}

// ActionRequest represents a request to perform an action
type ActionRequest struct {
	// Principal is the entity performing the action
	Principal string `json:"principal"`

	// Action is the action being requested
	Action string `json:"action"`

	// Resource is the resource being accessed
	Resource string `json:"resource"`

	// SessionToken for temporary credential validation
	SessionToken string `json:"sessionToken"`

	// RequestContext contains additional request information
	RequestContext map[string]interface{} `json:"requestContext,omitempty"`

	// PolicyNames to evaluate (overrides role-based policies if present)
	PolicyNames []string `json:"policyNames,omitempty"`
}

// NewIAMManager creates a new IAM manager
func NewIAMManager() *IAMManager {
	return &IAMManager{}
}

// SetUserStore assigns the user store used to resolve IAM user policy attachments.
func (m *IAMManager) SetUserStore(store UserStore) {
	m.userStore = store
}

// SyncRuntimePolicies keeps zero-config runtime policies available to the
// in-memory policy engine used by the advanced IAM authorizer.
func (m *IAMManager) SyncRuntimePolicies(ctx context.Context, policies []*iam_pb.Policy) error {
	if !m.initialized || m.policyEngine == nil {
		return nil
	}
	if m.policyEngine.StoreType() != sts.StoreTypeMemory {
		return nil
	}

	desiredPolicies := make(map[string]*policy.PolicyDocument, len(policies))
	for _, runtimePolicy := range policies {
		if runtimePolicy == nil || runtimePolicy.Name == "" {
			continue
		}

		var document policy.PolicyDocument
		if err := json.Unmarshal([]byte(runtimePolicy.Content), &document); err != nil {
			// Drop just this one: aborting here would leave every other policy
			// unsynced. Leaving it out of desiredPolicies also deletes it from
			// the engine below, which is the point — a policy whose stored
			// definition no longer parses must stop granting access rather than
			// keep enforcing a document the operator can no longer see.
			glog.Warningf("skipping unparsable runtime policy %q: %v", runtimePolicy.Name, err)
			continue
		}

		desiredPolicies[runtimePolicy.Name] = &document
	}

	m.runtimePolicyMu.Lock()
	defer m.runtimePolicyMu.Unlock()

	filerAddress := m.getFilerAddress()
	for policyName := range m.runtimePolicyNames {
		if _, keep := desiredPolicies[policyName]; keep {
			continue
		}
		if err := m.policyEngine.DeletePolicy(ctx, filerAddress, policyName); err != nil {
			return fmt.Errorf("failed to delete runtime policy %q: %w", policyName, err)
		}
	}

	for policyName, document := range desiredPolicies {
		if err := m.policyEngine.AddPolicy(filerAddress, policyName, document); err != nil {
			return fmt.Errorf("failed to sync runtime policy %q: %w", policyName, err)
		}
	}

	m.runtimePolicyNames = make(map[string]struct{}, len(desiredPolicies))
	for policyName := range desiredPolicies {
		m.runtimePolicyNames[policyName] = struct{}{}
	}

	return nil
}

// Initialize initializes the IAM manager with all components
func (m *IAMManager) Initialize(config *IAMConfig, filerAddressProvider func() string) error {
	if config == nil {
		return fmt.Errorf("config cannot be nil")
	}

	// Store the filer address provider function
	m.filerAddressProvider = filerAddressProvider

	// Initialize STS service
	m.stsService = sts.NewSTSService()
	if err := m.stsService.Initialize(config.STS); err != nil {
		return fmt.Errorf("failed to initialize STS service: %w", err)
	}

	// CRITICAL SECURITY: Set trust policy validator to ensure proper role assumption validation
	m.stsService.SetTrustPolicyValidator(m)

	// Initialize policy engine
	m.policyEngine = policy.NewPolicyEngine()
	if err := m.policyEngine.InitializeWithProvider(config.Policy, m.filerAddressProvider); err != nil {
		return fmt.Errorf("failed to initialize policy engine: %w", err)
	}

	// Initialize role store
	roleStore, err := m.createRoleStoreWithProvider(config.Roles, m.filerAddressProvider)
	if err != nil {
		return fmt.Errorf("failed to initialize role store: %w", err)
	}
	m.roleStore = roleStore

	// Initialize OIDC provider store and hydrate from static configuration so
	// the read-only IAM API can return the same providers the STS service
	// already accepts. Mutations will land in Phase 2b.
	if err := m.initOIDCProviderStore(config); err != nil {
		return fmt.Errorf("failed to initialize OIDC provider store: %w", err)
	}

	m.initialized = true
	return nil
}

// initOIDCProviderStore creates the OIDC provider store and seeds it from the
// static STS provider configuration. The static path remains the bootstrap
// source: each enabled OIDC entry under STS.Providers is mirrored as an
// OIDCProviderRecord so the IAM API surfaces the same set the STS service
// validates against.
func (m *IAMManager) initOIDCProviderStore(config *IAMConfig) error {
	store, err := m.createOIDCProviderStore(config.OIDCProviders)
	if err != nil {
		return err
	}
	m.oidcProviderStore = store

	if config.STS == nil {
		return nil
	}
	for _, pc := range config.STS.Providers {
		if pc == nil || !pc.Enabled || pc.Type != sts.ProviderTypeOIDC {
			continue
		}
		issuer, _ := pc.Config["issuer"].(string)
		if issuer == "" {
			glog.Warningf("OIDC provider %s in static config has empty issuer; skipping mirror to store", pc.Name)
			continue
		}
		accountID := ""
		if config.STS != nil {
			accountID = config.STS.AccountId
		}
		arn, err := DeriveOIDCProviderARN(accountID, issuer)
		if err != nil {
			glog.Warningf("derive ARN for static OIDC provider %s: %v", pc.Name, err)
			continue
		}
		clientIDs := extractClientIDs(pc.Config)
		ctx := context.Background()
		// Preserve CreatedAt across reboots when a persistent store already
		// has this provider — IAM's GetOpenIDConnectProvider response
		// shouldn't shift its CreateDate every time the server restarts.
		now := time.Now().UTC()
		createdAt := now
		if existing, err := store.GetProviderByARN(ctx, m.getFilerAddress(), arn); err == nil && existing != nil && !existing.CreatedAt.IsZero() {
			createdAt = existing.CreatedAt
		}
		rec := &OIDCProviderRecord{
			AccountID:               accountID,
			ARN:                     arn,
			URL:                     issuer,
			ClientIDs:               clientIDs,
			Thumbprints:             extractStringList(pc.Config, "thumbprints"),
			AllowedPrincipalTagKeys: extractStringList(pc.Config, "allowedPrincipalTagKeys"),
			PolicyClaim:             extractString(pc.Config, "policyClaim"),
			CreatedAt:               createdAt,
			UpdatedAt:               now,
		}
		if err := store.StoreProvider(ctx, m.getFilerAddress(), rec); err != nil {
			glog.Warningf("mirror static OIDC provider %s into store: %v", pc.Name, err)
		}
	}
	return nil
}

// refreshOIDCProvidersBestEffort calls RefreshOIDCProvidersFromStore and
// logs a warning on failure. The IAM API call has already succeeded by the
// time we get here, so a refresh failure must not surface to the caller —
// the worst case is that the local instance keeps the stale runtime view
// until a peer's metadata-subscribe event triggers another refresh.
func (m *IAMManager) refreshOIDCProvidersBestEffort(ctx context.Context, op, arn string) {
	if err := m.RefreshOIDCProvidersFromStore(ctx); err != nil {
		glog.Warningf("refresh OIDC providers after %s on %s: %v", op, arn, err)
	}
}

// RefreshOIDCProvidersFromStore reloads every OIDCProviderRecord from the
// configured store and pushes the resulting runtime providers into the STS
// service so AssumeRoleWithWebIdentity sees the latest set without a
// restart. Safe to call when no store is configured (returns nil) and when
// the store is empty (clears the IAM-managed map). Records with empty URLs
// or invalid configuration are logged and skipped so a single bad entry
// does not stop the rest from refreshing.
func (m *IAMManager) RefreshOIDCProvidersFromStore(ctx context.Context) error {
	if m.oidcProviderStore == nil || m.stsService == nil {
		return nil
	}
	records, err := m.oidcProviderStore.ListProviders(ctx, m.getFilerAddress())
	if err != nil {
		return fmt.Errorf("list OIDC providers: %w", err)
	}
	byIssuer := make(map[string][]sts.ScopedOIDCProvider, len(records))
	for _, rec := range records {
		if rec == nil || rec.URL == "" {
			continue
		}
		provider, err := buildOIDCProviderFromRecord(rec)
		if err != nil {
			glog.Warningf("skip refreshing OIDC provider %s: %v", rec.ARN, err)
			continue
		}
		// Multiple records may share an issuer when each is scoped to a
		// different account; STS picks the right one at validation time
		// based on the role being assumed. See lookupOIDCProviderForAccount.
		byIssuer[rec.URL] = append(byIssuer[rec.URL], sts.ScopedOIDCProvider{
			AccountID: rec.AccountID,
			Provider:  provider,
		})
	}
	m.stsService.SetIAMManagedOIDCProviders(byIssuer)
	return nil
}

// buildOIDCProviderFromRecord turns a stored record into a runtime
// OIDCProvider. The provider name is the ARN so re-registration is
// idempotent and never collides with static-config entries.
func buildOIDCProviderFromRecord(rec *OIDCProviderRecord) (*oidc.OIDCProvider, error) {
	if rec == nil {
		return nil, fmt.Errorf("record cannot be nil")
	}
	cfg := &oidc.OIDCConfig{
		Issuer:      rec.URL,
		ClientIDs:   append([]string(nil), rec.ClientIDs...),
		Thumbprints: append([]string(nil), rec.Thumbprints...),
	}
	provider := oidc.NewOIDCProvider(rec.ARN)
	if err := provider.Initialize(cfg); err != nil {
		return nil, err
	}
	return provider, nil
}

// createOIDCProviderStore selects an OIDCProviderStore implementation. Defaults
// to memory; "filer" requires a filerAddressProvider to be configured.
func (m *IAMManager) createOIDCProviderStore(cfg *OIDCProviderStoreConfig) (OIDCProviderStore, error) {
	if cfg == nil || cfg.StoreType == "" || cfg.StoreType == "memory" {
		return NewMemoryOIDCProviderStore(), nil
	}
	if cfg.StoreType == "filer" {
		return NewFilerOIDCProviderStore(cfg.StoreConfig, m.filerAddressProvider), nil
	}
	return nil, fmt.Errorf("unsupported OIDC provider store type: %s", cfg.StoreType)
}

// extractClientIDs reads a single clientId or a clientIds list from the
// provider's static config map. Mirrors the OIDCConfig schema.
func extractClientIDs(cfg map[string]interface{}) []string {
	if cfg == nil {
		return nil
	}
	if list, ok := cfg["clientIds"].([]interface{}); ok {
		out := make([]string, 0, len(list))
		for _, v := range list {
			if s, ok := v.(string); ok && s != "" {
				out = append(out, s)
			}
		}
		if len(out) > 0 {
			return out
		}
	}
	if id, ok := cfg["clientId"].(string); ok && id != "" {
		return []string{id}
	}
	return nil
}

// extractStringList reads a JSON string array out of the provider's static
// config map and returns the non-empty entries. Returns nil when the key is
// missing, the value is the wrong shape, or every entry is empty.
func extractStringList(cfg map[string]interface{}, key string) []string {
	if cfg == nil {
		return nil
	}
	list, ok := cfg[key].([]interface{})
	if !ok {
		return nil
	}
	out := make([]string, 0, len(list))
	for _, v := range list {
		if s, ok := v.(string); ok && s != "" {
			out = append(out, s)
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

// extractString reads a single string field from the provider's static
// config map; missing or non-string values produce "".
func extractString(cfg map[string]interface{}, key string) string {
	if cfg == nil {
		return ""
	}
	s, _ := cfg[key].(string)
	return s
}

// getFilerAddress returns the current filer address using the provider function
func (m *IAMManager) getFilerAddress() string {
	if m.filerAddressProvider != nil {
		return m.filerAddressProvider()
	}
	return "" // Fallback to empty string if no provider is set
}

// createRoleStoreWithProvider creates a role store with a filer address provider function
func (m *IAMManager) createRoleStoreWithProvider(config *RoleStoreConfig, filerAddressProvider func() string) (RoleStore, error) {
	if config == nil {
		// Default to generic cached filer role store when no config provided
		return NewGenericCachedRoleStore(nil, filerAddressProvider)
	}

	switch config.StoreType {
	case "", "filer":
		// Check if caching is explicitly disabled
		if config.StoreConfig != nil {
			if noCache, ok := config.StoreConfig["noCache"].(bool); ok && noCache {
				return NewFilerRoleStore(config.StoreConfig, filerAddressProvider)
			}
		}
		// Default to generic cached filer store for better performance
		return NewGenericCachedRoleStore(config.StoreConfig, filerAddressProvider)
	case "cached-filer", "generic-cached":
		return NewGenericCachedRoleStore(config.StoreConfig, filerAddressProvider)
	case "memory":
		return NewMemoryRoleStore(), nil
	default:
		return nil, fmt.Errorf("unsupported role store type: %s", config.StoreType)
	}
}

// RegisterIdentityProvider registers an identity provider
func (m *IAMManager) RegisterIdentityProvider(provider providers.IdentityProvider) error {
	if !m.initialized {
		return fmt.Errorf("IAM manager not initialized")
	}

	return m.stsService.RegisterProvider(provider)
}

// CreatePolicy creates a new policy
func (m *IAMManager) CreatePolicy(ctx context.Context, filerAddress string, name string, policyDoc *policy.PolicyDocument) error {
	if !m.initialized {
		return fmt.Errorf("IAM manager not initialized")
	}

	return m.policyEngine.AddPolicy(filerAddress, name, policyDoc)
}

// CreateRole creates a new role with trust policy and attached policies
func (m *IAMManager) CreateRole(ctx context.Context, filerAddress string, roleName string, roleDef *RoleDefinition) error {
	if !m.initialized {
		return fmt.Errorf("IAM manager not initialized")
	}

	if roleName == "" {
		return fmt.Errorf("role name cannot be empty")
	}

	if roleDef == nil {
		return fmt.Errorf("role definition cannot be nil")
	}

	// Set role ARN if not provided
	if roleDef.RoleArn == "" {
		roleDef.RoleArn = fmt.Sprintf("arn:aws:iam::role/%s", roleName)
	}

	// Validate trust policy
	if roleDef.TrustPolicy != nil {
		if err := policy.ValidateTrustPolicyDocument(roleDef.TrustPolicy); err != nil {
			return fmt.Errorf("invalid trust policy: %w", err)
		}
	}

	// Validate per-role MaxSessionDuration if specified. AWS bounds: 1h..12h.
	if roleDef.MaxSessionDuration != 0 {
		if roleDef.MaxSessionDuration < 3600 || roleDef.MaxSessionDuration > 43200 {
			return fmt.Errorf("MaxSessionDuration must be between 3600 and 43200 seconds, got %d", roleDef.MaxSessionDuration)
		}
	}

	// Store role definition
	return m.roleStore.StoreRole(ctx, "", roleName, roleDef)
}

// GetRole retrieves a role definition by name.
func (m *IAMManager) GetRole(ctx context.Context, roleName string) (*RoleDefinition, error) {
	if !m.initialized {
		return nil, fmt.Errorf("IAM manager not initialized")
	}
	if roleName == "" {
		return nil, fmt.Errorf("role name cannot be empty")
	}

	return m.roleStore.GetRole(ctx, m.getFilerAddress(), roleName)
}

// UpdateBucketPolicy updates the policy for a bucket
func (m *IAMManager) UpdateBucketPolicy(ctx context.Context, bucketName string, policyJSON []byte) error {
	if !m.initialized {
		return fmt.Errorf("IAM manager not initialized")
	}

	if bucketName == "" {
		return fmt.Errorf("bucket name cannot be empty")
	}

	// Parse the policy document handled by the IAM policy engine
	var policyDoc policy.PolicyDocument
	if err := json.Unmarshal(policyJSON, &policyDoc); err != nil {
		return fmt.Errorf("invalid policy JSON: %w", err)
	}

	// Store the policy with a special prefix to distinguish from IAM policies
	policyName := "bucket-policy:" + bucketName
	return m.policyEngine.AddPolicy(m.getFilerAddress(), policyName, &policyDoc)
}

// EnsureBucketPolicy stores the policy for a bucket only when no mirror is
// stored yet, backfilling policies that predate the IAM integration (the
// metadata subscription only sees changes). A present mirror is left alone,
// so repeat calls cost one cached read. Returns whether a write happened,
// so the caller can reconcile a write that raced a concurrent change.
func (m *IAMManager) EnsureBucketPolicy(ctx context.Context, bucketName string, policyJSON []byte) (bool, error) {
	if !m.initialized {
		return false, fmt.Errorf("IAM manager not initialized")
	}

	if bucketName == "" {
		return false, fmt.Errorf("bucket name cannot be empty")
	}

	if existing, err := m.policyEngine.GetPolicy(ctx, m.getFilerAddress(), "bucket-policy:"+bucketName); err == nil && existing != nil {
		return false, nil
	}

	if err := m.UpdateBucketPolicy(ctx, bucketName, policyJSON); err != nil {
		return false, err
	}
	return true, nil
}

// RemoveBucketPolicy deletes the stored policy for a bucket. Removing a
// policy that was never stored is a success.
func (m *IAMManager) RemoveBucketPolicy(ctx context.Context, bucketName string) error {
	if !m.initialized {
		return fmt.Errorf("IAM manager not initialized")
	}

	if bucketName == "" {
		return fmt.Errorf("bucket name cannot be empty")
	}

	return m.policyEngine.DeletePolicy(ctx, m.getFilerAddress(), "bucket-policy:"+bucketName)
}

// AssumeRoleWithWebIdentity assumes a role using web identity (OIDC)
func (m *IAMManager) AssumeRoleWithWebIdentity(ctx context.Context, request *sts.AssumeRoleWithWebIdentityRequest) (*sts.AssumeRoleResponse, error) {
	if !m.initialized {
		return nil, fmt.Errorf("IAM manager not initialized")
	}

	// Claim-based mode bypasses role lookup and trust policy entirely; the
	// STS service handles the policy resolution from the JWT itself. Account
	// scoping still applies but at the provider-resolution layer in Phase 3c
	// (we'll plug that in once a multi-account assume path lands).
	if sts.IsClaimBasedPolicyRoleArn(request.RoleArn) {
		return m.stsService.AssumeRoleWithWebIdentity(ctx, request)
	}

	// Extract role name from ARN
	roleName := utils.ExtractRoleNameFromArn(request.RoleArn)

	// Get role definition
	roleDef, err := m.roleStore.GetRole(ctx, m.getFilerAddress(), roleName)
	if err != nil {
		return nil, fmt.Errorf("role not found: %s", roleName)
	}

	// Account scoping: when the role lives in account A, the OIDC provider
	// validating the token must be either global (AccountID="") or also live
	// in account A. Skip when we can't resolve account context; fall through
	// to the existing trust-policy enforcement.
	if err := m.enforceProviderAccountScope(ctx, request); err != nil {
		return nil, err
	}

	// Validate trust policy before allowing STS to assume the role
	if err := m.validateTrustPolicyForWebIdentity(ctx, roleDef, request.WebIdentityToken, request.DurationSeconds); err != nil {
		return nil, fmt.Errorf("trust policy validation failed: %w", err)
	}

	// Apply role-level MaxSessionDuration cap. The STS service still applies
	// the global MaxSessionLength and the source-token-expiry cap on top of
	// this; per-role takes precedence whenever it is the tightest bound.
	request.DurationSeconds = capDurationByRole(request.DurationSeconds, roleDef.MaxSessionDuration)

	// Use STS service to assume the role
	return m.stsService.AssumeRoleWithWebIdentity(ctx, request)
}

// enforceProviderAccountScope checks that the OIDC provider matching the
// token's issuer is registered in either the role's account or as a global
// (account-less) provider. Returns nil when no provider store is configured
// (preserves the static-config-only path) or when the issuer is not known to
// the store (the existing STS-layer issuer→provider map handles that case
// during validation).
func (m *IAMManager) enforceProviderAccountScope(ctx context.Context, request *sts.AssumeRoleWithWebIdentityRequest) error {
	if m.oidcProviderStore == nil {
		return nil
	}
	roleAccount := utils.ParseRoleARN(request.RoleArn).AccountID
	if roleAccount == "" {
		// Legacy ARN form (no account): nothing to enforce.
		return nil
	}
	issuer, err := extractIssuerFromJWT(request.WebIdentityToken)
	if err != nil {
		return nil // signature validation will reject, no need to fail twice
	}
	// Look for a provider that is either global or scoped to the role's
	// account. Multiple providers may share an issuer (e.g. one global plus
	// one per tenant), and GetProviderByIssuer returns the first match
	// arbitrarily — using it here would falsely reject a valid request
	// whenever the wrong record happens to come back first.
	if _, err := m.oidcProviderStore.GetProviderByIssuerAndAccount(ctx, m.getFilerAddress(), issuer, roleAccount); err == nil {
		return nil
	}
	// No allowed match. Distinguish "issuer entirely unknown" (let the STS
	// layer reject with the existing not-registered error) from "issuer is
	// registered but only in a different account" (surface a precise error
	// so the operator knows the call was cross-account).
	if other, err := m.oidcProviderStore.GetProviderByIssuer(ctx, m.getFilerAddress(), issuer); err == nil {
		return fmt.Errorf("OIDC provider for issuer %s is registered in account %s; cannot be used to assume a role in account %s", issuer, other.AccountID, roleAccount)
	}
	return nil
}

// extractIssuerFromJWT returns the iss claim of a JWT without verifying its
// signature. Safe here because the caller still goes through full signature
// + issuer validation in the STS service; this is purely for routing.
func extractIssuerFromJWT(token string) (string, error) {
	parser := new(jwt.Parser)
	parsed, _, err := parser.ParseUnverified(token, jwt.MapClaims{})
	if err != nil {
		return "", err
	}
	claims, ok := parsed.Claims.(jwt.MapClaims)
	if !ok {
		return "", fmt.Errorf("invalid claims")
	}
	iss, _ := claims["iss"].(string)
	if iss == "" {
		return "", fmt.Errorf("token has no iss claim")
	}
	return iss, nil
}

// capDurationByRole returns the requested duration clamped to the role's
// MaxSessionDuration. A nil requested duration is left nil so the STS
// service's calculateSessionDuration applies the global default (typically
// 1 hour) — substituting the role's max here would silently mint a 12h
// session for any caller who omitted DurationSeconds, which AWS does not
// do. The role-max upper bound still applies in the downstream cap chain
// once the request has a concrete duration.
func capDurationByRole(requested *int64, roleMax int64) *int64 {
	if roleMax <= 0 || requested == nil {
		return requested
	}
	if *requested > roleMax {
		v := roleMax
		return &v
	}
	return requested
}

// AssumeRoleWithCredentials assumes a role using credentials (LDAP)
func (m *IAMManager) AssumeRoleWithCredentials(ctx context.Context, request *sts.AssumeRoleWithCredentialsRequest) (*sts.AssumeRoleResponse, error) {
	if !m.initialized {
		return nil, fmt.Errorf("IAM manager not initialized")
	}

	// Extract role name from ARN
	roleName := utils.ExtractRoleNameFromArn(request.RoleArn)

	// Get role definition
	roleDef, err := m.roleStore.GetRole(ctx, m.getFilerAddress(), roleName)
	if err != nil {
		return nil, fmt.Errorf("role not found: %s", roleName)
	}

	// Validate trust policy
	if err := m.validateTrustPolicyForCredentials(ctx, roleDef, request); err != nil {
		return nil, fmt.Errorf("trust policy validation failed: %w", err)
	}

	// Apply role-level MaxSessionDuration cap.
	request.DurationSeconds = capDurationByRole(request.DurationSeconds, roleDef.MaxSessionDuration)

	// Use STS service to assume the role
	return m.stsService.AssumeRoleWithCredentials(ctx, request)
}

// IsActionAllowed checks if a principal is allowed to perform an action on a resource
func (m *IAMManager) IsActionAllowed(ctx context.Context, request *ActionRequest) (bool, error) {
	if !m.initialized {
		return false, fmt.Errorf("IAM manager not initialized")
	}

	// Validate session token if present
	// We always try to validate with the internal STS service first if it's a SeaweedFS token.
	// This ensures that session policies embedded in the token are correctly extracted and enforced.
	var sessionInfo *sts.SessionInfo
	if request.SessionToken != "" {
		// Parse unverified to check issuer
		parsed, _, err := new(jwt.Parser).ParseUnverified(request.SessionToken, jwt.MapClaims{})
		isInternal := false
		if err == nil {
			if claims, ok := parsed.Claims.(jwt.MapClaims); ok {
				if issuer, ok := claims["iss"].(string); ok && m.stsService != nil && m.stsService.Config != nil {
					if issuer == m.stsService.Config.Issuer {
						isInternal = true
					}
				}
			}
		}

		if isInternal || !isOIDCToken(request.SessionToken) {
			var err error
			sessionInfo, err = m.stsService.ValidateSessionToken(ctx, request.SessionToken)
			if err != nil {
				return false, fmt.Errorf("invalid session: %w", err)
			}
			// Reject sessions whose JTI has been added to the revocation
			// blocklist. SessionId == JTI for STS-issued tokens; this lookup
			// is hot, so the store implementation must be O(1).
			if sessionInfo != nil && sessionInfo.SessionId != "" {
				revoked, rerr := m.IsSessionRevoked(ctx, sessionInfo.SessionId)
				if rerr != nil {
					return false, fmt.Errorf("revocation check failed: %w", rerr)
				}
				if revoked {
					return false, fmt.Errorf("session has been revoked")
				}
			}
		}
	}

	// Create evaluation context
	evalCtx := &policy.EvaluationContext{
		Principal:      request.Principal,
		Action:         request.Action,
		Resource:       request.Resource,
		RequestContext: request.RequestContext,
	}

	// Ensure RequestContext exists and populate with principal info
	if evalCtx.RequestContext == nil {
		evalCtx.RequestContext = make(map[string]interface{})
	}
	// Add principal to context for policy matching
	// The PolicyEngine checks RequestContext["principal"] or RequestContext["aws:PrincipalArn"]
	evalCtx.RequestContext["principal"] = request.Principal
	evalCtx.RequestContext["aws:PrincipalArn"] = request.Principal // AWS standard key

	// Check if this is an admin request - bypass policy evaluation if so
	// This mirrors the logic in auth_signature_v4.go but applies it at authorization time
	isAdmin := false
	if request.RequestContext != nil {
		if val, ok := request.RequestContext["is_admin"].(bool); ok && val {
			isAdmin = true
		}
		// Print full request context for debugging
	}

	// Parse principal ARN to extract details for context variables (e.g. ${aws:username})
	arnInfo := utils.ParsePrincipalARN(request.Principal)
	if arnInfo.RoleName != "" {
		// For assumed roles, AWS docs say aws:username IS the role name.
		// However, for user isolation in these tests, we typically map the session name (the user who assumed the role) to aws:username.
		// arn:aws:sts::account:assumed-role/RoleName/SessionName
		awsUsername := arnInfo.RoleName
		if idx := strings.LastIndex(request.Principal, "/"); idx != -1 && idx < len(request.Principal)-1 {
			awsUsername = request.Principal[idx+1:]
		}

		evalCtx.RequestContext["aws:username"] = awsUsername
		evalCtx.RequestContext["aws:userid"] = arnInfo.RoleName
	} else if userName := utils.ExtractUserNameFromPrincipal(request.Principal); userName != "" {
		evalCtx.RequestContext["aws:username"] = userName
		evalCtx.RequestContext["aws:userid"] = userName
	}
	if arnInfo.AccountID != "" {
		evalCtx.RequestContext["aws:PrincipalAccount"] = arnInfo.AccountID
	}

	// Determine if there is a bucket policy to evaluate
	var bucketPolicyName string
	if strings.HasPrefix(request.Resource, "arn:aws:s3:::") {
		resourcePath := request.Resource[13:] // remove "arn:aws:s3:::"
		parts := strings.SplitN(resourcePath, "/", 2)
		if len(parts) > 0 && parts[0] != "" {
			bucketPolicyName = "bucket-policy:" + parts[0]
		}
	}

	var baseResult *policy.EvaluationResult
	var err error
	// hasManagedSubject is true once we've resolved the principal to a registered
	// IAM user or role (or the caller has supplied PolicyNames directly). For a
	// managed subject, "no matching statement" must deny — the DefaultEffect=Allow
	// fallback is only meant for the unmanaged zero-config startup case.
	hasManagedSubject := false

	if isAdmin {
		// Admin always has base access allowed
		baseResult = &policy.EvaluationResult{Effect: policy.EffectAllow}
	} else {
		policies := request.PolicyNames
		if len(policies) > 0 {
			hasManagedSubject = true
		}
		if len(policies) == 0 {
			// Extract role name from principal ARN
			roleName := utils.ExtractRoleNameFromPrincipal(request.Principal)
			if roleName == "" {
				userName := utils.ExtractUserNameFromPrincipal(request.Principal)
				if userName == "" {
					return false, fmt.Errorf("could not extract role from principal: %s", request.Principal)
				}
				if m.userStore == nil {
					return false, fmt.Errorf("user store unavailable for principal: %s", request.Principal)
				}
				user, err := m.userStore.GetUser(ctx, userName)
				if err != nil || user == nil {
					return false, fmt.Errorf("user not found for principal: %s (user=%s)", request.Principal, userName)
				}
				hasManagedSubject = true
				policies = user.GetPolicyNames()
			} else {
				// Get role definition
				roleDef, err := m.roleStore.GetRole(ctx, m.getFilerAddress(), roleName)
				if err != nil {
					return false, fmt.Errorf("role not found: %s", roleName)
				}

				hasManagedSubject = true
				policies = roleDef.AttachedPolicies
			}
		}
		if bucketPolicyName != "" {
			// Enforce an upper bound on the number of policies to avoid excessive allocations
			if len(policies) >= maxPoliciesForEvaluation {
				return false, fmt.Errorf("too many policies for evaluation: %d >= %d", len(policies), maxPoliciesForEvaluation)
			}
			// Create a new slice to avoid modifying the original and append the bucket policy
			copied := make([]string, len(policies))
			copy(copied, policies)
			policies = append(copied, bucketPolicyName)
		}

		baseResult, err = m.policyEngine.Evaluate(ctx, "", evalCtx, policies)
		if err != nil {
			return false, fmt.Errorf("policy evaluation failed: %w", err)
		}
	}

	// Base policy must allow; if it doesn't, deny immediately (session policy can only further restrict)
	if baseResult.Effect != policy.EffectAllow {
		return false, nil
	}

	// Zero-config IAM uses DefaultEffect=Allow to preserve open-by-default behavior
	// for requests without any subject policies. Once we resolve the principal to
	// a registered IAM user or role (or the caller hands us policy names),
	// "no matching statement" must fall back to deny — otherwise a freshly
	// created user with zero policies would inherit full access.
	if hasManagedSubject && len(baseResult.MatchingStatements) == 0 {
		return false, nil
	}

	// If there's a session policy, it must also allow the action
	if sessionInfo != nil && sessionInfo.SessionPolicy != "" {
		var sessionPolicy policy.PolicyDocument
		if err := json.Unmarshal([]byte(sessionInfo.SessionPolicy), &sessionPolicy); err != nil {
			return false, fmt.Errorf("invalid session policy JSON: %w", err)
		}
		if err := policy.ValidatePolicyDocument(&sessionPolicy); err != nil {
			return false, fmt.Errorf("invalid session policy document: %w", err)
		}
		sessionResult, err := m.policyEngine.EvaluatePolicyDocument(ctx, evalCtx, "session-policy", &sessionPolicy, policy.EffectDeny)
		if err != nil {
			return false, fmt.Errorf("session policy evaluation failed: %w", err)
		}
		if sessionResult.Effect != policy.EffectAllow {
			// Session policy does not allow this action
			return false, nil
		}
	}

	return true, nil
}

// IsPrincipalActionExplicitlyDenied reports whether the action on the resource is
// explicitly denied for the principal by either the named policies or, for a
// chained STS caller, the inline session policy carried by sessionToken. Unlike
// IsActionAllowed it does not require an allow — the absence of a matching
// statement is not a denial. Used to enforce AWS deny-always-wins when the allow
// is granted elsewhere (e.g. a role trust policy for sts:AssumeRole).
//
// A chained session that fails validation or has been revoked yields an error so
// callers fail closed. Raw OIDC tokens are skipped here — they are validated on
// the JWT path, not by the STS service.
func (m *IAMManager) IsPrincipalActionExplicitlyDenied(ctx context.Context, principal, action, resource string, policyNames []string, sessionToken string, requestContext map[string]interface{}) (bool, error) {
	if !m.initialized {
		return false, fmt.Errorf("IAM manager not initialized")
	}

	if requestContext == nil {
		requestContext = make(map[string]interface{})
	}
	requestContext["principal"] = principal
	requestContext["aws:PrincipalArn"] = principal

	evalCtx := &policy.EvaluationContext{
		Principal:      principal,
		Action:         action,
		Resource:       resource,
		RequestContext: requestContext,
	}

	// Base policies: the caller's attached identity policies, or for a chained
	// caller the assumed role's attached policies.
	if len(policyNames) > 0 {
		result, err := m.policyEngine.Evaluate(ctx, "", evalCtx, policyNames)
		if err != nil {
			return false, fmt.Errorf("policy evaluation failed: %w", err)
		}
		if hasExplicitDeny(result.MatchingStatements) {
			return true, nil
		}
	}

	// A chained STS caller's session restricts what it may do. Skip raw OIDC
	// tokens (validated on the JWT path); for our own session tokens, reject a
	// revoked session and honor an explicit Deny in the inline session policy.
	if sessionToken != "" && m.stsService != nil && !isOIDCToken(sessionToken) {
		sessionInfo, err := m.stsService.ValidateSessionToken(ctx, sessionToken)
		if err != nil {
			return false, fmt.Errorf("session validation failed: %w", err)
		}
		if sessionInfo != nil && sessionInfo.SessionId != "" {
			revoked, rerr := m.IsSessionRevoked(ctx, sessionInfo.SessionId)
			if rerr != nil {
				return false, fmt.Errorf("revocation check failed: %w", rerr)
			}
			if revoked {
				return false, fmt.Errorf("session has been revoked")
			}
		}
		if sessionInfo != nil && sessionInfo.SessionPolicy != "" {
			var sessionPolicy policy.PolicyDocument
			if err := json.Unmarshal([]byte(sessionInfo.SessionPolicy), &sessionPolicy); err != nil {
				return false, fmt.Errorf("invalid session policy JSON: %w", err)
			}
			result, err := m.policyEngine.EvaluatePolicyDocument(ctx, evalCtx, "session-policy", &sessionPolicy, policy.EffectDeny)
			if err != nil {
				return false, fmt.Errorf("session policy evaluation failed: %w", err)
			}
			if hasExplicitDeny(result.MatchingStatements) {
				return true, nil
			}
		}
	}
	return false, nil
}

// hasExplicitDeny reports whether any matched statement is a Deny.
func hasExplicitDeny(matches []policy.StatementMatch) bool {
	for _, stmt := range matches {
		if stmt.Effect == policy.EffectDeny {
			return true
		}
	}
	return false
}

// ValidateTrustPolicy validates if a principal can assume a role (for testing)
func (m *IAMManager) ValidateTrustPolicy(ctx context.Context, roleArn, provider, userID string) bool {
	roleName := utils.ExtractRoleNameFromArn(roleArn)
	roleDef, err := m.roleStore.GetRole(ctx, m.getFilerAddress(), roleName)
	if err != nil {
		return false
	}

	// Simple validation based on provider in trust policy
	if roleDef.TrustPolicy != nil {
		for _, statement := range roleDef.TrustPolicy.Statement {
			if statement.Effect == "Allow" {
				if principal, ok := statement.Principal.(map[string]interface{}); ok {
					if federated, ok := principal["Federated"].(string); ok {
						// For OIDC, check against issuer URL
						if provider == "oidc" && federated == "test-oidc" {
							return true
						}
						// For LDAP, check against test-ldap
						if provider == "ldap" && federated == "test-ldap" {
							return true
						}
						// Also check for wildcard
						if federated == "*" {
							return true
						}
					}
				}
			}
		}
	}

	return false
}

// validateTrustPolicyForWebIdentity validates trust policy for OIDC assumption
func (m *IAMManager) validateTrustPolicyForWebIdentity(ctx context.Context, roleDef *RoleDefinition, webIdentityToken string, durationSeconds *int64) error {
	if roleDef.TrustPolicy == nil {
		return fmt.Errorf("role has no trust policy")
	}

	// Create evaluation context for trust policy validation
	requestContext := make(map[string]interface{})

	// Try to parse as JWT first, fallback to mock token handling
	tokenClaims, err := parseJWTTokenForTrustPolicy(webIdentityToken)
	if err != nil {
		// If JWT parsing fails, this might be a mock token (like "valid-oidc-token")
		// For mock tokens, we'll use default values that match the trust policy expectations
		requestContext["aws:FederatedProvider"] = "test-oidc"
		requestContext["oidc:iss"] = "test-oidc"
		// This ensures aws:userid key is populated even for mock tokens if needed
		requestContext["aws:userid"] = "mock-user"
		requestContext["oidc:sub"] = "mock-user"
	} else {
		// Add standard context values from JWT claims that trust policies might check
		// See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#condition-keys-web-identity-federation

		// The issuer is the federated provider for OIDC
		if iss, ok := tokenClaims["iss"].(string); ok {
			// Default to issuer URL
			requestContext["aws:FederatedProvider"] = iss
			requestContext["oidc:iss"] = iss

			// Try to resolve provider name from issuer for better policy matching
			// This allows policies to reference the provider name (e.g. "keycloak") instead of the full issuer URL
			if m.stsService != nil {
				for name, provider := range m.stsService.GetProviders() {
					if oidcProvider, ok := provider.(interface{ GetIssuer() string }); ok {
						confIssuer := oidcProvider.GetIssuer()

						if confIssuer == iss {
							requestContext["aws:FederatedProvider"] = name
							break
						}
					}
				}
			}
		}

		if sub, ok := tokenClaims["sub"].(string); ok {
			requestContext["oidc:sub"] = sub
			// Map subject to aws:userid as well for compatibility
			requestContext["aws:userid"] = sub
		}
		if aud, ok := tokenClaims["aud"].(string); ok {
			requestContext["oidc:aud"] = aud
		}
		// Custom claims can be prefixed if needed, but for "be 100% compatible with AWS",
		// we should rely on standard OIDC claims.

		// Add all other claims with oidc: prefix to support custom claims in trust policies
		// This enables checking claims like "oidc:roles", "oidc:groups", "oidc:email", etc.
		for k, v := range tokenClaims {
			// Skip claims we've already handled explicitly or shouldn't expose
			if k == "iss" || k == "sub" || k == "aud" {
				continue
			}

			// Add with oidc: prefix
			requestContext["oidc:"+k] = v
		}
	}

	// Add DurationSeconds to context if provided
	if durationSeconds != nil {
		requestContext["sts:DurationSeconds"] = *durationSeconds
	}

	// Create evaluation context for trust policy
	evalCtx := &policy.EvaluationContext{
		Principal:      "web-identity-user", // Placeholder principal for trust policy evaluation
		Action:         "sts:AssumeRoleWithWebIdentity",
		Resource:       roleDef.RoleArn,
		RequestContext: requestContext,
	}

	// Evaluate the trust policy directly
	if !m.evaluateTrustPolicy(roleDef.TrustPolicy, evalCtx) {
		return fmt.Errorf("trust policy denies web identity assumption")
	}

	return nil
}

// validateTrustPolicyForCredentials validates trust policy for credential assumption
func (m *IAMManager) validateTrustPolicyForCredentials(ctx context.Context, roleDef *RoleDefinition, request *sts.AssumeRoleWithCredentialsRequest) error {
	if roleDef.TrustPolicy == nil {
		return fmt.Errorf("role has no trust policy")
	}

	// Check if trust policy allows credential assumption for the specific provider
	for _, statement := range roleDef.TrustPolicy.Statement {
		if statement.Effect == "Allow" {
			for _, action := range statement.Action {
				if action == "sts:AssumeRoleWithCredentials" {
					if principal, ok := statement.Principal.(map[string]interface{}); ok {
						if federated, ok := principal["Federated"].(string); ok {
							if federated == request.ProviderName {
								return nil // Allow
							}
						}
					}
				}
			}
		}
	}

	return fmt.Errorf("trust policy does not allow credential assumption for provider: %s", request.ProviderName)
}

// Helper functions

// ExpireSessionForTesting manually expires a session for testing purposes
func (m *IAMManager) ExpireSessionForTesting(ctx context.Context, sessionToken string) error {
	if !m.initialized {
		return fmt.Errorf("IAM manager not initialized")
	}

	return m.stsService.ExpireSessionForTesting(ctx, sessionToken)
}

// GetPoliciesForUser returns the policy names attached to an IAM user.
// Returns an error if the user store is not configured or the lookup fails,
// so callers can fail closed on policy-resolution failures.
func (m *IAMManager) GetPoliciesForUser(ctx context.Context, username string) ([]string, error) {
	if m.userStore == nil {
		return nil, fmt.Errorf("user store not configured")
	}
	user, err := m.userStore.GetUser(ctx, username)
	if err != nil {
		return nil, fmt.Errorf("failed to look up user %q: %w", username, err)
	}
	if user == nil {
		return nil, nil
	}
	return user.PolicyNames, nil
}

// GetSTSService returns the STS service instance
func (m *IAMManager) GetSTSService() *sts.STSService {
	return m.stsService
}

// DefaultAllow returns whether the default effect is Allow
func (m *IAMManager) DefaultAllow() bool {
	if !m.initialized || m.policyEngine == nil {
		return true // Default to true if not initialized
	}
	return m.policyEngine.DefaultAllow()
}

// parseJWTTokenForTrustPolicy parses a JWT token to extract claims for trust policy evaluation
func parseJWTTokenForTrustPolicy(tokenString string) (map[string]interface{}, error) {
	// Simple JWT parsing without verification (for trust policy context only)
	// In production, this should use proper JWT parsing with signature verification
	parts := strings.Split(tokenString, ".")
	if len(parts) != 3 {
		return nil, fmt.Errorf("invalid JWT format")
	}

	// Decode the payload (second part)
	payload := parts[1]
	// Add padding if needed
	for len(payload)%4 != 0 {
		payload += "="
	}

	decoded, err := base64.URLEncoding.DecodeString(payload)
	if err != nil {
		return nil, fmt.Errorf("failed to decode JWT payload: %w", err)
	}

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

	return claims, nil
}

// evaluateTrustPolicy evaluates a trust policy against the evaluation context
// Now delegates to PolicyEngine for unified policy evaluation
func (m *IAMManager) evaluateTrustPolicy(trustPolicy *policy.PolicyDocument, evalCtx *policy.EvaluationContext) bool {
	if trustPolicy == nil {
		return false
	}

	// Use the PolicyEngine to evaluate the trust policy
	// The PolicyEngine now handles Principal, Action, Resource, and Condition matching
	result, err := m.policyEngine.EvaluateTrustPolicy(context.Background(), trustPolicy, evalCtx)
	if err != nil {
		return false
	}

	return result.Effect == policy.EffectAllow
}

// evaluateTrustPolicyConditions and evaluatePrincipalValue have been removed
// Trust policy evaluation is now handled entirely by PolicyEngine.EvaluateTrustPolicy()

// isOIDCToken checks if a token is an OIDC JWT token (vs STS session token)
func isOIDCToken(token string) bool {
	// JWT tokens have three parts separated by dots and start with base64-encoded JSON
	parts := strings.Split(token, ".")
	if len(parts) != 3 {
		return false
	}

	// JWT tokens typically start with "eyJ" (base64 encoded JSON starting with "{")
	if !strings.HasPrefix(token, "eyJ") {
		return false
	}

	parsed, _, err := new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{})
	if err != nil {
		return false
	}

	claims, ok := parsed.Claims.(jwt.MapClaims)
	if !ok {
		return false
	}

	if typ, ok := claims["typ"].(string); ok && typ == sts.TokenTypeSession {
		return false
	}
	if typ, ok := claims[sts.JWTClaimTokenType].(string); ok && typ == sts.TokenTypeSession {
		return false
	}

	return true
}

// TrustPolicyValidator interface implementation
// These methods allow the IAMManager to serve as the trust policy validator for the STS service

// ValidateTrustPolicyForWebIdentity implements the TrustPolicyValidator interface
func (m *IAMManager) ValidateTrustPolicyForWebIdentity(ctx context.Context, roleArn string, webIdentityToken string, durationSeconds *int64) error {
	if !m.initialized {
		return fmt.Errorf("IAM manager not initialized")
	}

	// Extract role name from ARN
	roleName := utils.ExtractRoleNameFromArn(roleArn)

	// Get role definition
	roleDef, err := m.roleStore.GetRole(ctx, m.getFilerAddress(), roleName)
	if err != nil {
		return fmt.Errorf("role not found: %s", roleName)
	}

	// Use existing trust policy validation logic
	return m.validateTrustPolicyForWebIdentity(ctx, roleDef, webIdentityToken, durationSeconds)
}

// ValidateTrustPolicyForCredentials implements the TrustPolicyValidator interface
func (m *IAMManager) ValidateTrustPolicyForCredentials(ctx context.Context, roleArn string, identity *providers.ExternalIdentity) error {
	if !m.initialized {
		return fmt.Errorf("IAM manager not initialized")
	}

	// Extract role name from ARN
	roleName := utils.ExtractRoleNameFromArn(roleArn)

	// Get role definition
	roleDef, err := m.roleStore.GetRole(ctx, m.getFilerAddress(), roleName)
	if err != nil {
		return fmt.Errorf("role not found: %s", roleName)
	}

	// For credentials, we need to create a mock request to reuse existing validation
	// This is a bit of a hack, but it allows us to reuse the existing logic
	mockRequest := &sts.AssumeRoleWithCredentialsRequest{
		ProviderName: identity.Provider, // Use the provider name from the identity
	}

	// Use existing trust policy validation logic
	return m.validateTrustPolicyForCredentials(ctx, roleDef, mockRequest)
}
