# :nodoc:
module Clear::Model::Relations::BelongsToMacro
  macro generate(self_type, relation)
    {% begin %}
      {%
        method_name = relation[:name].id
        relation_type = relation[:type]

        foreign_key = (relation[:foreign_key] || "#{method_name.stringify.underscore.id}_id").id

        foreign_key_type = relation[:foreign_key_type].id

        presence = relation[:presence]
        primary = relation[:primary]

        nilable = relation[:nilable]
      %}

      __define_association_cache__({{method_name}}, {{relation_type}})

      column {{foreign_key}} : {{foreign_key_type}}{{ nilable ? "?".id : "".id }}, primary: {{primary}}, presence: false

      # :nodoc:
      def self.__relation_filter_{{method_name}}__(query)
        query.inner_join({{self_type}}.table){ var( {{self_type}}.table, "{{foreign_key}}" ) == var( {{relation_type}}.table, {{relation_type}}.__pkey__ ) }
      end

      # :nodoc:
      def self.__relation_key_table_{{method_name}}__ : Tuple(String, String)
        {
          {{relation_type}}.table.to_s,
          {{relation_type}}.__pkey__.to_s
        }
      end

      __on_init__ do
        {{self_type}}::RELATION_FILTERS["{{method_name}}"] = -> (x : Clear::SQL::SelectBuilder) { __relation_filter_{{method_name}}__(x) }
      end

      # The method {{method_name}} is a `belongs_to` relation
      #   to {{relation_type}}
      def {{method_name}} : {{ relation_type }}{{ nilable ? "?".id : "".id }}
        if cached = @_cached_{{method_name}}
          cached
        elsif @cache.try &.active? "{{method_name}}"
          cache = @cache.not_nil!

          model = @_cached_{{method_name}} = cache.hit("{{method_name}}",
            self.{{foreign_key}}_column.to_sql_value,
            {{relation_type}}
          ).first?

          {% if !nilable %} raise Clear::SQL::RecordNotFoundError.new if model.nil? {% end %}

          model
        else
          fkey = self.{{foreign_key}}_column.value(nil)

          {% if !nilable %}
            raise Clear::SQL::RecordNotFoundError.new if fkey.nil?
          {% else %}
            return nil if fkey.nil?
          {% end %}

          @_cached_{{method_name}} =
            {{relation_type}}.query
              .where{ var({{relation_type}}.table, {{relation_type}}.__pkey__) == fkey }
              .{{ !nilable ? "first!".id : "first".id }}
        end
      end

      {% if nilable %}
        def {{method_name}}! : {{relation_type}}
          model = self.{{method_name}}
          raise Clear::SQL::RecordNotFoundError.new if model.nil?
          model.not_nil!
        end # /  *!
      {% end %}

      def {{method_name}}=(model : {{ relation_type }}{{ nilable ? "?".id : "".id }})
        if model && model.try &.persisted?
          raise "`#{model.__pkey_column__.name}` must be fetchable when assigning to a `belongs_to` relation." unless model.__pkey_column__.defined?
          @{{foreign_key}}_column.value = model.__pkey__
        else
          {% if nilable %}
            @_cached_{{method_name}} = nil
            @{{foreign_key}}_column.value = nil
          {% end %}
        end

        @_cached_{{method_name}} = model
      end

      # :nodoc:
      # save the belongs_to model first if needed
      def _bt_save_{{method_name}}
        c = @_cached_{{method_name}}

        {% unless nilable %}
          add_error("{{method_name}}", "must be present") if c.nil? && self.{{foreign_key}}_column.value(nil).nil?
        {% end %}

        return if c.nil?

        unless c.persisted?
          if c.save
            @{{foreign_key}}_column.reset_convert(c.__pkey__)
          else
            add_error("{{method_name}}", c.print_errors)
          end
        else # relation model has been persisted after assigned to current model
          self.{{foreign_key}} = c.__pkey__
        end

      end # / _bt_save_*

      __on_init__ do
        {{self_type}}.before(:validate) do |mdl|
          mdl.as(self)._bt_save_{{method_name}}
        end
      end

      class Collection
        def with_{{method_name}}(fetch_columns = false, &block : {{relation_type}}::Collection -> ) : self
          before_query do
            sub_query = self.dup.clear_select.select("#{ item_class.table }.{{foreign_key.id}}")

            cached_qry = {{relation_type}}.query.where{ var({{relation_type}}.table, {{relation_type}}.__pkey__ ).in?(sub_query) }

            block.call(cached_qry)

            cache_name = {{"#{method_name}"}}
            @cache.active cache_name

            cached_qry.each(fetch_columns: fetch_columns) do |mdl|
              @cache.set(cache_name, mdl.__pkey__, [mdl])
            end
          end

          self
        end # / with_*

        def with_{{method_name}}(fetch_columns = false) : self
          with_{{method_name}}(fetch_columns){}
          self
        end # / with_*

      end # / Collection
    {% end %} # / begin block
  end # / macro

end
