module Gitlab
  module Lfs
    class Response
      def initialize(project, user, ci, request)
        @origin_project = project
        @project = storage_project(project)
        @user = user
        @ci = ci
        @env = request.env
        @request = request
      end

      def render_download_object_response(oid)
        render_response_to_download do
          if check_download_sendfile_header?
            render_lfs_sendfile(oid)
          else
            render_not_found
          end
        end
      end

      def render_batch_operation_response
        request_body = JSON.parse(@request.body.read)
        case request_body["operation"]
        when "download"
          render_batch_download(request_body)
        when "upload"
          render_batch_upload(request_body)
        else
          render_not_found
        end
      end

      def render_storage_upload_authorize_response(oid, size)
        render_response_to_push do
          [
            200,
            { "Content-Type" => "application/json; charset=utf-8" },
            [JSON.dump({
              'StoreLFSPath' => "#{Gitlab.config.lfs.storage_path}/tmp/upload",
              'LfsOid' => oid,
              'LfsSize' => size
            })]
          ]
        end
      end

      def render_storage_upload_store_response(oid, size, tmp_file_name)
        render_response_to_push do
          render_lfs_upload_ok(oid, size, tmp_file_name)
        end
      end

      def render_unsupported_deprecated_api
        [
          501,
          { "Content-Type" => "application/json; charset=utf-8" },
          [JSON.dump({
            'message' => 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.',
            'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
          })]
        ]
      end

      private

      def render_not_enabled
        [
          501,
          {
            "Content-Type" => "application/json; charset=utf-8",
          },
          [JSON.dump({
            'message' => 'Git LFS is not enabled on this GitLab server, contact your admin.',
            'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
          })]
        ]
      end

      def render_unauthorized
        [
          401,
          {
            'Content-Type' => 'text/plain'
          },
          ['Unauthorized']
        ]
      end

      def render_not_found
        [
          404,
          {
            "Content-Type" => "application/vnd.git-lfs+json"
          },
          [JSON.dump({
            'message' => 'Not found.',
            'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
          })]
        ]
      end

      def render_forbidden
        [
          403,
          {
            "Content-Type" => "application/vnd.git-lfs+json"
          },
          [JSON.dump({
            'message' => 'Access forbidden. Check your access level.',
            'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
          })]
        ]
      end

      def render_lfs_sendfile(oid)
        return render_not_found unless oid.present?

        lfs_object = object_for_download(oid)

        if lfs_object && lfs_object.file.exists?
          [
            200,
            {
              # GitLab-workhorse will forward Content-Type header
              "Content-Type" => "application/octet-stream",
              "X-Sendfile" => lfs_object.file.path
            },
            []
          ]
        else
          render_not_found
        end
      end

      def render_batch_upload(body)
        return render_not_found if body.empty? || body['objects'].nil?

        render_response_to_push do
          response = build_upload_batch_response(body['objects'])
          [
            200,
            {
              "Content-Type" => "application/json; charset=utf-8",
              "Cache-Control" => "private",
            },
            [JSON.dump(response)]
          ]
        end
      end

      def render_batch_download(body)
        return render_not_found if body.empty? || body['objects'].nil?

        render_response_to_download do
          response = build_download_batch_response(body['objects'])
          [
            200,
            {
              "Content-Type" => "application/json; charset=utf-8",
              "Cache-Control" => "private",
            },
            [JSON.dump(response)]
          ]
        end
      end

      def render_lfs_upload_ok(oid, size, tmp_file)
        if store_file(oid, size, tmp_file)
          [
            200,
            {
              'Content-Type' => 'text/plain',
              'Content-Length' => 0
            },
            []
          ]
        else
          [
            422,
            { 'Content-Type' => 'text/plain' },
            ["Unprocessable entity"]
          ]
        end
      end

      def render_response_to_download
        return render_not_enabled unless Gitlab.config.lfs.enabled

        unless @project.public?
          return render_unauthorized unless @user || @ci
          return render_forbidden unless user_can_fetch?
        end

        yield
      end

      def render_response_to_push
        return render_not_enabled unless Gitlab.config.lfs.enabled
        return render_unauthorized unless @user
        return render_forbidden unless user_can_push?

        yield
      end

      def check_download_sendfile_header?
        @env['HTTP_X_SENDFILE_TYPE'].to_s == "X-Sendfile"
      end

      def user_can_fetch?
        # Check user access against the project they used to initiate the pull
        @ci || @user.can?(:download_code, @origin_project)
      end

      def user_can_push?
        # Check user access against the project they used to initiate the push
        @user.can?(:push_code, @origin_project)
      end

      def storage_project(project)
        if project.forked?
          storage_project(project.forked_from_project)
        else
          project
        end
      end

      def store_file(oid, size, tmp_file)
        tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file)

        object = LfsObject.find_or_create_by(oid: oid, size: size)
        if object.file.exists?
          success = true
        else
          success = move_tmp_file_to_storage(object, tmp_file_path)
        end

        if success
          success = link_to_project(object)
        end

        success
      ensure
        # Ensure that the tmp file is removed
        FileUtils.rm_f(tmp_file_path)
      end

      def object_for_download(oid)
        @project.lfs_objects.find_by(oid: oid)
      end

      def move_tmp_file_to_storage(object, path)
        File.open(path) do |f|
          object.file = f
        end

        object.file.store!
        object.save
      end

      def link_to_project(object)
        if object && !object.projects.exists?(@project.id)
          object.projects << @project
          object.save
        end
      end

      def select_existing_objects(objects)
        objects_oids = objects.map { |o| o['oid'] }
        @project.lfs_objects.where(oid: objects_oids).pluck(:oid).to_set
      end

      def build_upload_batch_response(objects)
        selected_objects = select_existing_objects(objects)

        upload_hypermedia_links(objects, selected_objects)
      end

      def build_download_batch_response(objects)
        selected_objects = select_existing_objects(objects)

        download_hypermedia_links(objects, selected_objects)
      end

      def download_hypermedia_links(all_objects, existing_objects)
        all_objects.each do |object|
          if existing_objects.include?(object['oid'])
            object['actions'] = {
              'download' => {
                'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}",
                'header' => {
                  'Authorization' => @env['HTTP_AUTHORIZATION']
                }.compact
              }
            }
          else
            object['error'] = {
              'code' => 404,
              'message' => "Object does not exist on the server or you don't have permissions to access it",
            }
          end
        end

        { 'objects' => all_objects }
      end

      def upload_hypermedia_links(all_objects, existing_objects)
        all_objects.each do |object|
          # generate actions only for non-existing objects
          next if existing_objects.include?(object['oid'])

          object['actions'] = {
            'upload' => {
              'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}/#{object['size']}",
              'header' => {
                'Authorization' => @env['HTTP_AUTHORIZATION']
              }.compact
            }
          }
        end

        { 'objects' => all_objects }
      end
    end
  end
end