import logging
import os
import platform
import re
import select
import socket
import sys

from alertaclient.api import Client

__version__ = '3.5.0'

SYSLOG_TCP_PORT = int(os.environ.get('SYSLOG_TCP_PORT', 514))
SYSLOG_UDP_PORT = int(os.environ.get('SYSLOG_UDP_PORT', 514))


SYSLOG_FACILITY_NAMES = [
    'kern',
    'user',
    'mail',
    'daemon',
    'auth',
    'syslog',
    'lpr',
    'news',
    'uucp',
    'cron',
    'authpriv',
    'ftp',
    'ntp',
    'audit',
    'alert',
    'clock',
    'local0',
    'local1',
    'local2',
    'local3',
    'local4',
    'local5',
    'local6',
    'local7'
]

SYSLOG_SEVERITY_NAMES = [
    'emerg',
    'alert',
    'crit',
    'err',
    'warning',
    'notice',
    'info',
    'debug'
]

SYSLOG_SEVERITY_MAP = {
    'emerg': 'critical',
    'alert': 'critical',
    'crit': 'major',
    'err': 'minor',
    'warning': 'warning',
    'notice': 'normal',
    'info': 'informational',
    'debug': 'debug',
}

if sys.version_info[0] >= 3:
    unicode = str


def priority_to_code(name):
    return SYSLOG_SEVERITY_MAP.get(name, 'unknown')


def decode_priority(priority):
    facility = priority >> 3
    level = priority & 7
    return SYSLOG_FACILITY_NAMES[facility], SYSLOG_SEVERITY_NAMES[level]


LOOP_EVERY = 20  # seconds

LOG = logging.getLogger('alerta.syslog')
logging.basicConfig(
    format='%(asctime)s - %(name)s: %(levelname)s - %(message)s', level=logging.DEBUG)


class SyslogDaemon:

    def __init__(self):

        self.api = Client()

        LOG.info('Starting UDP listener...')
        # Set up syslog UDP listener
        try:
            self.udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self.udp.bind(('', SYSLOG_UDP_PORT))
        except OSError as e:
            LOG.error('Syslog UDP error: %s', e)
            sys.exit(2)
        LOG.info('Listening on syslog port %s/udp' % SYSLOG_UDP_PORT)

        LOG.info('Starting TCP listener...')
        # Set up syslog TCP listener
        try:
            self.tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.tcp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            self.tcp.bind(('', SYSLOG_TCP_PORT))
            self.tcp.listen(5)
        except OSError as e:
            LOG.error('Syslog TCP error: %s', e)
            sys.exit(2)
        LOG.info('Listening on syslog port %s/tcp' % SYSLOG_TCP_PORT)

        self.shuttingdown = False

    def run(self):

        count = 0
        while not self.shuttingdown:
            try:
                LOG.debug('Waiting for syslog messages...')
                ip, op, rdy = select.select(
                    [self.udp, self.tcp], [], [], LOOP_EVERY)
                if ip:
                    for i in ip:
                        if i == self.udp:
                            data, addr = self.udp.recvfrom(4096)
                            data = unicode(data, 'utf-8', errors='ignore')
                            LOG.debug(
                                'Syslog UDP data received from %s: %s', addr, data)
                        if i == self.tcp:
                            client, addr = self.tcp.accept()
                            data = client.recv(4096)
                            data = unicode(data, 'utf-8', errors='ignore')
                            client.close()
                            LOG.debug(
                                'Syslog TCP data received from %s: %s', addr, data)

                        alerts = self.parse_syslog(ip=addr[0], data=data)
                        for alert in alerts:
                            try:
                                self.api.send_alert(**alert)
                            except Exception as e:
                                LOG.warning('Failed to send alert: %s', e)

                    count += 1
                if not ip or count % 5 == 0:
                    LOG.debug('Send heartbeat...')
                    try:
                        origin = '{}/{}'.format('syslog', platform.uname()[1])
                        self.api.heartbeat(origin, tags=[__version__])
                    except Exception as e:
                        LOG.warning('Failed to send heartbeat: %s', e)

            except (KeyboardInterrupt, SystemExit):
                self.shuttingdown = True

        LOG.info('Shutdown request received...')

    def parse_syslog(self, ip, data):

        LOG.debug('Parsing syslog message...')
        syslogAlerts = list()

        event = None
        resource = None

        for msg in data.split('\n'):
            if not msg or 'last message repeated' in msg:
                continue

            if re.match(r'<\d+>1', msg):
                # Parse RFC 5424 compliant message
                m = re.match(
                    r'<(\d+)>1 (\S+) (\S+) (\S+) (\S+) (\S+) (.*)', msg)
                if m:
                    PRI = int(m.group(1))
                    # ISOTIMESTAMP = m.group(2)
                    HOSTNAME = m.group(3)
                    APPNAME = m.group(4)
                    PROCID = m.group(5)
                    MSGID = m.group(6)
                    TAG = '{}[{}] {}'.format(APPNAME, PROCID, MSGID)
                    MSG = m.group(7)
                    LOG.info('Parsed RFC 5424 message OK')
                else:
                    LOG.error(
                        'Could not parse RFC 5424 syslog message: %s', msg)
                    continue

            elif re.match(r'<(\d{1,3})>\S{3}\s', msg):
                # Parse RFC 3164 compliant message
                m = re.match(
                    r'<(\d{1,3})>\S{3}\s{1,2}\d?\d \d{2}:\d{2}:\d{2} (\S+)( (\S+):)? (.*)', msg)
                if m:
                    PRI = int(m.group(1))
                    HOSTNAME = m.group(2)
                    TAG = m.group(4)
                    MSG = m.group(5)
                    LOG.info('Parsed RFC 3164 message OK')
                else:
                    LOG.error(
                        'Could not parse RFC 3164 syslog message: %s', msg)
                    continue

            elif re.match(r'<\d+>.*%[A-Z0-9_-]+', msg):
                # Parse Cisco Syslog message
                m = re.match(r'<(\d+)>.*(%([A-Z0-9_-]+)):? (.*)', msg)
                if m:
                    LOG.debug(m.groups())
                    PRI = int(m.group(1))
                    CISCO_SYSLOG = m.group(2)
                    try:
                        CISCO_FACILITY, CISCO_MNEMONIC = m.group(
                            3).split('-')
                    except ValueError as e:
                        LOG.error(
                            'Could not parse Cisco syslog - %s: %s', e, m.group(3))
                        CISCO_FACILITY = CISCO_MNEMONIC = 'na'

                    TAG = CISCO_MNEMONIC
                    MSG = m.group(4)

                    event = CISCO_SYSLOG

                    # replace IP address with a hostname, if necessary
                    try:
                        socket.inet_aton(ip)
                        (resource, _, _) = socket.gethostbyaddr(ip)
                    except (OSError, socket.herror):
                        resource = ip

                    resource = '{}:{}'.format(resource, CISCO_FACILITY)
                else:
                    LOG.error('Could not parse Cisco syslog message: %s', msg)
                    continue

            facility, level = decode_priority(PRI)

            # Defaults
            event = event or '{}{}'.format(
                facility.capitalize(), level.capitalize())
            resource = resource or '{}{}'.format(
                HOSTNAME, ':' + TAG if TAG else '')
            severity = priority_to_code(level)
            group = 'Syslog'
            value = level
            text = MSG
            environment = 'Production'
            service = ['Platform']
            tags = ['{}.{}'.format(facility, level)]
            correlate = ['{}{}'.format(facility.capitalize(), s.capitalize())
                         for s in SYSLOG_SEVERITY_NAMES]
            raw_data = msg

            syslogAlert = {
                'resource': resource,
                'event': event,
                'environment': environment,
                'severity': severity,
                'correlate': correlate,
                'service': service,
                'group': group,
                'value': value,
                'text': text,
                'tags': tags,
                'event_type': 'syslogAlert',
                'raw_data': raw_data
            }
            syslogAlerts.append(syslogAlert)

        return syslogAlerts


def main():

    LOG = logging.getLogger('alerta.syslog')

    try:
        SyslogDaemon().run()
    except (SystemExit, KeyboardInterrupt):
        LOG.info('Exiting alerta syslog.')
        sys.exit(0)
    except Exception as e:
        LOG.error(e, exc_info=1)
        sys.exit(1)
