# frozen_string_literal: true

# NOTE: This code is legacy. Do not add/modify code here unless you have
# discussed with the Gitaly team.  See
# https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code
# for more details.

module Gitlab
  module Git
    module RuggedImpl
      module Tree
        module ClassMethods
          extend ::Gitlab::Utils::Override
          include Gitlab::Git::RuggedImpl::UseRugged

          TREE_SORT_ORDER = { tree: 0, blob: 1, commit: 2 }.freeze

          override :tree_entries
          def tree_entries(repository, sha, path, recursive, pagination_params = nil)
            if use_rugged?(repository, :rugged_tree_entries)
              entries = execute_rugged_call(:tree_entries_with_flat_path_from_rugged, repository, sha, path, recursive)

              if pagination_params
                paginated_response(entries, pagination_params[:limit], pagination_params[:page_token].to_s)
              else
                [entries, nil]
              end
            else
              super
            end
          end

          # Rugged version of TreePagination in Go: https://gitlab.com/gitlab-org/gitaly/-/merge_requests/3611
          def paginated_response(entries, limit, token)
            total_entries = entries.count

            return [[], nil] if limit == 0 || limit.blank?

            entries = Gitlab::Utils.stable_sort_by(entries) { |x| TREE_SORT_ORDER[x.type] }

            if token.blank?
              index = 0
            else
              index = entries.index { |entry| entry.id == token }

              raise Gitlab::Git::CommandError, "could not find starting OID: #{token}" if index.nil?

              index += 1
            end

            return [entries[index..], nil] if limit < 0

            last_index = index + limit
            result = entries[index...last_index]

            if last_index < total_entries
              cursor = Gitaly::PaginationCursor.new(next_cursor: result.last.id)
            end

            [result, cursor]
          end

          def tree_entries_with_flat_path_from_rugged(repository, sha, path, recursive)
            tree_entries_from_rugged(repository, sha, path, recursive).tap do |entries|
              # This was an optimization to reduce N+1 queries for Gitaly
              # (https://gitlab.com/gitlab-org/gitaly/issues/530).  It
              # used to be done lazily in the view via
              # TreeHelper#flatten_tree, so it's possible there's a
              # performance impact by loading this eagerly.
              rugged_populate_flat_path(repository, sha, path, entries)
            end
          end

          def tree_entries_from_rugged(repository, sha, path, recursive)
            current_path_entries = get_tree_entries_from_rugged(repository, sha, path)
            ordered_entries = []

            current_path_entries.each do |entry|
              ordered_entries << entry

              if recursive && entry.dir?
                ordered_entries.concat(tree_entries_from_rugged(repository, sha, entry.path, true))
              end
            end

            ordered_entries
          end

          def rugged_populate_flat_path(repository, sha, path, entries)
            entries.each do |entry|
              entry.flat_path = entry.path

              next unless entry.dir?

              entry.flat_path =
                if path
                  File.join(path, rugged_flatten_tree(repository, sha, entry, path))
                else
                  rugged_flatten_tree(repository, sha, entry, path)
                end
            end
          end

          # Returns the relative path of the first subdir that doesn't have only one directory descendant
          def rugged_flatten_tree(repository, sha, tree, root_path)
            subtree = tree_entries_from_rugged(repository, sha, tree.path, false)

            if subtree.count == 1 && subtree.first.dir?
              File.join(tree.name, rugged_flatten_tree(repository, sha, subtree.first, root_path))
            else
              tree.name
            end
          end

          def get_tree_entries_from_rugged(repository, sha, path)
            commit = repository.lookup(sha)
            root_tree = commit.tree

            tree = if path
                     id = find_id_by_path(repository, root_tree.oid, path)
                     if id
                       repository.lookup(id)
                     else
                       []
                     end
                   else
                     root_tree
                   end

            tree.map do |entry|
              current_path = path ? File.join(path, entry[:name]) : entry[:name]

              new(
                id: entry[:oid],
                root_id: root_tree.oid,
                name: entry[:name],
                type: entry[:type],
                mode: entry[:filemode].to_s(8),
                path: current_path,
                commit_id: sha
              )
            end
          rescue Rugged::ReferenceError
            []
          end
        end
      end
    end
  end
end
