#!/usr/bin/env python
# -*- coding: utf-8 -*-

from os import environ, chdir
import logging
import platform
import subprocess
import sys
from shutil import rmtree

from logging.handlers import TimedRotatingFileHandler
from pathlib import Path
from utils import setup_logging

if getattr(sys, "frozen", False):
    logger = setup_logging(logfile=r"C:\Circle\agent.log") 
else: 
    logger = setup_logging() 

level = environ.get('LOGLEVEL', 'INFO')
logger.setLevel(level)

system = platform.system()  # noqa
logger.debug("system:%s", system)
win = platform.system() == "Windows"

if len(sys.argv) != 1 and (system == "Linux" or system == "FreeBSD"):  # noqa
    logger.info("Installing agent on %s system", system)
    try:  # noqa
        chdir(sys.path[0])  # noqa
        subprocess.call(('pip', 'install', '-r', 'requirements/linux.txt'))  # noqa
        from utils import copy_file
        copy_file("misc/vm_renewal", "/usr/bin/vm_renewal", mode=0o755)
        if copy_file("misc/agent.service", "/etc/systemd/system/agent.service"):
            subprocess.call(('systemctl', 'enable',  'agent'))
        rmtree('win_exe', ignore_errors=True) 
    except Exception as e:  # noqa
        logger.exception("Unhandled exeption: %s", e)
        pass  # hope it works  # noqa

from twisted.internet import reactor, defer
from twisted.internet.task import LoopingCall
import uptime
from inspect import getargs, isfunction
from utils import SerialLineReceiverBase

if win: 
    from windows.winutils import getRegistryVal, get_windows_version
    level = getRegistryVal(
            r"SYSTEM\\CurrentControlSet\\Services\\CIRCLE-agent\\Parameters",
            "LogLevel",
            level
        )
    logger.setLevel(level)
#    system = get_windows_version()

from context import get_context, get_serial  # noqa

try:
    # Python 2: "unicode" is built-in
    unicode
except NameError:
    unicode = str
    
try:
    from inspect import getfullargspec as getargspec
except ImportError:
    from inspect import getargspec as getargspec


#############################################################

Context = get_context()

class SerialLineReceiver(SerialLineReceiverBase):

    def __init__(self):
        super(SerialLineReceiver, self).__init__()
        self.tickId = LoopingCall(self.tick)
        self.mayStartNowId = LoopingCall(self.mayStartNow)
        reactor.addSystemEventTrigger("before", "shutdown", self.shutdown)
        self.running = True

    def connectionMade(self):
        logger.debug("connectionMade")
        self.clearLineBuffer()
        self.tickId.start(5, now=False)
        self.mayStartNowId.start(10, now=False)       
        self.send_startMsg()
        
    def connectionLost(self, reason):
        logger.debug("connectionLost")
        if self.tickId.running:
            self.tickId.stop()
        if self.mayStartNowId.running:
            self.mayStartNowId.stop()

    def connectionLost2(self, reason):
        self.send_command(command='agent_stopped', args={})

    def mayStartNow(self):
        if Context.placed:
            self.mayStartNowId.stop()
            logger.info("Placed")
            return
        self.send_startMsg()
        
    def tick(self):
#        logger.debug("Sending tick")
        try:
            self.send_status()
        except Exception as e:
            logger.debug("Exception durig tick: %s" % e)
#           logger.exception("Twisted hide exception")

    def shutdown(self):
        self.running = False
        logger.debug("shutdown")
        self.connectionLost2('shutdown')
        d = defer.Deferred()
        reactor.callLater(0.3, d.callback, "1")
        return d

    def send_startMsg(self):
        logger.debug("Sending start message: %s %s",  Context.get_agent_version(), system)
        # Hack for flushing the lower level buffersr
        self.transport.dataBuffer = b""
        self.transport._tempDataBuffer = [] # will be added to dataBuffer in doWrite
        self.transport._tempDataLen = 0
        self.transport.write(b'\r\n')
        if self.running:
            self.send_command(
                command='agent_started',
                args={'version': Context.get_agent_version(), 'system': system})

    def send_status(self):
        import psutil
        disk_usage = dict((disk.device.replace('/', '_'),
                           psutil.disk_usage(disk.mountpoint).percent)
                          for disk in psutil.disk_partitions())
        args = {"cpu": dict(psutil.cpu_times()._asdict()),
                "ram": dict(psutil.virtual_memory()._asdict()),
                "swap": dict(psutil.swap_memory()._asdict()),
                "uptime": {"seconds": uptime.uptime()},
                "disk": disk_usage,
                "user": {"count": len(psutil.users())}}
        self.send_response(response='status', args=args)
 #       logger.debug("send_status finished")

    def _check_args(self, func, args):
 #       logger.debug("_check_args %s %s" % (func, args))
        if not isinstance(args, dict):
            raise TypeError("Arguments should be all keyword-arguments in a "
                            "dict for command %s instead of %s." %
                            (self._pretty_fun(func), type(args).__name__))

        # check for unexpected keyword arguments
        argspec = getargspec(func)
        try:
            _kwargs = argspec.keywords
        except AttributeError:
            _kwargs = argspec.varkw     
        if _kwargs is None:  # _operation doesn't take ** args
            unexpected_kwargs = set(args) - set(argspec.args)
            if unexpected_kwargs:
                raise TypeError(
                    "Command %s got unexpected keyword arguments: %s" % (
                        self._pretty_fun(func), ", ".join(unexpected_kwargs)))

        mandatory_args = argspec.args
        if argspec.defaults:  # remove those with default value
            mandatory_args = mandatory_args[0:-len(argspec.defaults)]
        missing_kwargs = set(mandatory_args) - set(args)
        if missing_kwargs:
            raise TypeError("Command %s missing arguments: %s" % (
                self._pretty_fun(func), ", ".join(missing_kwargs)))
 #       logger.debug("_check_args finished")

    def _get_command(self, command, args):
 #       logger.debug("_get_command %s" % command)
        if not isinstance(command, unicode) or command.startswith('_'):
            raise AttributeError(u'Invalid command: %s' % command)
        try:
            func = getattr(Context, command)
        except AttributeError as e:
            raise AttributeError(u'Command not found: %s (%s)' % (command, e))

        if not isfunction(func):
            raise AttributeError("Command refers to non-static method %s." %
                                 self._pretty_fun(func))

        self._check_args(func, args)
#        logger.debug("_get_command finished")
        return func

    @staticmethod
    def _pretty_fun(fun):
        try:
            argspec = getargspec(fun)
            args = argspec.args
            if argspec.varargs:
                args.append("*" + argspec.varargs)
            if argspec.keywords:
                args.append("**" + argspec.keywords)
            return "%s(%s)" % (fun.__name__, ",".join(args))
        except Exception:
            return "<%s>" % type(fun).__name__

    def handle_command(self, command, args):
        logger.debug("handle_command %s" % command)
        func = self._get_command(command, args)
#        logger.debug("Call cmd: %s" % func)
        retval = func(**args)
        logger.debug("Retval: %s" % retval)
        self.send_response(
            response=func.__name__,
            args={'retval': retval, 'uuid': args.get('uuid', None)})

    def handle_response(self, response, args):
        pass

def main():

    if Context.postUpdate():
        # Service updated, Restart needed
        return 1

    # Get proper serial class and port name
    (serial, port) = get_serial()
    logger.info("Opening port %s", port)
    # Open serial connection
    serial(SerialLineReceiver(), port, reactor)
    try:
        from notify import register_publisher
        register_publisher(reactor)
    except Exception:
        logger.exception("Could not register notify publisher")
    logger.debug("Starting reactor.")
    reactor.run()
    logger.debug("Reactor finished.")
    return Context.exit_code

if __name__ == '__main__':
    main()
