# frozen_string_literal: true

module VulnerabilityFeedback
  class CreateService < ::BaseService
    include Gitlab::Utils::StrongMemoize

    def execute
      raise Gitlab::Access::AccessDeniedError unless can?(current_user, :create_vulnerability_feedback, vulnerability_feedback)

      if vulnerability_feedback.for_issue? && !vulnerability_feedback.vulnerability_data.blank?
        create_issue
      elsif vulnerability_feedback.for_merge_request? && !vulnerability_feedback.vulnerability_data.blank?
        create_merge_request
      else
        dismiss_existing_vulnerability
      end

      if vulnerability_feedback.persisted? && vulnerability_feedback.valid?
        success(vulnerability_feedback)
      else
        rollback_merge_request(vulnerability_feedback.merge_request) if vulnerability_feedback.merge_request

        error(vulnerability_feedback.errors)
      end

    rescue ArgumentError => e
      # VulnerabilityFeedback relies on #enum attributes which raise this exception
      error(e.message)
    end

    private

    attr_reader :params

    def vulnerability_feedback
      @vulnerability_feedback ||= @project.vulnerability_feedback.find_or_init_for(create_params)
    end

    def create_params
      @params[:author] = @current_user
      @params.merge(comment_params).except(:dismiss_vulnerability)
    end

    def comment_params
      return {} unless @params[:comment].present?

      {
        comment_author: @current_user,
        comment_timestamp: Time.zone.now
      }
    end

    def success(vulnerability_feedback)
      super().merge(vulnerability_feedback: vulnerability_feedback)
    end

    def create_issue
      # Wrap Feedback and Issue creation in the same transaction
      ActiveRecord::Base.transaction do
        result = Issues::CreateFromVulnerabilityDataService
          .new(@project, @current_user, vulnerability_feedback.vulnerability_data)
          .execute

        if result[:status] == :error
          vulnerability_feedback.errors[:issue] << result[:message]
          raise ActiveRecord::Rollback
        end

        issue = result[:issue]

        issue_link_result = create_vulnerability_issue_link(vulnerability_feedback.vulnerability_data[:vulnerability_id], issue)

        if issue_link_result&.error?
          vulnerability_feedback.errors[:issue_link] << issue_link_result.message
          raise ActiveRecord::Rollback
        end

        vulnerability_feedback.issue = issue

        # Ensure created association is rolled back if feedback can't be saved
        raise ActiveRecord::Rollback unless vulnerability_feedback.save
      end
    end

    def create_merge_request
      result = MergeRequests::CreateFromVulnerabilityDataService
        .new(@project, @current_user, vulnerability_feedback.vulnerability_data)
        .execute

      if result[:status] == :success
        merge_request = result[:merge_request]
        vulnerability_feedback.merge_request = merge_request

        vulnerability_feedback.save
      else
        vulnerability_feedback.errors[:merge_request] << result[:message]
      end
    end

    # Gitaly RPCs cannot occur within a transaction so we must manually
    # rollback MR and branch creation
    def rollback_merge_request(merge_request)
      branch_name = merge_request.source_branch

      merge_request&.destroy &&
        ::Branches::DeleteService.new(project, current_user).execute(branch_name)
    end

    def create_vulnerability_issue_link(vulnerability_id, issue)
      return unless vulnerability_id

      raise Gitlab::Access::AccessDeniedError unless can?(current_user, :read_vulnerability, project)

      vulnerability = project.vulnerabilities.find_by_id(vulnerability_id)

      VulnerabilityIssueLinks::CreateService
        .new(current_user, vulnerability, issue, link_type: Vulnerabilities::IssueLink.link_types[:created])
        .execute
    end

    def dismiss_existing_vulnerability
      ActiveRecord::Base.transaction do
        if dismiss_vulnerability? && existing_vulnerability
          Vulnerabilities::DismissService.new(current_user,
                                              existing_vulnerability,
                                              params[:comment],
                                              dismiss_findings: false).execute
        end

        raise ActiveRecord::Rollback unless vulnerability_feedback.save
      end
    end

    def existing_vulnerability
      strong_memoize(:existing_vulnerability) { vulnerability_feedback.finding&.vulnerability }
    end

    def dismiss_vulnerability?
      params[:dismiss_vulnerability] != false && can?(current_user, :admin_vulnerability, project)
    end
  end
end
