#
# Copyright (C) 2006 SIPfoundry Inc.
# Licensed by SIPfoundry under the LGPL license.
# 
# Copyright (C) 2006 Pingtel Corp.
# Licensed to SIPfoundry under a Contributor Agreement.
#
##############################################################################

# Application requires.  Assume that the load path has been set up for us.
require 'configure'
require 'database_url'
require 'exceptions'
require 'sipx_logger'


class CallResolverConfigure

  # Default config file path
  DEFAULT_CONFIG_FILE = '/etc/sipxpbx/callresolver-config'
  
  # If set, then this becomes a prefix to the default config file path
  SIPX_PREFIX = 'SIPX_PREFIX'

  # If the daily run is enabled, then it happens at 4 AM, always
  DAILY_RUN_TIME = '04:00'

  LOCALHOST = 'localhost'

  # How many seconds are there in a day
  SECONDS_IN_A_DAY = 86400

  # Max integer in a Fixnum, on a 32-bit machine
  INT_MAX = 2147483647

  # Configuration parameters and defaults

  # Whether console logging is enabled or disabled.  Legal values are "ENABLE"
  # or "DISABLE".  Comparison is case-insensitive with this and other values.
  LOG_CONSOLE_CONFIG = 'SIP_CALLRESOLVER_LOG_CONSOLE'
  LOG_CONSOLE_CONFIG_DEFAULT = Configure::DISABLE
  
  # The directory holding log files.  The default value is prefixed by
  # $SIPX_PREFIX if that environment variable is defined.
  LOG_DIR_CONFIG = 'SIP_CALLRESOLVER_LOG_DIR'
  LOG_DIR_CONFIG_DEFAULT = '/var/log/sipxpbx'
  
  # Logging severity level
  LOG_LEVEL_CONFIG = 'SIP_CALLRESOLVER_LOG_LEVEL'
  LOG_LEVEL_CONFIG_DEFAULT = 'NOTICE'
  
  LOG_FILE_NAME = 'sipcallresolver.log'

  DAILY_RUN = 'SIP_CALLRESOLVER_DAILY_RUN'
  DAILY_RUN_DEFAULT = Configure::DISABLE
  
  PURGE = 'SIP_CALLRESOLVER_PURGE'
  PURGE_DEFAULT = Configure::ENABLE
  
  PURGE_AGE_CDR = 'SIP_CALLRESOLVER_PURGE_AGE_CDR'
  PURGE_AGE_CDR_DEFAULT = 35
  
  PURGE_AGE_CSE = 'SIP_CALLRESOLVER_PURGE_AGE_CSE'
  PURGE_AGE_CSE_DEFAULT = 7
  
  CSE_HOSTS = 'SIP_CALLRESOLVER_CSE_HOSTS'
  CSE_HOSTS_DEFAULT = "#{LOCALHOST}:#{DatabaseUrl::DATABASE_PORT_DEFAULT}"

  # Specify this string as the config_file to get a completely default config 
  DEFAULT_CONFIG = 'default_config'

  #-----------------------------------------------------------------------------
  # Public methods

public

  def initialize(config_file = nil)
    # If the config_file arg is nil, then find the config file in the default location
    @config_file = apply_config_file_default(config_file)

    # Set up a generic Configure object that just knows how to parse a config
    # file with param:value lines.
    if @config_file == DEFAULT_CONFIG
      @config = Configure.new()
    else
      if File.exists?(@config_file)
        @config = Configure.new(@config_file)
      else
        puts("Config file #{@config_file} not found, using default settings")
        @config = Configure.new()
      end
    end
    
    # Read logging config and initialize logging.  Do this before initializing
    # the rest of the config so we can use logging there.
    init_logging
    
    # Finish setting up the config
    finish_config
  end

  #-----------------------------------------------------------------------------
  # Public configuration

  BOOL_PARAMS = [:daily_run?, :ha?, :purge?]
  OTHER_PARAMS = [:cdr_database_url, :cse_database_urls, :config_file,
                  :daily_start_time, :daily_end_time,
                  :host_list, :host_url_list, :host_port_list,
                  :log, :log_device,
                  :purge_start_time_cdr, :purge_start_time_cse]
  ALL_PARAMS = BOOL_PARAMS + OTHER_PARAMS

  # Return true if daily runs of the call resolver are enabled, false otherwise
  def daily_run?
    @daily_run
  end

  # Return true if High Availability (HA) is enabled, false otherwise
  def ha?
    @ha
  end

  # Return true if database purging is enabled, false otherwise
  def purge?
    @purge
  end

  # Define a reader for each of the other params
  OTHER_PARAMS.each {|sym| attr_reader(sym)}

  # Access the config as an array.  Use this method *only* for plugin config
  # params that are unknown to the call resolver.  All known params should be
  # retrieved using the above accessors.
  def [](param)
    config[param]
  end

  #-----------------------------------------------------------------------------
  # Private methods

private

  attr_accessor :config
    
  # For testing purposes only.  Test code calls these methods using low-level
  # message sending.
  
  attr_writer :cse_database_urls
  
  def host_port_list=(host_port_list)
    @host_port_list = host_port_list
    
    # Recompute the CSE DB urls, since the host port list changed
    set_cse_database_urls_config(config)
  end

  #-----------------------------------------------------------------------------
  # Logging configuration
    
  # Set up logging.  Return the Logger.
  def init_logging
    @log_device = nil
    @log = nil

    # Read the logging config
    set_log_console_config(@config)
    set_log_dir_config(@config)
    set_log_level_config(@config)

    # If console logging was specified, then do that.  Otherwise log to a file.
    if @log_console
      @log_device = STDOUT
    else
      if File.exists?(@log_dir)
        log_file = File.join(@log_dir, LOG_FILE_NAME)
        @log_device = log_file
        # If the file exists, then it must be writable. If it doesn't exist,
        # then the directory must be writable.
        if File.exists?(log_file)
          if !File.writable?(log_file)
            puts("init_logging: Log file \"#{log_file}\" exists but is not writable. " +
                 "Log messages will go to the console.")
            @log_device = STDOUT
          end
        else
          if !File.writable?(@log_dir)
            puts("init_logging: Log directory \"#{@log_dir}\" is not writable. " +
                 "Log messages will go to the console.")
            @log_device = STDOUT
          end
        end
      else
        puts("Unable to open log file, log directory \"#{@log_dir}\" does not " +
             "exist.  Log messages will go to the console.")
        @log_device = STDOUT
      end
    end
    @log = SipxLogger.new(@log_device)

    # Set the log level from the configuration
    @log.level = @log_level

    # Override the log level to DEBUG if $DEBUG is set.
    # :TODO: figure out why this isn't working.
    if $DEBUG then
      @log.level = Logger::DEBUG
    end
    
    @log
  end
  
  # Set the console logging from the configuration.
  # Return the console logging boolean.
  def set_log_console_config(config)
    # Look up the config param
    @log_console = config[LOG_CONSOLE_CONFIG]
    
    # Apply the default if the param was not specified
    @log_console ||= LOG_CONSOLE_CONFIG_DEFAULT

    # Convert to a boolean
    if @log_console.casecmp(Configure::ENABLE) == 0
      @log_console = true
    elsif @log_console.casecmp(Configure::DISABLE) == 0
      @log_console = false
    else
      raise(ConfigException, "Unrecognized value \"#{@log_console}\" for " +
            "#{LOG_CONSOLE_CONFIG}.  Must be ENABLE or DISABLE.")
    end
  end
  
  # Set the log directory from the configuration.  Return the log directory.
  def set_log_dir_config(config)
    # Look up the config param
    @log_dir = config[LOG_DIR_CONFIG]
    
    # Apply the default if the param was not specified
    if !@log_dir
      @log_dir = LOG_DIR_CONFIG_DEFAULT
      
      # Prepend the prefix dir if $SIPX_PREFIX is defined
      prefix = ENV[SIPX_PREFIX]
      if prefix
        @log_dir = File.join(prefix, @log_dir)
      end      
    end
    
    @log_dir
  end
  
  # Set the log level from the configuration.  Return the log level.
  def set_log_level_config(config)
    # Look up the config param
    log_level_name = config[LOG_LEVEL_CONFIG]
    
    # Apply the default if the param was not specified
    log_level_name ||= LOG_LEVEL_CONFIG_DEFAULT
    
    # Convert the log level name to a Logger log level
    @log_level = log_level_sipx_to_logger(log_level_name)
    
    # If we don't recognize the name, then refuse to run.  Would be nice to
    # log a warning and continue, but there is no log yet!
    if !@log_level
      raise(CallResolverException, "Unknown log level: #{log_level_name}")   
    end
    
    @log_level
  end

  # Given the name of a sipX log level, return the Logger log level value, or
  # nil if the name is not recognized.
  def log_level_sipx_to_logger(name)
    SipxLogger::LOG_LEVEL_SIPX_TO_LOGGER[name]
  end

  #-----------------------------------------------------------------------------
  
  # Finish setting up the config.  Logging has already been set up before this,
  # so we can log messages in the methods that are called here.
  def finish_config
    # Read config params, applying defaults
    
    set_cdr_database_url_config(@config)
    
    # These two methods must get called in this order
    set_cse_hosts_config(@config)
    set_cse_database_urls_config(@config)
    
    set_daily_run_config(@config)
    set_daily_start_time_config(@config)
    set_purge_config(@config)
    set_purge_start_time_cdr_config(@config)
    set_purge_start_time_cse_config(@config)
  end
  
  # Given a config_file name, if it is non-nil then just return it.
  # If it's nil then return the default config file path, prepending
  # $SIPX_PREFIX if that has been set.
  def apply_config_file_default(config_file)
    if !config_file
      config_file = DEFAULT_CONFIG_FILE
      prefix = ENV[SIPX_PREFIX]
      if prefix
        config_file = File.join(prefix, config_file)
      end
    end
    
    config_file
  end

  def set_cdr_database_url_config(config)
    # :TODO: read CDR database URL params from the Call Resolver config file
    # rather than just hardwiring default values.
    @cdr_database_url = DatabaseUrl.new
  end
  
  # Return an array of CSE database URLs.  With an HA configuration, there are
  # multiple CSE databases.  Note that usually one of these URLs is identical
  # to the CDR database URL, since a standard master server runs both the
  # proxies and the call resolver, which share the SIPXCDR database.
  def set_cse_database_urls_config(config)
    @cse_database_urls = []
    if @host_port_list and (@host_port_list.size > 0)
      # Build the list of CSE DB URLs.  From Call Resolver's point of view,
      # each URL is 'localhost:<port>'.  Stunnel takes care of forwarding the
      # local port to the database on a remote host.
      @host_port_list.each do |port|
        url = DatabaseUrl.new(DatabaseUrl::DATABASE_DEFAULT, port)
        @cse_database_urls << url
      end
    else
      @cse_database_urls << cdr_database_url
    end
    
    @cse_database_urls
  end
  
  # Enable/disable the daily run from the configuration.
  # Return true if daily runs are enabled, false otherwise.
  def set_daily_run_config(config)
    # Look up the config param
    @daily_run = config[DAILY_RUN]
    
    # Apply the default if the param was not specified
    @daily_run ||= DAILY_RUN_DEFAULT

    # Convert to a boolean
    if @daily_run.casecmp(Configure::ENABLE) == 0
      @daily_run = true
    elsif @daily_run.casecmp(Configure::DISABLE) == 0
      @daily_run = false
    else
      raise(ConfigException, "Unrecognized value \"#{@daily_run}\" for " +
            "#{DAILY_RUN}.  Must be ENABLE or DISABLE.")
    end
  end

  # Compute the start time of the daily call resolver run.
  # We decided not to make this configurable.  Too complicated given that the
  # cron job always runs at a fixed time.
  def set_daily_start_time_config(config)
    # Always start the time window at the time the resolver runs
    daily_start = DAILY_RUN_TIME
    
    # Turn the start time into a date/time.
    # Get today's date, cut out the date and paste our start time into
    # a time string.
    today = Time.now
    todayString = today.strftime("%m/%d/%YT")
    startString = todayString + daily_start
    
    # Convert to time, start same time yesterday
    @daily_start_time = Time.parse(startString)
    #log.debug("set_daily_start_time_config: String #{startString}, time #{@daily_start_time}")    
    @daily_end_time = @daily_start_time
    @daily_start_time -= SECONDS_IN_A_DAY   # 24 hours
  end
  
  # Enable/disable the daily run from the configuration.
  # Return true if purging is enabled, false otherwise.
  def set_purge_config(config)
    # Look up the config param
    @purge = config[PURGE]
    
    # Apply the default if the param was not specified
    @purge ||= PURGE_DEFAULT

    # Convert to a boolean
    if @purge.casecmp(Configure::ENABLE) == 0
      @purge = true
    elsif @purge.casecmp(Configure::DISABLE) == 0
      @purge = false
    else
      raise(ConfigException, "Unrecognized value \"#{@purge}\" for " +
            "#{PURGE}.  Must be ENABLE or DISABLE.")
    end
  end  
  
  # Compute start time of CDR records to be purged from configuration
  def set_purge_start_time_cdr_config(config)
    purge_age = parse_int_param(config, PURGE_AGE_CDR, PURGE_AGE_CDR_DEFAULT, 1)

    # Get today's date
    today = Time.now
    
    # Set the start time of the purge to be purge_age days ago
    @purge_start_time_cdr = today - (SECONDS_IN_A_DAY * purge_age)
  end    
    
  # Compute start time of CSE records to be purged from configuration
  def set_purge_start_time_cse_config(config)
    purge_age = parse_int_param(config, PURGE_AGE_CSE, PURGE_AGE_CSE_DEFAULT, 1)

    # Get today's date
    today = Time.now
    
    # Set the start time of the purge to be purge_age days ago
    @purge_start_time_cse = today - (SECONDS_IN_A_DAY * purge_age)
  end

  # Get distributed CSE hosts from the configuration. Initialize @host_url_list
  # to a list of hostnames and @host_port_list to the corresponding list of
  # ports. Call resolver connects to each of these ports on 'localhost' via the
  # magic of stunnel, so it doesn't ever use the hostnames.
  def set_cse_hosts_config(config)
    @host_list = config[CSE_HOSTS]    
    @host_list ||= CSE_HOSTS_DEFAULT
    
    @host_url_list = []
    @host_port_list = []
    @ha = false
    # Split host list into separate host:port names, then build two
    # arrays of URLs and ports.
    host_array = @host_list.split(',')
    host_array.each do |host_string|
      host_elements = host_string.split(':')
      # Strip leading and trailing whitespace
      host_elements[0] = host_elements[0].strip
      # Test if port was specified      
      if host_elements.length == 1
        # Supply default port for localhost
        if host_elements[0] == LOCALHOST
          host_elements[1] = DatabaseUrl::DATABASE_PORT_DEFAULT
        else
          Utils.raise_exception(
            "No port specified for host \"#{host_elements[0]}\". " +
            "A port number for hosts other than  \"localhost\" must be specified.",
            ConfigException)
        end
      else
        # Strip whitespace from port
        host_elements[1] = host_elements[1].strip
      end
      @host_url_list << host_elements[0]
      host_port = host_elements[1].to_i
      if host_port == 0
        raise(ConfigException, "Port for #{host_elements[0]} is invalid.")
      end
      @host_port_list << host_port
      log.debug("set_cse_hosts_config: host name #{host_elements[0]}, host port: #{host_elements[1]}")
      # If at least one of the hosts != 'localhost' we are HA enabled
      if host_elements[0] != 'localhost' && ! @ha
        @ha = true
        log.debug("get_cse_host: Found host other than localhost - enable HA")
      end
    end
    @host_port_list
  end  

  # Read the named param from the config.  Convert it to an integer and return
  # the value.  If the param is not defined, then use the default.  Validate
  # that the param is between the min and max and raise a ConfigException if not.
  def parse_int_param(config, param_name, default_value = nil, min = 0, max = INT_MAX)
    param_value = default_value
    begin
      value = config[param_name]
      param_value = Integer(value) if value
      if param_value < min
        raise_config_exception(
          "The value of the configuration parameter #{param_name}, #{param_value}, "+
          "is less than the minimum value allowed, #{min}")
      elsif param_value > max
        raise_config_exception(
          "The value of the configuration parameter #{param_name}, #{param_value}, "+
          "is greater than the maximum value allowed, #{max}")
      end
    rescue ArgumentError
      raise_config_exception(
        "The value of the configuration parameter #{param_name}, #{param_value}, "+
        "is not an integer")
    end

    param_value
  end
  
  def raise_config_exception(message)
    Utils.raise_exception(message, ConfigException)
  end
  
end


syntax highlighted by Code2HTML, v. 0.9.1