module Heimdallr
  # {AccessDenied} exception is to be raised when access is denied to an action.
  class AccessDenied < StandardError; end

  module ResourceImplementation
    class << self
      def prepare_options(klass, options)
        options = options.merge :resource => (options[:resource] || klass.name.sub(/Controller$/, '').underscore).to_s

        filter_options = {}
        filter_options[:only]   = options.delete(:only)   if options.has_key?(:only)
        filter_options[:except] = options.delete(:except) if options.has_key?(:except)

        [ options, filter_options ]
      end

      def load(controller, options)
        unless controller.instance_variable_defined?(ivar_name(controller, options))
          if options.has_key? :through
            target = load_target(controller, options)

            if target
              if options[:singleton]
                scope = target.send(:"#{options[:resource].parameterize('_')}")
              else
                scope = target.send(:"#{options[:resource].parameterize('_').pluralize}")
              end
            elsif options[:shallow]
              scope = options[:resource].classify.constantize.scoped
            else
              raise "Cannot fetch #{options[:resource]} via #{options[:through]}"
            end
          else
            scope = options[:resource].classify.constantize.scoped
          end

          loaders = {
            collection: -> {
              controller.instance_variable_set(ivar_name(controller, options), scope)
            },

            new_record: -> {
              controller.instance_variable_set(ivar_name(controller, options),
                  scope.new(controller.params[options[:resource].split('/').last]))
            },

            record: -> {
              controller.instance_variable_set(ivar_name(controller, options),
                  scope.find(controller.params[:"#{options[:resource]}_id"] ||
                             controller.params[:id]))
            },

            related_record: -> {
              if controller.params[:"#{options[:resource]}_id"]
                controller.instance_variable_set(ivar_name(controller, options),
                    scope.find(controller.params[:"#{options[:resource]}_id"]))
              end
            }
          }

          loaders[action_type(controller.params[:action], options)].()
        end
      end

      def authorize(controller, options)
        value = controller.instance_variable_get(ivar_name(controller, options))
        return unless value

        controller.instance_variable_set(ivar_name(controller, options.merge(:insecure => true)), value)

        value = value.restrict(controller.security_context)
        controller.instance_variable_set(ivar_name(controller, options), value)

        case controller.params[:action]
        when 'new', 'create'
          value.assign_attributes(value.reflect_on_security[:restrictions].fixtures[:create])

          unless value.reflect_on_security[:operations].include? :create
            raise Heimdallr::AccessDenied, "Cannot create model"
          end

        when 'edit', 'update'
          value.assign_attributes(value.reflect_on_security[:restrictions].fixtures[:update])

          unless value.reflect_on_security[:operations].include? :update
            raise Heimdallr::AccessDenied, "Cannot update model"
          end

        when 'destroy'
          unless value.destroyable?
            raise Heimdallr::AccessDenied, "Cannot delete model"
          end
        end unless options[:related]
      end

      def load_target(controller, options)
        Array.wrap(options[:through]).map do |parent|
          loaded = controller.instance_variable_get(:"@#{parent}")
          unless loaded
            load(controller, :resource => parent.to_s, :related => true)
            loaded = controller.instance_variable_get(:"@#{parent}")
          end
          if loaded && options[:authorize_chain]
            authorize(controller, :resource => parent.to_s, :related => true)
          end
          controller.instance_variable_get(:"@#{parent}")
        end.reject(&:nil?).first
      end

      def ivar_name(controller, options)
        if action_type(controller.params[:action], options) == :collection
          :"@#{options[:resource].parameterize('_').pluralize}"
        else
          :"@#{options[:resource].parameterize('_')}"
        end
      end

      def action_type(action, options)
        if options[:related]
          :related_record
        else
          action = action.to_sym
          case action
          when :index
            :collection
          when :new, :create
            :new_record
          when :show, :edit, :update, :destroy
            :record
          else
            if options[:collection] && options[:collection].include?(action)
              :collection
            elsif options[:new] && options[:new].include?(action)
              :new_record
            else
              :record
            end
          end
        end
      end
    end
  end

  # {Resource} is a mixin providing CanCan-like interface for Rails controllers.
  module Resource
    extend ActiveSupport::Concern

    module ClassMethods
      def load_and_authorize_resource(options={})
        options[:authorize_chain] = true
        load_resource(options)
        authorize_resource(options)
      end

      def load_resource(options={})
        options, filter_options = Heimdallr::ResourceImplementation.prepare_options(self, options)
        self.own_heimdallr_options = options

        before_filter filter_options do |controller|
          Heimdallr::ResourceImplementation.load(controller, options)
        end
      end

      def authorize_resource(options={})
        options, filter_options = Heimdallr::ResourceImplementation.prepare_options(self, options)
        self.own_heimdallr_options = options

        before_filter filter_options do |controller|
          Heimdallr::ResourceImplementation.authorize(controller, options)
        end
      end

      protected

      def own_heimdallr_options=(options)
        cattr_accessor :heimdallr_options
        self.heimdallr_options = options
      end
    end
  end
end