module Milia
  module Base

    def self.included(base)
      base.extend ClassMethods
    end

# #############################################################################
# #############################################################################
    module ClassMethods

# ------------------------------------------------------------------------
# acts_as_tenant -- makes a tenanted model
# Forces all references to be limited to current_tenant rows
# ------------------------------------------------------------------------
      def acts_as_tenant()
        belongs_to :tenant
        validates_presence_of :tenant_id

        default_scope lambda { where("#{table_name}.tenant_id = ?", Thread.current[:tenant_id]) }

        # ..........................callback enforcers............................
        after_initialize do |obj|
          # Whenever we initialize a new object it needs to have the correct tenant_id of the current_user.
          # Ensures that destroy can be called on tenanted records which haven't been persisted yet.
          obj.tenant_id ||= Thread.current[:tenant_id]
        end

        # ..........................callback enforcers............................
        before_save do |obj| # force tenant_id to be correct for current_user
          # raise exception if updates attempted on wrong data
          raise ::Milia::Control::InvalidTenantAccess unless obj.tenant_id == Thread.current[:tenant_id]
        end

        # ..........................callback enforcers............................
        before_destroy do |obj| # force tenant_id to be correct for current_user
          if (obj.tenant_id != Thread.current[:tenant_id])
            raise ::Milia::Control::InvalidTenantAccess
          end
        end

      end

# ------------------------------------------------------------------------
# acts_as_universal -- makes a univeral (non-tenanted) model
# Forces all reference to the universal tenant (nil)
# ------------------------------------------------------------------------
      def acts_as_universal()
        belongs_to :tenant

        default_scope { where("#{table_name}.tenant_id IS NULL") }

        # ..........................callback enforcers............................
        before_save do |obj| # force tenant_id to be universal
          if obj.tenant_id.present?
            raise ::Milia::Control::InvalidTenantAccess
          end
        end

        # ..........................callback enforcers............................
        before_destroy do |obj| # force tenant_id to be universal
          raise ::Milia::Control::InvalidTenantAccess unless obj.tenant_id.nil?
        end

      end

# ------------------------------------------------------------------------
# acts_as_universal_and_determines_tenant_reference
# All the characteristics of acts_as_universal AND also does the magic
# of binding a user to a tenant
# ------------------------------------------------------------------------
      def acts_as_universal_and_determines_account()
        include ::Milia::InviteMember
        has_and_belongs_to_many :tenants

        acts_as_universal()

        # validate that a tenant exists prior to a user creation
        before_create do |new_user|
          if Thread.current[:tenant_id].blank? ||
              !Thread.current[:tenant_id].kind_of?(Integer) ||
              Thread.current[:tenant_id].zero?

            raise ::Milia::Control::InvalidTenantAccess, "no existing valid current tenant"

          end
        end # before create callback do

        # before create, tie user with current tenant
        after_create do |new_user|
          tenant = Tenant.find(Thread.current[:tenant_id])
          unless tenant.users.include?(new_user)
						new_user.skip_reconfirmation! # For details why this is needed see milia issue #68
            tenant.users << new_user # add user to this tenant if not already there
          end
        end # before_create do

        before_destroy do |old_user|
          old_user.tenants.clear # remove all tenants for this user
        end # before_destroy do

      end

# ------------------------------------------------------------------------
# ------------------------------------------------------------------------
      def acts_as_universal_and_determines_tenant()
        has_and_belongs_to_many :users

        acts_as_universal()

        before_destroy do |old_tenant|
          old_tenant.users.clear # remove all users from this tenant
          true
        end # before_destroy do
      end

# ------------------------------------------------------------------------
# current_tenant -- returns tenant obj for current tenant
# return nil if no current tenant defined
# ------------------------------------------------------------------------
      def current_tenant()
        begin
          tenant = (
          Thread.current[:tenant_id].blank? ?
              nil :
              Tenant.find(Thread.current[:tenant_id])
          )

          return tenant

        rescue ActiveRecord::RecordNotFound
          return nil
        end
      end

# ------------------------------------------------------------------------
# current_tenant_id -- returns tenant_id for current tenant
# ------------------------------------------------------------------------
      def current_tenant_id()
        return Thread.current[:tenant_id]
      end

# ------------------------------------------------------------------------
# set_current_tenant -- model-level ability to set the current tenant
# NOTE: *USE WITH CAUTION* normally this should *NEVER* be done from
# the models ... it's only useful and safe WHEN performed at the start
# of a background job (DelayedJob#perform)
# ------------------------------------------------------------------------
      def set_current_tenant(tenant)
        # able to handle tenant obj or tenant_id
        case tenant
          when Tenant then
            tenant_id = tenant.id
          when Integer then
            tenant_id = tenant
          else
            raise ArgumentError, "invalid tenant object or id"
        end # case

        old_id                     = (Thread.current[:tenant_id].nil? ? '%' : Thread.current[:tenant_id])
        Thread.current[:tenant_id] = tenant_id
        logger.debug("MILIA >>>>> [Tenant#change_tenant] new: #{tenant_id}\told:#{old_id}") unless logger.nil?

      end

# ------------------------------------------------------------------------
# ------------------------------------------------------------------------

# ------------------------------------------------------------------------
# where_restrict_tenant -- gens tenant restrictive where clause for each klass
# NOTE: subordinate join tables will not get the default scope by Rails
# theoretically, the default scope on the master table alone should be sufficient
# in restricting answers to the current_tenant alone .. HOWEVER, it doesn't feel
# right. adding an additional .where( where_restrict_tenants(klass1, klass2,...))
# for each of the subordinate models in the join seems like a nice safety issue.
# ------------------------------------------------------------------------
      def where_restrict_tenant(*args)
        args.map { |klass| "#{klass.table_name}.tenant_id = #{Thread.current[:tenant_id]}" }.join(" AND ")
      end

# ------------------------------------------------------------------------
# ------------------------------------------------------------------------

    end # module ClassMethods
# #############################################################################
# #############################################################################

  end # module Base
end # module Milia
