# frozen_string_literal: true
# rubocop:todo all

module Mongoid
  module Persistable

    # Defines behavior for persistence operations that create new documents.
    module Creatable
      extend ActiveSupport::Concern

      # Insert a new document into the database. Will return the document
      # itself whether or not the save was successful.
      #
      # @example Insert a document.
      #   document.insert
      #
      # @param [ Hash ] options Options to pass to insert.
      #
      # @return [ Document ] The persisted document.
      def insert(options = {})
        prepare_insert(options) do
          if embedded?
            insert_as_embedded
          else
            insert_as_root
          end
        end
      end

      private

      # Get the atomic insert for embedded documents, either a push or set.
      #
      # @api private
      #
      # @example Get the inserts.
      #   document.inserts
      #
      # @return [ Hash ] The insert ops.
      def atomic_inserts
        { atomic_insert_modifier => { atomic_position => as_attributes }}
      end

      # Insert the embedded document.
      #
      # @api private
      #
      # @example Insert the document as embedded.
      #   document.insert_as_embedded
      #
      # @return [ Document ] The document.
      def insert_as_embedded
        raise Errors::NoParent.new(self.class.name) unless _parent
        if _parent.new_record?
          _parent.insert
        else
          selector = _parent.atomic_selector
          _root.collection.find(selector).update_one(
              positionally(selector, atomic_inserts),
              session: _session)
        end
      end

      # Insert the root document.
      #
      # @api private
      #
      # @example Insert the document as root.
      #   document.insert_as_root
      #
      # @return [ Document ] The document.
      def insert_as_root
        collection.insert_one(as_attributes, session: _session)
      end

      # Post process an insert, which sets the new record attribute to false
      # and flags all the children as persisted.
      #
      # @api private
      #
      # @example Post process the insert.
      #   document.post_process_insert
      #
      # @return [ true ] true.
      def post_process_insert
        self.new_record = false
        remember_storage_options!
        flag_descendants_persisted
        true
      end

      # Prepare the insert for execution. Validates and runs callbacks, etc.
      #
      # @api private
      #
      # @example Prepare for insertion.
      #   document.prepare_insert do
      #     collection.insert(as_document)
      #   end
      #
      # @param [ Hash ] options The options.
      #
      # @return [ Document ] The document.
      def prepare_insert(options = {})
        raise Errors::ReadonlyDocument.new(self.class) if readonly? && !Mongoid.legacy_readonly
        return self if performing_validations?(options) &&
          invalid?(options[:context] || :create)
        ensure_client_compatibility!
        run_callbacks(:commit, with_children: true, skip_if: -> { in_transaction? }) do
          run_callbacks(:save, with_children: false) do
            run_callbacks(:create, with_children: false) do
              run_callbacks(:persist_parent, with_children: false) do
                _mongoid_run_child_callbacks(:save) do
                  _mongoid_run_child_callbacks(:create) do
                    result = yield(self)
                    if !result.is_a?(Document) || result.errors.empty?
                      post_process_insert
                      post_process_persist(result, options)
                    end
                  end
                end
              end
            end
          end
        end
        self
      end

      module ClassMethods

        # Create a new document. This will instantiate a new document and
        # insert it in a single call. Will always return the document
        # whether save passed or not.
        #
        # @example Create a new document.
        #   Person.create(:title => "Mr")
        #
        # @example Create multiple new documents.
        #   Person.create({ title: "Mr" }, { title: "Mrs" })
        #
        # @param [ Hash | Array ] attributes The attributes to create with, or an
        #   Array of multiple attributes for multiple documents.
        #
        # @return [ Document | Array<Document> ] The newly created document(s).
        def create(attributes = nil, &block)
          _creating do
            if attributes.is_a?(::Array)
              attributes.map { |attrs| create(attrs, &block) }
            else
              doc = new(attributes, &block)
              doc.save
              doc
            end
          end
        end

        # Create a new document. This will instantiate a new document and
        # insert it in a single call. Will always return the document
        # whether save passed or not, and if validation fails an error will be
        # raise.
        #
        # @example Create a new document.
        #   Person.create!(:title => "Mr")
        #
        # @example Create multiple new documents.
        #   Person.create!({ title: "Mr" }, { title: "Mrs" })
        #
        # @param [ Hash | Array ] attributes The attributes to create with, or an
        #   Array of multiple attributes for multiple documents.
        #
        # @return [ Document | Array<Document> ] The newly created document(s).
        def create!(attributes = nil, &block)
          _creating do
            if attributes.is_a?(::Array)
              attributes.map { |attrs| create!(attrs, &block) }
            else
              doc = new(attributes, &block)
              doc.fail_due_to_validation! unless doc.insert.errors.empty?
              doc.fail_due_to_callback!(:create!) if doc.new_record?
              doc
            end
          end
        end
      end
    end
  end
end
