# frozen_string_literal: true

module Decidim
  # This class wraps the logic to trace resource changes and their authorship.
  # It is expected to be used with classes implementing the `Decidim::Traceable`
  # concern. Version authors can be retrieved using the methods in
  # `Decidim::TraceabilityHelper`.
  #
  # Examples:
  #
  #     # consider MyResource implements Decidim::Traceable
  #     resource = Decidim::Traceability.new.create!(MyResource, author, params)
  #     resource.versions.count # => 1
  #     resource.versions.last.whodunnit # => author.to_gid.to_s
  #     resource.versions.last.event # => "create"
  #     resource = Decidim::Traceability.new.update!(resource, author, params)
  #     resource.versions.count # => 2
  #     resource.versions.last.event # => "update"
  #
  # This class uses the `paper_trail` gem internally, so refer to its documentation
  # for further info on how to interact with versions.
  class Traceability
    # Calls the `create` method to the given class and sets the author of the version.
    #
    # klass - An ActiveRecord class that implements `Decidim::Traceable`
    # author - An object that implements `to_gid` or a String
    # params - a Hash with the attributes of the new resource
    # extra_log_info - a Hash with extra info that will be saved to the log
    #
    # Returns an instance of `klass`.
    def create(klass, author, params, extra_log_info = {})
      perform_action!(:create, klass, author, extra_log_info) do
        klass.create(params)
      end
    end

    # Calls the `create!` method to the given class and sets the author of the version.
    #
    # klass - An ActiveRecord class that implements `Decidim::Traceable`
    # author - An object that implements `to_gid` or a String
    # params - a Hash with the attributes of the new resource
    # extra_log_info - a Hash with extra info that will be saved to the log
    #
    # Returns an instance of `klass`.
    def create!(klass, author, params, extra_log_info = {})
      perform_action!(:create, klass, author, extra_log_info) do
        klass.create!(params)
      end
    end

    # Performs the given block and sets the author of the action.
    # It also logs the action with the given `action` parameter.
    # The action and the logging are run inside a transaction.
    #
    # action - a String or Symbol representing the action performed
    # resource - An ActiveRecord instance that implements `Decidim::Traceable`
    # author - An object that implements `to_gid` or a String
    # extra_log_info - a Hash with extra info that will be saved to the log
    #
    # Returns whatever the given block returns.
    def perform_action!(action, resource, author, extra_log_info = {})
      PaperTrail.request(whodunnit: gid(author)) do
        Decidim::ApplicationRecord.transaction do
          result = block_given? ? yield : nil
          loggable_resource = resource.is_a?(Class) ? result : resource
          log(action, author, loggable_resource, extra_log_info)
          result
        end
      end
    end

    # Performs the given block and sets the author of the action without log.
    #
    # author - An object that implements `to_gid` or a String
    #
    # Returns whatever the given block returns.
    def perform_action_without_log!(author)
      PaperTrail.request(whodunnit: gid(author)) do
        Decidim::ApplicationRecord.transaction do
          block_given? ? yield : nil
        end
      end
    end

    # Updates the `resource` with `update!` and sets the author of the version.
    #
    # resource - An ActiveRecord instance that implements `Decidim::Traceable`
    # author - An object that implements `to_gid` or a String
    # params - a Hash with the attributes to update to the resource
    # extra_log_info - a Hash with extra info that will be saved to the log
    #
    # Returns the updated `resource`.
    def update!(resource, author, params, extra_log_info = {})
      perform_action!(:update, resource, author, extra_log_info) do
        resource.update!(params)
        resource
      end
    end

    # Finds the author of the last version of the resource.
    #
    # resource - an object implementing `Decidim::Traceable`
    #
    # Returns an object identifiable via GlobalID or a String.
    def last_editor(resource)
      version_editor(resource.versions.last)
    end

    # Finds the author of the given version.
    #
    # version - an object that responds to `whodunnit` and returns a String.
    #
    # Returns an object identifiable via GlobalID or a String.
    def version_editor(version)
      ::GlobalID::Locator.locate(version.whodunnit) || version.whodunnit
    end

    private

    # Calculates the GlobalID of the version author. If the object does not respond to
    # `to_gid`, then it returns the object itself.
    def gid(author)
      return if author.blank?
      return author.to_gid if author.respond_to?(:to_gid)

      author
    end

    def log(action, user, resource, extra_log_info = {})
      return unless user.is_a?(Decidim::UserBaseEntity)
      # If the record is not valid, it may not yet have an ID causing an
      # exception when trying to save the log record.
      return if resource.nil?
      return unless resource.valid?

      Decidim::ActionLogger.log(
        action,
        user,
        resource,
        version_id(resource),
        extra_log_info
      )
    end

    def version_id(resource)
      return nil unless resource.is_a?(Decidim::Traceable)
      return nil if resource.versions.blank?

      resource.versions.last.id
    end
  end
end
