#!/usr/bin/env python3.4

import asyncio
import collections
import logging      # TODO: use asyncio.logger
import json
import os
import re
import subprocess
import urllib.request
import urllib.parse

# fixes windows pyodbc memory leak
if os.name == 'nt':
    import pypyodbc as pyodbc
else:
    import pyodbc

from utils import *

try:
    with open('/etc/usm/config.json') as config:
        config = json.load(config)
except FileNotFoundError:
    # use local config for development
    with open(os.path.join('..', '..', 'etc', 'config.json')):
        config = json.load(config)
        
host = config['python_server']['host']
port = config['python_server']['port']

alert_path = config['alert_handler']
checkin_url = None

database = config['database']

debug = config.get('debug', False)
if debug:
    level = logging.DEBUG
else:
    level = logging.WARNING

logging.basicConfig(level=level,
                    format='[%(asctime)s,%(filename)s,%(levelname)s] %(message)s',
                    datefmt='%Y-%m-%d %H:%M')


def valid(message):
    system, *lines, message_end = message
    sys_type, *lines_types, expected_end = LINES

    valid = (system[0] == sys_type and
             all(l[0] in lines_types for l in lines))

    return valid

def process(input):
    *_, expected_end = LINES

    messages = re.findall('(ID|.*?)\$\$\$END', input, flags=re.DOTALL)

    # allows slightly malformed message with $$$END *not* on its own line
    messages = (message.rstrip() + '\n$$$END' for message in messages)

    for message in messages:
        message = message.splitlines()
        message= [line.split('|') for line in message if line]

        if not valid(message):
            raise ValueError('Inproper format: {}'.format(message))

        system, *lines, message_end = message

        if expected_end != message_end[0]:
             lines += message_end

        lines = ((boolstr_to_int(val) for val in line) for line in lines)
        lines = ((empty_to_none(val) for val in line) for line in lines)

        yield system, lines

def update(system, lines):
    _, campus, building, room = system

    try:
        cursor.execute(INSERT_CAMPUS, (campus, campus))
    except Exception:
        pass
    campus_id, = cursor.execute(GET_CAMPUS_ID, (campus,)).fetchone()

    try:
        cursor.execute(INSERT_BUILDING, (campus_id, building, building))
    except Exception:
        pass
    cursor.execute(GET_BUILDING_ID, (campus_id, building))
    building_id, = cursor.fetchone()

    try:
        cursor.execute(INSERT_ROOM, (building_id, room, room))
    except Exception:
        cursor.execute(UPDATE_ROOM, (building_id, room))
    room_id, = cursor.execute(GET_ROOM_ID, (building_id, room)).fetchone()

    for line in lines:
        line_type, *rest = line
        line_fn = LINES[line_type]
        line_fn(room_id, rest)

    return campus_id, building_id, room_id

def update_property(room_id, property):
    device, prop, value, *ext = property

    try:
        cursor.execute(INSERT_DEVICE, (room_id, device, device))
    except Exception:
        cursor.execute(UPDATE_DEVICE, (room_id, device))
    device_id, = cursor.execute(GET_DEVICE_ID, (room_id, device)).fetchone()

    insert_prop = INSERT_PROP
    prop_args = (device_id, prop, prop, value)

    if ext:
        cols = ', '.join( EXT_PROP[:len(ext)] )
        vals = ', '.join( ['?']*len(ext) )

        insert_prop = INSERT_EXT_PROP.format(cols=cols, vals=vals)
        prop_args += tuple(ext)

    try:
        cursor.execute(insert_prop, prop_args)
        old_value = None
    except Exception:
        cursor.execute(GET_PROP_VALUE_TYPE, (device_id, prop))
        old_value, type = cursor.fetchone()

        if type=='SOURCE' or type=='SUM':
            cursor.execute(UPDATE_SOURCE_SUM, (value, device_id, prop))
        else:
            cursor.execute(UPDATE_PROP, (value, device_id, prop))

    prop_id, = cursor.execute(GET_PROP_ID, (device_id, prop)).fetchone()

    update_alert(prop_id, old_value)

    return device_id, prop_id

def update_cmdproc(room_id, cmdproc):
    path, args = cmdproc

    try:
        cursor.execute(INSERT_CMDPROC, (path, path))
    except Exception:
        pass
    cmdproc_id, = cursor.execute(GET_CMDPROC_ID, (path,)).fetchone()

    cursor.execute(UPDATE_ROOM_CMDPROC, (cmdproc_id, args, args, room_id))

    return cmdproc_id

def update_button(room_id, button):
    grp, name, cmd = button

    try:
        cursor.execute(INSERT_BUTTON, (room_id, grp, grp, name, name, cmd, cmd))
    except Exception:
        pass
    button_id, = cursor.execute(GET_BUTTON_ID, (room_id, grp, name)).fetchone()

    return button_id

LINES = collections.OrderedDict([
    ('ID', None),
    ('PROP', update_property),
    ('CP', update_cmdproc),
    ('BTN', update_button),
    ('$$$END', None)
])

def update_alert(prop_id, old_value):
    alert = cursor.execute(GET_ALERT, (prop_id,)).fetchone()
    value, alertconfig, alertvalue, old_alert, allowreset = alert

    if None not in alert and value:
        value_changed = (old_value != value)

        value = try_to_number(value)
        alertvalue = try_to_number(alertvalue)
        allowreset = bool(allowreset)

        if alertconfig in COMPARE_ALIAS:
            alertconfig = COMPARE_ALIAS[alertconfig]

        try:
            compare = COMPARE[alertconfig]
        except KeyError:
            is_alert = False
        else:
            try:
                is_alert = compare(value, alertvalue)
            except TypeError:
                is_alert = False

        if value_changed:
            if is_alert:
                cursor.execute(UPDATE_ALERT, (prop_id,))
                if alert_path is not None:
                    subprocess.Popen([alert_path, str(prop_id)])

            else:
                if allowreset:
                    cursor.execute(RESET_ALERT, (prop_id,))

def checkin(port=port, url=checkin_url, id_file='id.txt', encoding='utf-8'):
    if checkin_url is not None:
        data = {'myport': port}

        try:
            with open(id_file) as f:
                data['myid'] = f.read()
        except IOError:
            pass

        data = urllib.parse.urlencode(data).encode(encoding)
        response = urllib.request.urlopen(url, data)
        id = response.read().decode().strip()

        with open(id_file, 'w') as f:
            f.write(id)


class USMServer(asyncio.Protocol):
    def connection_made(self, transport):
        peername = transport.get_extra_info('peername')
        logging.info('connection from %s', peername)
        #transport.get_extra_info('socket').settimeout(5)

        self.transport = transport
        self.data = []

    def data_received(self, data):
        data = data.decode()
        self.data.append(data)

    def eof_received(self):
        self.data = ''.join(self.data)
        self.data = self.data.strip()

        if self.data:
            logging.info('data received: %s', self.data)

            try:
                for system, properties in process(self.data):
                    update(system, properties)

            except Exception as e:
                logging.exception(e)
                cursor.rollback()

            else:
                cursor.commit()

        self.data = None
        self.transport.close()

    def connection_lost(self, exc):
        if exc is not None:
            logging.exception(exc)
        
        self.data = None
        self.transport.close()


def main():
    conn = pyodbc.connect(**database)
    cursor = conn.cursor()

    loop = asyncio.get_event_loop()
    task = loop.create_server(USMServer, host, port)
    server = loop.run_until_complete(task)

    try:
        #checkin()
        loop.run_forever()
    except KeyboardInterrupt:
        pass
    except Exception as e:
        logging.exception(e)
    finally:
        server.close()
        loop.close()

        cursor.commit()
        cursor.close()
        
if __name__ == '__main__':
    try:
        main()
    except Exception as e:
        logging.warning(e)

