package systemaccount

import (
	"fmt"

	v32 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"

	v3 "github.com/rancher/rancher/pkg/generated/norman/management.cattle.io/v3"
	"github.com/rancher/rancher/pkg/ref"
	"github.com/rancher/rancher/pkg/types/config"
	"github.com/rancher/rancher/pkg/types/config/systemtokens"
	"github.com/rancher/rancher/pkg/user"
	"github.com/rancher/wrangler/pkg/randomtoken"
	errors2 "k8s.io/apimachinery/pkg/api/errors"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
	clusterOwnerRole           = "cluster-owner"
	projectMemberRole          = "project-member"
	ClusterSystemAccountPrefix = "System account for Cluster "
	ProjectSystemAccountPrefix = "System account for Project "
)

func NewManager(management *config.ManagementContext) *Manager {
	return &Manager{
		userManager:  management.UserManager,
		systemTokens: management.SystemTokens,
		crtbs:        management.Management.ClusterRoleTemplateBindings(""),
		crts:         management.Management.ClusterRegistrationTokens(""),
		prtbs:        management.Management.ProjectRoleTemplateBindings(""),
		prtbLister:   management.Management.ProjectRoleTemplateBindings("").Controller().Lister(),
		tokens:       management.Management.Tokens(""),
		users:        management.Management.Users(""),
	}
}

func NewManagerFromScale(management *config.ScaledContext) *Manager {
	return &Manager{
		userManager:  management.UserManager,
		systemTokens: management.SystemTokens,
		crtbs:        management.Management.ClusterRoleTemplateBindings(""),
		crts:         management.Management.ClusterRegistrationTokens(""),
		prtbs:        management.Management.ProjectRoleTemplateBindings(""),
		prtbLister:   management.Management.ProjectRoleTemplateBindings("").Controller().Lister(),
		tokens:       management.Management.Tokens(""),
		tokenLister:  management.Management.Tokens("").Controller().Lister(),
		users:        management.Management.Users(""),
	}
}

type Manager struct {
	userManager  user.Manager
	systemTokens systemtokens.Interface
	crtbs        v3.ClusterRoleTemplateBindingInterface
	crts         v3.ClusterRegistrationTokenInterface
	prtbs        v3.ProjectRoleTemplateBindingInterface
	prtbLister   v3.ProjectRoleTemplateBindingLister
	tokens       v3.TokenInterface
	tokenLister  v3.TokenLister
	users        v3.UserInterface
}

func (s *Manager) CreateSystemAccount(cluster *v3.Cluster) error {
	user, err := s.GetSystemUser(cluster.Name)
	if err != nil {
		return err
	}

	bindingName := user.Name + "-admin"
	_, err = s.crtbs.GetNamespaced(cluster.Name, bindingName, v1.GetOptions{})
	if err == nil {
		return nil
	}

	_, err = s.crtbs.Create(&v3.ClusterRoleTemplateBinding{
		ObjectMeta: v1.ObjectMeta{
			Name:      bindingName,
			Namespace: cluster.Name,
		},
		ClusterName:      cluster.Name,
		UserName:         user.Name,
		RoleTemplateName: clusterOwnerRole,
	})

	return err
}

func (s *Manager) GetSystemUser(clusterName string) (*v3.User, error) {
	return s.userManager.EnsureUser(fmt.Sprintf("system://%s", clusterName), ClusterSystemAccountPrefix+clusterName)
}

func (s *Manager) GetOrCreateSystemClusterToken(clusterName string) (string, error) {
	token := ""

	crt, err := s.crts.GetNamespaced(clusterName, "system", v1.GetOptions{})
	if errors2.IsNotFound(err) {
		token, err = randomtoken.Generate()
		if err != nil {
			return "", err
		}
		crt = &v3.ClusterRegistrationToken{
			ObjectMeta: v1.ObjectMeta{
				Name:      "system",
				Namespace: clusterName,
			},
			Spec: v32.ClusterRegistrationTokenSpec{
				ClusterName: clusterName,
			},
			Status: v32.ClusterRegistrationTokenStatus{
				Token: token,
			},
		}

		if _, err := s.crts.Create(crt); err != nil {
			return "", err
		}
	} else if err != nil {
		return "", err
	} else {
		token = crt.Status.Token
	}

	return token, nil
}

func (s *Manager) GetOrCreateProjectSystemAccount(projectID string) error {
	_, projectName := ref.Parse(projectID)

	u, err := s.GetProjectSystemUser(projectName)
	if err != nil {
		return err
	}

	bindingName := u.Name + "-member"
	_, err = s.prtbLister.Get(projectName, bindingName)
	if err != nil {
		if !errors2.IsNotFound(err) {
			return err
		}
		// prtb does not exist in cache, attempt to create it
		prtb := &v3.ProjectRoleTemplateBinding{
			ObjectMeta: v1.ObjectMeta{
				Name:      bindingName,
				Namespace: projectName,
			},
			ProjectName:      projectID,
			UserName:         u.Name,
			RoleTemplateName: projectMemberRole,
		}
		if _, err := s.prtbs.Create(prtb); err != nil && !errors2.IsAlreadyExists(err) {
			return err
		}
	}
	return nil
}

func (s *Manager) GetProjectSystemUser(projectName string) (*v3.User, error) {
	return s.userManager.EnsureUser(fmt.Sprintf("system://%s", projectName), ProjectSystemAccountPrefix+projectName)
}

func (s *Manager) GetProjectPipelineSystemToken(projectName string) (*v3.Token, error) {
	return s.tokenLister.Get("", projectName+"-pipeline")
}

// CreateProjectPipelineSystemToken will create a new pipeline token for the given project
// if one does not exist. If a token already exists, it's value will be overwritten with a
// new token.
func (s *Manager) CreateProjectPipelineSystemToken(projectName string) (string, error) {
	user, err := s.GetProjectSystemUser(projectName)
	if err != nil {
		return "", err
	}
	var ttl int64
	return s.systemTokens.EnsureSystemToken(projectName+"-pipeline", "Pipeline token for project "+projectName, "pipeline", user.Name, &ttl, false)
}

func (s *Manager) CreateProjectHelmSystemToken(projectName string) (string, error) {
	user, err := s.GetProjectSystemUser(projectName)
	if err != nil {
		return "", err
	}
	return s.systemTokens.EnsureSystemToken(projectName+"-helm", "Helm token for project "+projectName, "helm", user.Name, nil, true)
}

func (s *Manager) RemoveSystemAccount(userID string) error {
	u, err := s.userManager.GetUserByPrincipalID(fmt.Sprintf("system://%s", userID))
	if err != nil {
		return err
	}
	if u == nil {
		// user not found, must have been removed
		return nil
	}
	if err := s.users.Delete(u.Name, &v1.DeleteOptions{}); err != nil && !errors2.IsNotFound(err) && !errors2.IsGone(err) {
		return err
	}
	return nil
}
