# frozen_string_literal: true

# Copyright (C) 2017-2020 MongoDB Inc.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

module Mongo
  module Srv
    # SRV record lookup result.
    #
    # Contains server addresses that the query resolved to, and minimum TTL
    # of the DNS records.
    #
    # @api private
    class Result
      include Address::Validator

      # @return [ String ] MISMATCHED_DOMAINNAME Error message format string indicating that an SRV
      #   record found does not match the domain of a hostname.
      MISMATCHED_DOMAINNAME = 'Parent domain name in SRV record result (%s) does not match ' +
                              'that of the hostname (%s)'

      # @return [ String ] query_hostname The hostname pointing to the DNS records.
      attr_reader :query_hostname

      # @return [ Array<String> ] address_strs The host strings of the SRV records
      #   for the query hostname.
      attr_reader :address_strs

      # @return [ Integer | nil ] min_ttl The smallest TTL found among the
      #   records (or nil if no records have been added).
      attr_accessor :min_ttl

      # Create a new object to keep track of the SRV records of the hostname.
      #
      # @param [ String ] hostname The hostname pointing to the DNS records.
      def initialize(hostname)
        @query_hostname = hostname
        @address_strs = []
        @min_ttl = nil
      end

      # Checks whether there are any records.
      #
      # @return [ Boolean ] Whether or not there are any records.
      def empty?
        @address_strs.empty?
      end

      # Adds a new record.
      #
      # @param [ Resolv::DNS::Resource ] record An SRV record found for the hostname.
      def add_record(record)
        record_host = normalize_hostname(record.target.to_s)
        port = record.port
        validate_hostname!(record_host)
        validate_same_origin!(record_host)
        address_str = if record_host.index(':')
                        # IPV6 address
                        "[#{record_host}]:#{port}"
                      else
                        "#{record_host}:#{port}"
                      end
        @address_strs << address_str

        @min_ttl = if @min_ttl.nil?
                     record.ttl
                   else
                     [ @min_ttl, record.ttl ].min
                   end

        nil
      end

      private

      # Transforms the provided hostname to simplify its validation later on.
      #
      # This method is safe to call during both initial DNS seed list discovery
      # and during SRV monitoring, in that it does not convert invalid hostnames
      # into valid ones.
      #
      # - Converts the hostname to lower case.
      # - Removes one trailing dot, if there is exactly one. If the hostname
      #   has multiple trailing dots, it is unchanged.
      #
      # @param [ String ] host Hostname to transform.
      def normalize_hostname(host)
        host = host.downcase
        host = host.delete_suffix('.') unless host.end_with?('..')
        host
      end

      # Ensures that a record's domain name matches that of the hostname.
      #
      # A hostname's domain name consists of each of the '.' delineated
      # parts after the first. For example, the hostname 'foo.bar.baz'
      # has the domain name 'bar.baz'.
      #
      # If the hostname has less than three parts, its domain name is the hostname itself.
      #
      # @param [ String ] record_host The host of the SRV record.
      #
      # @raise [ Mongo::Error::MismatchedDomain ] If the record's domain name doesn't match that of
      #   the hostname.
      def validate_same_origin!(record_host)
        srv_host_domain = query_hostname.split('.')
        srv_is_less_than_three_parts = srv_host_domain.length < 3
        srv_host_domain = srv_host_domain[1..-1] unless srv_is_less_than_three_parts
        record_host_parts = record_host.split('.')

        if srv_is_less_than_three_parts && record_host_parts.length <= srv_host_domain.length
          raise Error::MismatchedDomain.new(format(MISMATCHED_DOMAINNAME, record_host, srv_host_domain))
        end

        record_matches_domain = (record_host_parts.size > srv_host_domain.size) &&
                                (srv_host_domain == record_host_parts[-srv_host_domain.size..-1])
        return if record_matches_domain

        raise Error::MismatchedDomain.new(format(MISMATCHED_DOMAINNAME, record_host, srv_host_domain))
      end
    end
  end
end
