#!/usr/bin/python3
"""
@file
@ingroup trs_common_trsutils
@copyright ESO - European Southern Observatory
@author C. Soenke

@brief Show status information related to time synchronization
"""

import argparse
import datetime
import getpass
import os
import socket
import subprocess

from ptpmon import local_interface as local_if
from trslib.tools import linux_clock
from trslib.ptp import pmc
from trslib.ptp import ptp4l

SYN1588_SHM_LIB = True
try:
    from syn1588 import shm
except ImportError as e:
    #print('Failed to import syn1588 SHM library - cannot print syn1588 PTP stack info')
    SYN1588_SHM_LIB = False


class Colors:
    """ANSI escape codes for color printing"""
    # HEADER = '\033[95m'
    # OKBLUE = '\033[94m'
    # OKCYAN = '\033[96m'
    GREEN = '\033[92m'
    YELLOW = '\033[93m'
    RED = '\033[91m'
    ENDC = '\033[0m'
    # BOLD = '\033[1m'
    # UNDERLINE = '\033[4m'


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

    description = ("Show information related to time synchronization status")

    parser = argparse.ArgumentParser(description=description,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument('--allptp', help='Print all PTP info', action='store_true')
    parser.add_argument('--sync-method', help='Print global status string',
                        action='store_true', dest='sync_method')


    return parser.parse_args(args=args)


def find_ifs_from_oui(oui):
    """Find interface MAC addresses related to a certain OUI"""

    result = []
    dirs = os.listdir('/sys/class/net')
    for ifce in dirs:
        f = open('/sys/class/net/' + ifce + '/address', 'r')
        line = f.read().strip()
        f.close()
        if line.lower().startswith(oui.lower()):
            result.append((ifce, line))

    return result


def find_if_name_from_mac(mac):
    """Find interface name related to MAC address"""

    result = ''
    dirs = os.listdir('/sys/class/net')
    for ifce in dirs:
        with open('/sys/class/net/' + ifce + '/address', 'r') as f:
            line = f.read().strip()
        if line.lower() == mac.lower():
            result = ifce

    return result


def find_services(pattern):
    """Find services matching a certain pattern"""
    ret = subprocess.run(['systemctl', '-a', '--plain', '--no-legend', 'list-units', pattern],
                         capture_output=True).stdout.decode('utf-8').strip()

    ret = ret.splitlines()

    return [s.split()[0] for s in ret]


def check_service_status(service):
    """Check status of the various services."""

    enabled = subprocess.run(['systemctl', 'is-enabled', service],
                             capture_output=True).stdout.decode("utf-8").strip()
    active  = subprocess.run(['systemctl', 'is-active', service],
                             capture_output=True).stdout.decode("utf-8").strip()
    failed  = subprocess.run(['systemctl', 'is-failed', service],
                             capture_output=True).stdout.decode("utf-8").strip()

    return [enabled, active, failed]


def print_service_status(service, enabled, active, failed):
    """Check status of the various services."""

    status_str = {'active'  : Colors.GREEN + 'active' + Colors.ENDC + '   ',
                  'failed'  : Colors.RED + 'failed' + Colors.ENDC + '   ',
                  'inactive': Colors.YELLOW + 'inactive' + Colors.ENDC + ' ',
                  'enabled' : Colors.GREEN + 'enabled' + Colors.ENDC + '  ',
                  'disabled': Colors.YELLOW + 'disabled' + Colors.ENDC + ' '}

    print("  {0:25s}: {1:9s} {2:9s} {3:}".format(service,
                                                    status_str.get(enabled, enabled),
                                                    status_str.get(active, active),
                                                    status_str.get(failed, failed)))


def show_ptp4l_status(print_all=False):
    """Show PTP related status."""

    # Set options for pmc command --> query local ptp4l instance
    pmc_opt = ['-u', '-s', '/var/run/ptp4lro', '-i', '/tmp/pmc.socket']

    # Get data sets
    clock_description = pmc.parse_reply(pmc.send_message(pmc_opt, 'GET CLOCK_DESCRIPTION'))
    time_status_np = pmc.parse_reply(pmc.send_message(pmc_opt, 'GET TIME_STATUS_NP'))
    current_data_set = pmc.parse_reply(pmc.send_message(pmc_opt, 'GET CURRENT_DATA_SET'))
    default_data_set = pmc.parse_reply(pmc.send_message(pmc_opt, 'GET DEFAULT_DATA_SET'))
    port_data_set = pmc.parse_reply(pmc.send_message(pmc_opt, 'GET PORT_DATA_SET'))
    parent_data_set = pmc.parse_reply(pmc.send_message(pmc_opt, 'GET PARENT_DATA_SET'))
    time_prop_data_set = pmc.parse_reply(pmc.send_message(pmc_opt, 'GET TIME_PROPERTIES_DATA_SET'))

    # Print most useful information
    if len(default_data_set) > 0:
        print('  clockIdentity         :', default_data_set[0]['clockIdentity'])
        for i in range(len(clock_description)):
            mac = clock_description[i]['physicalAddress']
            ifname = find_if_name_from_mac(mac)
            print('  physicalAddress[{}]    : {} (if={})'.format(i, mac, ifname))
            print('  protocolAddress[{}]    : {}'.format(
                i, clock_description[i]['protocolAddress']))
        for i in range(len(port_data_set)):
            print('  portState[{}]          : {}'.format(i, port_data_set[i]['portState']))
        print('  parentPortId          :', parent_data_set[0]['parentPortIdentity'])
        print('  grandmasterId         :', parent_data_set[0]['grandmasterIdentity'])
        print('  stepsRemoved          :', current_data_set[0]['stepsRemoved'])
        print('  currentUtcOffset      :', time_prop_data_set[0]['currentUtcOffset'])
        print('  currentUtcOffsetValid :', time_prop_data_set[0]['currentUtcOffsetValid'])
        print('  ptpTimescale          :', time_prop_data_set[0]['ptpTimescale'])
        print('  timeTraceable         :', time_prop_data_set[0]['timeTraceable'])
        print('  frequencyTraceable    :', time_prop_data_set[0]['frequencyTraceable'])
        print('  offsetFromMaster      :', current_data_set[0]['offsetFromMaster'])
        print('  meanPathDelay         :', current_data_set[0]['meanPathDelay'])
        #print()

    # Print extended information
    if print_all:
        pmc.print_reply(clock_description)
        print()
        pmc.print_reply(time_status_np)
        print()
        pmc.print_reply(current_data_set)
        print()
        pmc.print_reply(default_data_set)
        print()
        pmc.print_reply(port_data_set)
        print()
        pmc.print_reply(parent_data_set)
        print()
        pmc.print_reply(time_prop_data_set)
        print()


def show_ptp4l_status2(print_all=False):
    """
    Show ptp4l related information using Python PTP mgmt. interface library
    """

    ptp4l_if = ptp4l.UdsMgmtIfce()

    # TODO: CLOCK_DESCRIPTION is currently not implemented in trslib.ptp.mgmt
    #clock_description = ptp4l_if.get('CLOCK_DESCRIPTION')
    time_status_np = ptp4l_if.get('TIME_STATUS_NP')
    current_data_set = ptp4l_if.get('CURRENT_DATA_SET')
    default_data_set = ptp4l_if.get('DEFAULT_DATA_SET')
    port_data_set = ptp4l_if.get('PORT_DATA_SET')
    parent_data_set = ptp4l_if.get('PARENT_DATA_SET')
    time_prop_data_set = ptp4l_if.get('TIME_PROPERTIES_DATA_SET')


def show_phc2sys_status():
    """
    Show status of phc2sys
    Note: only user root has access to all journal entries
    """

    if getpass.getuser() == 'root':
        print(subprocess.run(['journalctl', '-u', 'phc2sys', '-n', '5'],
                             capture_output=True).stdout.decode('utf-8').strip())
    else:
        print('  status only accessible as root')


def show_ntp_status():
    """Show NTP related status."""
    print(subprocess.run(['chronyc', 'sources'],
                         capture_output=True).stdout.decode("utf-8").strip())


def show_syn1588_status(syn1588_ifcs):
    """Show syn1588 PTP stack info based on the systemd unit"""

    # Check for 'ptp' processes
    pstat = subprocess.run(['pgrep', '-xa', 'ptp'], capture_output=True)
    if pstat.returncode != 0:
        return

    # Check if SHM library is installed
    if not SYN1588_SHM_LIB:
        print('No syn1588 SHM library found - cannot print syn1588 related info')
        return

    procs = pstat.stdout.decode('utf-8').strip().splitlines()

    for p in procs:
        print()
        pinfo = p.split()
        pid = pinfo[0]
        #cmd = pinfo[1]
        args = pinfo[2:]

        ifce = None
        if '-i' in args:
            # Get interface specified on cmd. line
            ifce = args[args.index('-i') + 1]
        else:
            # Get interface from config file
            cfgfile = args[0]

            # Read the config file
            with open(cfgfile, 'r') as f:
                lines = f.readlines()

            # Get the interface
            # TODO: this only works when there is a single interface defined in the file
            for l in lines:
                if l.startswith('interface'):
                    ifce = l.split()[1].strip()

        # Print info
        if ifce is None:
            print('No information found for process {}'.format(pid))

        print('syn1588 PTP stack (pid={}): interface={}, clk_id={}'.format(pid,
                                                                           ifce,
                                                                           syn1588_ifcs[ifce]))

        # Get info via the syn1588 SHM interface
        sm = shm.Syn1588Shm()
        sm.open(syn1588_ifcs[ifce], 1)
        status = sm.get_summary()
        print(status)
        sm.close()


def show_sync_method(services):
    """
    Generate a global status string and print to stdout
    For the time being, this just outputs NTP or PTP based on the systemd unit status
    of the well-known timesync. related services
    """

    s_str = 'N/A'

    if (services['chronyd']['active'] == 'active' and
        services['chronyd']['failed'] == 'active' and
        services['ptp4l']['active'] == 'inactive' and
        services['phc2sys']['active'] == 'inactive'):
        s_str = 'NTP'
    elif (services['ptp4l']['active'] == 'active' and
          services['ptp4l']['failed'] == 'active' and
          services['phc2sys']['active'] == 'active' and
          services['phc2sys']['failed'] == 'active' and
          services['chronyd']['active'] == 'inactive'):
        s_str = 'PTP'
    else:
        s_str = 'Inconsistent - check with trsSyncStatus'

    print(s_str)


def show_trsmon_status():
    """
    Show the ptpmon or ntpmon status using the UDS interface
    """

    uds_adrs = '\0trsmon'

    # Create socket
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
    try:
        sock.connect(uds_adrs)
    except Exception as e:
        print('  socket connection error ({}, {})'.format(
            uds_adrs.replace('\0', '\\0'), e))
        return
    sock.settimeout(5)

    # Send request
    sock.sendall(bytes([local_if.PayloadType.STATUS_REQ.value]))

    # Get the reply
    try:
        data = sock.recv(512)
    except Exception as e:
        print('  timeout in request')
        return

    msg_type, status, msg_text = local_if.decode_status_msg(data)
    if msg_type != local_if.PayloadType.STATUS_REPLY:
        print('  wrong payload type ({}) in reply'.format(msg_type))
    else:
        print('  {} - {}'.format(status, msg_text))


def main(argv=None):
    """Main function."""
    args = parse_args(argv)

    # Check status of services
    services = {}
    svc_list = ['ptp4l', 'phc2sys', 'ptpmon', 'chronyd', 'ntpmon']

    # Note for Oregano/syn1588:
    #   depending on the setup (single- or dual-port card, redundancy) there will be different
    #   systemd services and targets in use. They should all be named 'syn1588-...'
    svc_list += find_services('syn1588*')

    for s in svc_list:
        status = check_service_status(s)
        services[s] = {'enabled': status[0],
                       'active': status[1],
                       'failed': status[2]
        }

    # If '--status-string' is specified, return a status string
    if args.sync_method:
        show_sync_method(services)
        return

    # Print some general info
    print('='*80)
    print('Synchronization status of {}'.format(socket.getfqdn()))
    print('='*80)
    print()

    # Print current date
    now = datetime.datetime.now()
    print('Local time:', now)
    print('Timezone:', now.astimezone().tzname())

    # Print kernel TAI offset
    lc = linux_clock.LinuxClock()
    offset = lc.get_kernel_tai_offset()
    if offset == 0:
        print('Kernel TAI offset: ' + Colors.RED + str(offset) + Colors.ENDC)
    else:
        print('Kernel TAI offset:', offset)

    # Print the unsync status from adjtimex
    if lc.get_clock_unsynced():
        print(Colors.RED + 'Clock unsynced: TRUE' + Colors.ENDC)
    else:
        print('Clock unsynced: --')

    # Print status of services related to timesync
    print()
    print("Service status (systemd):")

    for s in svc_list:
        print_service_status(s,
                             services[s]['enabled'],
                             services[s]['active'],
                             services[s]['failed'])

    # Show processes related to timesync
    # We do explicitly search for processes by name with pgrep to find also the ones stared
    # outside of systemd
    print()
    procs_list = 'ptp4l|phc2sys|chronyd|ptpmond|ptp|lSync|redSync|ntpmond'
    procs = subprocess.run(['pgrep', '-ax', procs_list], capture_output=True)
    procs = procs.stdout.decode('utf-8').strip().splitlines()
    print('Processes:')
    for l in procs:
        print(' ', l)

    # Find conflicting clock-sync processes
    plist = []
    for p in procs:
        progname = p.split()[1].split('/')[-1]
        if progname in ['phc2sys', 'chronyd', 'lSync']:
            plist.append(progname)

    if len(plist) > 1:
        print(Colors.RED + \
              'WARNING: more than one processes potentially controlling system clock ({})'.format( \
            plist) + Colors.ENDC)

    # Find/show Oregano cards
    print()
    interfaces = find_ifs_from_oui('8C:A5:A1')
    syn1588_ifcs = {}
    print('Oregano PTP cards:')
    if not interfaces:
        print('  none')

    for ifce in interfaces:
        mac = ifce[1].replace(':', '')
        clk_id = mac[0:6] + 'fffe' + mac[6:]
        syn1588_ifcs[ifce[0]] = clk_id
        print('  Interface: {}  MAC: {}  ClkId: {}'.format(ifce[0], ifce[1], clk_id))

    print()

    # Show ptp4l related info
    pstat = subprocess.run(['pgrep', '-x', 'ptp4l'], capture_output=True)
    if pstat.returncode == 0:
        print('ptp4l status:')
        show_ptp4l_status(print_all=args.allptp)
        #show_ptp4l_status2(print_all=args.allptp)
        print()

    # Show phc2sys related info
    pstat = subprocess.run(['pgrep', '-x', 'phc2sys'], capture_output=True)
    if pstat.returncode == 0:
        print('phc2sys status (last logs from journal):')
        show_phc2sys_status()
        print()

    # Show NTP related info
    if services['chronyd']['active'] == 'active':
        print('NTP status:')
        show_ntp_status()
        print()

    # Show Oregano/syn1588 PTP stack related info
    show_syn1588_status(syn1588_ifcs)

    # Show ptpmond status
    if services['ptpmon']['active'] == 'active' or \
        services['ntpmon']['active'] == 'active':
        print('trsmon status:')
        show_trsmon_status()


if __name__ == "__main__":
    main()
