# frozen_string_literal: false

# @!parse
#   class ActiveRecord::Migration
#     # Modify a composite type
#     #
#     # @param [#to_s] name (nil) The qualified name of the type
#     # @option options [Symbol] :force (:restrict) How to process dependent objects (`:cascade` or `:restrict`)
#     # @option options [#to_s] :comment (nil) The comment describing the constraint
#     # @yield [t] the block with the type's definition
#     # @yieldparam Object receiver of methods specifying the type
#     # @return [void]
#     #
#     # The operation can be used to add, drop, rename or change columns.
#     # The comment can be changed as well.
#     #
#     # Providing a type "paint.colored_point":
#     #
#     # ```ruby
#     # create_composite_type "paint.colored_point" do |t|
#     #   t.column "color", "text", collation: "en_US"
#     #   t.column "x", "integer"
#     #   t.column "z", "integer"
#     # end
#     # ```
#     #
#     # After the following change:
#     #
#     # ```ruby
#     # change_composite_type "paint.colored_point" do |t|
#     #   t.change_column "color", "text", collation: "ru_RU", from_collation: "en_US"
#     #   t.change_column "x", "bigint", from_type: "integer"
#     #   t.drop_column "z", "integer"
#     #   t.add_column "Y", "bigint"
#     #   t.rename_column "x", to: "X"
#     #   t.comment "2D point with a color", from: "2D point"
#     # end
#     # ```
#     #
#     # The definition became:
#     #
#     # ```ruby
#     # create_composite_type "paint.colored_point" do |t|
#     #   t.column "color", "text", collation: "ru_RU"
#     #   t.column "X", "bigint"
#     #   t.column "Y", "integer"
#     # end
#     # ```
#     #
#     # Notice, that all renames will be done AFTER other changes,
#     # so in `change_column` you should use the old names.
#     #
#     # In several cases the operation is not invertible:
#     #
#     # - when a column was dropped
#     # - when `force: :cascade` option is used (to update
#     #   objects that use the type)
#     # - when `if_exists: true` is added to the `drop_column` clause
#     # - when a previous state of the column type, collation or comment
#     #   is not specified.
#     def change_composite_type(name, **options, &block); end
#   end
module PGTrunk::Operations::CompositeTypes
  # @private
  class ChangeCompositeType < Base
    # Methods to populate `columns` from the block
    def add_column(name, type, collation: nil)
      columns << Column.new(
        name: name, type: type, collation: collation, change: :add, force: force,
      )
    end

    def drop_column(name, type = nil, **opts)
      opts = opts.slice(:if_exists, :collation)
      columns << Column.new(
        name: name, type: type, force: force, **opts, change: :drop,
      )
    end

    def change_column(name, type, **opts)
      opts = opts.slice(:collation, :from_type, :from_collation)
      columns << Column.new(
        name: name, type: type, force: force, change: :alter, **opts,
      )
    end

    def rename_column(name, to:)
      columns << Column.new(
        name: name, new_name: to, force: force, change: :rename,
      )
    end

    validates :if_exists, :new_name, absence: true
    validate { errors.add :base, "There are no changes" if change.blank? }

    def to_sql(_version)
      [*change_columns, *rename_columns, *change_comment].join(" ")
    end

    def invert
      keys = inversion.select { |_, v| v.nil? }.keys.join(", ").presence
      errors = columns.map(&:inversion_error).compact
      errors << "Can't invert #{keys}" if keys.present?
      errors << "Can't invert dropped columns" if columns.any? { |c| c.change == :drop }
      raise IrreversibleMigration.new(self, nil, *errors) if errors.any?

      self.class.new(**to_h, **inversion)
    end

    private

    def change_columns
      list = columns.select { |c| c.change&.!= :rename }
      return if list.blank?

      "ALTER TYPE #{name.to_sql} #{list.map(&:to_sql).join(', ')};"
    end

    def rename_columns
      columns.select { |c| c.change == :rename }.map do |c|
        "ALTER TYPE #{name.to_sql} #{c.to_sql};"
      end
    end

    def change_comment
      <<~SQL.squish if comment
        COMMENT ON TYPE #{name.to_sql} IS $comment$#{comment}$comment$;
      SQL
    end

    def change
      @change ||= {
        comment: comment,
        columns: columns.select(&:change).map(&:to_h).presence,
      }.compact
    end

    def inversion
      @inversion ||= {
        comment: from_comment,
        columns: columns.reverse.map(&:invert).presence,
      }.slice(*change.keys)
    end
  end
end
