# frozen_string_literal: true

require 'attr_json/nested_attributes/builder'
require 'attr_json/nested_attributes/writer'

module AttrJson
  # The implementation is based on ActiveRecord::NestedAttributes, from
  # https://github.com/rails/rails/blob/a45f234b028fd4dda5338e5073a3bf2b8bf2c6fd/activerecord/lib/active_record/nested_attributes.rb
  #
  # Re-used, and customized/overrode methods to match our implementation.
  # Copied over some implementation so we can use in ActiveModel's that original
  # isn't compatible with.
  # The original is pretty well put together and has had very low churn history.
  #
  #
  # Much of the AR implementation, copied, just works,
  # if we define `'#{attribute_name}_attributes='` methods that work. That's mostly what
  # we have to do here.
  #
  # Unlike AR, we try to put most of our implementation in seperate
  # implementation helper instances, instead of adding a bazillion methods to the model itself.
  module NestedAttributes
    extend ActiveSupport::Concern

    class_methods do
      # Much like ActiveRecord `accepts_nested_attributes_for`, but used with embedded
      # AttrJson::Model-type attributes (single or array). See doc page on Forms support.
      #
      # Note some AR options are _not_ supported.
      #
      # * _allow_destroy_, no such option. Effectively always true, doesn't make sense to try to gate this with our implementation.
      # * _update_only_, no such option. Not really relevant to this architecture where you're embedded models have no independent existence.
      #
      # If called on an array of 'primitive' (not AttrJson::Model) objects, it will do a kind of weird
      # thing where it creates an `#{attribute_name}_attributes=` method that does nothing
      # but filter out empty strings and nil values. This can be convenient in hackily form
      # handling array of primitives, see guide doc on forms.
      #
      # @overload attr_json_accepts_nested_attributes_for(define_build_method: true, reject_if: nil, limit: nil)
      #   @param define_build_method [Boolean] Default true, provide `build_attribute_name`
      #     method that works like you expect. [Cocoon](https://github.com/nathanvda/cocoon),
      #     for example, requires this. When true, you will get `model.build_#{attr_name}`
      #     methods. For array attributes, the attr_name will be singularized, as AR does.
      #   @param reject_if [Symbol,Proc] Allows you to specify a Proc or a Symbol pointing to a method
      #     that checks whether a record should be built for a certain attribute
      #     hash. Much like in AR accepts_nested_attributes_for.
      #   @param limit [Integer,Proc,Symbol] Allows you to specify the maximum number of associated records that
      #     can be processed with the nested attributes. Much like AR accepts_nested_attributes for.
      def attr_json_accepts_nested_attributes_for(*attr_names)
        options = { define_build_method: true }
        options.update(attr_names.extract_options!)
        options.assert_valid_keys(:reject_if, :limit, :define_build_method)
        options[:reject_if] = ActiveRecord::NestedAttributes::ClassMethods::REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank

        unless respond_to?(:nested_attributes_options)
          # Add it when we're in a AttrJson::Model.  In an ActiveRecord::Base we'll just use the
          # existing one, it'll be okay.
          # https://github.com/rails/rails/blob/c14deceb9f36f82cd5ca3db214d85e1642eb0bfd/activerecord/lib/active_record/nested_attributes.rb#L16
          class_attribute :nested_attributes_options, instance_writer: false
          self.nested_attributes_options ||= {}
        end

        attr_names.each do |attr_name|
          attr_def = attr_json_registry[attr_name]

          unless attr_def
            raise ArgumentError, "No attr_json found for name '#{attr_name}'. Has it been defined yet?"
          end

          unless attr_def.array_type? || attr_def.single_model_type?
            raise TypeError, "attr_json_accepts_nested_attributes_for is only for array or nested model types; `#{attr_name}` is type #{attr_def.type.type.inspect}"
          end

          # We're sharing AR class attr in an AR, or using our own in a Model.
          nested_attributes_options = self.nested_attributes_options.dup
          nested_attributes_options[attr_name.to_sym] = options
          self.nested_attributes_options = nested_attributes_options

          _attr_jsons_module.module_eval do
            if method_defined?(:"#{attr_name}_attributes=")
              remove_method(:"#{attr_name}_attributes=")
            end
            define_method "#{attr_name}_attributes=" do |attributes|
              Writer.new(self, attr_name).assign_nested_attributes(attributes)
            end
          end

          # No build method for our wacky array of primitive type.
          if options[:define_build_method] && !attr_def.array_of_primitive_type?
            _attr_jsons_module.module_eval do
              build_method_name = "build_#{attr_name.to_s.singularize}"
              if method_defined?(build_method_name)
                remove_method(build_method_name)
              end
              define_method build_method_name do |params = {}|
                Builder.new(self, attr_name).build(params)
              end
            end
          end
        end
      end
    end
  end
end
