#!/usr/bin/env python

#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#

from __future__ import unicode_literals
from __future__ import division
from __future__ import absolute_import
from __future__ import print_function

import os
from optparse import OptionGroup
import sys
import locale
import socket
import re
from time import ctime, strftime, gmtime
import  qpid_dispatch_site
from qpid_dispatch.management.client import Url, Node, Entity
from qpid_dispatch_internal.management.qdrouter import QdSchema
from qpid_dispatch_internal.tools import Display, Header, Sorter, YN, Commas, TimeLong
from qpid_dispatch_internal.tools.command import connection_options, main, OptionParser, opts_ssl_domain, opts_sasl, \
        opts_url
from qpid_dispatch_internal.compat import UNICODE

def parse_args(argv):
    """ Set global variables for options, return arguments """

    usage = "%prog [options]"

    parser = OptionParser(usage=usage)

    parser.add_option_group(connection_options(parser))

    parser.add_option("-g", "--general", help="Show General Router Stats",  action="store_const", const="g",   dest="show")
    parser.add_option("-c", "--connections", help="Show Connections",       action="store_const", const="c",   dest="show")
    parser.add_option("-l", "--links", help="Show Router Links",            action="store_const", const="l",   dest="show")
    parser.add_option("-n", "--nodes", help="Show Router Nodes",            action="store_const", const="n",   dest="show")
    parser.add_option("-e", "--edge", help="Show edge connections",         action="store_const", const="e",   dest="show")
    parser.add_option("-a", "--address", help="Show Router Addresses",      action="store_const", const="a",   dest="show")
    parser.add_option("-m", "--memory", help="Show Router Memory Stats",    action="store_const", const="m",   dest="show")
    parser.add_option("--autolinks", help="Show Auto Links",                action="store_const", const="autolinks",  dest="show")
    parser.add_option("--linkroutes", help="Show Link Routes",              action="store_const", const="linkroutes", dest="show")
    parser.add_option("-v", "--verbose", help="Show maximum detail",        action="store_true", dest="verbose")
    parser.add_option("--log", help="Show recent log entries", action="store_const", const="log", dest="show")

    # This limit can be used to limit the number of output rows and can be used in conjunction with options
    # like -c, -l, -a, --autolinks, --linkroutes and --log.
    # By default, the limit is set to 100 rows.
    parser.add_option("--limit", help="Limit number of output rows", type="int", default=100)

    opts, args = parser.parse_args(args=argv)

    if not opts.show:
        parser.error("You must specify one of these options: -g, -c, -l, -n, -a, -m, -h, --autolinks, --linkroutes, or --log.")

    return opts, args


def get(obj, attr):
    if attr in obj.__dict__:
        return obj.__dict__[attr]
    return None


class BusManager(Node):

    schema = QdSchema()

    def __init__(self, opts):
        self.opts = opts
        super(BusManager, self).__init__(
            Node.connection(opts_url(opts), opts.router,
                            timeout=opts.timeout,
                            ssl_domain=opts_ssl_domain(opts),
                            sasl=opts_sasl(self.opts)))

    def query(self, entity_type, attribute_names=None, limit=None):
        if attribute_names:
            unames = []
            for a in attribute_names:
                unames.append(UNICODE(a))
            attribute_names = unames
        return super(BusManager, self).query(entity_type, attribute_names, count=limit).get_entities()

    def connAuth(self, conn):
        ##
        ## Summarize the authentication for a connection:
        ##   no-auth
        ##   anonymous-user
        ##   <user>(PLAIN)
        ##   <user>(kerberos)
        ##   <user>(x.509)
        ##
        if not conn.isAuthenticated:
            return "no-auth"
        sasl = conn.sasl
        if sasl == "GSSAPI":
            sasl = "Kerberos"
        elif sasl == "EXTERNAL":
            sasl = "x.509"
        elif sasl == "ANONYMOUS":
            return "anonymous-user"
        if not conn.user:
            return sasl
        return "%s(%s)" % (conn.user, sasl)

    def connSecurity(self, conn):
        ##
        ## Summarize the security of a connection:
        ##   no-security
        ##   SSLv3 (cipher)
        ##   TLS (cipher)
        ##   Kerberos
        ##
        if not conn.isEncrypted:
            return "no-security"
        if conn.sasl == "GSSAPI":
            return "Kerberos"
        return "%s(%s)" % (conn.sslProto, conn.sslCipher)

    def noTrailingSlash(self, text):
        if text == None:
            return ""
        if text[-1:] == '/':
            return text[:-1]
        return text

    def displayEdges(self):
        disp = Display(prefix="  ")
        heads = []
        heads.append(Header("id"))
        heads.append(Header("host"))
        heads.append(Header("container"))
        heads.append(Header("role"))
        heads.append(Header("dir"))
        heads.append(Header("security"))
        heads.append(Header("authentication"))
        heads.append(Header("tenant"))
        
        rows = []
        objects = self.query('org.apache.qpid.dispatch.connection', limit=self.opts.limit)
        
        if not objects:
            print("No Edge Router Connections")
            return
        has_active = False
        
        first = objects[0]
        try:
            if first:
                active = first.active
                has_active = True
        except:
            pass
            
        if has_active:
            heads.append(Header("active"))
        
        for conn in objects:
            #print (conn)
            if conn.role == "edge":
               row = []
               row.append(conn.identity)
               row.append(conn.host)
               row.append(conn.container)
               row.append(conn.role)
               row.append(conn.dir)
               row.append(self.connSecurity(conn))
               row.append(self.connAuth(conn))
               row.append(self.noTrailingSlash(get(conn, 'tenant')))
               if has_active:
                   if conn.active:
                       row.append("yes")
                   else:
                       row.append("no")
               rows.append(row)
        
        if rows:
            title = "Connections"
        else:
            return
        
        dispRows = rows
        disp.formattedTable(title, heads, dispRows)            
        
    def displayConnections(self):
        disp = Display(prefix="  ")
        heads = []
        heads.append(Header("id"))
        heads.append(Header("host"))
        heads.append(Header("container"))
        heads.append(Header("role"))
        heads.append(Header("dir"))
        heads.append(Header("security"))
        heads.append(Header("authentication"))
        heads.append(Header("tenant"))

        rows = []
        objects = self.query('org.apache.qpid.dispatch.connection', limit=self.opts.limit)

        for conn in objects:
            row = []
            row.append(conn.identity)
            row.append(conn.host)
            row.append(conn.container)
            row.append(conn.role)
            row.append(conn.dir)
            row.append(self.connSecurity(conn))
            row.append(self.connAuth(conn))
            row.append(self.noTrailingSlash(get(conn, 'tenant')))
            rows.append(row)
        title = "Connections"
        dispRows = rows
        disp.formattedTable(title, heads, dispRows)

    def _addr_summary(self, addr):
        cls   = self._addr_class(addr)
        phase = self._addr_phase(addr)
        text  = self._addr_text(addr)
        if cls == '-':
            return "-"
        if cls == 'M':
            if phase == '0':
                return text
            else:
                return "%s:%s" % (phase, text)
        return "%s:%s" % (cls, text)


    def _addr_class(self, addr):
        if not addr:
            return ""
        if addr[0] == 'M'  : return "mobile"
        if addr[0] == 'R'  : return "router"
        if addr[0] == 'A'  : return "area"
        if addr[0] == 'L'  : return "local"
        if addr[0] == 'T'  : return "topo"
        if addr[0] in 'CE' : return "link-in"
        if addr[0] in 'DF' : return "link-out"
        if addr[0] == 'H'  : return "edge"
        return "unknown: %s" % addr[0]

    def _addr_text(self, addr):
        if not addr:
            return ""
        if addr[0] == 'M':
            return addr[2:]
        else:
            return addr[1:]

    def _addr_phase(self, addr):
        if not addr:
            return ""
        if addr[0] == 'M':
            return addr[1]
        return ''

    def _identity_clean(self, identity, router_id=None):
        if router_id:
            return router_id
        if not identity:
            return "-"
        pos = identity.find('/')
        if pos >= 0:
            return identity[pos + 1:]
        return identity

    def _list_clean(self, inlist):
        outlist = []
        for i in inlist:
            outlist.append(str(i))
        return outlist

    def displayGeneral(self):
        disp = Display(prefix="  ")
        heads = []
        heads.append(Header("attr"))
        heads.append(Header("value"))
        rows = []

        objects = self.query('org.apache.qpid.dispatch.router')

        router = objects[0]
        rows.append(('Version',       router.version))
        rows.append(('Mode',          router.mode))
        rows.append(('Router Id',     router.id))
        rows.append(('Area',          router.area))
        rows.append(('Link Routes',   router.linkRouteCount))
        rows.append(('Auto Links',    router.autoLinkCount))
        rows.append(('Links',         router.linkCount))
        rows.append(('Nodes',         router.nodeCount))
        rows.append(('Addresses',     router.addrCount))
        rows.append(('Connections',   router.connectionCount))

        # Overall delivery related counts.
        # These overall statistics were introduced in 1.1 version.
        # Wrap these in a try except so that newer versions of qdstat works with older version of router
        try:
            rows.append(('Presettled Count', router.presettledDeliveries))
            rows.append(('Dropped Presettled Count', router.droppedPresettledDeliveries))
            rows.append(('Accepted Count', router.acceptedDeliveries))
            rows.append(('Rejected Count', router.rejectedDeliveries))
            rows.append(('Released Count', router.releasedDeliveries))
            rows.append(('Modified Count', router.modifiedDeliveries))
            rows.append(('Ingress Count', router.deliveriesIngress))
            rows.append(('Egress Count', router.deliveriesEgress))
            rows.append(('Transit Count', router.deliveriesTransit))
            rows.append(('Deliveries from Route Container', router.deliveriesIngressRouteContainer))
            rows.append(('Deliveries to Route Container', router.deliveriesEgressRouteContainer))
        except:
            pass

        title = "Router Statistics"
        dispRows = rows
        disp.formattedTable(title, heads, dispRows)

    def displayRouterLinks(self):
        disp = Display(prefix="  ")
        heads = []
        heads.append(Header("type"))
        heads.append(Header("dir"))
        heads.append(Header("conn id"))
        heads.append(Header("id"))
        heads.append(Header("peer"))
        heads.append(Header("class"))
        heads.append(Header("addr"))
        heads.append(Header("phs"))
        heads.append(Header("cap"))


        rows = []
        cols = ('linkType', 'linkDir', 'connectionId', 'identity', 'peer', 'owningAddr',
                'capacity', 'undeliveredCount', 'unsettledCount', 'deliveryCount',
                'presettledCount', 'droppedPresettledCount', 'acceptedCount', 'rejectedCount', 'releasedCount',
                'modifiedCount', 'adminStatus', 'operStatus', 'linkName', 'priority')

        objects = self.query('org.apache.qpid.dispatch.router.link', cols, limit=self.opts.limit)

        has_dropped_presettled_count = False
        has_priority = False

        if objects:
            first_row = objects[0]
            if first_row:
                if hasattr(first_row, 'droppedPresettledCount'):
                    has_dropped_presettled_count = True
                if hasattr(first_row, 'priority'):
                    has_priority = True

        if has_priority:
            heads.append(Header("pri"))
        heads.append(Header("undel"))
        heads.append(Header("unsett"))
        heads.append(Header("del"))
        heads.append(Header("presett"))


        if has_dropped_presettled_count:
            heads.append(Header("psdrop"))

        heads.append(Header("acc"))
        heads.append(Header("rej"))
        heads.append(Header("rel"))
        heads.append(Header("mod"))
        heads.append(Header("admin"))
        heads.append(Header("oper"))
        if self.opts.verbose:
            heads.append(Header("name"))

        for link in objects:
            row = []
            row.append(link.linkType)
            row.append(link.linkDir)
            row.append(link.connectionId)
            row.append(link.identity)
            row.append(link.peer)
            row.append(self._addr_class(link.owningAddr))
            row.append(self._addr_text(link.owningAddr))
            row.append(self._addr_phase(link.owningAddr))
            row.append(link.capacity)
            if has_priority:
                row.append(link.priority)
            row.append(link.undeliveredCount)
            row.append(link.unsettledCount)
            row.append(link.deliveryCount)
            row.append(link.presettledCount)
            if has_dropped_presettled_count:
                row.append(link.droppedPresettledCount)
            row.append(link.acceptedCount)
            row.append(link.rejectedCount)
            row.append(link.releasedCount)
            row.append(link.modifiedCount)
            row.append(link.adminStatus)
            row.append(link.operStatus)
            if self.opts.verbose:
                row.append(link.linkName)
            rows.append(row)
        title = "Router Links"
        dispRows = rows
        disp.formattedTable(title, heads, dispRows)

    def displayRouterNodes(self):
        disp = Display(prefix="  ")
        heads = []
        heads.append(Header("router-id"))
        heads.append(Header("next-hop"))
        heads.append(Header("link"))
        if self.opts.verbose:
            heads.append(Header("ver"))
            heads.append(Header("cost"))
            heads.append(Header("neighbors"))
            heads.append(Header("valid-origins"))
        rows = []
        objects = self.query('org.apache.qpid.dispatch.router.node', limit=self.opts.limit)

        # Find the most recent topo change in this neighborhood.
        lastTopoChange = 0.0

        for node in objects:
            row = []
            if node.lastTopoChange:
                if float(node.lastTopoChange) > lastTopoChange:
                    lastTopoChange = float(node.lastTopoChange)
            row.append(node.id)
            if node.nextHop != None:
                row.append(node.nextHop)
                row.append('-')
            else:
                row.append('-')
                row.append(node.routerLink)

            if self.opts.verbose:
                row.append(get(node, 'protocolVersion'))
                row.append(get(node, 'cost'))
                row.append('%r' % self._list_clean(node.linkState))
                row.append('%r' % self._list_clean(node.validOrigins))
            rows.append(row)
        if len(rows) > 0:
            title = "Routers in the Network"
            # Use gmtime to make times comparable across large networks.
            if lastTopoChange > 1.0:
                topoLine = "\nLast Topology Change: " + strftime('%A %b %d %H:%M:%S %Y',gmtime(lastTopoChange)) + " GMT"
                title += topoLine
            sort = Sorter(heads, rows, 'router-id')
            dispRows = sort.getSorted()
            disp.formattedTable(title, heads, dispRows)
        else:
            print("Router is Standalone - No Router List")

    def displayAddresses(self):
        disp = Display(prefix="  ")
        heads = []
        heads.append(Header("class"))
        heads.append(Header("addr"))
        heads.append(Header("phs"))
        heads.append(Header("distrib"))

        rows = []
        cols = ('distribution', 'inProcess', 'subscriberCount', 'remoteCount',
                'containerCount', 'deliveriesIngress', 'deliveriesEgress',
                'deliveriesTransit', 'deliveriesToContainer', 'deliveriesFromContainer', 'name', 'priority')

        objects = self.query('org.apache.qpid.dispatch.router.address', cols, limit=self.opts.limit)

        has_priority = False

        if objects:
            first_row = objects[0]
            if first_row:
                if hasattr(first_row, 'priority'):
                    has_priority = True

        if has_priority:
            heads.append(Header("pri"))
        heads.append(Header("in-proc", Header.COMMAS))
        heads.append(Header("local", Header.COMMAS))
        heads.append(Header("remote", Header.COMMAS))
        heads.append(Header("cntnr", Header.COMMAS))
        heads.append(Header("in", Header.COMMAS))
        heads.append(Header("out", Header.COMMAS))
        heads.append(Header("thru", Header.COMMAS))
        heads.append(Header("to-proc", Header.COMMAS))
        heads.append(Header("from-proc", Header.COMMAS))

        for addr in objects:
            row = []
            row.append(self._addr_class(addr.name))
            row.append(self._addr_text(addr.name))
            row.append(self._addr_phase(addr.name))
            row.append(addr.distribution)
            if has_priority:
                row.append(addr.priority if addr.priority >= 0 else "")
            row.append(addr.inProcess)
            row.append(addr.subscriberCount)
            row.append(addr.remoteCount)
            row.append(addr.containerCount)
            row.append(addr.deliveriesIngress)
            row.append(addr.deliveriesEgress)
            row.append(addr.deliveriesTransit)
            row.append(addr.deliveriesToContainer)
            row.append(addr.deliveriesFromContainer)
            rows.append(row)
        title = "Router Addresses"
        sorter = Sorter(heads, rows, 'addr', 0, True)
        dispRows = sorter.getSorted()
        disp.formattedTable(title, heads, dispRows)

    def displayAutolinks(self):
        disp = Display(prefix="  ")
        heads = []
        heads.append(Header("addr"))
        heads.append(Header("dir"))
        heads.append(Header("phs"))
        heads.append(Header("extAddr"))
        heads.append(Header("link"))
        heads.append(Header("status"))
        heads.append(Header("lastErr"))
        rows = []
        cols = ('addr', 'direction', 'phase', 'externalAddr', 'linkRef', 'operStatus', 'lastError')

        objects = self.query('org.apache.qpid.dispatch.router.config.autoLink', cols, limit=self.opts.limit)

        for al in objects:
            row = []
            row.append(al.addr)
            row.append(al.direction)
            row.append(al.phase)
            row.append(al.externalAddr)
            row.append(al.linkRef)
            row.append(al.operStatus)
            row.append(al.lastError)
            rows.append(row)
        title = "AutoLinks"
        sorter = Sorter(heads, rows, 'addr', 0, True)
        dispRows = sorter.getSorted()
        disp.formattedTable(title, heads, dispRows)

    def displayLinkRoutes(self):
        disp = Display(prefix="  ")
        heads = []
        heads.append(Header("address"))
        heads.append(Header("dir"))
        heads.append(Header("distrib"))
        heads.append(Header("status"))
        rows = []
        cols = ('prefix', 'direction', 'distribution', 'operStatus', 'pattern', 'addExternalPrefix', 'delExternalPrefix')

        link_routes = self.query('org.apache.qpid.dispatch.router.config.linkRoute', cols, limit=self.opts.limit)

        have_add_del_prefix = False
        for link_route in link_routes:
            row = []
            row.append(link_route.prefix if link_route.prefix else link_route.pattern)
            row.append(link_route.direction)
            row.append(link_route.distribution)
            row.append(link_route.operStatus)
            try:
                if link_route.addExternalPrefix or link_route.delExternalPrefix:
                    row.append(link_route.addExternalPrefix)
                    row.append(link_route.delExternalPrefix)
                    have_add_del_prefix = True
            except KeyError:
                pass # added post 1.1.0
            rows.append(row)
        title = "Link Routes"
        if have_add_del_prefix:
            heads.append(Header("add-ext-prefix"))
            heads.append(Header("del-ext-prefix"))
        sorter = Sorter(heads, rows, 'address', 0, True)
        dispRows = sorter.getSorted()
        disp.formattedTable(title, heads, dispRows)

    def displayMemory(self):
        disp = Display(prefix="  ")
        heads = []
        heads.append(Header("type"))
        heads.append(Header("size", Header.COMMAS))
        heads.append(Header("batch"))
        heads.append(Header("thread-max", Header.COMMAS))
        heads.append(Header("total", Header.COMMAS))
        heads.append(Header("in-threads", Header.COMMAS))
        heads.append(Header("rebal-in", Header.COMMAS))
        heads.append(Header("rebal-out", Header.COMMAS))
        rows = []
        cols = ('identity', 'typeSize', 'transferBatchSize', 'localFreeListMax',
                'totalAllocFromHeap', 'heldByThreads', 'batchesRebalancedToThreads',
                'batchesRebalancedToGlobal')

        objects = self.query('org.apache.qpid.dispatch.allocator', cols)

        for t in objects:
            row = []
            row.append(self._identity_clean(t.identity))
            row.append(t.typeSize)
            row.append(t.transferBatchSize)
            row.append(t.localFreeListMax)
            row.append(t.totalAllocFromHeap)
            row.append(t.heldByThreads)
            row.append(t.batchesRebalancedToThreads)
            row.append(t.batchesRebalancedToGlobal)
            rows.append(row)
        if not rows:
            # router built w/o memory pools:
            print("No memory statistics available")
            return
        title = "Types"
        sorter = Sorter(heads, rows, 'type', 0, True)
        dispRows = sorter.getSorted()
        disp.formattedTable(title, heads, dispRows)

    def displayLog(self):
        log = self.get_log(limit=self.opts.limit)
        for line in log:
            print("%s %s (%s) %s" % (ctime(line[5]), line[0], line[1], line[2]))

    def displayMain(self, identitys, main):
        if   main == 'l': self.displayRouterLinks()
        elif main == 'n': self.displayRouterNodes()
        elif main == 'a': self.displayAddresses()
        elif main == 'm': self.displayMemory()
        elif main == 'g': self.displayGeneral()
        elif main == 'e': self.displayEdges()
        elif main == 'c': self.displayConnections()
        elif main == 'autolinks': self.displayAutolinks()
        elif main == 'linkroutes': self.displayLinkRoutes()
        elif main == 'log': self.displayLog()

    def display(self, identitys):
        self.displayMain(identitys, self.opts.show)

def run(argv):
    opts, args = parse_args(argv)
    if args[1:]:
        raise Exception("Unexpected arguments: %s" % " ".join(args[1:]))
    bm = BusManager(opts)
    try:
        bm.display(args)
    finally:
        bm.close()

if __name__ == "__main__":
        sys.exit(main(run, sys.argv))
