/*
 * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package rbac

import (
	"context"
	"fmt"

	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

const (
	// RBAC resource kind constants
	kindClusterRole    = "ClusterRole"
	kindServiceAccount = "ServiceAccount"
	apiGroupRBAC       = "rbac.authorization.k8s.io"
)

// Manager handles dynamic RBAC creation for cluster-wide operator installations.
type Manager struct {
	client client.Client
}

// NewManager creates a new RBAC manager.
func NewManager(client client.Client) *Manager {
	return &Manager{client: client}
}

// needsRoleRefRecreate checks if the RoleRef has changed, which requires
// deleting and recreating the RoleBinding since RoleRef is immutable.
func needsRoleRefRecreate(existing *rbacv1.RoleBinding, clusterRoleName string) bool {
	return existing.RoleRef.Name != clusterRoleName ||
		existing.RoleRef.Kind != kindClusterRole ||
		existing.RoleRef.APIGroup != apiGroupRBAC
}

// needsSubjectUpdate checks if the Subjects field needs updating.
// Subjects are mutable so they can be updated in-place.
func needsSubjectUpdate(existing *rbacv1.RoleBinding, serviceAccountName, targetNamespace string) bool {
	return len(existing.Subjects) != 1 ||
		existing.Subjects[0].Kind != kindServiceAccount ||
		existing.Subjects[0].Name != serviceAccountName ||
		existing.Subjects[0].Namespace != targetNamespace
}

// EnsureServiceAccountWithRBAC creates or updates a ServiceAccount and RoleBinding
// in the target namespace. This should ONLY be called in cluster-wide mode.
//
// In cluster-wide mode, the operator dynamically creates:
//   - ServiceAccount in the target namespace
//   - RoleBinding in the target namespace that binds the SA to a ClusterRole
//
// The ClusterRole must already exist (created by Helm).
//
// Parameters:
//   - ctx: context
//   - targetNamespace: namespace to create RBAC resources in
//   - serviceAccountName: name of the ServiceAccount to create
//   - clusterRoleName: name of the ClusterRole to bind to (must exist)
func (m *Manager) EnsureServiceAccountWithRBAC(
	ctx context.Context,
	targetNamespace string,
	serviceAccountName string,
	clusterRoleName string,
) error {
	logger := log.FromContext(ctx)

	if targetNamespace == "" {
		return fmt.Errorf("target namespace is required")
	}
	if serviceAccountName == "" {
		return fmt.Errorf("service account name is required")
	}
	if clusterRoleName == "" {
		return fmt.Errorf("cluster role name is required")
	}

	// Verify ClusterRole exists before creating RoleBinding
	clusterRole := &rbacv1.ClusterRole{}
	if err := m.client.Get(ctx, client.ObjectKey{Name: clusterRoleName}, clusterRole); err != nil {
		if apierrors.IsNotFound(err) {
			return fmt.Errorf("cluster role %q does not exist: ensure it is created by Helm before deploying components", clusterRoleName)
		}
		return fmt.Errorf("failed to verify cluster role %q: %w", clusterRoleName, err)
	}
	logger.V(1).Info("ClusterRole verified",
		"clusterRole", clusterRoleName,
		"rules", len(clusterRole.Rules))

	// Create/update ServiceAccount
	sa := &corev1.ServiceAccount{
		ObjectMeta: metav1.ObjectMeta{
			Name:      serviceAccountName,
			Namespace: targetNamespace,
			Labels: map[string]string{
				"app.kubernetes.io/managed-by": "dynamo-operator",
				"app.kubernetes.io/component":  "rbac",
				"app.kubernetes.io/name":       serviceAccountName,
			},
		},
	}

	if err := m.client.Get(ctx, client.ObjectKeyFromObject(sa), sa); err != nil {
		if !apierrors.IsNotFound(err) {
			return fmt.Errorf("failed to get service account: %w", err)
		}
		// ServiceAccount doesn't exist, create it
		if err := m.client.Create(ctx, sa); err != nil {
			return fmt.Errorf("failed to create service account: %w", err)
		}
		logger.V(1).Info("ServiceAccount created",
			"serviceAccount", serviceAccountName,
			"namespace", targetNamespace)
	} else {
		logger.V(1).Info("ServiceAccount already exists",
			"serviceAccount", serviceAccountName,
			"namespace", targetNamespace)
	}

	// Create/update RoleBinding
	roleBindingName := fmt.Sprintf("%s-binding", serviceAccountName)
	rb := &rbacv1.RoleBinding{
		ObjectMeta: metav1.ObjectMeta{
			Name:      roleBindingName,
			Namespace: targetNamespace,
			Labels: map[string]string{
				"app.kubernetes.io/managed-by": "dynamo-operator",
				"app.kubernetes.io/component":  "rbac",
				"app.kubernetes.io/name":       serviceAccountName,
			},
		},
		Subjects: []rbacv1.Subject{{
			Kind:      kindServiceAccount,
			Name:      serviceAccountName,
			Namespace: targetNamespace,
		}},
		RoleRef: rbacv1.RoleRef{
			APIGroup: apiGroupRBAC,
			Kind:     kindClusterRole,
			Name:     clusterRoleName,
		},
	}

	existingRB := &rbacv1.RoleBinding{}
	if err := m.client.Get(ctx, client.ObjectKeyFromObject(rb), existingRB); err != nil {
		if !apierrors.IsNotFound(err) {
			return fmt.Errorf("failed to get role binding: %w", err)
		}
		// RoleBinding doesn't exist, create it
		if err := m.client.Create(ctx, rb); err != nil {
			return fmt.Errorf("failed to create role binding: %w", err)
		}
		logger.V(1).Info("RoleBinding created",
			"roleBinding", roleBindingName,
			"clusterRole", clusterRoleName,
			"namespace", targetNamespace)
	} else {
		// RoleBinding exists, check if it needs updating
		needsRecreate := needsRoleRefRecreate(existingRB, clusterRoleName)
		needsUpdate := needsSubjectUpdate(existingRB, serviceAccountName, targetNamespace)

		if needsRecreate {
			// RoleRef is immutable, so delete and recreate the RoleBinding
			if err := m.client.Delete(ctx, existingRB); err != nil {
				return fmt.Errorf("failed to delete role binding for recreation: %w", err)
			}
			logger.V(1).Info("RoleBinding deleted for recreation due to RoleRef change",
				"roleBinding", roleBindingName,
				"oldClusterRole", existingRB.RoleRef.Name,
				"newClusterRole", clusterRoleName,
				"namespace", targetNamespace)

			// Recreate with new RoleRef
			if err := m.client.Create(ctx, rb); err != nil {
				return fmt.Errorf("failed to recreate role binding: %w", err)
			}
			logger.V(1).Info("RoleBinding recreated",
				"roleBinding", roleBindingName,
				"clusterRole", clusterRoleName,
				"namespace", targetNamespace)
		} else if needsUpdate {
			// Only Subjects changed, can update in-place
			existingRB.Subjects = rb.Subjects
			if err := m.client.Update(ctx, existingRB); err != nil {
				return fmt.Errorf("failed to update role binding: %w", err)
			}
			logger.V(1).Info("RoleBinding subjects updated",
				"roleBinding", roleBindingName,
				"namespace", targetNamespace)
		} else {
			logger.V(1).Info("RoleBinding already up-to-date",
				"roleBinding", roleBindingName,
				"clusterRole", clusterRoleName,
				"namespace", targetNamespace)
		}
	}

	return nil
}
