# frozen_string_literal: true

module ActiveRecord
  class Base
    attr_reader :orginal_selected_record

    def save_original
      unless defined?(@orginal_selected_record) && @orginal_selected_record
        @orginal_selected_record = @attributes.to_hash.clone
      end
      self
    end

    def callbacks_closed_scheduled_operation
      (@callbacks_closed_scheduled_operation ||= Set.new)
    end

    def on_closed_scheduled_operation
      defined?(@callbacks_closed_scheduled_operation) && @callbacks_closed_scheduled_operation && @callbacks_closed_scheduled_operation.each(&:on_closed_scheduled_operation)
    end

    def unset_new_record
      @new_record = false
    end
    class << self
      # will insert or update the given array
      #
      # +group+ array of activerecord objects
      def merge_group(group, options = {})
        check_group(group)

        to_insert = group.select(&:new_record?)

        to_update = group.reject(&:new_record?)

        affected_rows = 0

        unless to_insert.empty?
          affected_rows += insert_group(to_insert, options)
        end
        unless to_update.empty?
          affected_rows += update_group(to_update, options)
        end

        affected_rows
      end

      def to_type_symbol(column)
        return :string if column.sql_type.index('CHAR')
        if column.sql_type.index('DATE') || column.sql_type.index('TIMESTAMP')
          return :date
        end
        if column.sql_type.index('NUMBER') && (column.sql_type.count(',') == 0)
          return :integer
        end
        if column.sql_type.index('NUMBER') && (column.sql_type.count(',') == 1)
          return :float
        end

        raise ArgumentError, "type #{column.sql_type} of #{column.name} is unsupported"
      end

      def foreign_detail_tables
        @foreign_detail_tables ||= find_foreign_detail_tables(table_name)
      end

      def foreign_master_tables
        @foreign_master_tables ||= find_foreign_master_tables(table_name)
      end

      def check_group(group)
        unless group.is_a?(Array) || group.is_a?(Set) || group.is_a?(ActiveRecord::Relation)
          raise ArgumentError, "Array expected. Got #{group.class.name}."
        end
        unless group.reject { |i| i.is_a? self }.empty?
          raise ArgumentError, "only records of #{name} expected. Unexpected #{group.reject { |i| i.is_a? self }.map { |i| i.class.name }.uniq.join(',')} found."
        end
      end

      def insert_group(group, _options = {})
        group.reject(&:new_record?)

        sql = "INSERT INTO #{table_name} " \
              '( ' \
              "#{columns.map(&:name).join(', ')} " \
              ') VALUES ( ' \
              "#{(1..columns.count).map { |i| ":#{i}" }.join(', ')} " \
              ')'

        types = []

        columns.each do |c|
          type = to_type_symbol(c)
          types << type
        end

        values = []

        group.each do |record|
          row = []

          columns.each do |c|
            v = record.read_attribute(c.name)
            row << v
          end

          values << row
        end

        result = execute_batch_update(sql, types, values)

        group.each(&:unset_new_record)

        result
      end

      def update_group(group, options = {})
        check_group(group)

        optimistic = options[:optimistic]
        optimistic = true if optimistic.nil?

        if optimistic && !group.reject(&:orginal_selected_record).empty?
          raise NoOrginalRecordFound, "#{name} ( #{table_name} )"
        end

        sql = optimistic ? build_optimistic_update_sql : build_update_by_primary_key_sql

        types = []

        columns.each do |c|
          type = to_type_symbol(c)
          types << type
        end

        if optimistic

          columns.each do |c|
            type = to_type_symbol(c)
            types << type
            types << type if c.null
          end

        else

          keys = primary_key_columns

          keys.each do |c|
            type = to_type_symbol(c)
            types << type
          end

        end

        values = []

        keys = primary_key_columns

        group.each do |record|
          row = []

          columns.each do |c|
            v = record.read_attribute(c.name)
            row << v
          end

          if optimistic

            orginal = record.orginal_selected_record
            columns.each do |c|
              v = orginal[c.name]
              row << v
              row << v if c.null
            end

          else

            keys.each { |c| row << record[c.name] }

          end

          values << row
        end

        count = execute_batch_update(sql, types, values, optimistic)

        count
      end

      public

      def insert_on_missing_group(keys, group, _options = {})
        # fail 'the give key array is empty' if keys.empty?

        keys = Array(primary_key) if keys.nil? || keys.empty?

        sql = "
        merge into #{table_name} target
        using ( select #{columns.map { |c| ":#{columns.index(c) + 1} #{c.name}" }.join(', ')} from dual ) source
        on ( #{keys.map { |c| "target.#{c} = source.#{c}" }.join(' and ')}  )
        when not matched then
        insert ( #{columns.map(&:name).join(', ')} )
        values( #{columns.map { |c| "source.#{c.name}" }.join(', ')} )
        "

        types = columns.map { |c| to_type_symbol(c) }

        values = []

        group.each do |record|
          row = []

          columns.each do |c|
            v = record.read_attribute(c.name)
            row << v
          end

          values << row
        end

        begin
          result = execute_batch_update(sql, types, values, false)
        rescue StandardError => e
          raise ActiveRecord::StatementInvalid, "#{e.message}\n#{sql}"
        end

        group.each(&:unset_new_record)

        result
      end

      def delete_group(group, options = {})
        check_group(group)

        optimistic = options[:optimistic]
        optimistic = true if optimistic.nil?

        to_delete = group.reject(&:new_record?)

        if optimistic && !to_delete.reject(&:orginal_selected_record).empty?
          raise NoOrginalRecordFound
        end

        sql = optimistic ? build_optimistic_delete_sql : build_delete_by_primary_key_sql

        types = []

        if optimistic

          columns.each do |c|
            type = to_type_symbol(c)
            types << type
            types << type if c.null
          end

        else

          keys = primary_key_columns

          keys.each do |c|
            type = to_type_symbol(c)
            types << type
          end

        end

        values = []

        if optimistic

          to_delete.each do |record|
            row = []
            orginal = record.orginal_selected_record
            columns.each do |c|
              v = orginal[c.name]
              row << v
              row << v if c.null
            end

            values << row
          end

        else

          keys = primary_key_columns

          to_delete.each do |record|
            row = keys.map { |c| record[c.name] }
            values << row
          end

        end

        count = execute_batch_update(sql, types, values, optimistic)

        count
      rescue ExternalDataChange => e
        raise e
      rescue Exception => e
        raise StatementError, "#{sql} #{e.message}"
      end
    end
  end
end