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

    resource :runners do
      # Get runners available for user
      #
      # Example Request:
      #   GET /runners
      get do
        runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: ['specific', 'shared'])
        present paginate(runners), with: Entities::Runner
      end

      # Get all runners - shared and specific
      #
      # Example Request:
      #   GET /runners/all
      get 'all' do
        authenticated_as_admin!
        runners = filter_runners(Ci::Runner.all, params[:scope])
        present paginate(runners), with: Entities::Runner
      end

      # Get runner's details
      #
      # Parameters:
      #   id (required) - The ID of ther runner
      # Example Request:
      #   GET /runners/:id
      get ':id' do
        runner = get_runner(params[:id])
        authenticate_show_runner!(runner)

        present runner, with: Entities::RunnerDetails, current_user: current_user
      end

      # Update runner's details
      #
      # Parameters:
      #   id (required) - The ID of ther runner
      #   description (optional) - Runner's description
      #   active (optional) - Runner's status
      #   tag_list (optional) - Array of tags for runner
      # Example Request:
      #   PUT /runners/:id
      put ':id' do
        runner = get_runner(params[:id])
        authenticate_update_runner!(runner)

        attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged, :locked]
        if runner.update(attrs)
          present runner, with: Entities::RunnerDetails, current_user: current_user
        else
          render_validation_error!(runner)
        end
      end

      # Remove runner
      #
      # Parameters:
      #   id (required) - The ID of ther runner
      # Example Request:
      #   DELETE /runners/:id
      delete ':id' do
        runner = get_runner(params[:id])
        authenticate_delete_runner!(runner)
        runner.destroy!

        present runner, with: Entities::Runner
      end
    end

    resource :projects do
      before { authorize_admin_project }

      # Get runners available for project
      #
      # Example Request:
      #   GET /projects/:id/runners
      get ':id/runners' do
        runners = filter_runners(Ci::Runner.owned_or_shared(user_project.id), params[:scope])
        present paginate(runners), with: Entities::Runner
      end

      # Enable runner for project
      #
      # Parameters:
      #   id (required) - The ID of the project
      #   runner_id (required) - The ID of the runner
      # Example Request:
      #   POST /projects/:id/runners/:runner_id
      post ':id/runners' do
        required_attributes! [:runner_id]

        runner = get_runner(params[:runner_id])
        authenticate_enable_runner!(runner)

        runner_project = runner.assign_to(user_project)

        if runner_project.persisted?
          present runner, with: Entities::Runner
        else
          conflict!("Runner was already enabled for this project")
        end
      end

      # Disable project's runner
      #
      # Parameters:
      #   id (required) - The ID of the project
      #   runner_id (required) - The ID of the runner
      # Example Request:
      #   DELETE /projects/:id/runners/:runner_id
      delete ':id/runners/:runner_id' do
        runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id])
        not_found!('Runner') unless runner_project

        runner = runner_project.runner
        forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1

        runner_project.destroy

        present runner, with: Entities::Runner
      end
    end

    helpers do
      def filter_runners(runners, scope, options = {})
        return runners unless scope.present?

        available_scopes = ::Ci::Runner::AVAILABLE_SCOPES
        if options[:without]
          available_scopes = available_scopes - options[:without]
        end

        if (available_scopes & [scope]).empty?
          render_api_error!('Scope contains invalid value', 400)
        end

        runners.send(scope)
      end

      def get_runner(id)
        runner = Ci::Runner.find(id)
        not_found!('Runner') unless runner
        runner
      end

      def authenticate_show_runner!(runner)
        return if runner.is_shared || current_user.is_admin?
        forbidden!("No access granted") unless user_can_access_runner?(runner)
      end

      def authenticate_update_runner!(runner)
        return if current_user.is_admin?
        forbidden!("Runner is shared") if runner.is_shared?
        forbidden!("No access granted") unless user_can_access_runner?(runner)
      end

      def authenticate_delete_runner!(runner)
        return if current_user.is_admin?
        forbidden!("Runner is shared") if runner.is_shared?
        forbidden!("Runner associated with more than one project") if runner.projects.count > 1
        forbidden!("No access granted") unless user_can_access_runner?(runner)
      end

      def authenticate_enable_runner!(runner)
        forbidden!("Runner is shared") if runner.is_shared?
        forbidden!("Runner is locked") if runner.locked?
        return if current_user.is_admin?
        forbidden!("No access granted") unless user_can_access_runner?(runner)
      end

      def user_can_access_runner?(runner)
        current_user.ci_authorized_runners.exists?(runner.id)
      end
    end
  end
end
