# OS parameter collection - shared helpers (SERVER-1794)
#
# OS parameter values (kernel tunables, network tunables, memory tunables ...)
# change very rarely, but querying them can spawn several external commands.
# To keep the counter task fast (it runs under a short task timeout) each OS
# specific collector refreshes an in-memory cache from a daemon background
# thread, exactly like nic_info_*.  The counter task only reads the cache.

import re
import threading
import time
import whatap.util.logging_util as logging_util

# Custom parameter names are interpolated into shell commands (sysctl / vmo /
# ndd ...), so only allow the characters that appear in legitimate tunable
# keys: letters, digits, dot, underscore, hyphen, percent (AIX e.g. minperm%),
# and colon (Solaris e.g. shmsys:shminfo_shmmax).  This blocks shell
# metacharacters and thus command injection through the custom option.
_SAFE_PARAM_RE = re.compile(r'^[A-Za-z0-9._%:-]+$')


def is_safe_param(name):
    return bool(name) and bool(_SAFE_PARAM_RE.match(name))


class OsParameter(object):
    def __init__(self, parameter, category, os_name, value, custom=False):
        self.parameter = parameter          # collected os parameter key
        self.category = category            # network / kernel / memory ... (parameter nature)
        self.os = os_name                   # os name (uname -s)
        self.value = value                  # parameter value (string)
        self.custom = custom                # flag: user-requested extra parameter


def parse_custom_params(raw):
    """Parse the ``infra_os_parameter.custom`` config value into a list.

    Format: comma separated entries, each either ``parameter`` or
    ``parameter:category``.  When category is omitted the collector auto
    detects it.  Returns a list of ``(parameter, category_or_None)`` tuples.
    """
    result = []
    if not raw:
        return result
    for entry in str(raw).split(','):
        entry = entry.strip()
        if not entry:
            continue
        if ':' in entry:
            param, category = entry.split(':', 1)
            param = param.strip()
            category = category.strip() or None
        else:
            param = entry
            category = None
        if not param:
            continue
        if not is_safe_param(param):
            logging_util.error(
                'infra_os_parameter: ignoring unsafe custom parameter name:', param)
            continue
        result.append((param, category))
    return result


class BackgroundCollector(object):
    """Runs ``collect_fn`` on a daemon thread and caches the result."""

    def __init__(self, name, collect_fn, interval=300):
        self._name = name
        self._collect_fn = collect_fn
        self._interval = interval
        self._cache = []
        self._cache_lock = threading.Lock()
        self._started = False
        self._start_lock = threading.Lock()

    def _loop(self):
        while True:
            try:
                data = self._collect_fn()
                with self._cache_lock:
                    self._cache = list(data) if data else []
            except Exception as e:
                logging_util.error('%s background error:' % self._name, e)
            time.sleep(self._interval)

    def _ensure_background(self):
        with self._start_lock:
            if self._started:
                return
            self._started = True
        t = threading.Thread(target=self._loop)
        t.daemon = True
        t.start()

    def get(self):
        self._ensure_background()
        with self._cache_lock:
            return list(self._cache)
