#
# Copyright:: Copyright (c) Chef Software Inc.
# License:: Apache License, Version 2.0
#
# 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.
#

require "uri" unless defined?(URI)
require "chef-utils/dist" unless defined?(ChefUtils::Dist)

class Chef
  class DataCollector

    # @api private
    module ConfigValidation
      class << self
        def validate_server_url!
          # if we have a server_url set we ALWAYS validate it, and we MUST have an output_location set to skip server_url validation
          # (having output_locations set and no server_url is valid, but both of them unset blows up in here)
          return if !Chef::Config[:data_collector][:server_url] && Chef::Config[:data_collector][:output_locations]

          begin
            uri = URI(Chef::Config[:data_collector][:server_url])
          rescue
            raise Chef::Exceptions::ConfigurationError, "Chef::Config[:data_collector][:server_url] (#{Chef::Config[:data_collector][:server_url]}) is not a valid URI."
          end

          if uri.host.nil?
            raise Chef::Exceptions::ConfigurationError,
              "Chef::Config[:data_collector][:server_url] (#{Chef::Config[:data_collector][:server_url]}) is a URI with no host. Please supply a valid URL."
          end
        end

        def validate_output_locations!
          # not having an output_location set at all is fine, we just skip it then
          output_locations = Chef::Config[:data_collector][:output_locations]
          return unless output_locations

          # but deliberately setting an empty output_location we consider to be an error (XXX: but should we?)
          unless valid_hash_with_keys?(output_locations, :urls, :files)
            raise Chef::Exceptions::ConfigurationError,
              "Chef::Config[:data_collector][:output_locations] is empty. Please supply an hash of valid URLs and / or local file paths."
          end

          # loop through all the types and locations and validate each one-by-one
          output_locations.each do |type, locations|
            Array(locations).each do |location|
              validate_url!(location) if type == :urls
              validate_file!(location) if type == :files
            end
          end
        end

        # Main logic controlling the data collector being enabled or disabled:
        #
        # * disabled in why-run mode
        # * disabled when `Chef::Config[:data_collector][:mode]` excludes the solo-vs-client mode
        # * disabled if there is no server_url or no output_locations to log to
        # * enabled if there is a configured output_location even without a token
        # * disabled in solo mode if the user did not configure the auth token
        #
        # @return [Boolean] true if the data collector should be enabled
        #
        def should_be_enabled?
          running_mode = ( Chef::Config[:solo_legacy_mode] || Chef::Config[:local_mode] ) ? :solo : :client
          want_mode = Chef::Config[:data_collector][:mode]

          case
          when Chef::Config[:why_run]
            Chef::Log.trace("data collector is disabled for why run mode")
            false
          when (want_mode != :both) && running_mode != want_mode
            Chef::Log.trace("data collector is configured to only run in #{Chef::Config[:data_collector][:mode]} modes, disabling it")
            false
          when !(Chef::Config[:data_collector][:server_url] || Chef::Config[:data_collector][:output_locations])
            Chef::Log.trace("Neither data collector URL or output locations have been configured, disabling data collector")
            false
          when running_mode == :client && Chef::Config[:data_collector][:token]
            Chef::Log.warn("Data collector token authentication is not recommended for client-server mode. " \
                           "Please upgrade #{ChefUtils::Dist::Server::PRODUCT} to 12.11 or later and remove the token from your config file " \
                           "to use key based authentication instead")
            true
          when Chef::Config[:data_collector][:output_locations] && !valid_hash_with_keys?(Chef::Config[:data_collector][:output_locations], :urls)
            # we can run fine to a file without a token, even in solo mode.
            unless valid_hash_with_keys?(Chef::Config[:data_collector][:output_locations], :files)
              raise Chef::Exceptions::ConfigurationError,
                "Chef::Config[:data_collector][:output_locations] is empty. Please supply an hash of valid URLs and / or local file paths."
            end

            true
          when running_mode == :solo && !Chef::Config[:data_collector][:token]
            # we are in solo mode and are not logging to a file, so must have a token
            Chef::Log.trace("Data collector token must be configured to use #{ChefUtils::Dist::Automate::PRODUCT} data collector with #{ChefUtils::Dist::Solo::PRODUCT}")
            false
          else
            true
          end
        end

        private

        # validate an output_location file
        def validate_file!(file)
          return true if Chef::Config.path_accessible?(File.expand_path(file))

          raise Chef::Exceptions::ConfigurationError,
            "Chef::Config[:data_collector][:output_locations][:files] contains the location #{file}, which is a non existent file path."
        end

        # validate an output_location url
        def validate_url!(url)
          URI(url)
        rescue
          raise Chef::Exceptions::ConfigurationError,
            "Chef::Config[:data_collector][:output_locations][:urls] contains the url #{url} which is not valid."
        end

        # Validate the hash contains at least one of the given keys.
        #
        # @param hash [Hash] the hash to be validated.
        # @param keys [Array] an array of keys to check existence of in the hash.
        # @return [Boolean] true if the hash contains any of the given keys.
        #
        def valid_hash_with_keys?(hash, *keys)
          hash.is_a?(Hash) && keys.any? { |k| hash.key?(k) }
        end
      end
    end
  end
end
