### Copyright 2023 Pixar

###
###    Licensed under the Apache License, Version 2.0 (the "Apache License")
###    with the following modification; you may not use this file except in
###    compliance with the Apache License and the following modification to it:
###    Section 6. Trademarks. is deleted and replaced with:
###
###    6. Trademarks. This License does not grant permission to use the trade
###       names, trademarks, service marks, or product names of the Licensor
###       and its affiliates, except as required to comply with Section 4(c) of
###       the License and to reproduce the content of the NOTICE file.
###
###    You may obtain a copy of the Apache License at
###
###        http://www.apache.org/licenses/LICENSE-2.0
###
###    Unless required by applicable law or agreed to in writing, software
###    distributed under the Apache License with the above modification is
###    distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
###    KIND, either express or implied. See the Apache License for the specific
###    language governing permissions and limitations under the Apache License.
###
###

###
module Jamf

  ### A class for working with pre-defined settings & preferences for the JSS Gem
  ###
  ### This is a singleton class, only one instance can exist at a time.
  ###
  ### When the JSS module loads, that instance is created, and can then be used via
  ### Jamf.config in applications to avoid always having to pass in server names, API user names
  ### and so on.
  ###
  ### When the Jamf::Configuration instance is created, the {GLOBAL_CONF} file (/etc/jss_gem.conf) is examined if it exists, and
  ### the items in it are loaded into the attributes.
  ###
  ### Then the user-specific {USER_CONF} file (~/.jss_gem.conf) is examined if it exists, and any attributes defined there will
  ### override those values from the {GLOBAL_CONF}.
  ###
  ### The file format is one attribute per line, thus:
  ###   attr_name: value
  ###
  ### Lines that don't start with a known attribute name followed by a colon are ignored. If an attribute is defined
  ### more than once, the last one wins.
  ###
  ### The known attributes are:
  ### - api_server_name [String] the hostname of the JSS API server
  ### - api_server_port [Integer] the port number for the API connection
  ### - api_ssl_version [String] the SSL version (from the open_ssl module) to use for the connection.
  ### - api_verify_cert [Boolean] if SSL is used, should the SSL certificate be verified (usually false for a self-signed cert)
  ### - api_username [String] the JSS username for connecting to the API
  ### - api_timeout_open [Integer] the number of seconds for the open-connection timeout
  ### - api_timeout [Integer] the number of seconds for the response timeout
  ###
  ### The {APIConnection#connect} method will first use any values given as method arguments. For any not given, it will
  ### look at the Preferences instance for any values there, and if still none, will use default values or raise an exception.
  ###
  ### At any point, the attributes can read or changed using standard Ruby getter/setter methods matching the name of the attribute,
  ### e.g.
  ###   Jamf.config.api_server_name  # => 'myjss.mycompany.com'
  ###   Jamf.config.api_server_name = 'otherjss.mycompany.com'  # sets the api_server_name to a new value
  ###
  ###
  ### The current settings may be saved to the GLOBAL_CONF file, the USER_CONF file, or an arbitrary file using {#save}.
  ### The argument to {#save} should be either :user, :global, or a String or Pathname file path.
  ### NOTE: This overwrites any existing file.
  ###
  ### To re-load the settings use {#reload}. This clears the current settings, and re-reads both the global and user files.
  ### If a pathname is provided, e.g.
  ###   Jamf::CONFIG.reload '/path/to/other/file'
  ### the current settings are cleared and reloaded from that other file.
  ###
  ### To view the current settings, use {#print}.
  ###
  ### @note Passwords are not saved in prefs files. Your application will have to acquire them using the :prompt or :stdin options to
  ###   {APIConnection#connect}, or by custom means.
  ###
  class Configuration

    include Singleton

    #####################################
    ### Class Constants
    #####################################

    ### The filename for storing the config, globally or user-level.
    ### The first matching file is used - the array provides
    ### backward compatibility with earlier versions.
    ### Saving will always happen to the first filename
    CONF_FILES = ['ruby-jss.conf', 'jss_gem.conf']

    ### The Pathname to the machine-wide preferences plist
    GLOBAL_CONFS = CONF_FILES.map { |cf| Pathname.new "/etc/#{cf}" }

    ### The Pathname to the user-specific preferences plist
    USER_CONFS = CONF_FILES.map { |cf| ENV['HOME'] ? Pathname.new("~/.#{cf}").expand_path : nil }.compact

    ### The attribute keys we maintain, and the type they should be stored as
    CONF_KEYS = {
      api_server_name: :to_s,
      api_server_port: :to_i,
      api_ssl_version: :to_s,
      api_verify_cert: :jss_to_bool,
      api_username: :to_s,
      api_timeout_open: :to_i,
      api_timeout: :to_i,
      db_server_name: :to_s,
      db_server_port: :to_i,
      db_server_socket: :to_s,
      db_username: :to_s,
      db_name: :to_s,
      db_connect_timeout: :to_i,
      db_read_timeout: :to_i,
      db_write_timeout: :to_i
    }

    #####################################
    ### Class Variables
    #####################################

    #####################################
    ### Class Methods
    #####################################

    #####################################
    ### Attributes
    #####################################

    # automatically create accessors for all the CONF_KEYS
    CONF_KEYS.keys.each { |k| attr_accessor k }

    #####################################
    ### Constructor
    #####################################

    ###
    ### Initialize!
    ###
    def initialize
      read_global
      read_user
    end

    #####################################
    ### Public Instance Methods
    #####################################

    ###
    ### Clear all values
    ###
    ### @return [void]
    ###
    def clear_all
      CONF_KEYS.keys.each { |k| send "#{k}=".to_sym, nil }
    end

    ###
    ### (Re)read the global prefs, if it exists.
    ###
    ### @return [void]
    ###
    def read_global
      GLOBAL_CONFS.each do |gcf|
        if gcf.file? and gcf.readable?
          read gcf
          return
        end
      end
    end

    ###
    ### (Re)read the user prefs, if it exists.
    ###
    ### @return [void]
    ###
    def read_user
      USER_CONFS.each do |ucf|
        if ucf.file? and ucf.readable?
          read ucf
          return
        end
      end
    end

    ###
    ### Clear the settings and reload the prefs files, or another file if provided
    ###
    ### @param file[String,Pathname] a non-standard prefs file to load
    ###
    ### @return [void]
    ###
    def reload(file = nil)
      clear_all
      if file
        read file
        return true
      end
      read_global
      read_user
      true
    end

    ###
    ### Save the prefs into a file
    ###
    ### @param file[Symbol,String,Pathname] either :user, :global, or an arbitrary file to save.
    ###
    ### @return [void]
    ###
    def save(file)
      path = case file
             when :global then GLOBAL_CONFS.first
             when :user then USER_CONFS.first
             else Pathname.new(file)
             end

      raise Jamf::MissingDataError, "No HOME environment variable, can't write to user conf file." if path.nil?

      # file already exists? read it in and update the values.
      if path.readable?
        data = path.read

        # go thru the known attributes/keys
        CONF_KEYS.keys.sort.each do |k|
          # if the key exists, update it.
          if data =~ /^#{k}:/
            data.sub!(/^#{k}:.*$/, "#{k}: #{send k}")

          # if not, add it to the end unless it's nil
          else
            data += "\n#{k}: #{send k}" unless send(k).nil?
          end # if data =~ /^#{k}:/
        end # each do |k|

      else # not readable, make a new file
        data = ''
        CONF_KEYS.keys.sort.each do |k|
          data << "#{k}: #{send k}\n" unless send(k).nil?
        end
      end # if path readable

      # make sure we end with a newline, the save it.
      data << "\n" unless data.end_with?("\n")
      path.jss_save data
    end # read file

    ###
    ### Print out the current settings to stdout
    ###
    ### @return [void]
    ###
    def print
      CONF_KEYS.keys.sort.each { |k| puts "#{k}: #{send k}" }
    end

    #####################################
    ### Private Instance Methods
    #####################################
    private

    ###
    ### Read in any prefs file
    ###
    ### @param file[String,Pathname] the file to read
    ###
    ### @return [void]
    ###
    def read(file)
      Pathname.new(file).read.each_line do |line|
        # skip blank lines and those starting with #
        next if line =~ /^\s*(#|$)/

        line.strip =~ /^(\w+?):\s*(\S.*)$/
        next unless Regexp.last_match(1)

        attr = Regexp.last_match(1).to_sym
        setter = "#{attr}=".to_sym
        value = Regexp.last_match(2).strip

        next unless CONF_KEYS.keys.include? attr

        if value
          # convert the value to the correct class
          value = value.send(CONF_KEYS[attr])
        end
        send(setter, value)
        # if
      end # do line
    end # read file

  end # class Preferences


end # module