#!/usr/bin/env python3
#
# Copyright (c) 2019, AT&T Intellectual Property.
# All rights reserved.
#
# SPDX-License-Identifier: GPL-2.0-only
#

"""Scripts for CGNAT public address op-mode commands"""

import sys
import getopt
import vplaned
import socket
import struct


#
# int2ip
#
def int2ip(addr):
    """Convert a uint to an IP address string"""

    return socket.inet_ntoa(struct.pack("!I", addr))


#
# cgn_get_public_list
#
def cgn_get_public_list(prefix):
    """Get sorted list of active cgnat public addresses.

    Fetches a list of uints from the dataplane, sorts them, then converts to
    IP address strings.  An optional address or prefix/length may be
    specified, in which case the dataplane will only return active public
    addresses matching that value.

    """

    pub_list = []
    base_cmd = "cgn-op list public"

    with vplaned.Controller() as controller:
        for dp in controller.get_dataplanes():
            with dp:
                cmd = base_cmd
                if prefix:
                    cmd = "%s prefix %s" % (cmd, prefix)

                cgn_dict = dp.json_command(cmd)

                # Remove outer object
                tmp_list = cgn_dict.get('public')

                if tmp_list:
                    pub_list.extend(tmp_list)

    #
    # Remove duplicates from pub_list
    #
    # There may be up to three entries per address (one per proto)
    # so use the list-to-dict-to-list trick to remove duplicates
    #
    pub_list = list(dict.fromkeys(pub_list))

    # Sort list while it is in number format
    pub_list.sort()

    # Return list of addresses in IP address string format
    return [int2ip(addr) for addr in pub_list]


#
# Show one public address, with bitmap details.
#
# Each line holds a bitmap for 64 ports.  So if a port-block size is 128 then
# there will be two lines per protocol per port-block.
#
# This function format and outputs the latter five columns (blk_str, pr_str
# and bm_str):
#
# <--------------- pfx_str ----------------><--- blk_str ---><--pr_str--><---- bm_str ----->
#
# Public Address  Port Range #Prts Blks Used  Blk  Port range Proto Ports            Bitmaps
#      10.10.4.1  1024-65535 64512    3/1008    0   1024-1087   tcp     0 0x0000000000000000
#                                                               udp    64 0xffffffffffffffff
#                                                             other     0 0x0000000000000000
#                                               1   1088-1151   tcp     0 0x0000000000000000
#                                                               udp    64 0xffffffffffffffff
#                                                             other     0 0x0000000000000000
#                                               2   1152-1215   tcp     0 0x0000000000000000
#                                                               udp     2 0x0000000000000003
#                                                             other     0 0x0000000000000000
#
def cgn_op_show_pub_block_detail(pub, pfx_len):

    # Get port-block array
    blocks = pub.get('blocks')

    pfx_indent = 0
    first_blk = True

    for block in blocks:
        port_range = "%u-%u" % (block.get('port_start'), block.get('port_end'))
        blk_str = " %4s %11s" % (block.get('block'), port_range)

        print("%*s%s" % (pfx_indent, "", blk_str), end='')
        pfx_indent = 0

        # Protocol list
        protocols = block.get('protocols')

        first_pr = True
        blk_indent = 0

        for proto in protocols:
            pr_str = " %5s %5u" % (proto.get('protocol'),
                                   proto.get('ports_used'))

            print("%*s%*s%s" % (pfx_indent, "", blk_indent, "", pr_str), end='')

            pfx_indent = 0
            blk_indent = 0

            # Bitmap list
            bms = proto.get('bitmaps')

            first_bm = True
            pr_indent = 0

            # Second and later bitmaps are on a line (at the end) by themselves
            for bm in bms:

                print("%*s%*s%*s 0x%016x" % (pfx_indent, "", blk_indent, "",
                                             pr_indent, "", bm))

                if first_bm:
                    pr_indent = len(pr_str)
                    blk_indent = len(blk_str)
                    pfx_indent = pfx_len
                    first_bm = False

            if first_pr:
                first_pr = False

        # For subsequent lines we indent by the prefix string length
        if first_blk:
            blk_indent = 0
            first_blk = False

    return


#
# Show one public address, without bitmap details (default).  Each port-block
# is displayed on a new line.
#
# <--------------- pfx_str ----------------><--- blk_str ---><--- pr_str ---->
#
# Public Address  Port Range #Prts Blks Used  Blk  Port range   TCP  UDP Other
#      10.10.4.1  1024-65535 64512    3/1008    0   1024-1087     0   64     0
#                                               1   1088-1151     0   64     0
#
def cgn_op_show_pub_block_brief(pub, pfx_len):
    # Get port-block array
    blocks = pub.get('blocks')

    pfx_indent = 0

    for block in blocks:
        port_range = "%u-%u" % (block.get('port_start'), block.get('port_end'))
        blk_str = " %4s %11s" % (block.get('block'), port_range)

        print("%*s%s" % (pfx_indent, "", blk_str), end='')
        pfx_indent = pfx_len

        # Protocol list
        protocols = block.get('protocols')

        for proto in protocols:
            print(" %5u" % (proto.get('ports_used')), end='')

        print()

    return


#
# cgn_op_show_pub_one
#
def cgn_op_show_pub_one(pub, detail):
    """Show one CGN public address"""

    addr = pub.get('address')

    port_range = "%u-%u" % (pub.get('port_start'), pub.get('port_end'))
    nports = pub.get('port_end') - pub.get('port_start') + 1

    # Get port-block array
    blocks = pub.get('blocks')

    blks_used = "%u/%u" % (len(blocks), pub.get('nblocks'))

    pfx_str = "%15s %11s %5s %9s" % (addr, port_range, nports, blks_used)

    # Remember the length of pfx before blocks and port ranges are added.
    # This is used for lines where pfx is empty.
    pfx_len = len(pfx_str)

    print("%s" % (pfx_str), end='')

    if detail:
        cgn_op_show_pub_block_detail(pub, pfx_len)
        return
    else:
        cgn_op_show_pub_block_brief(pub, pfx_len)


#
# cgn_op_show_pub_addr
#
def cgn_op_show_pub_addr(addr, detail):
    """
    Show one CGN pub addr
    addr may be a prefix/length or host.  If it is a host, then up to 3 apm
    entries may be returned - one per proto.
    """

    pub_list = []

    cmd = "cgn-op show apm"
    if addr:
        cmd = "%s address %s" % (cmd, addr)

    if detail:
        cmd = "%s detail" % (cmd)

    with vplaned.Controller() as controller:
        for dp in controller.get_dataplanes():
            with dp:
                cgn_dict = dp.json_command(cmd)

                # Get list
                new = cgn_dict.get('apm')

                # Extend session list
                if new:
                    pub_list.extend(new)

    for pub in pub_list:
        cgn_op_show_pub_one(pub, detail)


#
# cgn_op_show_public
#
def cgn_op_show_public(pa_opt, detail):
    """Show CGN public"""

    # Get list of sorted public addresses
    addr_list = cgn_get_public_list(pa_opt)

    banner = "%15s %11s %5s %9s %4s %11s" % ("Public Address",
                                             "Port Range",
                                             "#Prts", "Blks Used",
                                             "Blk", "Port range")

    if detail:
        banner += " %5s %5s %18s" % ("Proto", "Ports", "Bitmaps")
    else:
        banner += " %5s %5s %5s" % ("TCP", "UDP", "Other")

    print(banner)

    for addr in addr_list:
        cgn_op_show_pub_addr(addr, detail)


#
# usage
#
def cgn_usage():
    """Show command help"""

    print("usage: {} --show | --clear".format(sys.argv[0]),
          file=sys.stderr)


#
# cgn_op_main
#
def cgn_op_main():
    """Main function"""

    s_opt = False
    pa_opt = None
    d_opt = False

    #
    # Parse options
    #
    try:
        opts, args = getopt.getopt(sys.argv[1:],
                                   "",
                                   ['show', 'detail', 'pub-addr='])

    except getopt.GetoptError as r:
        print(r, file=sys.stderr)
        cgn_usage()
        sys.exit(2)

    for opt, arg in opts:
        if opt in '--show':
            s_opt = True

        if opt in '--detail':
            d_opt = True

        if opt in '--pub-addr':
            pa_opt = arg

    # show ...
    if s_opt:
        cgn_op_show_public(pa_opt, d_opt)


#
# main
#
if __name__ == '__main__':
    cgn_op_main()
