"""Gather metrics on specific processes.

"""
from collections import defaultdict
import monasca_agent.collector.checks as checks


class ProcessCheck(checks.AgentCheck):
    PROCESS_GAUGE = ('process.thread_count',
                     'process.cpu_perc',
                     'process.mem.rss_mbytes',
                     'process.mem.real_mbytes',
                     'process.open_file_descriptors',
                     'process.io.read_count',
                     'process.io.write_count',
                     'process.io.read_kbytes',
                     'process.io.write_kbytes',
                     'process.voluntary_ctx_switches',
                     'process.involuntary_ctx_switches')

    def __init__(self, name, init_config, agent_config, instances=None):
        super(ProcessCheck, self).__init__(name, init_config, agent_config,
                                           instances)
        self._cached_processes = defaultdict(dict)
        self._current_process_list = None

    @staticmethod
    def is_psutil_version_later_than(v):
        try:
            import psutil
            vers = psutil.version_info
            return vers >= v
        except Exception:
            return False

    def find_pids(self, search_string, psutil, exact_match=True):
        """Create a set of pids of selected processes.

        Search for search_string
        """
        found_process_list = []
        for proc in self._current_process_list:
            found = False
            for string in search_string:
                try:
                    if exact_match:
                        if proc.name() == string:
                            found = True
                    else:
                        cmdline = proc.cmdline()

                        if string in ' '.join(cmdline):
                            found = True

                    if found or string == 'All':
                        found_process_list.append(proc.pid)
                except psutil.NoSuchProcess:
                    # No way to log useful information here so just move on
                    pass
                except psutil.AccessDenied as e:
                    self.log.error('Access denied to %s process' % string)
                    self.log.error('Error: %s' % e)
                    raise

        return set(found_process_list)

    def get_process_metrics(self, pids, psutil, name):
        processes_to_remove = set(self._cached_processes[name].keys()) - pids
        for pid in processes_to_remove:
            del self._cached_processes[name][pid]
        got_denied = False
        io_permission = True

        # initialize aggregation values
        total_thr = 0
        total_cpu = None
        total_rss = 0
        total_real = 0
        total_open_file_descriptors = 0
        total_read_count = 0
        total_write_count = 0
        total_read_kbytes = 0
        total_write_kbytes = 0
        total_voluntary_ctx_switches = 0
        total_involuntary_ctx_switches = 0

        for pid in set(pids):
            try:
                added_process = False
                if pid not in self._cached_processes[name]:
                    p = psutil.Process(pid)
                    self._cached_processes[name][pid] = p
                    added_process = True
                else:
                    p = self._cached_processes[name][pid]

                mem = p.memory_info_ex()
                total_real += float((mem.rss - mem.shared) / 1048576)
                total_rss += float(mem.rss / 1048576)
                total_thr += p.num_threads()

                try:
                    ctx_switches = p.num_ctx_switches()
                    total_voluntary_ctx_switches += ctx_switches.voluntary
                    total_involuntary_ctx_switches += ctx_switches.involuntary
                except NotImplementedError:
                    # Handle old Kernels which don't provide this info.
                    total_voluntary_ctx_switches = None
                    total_involuntary_ctx_switches = None

                try:
                    total_open_file_descriptors += float(p.num_fds())
                except psutil.AccessDenied:
                    got_denied = True

                if not added_process:
                    cpu = p.cpu_percent(interval=None)
                    if not total_cpu:
                        total_cpu = cpu
                    else:
                        total_cpu += cpu
                else:
                    p.cpu_percent(interval=None)

                # user might not have permission to call io_counters()
                if io_permission:
                    try:
                        io_counters = p.io_counters()
                        total_read_count += io_counters.read_count
                        total_write_count += io_counters.write_count
                        total_read_kbytes += float(io_counters.read_bytes / 1024)
                        total_write_kbytes += float(io_counters.write_bytes / 1024)
                    except psutil.AccessDenied:
                        self.log.debug('monasca-agent user does not have ' +
                                       'access to I/O counters for process' +
                                       ' %d: %s'
                                       % (pid, p.name))
                        io_permission = False
                        total_read_count = None
                        total_write_count = None
                        total_read_kbytes = None
                        total_write_kbytes = None

            # Skip processes dead in the meantime
            except psutil.NoSuchProcess:
                self.warning('Process %s disappeared while metrics were being collected' % pid)
                pass

        if got_denied:
            self.warning("The Monitoring Agent was denied access " +
                         "when trying to get the number of file descriptors")

        return dict(zip(ProcessCheck.PROCESS_GAUGE,
                        (total_thr, total_cpu, total_rss, total_real, total_open_file_descriptors,
                         total_read_count, total_write_count, total_read_kbytes, total_write_kbytes,
                         total_voluntary_ctx_switches, total_involuntary_ctx_switches)))

    def prepare_run(self):
        """Collect the list of processes once before each run"""
        try:
            import psutil
        except ImportError:
            raise Exception('You need the "psutil" package to run this check')

        self._current_process_list = [process for process in psutil.process_iter()]

    def check(self, instance):
        try:
            import psutil
        except ImportError:
            raise Exception('You need the "psutil" package to run this check')

        name = instance.get('name', None)
        exact_match = instance.get('exact_match', True)
        search_string = instance.get('search_string', None)

        if name is None:
            raise KeyError('The "name" of process groups is mandatory')

        if search_string is None:
            raise KeyError('The "search_string" is mandatory')

        pids = self.find_pids(search_string, psutil, exact_match=exact_match)
        dimensions = self._set_dimensions({'process_name': name}, instance)

        self.log.debug('ProcessCheck: process %s analysed' % name)

        self.gauge('process.pid_count', len(pids), dimensions=dimensions)

        if instance.get('detailed', False):
            metrics = self.get_process_metrics(pids, psutil, name)
            for metric_name, metric_value in metrics.iteritems():
                if metric_value is not None:
                    self.gauge(metric_name, metric_value, dimensions=dimensions)
