# frozen_string_literal: true

module AdaptiveAlias
  module Patches
    class Base
      attr_reader :check_matched
      attr_reader :remove_and_fix_association
      attr_reader :removed
      attr_reader :removable

      def initialize(klass, old_column, new_column)
        @klass = klass
        @old_column = old_column
        @new_column = new_column
      end

      def add_hooks!(current_column:, alias_column:, log_warning: false)
        patch = self
        klass = @klass
        old_column = @old_column
        new_column = @new_column

        AdaptiveAlias.get_or_create_model_module(klass).instance_exec do
          remove_method(new_column) if method_defined?(new_column)
          define_method(new_column) do
            self[new_column]
          end

          remove_method("#{new_column}=") if method_defined?("#{new_column}=")
          define_method("#{new_column}=") do |*args|
            super(*args)
          end

          remove_method(old_column) if method_defined?(old_column)
          define_method(old_column) do
            patch.log_warning if log_warning
            self[old_column]
          end

          remove_method("#{old_column}=") if method_defined?("#{old_column}=")
          define_method("#{old_column}=") do |*args|
            patch.log_warning if log_warning
            super(*args)
          end
        end

        expected_association_err_msgs = [
          "Mysql2::Error: Unknown column '#{klass.table_name}.#{current_column}' in 'where clause'".freeze,
          "Mysql2::Error: Unknown column '#{klass.table_name}.#{current_column}' in 'on clause'".freeze,
          "Mysql2::Error: Unknown column '#{klass.table_name}.#{current_column}' in 'field list'".freeze,
        ].freeze

        expected_ambiguous_association_err_msgs = [
          "Mysql2::Error: Unknown column '#{current_column}' in 'field list'".freeze,
        ].freeze

        fix_arel_attributes = proc do |attr|
          next if not attr.is_a?(Arel::Attributes::Attribute)
          next if attr.name != current_column.to_s
          next if klass.table_name != attr.relation.name

          attr.name = alias_column.to_s
        end

        fix_arel_nodes = proc do |nodes|
          each_nodes(nodes) do |node|
            fix_arel_attributes.call(node.left)
            fix_arel_attributes.call(node.right)
          end
        end

        @check_matched = proc do |relation, reflection, model, error|
          next false if not patch.removable

          # Error highlight behavior in Ruby 3.1 pollutes the error message
          error_msg = error.respond_to?(:original_message) ? error.original_message : error.message
          ambiguous = expected_ambiguous_association_err_msgs.include?(error_msg)

          if ambiguous
            next false if relation and klass.table_name != relation.klass.table_name
            next false if reflection and klass.table_name != reflection.klass.table_name
            next false if model and klass.table_name != model.class.table_name
            next false if !relation and !reflection and !model
          end

          next false if not expected_association_err_msgs.include?(error_msg) and not ambiguous
          next true
        end

        @remove_and_fix_association = proc do |relation, reflection, &block|
          patch.remove! do
            if relation
              relation.reset # reset @arel

              joins = relation.arel.source.right # @ctx.source.right << create_join(relation, nil, klass)

              # adjust select fields
              index = relation.select_values.index(current_column)
              relation.select_values[index] = alias_column if index

              fix_arel_nodes.call(joins.map{|s| s.right.expr })
              fix_arel_nodes.call(relation.where_clause.send(:predicates))
            end

            reflection.clear_association_scope_cache if reflection

            block.call
          end
        end
      end

      def log_warning
        if @prev_warning_time == nil || @prev_warning_time < Time.now - AdaptiveAlias.log_interval
          @prev_warning_time = Time.now
          AdaptiveAlias.unexpected_old_column_proc&.call
        end
      end

      def remove!
        if not @removed
          @removed = true
          new_patch = do_remove!
        end

        yield if block_given?
      ensure
        new_patch.mark_removable if new_patch
      end

      def do_remove!
        reset_caches(@klass)
        ActiveRecord::Base.descendants.each do |model_klass|
          reset_caches(model_klass) if model_klass.table_name == @klass.table_name
        end

        @check_matched = nil
        @remove_and_fix_association = nil
      end

      def mark_removable
        @removable = true
      end

      private

      def reset_caches(klass)
        # We need to call reload_schema_from_cache (which is called in reset_column_information),
        # in order to reset klass.attributes_builder which are initialized with outdated defaults.
        # If not, it will not raise missing attributes error when we try to access the column which has already been renamed,
        # and we will have no way to know the column has been renamed since no error is raised for us to rescue.
        klass.reset_column_information
        klass.columns_hash
      end

      def each_nodes(nodes, &block)
        nodes.each do |node|
          case node
          when Arel::Nodes::Grouping
            each_nodes([node.expr], &block)
          when Arel::Nodes::Equality
            yield(node)
          when Arel::Nodes::And
            each_nodes(node.children, &block)
          when Arel::Nodes::Or
            each_nodes([node.left, node.right], &block)
          end
        end
      end
    end
  end
end