# frozen_string_literal: true

module SystemNotes
  class IssuablesService < ::SystemNotes::BaseService
    #
    # noteable_ref - Referenced noteable object
    #
    # Example Note text:
    #
    #   "marked this issue as related to gitlab-foss#9001"
    #
    # Returns the created Note object
    def relate_issue(noteable_ref)
      body = "marked this issue as related to #{noteable_ref.to_reference(noteable.project)}"

      issue_activity_counter.track_issue_related_action(author: author) if noteable.is_a?(Issue)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'relate'))
    end

    #
    # noteable_ref - Referenced noteable object
    #
    # Example Note text:
    #
    #   "removed the relation with gitlab-foss#9001"
    #
    # Returns the created Note object
    def unrelate_issue(noteable_ref)
      body = "removed the relation with #{noteable_ref.to_reference(noteable.project)}"

      issue_activity_counter.track_issue_unrelated_action(author: author) if noteable.is_a?(Issue)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'unrelate'))
    end

    # Called when the assignee of a Noteable is changed or removed
    #
    # assignee - User being assigned, or nil
    #
    # Example Note text:
    #
    #   "removed assignee"
    #
    #   "assigned to @rspeicher"
    #
    # Returns the created Note object
    def change_assignee(assignee)
      body = assignee.nil? ? 'removed assignee' : "assigned to #{assignee.to_reference}"

      issue_activity_counter.track_issue_assignee_changed_action(author: author) if noteable.is_a?(Issue)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee'))
    end

    # Called when the assignees of an issuable is changed or removed
    #
    # assignees - Users being assigned, or nil
    #
    # Example Note text:
    #
    #   "removed all assignees"
    #
    #   "assigned to @user1 additionally to @user2"
    #
    #   "assigned to @user1, @user2 and @user3 and unassigned from @user4 and @user5"
    #
    #   "assigned to @user1 and @user2"
    #
    # Returns the created Note object
    def change_issuable_assignees(old_assignees)
      unassigned_users = old_assignees - noteable.assignees
      added_users = noteable.assignees.to_a - old_assignees
      text_parts = []

      Gitlab::I18n.with_default_locale do
        text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
        text_parts << "unassigned #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any?
      end

      body = text_parts.join(' and ')

      issue_activity_counter.track_issue_assignee_changed_action(author: author) if noteable.is_a?(Issue)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee'))
    end

    # Called when the reviewers of an issuable is changed or removed
    #
    # reviewers - Users being requested to review, or nil
    #
    # Example Note text:
    #
    #   "requested review from @user1 and @user2"
    #
    #   "requested review from @user1, @user2 and @user3 and removed review request for @user4 and @user5"
    #
    # Returns the created Note object
    def change_issuable_reviewers(old_reviewers)
      unassigned_users = old_reviewers - noteable.reviewers
      added_users = noteable.reviewers - old_reviewers
      text_parts = []

      Gitlab::I18n.with_default_locale do
        text_parts << "requested review from #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
        text_parts << "removed review request for #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any?
      end

      body = text_parts.join(' and ')

      create_note(NoteSummary.new(noteable, project, author, body, action: 'reviewer'))
    end

    # Called when the contacts of an issuable are changed or removed
    # We intend to reference the contacts but for security we are just
    # going to state how many were added/removed for now. See discussion:
    # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77816#note_806114273
    #
    # added_count - number of contacts added, or 0
    # removed_count - number of contacts  removed, or 0
    #
    # Example Note text:
    #
    #   "added 2 contacts"
    #
    #   "added 3 contacts and removed one contact"
    #
    # Returns the created Note object
    def change_issuable_contacts(added_count, removed_count)
      text_parts = []

      Gitlab::I18n.with_default_locale do
        text_parts << "added #{added_count} #{'contact'.pluralize(added_count)}" if added_count > 0
        text_parts << "removed #{removed_count} #{'contact'.pluralize(removed_count)}" if removed_count > 0
      end

      return if text_parts.empty?

      body = text_parts.join(' and ')
      create_note(NoteSummary.new(noteable, project, author, body, action: 'contact'))
    end

    # Called when the title of a Noteable is changed
    #
    # old_title - Previous String title
    #
    # Example Note text:
    #
    #   "changed title from **Old** to **New**"
    #
    # Returns the created Note object
    def change_title(old_title)
      new_title = noteable.title.dup

      old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs

      marked_old_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(old_title).mark(old_diffs)
      marked_new_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(new_title).mark(new_diffs)

      body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**"

      issue_activity_counter.track_issue_title_changed_action(author: author) if noteable.is_a?(Issue)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
    end

    # Called when the description of a Noteable is changed
    #
    # noteable  - Noteable object that responds to `description`
    # project   - Project owning noteable
    # author    - User performing the change
    #
    # Example Note text:
    #
    #   "changed the description"
    #
    # Returns the created Note object
    def change_description
      body = 'changed the description'

      issue_activity_counter.track_issue_description_changed_action(author: author) if noteable.is_a?(Issue)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'description'))
    end

    # Called when a Mentionable (the `mentioned_in`) references another Mentionable (the `mentioned`,
    # passed to this service as `noteable`).
    #
    # Example Note text:
    #
    #   "mentioned in #1"
    #
    #   "mentioned in !2"
    #
    #   "mentioned in 54f7727c"
    #
    # See cross_reference_note_content.
    #
    # @param mentioned_in [Mentionable]
    # @return [Note]
    def cross_reference(mentioned_in)
      return if cross_reference_disallowed?(mentioned_in)

      gfm_reference = mentioned_in.gfm_reference(noteable.project || noteable.group)
      body = cross_reference_note_content(gfm_reference)

      if noteable.is_a?(ExternalIssue)
        Integrations::CreateExternalCrossReferenceWorker.perform_async(
          noteable.project_id,
          noteable.id,
          mentioned_in.class.name,
          mentioned_in.id,
          author.id
        )
      else
        track_cross_reference_action
        create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference'))
      end
    end

    # Check if a cross-reference is disallowed
    #
    # This method prevents adding a "mentioned in !1" note on every single commit
    # in a merge request. Additionally, it prevents the creation of references to
    # external issues (which would fail).
    #
    # @param mentioned_in [Mentionable]
    # @return [Boolean]
    def cross_reference_disallowed?(mentioned_in)
      return true if noteable.is_a?(ExternalIssue) && !noteable.project&.external_references_supported?
      return false unless mentioned_in.is_a?(MergeRequest)
      return false unless noteable.is_a?(Commit)

      mentioned_in.commits.include?(noteable)
    end

    # Called when the status of a Task has changed
    #
    # new_task  - TaskList::Item object.
    #
    # Example Note text:
    #
    #   "marked the task Whatever as completed."
    #
    # Returns the created Note object
    def change_task_status(new_task)
      status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE
      body = "marked the task **#{new_task.source}** as #{status_label}"

      issue_activity_counter.track_issue_description_changed_action(author: author) if noteable.is_a?(Issue)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'task'))
    end

    # Called when noteable has been moved to another project
    #
    # noteable_ref - Referenced noteable
    # direction    - symbol, :to or :from
    #
    # Example Note text:
    #
    #   "moved to some_namespace/project_new#11"
    #
    # Returns the created Note object
    def noteable_moved(noteable_ref, direction)
      unless [:to, :from].include?(direction)
        raise ArgumentError, "Invalid direction `#{direction}`"
      end

      cross_reference = noteable_ref.to_reference(project)
      body = "moved #{direction} #{cross_reference}"

      issue_activity_counter.track_issue_moved_action(author: author) if noteable.is_a?(Issue)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'moved'))
    end

    # Called when noteable has been cloned
    #
    # noteable_ref - Referenced noteable
    # direction    - symbol, :to or :from
    #
    # Example Note text:
    #
    #   "cloned to some_namespace/project_new#11"
    #
    # Returns the created Note object
    def noteable_cloned(noteable_ref, direction)
      unless [:to, :from].include?(direction)
        raise ArgumentError, "Invalid direction `#{direction}`"
      end

      cross_reference = noteable_ref.to_reference(project)
      body = "cloned #{direction} #{cross_reference}"

      issue_activity_counter.track_issue_cloned_action(author: author) if noteable.is_a?(Issue) && direction == :to

      create_note(NoteSummary.new(noteable, project, author, body, action: 'cloned'))
    end

    # Called when the confidentiality changes
    #
    # Example Note text:
    #
    #   "made the issue confidential"
    #
    # Returns the created Note object
    def change_issue_confidentiality
      if noteable.confidential
        body = 'made the issue confidential'
        action = 'confidential'

        issue_activity_counter.track_issue_made_confidential_action(author: author) if noteable.is_a?(Issue)
      else
        body = 'made the issue visible to everyone'
        action = 'visible'

        issue_activity_counter.track_issue_made_visible_action(author: author) if noteable.is_a?(Issue)
      end

      create_note(NoteSummary.new(noteable, project, author, body, action: action))
    end

    # Called when the status of a Noteable is changed
    #
    # status   - String status
    # source   - Mentionable performing the change, or nil
    #
    # Example Note text:
    #
    #   "merged"
    #
    #   "closed via bc17db76"
    #
    # Returns the created Note object
    def change_status(status, source = nil)
      create_resource_state_event(status: status, mentionable_source: source)
    end

    # Check if a cross reference to a Mentionable from the `mentioned_in` Mentionable
    # already exists.
    #
    # This method is used to prevent multiple notes being created for a mention
    # when a issue is updated, for example. The method also calls `existing_mentions_for`
    # to check if the mention is in a commit, and return matches only on commit hash
    # instead of project + commit, to avoid repeated mentions from forks.
    #
    # @param mentioned_in [Mentionable]
    # @return [Boolean]
    def cross_reference_exists?(mentioned_in)
      notes = noteable.notes.system
      existing_mentions_for(mentioned_in, noteable, notes).exists?
    end

    # Called when a user's attention has been requested for a Notable
    #
    # user - User's whos attention has been requested
    #
    # Example Note text:
    #
    #   "requested attention from @eli.wisoky"
    #
    # Returns the created Note object
    def request_attention(user)
      body = "requested attention from #{user.to_reference}"

      create_note(NoteSummary.new(noteable, project, author, body, action: 'attention_requested'))
    end

    # Called when a user's attention request has been removed for a Notable
    #
    # user - User's whos attention request has been removed
    #
    # Example Note text:
    #
    #   "removed attention request from @eli.wisoky"
    #
    # Returns the created Note object
    def remove_attention_request(user)
      body = "removed attention request from #{user.to_reference}"

      create_note(NoteSummary.new(noteable, project, author, body, action: 'attention_request_removed'))
    end

    # Called when a Noteable has been marked as the canonical Issue of a duplicate
    #
    # duplicate_issue - Issue that was a duplicate of this
    #
    # Example Note text:
    #
    #   "marked #1234 as a duplicate of this issue"
    #
    #   "marked other_project#5678 as a duplicate of this issue"
    #
    # Returns the created Note object
    def mark_canonical_issue_of_duplicate(duplicate_issue)
      body = "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue"
      create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
    end

    # Called when a Noteable has been marked as a duplicate of another Issue
    #
    # canonical_issue - Issue that this is a duplicate of
    #
    # Example Note text:
    #
    #   "marked this issue as a duplicate of #1234"
    #
    #   "marked this issue as a duplicate of other_project#5678"
    #
    # Returns the created Note object
    def mark_duplicate_issue(canonical_issue)
      body = "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}"

      issue_activity_counter.track_issue_marked_as_duplicate_action(author: author) if noteable.is_a?(Issue)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
    end

    def add_email_participants(body)
      create_note(NoteSummary.new(noteable, project, author, body))
    end

    def discussion_lock
      action = noteable.discussion_locked? ? 'locked' : 'unlocked'
      body = "#{action} this #{noteable.class.to_s.titleize.downcase}"

      if noteable.is_a?(Issue)
        if action == 'locked'
          issue_activity_counter.track_issue_locked_action(author: author)
        else
          issue_activity_counter.track_issue_unlocked_action(author: author)
        end
      end

      create_note(NoteSummary.new(noteable, project, author, body, action: action))
    end

    def close_after_error_tracking_resolve
      create_resource_state_event(status: 'closed', close_after_error_tracking_resolve: true)
    end

    def auto_resolve_prometheus_alert
      create_resource_state_event(status: 'closed', close_auto_resolve_prometheus_alert: true)
    end

    def change_issue_type
      body = "changed issue type to #{noteable.issue_type.humanize(capitalize: false)}"

      create_note(NoteSummary.new(noteable, project, author, body, action: 'issue_type'))
    end

    private

    def cross_reference_note_content(gfm_reference)
      "#{self.class.cross_reference_note_prefix}#{gfm_reference}"
    end

    def existing_mentions_for(mentioned_in, noteable, notes)
      if mentioned_in.is_a?(Commit)
        text = "#{self.class.cross_reference_note_prefix}%#{mentioned_in.to_reference(nil)}"
        notes.like_note_or_capitalized_note(text)
      else
        gfm_reference = mentioned_in.gfm_reference(noteable.project || noteable.group)
        text = cross_reference_note_content(gfm_reference)
        notes.for_note_or_capitalized_note(text)
      end
    end

    def self.cross_reference_note_prefix
      'mentioned in '
    end

    def self.cross_reference?(note_text)
      note_text =~ /\A#{cross_reference_note_prefix}/i
    end

    def create_resource_state_event(params)
      ResourceEvents::ChangeStateService.new(resource: noteable, user: author)
        .execute(params)
    end

    def issue_activity_counter
      Gitlab::UsageDataCounters::IssueActivityUniqueCounter
    end

    def track_cross_reference_action
      issue_activity_counter.track_issue_cross_referenced_action(author: author) if noteable.is_a?(Issue)
    end
  end
end

SystemNotes::IssuablesService.prepend_mod_with('SystemNotes::IssuablesService')
