package native

import (
	"context"

	"github.com/tech/sendico/pkg/auth/internal/native/nstructures"
	"github.com/tech/sendico/pkg/db/role"
	"github.com/tech/sendico/pkg/db/storable"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/mutil/mzap"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

// RoleManager manages roles using Casbin.
type RoleManager struct {
	logger            mlogger.Logger
	enforcer          *Enforcer
	rdb               role.DB
	rolePermissionRef bson.ObjectID
}

// NewRoleManager creates a new RoleManager.
func NewRoleManager(logger mlogger.Logger, enforcer *Enforcer, rolePermissionRef bson.ObjectID, rdb role.DB) *RoleManager {
	return &RoleManager{
		logger:            logger.Named("role"),
		enforcer:          enforcer,
		rdb:               rdb,
		rolePermissionRef: rolePermissionRef,
	}
}

// validateObjectIDs ensures that all provided ObjectIDs are non-zero.
func (rm *RoleManager) validateObjectIDs(ids ...bson.ObjectID) error {
	for _, id := range ids {
		if id.IsZero() {
			return merrors.InvalidArgument("Object references cannot be zero", "objectRef")
		}
	}
	return nil
}

// fetchRolesFromPolicies retrieves and converts policies to roles.
func (rm *RoleManager) fetchRolesFromPolicies(roles []nstructures.RoleAssignment, organizationRef bson.ObjectID) []model.RoleDescription {
	result := make([]model.RoleDescription, len(roles))
	for i, role := range roles {
		result[i] = model.RoleDescription{
			Base:            storable.Base{ID: *role.GetID()},
			OrganizationRef: organizationRef,
		}
	}
	return result
}

// Create creates a new role in an organization.
func (rm *RoleManager) Create(ctx context.Context, organizationRef bson.ObjectID, description *model.Describable) (*model.RoleDescription, error) {
	if err := rm.validateObjectIDs(organizationRef); err != nil {
		return nil, err
	}

	role := &model.RoleDescription{
		OrganizationRef: organizationRef,
		Describable:     *description,
	}
	if err := rm.rdb.Create(ctx, role); err != nil {
		rm.logger.Warn("Failed to create role", zap.Error(err), mzap.ObjRef("organization_ref", organizationRef))
		return nil, err
	}

	rm.logger.Info("Role created successfully", mzap.StorableRef(role), mzap.ObjRef("organization_ref", organizationRef))
	return role, nil
}

// Assign assigns a role to a user in the given organization.
func (rm *RoleManager) Assign(ctx context.Context, role *model.Role) error {
	if err := rm.validateObjectIDs(role.DescriptionRef, role.AccountRef, role.OrganizationRef); err != nil {
		return err
	}
	assogment := nstructures.RoleAssignment{Role: *role}
	err := rm.enforcer.rdb.Create(ctx, &assogment)
	return rm.logPolicyResult("assign", err == nil, err, role.DescriptionRef, role.AccountRef, role.OrganizationRef)
}

// Delete removes a role entirely and cleans up associated Casbin policies.
func (rm *RoleManager) Delete(ctx context.Context, roleRef bson.ObjectID) error {
	if err := rm.validateObjectIDs(roleRef); err != nil {
		rm.logger.Warn("Failed to delete role", mzap.ObjRef("role_ref", roleRef))
		return err
	}

	if err := rm.rdb.Delete(ctx, roleRef); err != nil {
		rm.logger.Warn("Failed to delete role", mzap.ObjRef("role_ref", roleRef))
		return err
	}

	if err := rm.enforcer.rdb.DeleteRole(ctx, roleRef); err != nil {
		rm.logger.Warn("Failed to remove role", zap.Error(err), mzap.ObjRef("role_ref", roleRef))
		return err
	}

	rm.logger.Info("Role deleted successfully along with associated policies", mzap.ObjRef("role_ref", roleRef))
	return nil
}

// Revoke removes a role from a user.
func (rm *RoleManager) Revoke(ctx context.Context, roleRef, accountRef, organizationRef bson.ObjectID) error {
	if err := rm.validateObjectIDs(roleRef, accountRef, organizationRef); err != nil {
		return err
	}

	err := rm.enforcer.rdb.RemoveRole(ctx, roleRef, organizationRef, accountRef)
	return rm.logPolicyResult("revoke", err == nil, err, roleRef, accountRef, organizationRef)
}

// logPolicyResult logs results for Assign and Revoke.
func (rm *RoleManager) logPolicyResult(action string, result bool, err error, roleRef, accountRef, organizationRef bson.ObjectID) error {
	if err != nil {
		rm.logger.Warn("Failed to "+action+" role", zap.Error(err), mzap.ObjRef("role_ref", roleRef), mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
		return err
	}
	msg := "Role " + action + "ed successfully"
	if !result {
		msg = "Role already " + action + "ed"
	}
	rm.logger.Info(msg, mzap.ObjRef("role_ref", roleRef), mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
	return nil
}

// List retrieves all roles in an organization or all roles if organizationRef is zero.
func (rm *RoleManager) List(ctx context.Context, organizationRef bson.ObjectID) ([]model.RoleDescription, error) {
	roles4Venues, err := rm.enforcer.rdb.RolesForVenue(ctx, organizationRef)
	if err != nil {
		rm.logger.Warn("Failed to fetch grouping policies", zap.Error(err), mzap.ObjRef("organization_ref", organizationRef))
		return nil, err
	}

	roles := rm.fetchRolesFromPolicies(roles4Venues, organizationRef)
	rm.logger.Info("Retrieved roles for organization", mzap.ObjRef("organization_ref", organizationRef), zap.Int("count", len(roles)))
	return roles, nil
}
