# frozen_string_literal: true

require 'repository/base/version'

require 'repository/base/internals/internals'
require 'repository/support/store_result'

# The `Repository` module functions as a namespace for implementing the
# repository logic in a [Data Mapper pattern](http://martinfowler.com/eaaCatalog/dataMapper.html)
# implementation.
# The Repository module initially included two names:
#
#  1. `Base`; and
#  1. `Support` (a namespace containing several related classes).
#
module Repository
  # Base class for Repository in Data Mapper pattern.
  class Base
    # Internal support code exclusively used by Repository::Base
    # @since 0.0.1
    module Internals
    end
    private_constant :Internals
    include Internals

    attr_reader :dao, :factory

    # Initialise a new `Repository::Base` instance.
    # @param factory Has a .create method to create entities from DAO records.
    # @param dao Data Access Object implements persistence without business
    #            logic.
    def initialize(factory:, dao:)
      validate_initializer_argument(:dao, dao)
      validate_initializer_argument(:factory, factory)
      @factory = factory
      @dao = dao
    end

    # Add a new record with attributes matching the specified entity to the
    # associated DAO.
    # @param entity Entity specifying record to be persisted to new DAO record.
    # @return [Repository::Support::StoreResult] An object containing
    #     information about the success or failure of an action.
    def add(entity)
      record = dao.new filtered_attributes_for(entity)
      RecordSaver.new(record: record, factory: factory).result
    end

    # Return an array of entities matching all records currently in the
    # associated DAO.
    # @return [Array] Array of entities as supplied by the `factory`.
    # @since 0.0.2
    def all
      dao.all.map { |record| factory.create record }
    end

    # Remove a record from the underlying DAO whose slug matches the passed-in
    # identifier.
    # @param identifier [String] [Slug](http://en.wikipedia.org/wiki/Semantic_URL#Slug)
    #                   for record to be deleted.
    # @return [Repository::Support::StoreResult] An object containing
    #     information about the success or failure of an action.
    # @since 0.0.5
    def delete(identifier)
      RecordDeleter.new(identifier: identifier, dao: dao, factory: factory)
                   .delete
    end

    # Find a record in the DAO and, on success, return a corresponding entity
    # using the specified [slug](http://en.wikipedia.org/wiki/Semantic_URL#Slug),
    # *not* a numeric record ID, as a search identifier.
    # @param slug [String] [Slug](http://en.wikipedia.org/wiki/Semantic_URL#Slug)
    #                      for record to be deleted.
    # @return [Repository::Support::StoreResult] An object containing
    #     information about the success or failure of an action.
    # @since 0.0.3
    def find_by_slug(slug)
      SlugFinder.new(slug: slug, dao: dao, factory: factory).find
    end

    # Update a record in the DAO corresponding to the specified identifier,
    # using the specified attribute-name/value pairs.
    # @param identifier [String] [Slug](http://en.wikipedia.org/wiki/Semantic_URL#Slug)
    #                      for record to be deleted.
    # @param updated_attrs [Hash] Attributes to be updated.
    # @since 0.0.4
    # @example
    #   result = user_repo.update @user.slug, params[:user_params]
    #   @user = result.entity if result.success?
    def update(identifier:, updated_attrs:)
      RecordUpdater.new(identifier: identifier, updated_attrs: updated_attrs,
                        dao: dao, factory: factory).update
    end

    private

    # supporting #initialize

    # Verifies that parameter passed to #initialize is a Class.
    # @param arg_sym [Symbol] Which parameter is being validated, either `:dao`
    #                         or `:factory`.
    # @param value Parameter value being validated. Must be a Class.
    # @return [boolean]
    def validate_initializer_argument(arg_sym, value)
      message = "the :#{arg_sym} argument must be a Class"
      raise ArgumentError, message unless value.respond_to? :new
    end

    # supporting #add

    def filtered_attributes_for(entity) # :nodoc:
      entity.attributes.to_hash.reject { |k, _v| k == :errors }
    end
  end # class Repository::Base
end