module Clear::Model::HasSaving
  # Default class-wise read_only? method is `false`
  macro included # When included into Model
    macro included # When included into final Model
      class_property? read_only : Bool = false

      # Import a bulk of models in one SQL insert query.
      # Each model must be non-persisted.
      #
      # `on_conflict` callback can be optionnaly turned on
      # to manage constraints of the database.
      #
      # Note: Old models are not modified. This method return a copy of the
      # models as saved in the database.
      #
      # ## Example:
      # ```crystal
      #
      #  users = [ User.new(id: 1), User.new(id: 2), User.new(id: 3)]
      #  users = User.import(users)
      # ```
      def self.import(array : Enumerable(self), on_conflict : (Clear::SQL::InsertQuery -> )? = nil)
        array.each do |item|
          raise "One of your model is persisted while calling import" if item.persisted?
        end

        hashes = array.map do |item|
          item.trigger_before_events(:save)
          raise "import: Validation failed for `#{item}`" unless item.valid?
          item.trigger_before_events(:create)
          item.to_h
        end

        query = Clear::SQL.insert_into(self.table, hashes).returning("*")
        on_conflict.call(query) if on_conflict

        o = [] of self
        query.fetch(@@connection) do |hash|
          o << Clear::Model::Factory.build(self.name, hash, persisted: true,
          fetch_columns: false, cache: nil).as(self)
        end

        o.each(&.trigger_after_events(:create))
        o.each(&.trigger_after_events(:save))

        o
      end
    end
  end

  getter? persisted : Bool

  # Save the model. If the model is already persisted, will call `UPDATE` query.
  # If the model is not persisted, will call `INSERT`
  #
  # Optionally, you can pass a `Proc` to refine the `INSERT` with on conflict
  # resolution functions.
  #
  # Return `false` if the model cannot be saved (validation issue)
  # Return `true` if the model has been correctly saved.
  #
  # Example:
  #
  # ```crystal
  # u = User.new
  # if u.save
  #   puts "User correctly saved !"
  # else
  #   puts "There was a problem during save: "
  #   # do something with `u.errors`
  # end
  # ```
  #
  # ## `on_conflict` optional parameter
  #
  # Example:
  #
  # ```crystal
  # u = User.new id: 123, email: "email@example.com"
  # u.save(-> (qry) { qry.on_conflict.do_update{ |u| u.set(email: "email@example.com") } #update
  # # IMPORTANT NOTICE: user may not be saved, but will be still detected as persisted !
  # ```
  #
  # You may want to use a block for `on_conflict` optional parameter:
  #
  # ```crystal
  # u = User.new id: 123, email: "email@example.com"
  # u.save do |qry|
  #    qry.on_conflict.do_update{ |u| u.set(email: "email@example.com")
  # end
  # ```
  #
  def save(on_conflict : (Clear::SQL::InsertQuery ->)? = nil)
    return false if self.class.read_only?

    with_triggers(:save) do
      if valid?
        if persisted?
          h = update_h

          if h.any?
            with_triggers(:update) do
              hash = Clear::SQL.update(self.class.full_table_name).set(update_h).where { var("#{self.class.__pkey__}") == self.__pkey__ }.execute(@@connection)
            end
          end
        else
          with_triggers(:create) do
            execute_insert = ->{
              query = Clear::SQL.insert_into(self.class.full_table_name, to_h).returning("*")
              on_conflict.try &.call(query)
              hash = query.execute(@@connection)

              self.reset(hash)
              @persisted = true
            }

            if has_trigger?(:commit, :before) || has_trigger?(:commit, :after)
              Clear::SQL.transaction do
                Clear::SQL.after_commit {
                  trigger_before_events(:commit)
                  trigger_after_events(:commit)
                }
                execute_insert.call
              end
            else
              execute_insert.call
            end
          end
        end

        self.clear_change_flags
        return true
      else
        return false
      end
    end
  end

  def save(&block)
    save(on_conflict: block)
  end

  # Performs `save` call, but instead of returning `false` if validation failed,
  # raise `Clear::Model::InvalidModelError` exception
  def save!(on_conflict : (Clear::SQL::InsertQuery ->)? = nil)
    raise Clear::Model::ReadOnlyError.new(self) if self.class.read_only?

    raise Clear::Model::InvalidError.new(self) unless save(on_conflict)

    self
  end

  # Pass the `on_conflict` optional parameter via block.
  def save!(&block : Clear::SQL::InsertQuery ->)
    save!(block)
  end

  # Set the fields passed as argument and call `save` on the object
  def update(**args)
    set(**args)
    save
  end

  # Set the fields passed as argument and call `save!` on the object
  def update!(**args)
    set(**args)
    save!
  end

  # :nodoc:
  def update(named_tuple : NamedTuple)
    set(named_tuple)
    save
  end

  # :nodoc:
  def update!(named_tuple : NamedTuple)
    set(named_tuple)
    save!
  end

  def reload : self
    set(self.class.query.where { var("#{self.class.__pkey__}") == __pkey__ }.fetch_first!)

    invalidate_caches

    @attributes.clear
    clear_change_flags
    @persisted = true

    self
  end

  #  Delete the model by building and executing a `DELETE` query.
  #  A deleted model is not persisted anymore, and can be saved again.
  #     Clear will do `INSERT` instead of `UPDATE` then
  #  Return `true` if the model has been successfully deleted, and `false` otherwise.
  def delete
    return false unless persisted?

    with_triggers(:delete) do
      Clear::SQL::DeleteQuery.new.from(self.class.full_table_name).where {
        var("#{self.class.__pkey__}") == __pkey__
      }.execute(@@connection)

      @persisted = false
      clear_change_flags
    end

    true
  end
end
