package native

import (
	"context"
	"errors"

	"github.com/tech/sendico/pkg/auth/internal/native/nstructures"
	"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"
)

type Enforcer struct {
	logger mlogger.Logger
	pdb    PoliciesDB
	rdb    RolesDB
}

func NewEnforcer(
	logger mlogger.Logger,
	db *mongo.Database,
) (*Enforcer, error) {
	e := &Enforcer{logger: logger.Named("enforcer")}

	var err error
	if e.pdb, err = NewPoliciesDBDB(e.logger, db); err != nil {
		e.logger.Warn("Failed to create permission assignments database", zap.Error(err))
		return nil, err
	}

	if e.rdb, err = NewRolesDB(e.logger, db); err != nil {
		e.logger.Warn("Failed to create role assignments database", zap.Error(err))
		return nil, err
	}

	logger.Info("Native enforcer created")
	return e, nil
}

// Enforce checks if a user has the specified action permission on an object within a domain.
func (n *Enforcer) Enforce(
	ctx context.Context,
	permissionRef, accountRef, organizationRef, objectRef bson.ObjectID,
	action model.Action,
) (bool, error) {
	if organizationRef.IsZero() {
		n.logger.Warn("Missing organization context", mzap.AccRef(accountRef),
			mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef),
			mzap.ObjRef("object", objectRef), zap.String("action", string(action)))
		return false, merrors.InvalidArgument("organization context missing", "organizationRef")
	}
	roleAssignments, err := n.rdb.Roles(ctx, accountRef, organizationRef)
	if errors.Is(err, merrors.ErrNoData) {
		n.logger.Debug("No roles defined for account", mzap.AccRef(accountRef))
		return false, nil
	}
	if err != nil {
		n.logger.Warn("Failed to fetch roles while checking permissions", zap.Error(err), mzap.AccRef(accountRef),
			mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef),
			mzap.ObjRef("object", objectRef), zap.String("action", string(action)))
		return false, err
	}
	if len(roleAssignments) == 0 {
		n.logger.Warn("No roles found for account", zap.Error(err), mzap.AccRef(accountRef),
			mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef),
			mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action)))
		return false, merrors.Internal("No roles found for account " + accountRef.Hex())
	}
	allowFound := false // Track if any allow is found across roles

	for _, roleAssignment := range roleAssignments {
		policies, err := n.pdb.PoliciesForPermissionAction(ctx, roleAssignment.DescriptionRef, permissionRef, action)
		if err != nil && !errors.Is(err, merrors.ErrNoData) {
			n.logger.Warn("Failed to fetch permissions", zap.Error(err), mzap.AccRef(accountRef),
				mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef),
				mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action)))
			return false, err
		}

		for _, permission := range policies {
			if permission.Effect.Effect == model.EffectDeny {
				n.logger.Debug("Found denying policy", mzap.ObjRef("account", accountRef),
					mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef),
					mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action)))
				return false, nil // Deny takes precedence immediately
			}

			if permission.Effect.Effect == model.EffectAllow {
				n.logger.Debug("Allowing policy found", mzap.ObjRef("account", accountRef),
					mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef),
					mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action)))
				allowFound = true // At least one allow found
			} else {
				n.logger.Warn("Corrupted policy", mzap.StorableRef(&permission))
				return false, merrors.Internal("Corrupted action effect data for permissions entry " + permission.ID.Hex() + ": " + string(permission.Effect.Effect))
			}
		}
	}

	// Final decision based on whether any allow was found
	if allowFound {
		return true, nil // At least one allow and no deny
	}

	n.logger.Debug("No allowing policy found", mzap.ObjRef("account", accountRef),
		mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef),
		mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action)))

	return false, nil // No allow found, default deny
}

// EnforceBatch checks a user’s permission for multiple objects at once.
// It returns a map from objectRef -> boolean indicating whether access is granted.
func (n *Enforcer) 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))

	// Group objectRefs by organizationRef.
	objectsByVenue := make(map[bson.ObjectID][]model.PermissionBoundStorable)
	for _, obj := range objectRefs {
		organizationRef := obj.GetOrganizationRef()
		objectsByVenue[organizationRef] = append(objectsByVenue[organizationRef], obj)
	}

	// Process each venue group separately.
	for organizationRef, objs := range objectsByVenue {
		// 1. Fetch roles once for this account and venue.
		roles, err := n.rdb.Roles(ctx, accountRef, organizationRef)
		if err != nil {
			if errors.Is(err, merrors.ErrNoData) {
				n.logger.Debug("No roles defined for account", zap.Error(err),
					mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
				// With no roles, mark all objects in this venue as denied.
				for _, obj := range objs {
					results[*obj.GetID()] = false
				}
				// Continue to next venue
				continue
			}
			n.logger.Warn("Failed to fetch roles", zap.Error(err),
				mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
			return nil, err
		}

		// 2. Extract role description references
		var roleRefs []bson.ObjectID
		for _, role := range roles {
			roleRefs = append(roleRefs, role.DescriptionRef)
		}

		// 3. Fetch all policies for these roles and the given action in one call.
		allPolicies, err := n.pdb.PoliciesForRoles(ctx, roleRefs, action)
		if err != nil {
			n.logger.Warn("Failed to fetch policies", zap.Error(err))
			return nil, err
		}

		// 4. Build a lookup map keyed by PermissionRef.
		policyMap := make(map[bson.ObjectID][]nstructures.PolicyAssignment)
		for _, policy := range allPolicies {
			policyMap[policy.DescriptionRef] = append(policyMap[policy.DescriptionRef], policy)
		}

		// 5. Evaluate permissions for each object in this venue group.
		for _, obj := range objs {
			permRef := obj.GetPermissionRef()
			allow := false
			if policies, ok := policyMap[permRef]; ok {
				for _, policy := range policies {
					// Deny takes precedence.
					if policy.Effect.Effect == model.EffectDeny {
						allow = false
						break
					}
					if policy.Effect.Effect == model.EffectAllow {
						allow = true
						// Continue checking in case a deny exists among policies.
					} else {
						// should never get here
						return nil, merrors.Internal("Corrupted permissions effect in policy assignment '" + policy.GetID().Hex() + "': " + string(policy.Effect.Effect))
					}
				}
			}
			results[*obj.GetID()] = allow
		}
	}

	return results, nil
}

// GetRoles retrieves all roles assigned to the user within the domain.
func (n *Enforcer) GetRoles(ctx context.Context, accountRef, organizationRef bson.ObjectID) ([]model.Role, error) {
	n.logger.Debug("Fetching roles for user", mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
	ra, err := n.rdb.Roles(ctx, accountRef, organizationRef)
	if errors.Is(err, merrors.ErrNoData) {
		n.logger.Debug("No roles assigned to user", mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
		return []model.Role{}, nil
	}
	if err != nil {
		n.logger.Warn("Failed to fetch roles", zap.Error(err), mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
		return nil, err
	}

	roles := make([]model.Role, len(ra))
	for i, roleAssignement := range ra {
		roles[i] = roleAssignement.Role
	}

	n.logger.Debug("Fetched roles", zap.Int("roles_count", len(roles)))
	return roles, nil
}

func (n *Enforcer) Reload() error {
	n.logger.Info("Policies reloaded") // do nothing actually
	return nil
}

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

	roles, err := n.GetRoles(ctx, accountRef, organizationRef)
	if err != nil {
		n.logger.Warn("Failed to get roles", zap.Error(err))
		return nil, nil, err
	}

	uniquePermissions := make(map[bson.ObjectID]model.Permission)
	for _, role := range roles {
		perms, err := n.pdb.PoliciesForRole(ctx, role.DescriptionRef)
		if err != nil {
			n.logger.Warn("Failed to get policies for role", zap.Error(err), mzap.ObjRef("role_ref", role.DescriptionRef))
			continue
		}
		n.logger.Debug("Policies fetched for role", mzap.ObjRef("role_ref", role.DescriptionRef), zap.Int("count", len(perms)))
		for _, p := range perms {
			uniquePermissions[*p.GetID()] = model.Permission{
				RolePolicy: model.RolePolicy{
					Policy:             p.Policy,
					RoleDescriptionRef: p.RoleRef,
				},
				AccountRef: accountRef,
			}
		}
	}

	permissionsSlice := make([]model.Permission, 0, len(uniquePermissions))
	for _, permission := range uniquePermissions {
		permissionsSlice = append(permissionsSlice, permission)
	}

	n.logger.Debug("Policies fetched successfully", zap.Int("count", len(permissionsSlice)))
	return roles, permissionsSlice, nil
}
