package credential

import (
	"context"
	"fmt"
	"os"
	"strings"
	"sync"

	"github.com/seaweedfs/seaweedfs/weed/glog"
	"github.com/seaweedfs/seaweedfs/weed/pb"
	"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
	"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
	"github.com/seaweedfs/seaweedfs/weed/util"
	"github.com/seaweedfs/seaweedfs/weed/wdclient"
	"google.golang.org/grpc"
	"google.golang.org/protobuf/encoding/protojson"
)

// FilerAddressSetter is an interface for credential stores that need a dynamic filer address
type FilerAddressSetter interface {
	SetFilerAddressFunc(getFiler func() pb.ServerAddress, grpcDialOption grpc.DialOption)
}

// CredentialManager manages user credentials using a configurable store
type CredentialManager struct {
	Store CredentialStore
	// staticMu protects staticIdentities and staticNames, which are written
	// by SetStaticIdentities (startup + config reload) and read concurrently
	// by LoadConfiguration, SaveConfiguration, GetStaticUsernames, and IsStaticIdentity.
	staticMu sync.RWMutex
	// staticIdentities holds identities loaded from a static config file (-s3.config).
	// These are included in LoadConfiguration so that listing operations
	// return all configured identities, not just dynamic ones from the store.
	staticIdentities []*iam_pb.Identity
	staticNames      map[string]bool
}

// NewCredentialManager creates a new credential manager with the specified store
func NewCredentialManager(storeName CredentialStoreTypeName, configuration util.Configuration, prefix string) (*CredentialManager, error) {
	var store CredentialStore

	// Find the requested store implementation
	for _, s := range Stores {
		if s.GetName() == storeName {
			store = s
			break
		}
	}

	if store == nil {
		return nil, fmt.Errorf("credential store '%s' not found. Available stores: %s",
			storeName, getAvailableStores())
	}

	// Initialize the store
	if err := store.Initialize(configuration, prefix); err != nil {
		return nil, fmt.Errorf("failed to initialize credential store '%s': %v", storeName, err)
	}

	return &CredentialManager{
		Store: store,
	}, nil
}

func (cm *CredentialManager) SetMasterClient(masterClient *wdclient.MasterClient, grpcDialOption grpc.DialOption) {
	cm.Store = NewPropagatingCredentialStore(cm.Store, masterClient, grpcDialOption)
}

// SetFilerAddressFunc sets the function to get the current filer address
func (cm *CredentialManager) SetFilerAddressFunc(getFiler func() pb.ServerAddress, grpcDialOption grpc.DialOption) {
	if s, ok := cm.Store.(FilerAddressSetter); ok {
		s.SetFilerAddressFunc(getFiler, grpcDialOption)
	}
}

// GetStore returns the underlying credential store
func (cm *CredentialManager) GetStore() CredentialStore {
	return cm.Store
}

// GetStoreName returns the name of the underlying credential store
func (cm *CredentialManager) GetStoreName() string {
	if cm.Store != nil {
		return string(cm.Store.GetName())
	}
	return ""
}

// SetStaticIdentities registers identities loaded from a static config file.
// These identities are included in LoadConfiguration and ListUsers results
// but are never persisted to the dynamic store.
func (cm *CredentialManager) SetStaticIdentities(identities []*iam_pb.Identity) {
	filtered := make([]*iam_pb.Identity, 0, len(identities))
	names := make(map[string]bool, len(identities))
	for _, ident := range identities {
		if ident != nil {
			filtered = append(filtered, ident)
			names[ident.Name] = true
		}
	}
	cm.staticMu.Lock()
	cm.staticIdentities = filtered
	cm.staticNames = names
	cm.staticMu.Unlock()
}

// IsStaticIdentity returns true if the named identity was loaded from static config.
func (cm *CredentialManager) IsStaticIdentity(name string) bool {
	cm.staticMu.RLock()
	defer cm.staticMu.RUnlock()
	return cm.staticNames[name]
}

// GetStaticIdentity returns the protobuf identity for a static user, or nil.
func (cm *CredentialManager) GetStaticIdentity(name string) *iam_pb.Identity {
	cm.staticMu.RLock()
	defer cm.staticMu.RUnlock()
	for _, ident := range cm.staticIdentities {
		if ident.Name == name {
			return ident
		}
	}
	return nil
}

// GetStaticUsernames returns the names of all static identities.
func (cm *CredentialManager) GetStaticUsernames() []string {
	cm.staticMu.RLock()
	defer cm.staticMu.RUnlock()
	names := make([]string, 0, len(cm.staticIdentities))
	for _, ident := range cm.staticIdentities {
		names = append(names, ident.Name)
	}
	return names
}

// LoadConfiguration loads the S3 API configuration from the store and merges
// in any static identities so that listing operations show all users.
func (cm *CredentialManager) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) {
	config, err := cm.Store.LoadConfiguration(ctx)
	if err != nil {
		return config, err
	}
	// Merge static identities that are not already in the dynamic config
	cm.staticMu.RLock()
	staticIdents := cm.staticIdentities
	cm.staticMu.RUnlock()
	if len(staticIdents) > 0 {
		dynamicNames := make(map[string]bool, len(config.Identities))
		for _, ident := range config.Identities {
			dynamicNames[ident.Name] = true
		}
		for _, si := range staticIdents {
			if !dynamicNames[si.Name] {
				config.Identities = append(config.Identities, si)
			}
		}
	}
	return config, nil
}

// SaveConfiguration saves the S3 API configuration.
// Static identities are filtered out before saving to the store.
// The caller's config is not mutated.
func (cm *CredentialManager) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error {
	cm.staticMu.RLock()
	staticNames := cm.staticNames
	cm.staticMu.RUnlock()
	if len(staticNames) > 0 {
		var dynamicOnly []*iam_pb.Identity
		for _, ident := range config.Identities {
			if !staticNames[ident.Name] {
				dynamicOnly = append(dynamicOnly, ident)
			}
		}
		configCopy := *config
		configCopy.Identities = dynamicOnly
		return cm.Store.SaveConfiguration(ctx, &configCopy)
	}
	return cm.Store.SaveConfiguration(ctx, config)
}

// CreateUser creates a new user
func (cm *CredentialManager) CreateUser(ctx context.Context, identity *iam_pb.Identity) error {
	return cm.Store.CreateUser(ctx, identity)
}

// GetUser retrieves a user by username
func (cm *CredentialManager) GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) {
	return cm.Store.GetUser(ctx, username)
}

// UpdateUser updates an existing user
func (cm *CredentialManager) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error {
	return cm.Store.UpdateUser(ctx, username, identity)
}

// DeleteUser removes a user
func (cm *CredentialManager) DeleteUser(ctx context.Context, username string) error {
	return cm.Store.DeleteUser(ctx, username)
}

// ListUsers returns usernames from the dynamic store via cm.Store.ListUsers.
// On store error the error is returned directly without merging static entries.
// Static identities (cm.staticIdentities) are NOT included here because
// internal callers (e.g. DeletePolicy) look up each user in the store and
// would fail on non-existent static entries. External callers that need the
// full list should merge GetStaticUsernames separately.
func (cm *CredentialManager) ListUsers(ctx context.Context) ([]string, error) {
	return cm.Store.ListUsers(ctx)
}

// GetUserByAccessKey retrieves a user by access key
func (cm *CredentialManager) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) {
	return cm.Store.GetUserByAccessKey(ctx, accessKey)
}

// CreateAccessKey creates a new access key for a user
func (cm *CredentialManager) CreateAccessKey(ctx context.Context, username string, credential *iam_pb.Credential) error {
	return cm.Store.CreateAccessKey(ctx, username, credential)
}

// DeleteAccessKey removes an access key for a user
func (cm *CredentialManager) DeleteAccessKey(ctx context.Context, username string, accessKey string) error {
	return cm.Store.DeleteAccessKey(ctx, username, accessKey)
}

// GetPolicies returns all policies
func (cm *CredentialManager) GetPolicies(ctx context.Context) (map[string]policy_engine.PolicyDocument, error) {
	return cm.Store.GetPolicies(ctx)
}

// PutPolicy creates or updates a policy
func (cm *CredentialManager) PutPolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
	return cm.Store.PutPolicy(ctx, name, document)
}

// DeletePolicy removes a policy
func (cm *CredentialManager) DeletePolicy(ctx context.Context, name string) error {
	return cm.Store.DeletePolicy(ctx, name)
}

// GetPolicy retrieves a policy by name
func (cm *CredentialManager) GetPolicy(ctx context.Context, name string) (*policy_engine.PolicyDocument, error) {
	return cm.Store.GetPolicy(ctx, name)
}

// ListPolicyNames returns the names of all policies
func (cm *CredentialManager) ListPolicyNames(ctx context.Context) ([]string, error) {
	return cm.Store.ListPolicyNames(ctx)
}

// CreatePolicy creates a new policy (if supported by the store)
func (cm *CredentialManager) CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
	// Check if the store implements PolicyManager interface with CreatePolicy
	if policyStore, ok := cm.Store.(PolicyManager); ok {
		return policyStore.CreatePolicy(ctx, name, document)
	}
	// Fallback to PutPolicy for stores that only implement CredentialStore
	return cm.Store.PutPolicy(ctx, name, document)
}

// UpdatePolicy updates an existing policy (if supported by the store)
func (cm *CredentialManager) UpdatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
	// Check if the store implements PolicyManager interface with UpdatePolicy
	if policyStore, ok := cm.Store.(PolicyManager); ok {
		return policyStore.UpdatePolicy(ctx, name, document)
	}
	// Fallback to PutPolicy for stores that only implement CredentialStore
	return cm.Store.PutPolicy(ctx, name, document)
}

// PutUserInlinePolicy stores a per-user inline policy document.
// Returns nil without error if the underlying store does not support inline policies.
func (cm *CredentialManager) PutUserInlinePolicy(ctx context.Context, userName, policyName string, document policy_engine.PolicyDocument) error {
	if store, ok := cm.Store.(InlinePolicyStore); ok {
		return store.PutUserInlinePolicy(ctx, userName, policyName, document)
	}
	return nil
}

// GetUserInlinePolicy retrieves a per-user inline policy document.
// Returns nil without error if the underlying store does not support inline policies.
func (cm *CredentialManager) GetUserInlinePolicy(ctx context.Context, userName, policyName string) (*policy_engine.PolicyDocument, error) {
	if store, ok := cm.Store.(InlinePolicyStore); ok {
		return store.GetUserInlinePolicy(ctx, userName, policyName)
	}
	return nil, nil
}

// DeleteUserInlinePolicy removes a per-user inline policy document.
func (cm *CredentialManager) DeleteUserInlinePolicy(ctx context.Context, userName, policyName string) error {
	if store, ok := cm.Store.(InlinePolicyStore); ok {
		return store.DeleteUserInlinePolicy(ctx, userName, policyName)
	}
	return nil
}

// ListUserInlinePolicies returns the names of all inline policies for a user.
func (cm *CredentialManager) ListUserInlinePolicies(ctx context.Context, userName string) ([]string, error) {
	if store, ok := cm.Store.(InlinePolicyStore); ok {
		return store.ListUserInlinePolicies(ctx, userName)
	}
	return nil, nil
}

// LoadAllInlinePolicies returns every user inline policy in a single map
// keyed by username then policy name. Returns nil, nil if the underlying
// store does not implement bulk loading.
func (cm *CredentialManager) LoadAllInlinePolicies(ctx context.Context) (map[string]map[string]policy_engine.PolicyDocument, error) {
	if loader, ok := cm.Store.(InlinePoliciesLoader); ok {
		return loader.LoadInlinePolicies(ctx)
	}
	return nil, nil
}

// PutGroupInlinePolicy stores a per-group inline policy document.
// Returns nil without error if the underlying store does not support
// group inline policies.
func (cm *CredentialManager) PutGroupInlinePolicy(ctx context.Context, groupName, policyName string, document policy_engine.PolicyDocument) error {
	if store, ok := cm.Store.(GroupInlinePolicyStore); ok {
		return store.PutGroupInlinePolicy(ctx, groupName, policyName, document)
	}
	return nil
}

// GetGroupInlinePolicy retrieves a per-group inline policy document.
func (cm *CredentialManager) GetGroupInlinePolicy(ctx context.Context, groupName, policyName string) (*policy_engine.PolicyDocument, error) {
	if store, ok := cm.Store.(GroupInlinePolicyStore); ok {
		return store.GetGroupInlinePolicy(ctx, groupName, policyName)
	}
	return nil, nil
}

// DeleteGroupInlinePolicy removes a per-group inline policy document.
func (cm *CredentialManager) DeleteGroupInlinePolicy(ctx context.Context, groupName, policyName string) error {
	if store, ok := cm.Store.(GroupInlinePolicyStore); ok {
		return store.DeleteGroupInlinePolicy(ctx, groupName, policyName)
	}
	return nil
}

// ListGroupInlinePolicies returns the names of all inline policies for a group.
func (cm *CredentialManager) ListGroupInlinePolicies(ctx context.Context, groupName string) ([]string, error) {
	if store, ok := cm.Store.(GroupInlinePolicyStore); ok {
		return store.ListGroupInlinePolicies(ctx, groupName)
	}
	return nil, nil
}

// LoadAllGroupInlinePolicies returns every group inline policy in a single
// map keyed by group name then policy name. Returns nil, nil if the
// underlying store does not implement bulk loading.
func (cm *CredentialManager) LoadAllGroupInlinePolicies(ctx context.Context) (map[string]map[string]policy_engine.PolicyDocument, error) {
	if loader, ok := cm.Store.(GroupInlinePoliciesLoader); ok {
		return loader.LoadGroupInlinePolicies(ctx)
	}
	return nil, nil
}

// RenameUser atomically renames a user along with all FK-backed
// dependents (credentials, inline policies, ...) when the underlying
// store implements UserRenamer. Returns false if the store does not
// support an atomic rename, in which case the caller is expected to
// fall back to its own best-effort path. A returned non-nil error
// always means the rename was attempted and failed.
func (cm *CredentialManager) RenameUser(ctx context.Context, oldName, newName string) (bool, error) {
	store, ok := cm.Store.(UserRenamer)
	if !ok {
		return false, nil
	}
	return true, store.RenameUser(ctx, oldName, newName)
}

// LoadS3ConfigFile reads a static S3 identity config file and registers
// the identities so they appear in LoadConfiguration and listing results.
func (cm *CredentialManager) LoadS3ConfigFile(path string) error {
	content, err := os.ReadFile(path)
	if err != nil {
		return fmt.Errorf("read %s: %w", path, err)
	}
	config := &iam_pb.S3ApiConfiguration{}
	opts := protojson.UnmarshalOptions{DiscardUnknown: true, AllowPartial: true}
	if err := opts.Unmarshal(content, config); err != nil {
		return fmt.Errorf("parse %s: %w", path, err)
	}
	for _, ident := range config.Identities {
		ident.IsStatic = true
	}
	cm.SetStaticIdentities(config.Identities)
	glog.V(1).Infof("Loaded %d static identities from %s", len(config.Identities), path)
	return nil
}

// Shutdown performs cleanup
func (cm *CredentialManager) Shutdown() {
	if cm.Store != nil {
		cm.Store.Shutdown()
	}
}

// getAvailableStores returns a comma-separated list of available store names
func getAvailableStores() string {
	var storeNames []string
	for _, store := range Stores {
		storeNames = append(storeNames, string(store.GetName()))
	}
	return strings.Join(storeNames, ", ")
}

// GetAvailableStores returns a list of available credential store names
func GetAvailableStores() []CredentialStoreTypeName {
	var storeNames []CredentialStoreTypeName
	for _, store := range Stores {
		storeNames = append(storeNames, store.GetName())
	}
	if storeNames == nil {
		return []CredentialStoreTypeName{}
	}
	return storeNames
}

// CreateServiceAccount creates a new service account
func (cm *CredentialManager) CreateServiceAccount(ctx context.Context, sa *iam_pb.ServiceAccount) error {
	return cm.Store.CreateServiceAccount(ctx, sa)
}

// UpdateServiceAccount updates an existing service account
func (cm *CredentialManager) UpdateServiceAccount(ctx context.Context, id string, sa *iam_pb.ServiceAccount) error {
	return cm.Store.UpdateServiceAccount(ctx, id, sa)
}

// DeleteServiceAccount removes a service account
func (cm *CredentialManager) DeleteServiceAccount(ctx context.Context, id string) error {
	return cm.Store.DeleteServiceAccount(ctx, id)
}

// GetServiceAccount retrieves a service account by ID
func (cm *CredentialManager) GetServiceAccount(ctx context.Context, id string) (*iam_pb.ServiceAccount, error) {
	return cm.Store.GetServiceAccount(ctx, id)
}

// ListServiceAccounts returns all service accounts
func (cm *CredentialManager) ListServiceAccounts(ctx context.Context) ([]*iam_pb.ServiceAccount, error) {
	return cm.Store.ListServiceAccounts(ctx)
}

// AttachUserPolicy attaches a managed policy to a user
func (cm *CredentialManager) AttachUserPolicy(ctx context.Context, username string, policyName string) error {
	return cm.Store.AttachUserPolicy(ctx, username, policyName)
}

// DetachUserPolicy detaches a managed policy from a user
func (cm *CredentialManager) DetachUserPolicy(ctx context.Context, username string, policyName string) error {
	return cm.Store.DetachUserPolicy(ctx, username, policyName)
}

// ListAttachedUserPolicies returns the list of policy names attached to a user
func (cm *CredentialManager) ListAttachedUserPolicies(ctx context.Context, username string) ([]string, error) {
	return cm.Store.ListAttachedUserPolicies(ctx, username)
}

// Group Management

func (cm *CredentialManager) CreateGroup(ctx context.Context, group *iam_pb.Group) error {
	return cm.Store.CreateGroup(ctx, group)
}

func (cm *CredentialManager) GetGroup(ctx context.Context, groupName string) (*iam_pb.Group, error) {
	return cm.Store.GetGroup(ctx, groupName)
}

func (cm *CredentialManager) DeleteGroup(ctx context.Context, groupName string) error {
	return cm.Store.DeleteGroup(ctx, groupName)
}

func (cm *CredentialManager) ListGroups(ctx context.Context) ([]string, error) {
	return cm.Store.ListGroups(ctx)
}

func (cm *CredentialManager) UpdateGroup(ctx context.Context, group *iam_pb.Group) error {
	return cm.Store.UpdateGroup(ctx, group)
}
