// casbin_enforcer.go
package casbin

import (
	"context"

	"github.com/casbin/casbin/v2"
	"github.com/mitchellh/mapstructure"
	"github.com/tech/sendico/pkg/auth/anyobject"
	cc "github.com/tech/sendico/pkg/auth/internal/casbin/config"
	"github.com/tech/sendico/pkg/auth/internal/casbin/serialization"
	"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.mongodb.org/mongo-driver/v2/mongo"
	"go.uber.org/zap"
)

// CasbinEnforcer implements the Enforcer interface using Casbin.
type CasbinEnforcer struct {
	logger               mlogger.Logger
	enforcer             *casbin.Enforcer
	roleSerializer       serialization.Role
	permissionSerializer serialization.Policy
}

// NewCasbinEnforcer initializes a new CasbinEnforcer with a MongoDB adapter, logger, and PolicySerializer.
// The 'domain' parameter is no longer stored internally, as the interface requires passing a domainRef per method call.
func NewEnforcer(
	logger mlogger.Logger,
	client *mongo.Client,
	settings model.SettingsT,
) (*CasbinEnforcer, error) {
	var config cc.Config
	if err := mapstructure.Decode(settings, &config); err != nil {
		logger.Warn("Failed to decode Casbin configuration", zap.Error(err), zap.Any("settings", settings))
		return nil, merrors.Internal("failed to decode Casbin configuration")
	}

	// Create a Casbin adapter + enforcer from your config and client.
	l := logger.Named("enforcer")
	e, err := createAdapter(l, &config, client)
	if err != nil {
		logger.Warn("Failed to create Casbin enforcer", zap.Error(err))
		return nil, merrors.Internal("failed to create Casbin enforcer")
	}

	logger.Info("Casbin enforcer created")
	return &CasbinEnforcer{
		logger:               l,
		enforcer:             e,
		permissionSerializer: serialization.NewPolicySerializer(),
		roleSerializer:       serialization.NewRoleSerializer(),
	}, nil
}

// Enforce checks if a user has the specified action permission on an object within a domain.
func (c *CasbinEnforcer) Enforce(
	_ context.Context,
	permissionRef, accountRef, organizationRef, objectRef bson.ObjectID,
	action model.Action,
) (bool, error) {
	// Convert ObjectIDs to strings for Casbin
	account := accountRef.Hex()
	organization := organizationRef.Hex()
	permission := permissionRef.Hex()
	object := anyobject.ID
	if objectRef != bson.NilObjectID {
		object = objectRef.Hex()
	}
	act := string(action)

	c.logger.Debug("Enforcing policy",
		zap.String("account", account), zap.String("organization", organization),
		zap.String("permission", permission), zap.String("object", object),
		zap.String("action", act))

	// Perform the enforcement
	result, err := c.enforcer.Enforce(account, organization, permission, object, act)
	if err != nil {
		c.logger.Warn("Failed to enforce policy", zap.Error(err),
			zap.String("account", account), zap.String("organization", organization),
			zap.String("permission", permission), zap.String("object", object),
			zap.String("action", act))
		return false, err
	}

	c.logger.Debug("Policy enforcement result", zap.Bool("result", result))
	return result, nil
}

// EnforceBatch checks a user’s permission for multiple objects at once.
// It returns a map from objectRef -> boolean indicating whether access is granted.
func (c *CasbinEnforcer) EnforceBatch(
	ctx context.Context,
	objectRefs []model.PermissionBoundStorable,
	accountRef bson.ObjectID,
	action model.Action,
) (map[bson.ObjectID]bool, error) {
	results := make(map[bson.ObjectID]bool, len(objectRefs))
	for _, desc := range objectRefs {
		ok, err := c.Enforce(ctx, desc.GetPermissionRef(), accountRef, desc.GetOrganizationRef(), *desc.GetID(), action)
		if err != nil {
			c.logger.Warn("Failed to enforce", zap.Error(err), mzap.ObjRef("permission_ref", desc.GetPermissionRef()),
				mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", desc.GetOrganizationRef()),
				mzap.ObjRef("object_ref", *desc.GetID()), zap.String("action", string(action)))
			return nil, err
		}
		results[*desc.GetID()] = ok
	}

	return results, nil
}

// GetRoles retrieves all roles assigned to the user within the domain.
func (c *CasbinEnforcer) GetRoles(ctx context.Context, accountRef, orgRef bson.ObjectID) ([]model.Role, error) {
	sub := accountRef.Hex()
	dom := orgRef.Hex()

	c.logger.Debug("Fetching roles for user", zap.String("subject", sub), zap.String("domain", dom))

	// Get all roles for the user in the domain
	sroles, err := c.enforcer.GetFilteredGroupingPolicy(0, sub, "", dom)
	if err != nil {
		c.logger.Warn("Failed to get roles from policies", zap.Error(err),
			zap.String("account_ref", sub), zap.String("organization_ref", dom),
		)
		return nil, merrors.Internal("failed to fetch roles from policies")
	}

	roles := make([]model.Role, 0, len(sroles))
	for _, srole := range sroles {
		role, err := c.roleSerializer.Deserialize(srole)
		if err != nil {
			c.logger.Warn("Failed to deserialize role", zap.Error(err))
			return nil, err
		}
		roles = append(roles, *role)
	}

	c.logger.Debug("Roles fetched successfully", zap.Int("count", len(roles)))
	return roles, nil
}

// GetPermissions retrieves all effective policies for the user within the domain.
func (c *CasbinEnforcer) GetPermissions(ctx context.Context, accountRef, orgRef bson.ObjectID) ([]model.Role, []model.Permission, error) {
	c.logger.Debug("Fetching policies for user", mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", orgRef))

	// Step 1: Retrieve all roles assigned to the user within the domain
	roles, err := c.GetRoles(ctx, accountRef, orgRef)
	if err != nil {
		c.logger.Warn("Failed to get roles", zap.Error(err))
		return nil, nil, err
	}

	// Map to hold unique policies
	permissionsMap := make(map[string]*model.Permission)
	for _, role := range roles {
		// Step 2a: Retrieve all policies associated with the role within the domain
		policies, err := c.enforcer.GetFilteredPolicy(0, role.DescriptionRef.Hex())
		if err != nil {
			c.logger.Warn("Failed to get policies for role", zap.Error(err), mzap.ObjRef("role_ref", role.DescriptionRef))
			continue
		}

		// Step 2b: Process each policy to extract Permission, Action, and Effect
		for _, policy := range policies {

			if len(policy) < 5 {
				c.logger.Warn("Incomplete policy encountered", zap.Strings("policy", policy))
				continue // Ensure the policy line has enough fields
			}

			// Deserialize the policy using
			deserializedPolicy, err := c.permissionSerializer.Deserialize(policy)
			if err != nil {
				c.logger.Warn("Failed to deserialize policy", zap.Error(err), zap.Strings("policy", policy))
				continue
			}

			// Construct a unique key combining Permission ID and Action to prevent duplicates
			policyKey := deserializedPolicy.DescriptionRef.Hex() + ":" + string(deserializedPolicy.Effect.Action)
			if _, exists := permissionsMap[policyKey]; exists {
				continue // Policy-action pair already accounted for
			}

			// Add the Policy to the map
			permissionsMap[policyKey] = &model.Permission{
				RolePolicy: *deserializedPolicy,
				AccountRef: accountRef,
			}
			c.logger.Debug("Policy added to policyMap", zap.Any("policy_key", policyKey))
		}
	}

	// Convert the map to a slice
	permissions := make([]model.Permission, 0, len(permissionsMap))
	for _, permission := range permissionsMap {
		permissions = append(permissions, *permission)
	}

	c.logger.Debug("Permissions fetched successfully", zap.Int("count", len(permissions)))
	return roles, permissions, nil
}
