# frozen_string_literal: true

module MergeRequests
  class AddContextService < MergeRequests::BaseService
    def execute
      return error("You are not allowed to access the requested resource", 403) unless current_user&.can?(:update_merge_request, merge_request)
      return error("Context commits: #{duplicates} are already created", 400) unless duplicates.empty?
      return error("One or more context commits' sha is not valid.", 400) if commits.size != commit_ids.size

      context_commit_ids = []
      MergeRequestContextCommit.transaction do
        context_commit_ids = MergeRequestContextCommit.bulk_insert(context_commit_rows, return_ids: true)
        MergeRequestContextCommitDiffFile.bulk_insert(diff_rows(context_commit_ids))
      end

      commits
    end

    private

    def raw_repository
      project.repository.raw_repository
    end

    def merge_request
      params[:merge_request]
    end

    def commit_ids
      params[:commits]
    end

    def commits
      project.repository.commits_by(oids: commit_ids)
    end

    def context_commit_rows
      @context_commit_rows ||= build_context_commit_rows(merge_request.id, commits)
    end

    def diff_rows(context_commit_ids)
      @diff_rows ||= build_diff_rows(raw_repository, commits, context_commit_ids)
    end

    def encode_in_base64?(diff_text)
      (diff_text.encoding == Encoding::BINARY && !diff_text.ascii_only?) ||
        diff_text.include?("\0")
    end

    def duplicates
      existing_oids = merge_request.merge_request_context_commits.map { |commit| commit.sha.to_s }
      existing_oids.select do |existing_oid|
        commit_ids.count { |commit_id| existing_oid.start_with?(commit_id) } > 0
      end
    end

    def build_context_commit_rows(merge_request_id, commits)
      commits.map.with_index do |commit, index|
        # generate context commit information for given commit
        commit_hash = commit.to_hash.except(:parent_ids, :referenced_by)
        sha = Gitlab::Database::ShaAttribute.serialize(commit_hash.delete(:id))
        commit_hash.merge(
          merge_request_id: merge_request_id,
          relative_order: index,
          sha: sha,
          authored_date: Gitlab::Database.sanitize_timestamp(commit_hash[:authored_date]),
          committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date]),
          trailers: Gitlab::Json.dump(commit_hash.fetch(:trailers, {}))
        )
      end
    end

    def build_diff_rows(raw_repository, commits, context_commit_ids)
      diff_rows = []
      diff_order = 0

      commits.flat_map.with_index do |commit, index|
        commit_hash = commit.to_hash.except(:parent_ids, :referenced_by)
        sha = Gitlab::Database::ShaAttribute.serialize(commit_hash.delete(:id))
        # generate context commit diff information for given commit
        diffs = commit.diffs

        compare = Gitlab::Git::Compare.new(
          raw_repository,
          diffs.diff_refs.start_sha,
          diffs.diff_refs.head_sha
        )
        compare.diffs.map do |diff|
          diff_hash = diff.to_hash.merge(
            sha: sha,
            binary: false,
            merge_request_context_commit_id: context_commit_ids[index],
            relative_order: diff_order
          )

          # Compatibility with old diffs created with Psych.
          diff_hash.tap do |hash|
            diff_text = hash[:diff]

            if encode_in_base64?(diff_text)
              hash[:binary] = true
              hash[:diff] = [diff_text].pack('m0')
            end
          end

          # Increase order for commit so when present the diffs we can use it to keep order
          diff_order += 1
          diff_rows << diff_hash
        end
      end

      diff_rows
    end
  end
end
