#!/usr/bin/env python

# Copyright (C) 2019 Cumulus Networks, Inc. all rights reserved
#
# This software is subject to the Cumulus Networks End User License Agreement available
# at the following locations:
#
# Internet: https://cumulusnetworks.com/downloads/eula/latest/view/
# Cumulus Linux systems: /usr/share/cumulus/EULA.txt

"""
Usage:
  l1-show PORTLIST [--json] [--debug MODULES]


Options:
  PORTLIST                  Port to run against e.g swp1,swp2  or all
  -j, --json                Output data to screen in json format
  -v, --debug MODULES       Enable debugging on the list of MODULES. Specify 'main' to debug the testrunner module itself.
  -h, --help                Show this screen

"""
import sys
import logging
import json
from pprint import pformat

from docopt import docopt
from cl_parsers.parse import parse

from l1_show_lib import *
from module_qualify.parse_port_module_data import parse_bcm_eye

arguments = docopt(__doc__, version='l1-show 0.1')


setup_logging(default_level=logging.ERROR, quiet=True, logformat='%(levelname)s: %(message)s')
this_log = logging.getLogger(__name__)
debug = this_log.debug
info = this_log.info
warn = this_log.warning
error = this_log.error

def critical(mesg):
    this_log.critical(mesg)
    this_log.critical('Exiting abnormally')
    sys.exit(1)

compliance_conversion = {
    '100g-sr4': ['25g-sr'],
    '100g-lr4': ['25g-lr'],
    '100g-er4': ['25g-er'],
    '100g-active-cu': ['25g-active-cu'],
    '100g-cr4': ['25g-cr', '50g-cr2'],
    '100g-active': ['25g-active']
}


def check_for_override(dut, port, vendor=None, oui=None, pn=None):
    override = None
    if dut.asic_vendor == 'bcm':
        name_pn = str(vendor) + ',' + str(pn)
        oui_pn = str(oui) + ',' + str(pn)
        if name_pn in dut.cable_override:
            override = dut.cable_override[name_pn][0] + '(override)'
        elif oui_pn in dut.cable_override:
            override = dut.cable_override[oui_pn][0] + '(override)'

        if port in dut.port_override:
            override = dut.port_override[port][0] + '(override)'

    return override


def convert_kern_speed(kern_speed):
    return kern_speed.replace('000Mb/s', 'G')


def determine_hw_speed(dut, port):
    if dut.asic_vendor == 'mlx':
        hw_speed = dut.mlxlink_dct[port]['Operational Info'].get('Speed', None)
        hw_speed = '1G' if hw_speed == 'CX' else hw_speed
        hw_speed = hw_speed.split('bE')[0]
    elif dut.asic_vendor == 'bcm':
        bcm_port, unit = dut.porttab_dct[port]
        hw_speed = dut.bcm_portstat_dct.get(unit, {}).get(bcm_port, {}).get('speed', None)
    else:
        return 'Error'

    return hw_speed


def convert_compliance_speed(compliance_codes, hardware_speed):
    new_compliance_codes = []
    hardware_speed = str(hardware_speed).split('G')[0]
    for code in compliance_codes:
        new_compliance_codes.append(code)
        # 25G//50G/100G share many of the same compliance codes. Add the lower speed code if speed < code
        if code in compliance_conversion:
            for new_code in compliance_conversion[code]:
                if hardware_speed in new_code:
                    debug('interface speed: {}, compliance code {}. Appending code: {} to compliance code list'.format(
                        hardware_speed, code, new_code))
                    new_compliance_codes.append(new_code)

    return new_compliance_codes


def convert_mlx_autoneg(mlx_autoneg):
    if mlx_autoneg == "ON":
        return 'On (Autodetect enabld)',  'Enabled'
    elif 'FORCE' in mlx_autoneg:
        return 'Off', mlx_autoneg
    else:
        return str(mlx_autoneg).capitalize(), str(mlx_autoneg).capitalize()


def port_info(dut, port):
    return {'port': port}


def module_info(dut, port):
    mod_info_dct = dict(
        vendorName=None,
        vendorPartNumber=None,
        vendorPartRevision=None,
        vendorOUI=None,
        vendorSerialNumber=None,
        identifier=None
    )
    override = None
    eeprom = dut.eeprom_dct.get(port, {})
    debug('EEPROM: ' + pformat(eeprom))
    compliance_codes = get_compliance_codes(eeprom)
    if not compliance_codes and port.replace('swp', '').split('s')[0] not in dut.eeprom_list:
        compliance_codes = ['Fixed']

    debug('Compliance Codes: {}'.format(compliance_codes))
    if port in dut.ethtool_m_dct and dut.ethtool_m_dct[port]:
        mod_info_dct['vendorName'] = dut.ethtool_m_dct[port].get('Vendor name', None)
        mod_info_dct['vendorPartNumber'] = dut.ethtool_m_dct[port].get('Vendor PN', None)
        mod_info_dct['vendorPartRevision'] = dut.ethtool_m_dct[port].get('Vendor rev', None)
        mod_info_dct['vendorOUI'] = dut.ethtool_m_dct[port].get('Vendor OUI', None)
        mod_info_dct['vendorSerialNumber'] = dut.ethtool_m_dct[port].get('Vendor SN', None)
        mod_info_dct['identifier'] = dut.ethtool_m_dct[port].get('Identifier', None)
        override = check_for_override(dut, port,
                                      vendor=mod_info_dct['vendorName'],
                                      oui=mod_info_dct['vendorOUI'],
                                      pn=mod_info_dct['vendorPartNumber'])
    if override:
        debug('Override found: {}'.format(override))
        compliance_codes = [override]
    compliance_codes = convert_compliance_speed(compliance_codes, determine_hw_speed(dut, port))
    mod_info_dct['ethernetComplianceCodes'] = ", ".join(compliance_codes)

    return mod_info_dct


def configured_info(dut, port):
    conf_info_dct = {}
    conf_info_dct['admin'], oper = translate_ip_link_status(dut, port)
    if dut.version[0:1] > '3':
        conf_info_dct['configuredFEC'] = dut.showfec_dct.get(port, {}).get('config_fec', 'Unknown')
    else:
        if dut.asic_vendor == 'mlx':
            conf_info_dct['configuredFEC'] = 'Autodetected'
        elif dut.asic_vendor == 'bcm':
            conf_info_dct['configuredFEC'] = convert_bcm_fec(dut.showfec_dct.get(port, {}).get('active_fec', 'error'))

    conf_info_dct['kernelSpeed'] = convert_kern_speed(dut.ethtool_dct.get(port, {}).get('speed', None))
    conf_info_dct['mtu'] = dut.ip_link_dct.get(port, {}).get('mtu', None)
    conf_info_dct['configuredAutoneg'] = dut.ethtool_dct.get(port, {}).get('autoneg', None).capitalize()

    return conf_info_dct


def operational_info(dut, port):
    op_info_dct = {}
    if dut.asic_vendor == 'mlx':
        if port not in dut.mlxlink_dct:
            warn("Error: Port {} data not retrieved".format(port))
            op_info_dct['operationalAutoneg'] = 'error'
            op_info_dct['operationalFEC'] = 'error'
            op_info_dct['hardwareLinkstate'] = 'error'
            op_info_dct['hardwareSpeed'] = 'error'

        op_info_dct['operationalFEC'] = convert_mlx_fec(dut.mlxlink_dct[port]['Operational Info'].get('FEC', None))
        autoneg, autodetect = convert_mlx_autoneg(dut.mlxlink_dct[port]['Operational Info'].get('Auto Negotiation', None))
        op_info_dct['operationalAutoneg'] = autoneg
        op_info_dct['hardwareLinkstate'] = 'Up' if dut.mlxlink_dct[port]['Operational Info'].get('Physical state', None) == 'LinkUp' else 'Down'
    elif dut.asic_vendor == 'bcm':
        bcm_port, unit = dut.porttab_dct[port]
        if dut.ethtool_dct.get(port, {}).get('linkstate', None) != 'yes':
            op_info_dct['operationalFEC'] = 'None (down)'
        else:
            op_info_dct['operationalFEC'] = convert_bcm_fec(dut.showfec_dct.get(port, {}).get('active_fec', 'error'))
        bcm_autoneg = dut.bcm_portstat_dct.get(unit, {}).get(bcm_port, {}).get('autoneg', None)
        if bcm_autoneg == 'No':
            op_info_dct['operationalAutoneg'] = 'Off'
            if dut.ethtool_dct.get(port, {}).get('autoneg', None) == 'on':
                op_info_dct['operationalAutoneg'] += ' (Autodetect enabld)'
        elif bcm_autoneg == 'Yes':
            op_info_dct['operationalAutoneg'] = 'On'
        else:
            op_info_dct['operationalAutoneg'] = bcm_autoneg

        op_info_dct['hardwareLinkstate'] = 'Up' if dut.bcm_portstat_dct.get(unit, {}).get(bcm_port, {}).get('linkstate', None) == 'up' else 'Down'

    else:
        error('Asic not determined')
        return 'Error'

    eeprom = dut.eeprom_dct.get(port, {})
    op_info_dct['domTX'] = None
    op_info_dct['domRX'] = None
    dom_dct = get_dom(eeprom)
    if dom_dct:
        debug('dom_dct: {}'.format(dom_dct))
        dom_tx_list = dom_dct.get('TX', ())
        op_info_dct['domTX'] = {i + 1: dom_tx_list[i] for i in range(0, len(dom_tx_list))}
        dom_rx_list = dom_dct.get('RX', ())
        op_info_dct['domRX'] = {i + 1: dom_rx_list[i] for i in range(0, len(dom_rx_list))}

    topo_neighbor_dut, topo_neighbor_port = None, None
    if dut.ptmctl_dct and port in dut.ptmctl_dct and dut.ptmctl_dct[port]:
        topo_neighbor_dut = str(dut.ptmctl_dct.get(port, {})).split(':')[0]
        topo_neighbor_port = str(dut.ptmctl_dct.get(port, {})).split(':')[1]

    op_info_dct['topologyFileNeighbor'] = str(topo_neighbor_dut) + ', ' + str(topo_neighbor_port)

    neighbor_dut, neighbor_port = lldp_remote_port_lookup(dut, port, force_refresh=False)
    op_info_dct['lldpNeighbor'] = str(neighbor_dut) + ', ' + str(neighbor_port)

    op_info_dct['kernelSpeed'] = convert_kern_speed(dut.ethtool_dct.get(port, {}).get('speed', None))
    op_info_dct['hardwareSpeed'] = determine_hw_speed(dut, port)
    op_info_dct['kernelLinkstate'] = 'Up' if dut.ethtool_dct.get(port, {}).get('linkstate', None) == 'yes' else 'Down'

    return op_info_dct


def bcm_hardware_info(dut, port):
    hw_bcm_dct = {}
    if port in dut.ethmode_dct:
        hw_bcm_dct['ethtoolMode'] = dut.reverse_ethmode_map.get(dut.ethmode_dct[port], 'Unknown')
    else:
        hw_bcm_dct['ethtoolMode'] = 'Not Found'
    bcm_port, unit = dut.porttab_dct[port]
    hw_bcm_dct['localAdvertisedProperties'] = dut.bcm_port_all_dct.get(unit, {}).get(bcm_port, {}).get('Local', None)
    parser, command, _, meta = parse_bcm_eye()
    debug('Getting eye info for bcm_port: {}'.format(bcm_port))
    cmd = command.format(sdk=bcm_port, unit=unit[-1:])
    output = shell_out_catch_errors(cmd)
    debug('dsc output: {}'.format(output))
    parsed_dsc = parse(cmd, output)
    eye_dct = parser(parsed_dsc)
    debug('eye_dct: {}'.format(eye_dct))
    hw_bcm_dct['bcmEyeSize'] = eye_dct.get('eye', {})
    hw_bcm_dct['bcmHardwareSpeed'] = dut.bcm_portstat_dct.get(unit, {}).get(bcm_port, {}).get('speed', None)
    hw_bcm_dct['carrierDetect'] = 'yes' if dut.bcm_port_all_dct.get(unit, {}).get(bcm_port, {}).get('Up', None) == '*' else 'no'
    hw_bcm_dct['RXfault'] = dut.bcm_port_all_dct.get(unit, {}).get(bcm_port, {}).get('Fault', None)
    hw_bcm_dct['signalDetect'] = (''.join([convert_bcm_signal_detect(eye_dct.get('signal_detect', {}).get('signal', {}).get(i, {}).get('detect', ''))
                                   for i in eye_dct.get('signal_detect', {}).get('signal', {})]) or 'N/A')
    hw_bcm_dct['rx_lock'] = (''.join([convert_bcm_signal_detect(eye_dct.get('signal_detect', {}).get('signal', {}).get(i, {}).get('lock', ''))
                                     for i in eye_dct.get('signal_detect', {}).get('signal', {})]) or 'N/A')
    hw_bcm_dct['bcmInterfaceMode'] = dut.bcm_portstat_dct.get(unit, {}).get(bcm_port, {}).get('cable', None)
    hw_bcm_dct['hw_active_fec'] = convert_bcm_hw_fec_str(dut.showfec_dct.get(port, {}).get('hw_active_fec', 'error'))
    if dut.bcm_portstat_dct.get(unit, {}).get(bcm_port, {}).get('autoneg', None) == 'Yes':
        hw_bcm_dct['bcmHardwareAutoneg'] = 'On'
    else:
        hw_bcm_dct['bcmHardwareAutoneg'] = 'Off'

    hw_bcm_dct['mdix'] = dut.bcm_port_all_dct.get(unit, {}).get(bcm_port, {}).get('MDIX', None)
    hw_bcm_dct['remoteAdvertisedProperties'] = dut.bcm_port_all_dct.get(unit, {}).get(bcm_port, {}).get('Remote', None)

    return hw_bcm_dct


def mlx_hardware_info(dut, port):
    hw_mlx_dct = {}
    if port not in dut.mlxlink_dct:
        return "Error: Port {} data not retrieved".format(port)

    debug('mlxlink_dct[port]: {}'.format(dut.mlxlink_dct[port]))
    hw_speed_raw = dut.mlxlink_dct[port]['Operational Info'].get('Speed', 'None').replace('bE', '')
    hw_mlx_dct['mlxHardwareSpeed'] = '1G' if hw_speed_raw == 'CX' else hw_speed_raw
    hw_mlx_dct['mlxComplianceCode'] = dut.mlxlink_dct[port]['Module Info'].get('Compliance', None)
    hw_mlx_dct['mlxCableType'] = dut.mlxlink_dct[port]['Module Info'].get('Cable Type', None)
    autoneg, autodetect = convert_mlx_autoneg(dut.mlxlink_dct[port]['Operational Info'].get('Auto Negotiation', None))
    hw_mlx_dct['mlxHardwareAutodetect'] = autodetect
    eyes = dut.mlxlink_dct[port]['EYE Opening Info'].get('Height Eye Opening [mV]', None)
    hw_mlx_dct['mlxEyeHeight'] = ', '.join([eye.strip() for eye in eyes.split(',')])
    grades = dut.mlxlink_dct[port]['EYE Opening Info'].get('Physical Grade', None)
    hw_mlx_dct['mlxEyeGrade'] = ', '.join([grade.strip() for grade in grades.split(',')])
    hw_mlx_dct['troubleshootingInfo'] = dut.mlxlink_dct[port]['Troubleshooting Info'].get('Recommendation', None)

    return hw_mlx_dct


def format_port_info(port_dct):
    return """Port:  {port}""".format(**port_dct)


def format_module_info(mod_info_dct):
    return """  Module Info
      Vendor Name: {vendorName:22s} PN: {vendorPartNumber}
      Identifier: {identifier:23s} Type: {ethernetComplianceCodes}""".format(**mod_info_dct)


def format_configured_info(conf_dct):
    return """  Configured State
      Admin: {admin:12s} Speed: {kernelSpeed:8s} MTU: {mtu}
      Autoneg: {configuredAutoneg:26s} FEC: {configuredFEC}""".format(**conf_dct)


def format_operational_info(op_info_dct):
    if op_info_dct['domTX']:
        op_info_dct['domTX'] = str([op_info_dct['domTX'][i] for i in range(1, len(op_info_dct['domTX']) + 1)])
    if op_info_dct['domRX']:
        op_info_dct['domRX'] = str([op_info_dct['domRX'][i] for i in range(1, len(op_info_dct['domRX']) + 1)])
    return """  Operational State
      Link Status: Kernel: {kernelLinkstate:14s} Hardware: {hardwareLinkstate}
      Speed: Kernel: {kernelSpeed:20s} Hardware: {hardwareSpeed}
      Autoneg: {operationalAutoneg:26s} FEC: {operationalFEC}
      TX Power (mW): {domTX}
      RX Power (mW): {domRX}
      Topo File Neighbor: {topologyFileNeighbor}
      LLDP Neighbor:      {lldpNeighbor}""".format(**op_info_dct)


def format_bcm_hardware_info(hw_bcm_dct):
    if len(str(hw_bcm_dct['localAdvertisedProperties'])) > 20:
        hw_bcm_dct['localAdvertisedProperties'] = ' ' + str(hw_bcm_dct['localAdvertisedProperties']) + '\n     '

    eye_out = []
    eye_out_formatted = ''
    if hw_bcm_dct['bcmEyeSize']:
        for lane in sorted(hw_bcm_dct['bcmEyeSize']):
            lane_dct = hw_bcm_dct['bcmEyeSize'][lane]
            eye_out.append('L: {left}, R: {right}, U: {up}, D: {down}'.format(
                left=lane_dct['L'], right=lane_dct['R'], up=lane_dct['U'], down=lane_dct['D']))
        for i, eye in enumerate(eye_out):
            if i % 2 == 1:
                eye_out_formatted += '{},\n            '.format(eye)
            else:
                eye_out_formatted += '{}, '.format(eye)
        hw_bcm_dct['bcmEyeSize'] = eye_out_formatted.strip()[:-1]
    else:
        hw_bcm_dct['bcmEyeSize'] = 'N/A'

    if '1G' in str(hw_bcm_dct['bcmHardwareSpeed']):
        hw_bcm_dct['RXfault'] = str(hw_bcm_dct['RXfault']) + ' (ignore on 1G)'

    return """  Port Hardware State:
      Rx Fault: {RXfault:25s} Carrier Detect: {carrierDetect}
      Rx Signal: Detect: {signalDetect:16s} Signal Lock: {rx_lock}
      Ethmode Type: {ethtoolMode:21s} Interface Type: {bcmInterfaceMode}
      Speed: {bcmHardwareSpeed:28s} Autoneg: {bcmHardwareAutoneg}
      MDIX: {mdix:29s} FEC: {hw_active_fec}
      Local Advrtsd: {localAdvertisedProperties:20s} Remote Advrtsd: {remoteAdvertisedProperties}
      Eyes: {bcmEyeSize}""".format(**hw_bcm_dct)


def format_mlx_hardware_info(hw_mlx_dct):
    return """  Port Hardware State:
      Compliance Code: {mlxComplianceCode}
      Cable Type: {mlxCableType}
      Speed: {mlxHardwareSpeed:28s} Autodetect: {mlxHardwareAutodetect}
      Eyes: {mlxEyeHeight:29s} Grade: {mlxEyeGrade}
      Troubleshooting Info: {troubleshootingInfo}""".format(**hw_mlx_dct)


def create_all_ports_list(dut):
    # Match groups are used for natural sort - 1st group is whole match
    path = '/sys/class/net/'
    if os.path.exists(path):
        p = re.compile(r'sw[0-9]*p[0-9]+(s[0-3])?$')
        matches = [f for f in os.listdir(path) if p.match(f)]
    else:
        critical('Node /sys/class/net does not exist')

    allportslist = sorted(matches, key=lambda l: swp_sort_pattern(l))

    return allportslist


def filter_port_list(allportslist, port_filter):
    if port_filter == 'all':
        debug('Portlist is {}'.format(allportslist))
        return allportslist

    port_filter = expand_port_filter(port_filter)

    sanitized_portlist = []
    for port in port_filter:
        if re.match('^[0-9]+(s[0123])?$', port):
            port = 'swp' + port
        if re.match('^[0-9]+p[0-9]+(s[0-3])?$', port):
            port = 'sw' + port
        if not re.match('^sw[0-9]*p[0-9]+(s[0-3])?$', port):
            error('Port: \'{}\' not formatted correctly. Skipping.'.format(port))
            continue
        if port not in allportslist:
            error('Port: \'{}\' does not exist on this switch. Skipping.'.format(port))
            continue
        if (os.path.isdir("/sys/class/net/{}/bridge".format(port))
                or os.path.isdir("/sys/class/net/{}/bonding".format(port))):
            warn('Port: \'{}\' is not a physical interface. Skipping.'.format(port))
            continue

        sanitized_portlist.append(port)

    debug('Portlist is {}'.format(sanitized_portlist))
    return sanitized_portlist


def expand_port_filter(port_filter):
    port_filter = port_filter.split(',')
    port_list = []
    for port_group in port_filter:
        if '-' in port_group:
            rangelist = port_group.split('-')
            for i in range(int(rangelist[0]), int(rangelist[1]) + 1):
                port_list.append(str(i))
        else:
            port_list.append(port_group)
    return port_list


def collect_all_l1_data(dut, portlist):
    dut_install_version(dut)
    asic_vendor = dut.asic_vendor
    if asic_vendor == 'bcm':
        debug('Starting porttab collection')
        dut_install_porttab(dut)
        debug('Starting portstat collection')
        dut_install_bcm_dct(dut, 'portstat')
        debug('Starting port all collection')
        dut_install_bcm_dct(dut, 'port all')
        debug('Setting up portwd overrides')
        setup_overrides(dut)
        debug('Starting ethmodes collection')
        dut_install_ethmodes(dut, portlist)

    elif asic_vendor == 'mlx':
        dut_install_mlxlink(dut, portlist)

    elif asic_vendor == 'vx':
        error('L1-show is not a valid command for Vx switches.')
        sys.exit(1)
    debug('Starting ethtool collection')
    dut_install_ethtool(dut, portlist)
    debug('Starting ethtool -m collection')
    dut_install_ethtool_m(dut, portlist)
    debug(dut.ethtool_m_dct)
    debug('Starting eeprom collection')
    dut_install_eeprom(dut, portlist)
    debug('Starting ptmctl collection')
    dut_install_ptmctl(dut)
    debug('Starting lldp collection')
    dut_install_lldpctl(dut)
    debug('Staring ethtool --show-fec collection')
    dut_install_ethtool_showfec(dut, portlist)
    dut_install_ip_link(dut)

    return asic_vendor


def l1_show_output_to_screen(asic_vendor, dut, portlist):
    for port in portlist:
        print(format_port_info(port_info(dut, port)))
        print(format_module_info(module_info(dut, port)))
        print(format_configured_info(configured_info(dut, port)))
        print(format_operational_info(operational_info(dut, port)))

        if asic_vendor == 'bcm':
            print(format_bcm_hardware_info(bcm_hardware_info(dut, port)))

        elif asic_vendor == 'mlx':
            print(format_mlx_hardware_info(mlx_hardware_info(dut, port)))


def l1_show_output_to_json(asic_vendor, dut, portlist):
    l1_show_dct = {}
    for port in portlist:
        l1_show_dct[port] = {}
        l1_show_dct[port]['portInfo'] = port_info(dut, port)
        l1_show_dct[port]['moduleInfo'] = module_info(dut, port)
        l1_show_dct[port]['configuredInfo'] = configured_info(dut, port)
        l1_show_dct[port]['operationalInfo'] = operational_info(dut, port)
        if asic_vendor == 'bcm':
            l1_show_dct[port]['hardwareInfoBroadcom'] = bcm_hardware_info(dut, port)
        elif asic_vendor == 'mlx':
            l1_show_dct[port]['hardwareInfoMellanox'] = mlx_hardware_info(dut, port)

    return json.dumps(l1_show_dct, indent=4)


def main():

    if not os.geteuid() == 0:
        print('Error: Command must be run as root')
        print('Error: See l1-show -h for more')
        sys.exit(1)

    if arguments['--debug']:
        debug_logging(arguments['--debug'].split(','), this_log)
        list_logger_levels()

    debug('Argument: {}'.format(arguments))
    dut = Dut()

    if arguments['PORTLIST']:
        port_filter = arguments['PORTLIST']
        allportslist = create_all_ports_list(dut)

        if not allportslist:
            error('No valid ports')
            sys.exit(1)
    else:
        error('Must specify a port list or "all"')
        sys.exit(1)

    portlist = filter_port_list(allportslist, port_filter)
    debug('[outer routine] portlist is {}'.format(portlist))

    if portlist:
        asic_vendor = collect_all_l1_data(dut, portlist)

        if arguments['--json']:
            print(l1_show_output_to_json(asic_vendor, dut, portlist))
        else:
            l1_show_output_to_screen(asic_vendor, dut, portlist)


if __name__ == '__main__':
    sys.exit(main())
