#!/usr/bin/python
"""net_monitor: wifi monitoring"""

import os
import os.path
import socket
import sys
import fcntl
import struct
import traceback
import array
import time
import json
from subprocess import Popen, PIPE

# native library implements a few bits
from . import _native

# localization
import gettext
try:
    gettext.install("net_monitor")
except IOError:
    _ = str


class Monitor:
    # based on http://svn.pardus.org.tr/pardus/tags/pardus-1.0/system/base/wireless-tools/comar/link.py

    # network uptime log file
    LOGFILE="/var/log/net_monitor.log"

    # wireless IOCTL constants
    SIOCGIWMODE = 0x8B07    # get operation mode
    SIOCGIWRATE = 0x8B21    # get default bit rate
    SIOCGIWESSID = 0x8B1B   # get essid

    # wireless modes
    modes = ['Auto', 'Ad-Hoc', 'Managed', 'Master', 'Repeat', 'Second', 'Monitor']

    # tcp statuses
    netstats = {"tcp": [
                "",
                _("ESTABLISHED"),
                _("SYN_SENT"),
                _("SYN_RECV"),
                _("FIN_WAIT1"),
                _("FIN_WAIT2"),
                _("TIME_WAIT"),
                _("CLOSE"),
                _("CLOSE_WAIT"),
                _("LAST_ACK"),
                _("LISTEN"),
                _("CLOSING")
                ],
            "tcp6": [
                "",
                _("ESTABLISHED"),
                _("SYN_SENT"),
                _("SYN_RECV"),
                _("FIN_WAIT1"),
                _("FIN_WAIT2"),
                _("TIME_WAIT"),
                _("CLOSE"),
                _("CLOSE_WAIT"),
                _("LAST_ACK"),
                _("LISTEN"),
                _("CLOSING")
                ],
            "udp": [
                "",
                _("ESTABLISHED"),
                _("SYN_SENT"),
                _("SYN_RECV"),
                _("FIN_WAIT1"),
                _("FIN_WAIT2"),
                _("TIME_WAIT"),
                "",
                _("CLOSE_WAIT"),
                _("LAST_ACK"),
                _("LISTEN"),
                _("CLOSING")
                ],
            "udp6": [
                "",
                _("ESTABLISHED"),
                _("SYN_SENT"),
                _("SYN_RECV"),
                _("FIN_WAIT1"),
                _("FIN_WAIT2"),
                _("TIME_WAIT"),
                "",
                _("CLOSE_WAIT"),
                _("LAST_ACK"),
                _("LISTEN"),
                _("CLOSING")
                ],
            }

    # constants
    SIZE_KB=1000
    SIZE_MB=1000**2
    SIZE_GB=1000**3

    def __init__(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.net = {}
        self.uptime_log = {}

    def ioctl(self, func, params):
        return fcntl.ioctl(self.sock.fileno(), func, params)

    def wifi_ioctl(self, iface, func, arg=None):
        """Prepares some variables for wifi and runs ioctl"""
        if not arg:
            data = (iface.encode() + b'\0' * 32)[:32]
        else:
            data = (iface.encode() + b'\0' * 16)[:16] + arg
        try:
            res = self.ioctl(func, data)
            return res
        except:
            return None

    def wifi_get_max_quality(self, iface):
        """Gets maximum quality value"""
        try:
            ret = _native.wifi_get_max_quality(iface)
            return ret
        except:
            # TODO: this happens when the card does not supports the settings
            # but maybe we could log it somewhere..
            return _("Unknown")

    def wifi_get_ap(self, iface):
        """Gets access point address"""
        try:
            ret = _native.wifi_get_ap(iface)
            return ret
        except:
            # TODO: this happens when the card does not supports the settings
            # but maybe we could log it somewhere..
            return _("Unknown")

    def wifi_get_essid(self, iface):
        """Get current essid for an interface"""
        buffer = array.array('b', b'\0' * 16)
        addr, length = buffer.buffer_info()
        arg = struct.pack('Pi', addr, length)
        self.wifi_ioctl(iface, self.SIOCGIWESSID, arg)
        return buffer.tobytes().strip(b'\0').decode('utf-8')

    def wifi_get_mode(self, iface):
        """Get current mode from an interface"""
        result = self.wifi_ioctl(iface, self.SIOCGIWMODE)
        if not result:
            return _("Unknown")
        mode = struct.unpack("i", result[16:20])[0]
        return self.modes[mode]

    def wifi_get_bitrate(self, iface):
        """Gets current operating rate from an interface"""
        # Note: KILO is not 2^10 in wireless tools world

        result = self.wifi_ioctl(iface, self.SIOCGIWRATE)

        if result:
            size = struct.calcsize('ihbb')
            m, e, i, pad = struct.unpack('ihbb', result[16:16+size])
            if e == 0:
                bitrate =  m
            else:
                bitrate = float(m) * 10**e
            return bitrate
        else:
            return 0

    def get_status(self, ifname):
        """Determines interface status"""
        try:
            with open("/sys/class/net/%s/operstate" % ifname) as fd:
                status = fd.readline().strip()
        except:
            status="unknown"
        if status == "unknown":
            # pretty-format interface status
            status = _("Unknown")
        return status

    def wireless_stats(self):
        """Check if device is wireless and get its details if necessary"""
        try:
            stats = {}
            if not os.path.isfile("/proc/net/wireless"):
                return stats
            with open("/proc/net/wireless") as fd:
                ifaces = fd.readlines()[2:]
            for line in ifaces:
                line = line.strip()
                if not line:
                    continue
                iface, params = line.split(":", 1)
                iface = iface.strip()
                params = params.replace(".", "").split()
                link = int(params[1])
                stats[iface] = link
            return stats
        except:
            # something bad happened
            traceback.print_exc()
            return {}

    def has_wireless(self, iface):
        """Checks if device has wireless capabilities"""
        return os.access("/sys/class/net/%s/wireless" % iface, os.R_OK)

    def get_address(self, ifname):
        """Get MAC address of a card"""
        mac=_("No physical address")
        # ip address
        try:
            res = self.ioctl(0x8915, struct.pack('256s',bytes(ifname[:15], 'utf-8')))[20:24]
            addr=socket.inet_ntoa(res)
        except:
            addr=_("No address assigned")
        # mac address
        try:
            mac_struct=self.ioctl(0x8927, struct.pack('256s', bytes(ifname[:15], 'utf-8')))[18:24]
            mac=":".join(["%02x" % ord(chr(char)) for char in mac_struct])
        except:
            pass
        # addr, mac
        return addr, mac

    def readnet(self):
        """Reads values from /proc/net/dev"""
        net = {}
        try:
            with open("/proc/net/dev") as fd:
                data = fd.readlines()[2:]
            for l in data:
                dev, vals = l.split(":")
                dev = dev.strip()
                vals = vals.split()
                net[dev] = vals
        except:
            traceback.print_exc()
        return net

    def has_network_accounting(self, iface):
        """Checks if network accounting was enabled on interface"""
        try:         
            pr = Popen(("vnstat --json -i %s" % (iface)).split(), stdout=PIPE)
            data, _ = pr.communicate()
            data = data.decode('utf-8')
            if data[0] == "{":
                data_tab = json.loads(data)
                if data_tab['interfaces'][0]['name'] == iface:
                    return True
        except:
            return False
        return False

    def get_traffic(self, iface, net=None):
        """Get traffic information"""
        device_exists=False
        if not net:
            if not self.net:
                self.readnet()
            net = self.net
        if iface in net:
            device_exists=True
            bytes_in = int(net[iface][0])
            bytes_out = int(net[iface][8])
        else:
            bytes_in = 0
            bytes_out = 0
        return device_exists, bytes_in, bytes_out

    def format_size(self, size, opt=""):
        """Pretty-Formats size"""
        # convert to float
        size_f = size * 1.0
        pretty_size = None
        pretty_bytes = _("%d Bytes%s") % (size, opt)
        if size > self.SIZE_GB:
            pretty_size = "%3.3f GB%s" % (size_f / self.SIZE_GB, opt)
        elif size > self.SIZE_MB:
            pretty_size = "%3.3f MB%s" % (size_f / self.SIZE_MB, opt)
        elif size > self.SIZE_KB:
            pretty_size = "%3.3f KB%s" % (size_f / self.SIZE_KB, opt)
        else:
            pretty_size = pretty_bytes
        return pretty_size, pretty_bytes

    def get_dns(self):
        """Returns list of DNS servers"""
        servers = []
        try:
            with open("/etc/resolv.conf") as fd:
                data = fd.readlines()
            for l in data:
                l = l.strip()
                if not l:
                    continue
                fields = l.split()
                if fields[0] == 'nameserver':
                    servers.append(fields[1])
        except:
            traceback.print_exc()
        return servers

    def get_routes(self):
        """Read network routes"""
        routes = []
        default_routes = []
        try:
            with open("/proc/net/route") as fd:
                data = fd.readlines()[1:]
            for l in data:
                l = l.strip()
                if not l:
                    continue
                params = l.split()
                iface = params[0]
                dst = int(params[1], 16)
                gw = int(params[2], 16)
                gw_str = socket.inet_ntoa(struct.pack("I", gw))
                metric = int(params[6], 16)
                mask = int(params[7], 16)
                routes.append((iface, dst, mask, gw, metric))
                if dst == 0 and mask == 0:
                    default_routes.append((gw_str, iface))
        except:
            traceback.print_exc()
            pass
        return routes, default_routes

    def protocols_list(self):
        protos = []
        for proto in ["tcp", "tcp6", "udp", "udp6"]:
            if os.path.exists("/proc/net/%s" % proto):
                protos.append(proto)
        return protos

    def get_connections(self, proto="tcp"):
        """Reads active connections"""
        connections=[]
        try:
            with open("/proc/net/%s" % proto) as fd:
                data = fd.readlines()[1:]
        except:
            # unable to read connections
            traceback.print_exc()
            return connections

        if proto in self.netstats:
            netstats = self.netstats[proto]
        else:
            netstats = None
        # parse connections
        for l in data:
            fields = l.strip().split()
            loc=fields[1]
            rem=fields[2]
            status=fields[3]
            loc_a,loc_p = loc.split(":")
            rem_a,rem_p = rem.split(":")
            if ( proto.find('6') != -1 ):
                # the bytes must be reversed as IPV6 is four 32 bits blocks
                loc_a = loc_a[24:32] + loc_a[16:24] + loc_a[8:16] + loc_a[0:8]
                loc_a = int(loc_a, 16)
                rem_a = rem_a[24:32] + rem_a[16:24] + rem_a[8:16] + rem_a[0:8]
                rem_a = int(rem_a, 16)
                loc_addr = socket.inet_ntop(socket.AF_INET6, loc_a.to_bytes(16, sys.byteorder))
                rem_addr = socket.inet_ntop(socket.AF_INET6, rem_a.to_bytes(16, sys.byteorder))
            else:
                loc_addr = socket.inet_ntop(socket.AF_INET, struct.pack('I', int(loc_a, 16)))
                rem_addr = socket.inet_ntop(socket.AF_INET, struct.pack('I', int(rem_a, 16)))
            loc_port = (int(loc_p, 16))
            rem_port = (int(rem_p, 16))
            # parse status
            status = int(status, 16)
            status_s = _("Unknown")
            if netstats:
                if status < len(netstats):
                    status_s = netstats[status]
            connections.append((loc_addr, loc_port, rem_addr, rem_port, status_s))
        return connections

    def load_uptime_log(self):
        """Loads network uptime log, handled by /etc/sysconfig/network-scripts/if{up,down}.d/netprofile*"""
        self.uptime_log = {}
        if not os.access(self.LOGFILE, os.R_OK):
            # no log file
            return
        with open(self.LOGFILE) as fd:
            data = fd.readlines()

        for l in data:
            dev, status, secs = l.strip().split(":")
            secs = int(secs)
            if dev not in self.uptime_log:
                self.uptime_log[dev] = {"uptime": None, "log": []}
            self.uptime_log[dev]["log"].append((secs, status))

        # now reload the last uptime data
        for i in self.uptime_log:
            self.calc_uptime(i)

    def calc_uptime(self, iface):
        """Calculates uptime data for an interface"""
        if iface not in self.uptime_log:
            self.uptime_log[iface]["uptime"] = -1
            return
        # ok, interface is there, calculate last uptime status
        last_up=0
        last_down=0
        for s, status in self.uptime_log[iface]["log"]:
            if status == "UP":
                last_up = s
            elif status == "DOWN":
                last_down = s

        # now get the uptime
        # is the device up and running?
        if not last_up:
            self.uptime_log[iface]["uptime"] = -1
            return

        # was the interface disconnected?
        if last_down > last_up:
            self.uptime_log[iface]["uptime"] = 0
            return

        # ok, we are up and running, lets get the uptime
        self.uptime_log[iface]["uptime"] = last_up

    def get_uptime(self, iface):
        """Determines interface uptime"""
        if iface not in self.uptime_log:
            return _("Unknown")
        uptime = self.uptime_log[iface]["uptime"]
        if uptime < 0:
            return _("Unknown")
        elif uptime == 0:
            # device is offline
            return _("Device is offline")
        else:
            curtime = int(time.time())
            uptime = curtime - uptime
            hours = uptime // 3600
            mins = (uptime - (hours * 3600)) // 60
            secs = uptime % 60
            return _("%(hours)d hours, %(mins)d minutes, %(secs)d seconds") % {"hours" : hours, "mins" : mins, "secs" : secs}
