# frozen_string_literal: true

module BitbucketServer
  class Connection
    include ActionView::Helpers::SanitizeHelper

    DEFAULT_API_VERSION = '1.0'
    SEPARATOR = '/'

    attr_reader :api_version, :base_uri, :username, :token

    ConnectionError = Class.new(StandardError)

    def initialize(options = {})
      @api_version = options.fetch(:api_version, DEFAULT_API_VERSION)
      @base_uri = options[:base_uri]
      @username = options[:user]
      @token = options[:password]
    end

    def get(path, extra_query = {})
      response = Gitlab::HTTP.get(build_url(path),
                                  basic_auth: auth,
                                  headers: accept_headers,
                                  query: extra_query)

      check_errors!(response)

      response.parsed_response
    end

    def post(path, body)
      response = Gitlab::HTTP.post(build_url(path),
                                   basic_auth: auth,
                                   headers: post_headers,
                                   body: body)

      check_errors!(response)

      response.parsed_response
    end

    # We need to support two different APIs for deletion:
    #
    # /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/branches/default
    # /rest/branch-utils/1.0/projects/{projectKey}/repos/{repositorySlug}/branches
    def delete(resource, path, body)
      url = delete_url(resource, path)

      response = Gitlab::HTTP.delete(url,
                                     basic_auth: auth,
                                     headers: post_headers,
                                     body: body)

      check_errors!(response)

      response.parsed_response
    end

    private

    def check_errors!(response)
      raise ConnectionError, "Response is not valid JSON" unless response.parsed_response.is_a?(Hash)

      return if response.code >= 200 && response.code < 300

      details = sanitize(response.parsed_response.dig('errors', 0, 'message'))
      message = "Error #{response.code}"
      message += ": #{details}" if details

      raise ConnectionError, message
    rescue JSON::ParserError
      raise ConnectionError, "Unable to parse the server response as JSON"
    end

    def auth
      @auth ||= { username: username, password: token }
    end

    def accept_headers
      @accept_headers ||= { 'Accept' => 'application/json' }
    end

    def post_headers
      @post_headers ||= accept_headers.merge({ 'Content-Type' => 'application/json' })
    end

    def build_url(path)
      return path if path.starts_with?(root_url)

      url_join_paths(root_url, path)
    end

    def root_url
      url_join_paths(base_uri, "/rest/api/#{api_version}")
    end

    def delete_url(resource, path)
      if resource == :branches
        url_join_paths(base_uri, "/rest/branch-utils/#{api_version}#{path}")
      else
        build_url(path)
      end
    end

    # URI.join is stupid in that slashes are important:
    #
    # # URI.join('http://example.com/subpath', 'hello')
    # => http://example.com/hello
    #
    # We really want http://example.com/subpath/hello
    #
    def url_join_paths(*paths)
      paths.map { |path| strip_slashes(path) }.join(SEPARATOR)
    end

    def strip_slashes(path)
      path = path[1..-1] if path.starts_with?(SEPARATOR)
      path.chomp(SEPARATOR)
    end
  end
end
