lib/lazylead/confluence.rb
# frozen_string_literal: true

# The MIT License
#
# Copyright (c) 2019-2022 Yurii Dubinka
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom
# the Software is  furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
# OR OTHER DEALINGS IN THE SOFTWARE.

require "faraday"
require "jira-ruby"

module Lazylead
  # Represents single Confluence instance.
  #
  # Author:: Yurii Dubinka (yurii.dubinka@gmail.com)
  # Copyright:: Copyright (c) 2019-2020 Yurii Dubinka
  # License:: MIT
  class Confluence
    def initialize(conf)
      @conf = conf
      @http = Faraday.new(url: @conf.url) do |conn|
        conn.request :authorization, :basic, @conf.user, @conf.pass
      end
    end

    # Confluence instance url like
    #  http://confluence.com
    #  https://confluence.com
    def url
      @conf.url
    end

    # Construct the remote link from Jira ticket to Confluence page.
    # Read more:
    #  - https://bit.ly/2zkfTwB
    #  - https://bit.ly/2zk8iOB
    #  - https://bit.ly/3bkGkiU
    def make_link(url, type = "Wiki Page")
      {
        globalId: "appId=#{@conf.app}&pageId=#{url[/pageId=(?<id>\d+)/, 1]}",
        application: { type: @conf.type, name: @conf.name },
        relationship: type,
        object: { url: url, title: type }
      }
    end

    # Fetch the page url with pageId (Atlassian global id)
    #  using confluence space name and page title.
    #
    # @return url with following format
    #  http://host/pages/viewpage.action?pageId=xxxxx or blank string in case of
    #  errors
    #
    # @todo #/DEV Unable to find confluence pages where title has ":" symbol.
    #  The symbol ":" might be replaced by "%3A". The search needs to consider
    #  this case as well.
    def fetch_page_id(space, title)
      resp = @http.get(
        "/rest/api/content", limit: 1, spaceKey: space, title: title
      ).body
      return "" if resp.blank?
      pages = JSON.parse(resp)
      return "" if pages["results"].empty?
      "#{@conf.url}/pages/viewpage.action?pageId=#{pages['results'].first['id']}"
    end
  end

  # The reference from Jira issue to Confluence system.
  class Link
    # :issue  :: The issue from external system
    # :sys    :: The jira ticketing system
    # :cnfl   :: The Confluence instances details (host, appId)
    #            which expected within the comments
    def initialize(issue, sys, cnfl)
      @issue = issue
      @sys = sys
      @confl = cnfl
    end

    # Fetch ticket links from comments
    def fetch_links
      ticket = @sys.raw do |conn|
        conn.Issue.find(@issue.id, expand: "comments,changelog", fields: "")
      end
      @mentioned = mentioned_links(ticket)
      @existing = existing_links(ticket)
      @diff = @mentioned.reject { |url| @existing.include? url }
    end

    # Detect links mentioned in ticket comments
    # @todo #/DEV Refactor this method in order to make it more human-readable and remove the
    #  suppresion below
    #
    # rubocop:disable Metrics/CyclomaticComplexity
    # rubocop:disable Metrics/PerceivedComplexity
    def mentioned_links(ticket)
      ticket.comments
            .map { |cmnt| cmnt.attrs["body"] }
            .select { |cmnt| @confl.any? { |c| cmnt.include? c.url } }
            .flat_map(&:split)
            .select { |cmnt| @confl.any? { |c| cmnt.start_with? c.url } }
            .map { |url| to_page_id(url) }
            .reject(&:blank?)
            .uniq
    end
    # rubocop:enable Metrics/CyclomaticComplexity
    # rubocop:enable Metrics/PerceivedComplexity

    # Convert confluence page url to the following format:
    #  http://confluence.com/pages/viewpage.action?pageId=xxxxx
    #
    # Sometimes links to confluence pages have the following formats:
    #  http://confluence.com/pages/spaceKey=THESPACE&title=THETITLE
    #  http://confluence.com/display/THESPACE/THETITLE
    # and can't be linked though the "remote link" Jira functionality.
    def to_page_id(url)
      return url if url.include? "pageId="
      if url.include? "&title="
        space = url[/spaceKey=(?<space>[A-Z0-9+.]+)/, 1]
        title = url[/&title=(?<title>[A-Za-z0-9+.]+)/, 1]
      else
        space, title = url.split("/").last(2)
      end
      @confl.find { |c| url.start_with? c.url }
            .fetch_page_id(space, title.tr("+", " "))
    end

    # Detect existing links from the ticket
    def existing_links(ticket, type = "Wiki Page")
      ticket.remotelink
            .all
            .select { |l| l.attrs["relationship"] == type }
            .map { |e| e.attrs["object"]["url"] }
    end

    def need_link?
      !@diff.empty?
    end

    # Add reference to Confluence page from current issue
    def add_link
      @diff.each do |url|
        cnf = @confl.find { |c| url.start_with? c.url }
        @issue.remotelink.build.save cnf.make_link(url)
      end
    end
  end
end
