#!/usr/bin/python3

"""
@file
@ingroup trs_common_ntpmond
@copyright ESO - European Southern Observatory
@author C. Soenke

@brief NTP monitoring daemon


"""


import argparse
#import json
import logging
import logging.handlers
import signal
import sys
import textwrap
import threading
from threading import Event
import traceback
import yaml

from ntpmon import chrony
from ptpmon import local_interface as local_if
from trslib.tools import err_condition


class NtpMon(object):
    """
    NTP monitoring class
    """

    def __init__(self, args):
        # Store command line arguments
        self.cla_config = args

        # Read the yaml config file
        with open(self.cla_config.cfgfile, 'r') as yamlcfg:
            try:
                print('Loading config file', self.cla_config.cfgfile)
                self.config = yaml.safe_load(yamlcfg)
            except yaml.YAMLError as e:
                print(e)
                raise RuntimeError('Cannot load YAML file {}: {}'.format(
                    e, self.cla_config.cfgfile))
        
        # Overwrite file-config from CLA-config
        for arg, value in self.cla_config.__dict__.items():
            if (arg in self.config) and value is not None:
                self.config[arg] = value

        # Configure logging
        self.logger = logging.getLogger('NtpMon')
        self.configure_logging()

        # Global synchronization status
        self.global_status = local_if.GlobalStatus.NTP_BAD
        self.last_global_status = local_if.GlobalStatus.NTP_BAD

        # Error checks
        self.err_checks = []
        self.init_error_checks()

        # Create an NTP status monitor
        if self.config['daemon'] == 'chrony':
            self.ntp_mon = chrony.ChronyStatusMon(
                event_callback=self.event_callback,
                mon_period=self.config['period'],
                logger=self.logger)
        else:
            raise RuntimeError('daemon type {} not supported'.format(
                self.config['daemon']
            ))

        # Create local notification object
        uds_adrs = '\0trsmon'
        if 'udsadrs_ntpmond' in self.config:
            uds_adrs = '\0' + self.config['udsadrs_ntpmond']
        self.local_if = local_if.UdsServer(self,
                                      logger=self.logger,
                                      uds_adrs=uds_adrs)


    # Event callback for chrony state changes
    def event_callback(self, status):
        """Callback for status updates from the chrony status monitor."""

        # Handle every update through a separate thread
        t = threading.Thread(target=self.handle_event, args=(status,))
        t.name = 'NtpMon_evt_callback'
        t.start()


    def configure_logging(self):
        """Configure logging."""
        # Set global loglevel
        self.logger.setLevel(self.config['loglevel'])

        # create syslog channel
        if self.config['syslog']:
            ch = logging.handlers.SysLogHandler(address='/dev/log')
            #ch.ident = 'trala'
            ch.setLevel(self.config['loglevel'])
            formatter = logging.Formatter('ntpmon - %(message)s')
            ch.setFormatter(formatter)
            self.logger.addHandler(ch)

        # create stream channel
        if self.config['conslog']:
            ch = logging.StreamHandler(sys.stdout)
            ch.setLevel(self.config['loglevel'])
            #formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
            #ch.setFormatter(formatter)
            self.logger.addHandler(ch)

        # Log to file
        if self.config['logfile'] is not None:
            ch = logging.FileHandler(self.config['logfile'])
            ch.setLevel(self.config['loglevel'])
            self.logger.addHandler(ch)


    def init_error_checks(self):
        """
        Initialize list of error checks
        """

        # Check Reference ID
        ec = err_condition.TimedErrorCondition(
            'Reference ID',
            check_type=err_condition.CheckType.NOK_LIST,
            check_cond = ['00000000', '7F7F0101', '7f7f0101'],
            grace_period=60
        )
        self.err_checks.append(
            {
                'err_cond': ec,
                'dataset': 'tracking',
                'dataset_item': 'reference_id'
            }
        )


    def handle_event(self, status):
        """Event handler"""

        self.logger.debug('NtpMon::handle_event()')

        #print(json.dumps(status, indent=4))

        # Check for errors and notify local client
        try:
            #daemon_state_change = False
            is_recovered = False
            is_new_error = False
            notify_reason = ''
            is_error = False

            # Check if daemon status changed
            if status['daemon_error'] != status['last_daemon_error']:
                #daemon_state_change = True
                if status['daemon_error']:
                    notify_reason = 'NTP daemon error'
                else:
                    notify_reason = 'NTP daemon OK'

            # Check error conditions on NTP daemon
            if not status['daemon_error']:
                for err_cond in self.err_checks:
                    self.logger.debug('Checking error condition "{}"'.format(
                        err_cond['err_cond'].name))
                    value = getattr(status[err_cond['dataset']], err_cond['dataset_item'])
                    #print('Value:', value)
                    active, recovered, new_error, reason = err_cond['err_cond'].check(
                        value)
                    if new_error:
                        self.logger.warning('WARNING: {}'.format(reason))
                    if recovered:
                        self.logger.info(reason)
                    if new_error or recovered:
                        notify_reason = reason
                    is_new_error = is_new_error or new_error
                    is_recovered = is_recovered or recovered
                    is_error = is_error or active
                
            # Set global state flag
            if status['daemon_error'] or is_error:
                self.global_status = local_if.GlobalStatus.NTP_BAD
            else:
                self.global_status = local_if.GlobalStatus.NTP_GOOD

            # Check if global status changed and store state
            global_status_changed = self.global_status != self.last_global_status
            self.last_global_status = self.global_status

            self.logger.debug('Global status: {} (d_err={} recov={} new_e={} e={})'.format(
                self.global_status,
                status['daemon_error'],
                is_recovered,
                is_new_error,
                is_error))
            
            # Notify clients if required
            # Note: we only notify when the global state changes
            if global_status_changed:
                self.logger.info('Global status changed {} - {}'.format(
                    self.global_status, notify_reason))
                self.local_if.notify(self.global_status, notify_reason)

        except Exception as e:
            self.logger.error('error during error check or local notification')
            self.logger.error(e)
            self.logger.debug(traceback.format_exc())


    def start(self):
        """Start monitor."""
        self.logger.debug('NtplMon::start()')
        self.ntp_mon.start()
        self.local_if.start()


    def stop(self):
        """Stop monitor."""
        self.logger.debug('NtpMon::stop()')
        self.ntp_mon.stop()
        self.local_if.stop()


# Stop event
stop = Event()


# Signal handler
def sig_handler(signum, frame):
    """Signal handler."""
    print('ntpmond - received signal', signum)
    stop.set()


def parse_args():
    """
    @brief Parse command line arguments
    """

    description = textwrap.dedent('''\
            Monitoring daemon for NTP daemons

            Command line arguments overwrite config from file.
            ''')

    # Note: args shall NOT have a default - the defaults come from the config file.
    #       If an argument is not specified, the parser will set it to None - this
    #       will be used to determine if it is present and if so, will overwrite the
    #       config from file
    parser = argparse.ArgumentParser(description=description,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)

    parser.add_argument('cfgfile', help='Configuration file in yaml format',
                        action='store', default='/elt/trs/resource/ntpmond.yaml')

    parser.add_argument('--period', help='Monitor period', action='store',
                        dest='period', type=float)
    parser.add_argument('--loglevel', help='Log level [ERROR|WARNING|INFO|DEBUG]', action='store',
                        dest='loglevel')
    parser.add_argument('--conslog', help='Log to stdout', action='store_const',
                        const=True, default=None)
    parser.add_argument('--syslog', help='Log to syslog', action='store_const',
                        const=True, default=None)
    parser.add_argument('--logfile', help='Log to the specified file name', action='store',
                        dest='logfile')

    return parser.parse_args()


def main():
    """Main."""

    # Parse command line arguments
    args = parse_args()

    # Register the signal handler
    signal.signal(signal.SIGINT, sig_handler)
    signal.signal(signal.SIGTERM, sig_handler)

    # Create monitor and start
    try:
        monitor = NtpMon(args)
        monitor.start()
    except Exception as e:
        print('ERROR creating/starting class NtpMon')
        print(e)
        print(traceback.format_exc())

    # Wait for signals and stop the monitor
    stop.wait()
    monitor.stop()


if __name__ == "__main__":
    main()
