# frozen_string_literal: true

module Gitlab
  module BackgroundMigration
    # Migrates the integration.properties column from plaintext to encrypted text.
    class EncryptIntegrationProperties
      # The Integration model, with just the relevant bits.
      class Integration < ActiveRecord::Base
        include EachBatch

        ALGORITHM = 'aes-256-gcm'

        self.table_name = 'integrations'
        self.inheritance_column = :_type_disabled

        scope :with_properties, -> { where.not(properties: nil) }
        scope :not_already_encrypted, -> { where(encrypted_properties: nil) }
        scope :for_batch, ->(range) { where(id: range) }

        attr_encrypted :encrypted_properties_tmp,
                       attribute: :encrypted_properties,
                       mode: :per_attribute_iv,
                       key: ::Settings.attr_encrypted_db_key_base_32,
                       algorithm: ALGORITHM,
                       marshal: true,
                       marshaler: ::Gitlab::Json,
                       encode: false,
                       encode_iv: false

        # See 'Integration#reencrypt_properties'
        def encrypt_properties
          data = ::Gitlab::Json.parse(properties)
          iv = generate_iv(ALGORITHM)
          ep = self.class.encrypt(:encrypted_properties_tmp, data, { iv: iv })

          [ep, iv]
        end
      end

      def perform(start_id, stop_id)
        batch_query = Integration.with_properties.not_already_encrypted.for_batch(start_id..stop_id)
        encrypt_batch(batch_query)
        mark_job_as_succeeded(start_id, stop_id)
      end

      private

      def mark_job_as_succeeded(*arguments)
        Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
          self.class.name.demodulize,
          arguments
        )
      end

      # represent binary string as a PSQL binary literal:
      # https://www.postgresql.org/docs/9.4/datatype-binary.html
      def bytea(value)
        "'\\x#{value.unpack1('H*')}'::bytea"
      end

      def encrypt_batch(batch_query)
        values = batch_query.select(:id, :properties).map do |record|
          encrypted_properties, encrypted_properties_iv = record.encrypt_properties
          "(#{record.id}, #{bytea(encrypted_properties)}, #{bytea(encrypted_properties_iv)})"
        end

        return if values.empty?

        Integration.connection.execute(<<~SQL.squish)
          WITH cte(cte_id, cte_encrypted_properties, cte_encrypted_properties_iv)
            AS #{::Gitlab::Database::AsWithMaterialized.materialized_if_supported} (
            SELECT *
            FROM (VALUES #{values.join(',')}) AS t (id, encrypted_properties, encrypted_properties_iv)
          )
          UPDATE #{Integration.table_name}
          SET encrypted_properties = cte_encrypted_properties
            , encrypted_properties_iv = cte_encrypted_properties_iv
          FROM cte
          WHERE cte_id = id
        SQL
      end
    end
  end
end