# frozen_string_literal: false

module PGTrunk::Operations::ForeignKeys
  # @abstract
  # @private
  # Base class for operations with foreign keys
  class Base < PGTrunk::Operation
    # All attributes that can be used by fk-related commands
    attribute :columns, :pg_trunk_array_of_strings, aliases: :column, default: []
    attribute :match, :pg_trunk_symbol
    attribute :on_delete, :pg_trunk_symbol
    attribute :on_update, :pg_trunk_symbol
    attribute :primary_key, :pg_trunk_array_of_strings, default: %(id)
    attribute :reference, :pg_trunk_qualified_name
    attribute :table, :pg_trunk_qualified_name
    attribute :validate, :boolean, default: true

    # Generate missed columns from the reference ant its foreign keys.
    # The generation of the missed name is operation-specific.
    after_initialize { self.columns = generated_columns if columns.blank? }

    # Ensure correctness of present values
    # The table must be defined because the name only
    # is not enough to identify the key.
    validates :table, presence: true
    validates :force, absence: true
    validates :match, inclusion: { in: %i[full partial simple] }, allow_nil: true
    validates :on_update, :on_delete,
              inclusion: { in: %i[restrict cascade nullify reset] },
              allow_nil: true

    # By default foreign keys are sorted by tables and names.
    def <=>(other)
      return unless other.is_a?(self.class)

      result = table <=> other.table
      result.zero? ? super : result
    end

    # Support `table` and `reference` in positional arguments.
    # @example
    #   add_foreign_key :users, :roles, **opts
    ruby_params :table, :reference

    ruby_snippet do |s|
      s.ruby_param(table.lean) if table.present?
      s.ruby_param(reference.lean) if reference.present?
      unless default_columns?
        s.ruby_param(column: columns.first) if columns.size == 1
        s.ruby_param(columns: columns) if columns.size > 1
      end
      unless default_pkey?
        s.ruby_param(primary_key: primary_key.first) if primary_key.size == 1
        s.ruby_param(primary_key: primary_key) if primary_key.size > 1
      end
      s.ruby_param(match: match) if match&.!= :simple
      s.ruby_param(on_update: on_update) if on_update
      s.ruby_param(on_delete: on_delete) if on_delete
      s.ruby_param(name: name.lean) if custom_name?
      s.ruby_param(to: new_name.lean) if custom_name?(new_name)
      s.ruby_param(if_exists: true) if if_exists
      s.ruby_param(comment: comment) if comment.present?
    end

    private

    # ***********************************************************************
    # Helpers for operation definitions
    # ***********************************************************************

    # @param [#reference] operation
    # @return [Array<String>]
    def generated_columns
      return @generated_columns if instance_variable_defined?(:@generated_columns)

      @generated_columns = begin
        return if reference.blank? || primary_key.blank?

        prefix =
          PGTrunk
          .database
          .strip_table_name(reference.name)
          .to_s
          .singularize

        primary_key.map { |pk| "#{prefix}_#{pk}" }
      end
    end

    # Generate the name for the foreign key using the essential options
    # @return [PGTrunk::QualifiedName]
    def generated_name
      return @generated_name if instance_variable_defined?(:@generated_name)

      @generated_name = begin
        return if table.blank? || reference.blank?
        return if primary_key.blank? || columns.blank?

        key_options = to_h.slice(:reference, :columns, :primary_key)
        identifier = "#{table.lean}_#{key_options}_fk"
        hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
        PGTrunk::QualifiedName.wrap("fk_rails_#{hashed_identifier}")
      end
    end

    # Check if the fk name wasn't generated by Rails
    # @param [#name] operation The operation
    # @return [Boolean]
    def custom_name?(qname = name)
      qname&.differs_from?(/^fk_rails_\w+$/)
    end

    # Check if the only column is custom
    # @param [#table, #reference, #columns, #primary_key] operation
    def default_columns?
      columns == generated_columns
    end

    # Check if columns are default for the reference
    # @param [#table, #reference, #columns, #primary_key] operation
    def default_pkey?
      primary_key == %w[id]
    end

    # Notice that in Rails the key `if_not_exists: true` means that
    # the constraint should not be created if the table has ANY
    # fk reference to the other table (even though the keys differ).
    #
    # @param [#table, #reference] operation
    # @return [Boolean]
    def added?
      PGTrunk.database.foreign_key_exists?(table.name, reference.name)
    end

    # Add a name of the existing foreign key
    def current_name
      @current_name ||= AddForeignKey.find do |fk|
        fk.table == table &&
          fk.columns == columns &&
          fk.reference == reference &&
          fk.primary_key == primary_key
      end&.name
    end

    def sql_action(key)
      case key
      when :nullify then "SET NULL"
      when :reset then "SET DEFAULT"
      when :restrict then "RESTRICT"
      when :cascade then "CASCADE"
      else "NO ACTION"
      end
    end
  end
end
