package iam

import (
	"context"
	"time"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apiserver/pkg/registry/generic/registry"

	iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
	v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
)

// AfterUserCreate is a post-create hook that writes the user's basic role assignment to Zanzana (openFGA)
func (b *IdentityAccessManagementAPIBuilder) AfterUserCreate(obj runtime.Object, _ *metav1.CreateOptions) {
	if b.zClient == nil {
		return
	}

	user, ok := obj.(*iamv0.User)
	if !ok {
		b.logger.Error("failed to convert object to User type", "object", obj)
		return
	}

	// Skip if user has no role assigned
	if user.Spec.Role == "" {
		b.logger.Debug("user has no role assigned, skipping basic role sync",
			"namespace", user.Namespace,
			"name", user.Name,
		)
		return
	}

	resourceType := "user"
	operation := "create"

	// Grab a ticket to write to Zanzana
	wait := time.Now()
	b.zTickets <- true
	hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds())

	go func(namespace, subjectName, role, resourceType, operation string) {
		start := time.Now()
		status := "success"

		defer func() {
			<-b.zTickets
			hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds())
			hooksOperationCounter.WithLabelValues(resourceType, operation, status).Inc()
		}()

		b.logger.Debug("writing user basic role to zanzana",
			"namespace", namespace,
			"name", subjectName,
			"role", role,
		)

		ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout)
		defer cancel()

		err := b.zClient.Mutate(ctx, &v1.MutateRequest{
			Namespace: namespace,
			Operations: []*v1.MutateOperation{
				{
					Operation: &v1.MutateOperation_UpdateUserOrgRole{
						UpdateUserOrgRole: &v1.UpdateUserOrgRoleOperation{User: subjectName, Role: role},
					},
				},
			},
		})

		if err != nil {
			status = "failure"
			b.logger.Error("failed to write user basic role to zanzana",
				"err", err,
				"namespace", namespace,
				"name", subjectName,
				"role", role,
			)
		} else {
			hooksTuplesCounter.WithLabelValues(resourceType, operation, "write").Inc()
		}
	}(user.Namespace, user.Name, user.Spec.Role, resourceType, operation)
}

// BeginUserUpdate is a pre-update hook that gets called on user updates
// It compares old and new roles and performs the zanzana write after K8s update succeeds
func (b *IdentityAccessManagementAPIBuilder) BeginUserUpdate(ctx context.Context, obj, oldObj runtime.Object, options *metav1.UpdateOptions) (registry.FinishFunc, error) {
	if b.zClient == nil {
		return nil, nil
	}

	oldUser, ok := oldObj.(*iamv0.User)
	if !ok {
		return nil, nil
	}

	newUser, ok := obj.(*iamv0.User)
	if !ok {
		return nil, nil
	}

	// If role hasn't changed, no need to update
	if oldUser.Spec.Role == newUser.Spec.Role {
		return nil, nil
	}

	// Return a finish function that performs the zanzana write only on success
	return func(ctx context.Context, success bool) {
		if !success {
			return
		}

		wait := time.Now()
		b.zTickets <- true
		hooksWaitHistogram.WithLabelValues("user", "update").Observe(time.Since(wait).Seconds())

		go func(namespace, subjectName, oldRole, newRole string) {
			start := time.Now()
			status := "success"

			defer func() {
				<-b.zTickets
				hooksDurationHistogram.WithLabelValues("user", "update", status).Observe(time.Since(start).Seconds())
				hooksOperationCounter.WithLabelValues("user", "update", status).Inc()
			}()

			b.logger.Debug("updating user basic role in zanzana",
				"namespace", namespace,
				"name", subjectName,
				"oldRole", oldRole,
				"newRole", newRole,
			)

			ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout)
			defer cancel()

			err := b.zClient.Mutate(ctx, &v1.MutateRequest{
				Namespace: namespace,
				Operations: []*v1.MutateOperation{
					{
						Operation: &v1.MutateOperation_UpdateUserOrgRole{
							UpdateUserOrgRole: &v1.UpdateUserOrgRoleOperation{User: subjectName, Role: newRole},
						},
					}, {
						Operation: &v1.MutateOperation_DeleteUserOrgRole{
							DeleteUserOrgRole: &v1.DeleteUserOrgRoleOperation{User: subjectName, Role: oldRole},
						},
					},
				},
			})
			if err != nil {
				status = "failure"
				b.logger.Error("failed to update user basic role in zanzana",
					"err", err,
					"namespace", namespace,
					"name", subjectName,
					"role", newRole,
					"oldRole", oldRole,
				)
			}
		}(oldUser.Namespace, oldUser.Name, oldUser.Spec.Role, newUser.Spec.Role)
	}, nil
}

// AfterUserDelete is a post-delete hook that removes the user's basic role assignment from Zanzana (openFGA)
func (b *IdentityAccessManagementAPIBuilder) AfterUserDelete(obj runtime.Object, _ *metav1.DeleteOptions) {
	if b.zClient == nil {
		return
	}

	user, ok := obj.(*iamv0.User)
	if !ok {
		b.logger.Error("failed to convert object to User type", "object", obj)
		return
	}

	resourceType := "user"
	operation := "delete"

	// Skip if user had no role assigned
	if user.Spec.Role == "" {
		b.logger.Debug("user had no role assigned, skipping basic role sync",
			"namespace", user.Namespace,
			"name", user.Name,
		)
		return
	}

	wait := time.Now()
	b.zTickets <- true
	hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds())

	go func(namespace, subjectName, role string) {
		start := time.Now()
		status := "success"

		defer func() {
			<-b.zTickets
			hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds())
			hooksOperationCounter.WithLabelValues(resourceType, operation, status).Inc()
		}()

		b.logger.Debug("deleting user basic role from zanzana",
			"namespace", namespace,
			"name", subjectName,
			"role", role,
		)

		ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout)
		defer cancel()

		err := b.zClient.Mutate(ctx, &v1.MutateRequest{
			Namespace: namespace,
			Operations: []*v1.MutateOperation{
				{
					Operation: &v1.MutateOperation_DeleteUserOrgRole{
						DeleteUserOrgRole: &v1.DeleteUserOrgRoleOperation{User: subjectName, Role: role},
					},
				},
			},
		})

		if err != nil {
			status = "failure"
			b.logger.Error("failed to delete user basic role from zanzana",
				"err", err,
				"namespace", namespace,
				"name", subjectName,
				"role", role,
			)
		} else {
			hooksTuplesCounter.WithLabelValues(resourceType, operation, "delete").Inc()
		}
	}(user.Namespace, user.Name, user.Spec.Role)
}
