# frozen_string_literal: true

require "active_support/concern"
require "action_policy/behaviour"

module ActionPolicy
  # Raised when `authorize!` hasn't been called for action
  class UnauthorizedAction < Error
    def initialize(controller, action)
      super("Action '#{controller}##{action}' hasn't been authorized")
    end
  end

  # Controller concern.
  # Add `authorize!` and `allowed_to?` methods,
  # provide `verify_authorized` hook.
  module Controller
    extend ActiveSupport::Concern

    include ActionPolicy::Behaviour
    include ActionPolicy::Behaviours::ThreadMemoized
    include ActionPolicy::Behaviours::Memoized
    include ActionPolicy::Behaviours::Namespaced

    included do
      helper_method :allowed_to? if respond_to?(:helper_method)

      attr_writer :authorize_count
      attr_reader :verify_authorized_skipped

      protected :authorize_count=, :authorize_count
    end

    # Authorize action against a policy.
    #
    # Policy is inferred from record
    # (unless explicitly specified through `with` option).
    #
    # If action is not provided, it's inferred from `action_name`.
    #
    # If record is not provided, tries to infer the resource class
    # from controller name (i.e. `controller_name.classify.safe_constantize`).
    #
    # Raises `ActionPolicy::Unauthorized` if check failed.
    def authorize!(record = :__undef__, to: nil, **options)
      to ||= :"#{action_name}?"

      super(record, to: to, **options)

      self.authorize_count += 1
    end

    # Tries to infer the resource class from controller name
    # (i.e. `controller_name.classify.safe_constantize`).
    def implicit_authorization_target
      controller_name.classify.safe_constantize
    end

    def verify_authorized
      Kernel.raise UnauthorizedAction.new(controller_path, action_name) if
        authorize_count.zero? && !verify_authorized_skipped
    end

    def authorize_count
      @authorize_count ||= 0
    end

    def skip_verify_authorized!
      @verify_authorized_skipped = true
    end

    class_methods do
      # Adds after_action callback to check that
      # authorize! method has been called.
      def verify_authorized(**options)
        after_action :verify_authorized, options
      end

      # Skips verify_authorized after_action callback.
      def skip_verify_authorized(**options)
        skip_after_action :verify_authorized, options
      end
    end
  end
end