module BulletTrain::LoadsAndAuthorizesResource
  extend ActiveSupport::Concern

  class_methods do
    # Returns an array of module names based on the classes namespace minus regex_to_remove_controller_namespace
    def model_namespace_from_controller_namespace
      name
        .gsub(regex_to_remove_controller_namespace || //, "")
        .split("::")
        .tap(&:pop) # drops actual class name
    end

    def regex_to_remove_controller_namespace
      return super if defined?(super)

      raise "This is a template method that needs to be implemented by controllers including LoadsAndAuthorizesResource."
    end

    # this is one of the few pieces of 'magical' functionality that bullet train implements
    # for you in your controllers beyond that is provided by the underlying gems that we've
    # tied together. we've taken the liberty of doing this because it's heavily based on
    # cancancan's `load_and_authorize_resource` method, which is awesome, but it also
    # implements a lot of the options required to make that method work very well for our
    # controllers in the account namespace, including our shallow nested routes.
    #
    # there are also some complications that were introduced into this method by our support
    # for namespaced models and controllers. (we introduced this complexity in support of
    # namespacing our `Oauth::` models and controllers.)
    #
    # to help you understand the code below, usually `through` is `team`
    # and `model` is something like `project`.
    def account_load_and_authorize_resource(model, positional_through = nil, through: positional_through, collection_actions: [], member_actions: [], except: [], **options)
      # options are now required, because you have to have at least a 'through' setting.

      # we used to support calling this method with a signature like this:
      #
      #   `account_load_and_authorize_resource [:oauth, :twitter_account], :team`
      #
      # however this abstraction was too short-sighted so we've updated this method to accept the exact same method
      # signature as cancancan's original `load_and_authorize_resource` method.
      if model.is_a?(Array)
        raise "Bullet Train has depreciated this method of calling `account_load_and_authorize_resource`. Read the comments on this line of source for more details."
      end

      # fetch the namespace of the controller. this should generally match the namespace of the model, except for the
      # `account` part.
      namespace = model_namespace_from_controller_namespace

      model_class_names = namespace.size.downto(0).map do
        [*namespace, model.to_s.classify].join("::").tap { namespace.pop }
      end

      model_class = model_class_names.find(&:safe_constantize)&.safe_constantize
      unless model_class
        raise "Your 'account_load_and_authorize_resource' is broken. We tried #{model_class_names.join(" and ")}, but didn't find a valid class name."
      end

      through_as_symbols = Array(through)
      through_class_names = through_as_symbols.map do |through_as_symbol|
        # reflect on the belongs_to association of the child model to figure out the class names of the parents.
        association = model_class.reflect_on_association(through_as_symbol)
        unless association
          raise "Your 'account_load_and_authorize_resource' is broken. Tried to reflect on the `#{through_as_symbol}` association of #{model_class_names}, but didn't find one."
        end

        association.klass.name
      end

      if through_as_symbols.count > 1 && !options[:polymorphic]
        raise "When a resource can be loaded through multiple parents, please specify the 'polymorphic' option to tell us what that controller calls the parent, e.g. `polymorphic: :imageable`."
      end

      instance_variable_name = "@#{options[:polymorphic] || through_as_symbols.first}"

      # `collection_actions:` and `member_actions:` provide support for shallow nested resources, which
      # keep our routes tidy even after many levels of nesting. most people
      # i talk to don't actually know about this feature in rails, but it's
      # actually the recommended approach in the rails routing documentation.
      #
      # also, similar to `load_and_authorize_resource`, people can pass in additional
      # actions for which the resource should be loaded, but because we're making
      # separate calls to `load_and_authorize_resource` for member and collection
      # actions, we ask controllers to specify these actions separately, e.g.:
      #   `account_load_and_authorize_resource :invitation, :team, member_actions: [:accept, :promote]`
      #
      # `except:` is native to cancancan and allows you to skip account_load_and_authorize_resource
      # for a specific action that would otherwise run it (e.g. see invitations#show.)
      collection_actions = (%i[index new create reorder] + collection_actions) - except
      member_actions = (%i[show edit update destroy] + member_actions) - except

      # NOTE: because we're using prepend for all of these, these are written in backwards order
      # of how they'll be executed during a request!

      # 4. finally, load the team and parent resource if we can.
      prepend_before_action :load_team

      # x. this and the thing below it are only here to make a sortable concern possible.
      prepend_before_action only: member_actions do
        @child_object = instance_variable_get(:"@#{model}")
        @parent_object = instance_variable_get instance_variable_name
      end

      prepend_before_action only: collection_actions do
        @parent_object = instance_variable_get instance_variable_name
        @child_collection = options[:through_association].presence&.to_sym || model.to_s.pluralize.to_sym
      end

      prepend_before_action only: member_actions do
        model_instance = instance_variable_get(:"@#{model}")
        if model_instance && !instance_variable_defined?(instance_variable_name)
          parent = through_as_symbols.lazy.filter_map { model_instance.public_send(_1) }.first
          instance_variable_set instance_variable_name, parent
        end
      end

      if options[:polymorphic]
        prepend_before_action only: collection_actions do
          unless instance_variable_defined?(:"@#{options[:polymorphic]}")
            parent = through_as_symbols.lazy.filter_map { instance_variable_get :"@#{_1}" }.first
            instance_variable_set :"@#{options[:polymorphic]}", parent
          end
        end
      end

      # 3. on action resource, we have a specific id for the child resource, so load it directly.
      load_and_authorize_resource model,
        options.merge(
          class: model_class.name,
          only: member_actions,
          prepend: true,
          shallow: true
        )

      # 2. only load the child resource through the parent resource for collection actions.
      load_and_authorize_resource model,
        options.merge(
          class: model_class.name,
          through: through_as_symbols,
          only: collection_actions,
          prepend: true,
          shallow: true
        )

      # 1. load the parent resource for collection actions only. (we're using shallow routes.)
      # since a controller can have multiple potential parents, we have to run this as a loop on every possible
      # parent. (the vast majority of controllers only have one parent.)

      through_class_names.each_with_index do |through_class_name, index|
        load_and_authorize_resource through_as_symbols[index],
          options.merge(
            class: through_class_name,
            only: collection_actions,
            prepend: true,
            shallow: true
          )
      end
    end
  end

  def load_team
    @team ||= @child_object&.try(:team) || @parent_object&.try(:team)

    return unless @team

    if defined?(Current) && Current.respond_to?(:team=)
      Current.team = @team
    end

    # If the currently loaded team is saved to the database, make that the user's new current team.
    if @team.try(:persisted?)
      if can? :show, @team
        current_user.update_column(:current_team_id, @team.id)
      end
    end
  end

  # These are methods that `account_load_and_authorize_resource` assumes will be present on any controllers
  # that call that method. In order to use the new Rails default for `config.raise_on_missing_callback_actions = true`
  # we need to have these methods defined on any controller that includes this module. We define them here so
  # that they will exist at the time that `account_load_and_authorize_resource` is called. We assume that controllers
  # will implement real versions of these methods (if needed) to override these dummy methods. We raise, instead of
  # just defining empty methods, so that if these methods are ever reached it will be more obvious what's happening
  # that it would be if an empty method were called and it appears that nothing happens.
  def reorder
    raise "This is a template method that needs to be implemented by controllers including LoadsAndAuthorizesResource."
  end

  def index
    raise "This is a template method that needs to be implemented by controllers including LoadsAndAuthorizesResource."
  end

  def show
    raise "This is a template method that needs to be implemented by controllers including LoadsAndAuthorizesResource."
  end

  def new
    raise "This is a template method that needs to be implemented by controllers including LoadsAndAuthorizesResource."
  end

  def edit
    raise "This is a template method that needs to be implemented by controllers including LoadsAndAuthorizesResource."
  end

  def create
    raise "This is a template method that needs to be implemented by controllers including LoadsAndAuthorizesResource."
  end

  def update
    raise "This is a template method that needs to be implemented by controllers including LoadsAndAuthorizesResource."
  end

  def destroy
    raise "This is a template method that needs to be implemented by controllers including LoadsAndAuthorizesResource."
  end
end
