# frozen_string_literal: true

module API
  module Helpers
    module Packages
      module Conan
        module ApiHelpers
          include Gitlab::Utils::StrongMemoize

          def check_username_channel
            username = declared(params)[:package_username]
            channel = declared(params)[:package_channel]

            if username == ::Packages::Conan::Metadatum::NONE_VALUE && package_scope == :instance
              # at the instance level, username must not be empty (naming convention)
              # don't try to process the empty username and eagerly return not found.
              not_found!
            end

            ::Packages::Conan::Metadatum.validate_username_and_channel(username, channel) do |none_field|
              bad_request!("#{none_field} can't be solely blank")
            end
          end

          def present_download_urls(entity)
            authorize!(:read_package, project)

            presenter = ::Packages::Conan::PackagePresenter.new(
              package,
              current_user,
              project,
              conan_package_reference: params[:conan_package_reference],
              id: params[:id]
            )

            render_api_error!("No recipe manifest found", 404) if yield(presenter).empty?

            present presenter, with: entity
          end

          def present_package_download_urls
            present_download_urls(::API::Entities::ConanPackage::ConanPackageManifest, &:package_urls)
          end

          def present_recipe_download_urls
            present_download_urls(::API::Entities::ConanPackage::ConanRecipeManifest, &:recipe_urls)
          end

          def recipe_upload_urls
            { upload_urls: file_names.select(&method(:recipe_file?)).to_h do |file_name|
                             [file_name, build_recipe_file_upload_url(file_name)]
                           end }
          end

          def package_upload_urls
            { upload_urls: file_names.select(&method(:package_file?)).to_h do |file_name|
                             [file_name, build_package_file_upload_url(file_name)]
                           end }
          end

          def recipe_file?(file_name)
            file_name.in?(::Packages::Conan::FileMetadatum::RECIPE_FILES)
          end

          def package_file?(file_name)
            file_name.in?(::Packages::Conan::FileMetadatum::PACKAGE_FILES)
          end

          def build_package_file_upload_url(file_name)
            options = url_options(file_name).merge(
              conan_package_reference: params[:conan_package_reference],
              package_revision: ::Packages::Conan::FileMetadatum::DEFAULT_PACKAGE_REVISION
            )

            package_file_url(options)
          end

          def build_recipe_file_upload_url(file_name)
            recipe_file_url(url_options(file_name))
          end

          def url_options(file_name)
            {
              package_name: params[:package_name],
              package_version: params[:package_version],
              package_username: params[:package_username],
              package_channel: params[:package_channel],
              file_name: file_name,
              recipe_revision: ::Packages::Conan::FileMetadatum::DEFAULT_RECIPE_REVISION
            }
          end

          def package_file_url(options)
            case package_scope
            when :project
              expose_url(
                api_v4_projects_packages_conan_v1_files_package_path(
                  options.merge(id: project.id)
                )
              )
            when :instance
              expose_url(
                api_v4_packages_conan_v1_files_package_path(options)
              )
            end
          end

          def recipe_file_url(options)
            case package_scope
            when :project
              expose_url(
                api_v4_projects_packages_conan_v1_files_export_path(
                  options.merge(id: project.id)
                )
              )
            when :instance
              expose_url(
                api_v4_packages_conan_v1_files_export_path(options)
              )
            end
          end

          def recipe
            "%{package_name}/%{package_version}@%{package_username}/%{package_channel}" % params.symbolize_keys
          end

          def project
            strong_memoize(:project) do
              case package_scope
              when :project
                find_project!(params[:id])
              when :instance
                full_path = ::Packages::Conan::Metadatum.full_path_from(package_username: params[:package_username])
                find_project!(full_path)
              end
            end
          end

          def package
            strong_memoize(:package) do
              project.packages
                .conan
                .with_name(params[:package_name])
                .with_version(params[:package_version])
                .with_conan_username(params[:package_username])
                .with_conan_channel(params[:package_channel])
                .order_created
                .not_pending_destruction
                .last
            end
          end

          def token
            strong_memoize(:token) do
              token = nil
              token = ::Gitlab::ConanToken.from_personal_access_token(find_personal_access_token.user_id, access_token_from_request) if find_personal_access_token
              token = ::Gitlab::ConanToken.from_deploy_token(deploy_token_from_request) if deploy_token_from_request
              token = ::Gitlab::ConanToken.from_job(find_job_from_token) if find_job_from_token
              token
            end
          end

          def download_package_file(file_type)
            authorize!(:read_package, project)

            package_file = ::Packages::Conan::PackageFileFinder
              .new(
                package,
                params[:file_name].to_s,
                conan_file_type: file_type,
                conan_package_reference: params[:conan_package_reference]
              ).execute!

            track_package_event('pull_package', :conan, category: 'API::ConanPackages', user: current_user, project: project, namespace: project.namespace) if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY

            present_carrierwave_file!(package_file.file)
          end

          def find_or_create_package
            package || ::Packages::Conan::CreatePackageService.new(
              project,
              current_user,
              params.merge(build: current_authenticated_job)
            ).execute
          end

          def track_push_package_event
            if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY && params[:file].size > 0 # rubocop: disable Style/ZeroLengthPredicate
              track_package_event('push_package', :conan, category: 'API::ConanPackages', user: current_user, project: project, namespace: project.namespace)
            end
          end

          def file_names
            json_payload = Gitlab::Json.parse(request.body.read)

            bad_request!(nil) unless json_payload.is_a?(Hash)

            json_payload.keys
          end

          def create_package_file_with_type(file_type, current_package)
            unless params[:file].size == 0 # rubocop: disable Style/ZeroLengthPredicate
              # conan sends two upload requests, the first has no file, so we skip record creation if file.size == 0
              ::Packages::Conan::CreatePackageFileService.new(
                current_package,
                params[:file],
                params.merge(conan_file_type: file_type, build: current_authenticated_job)
              ).execute
            end
          end

          def upload_package_file(file_type)
            authorize_upload!(project)
            bad_request!('File is too large') if project.actual_limits.exceeded?(:conan_max_file_size, params['file.size'].to_i)

            current_package = find_or_create_package

            track_push_package_event

            create_package_file_with_type(file_type, current_package)
          rescue ObjectStorage::RemoteStoreError => e
            Gitlab::ErrorTracking.track_exception(e, file_name: params[:file_name], project_id: project.id)

            forbidden!
          end

          # We override this method from auth_finders because we need to
          # extract the token from the Conan JWT which is specific to the Conan API
          def find_personal_access_token
            strong_memoize(:find_personal_access_token) do
              PersonalAccessToken.find_by_token(access_token_from_request)
            end
          end

          def access_token_from_request
            strong_memoize(:access_token_from_request) do
              find_personal_access_token_from_conan_jwt ||
                find_password_from_basic_auth
            end
          end

          def find_password_from_basic_auth
            return unless route_authentication_setting[:basic_auth_personal_access_token]
            return unless has_basic_credentials?(current_request)

            _username, password = user_name_and_password(current_request)
            password
          end

          def find_user_from_job_token
            return unless route_authentication_setting[:job_token_allowed]

            job = find_job_from_token || return
            @current_authenticated_job = job # rubocop:disable Gitlab/ModuleWithInstanceVariables

            job.user
          end

          def deploy_token_from_request
            find_deploy_token_from_conan_jwt || find_deploy_token_from_http_basic_auth
          end

          def find_job_from_token
            find_job_from_conan_jwt || find_job_from_http_basic_auth
          end

          # We need to override this one because it
          # looks into Bearer authorization header
          def find_oauth_access_token
          end

          def find_personal_access_token_from_conan_jwt
            token = decode_oauth_token_from_jwt

            return unless token

            token.access_token_id
          end

          def find_deploy_token_from_conan_jwt
            token = decode_oauth_token_from_jwt

            return unless token

            deploy_token = DeployToken.active.find_by_token(token.access_token_id.to_s)
            # note: uesr_id is not a user record id, but is the attribute set on ConanToken
            return if token.user_id != deploy_token&.username

            deploy_token
          end

          def find_job_from_conan_jwt
            token = decode_oauth_token_from_jwt

            return unless token

            ::Ci::AuthJobFinder.new(token: token.access_token_id.to_s).execute
          end

          def decode_oauth_token_from_jwt
            jwt = Doorkeeper::OAuth::Token.from_bearer_authorization(current_request)

            return unless jwt

            token = ::Gitlab::ConanToken.decode(jwt)

            return unless token && token.access_token_id && token.user_id

            token
          end

          def package_scope
            params[:id].present? ? :project : :instance
          end
        end
      end
    end
  end
end