# -*- coding: utf-8 -*-
'''
Module for returning various status data about a minion.
These data can be useful for compiling into stats later,
or for problem solving if your minion is having problems.

.. versionadded:: 0.12.0

:depends:  - wmi
'''

# Import Python Libs
from __future__ import absolute_import, unicode_literals, print_function
import ctypes
import datetime
import logging
import subprocess
log = logging.getLogger(__name__)

# Import Salt Libs
import salt.utils.event
import salt.utils.platform
import salt.utils.stringutils
import salt.utils.win_pdh
from salt.utils.network import host_to_ips as _host_to_ips
from salt.utils.functools import namespaced_function as _namespaced_function

# Import 3rd party Libs
from salt.ext import six

# These imports needed for namespaced functions
# pylint: disable=W0611
from salt.modules.status import ping_master, time_
import copy
# pylint: enable=W0611

# Import 3rd Party Libs
try:
    if salt.utils.platform.is_windows():
        import wmi
        import salt.utils.winapi
        HAS_WMI = True
    else:
        HAS_WMI = False
except ImportError:
    HAS_WMI = False

HAS_PSUTIL = False
if salt.utils.platform.is_windows():
    import psutil
    HAS_PSUTIL = True

__opts__ = {}
__virtualname__ = 'status'


# Taken from https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/performance.htm
class SYSTEM_PERFORMANCE_INFORMATION(ctypes.Structure):
    _fields_ = [('IdleProcessTime', ctypes.c_int64),
                ('IoReadTransferCount', ctypes.c_int64),
                ('IoWriteTransferCount', ctypes.c_int64),
                ('IoOtherTransferCount', ctypes.c_int64),
                ('IoReadOperationCount', ctypes.c_ulong),
                ('IoWriteOperationCount', ctypes.c_ulong),
                ('IoOtherOperationCount', ctypes.c_ulong),
                ('AvailablePages', ctypes.c_ulong),
                ('CommittedPages', ctypes.c_ulong),
                ('CommitLimit', ctypes.c_ulong),
                ('PeakCommitment', ctypes.c_ulong),
                ('PageFaultCount', ctypes.c_ulong),
                ('CopyOnWriteCount', ctypes.c_ulong),
                ('TransitionCount', ctypes.c_ulong),
                ('CacheTransitionCount', ctypes.c_ulong),
                ('DemandZeroCount', ctypes.c_ulong),
                ('PageReadCount', ctypes.c_ulong),
                ('PageReadIoCount', ctypes.c_ulong),
                ('CacheReadCount', ctypes.c_ulong),  # Was c_ulong ** 2
                ('CacheIoCount', ctypes.c_ulong),
                ('DirtyPagesWriteCount', ctypes.c_ulong),
                ('DirtyWriteIoCount', ctypes.c_ulong),
                ('MappedPagesWriteCount', ctypes.c_ulong),
                ('MappedWriteIoCount', ctypes.c_ulong),
                ('PagedPoolPages', ctypes.c_ulong),
                ('NonPagedPoolPages', ctypes.c_ulong),
                ('PagedPoolAllocs', ctypes.c_ulong),
                ('PagedPoolFrees', ctypes.c_ulong),
                ('NonPagedPoolAllocs', ctypes.c_ulong),
                ('NonPagedPoolFrees', ctypes.c_ulong),
                ('FreeSystemPtes', ctypes.c_ulong),
                ('ResidentSystemCodePage', ctypes.c_ulong),
                ('TotalSystemDriverPages', ctypes.c_ulong),
                ('TotalSystemCodePages', ctypes.c_ulong),
                ('NonPagedPoolLookasideHits', ctypes.c_ulong),
                ('PagedPoolLookasideHits', ctypes.c_ulong),
                ('AvailablePagedPoolPages', ctypes.c_ulong),
                ('ResidentSystemCachePage', ctypes.c_ulong),
                ('ResidentPagedPoolPage', ctypes.c_ulong),
                ('ResidentSystemDriverPage', ctypes.c_ulong),
                ('CcFastReadNoWait', ctypes.c_ulong),
                ('CcFastReadWait', ctypes.c_ulong),
                ('CcFastReadResourceMiss', ctypes.c_ulong),
                ('CcFastReadNotPossible', ctypes.c_ulong),
                ('CcFastMdlReadNoWait', ctypes.c_ulong),
                ('CcFastMdlReadWait', ctypes.c_ulong),
                ('CcFastMdlReadResourceMiss', ctypes.c_ulong),
                ('CcFastMdlReadNotPossible', ctypes.c_ulong),
                ('CcMapDataNoWait', ctypes.c_ulong),
                ('CcMapDataWait', ctypes.c_ulong),
                ('CcMapDataNoWaitMiss', ctypes.c_ulong),
                ('CcMapDataWaitMiss', ctypes.c_ulong),
                ('CcPinMappedDataCount', ctypes.c_ulong),
                ('CcPinReadNoWait', ctypes.c_ulong),
                ('CcPinReadWait', ctypes.c_ulong),
                ('CcPinReadNoWaitMiss', ctypes.c_ulong),
                ('CcPinReadWaitMiss', ctypes.c_ulong),
                ('CcCopyReadNoWait', ctypes.c_ulong),
                ('CcCopyReadWait', ctypes.c_ulong),
                ('CcCopyReadNoWaitMiss', ctypes.c_ulong),
                ('CcCopyReadWaitMiss', ctypes.c_ulong),
                ('CcMdlReadNoWait', ctypes.c_ulong),
                ('CcMdlReadWait', ctypes.c_ulong),
                ('CcMdlReadNoWaitMiss', ctypes.c_ulong),
                ('CcMdlReadWaitMiss', ctypes.c_ulong),
                ('CcReadAheadIos', ctypes.c_ulong),
                ('CcLazyWriteIos', ctypes.c_ulong),
                ('CcLazyWritePages', ctypes.c_ulong),
                ('CcDataFlushes', ctypes.c_ulong),
                ('CcDataPages', ctypes.c_ulong),
                ('ContextSwitches', ctypes.c_ulong),
                ('FirstLevelTbFills', ctypes.c_ulong),
                ('SecondLevelTbFills', ctypes.c_ulong),
                ('SystemCalls', ctypes.c_ulong),
                # Windows 8 and above
                ('CcTotalDirtyPages', ctypes.c_ulonglong),
                ('CcDirtyPagesThreshold', ctypes.c_ulonglong),
                ('ResidentAvailablePages', ctypes.c_longlong),
                # Windows 10 and above
                ('SharedCommittedPages', ctypes.c_ulonglong)]


def __virtual__():
    '''
    Only works on Windows systems with WMI and WinAPI
    '''
    if not salt.utils.platform.is_windows():
        return False, 'win_status.py: Requires Windows'

    if not HAS_WMI:
        return False, 'win_status.py: Requires WMI and WinAPI'

    if not HAS_PSUTIL:
        return False, 'win_status.py: Requires psutil'

    # Namespace modules from `status.py`
    global ping_master, time_
    ping_master = _namespaced_function(ping_master, globals())
    time_ = _namespaced_function(time_, globals())

    return __virtualname__


__func_alias__ = {
    'time_': 'time'
}


def cpustats():
    '''
    Return information about the CPU.

    Returns
        dict: A dictionary containing information about the CPU stats

    CLI Example:

    .. code-block:: bash

        salt * status.cpustats
    '''
    # Tries to gather information similar to that returned by a Linux machine
    # Avoid using WMI as there's a lot of overhead

    # Time related info
    user, system, idle, interrupt, dpc = psutil.cpu_times()
    cpu = {'user': user,
           'system': system,
           'idle': idle,
           'irq': interrupt,
           'dpc': dpc}
    # Count related info
    ctx_switches, interrupts, soft_interrupts, sys_calls = psutil.cpu_stats()
    intr = {'irqs': {'irqs': [],
                     'total': interrupts}}
    soft_irq = {'softirqs': [],
               'total': soft_interrupts}
    return {'btime': psutil.boot_time(),
            'cpu': cpu,
            'ctxt': ctx_switches,
            'intr': intr,
            'processes': len(psutil.pids()),
            'softirq': soft_irq,
            'syscalls': sys_calls}


def meminfo():
    '''
    Return information about physical and virtual memory on the system

    Returns:
        dict: A dictionary of information about memory on the system

    CLI Example:

    .. code-block:: bash

        salt * status.meminfo
    '''
    # Get physical memory
    vm_total, vm_available, vm_percent, vm_used, vm_free = psutil.virtual_memory()
    # Get swap memory
    swp_total, swp_used, swp_free, swp_percent, _, _ = psutil.swap_memory()

    def get_unit_value(memory):
        symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
        prefix = {}
        for i, s in enumerate(symbols):
            prefix[s] = 1 << (i + 1) * 10
        for s in reversed(symbols):
            if memory >= prefix[s]:
                value = float(memory) / prefix[s]
                return {'unit': s,
                        'value': value}
        return {'unit': 'B',
                'value': memory}

    return {'VmallocTotal': get_unit_value(vm_total),
            'VmallocUsed': get_unit_value(vm_used),
            'VmallocFree': get_unit_value(vm_free),
            'VmallocAvail': get_unit_value(vm_available),
            'SwapTotal': get_unit_value(swp_total),
            'SwapUsed': get_unit_value(swp_used),
            'SwapFree': get_unit_value(swp_free)}


def vmstats():
    '''
    Return information about the virtual memory on the machine

    Returns:
        dict: A dictionary of virtual memory stats

    CLI Example:

    .. code-block:: bash

        salt * status.vmstats
    '''
    # Setup the SPI Structure
    spi = SYSTEM_PERFORMANCE_INFORMATION()
    retlen = ctypes.c_ulong()

    # 2 means to query System Performance Information and return it in a
    # SYSTEM_PERFORMANCE_INFORMATION Structure
    ctypes.windll.ntdll.NtQuerySystemInformation(
        2, ctypes.byref(spi), ctypes.sizeof(spi), ctypes.byref(retlen))

    # Return each defined field in a dict
    ret = {}
    for field in spi._fields_:
        ret.update({field[0]: getattr(spi, field[0])})

    return ret


def loadavg():
    '''
    Returns counter information related to the load of the machine

    Returns:
        dict: A dictionary of counters

    CLI Example:

    .. code-block:: bash

        salt * status.loadavg
    '''
    # Counter List (obj, instance, counter)
    counter_list = [
        ('Memory', None, 'Available Bytes'),
        ('Memory', None, 'Pages/sec'),
        ('Paging File', '*', '% Usage'),
        ('Processor', '*', '% Processor Time'),
        ('Processor', '*', 'DPCs Queued/sec'),
        ('Processor', '*', '% Privileged Time'),
        ('Processor', '*', '% User Time'),
        ('Processor', '*', '% DPC Time'),
        ('Processor', '*', '% Interrupt Time'),
        ('Server', None, 'Work Item Shortages'),
        ('Server Work Queues', '*', 'Queue Length'),
        ('System', None, 'Processor Queue Length'),
        ('System', None, 'Context Switches/sec'),
    ]
    return salt.utils.win_pdh.get_counters(counter_list=counter_list)


def cpuload():
    '''
    .. versionadded:: 2015.8.0

    Return the processor load as a percentage

    CLI Example:

    .. code-block:: bash

       salt '*' status.cpuload
    '''
    return psutil.cpu_percent()


def diskusage(human_readable=False, path=None):
    '''
    .. versionadded:: 2015.8.0

    Return the disk usage for this minion

    human_readable : False
        If ``True``, usage will be in KB/MB/GB etc.

    CLI Example:

    .. code-block:: bash

        salt '*' status.diskusage path=c:/salt
    '''
    if not path:
        path = 'c:/'

    disk_stats = psutil.disk_usage(path)

    total_val = disk_stats.total
    used_val = disk_stats.used
    free_val = disk_stats.free
    percent = disk_stats.percent

    if human_readable:
        total_val = _byte_calc(total_val)
        used_val = _byte_calc(used_val)
        free_val = _byte_calc(free_val)

    return {'total': total_val,
            'used': used_val,
            'free': free_val,
            'percent': percent}


def procs(count=False):
    '''
    Return the process data

    count : False
        If ``True``, this function will simply return the number of processes.

        .. versionadded:: 2015.8.0

    CLI Example:

    .. code-block:: bash

        salt '*' status.procs
        salt '*' status.procs count
    '''
    with salt.utils.winapi.Com():
        wmi_obj = wmi.WMI()
        processes = wmi_obj.win32_process()

    #this short circuit's the function to get a short simple proc count.
    if count:
        return len(processes)

    #a propper run of the function, creating a nonsensically long out put.
    process_info = {}
    for proc in processes:
        process_info[proc.ProcessId] = _get_process_info(proc)

    return process_info


def saltmem(human_readable=False):
    '''
    .. versionadded:: 2015.8.0

    Returns the amount of memory that salt is using

    human_readable : False
        return the value in a nicely formatted number

    CLI Example:

    .. code-block:: bash

        salt '*' status.saltmem
        salt '*' status.saltmem human_readable=True
    '''
    # psutil.Process defaults to current process (`os.getpid()`)
    p = psutil.Process()

    # Use oneshot to get a snapshot
    with p.oneshot():
        mem = p.memory_info().rss

    if human_readable:
        return _byte_calc(mem)

    return mem


def uptime(human_readable=False):
    '''
    .. versionadded:: 2015.8.0

    Return the system uptime for the machine

    Args:

        human_readable (bool):
            Return uptime in human readable format if ``True``, otherwise
            return seconds. Default is ``False``

            .. note::
                Human readable format is ``days, hours:min:sec``. Days will only
                be displayed if more than 0

    Returns:
        str:
            The uptime in seconds or human readable format depending on the
            value of ``human_readable``

    CLI Example:

    .. code-block:: bash

        salt '*' status.uptime
        salt '*' status.uptime human_readable=True
    '''
    # Get startup time
    startup_time = datetime.datetime.fromtimestamp(psutil.boot_time())

    # Subtract startup time from current time to get the uptime of the system
    uptime = datetime.datetime.now() - startup_time

    return six.text_type(uptime) if human_readable else uptime.total_seconds()


def _get_process_info(proc):
    '''
    Return  process information
    '''
    cmd = salt.utils.stringutils.to_unicode(proc.CommandLine or '')
    name = salt.utils.stringutils.to_unicode(proc.Name)
    info = dict(
        cmd=cmd,
        name=name,
        **_get_process_owner(proc)
    )
    return info


def _get_process_owner(process):
    owner = {}
    domain, error_code, user = None, None, None
    try:
        domain, error_code, user = process.GetOwner()
        owner['user'] = salt.utils.stringutils.to_unicode(user)
        owner['user_domain'] = salt.utils.stringutils.to_unicode(domain)
    except Exception as exc:  # pylint: disable=broad-except
        pass
    if not error_code and all((user, domain)):
        owner['user'] = salt.utils.stringutils.to_unicode(user)
        owner['user_domain'] = salt.utils.stringutils.to_unicode(domain)
    elif process.ProcessId in [0, 4] and error_code == 2:
        # Access Denied for System Idle Process and System
        owner['user'] = 'SYSTEM'
        owner['user_domain'] = 'NT AUTHORITY'
    else:
        log.warning('Error getting owner of process; PID=\'%s\'; Error: %s',
                    process.ProcessId, error_code)
    return owner


def _byte_calc(val):
    if val < 1024:
        tstr = six.text_type(val)+'B'
    elif val < 1038336:
        tstr = six.text_type(val/1024)+'KB'
    elif val < 1073741824:
        tstr = six.text_type(val/1038336)+'MB'
    elif val < 1099511627776:
        tstr = six.text_type(val/1073741824)+'GB'
    else:
        tstr = six.text_type(val/1099511627776)+'TB'
    return tstr


def master(master=None, connected=True):
    '''
    .. versionadded:: 2015.5.0

    Fire an event if the minion gets disconnected from its master. This
    function is meant to be run via a scheduled job from the minion. If
    master_ip is an FQDN/Hostname, is must be resolvable to a valid IPv4
    address.

    CLI Example:

    .. code-block:: bash

        salt '*' status.master
    '''

    def _win_remotes_on(port):
        '''
        Windows specific helper function.
        Returns set of ipv4 host addresses of remote established connections
        on local or remote tcp port.

        Parses output of shell 'netstat' to get connections

        PS C:> netstat -n -p TCP

        Active Connections

          Proto  Local Address          Foreign Address        State
          TCP    10.1.1.26:3389         10.1.1.1:4505          ESTABLISHED
          TCP    10.1.1.26:56862        10.1.1.10:49155        TIME_WAIT
          TCP    10.1.1.26:56868        169.254.169.254:80     CLOSE_WAIT
          TCP    127.0.0.1:49197        127.0.0.1:49198        ESTABLISHED
          TCP    127.0.0.1:49198        127.0.0.1:49197        ESTABLISHED
        '''
        remotes = set()
        try:
            data = subprocess.check_output(['netstat', '-n', '-p', 'TCP'])  # pylint: disable=minimum-python-version
        except subprocess.CalledProcessError:
            log.error('Failed netstat')
            raise

        lines = salt.utils.stringutils.to_unicode(data).split('\n')
        for line in lines:
            if 'ESTABLISHED' not in line:
                continue
            chunks = line.split()
            remote_host, remote_port = chunks[2].rsplit(':', 1)
            if int(remote_port) != port:
                continue
            remotes.add(remote_host)
        return remotes

    # the default publishing port
    port = 4505
    master_ips = None

    if master:
        master_ips = _host_to_ips(master)

    if not master_ips:
        return

    if __salt__['config.get']('publish_port') != '':
        port = int(__salt__['config.get']('publish_port'))

    master_connection_status = False
    connected_ips = _win_remotes_on(port)

    # Get connection status for master
    for master_ip in master_ips:
        if master_ip in connected_ips:
            master_connection_status = True
            break

    # Connection to master is not as expected
    if master_connection_status is not connected:
        with salt.utils.event.get_event('minion', opts=__opts__, listen=False) as event_bus:
            if master_connection_status:
                event_bus.fire_event({'master': master}, salt.minion.master_event(type='connected'))
            else:
                event_bus.fire_event({'master': master}, salt.minion.master_event(type='disconnected'))

    return master_connection_status
