// Package cleanup defines a type that represents a cleanup routine for an auth provider.
package cleanup

import (
	"errors"
	"fmt"
	"strings"

	v3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
	"github.com/rancher/rancher/pkg/auth/api/secrets"
	"github.com/rancher/rancher/pkg/auth/providers/local/pbkdf2"
	controllers "github.com/rancher/rancher/pkg/generated/controllers/management.cattle.io/v3"
	wcorev1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
)

var errAuthConfigNil = errors.New("cannot get auth provider if its config is nil")

// Service performs cleanup of resources associated with an auth provider.
type Service struct {
	secretsInterface wcorev1.SecretController
	secretsCache     wcorev1.SecretCache

	userClient controllers.UserClient

	clusterRoleTemplateBindingsCache  controllers.ClusterRoleTemplateBindingCache
	clusterRoleTemplateBindingsClient controllers.ClusterRoleTemplateBindingClient

	globalRoleBindingsCache  controllers.GlobalRoleBindingCache
	globalRoleBindingsClient controllers.GlobalRoleBindingClient

	projectRoleTemplateBindingsCache  controllers.ProjectRoleTemplateBindingCache
	projectRoleTemplateBindingsClient controllers.ProjectRoleTemplateBindingClient

	tokensCache  controllers.TokenCache
	tokensClient controllers.TokenClient
}

// NewCleanupService creates and returns a new auth provider cleanup service.
func NewCleanupService(secretsInterface wcorev1.SecretController, c controllers.Interface) *Service {
	return &Service{
		secretsInterface: secretsInterface,
		secretsCache:     secretsInterface.Cache(),

		userClient: c.User(),

		clusterRoleTemplateBindingsCache:  c.ClusterRoleTemplateBinding().Cache(),
		clusterRoleTemplateBindingsClient: c.ClusterRoleTemplateBinding(),

		projectRoleTemplateBindingsCache:  c.ProjectRoleTemplateBinding().Cache(),
		projectRoleTemplateBindingsClient: c.ProjectRoleTemplateBinding(),

		globalRoleBindingsCache:  c.GlobalRoleBinding().Cache(),
		globalRoleBindingsClient: c.GlobalRoleBinding(),

		tokensCache:  c.Token().Cache(),
		tokensClient: c.Token(),
	}
}

// Run takes an auth config and checks if its auth provider is disabled, and ensures that any resources associated with it,
// such as secrets, CRTBs, PRTBs, GRBs, Users, are deleted.
func (s *Service) Run(config *v3.AuthConfig) error {
	if err := secrets.CleanupClientSecrets(s.secretsInterface, config); err != nil {
		return fmt.Errorf("error cleaning up resources associated with a disabled auth provider %s: %w", config.Name, err)
	}

	if err := s.deleteGlobalRoleBindings(config); err != nil {
		return fmt.Errorf("error cleaning up global role bindings associated with a disabled auth provider %s: %w", config.Name, err)
	}

	if err := s.deleteClusterRoleTemplateBindings(config); err != nil {
		return fmt.Errorf("error cleaning up cluster role template bindings associated with a disabled auth provider %s: %w", config.Name, err)
	}

	if err := s.deleteProjectRoleTemplateBindings(config); err != nil {
		return fmt.Errorf("error cleaning up project role template bindings associated with a disabled auth provider %s: %w", config.Name, err)
	}

	if err := s.deleteUsers(config); err != nil {
		return fmt.Errorf("error cleaning up users associated with a disabled auth provider %s: %w", config.Name, err)
	}

	if err := s.deleteTokens(config); err != nil {
		return fmt.Errorf("error cleaning up tokens associated with a disabled auth provider %s: %w", config.Name, err)
	}

	return nil
}

func (s *Service) deleteClusterRoleTemplateBindings(config *v3.AuthConfig) error {
	if config == nil {
		return errAuthConfigNil
	}
	list, err := s.clusterRoleTemplateBindingsCache.List("", labels.Everything())
	if err != nil {
		return fmt.Errorf("failed to list cluster role template bindings: %w", err)
	}

	for _, b := range list {
		providerName := getProviderNameFromPrincipalNames(b.UserPrincipalName, b.GroupPrincipalName)
		if providerName == config.Name {
			err := s.clusterRoleTemplateBindingsClient.Delete(b.Namespace, b.Name, &metav1.DeleteOptions{})
			if err != nil && !apierrors.IsNotFound(err) {
				return err
			}
		}
	}

	return nil
}

func (s *Service) deleteGlobalRoleBindings(config *v3.AuthConfig) error {
	if config == nil {
		return errAuthConfigNil
	}
	list, err := s.globalRoleBindingsCache.List(labels.Everything())
	if err != nil {
		return fmt.Errorf("failed to list global role bindings: %w", err)
	}

	for _, b := range list {
		providerName := getProviderNameFromPrincipalNames(b.GroupPrincipalName)
		if providerName == config.Name {
			err := s.globalRoleBindingsClient.Delete(b.Name, &metav1.DeleteOptions{})
			if err != nil && !apierrors.IsNotFound(err) {
				return err
			}
		}
	}

	return nil
}

func (s *Service) deleteProjectRoleTemplateBindings(config *v3.AuthConfig) error {
	if config == nil {
		return errAuthConfigNil
	}
	prtbs, err := s.projectRoleTemplateBindingsCache.List("", labels.Everything())
	if err != nil {
		return fmt.Errorf("failed to list project role template bindings: %w", err)
	}

	for _, b := range prtbs {
		providerName := getProviderNameFromPrincipalNames(b.UserPrincipalName, b.GroupPrincipalName)
		if providerName == config.Name {
			err := s.projectRoleTemplateBindingsClient.Delete(b.Namespace, b.Name, &metav1.DeleteOptions{})
			if err != nil && !apierrors.IsNotFound(err) {
				return err
			}
		}
	}

	return nil
}

// deleteUsers deletes all external users (for the given provider specified in the config),
// who never were local users. It does not delete external users who were local before the provider had been set up.
// The method only removes the external principal IDs from those users.
// External users are those who have multiple principal IDs associated with them.
// A local admin (not necessarily the default admin) who had set up the provider will have two principal IDs,
// but will also have a password.
// This is how Rancher distinguishes fully external users from those who are external, too, but were once local.
func (s *Service) deleteUsers(config *v3.AuthConfig) error {
	if config == nil {
		return errAuthConfigNil
	}
	users, err := s.userClient.List(metav1.ListOptions{})
	if err != nil {
		return fmt.Errorf("failed to list users: %w", err)
	}

	for _, u := range users.Items {
		providerName := getProviderNameFromPrincipalNames(u.PrincipalIDs...)
		if providerName == config.Name {
			// A fully external user (who was never local) has no password.
			_, err := s.secretsCache.Get(pbkdf2.LocalUserPasswordsNamespace, u.Name)
			if err != nil && !apierrors.IsNotFound(err) {
				return fmt.Errorf("failed to get user secret: %w", err)
			}
			if u.Password == "" && apierrors.IsNotFound(err) {
				err := s.userClient.Delete(u.Name, &metav1.DeleteOptions{})
				if err != nil && !apierrors.IsNotFound(err) {
					return err
				}
			} else {
				if err := s.resetLocalUser(&u); err != nil {
					return fmt.Errorf("failed to reset local user: %w", err)
				}
			}
		}
	}

	return nil
}

// deleteTokens deletes all the tokens created with the disabled provider
func (s *Service) deleteTokens(config *v3.AuthConfig) error {
	if config == nil {
		return errAuthConfigNil
	}

	tokens, err := s.tokensCache.List(labels.Everything())
	if err != nil {
		return fmt.Errorf("failed to list tokens: %w", err)
	}

	for _, t := range tokens {
		if t.AuthProvider == config.Name {
			err := s.tokensClient.Delete(t.Name, &metav1.DeleteOptions{})
			if err != nil && !apierrors.IsNotFound(err) {
				return fmt.Errorf("failed deleting token %s while disabling authprovider %s: %w", t.Name, t.AuthProvider, err)
			}
		}
	}

	return nil
}

// resetLocalUser takes a user and removes all its principal IDs except that of the local user.
// It updates the user so that it is effectively as it was before any auth provider had been enabled.
func (s *Service) resetLocalUser(user *v3.User) error {
	if user == nil || len(user.PrincipalIDs) < 2 {
		return nil
	}

	var localID string
	for _, id := range user.PrincipalIDs {
		if strings.HasPrefix(id, "local") {
			localID = id
			break
		}
	}

	if localID == "" {
		return nil
	}

	user.PrincipalIDs = []string{localID}
	_, err := s.userClient.Update(user)
	if err != nil && !apierrors.IsNotFound(err) {
		return err
	}
	return nil
}

// getProviderNameFromPrincipalNames tries to extract the provider name from any one string that represents
// a user principal or group principal.
func getProviderNameFromPrincipalNames(names ...string) string {
	for _, name := range names {
		parts := strings.Split(name, "_")
		if len(parts) > 0 && parts[0] != "" && !strings.HasPrefix(parts[0], "local") {
			return parts[0]
		}
	}
	return ""
}
