module API
  # Projects API
  class Projects < Grape::API
    before { authenticate! }

    resource :projects, requirements: { id: /[^\/]+/ } do
      helpers do
        def map_public_to_visibility_level(attrs)
          publik = attrs.delete(:public)
          if publik.present? && !attrs[:visibility_level].present?
            publik = to_boolean(publik)
            # Since setting the public attribute to private could mean either
            # private or internal, use the more conservative option, private.
            attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE
          end
          attrs
        end
      end

      # Get a projects list for authenticated user
      #
      # Example Request:
      #   GET /projects
      get do
        projects = current_user.authorized_projects
        projects = filter_projects(projects)
        projects = paginate projects
        entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess

        present projects, with: entity, user: current_user
      end

      # Get a list of visible projects for authenticated user
      #
      # Example Request:
      #   GET /projects/visible
      get '/visible' do
        projects = ProjectsFinder.new.execute(current_user)
        projects = filter_projects(projects)
        projects = paginate projects
        entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess

        present projects, with: entity, user: current_user
      end

      # Get an owned projects list for authenticated user
      #
      # Example Request:
      #   GET /projects/owned
      get '/owned' do
        projects = current_user.owned_projects
        projects = filter_projects(projects)
        projects = paginate projects
        present projects, with: Entities::ProjectWithAccess, user: current_user
      end

      # Gets starred project for the authenticated user
      #
      # Example Request:
      #   GET /projects/starred
      get '/starred' do
        projects = current_user.viewable_starred_projects
        projects = filter_projects(projects)
        projects = paginate projects
        present projects, with: Entities::Project, user: current_user
      end

      # Get all projects for admin user
      #
      # Example Request:
      #   GET /projects/all
      get '/all' do
        authenticated_as_admin!
        projects = Project.all
        projects = filter_projects(projects)
        projects = paginate projects
        present projects, with: Entities::ProjectWithAccess, user: current_user
      end

      # Get a single project
      #
      # Parameters:
      #   id (required) - The ID of a project
      # Example Request:
      #   GET /projects/:id
      get ":id" do
        present user_project, with: Entities::ProjectWithAccess, user: current_user,
                              user_can_admin_project: can?(current_user, :admin_project, user_project)
      end

      # Get events for a single project
      #
      # Parameters:
      #   id (required) - The ID of a project
      # Example Request:
      #   GET /projects/:id/events
      get ":id/events" do
        events = paginate user_project.events.recent
        present events, with: Entities::Event
      end

      # Create new project
      #
      # Parameters:
      #   name (required)                   - name for new project
      #   description (optional)            - short project description
      #   issues_enabled (optional)
      #   merge_requests_enabled (optional)
      #   builds_enabled (optional)
      #   wiki_enabled (optional)
      #   snippets_enabled (optional)
      #   container_registry_enabled (optional)
      #   shared_runners_enabled (optional)
      #   namespace_id (optional)           - defaults to user namespace
      #   public (optional)                 - if true same as setting visibility_level = 20
      #   visibility_level (optional)       - 0 by default
      #   import_url (optional)
      #   public_builds (optional)
      #   lfs_enabled (optional)
      #   request_access_enabled (optional) - Allow users to request member access
      # Example Request
      #   POST /projects
      post do
        required_attributes! [:name]
        attrs = attributes_for_keys [:builds_enabled,
                                     :container_registry_enabled,
                                     :description,
                                     :import_url,
                                     :issues_enabled,
                                     :lfs_enabled,
                                     :merge_requests_enabled,
                                     :name,
                                     :namespace_id,
                                     :only_allow_merge_if_build_succeeds,
                                     :path,
                                     :public,
                                     :public_builds,
                                     :request_access_enabled,
                                     :shared_runners_enabled,
                                     :snippets_enabled,
                                     :visibility_level,
                                     :wiki_enabled]
        attrs = map_public_to_visibility_level(attrs)
        @project = ::Projects::CreateService.new(current_user, attrs).execute
        if @project.saved?
          present @project, with: Entities::Project,
                            user_can_admin_project: can?(current_user, :admin_project, @project)
        else
          if @project.errors[:limit_reached].present?
            error!(@project.errors[:limit_reached], 403)
          end
          render_validation_error!(@project)
        end
      end

      # Create new project for a specified user.  Only available to admin users.
      #
      # Parameters:
      #   user_id (required)                - The ID of a user
      #   name (required)                   - name for new project
      #   description (optional)            - short project description
      #   default_branch (optional)         - 'master' by default
      #   issues_enabled (optional)
      #   merge_requests_enabled (optional)
      #   builds_enabled (optional)
      #   wiki_enabled (optional)
      #   snippets_enabled (optional)
      #   container_registry_enabled (optional)
      #   shared_runners_enabled (optional)
      #   public (optional)                 - if true same as setting visibility_level = 20
      #   visibility_level (optional)
      #   import_url (optional)
      #   public_builds (optional)
      #   lfs_enabled (optional)
      #   request_access_enabled (optional) - Allow users to request member access
      # Example Request
      #   POST /projects/user/:user_id
      post "user/:user_id" do
        authenticated_as_admin!
        user = User.find(params[:user_id])
        attrs = attributes_for_keys [:builds_enabled,
                                     :default_branch,
                                     :description,
                                     :import_url,
                                     :issues_enabled,
                                     :lfs_enabled,
                                     :merge_requests_enabled,
                                     :name,
                                     :only_allow_merge_if_build_succeeds,
                                     :public,
                                     :public_builds,
                                     :request_access_enabled,
                                     :shared_runners_enabled,
                                     :snippets_enabled,
                                     :visibility_level,
                                     :wiki_enabled]
        attrs = map_public_to_visibility_level(attrs)
        @project = ::Projects::CreateService.new(user, attrs).execute
        if @project.saved?
          present @project, with: Entities::Project,
                            user_can_admin_project: can?(current_user, :admin_project, @project)
        else
          render_validation_error!(@project)
        end
      end

      # Fork new project for the current user or provided namespace.
      #
      # Parameters:
      #   id (required) - The ID of a project
      #   namespace (optional) - The ID or name of the namespace that the project will be forked into.
      # Example Request
      #   POST /projects/fork/:id
      post 'fork/:id' do
        attrs = {}
        namespace_id = params[:namespace]

        if namespace_id.present?
          namespace = Namespace.find_by(id: namespace_id) || Namespace.find_by_path_or_name(namespace_id)

          unless namespace && can?(current_user, :create_projects, namespace)
            not_found!('Target Namespace')
          end

          attrs[:namespace] = namespace
        end

        @forked_project =
          ::Projects::ForkService.new(user_project,
                                      current_user,
                                      attrs).execute

        if @forked_project.errors.any?
          conflict!(@forked_project.errors.messages)
        else
          present @forked_project, with: Entities::Project,
                                   user_can_admin_project: can?(current_user, :admin_project, @forked_project)
        end
      end

      # Update an existing project
      #
      # Parameters:
      #   id (required) - the id of a project
      #   name (optional) - name of a project
      #   path (optional) - path of a project
      #   description (optional) - short project description
      #   issues_enabled (optional)
      #   merge_requests_enabled (optional)
      #   builds_enabled (optional)
      #   wiki_enabled (optional)
      #   snippets_enabled (optional)
      #   container_registry_enabled (optional)
      #   shared_runners_enabled (optional)
      #   public (optional) - if true same as setting visibility_level = 20
      #   visibility_level (optional) - visibility level of a project
      #   public_builds (optional)
      #   lfs_enabled (optional)
      # Example Request
      #   PUT /projects/:id
      put ':id' do
        attrs = attributes_for_keys [:builds_enabled,
                                     :container_registry_enabled,
                                     :default_branch,
                                     :description,
                                     :issues_enabled,
                                     :lfs_enabled,
                                     :merge_requests_enabled,
                                     :name,
                                     :only_allow_merge_if_build_succeeds,
                                     :path,
                                     :public,
                                     :public_builds,
                                     :request_access_enabled,
                                     :shared_runners_enabled,
                                     :snippets_enabled,
                                     :visibility_level,
                                     :wiki_enabled]
        attrs = map_public_to_visibility_level(attrs)
        authorize_admin_project
        authorize! :rename_project, user_project if attrs[:name].present?
        if attrs[:visibility_level].present?
          authorize! :change_visibility_level, user_project
        end

        ::Projects::UpdateService.new(user_project,
                                      current_user, attrs).execute

        if user_project.errors.any?
          render_validation_error!(user_project)
        else
          present user_project, with: Entities::Project,
                                user_can_admin_project: can?(current_user, :admin_project, user_project)
        end
      end

      # Archive project
      #
      # Parameters:
      #   id (required) - The ID of a project
      # Example Request:
      #   PUT /projects/:id/archive
      post ':id/archive' do
        authorize!(:archive_project, user_project)

        user_project.archive!

        present user_project, with: Entities::Project
      end

      # Unarchive project
      #
      # Parameters:
      #   id (required) - The ID of a project
      # Example Request:
      #   PUT /projects/:id/unarchive
      post ':id/unarchive' do
        authorize!(:archive_project, user_project)

        user_project.unarchive!

        present user_project, with: Entities::Project
      end

      # Star project
      #
      # Parameters:
      #   id (required) - The ID of a project
      # Example Request:
      #   POST /projects/:id/star
      post ':id/star' do
        if current_user.starred?(user_project)
          not_modified!
        else
          current_user.toggle_star(user_project)
          user_project.reload

          present user_project, with: Entities::Project
        end
      end

      # Unstar project
      #
      # Parameters:
      #   id (required) - The ID of a project
      # Example Request:
      #   DELETE /projects/:id/star
      delete ':id/star' do
        if current_user.starred?(user_project)
          current_user.toggle_star(user_project)
          user_project.reload

          present user_project, with: Entities::Project
        else
          not_modified!
        end
      end

      # Remove project
      #
      # Parameters:
      #   id (required) - The ID of a project
      # Example Request:
      #   DELETE /projects/:id
      delete ":id" do
        authorize! :remove_project, user_project
        ::Projects::DestroyService.new(user_project, current_user, {}).async_execute
      end

      # Mark this project as forked from another
      #
      # Parameters:
      #   id: (required) - The ID of the project being marked as a fork
      #   forked_from_id: (required) - The ID of the project it was forked from
      # Example Request:
      #   POST /projects/:id/fork/:forked_from_id
      post ":id/fork/:forked_from_id" do
        authenticated_as_admin!
        forked_from_project = find_project(params[:forked_from_id])
        unless forked_from_project.nil?
          if user_project.forked_from_project.nil?
            user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id)
          else
            render_api_error!("Project already forked", 409)
          end
        else
          not_found!("Source Project")
        end
      end

      # Remove a forked_from relationship
      #
      # Parameters:
      #   id: (required) - The ID of the project being marked as a fork
      # Example Request:
      #  DELETE /projects/:id/fork
      delete ":id/fork" do
        authorize! :remove_fork_project, user_project
        if user_project.forked?
          user_project.forked_project_link.destroy
        end
      end

      # Share project with group
      #
      # Parameters:
      #   id (required)           - The ID of a project
      #   group_id (required)     - The ID of a group
      #   group_access (required) - Level of permissions for sharing
      #   expires_at (optional)   - Share expiration date
      #
      # Example Request:
      #   POST /projects/:id/share
      post ":id/share" do
        authorize! :admin_project, user_project
        required_attributes! [:group_id, :group_access]
        attrs = attributes_for_keys [:group_id, :group_access, :expires_at]

        group = Group.find_by_id(attrs[:group_id])

        unless group && can?(current_user, :read_group, group)
          not_found!('Group')
        end

        unless user_project.allowed_to_share_with_group?
          return render_api_error!("The project sharing with group is disabled", 400)
        end

        link = user_project.project_group_links.new(attrs)

        if link.save
          present link, with: Entities::ProjectGroupLink
        else
          render_api_error!(link.errors.full_messages.first, 409)
        end
      end

      # Upload a file
      #
      # Parameters:
      #   id: (required) - The ID of the project
      #   file: (required) - The file to be uploaded
      post ":id/uploads" do
        ::Projects::UploadService.new(user_project, params[:file]).execute
      end

      # search for projects current_user has access to
      #
      # Parameters:
      #   query (required) - A string contained in the project name
      #   per_page (optional) - number of projects to return per page
      #   page (optional) - the page to retrieve
      # Example Request:
      #   GET /projects/search/:query
      get "/search/:query" do
        search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
        projects = search_service.objects('projects', params[:page])
        projects = projects.reorder(project_order_by => project_sort)

        present paginate(projects), with: Entities::Project
      end

      # Get a users list
      #
      # Example Request:
      #  GET /users
      get ':id/users' do
        @users = User.where(id: user_project.team.users.map(&:id))
        @users = @users.search(params[:search]) if params[:search].present?
        @users = paginate @users
        present @users, with: Entities::UserBasic
      end
    end
  end
end
