# frozen_string_literal: true

# This service yields operation on each download link from a remote source based on the
# oids provided
module Projects
  module LfsPointers
    class LfsDownloadLinkListService < BaseService
      DOWNLOAD_ACTION = 'download'

      # This could be different per server, but it seems like a reasonable value to start with.
      # https://github.com/git-lfs/git-lfs/issues/419
      REQUEST_BATCH_SIZE = 100

      DownloadLinksError = Class.new(StandardError)
      DownloadLinkNotFound = Class.new(StandardError)
      DownloadLinksRequestEntityTooLargeError = Class.new(StandardError)

      attr_reader :remote_uri

      def initialize(project, remote_uri: nil)
        super(project)

        @remote_uri = remote_uri
      end

      # - oids: hash of oids to query. The structure is { lfs_file_oid => lfs_file_size }
      # Yields operation for each link batch-by-batch
      def each_link(oids, &block)
        return unless project&.lfs_enabled? && remote_uri && oids.present?

        download_links_in_batches(oids, &block)
      end

      private

      def download_links_in_batches(oids, batch_size = REQUEST_BATCH_SIZE, &block)
        oids.each_slice(batch_size) do |batch|
          download_links_for(batch).each(&block)
        end
      rescue DownloadLinksRequestEntityTooLargeError => e
        # Log this exceptions to see how often it happens
        Gitlab::ErrorTracking
          .track_exception(e, project_id: project&.id, batch_size: batch_size, oids_count: oids.count)

        # Try again with a smaller batch
        batch_size /= 2

        retry if batch_size > REQUEST_BATCH_SIZE / 3

        raise DownloadLinksError, 'Unable to download due to RequestEntityTooLarge errors'
      end

      def download_links_for(oids)
        response = Gitlab::HTTP.post(remote_uri,
                                     body: request_body(oids),
                                     headers: headers)

        raise DownloadLinksRequestEntityTooLargeError if response.request_entity_too_large?
        raise DownloadLinksError, response.message unless response.success?

        # Since the LFS Batch API may return a Content-Ttpe of
        # application/vnd.git-lfs+json
        # (https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md#requests),
        # HTTParty does not know this is actually JSON.
        data = Gitlab::Json.parse(response.body)

        raise DownloadLinksError, "LFS Batch API did return any objects" unless data.is_a?(Hash) && data.key?('objects')

        parse_response_links(data['objects'])
      rescue JSON::ParserError
        raise DownloadLinksError, "LFS Batch API response is not JSON"
      end

      def parse_response_links(objects_response)
        objects_response.each_with_object([]) do |entry, link_list|
          link = entry.dig('actions', DOWNLOAD_ACTION, 'href')
          headers = entry.dig('actions', DOWNLOAD_ACTION, 'header')

          raise DownloadLinkNotFound unless link

          link_list << LfsDownloadObject.new(oid: entry['oid'],
                                             size: entry['size'],
                                             headers: headers,
                                             link: add_credentials(link))
        rescue DownloadLinkNotFound, Addressable::URI::InvalidURIError
          log_error("Link for Lfs Object with oid #{entry['oid']} not found or invalid.")
        end
      end

      def request_body(oids)
        body = {
          operation: DOWNLOAD_ACTION,
          objects: oids.map { |oid, size| { oid: oid, size: size } }
        }

        Gitlab::Json.dump(body)
      end

      def headers
        {
          'Accept' => LfsRequest::CONTENT_TYPE,
          'Content-Type' => LfsRequest::CONTENT_TYPE
        }.freeze
      end

      def add_credentials(link)
        uri = Addressable::URI.parse(link)

        if should_add_credentials?(uri)
          uri.user = remote_uri.user
          uri.password = remote_uri.password
        end

        uri.to_s
      end

      # The download link can be a local url or an object storage url
      # If the download link has the some host as the import url then
      # we add the same credentials because we may need them
      def should_add_credentials?(link_uri)
        url_credentials? && link_uri.host == remote_uri.host
      end

      def url_credentials?
        remote_uri.user.present? || remote_uri.password.present?
      end
    end
  end
end
