""" This module provides a structure to hang the configuration settings. We
use an empty class structure and manually populate it. The global defaults
will be overlaid with any settings from the local agent configuration file.
For a specific application we will then deep copy the global default
settings and then overlay that with application settings obtained from the
server side core application. Finally, to allow for local testing and
debugging, for selected override configuration settings, we will apply back
the global defaults or those from local agent configuration.

"""

import os
import logging
import copy
import socket

# The Settings objects and the global default settings. We create a
# distinct type for each sub category of settings that the agent knows
# about so that an error when accessing a non existant setting is more
# descriptive and identifies the category of settings. When applying
# server side configuration we create normal Settings object for new
# sub categories we don't know about.

class Settings(object):
    def __repr__(self):
        return repr(self.__dict__)
    def __iter__(self):
        return iter(flatten_settings(self).items())

class ThreadProfilerSettings(Settings): pass
class TransactionTracerSettings(Settings): pass
class ErrorCollectorSettings(Settings): pass
class BrowserMonitorSettings(Settings): pass
class TransactionNameSettings(Settings): pass
class TransactionMetricsSettings(Settings): pass
class RumSettings(Settings): pass
class SlowSqlSettings(Settings): pass
class AgentLimitsSettings(Settings): pass
class ConsoleSettings(Settings): pass
class DebugSettings(Settings): pass
class CrossApplicationTracerSettings(Settings): pass
class XraySessionSettings(Settings): pass
class AnalyticsEventsSettings(Settings): pass
class AnalyticsEventsTransactionsSettings(Settings): pass
class ProcessHostSettings(Settings): pass

_settings = Settings()
_settings.thread_profiler = ThreadProfilerSettings()
_settings.xray_session = XraySessionSettings()
_settings.transaction_tracer = TransactionTracerSettings()
_settings.error_collector = ErrorCollectorSettings()
_settings.browser_monitoring = BrowserMonitorSettings()
_settings.transaction_name = TransactionNameSettings()
_settings.transaction_metrics = TransactionMetricsSettings()
_settings.rum = RumSettings()
_settings.slow_sql = SlowSqlSettings()
_settings.agent_limits = AgentLimitsSettings()
_settings.console = ConsoleSettings()
_settings.debug = DebugSettings()
_settings.cross_application_tracer = CrossApplicationTracerSettings()
_settings.analytics_events = AnalyticsEventsSettings()
_settings.analytics_events.transactions = AnalyticsEventsTransactionsSettings()
_settings.process_host = ProcessHostSettings()

_settings.log_file = os.environ.get('NEW_RELIC_LOG', None)
_settings.audit_log_file = os.environ.get('NEW_RELIC_AUDIT_LOG', None)

def _environ_as_bool(name, default=False):
    flag = os.environ.get(name, default)
    if default is None or default:
        try:
            flag = not flag.lower() in ['off', 'false', '0']
        except AttributeError:
            pass
    else:
        try:
            flag = flag.lower() in ['on', 'true', '1']
        except AttributeError:
            pass
    return flag

def _parse_ignore_status_codes(value, target):
    items = value.split()
    for item in items:
        try:
            negate = item.startswith('!')
            if negate:
                item = item[1:]

            start, end = item.split('-')

            values = set(range(int(start), int(end)+1))

            if negate:
                target.symmetric_difference_update(values)
            else:
                target.update(values)

        except ValueError:
            if negate:
                target.remove(int(item))
            else:
                target.add(int(item))
    return target

_LOG_LEVEL = {
    'CRITICAL': logging.CRITICAL,
    'ERROR': logging.ERROR,
    'WARNING': logging.WARNING,
    'INFO': logging.INFO,
    'DEBUG': logging.DEBUG,
}

_settings.enabled = _environ_as_bool('NEW_RELIC_ENABLED', False)

_settings.log_level = os.environ.get('NEW_RELIC_LOG_LEVEL', 'INFO').upper()

if _settings.log_level in _LOG_LEVEL:
    _settings.log_level = _LOG_LEVEL[_settings.log_level]
else:
    _settings.log_level = logging.INFO

_settings.license_key = os.environ.get('NEW_RELIC_LICENSE_KEY', None)
_settings.api_key = os.environ.get('NEW_RELIC_API_KEY', None)

_settings.ssl = _environ_as_bool('NEW_RELIC_SSL', True)

_settings.host = os.environ.get('NEW_RELIC_HOST', 'collector.newrelic.com')
_settings.port = int(os.environ.get('NEW_RELIC_PORT', '0'))

_settings.proxy_scheme = os.environ.get('NEW_RELIC_PROXY_SCHEME', None)
_settings.proxy_host = os.environ.get('NEW_RELIC_PROXY_HOST', None)
_settings.proxy_port = int(os.environ.get('NEW_RELIC_PROXY_PORT', '0'))
_settings.proxy_user = os.environ.get('NEW_RELIC_PROXY_USER', None)
_settings.proxy_pass = os.environ.get('NEW_RELIC_PROXY_PASS', None)

_settings.app_name = os.environ.get('NEW_RELIC_APP_NAME', 'Python Application')

_settings.process_host.display_name = None

_settings.monitor_mode = _environ_as_bool('NEW_RELIC_MONITOR_MODE', True)

_settings.high_security = _environ_as_bool('NEW_RELIC_HIGH_SECURITY', False)

_settings.collect_errors = True
_settings.collect_traces = True
_settings.collect_analytics_events = True

_settings.apdex_t = 0.5
_settings.web_transactions_apdex = {}

_settings.capture_params = False
_settings.ignored_params = []

_settings.capture_environ = True
_settings.include_environ = ['REQUEST_METHOD', 'HTTP_USER_AGENT',
                              'HTTP_REFERER', 'CONTENT_TYPE',
                              'CONTENT_LENGTH']

_settings.sampling_rate = 0

_settings.startup_timeout = float(
       os.environ.get('NEW_RELIC_STARTUP_TIMEOUT', '0.0'))
_settings.shutdown_timeout = float(
       os.environ.get('NEW_RELIC_SHUTDOWN_TIMEOUT', '2.5'))

_settings.beacon = None
_settings.error_beacon = None
_settings.application_id = None
_settings.browser_key = None
_settings.episodes_url = None
_settings.js_agent_loader = None
_settings.js_agent_file = None

_settings.url_rules = []
_settings.metric_name_rules = []
_settings.transaction_name_rules = []

_settings.cross_process_id = None
_settings.trusted_account_ids = []
_settings.encoding_key = None

_settings.thread_profiler.enabled = True
_settings.cross_application_tracer.enabled = True
_settings.xray_session.enabled = True

_settings.analytics_events.enabled = True
_settings.analytics_events.capture_attributes = True
_settings.analytics_events.max_samples_stored = 1200
_settings.analytics_events.transactions.enabled = True

_settings.transaction_tracer.enabled = True
_settings.transaction_tracer.transaction_threshold = None
_settings.transaction_tracer.record_sql = 'obfuscated'
_settings.transaction_tracer.stack_trace_threshold = 0.5
_settings.transaction_tracer.explain_enabled = True
_settings.transaction_tracer.explain_threshold = 0.5
_settings.transaction_tracer.function_trace = []
_settings.transaction_tracer.generator_trace = []
_settings.transaction_tracer.top_n = 20
_settings.transaction_tracer.capture_attributes = True

_settings.error_collector.enabled = True
_settings.error_collector.capture_source = False
_settings.error_collector.ignore_errors = []
_settings.error_collector.ignore_status_codes = _parse_ignore_status_codes(
        '100-102 200-208 226 300-308 404', set())
_settings.error_collector.capture_attributes = True

_settings.browser_monitoring.enabled = True
_settings.browser_monitoring.auto_instrument = True
_settings.browser_monitoring.loader = 'rum'  # Valid values: 'full', 'none'
_settings.browser_monitoring.loader_version = None
_settings.browser_monitoring.debug = False
_settings.browser_monitoring.ssl_for_http = None
_settings.browser_monitoring.capture_attributes = False

_settings.transaction_name.limit = None
_settings.transaction_name.naming_scheme = os.environ.get(
        'NEW_RELIC_TRANSACTION_NAMING_SCHEME')

_settings.slow_sql.enabled = True

_settings.agent_limits.data_collector_timeout = 30.0
_settings.agent_limits.transaction_traces_nodes = 2000
_settings.agent_limits.sql_query_length_maximum = 16384
_settings.agent_limits.slow_sql_stack_trace = 30
_settings.agent_limits.max_sql_connections = 4
_settings.agent_limits.sql_explain_plans = 30
_settings.agent_limits.sql_explain_plans_per_harvest = 60
_settings.agent_limits.slow_sql_data = 10
_settings.agent_limits.merge_stats_maximum = 5
_settings.agent_limits.errors_per_transaction = 5
_settings.agent_limits.errors_per_harvest = 20
_settings.agent_limits.slow_transaction_dry_harvests = 5
_settings.agent_limits.thread_profiler_nodes = 20000
_settings.agent_limits.browser_transactions = 10
_settings.agent_limits.xray_transactions = 10
_settings.agent_limits.xray_profile_overhead = 0.05
_settings.agent_limits.xray_profile_maximum = 500

_settings.console.listener_socket = None
_settings.console.allow_interpreter_cmd = False

_settings.debug.ignore_all_server_settings = False
_settings.debug.local_settings_overrides = []

_settings.debug.log_agent_initialization = False
_settings.debug.log_data_collector_calls = False
_settings.debug.log_data_collector_payloads = False
_settings.debug.log_malformed_json_data = False
_settings.debug.log_transaction_trace_payload = False
_settings.debug.log_thread_profile_payload = False
_settings.debug.log_normalization_rules = False
_settings.debug.log_raw_metric_data = False
_settings.debug.log_normalized_metric_data = False
_settings.debug.log_explain_plan_queries = False
_settings.debug.record_transaction_failure = False
_settings.debug.enable_coroutine_profiling = False
_settings.debug.explain_plan_obfuscation = 'simple'


def global_settings():
    """This returns the default global settings. Generally only used
    directly in test scripts and test harnesses or when applying global
    settings from agent configuration file. Making changes to the settings
    object returned by this function will not have any effect on any
    applications that have already been initialised. This is because when
    the settings are obtained from the core application a snapshot of these
    settings will be taken.

    >>> global_settings = global_settings()
    >>> global_settings.browser_monitoring.auto_instrument = False
    >>> global_settings.browser_monitoring.auto_instrument
    False

    """

    return _settings

def flatten_settings(settings):
    """This returns dictionary of settings flattened into a single
    key namespace rather than nested hierarchy.

    """

    def _flatten(settings, name, object):
        for key, value in object.__dict__.items():
            if isinstance(value, Settings):
                if name:
                    _flatten(settings, '%s.%s' % (name, key), value)
                else:
                    _flatten(settings, key, value)
            else:
                if name:
                    settings['%s.%s' % (name, key)] = value
                else:
                    settings[key] = value

        return settings

    return _flatten({}, None, settings)

def global_settings_dump():
    """This returns dictionary of global settings flattened into a single
    key namespace rather than nested hierarchy. This is used to send the
    global settings configuration back to core application.

    """

    settings = flatten_settings(_settings)

    # Strip out any sensitive settings as can be sent unencrypted.
    # The license key is being sent already, but no point sending
    # it again.

    del settings['license_key']
    del settings['api_key']

    del settings['proxy_user']
    del settings['proxy_pass']

    return settings

# Creation of an application settings object from global default settings
# and any server side configuration settings.

def apply_config_setting(settings_object, name, value):
    """Apply a setting to the settings object where name is a dotted path.
    If there is no pre existing settings object for a sub category then
    one will be created and added automatically.

    >>> name = 'browser_monitoring.auto_instrument'
    >>> value = False
    >>>
    >>> global_settings = global_settings()
    >>> _apply_config_setting(global_settings, name, value)

    """

    target = settings_object
    fields = name.split('.', 1)

    while len(fields) > 1:
        if not hasattr(target, fields[0]):
            setattr(target, fields[0], Settings())
        target = getattr(target, fields[0])
        fields = fields[1].split('.', 1)

    setattr(target, fields[0], value)

def fetch_config_setting(settings_object, name):
    """Fetch a setting from the settings object where name is a dotted path.

    >>> name = 'browser_monitoring.auto_instrument'
    >>>
    >>> global_settings = global_settings()
    >>> _fetch_config_setting(global_settings, name)
    'browser_monitoring.auto_instrument'

    """

    target = settings_object
    fields = name.split('.', 1)

    target = getattr(target, fields[0])

    while len(fields) > 1:
        fields = fields[1].split('.', 1)
        target = getattr(target, fields[0])

    return target

def create_settings_snapshot(server_side_config={}, settings=_settings):
    """Create a snapshot of the global default settings and overlay it
    with any server side configuration settings. Any local settings
    overrides to take precedence over server side configuration settings
    will then be reapplied to the copy. Note that the intention is that
    the resulting settings object will be cached for subsequent use
    within the application object the settings pertain to.

    >>> server_config = { 'browser_monitoring.auto_instrument': False }
    >>>
    >>> settings_snapshot = create_settings_snapshot(server_config)

    """

    settings_snapshot = copy.deepcopy(settings)

    # Break out the server side agent config settings which
    # are stored under 'agent_config' key.

    agent_config = server_side_config.pop('agent_config', {})

    # Remap as necessary any server side agent config settings.

    if 'transaction_tracer.transaction_threshold' in agent_config:
        value = agent_config['transaction_tracer.transaction_threshold']
        if value == 'apdex_f':
            agent_config['transaction_tracer.transaction_threshold'] = None

    # Overlay with global server side configuration settings.

    for (name, value) in server_side_config.items():
        apply_config_setting(settings_snapshot, name, value)

    # Overlay with agent server side configuration settings.
    # Assuming for now that agent service side configuration
    # can always take precedence over the global server side
    # configuration settings.

    for (name, value) in agent_config.items():
        apply_config_setting(settings_snapshot, name, value)

    # Reapply on top any local setting overrides.

    for name in _settings.debug.local_settings_overrides:
        value = fetch_config_setting(_settings, name)
        apply_config_setting(settings_snapshot, name, value)

    return settings_snapshot

def ignore_status_code(status):
    return status in _settings.error_collector.ignore_status_codes
