# frozen_string_literal: true

require "hanami/model/types"

module Hanami
  module Model
    module Associations
      # One-To-Many association
      #
      # @since 0.7.0
      # @api private
      class HasMany
        # @since 0.7.0
        # @api private
        def self.schema_type(entity)
          type = Sql::Types::Schema::AssociationType.new(entity)
          Types::Strict::Array.member(type)
        end

        # @since 0.7.0
        # @api private
        attr_reader :repository

        # @since 0.7.0
        # @api private
        attr_reader :source

        # @since 0.7.0
        # @api private
        attr_reader :target

        # @since 0.7.0
        # @api private
        attr_reader :subject

        # @since 0.7.0
        # @api private
        attr_reader :scope

        # @since 0.7.0
        # @api private
        def initialize(repository, source, target, subject, scope = nil)
          @repository = repository
          @source     = source
          @target     = target
          @subject    = subject.to_hash unless subject.nil?
          @scope      = scope || _build_scope
          freeze
        end

        # @since 0.7.0
        # @api private
        def create(data)
          entity.new(command(:create, aggregate(target), mapper: nil, use: [:timestamps])
            .call(serialize(data)))
        rescue => exception
          raise Hanami::Model::Error.for(exception)
        end

        # @since 0.7.0
        # @api private
        def add(data)
          command(:create, relation(target), use: [:timestamps])
            .call(associate(serialize(data)))
        rescue => exception
          raise Hanami::Model::Error.for(exception)
        end

        # @since 0.7.0
        # @api private
        def remove(id)
          command(:update, relation(target), use: [:timestamps])
            .by_pk(id)
            .call(unassociate)
        end

        # @since 0.7.0
        # @api private
        def delete
          scope.delete
        end

        # @since 0.7.0
        # @api private
        def each(&blk)
          scope.each(&blk)
        end

        # @since 0.7.0
        # @api private
        def map(&blk)
          to_a.map(&blk)
        end

        # @since 0.7.0
        # @api private
        def to_a
          scope.to_a
        end

        # @since 0.7.0
        # @api private
        def where(condition)
          __new__(scope.where(condition))
        end

        # @since 0.7.0
        # @api private
        def count
          scope.count
        end

        private

        # @since 0.7.0
        # @api private
        def command(target, relation, options = {})
          repository.command(target, relation, options)
        end

        # @since 0.7.0
        # @api private
        def entity
          repository.class.entity
        end

        # @since 0.7.0
        # @api private
        def relation(name)
          repository.relations[name]
        end

        # @since 0.7.0
        # @api private
        def aggregate(name)
          repository.aggregate(name)
        end

        # @since 0.7.0
        # @api private
        def association(name)
          relation(target).associations[name]
        end

        # @since 0.7.0
        # @api private
        def associate(data)
          relation(source)
            .associations[target]
            .associate(container.relations, data, subject)
        end

        # @since 0.7.0
        # @api private
        def unassociate
          {foreign_key => nil}
        end

        # @since 0.7.0
        # @api private
        def container
          repository.container
        end

        # @since 0.7.0
        # @api private
        def primary_key
          association_keys.first
        end

        # @since 0.7.0
        # @api private
        def foreign_key
          association_keys.last
        end

        # Returns primary key and foreign key
        #
        # @since 0.7.0
        # @api private
        def association_keys
          target_association
            .__send__(:join_key_map, container.relations)
        end

        # Returns the targeted association for a given source
        #
        # @since 0.7.0
        # @api private
        def target_association
          relation(source).associations[target]
        end

        # @since 0.7.0
        # @api private
        def _build_scope
          result = relation(target_association.target.to_sym)
          result = result.where(foreign_key => subject.fetch(primary_key)) unless subject.nil?
          result.as(Model::MappedRelation.mapper_name)
        end

        # @since 0.7.0
        # @api private
        def __new__(new_scope)
          self.class.new(repository, source, target, subject, new_scope)
        end

        def serialize(data)
          Utils::Hash.deep_serialize(data)
        end
      end
    end
  end
end
