#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
#  =================================================================
#           #     #                 #     #
#           ##    #   ####   #####  ##    #  ######   #####
#           # #   #  #    #  #    # # #   #  #          #
#           #  #  #  #    #  #    # #  #  #  #####      #
#           #   # #  #    #  #####  #   # #  #          #
#           #    ##  #    #  #   #  #    ##  #          #
#           #     #   ####   #    # #     #  ######     #
#
#        ---   The NorNet Testbed for Multi-Homed Systems  ---
#                        https://www.nntb.no
#  =================================================================
#
#  High-Performance Connectivity Tracer (HiPerConTracer)
#  Copyright (C) 2015-2020 by Thomas Dreibholz
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#  Contact: dreibh@simula.no

import atexit
import bson.json_util
import datetime
import io
import ipaddress
import json
import os
import re
import readline
import sys

import AtlasMNS
import AtlasMNSLogger
import AtlasMNSTools


# ###### List measurement runs ##############################################
def addMeasurementRunsFromJSON(atlasMNS, jsonName):
   # ====== Parse JSON ======================================================
   try:
      jsonFile = open(jsonName, 'r')
   except Exception as e:
      print('Unable to open input file: ' + str(e))
      return False

   try:
      jsonData = json.load(jsonFile)
   except Exception as e:
      print('Unable to read JSON: ' + str(e))
      return False

   # print(json.dumps(jsonData, indent=3, sort_keys=True))

   # ====== Create measurements from JSON ===================================
   for dryRun in [ True, False ]:
      for i in range(0, len(jsonData)):
         try:
            agentHostIP       = ipaddress.ip_address(jsonData[i]['agentHostIP'])
            agentTrafficClass = int(jsonData[i]['agentTrafficClass'])
            agentFromIP       = ipaddress.ip_address(jsonData[i]['agentFromIP'])
            probeID           = int(jsonData[i]['probeID'])

            if dryRun == False:
               print('=>', agentHostIP, agentTrafficClass, agentFromIP, probeID)
               atlasMNS.addMeasurementRun(agentHostIP, agentTrafficClass, agentFromIP, probeID)

         except Exception as e:
            print('Bad entry #' + str(i + 1) + ': ' + str(e))
            return False

   return True


# ###### Print measurement runs #############################################
def printMeasurementRuns(rows, indent = '* '):
   sys.stdout.write(' ' * len(indent))
   sys.stdout.write('{0:>8s} {1:>8s} {2:>36s} {3:>36s} {4:>24s} {5:>2s} {6:>24s} {7:>16s} {8:>10s} {9:>6s} {10:>26s} {11:s}\n'.format(
      'ID', 'ProbeID', 'ProbeHostIP', 'ProbeFromIP', 'AgentHostIP', 'TC', 'AgentFromIP',
      'State', 'ProbeMsmID', 'Probe₡', 'AgentMsmTime', 'Info'
   ))
   for row in rows:
      # print(row)
      sys.stdout.write(indent)
      sys.stdout.write("{0:8d} {1:8s} {2:>36s} {3:>36s} {4:>24s} {5:02x} {6:>24s} {7:>16s} {8:10s} {9:6d} {10:>26s} {11:s}\n".format(
         row['Identifier'],
         AtlasMNSTools.valueOrNoneString(row['ProbeID']),
         AtlasMNSTools.valueOrNoneString(row['ProbeHostIP']),
         AtlasMNSTools.valueOrNoneString(row['ProbeFromIP']),
         AtlasMNSTools.valueOrNoneString(row['AgentHostIP']),
         row['AgentTrafficClass'],
         AtlasMNSTools.valueOrNoneString(row['AgentFromIP']),
         AtlasMNSTools.valueOrNoneString(row['State']),
         AtlasMNSTools.valueOrNoneString(row['ProbeMeasurementID']),
         row['ProbeCost'],
         AtlasMNSTools.valueOrNoneString(row['AgentMeasurementTime']),
         AtlasMNSTools.valueOrNoneString(row['Info']).strip()
      ))


# ###### Check status of measurement run ####################################
def checkMeasurementRun(atlasMNS, identifier):
   rows = atlasMNS.querySchedule(identifier)
   if len(rows) > 0:
      printMeasurementRuns(rows, '')
   else:
      print('No experiment run found.')


# ###### List measurement runs ##############################################
def listMeasurementRuns(atlasMNS):
   rows = atlasMNS.querySchedule()
   print('Measurements: ' + str(len(rows)))
   printMeasurementRuns(rows)


# ###### Print agents #######################################################
def printAgents(rows, indent = '* '):
   sys.stdout.write(' ' * len(indent))
   sys.stdout.write('{0:40s} {1:>32s} {2:>15s} {3:16s}\n'.format(
      'AgentHostName', 'AgentHostIP', 'LastSeen', 'Location'
   ))

   now = datetime.datetime.now()
   for row in rows:
      # print(row)
      lastSeen = int((now - row['LastSeen']).total_seconds() / 60.0)
      if lastSeen > 24*60:
         lastSeenStr = str(int(lastSeen / (24*60))) + ' days ago'
      else:
         lastSeenStr = str(lastSeen) + ' min ago'
      sys.stdout.write(indent)
      sys.stdout.write("{0:40s} {1:>32s} {2:>15s} {3:16s}\n".format(
         row['AgentHostName'].strip(),
         row['AgentHostIP'],
         lastSeenStr,
         AtlasMNSTools.valueOrNoneString(row['Location'])
      ))


# ###### List agents ########################################################
def listAgents(atlasMNS):
   rows = atlasMNS.queryAgents()
   print('Agents: ' + str(len(rows)))
   printAgents(rows)


# ###### Show results #######################################################
def showResults(atlasMNS, identifier):
   [ success, summary, ripeAtlasResults, hiPerConTracerResults ] = \
      atlasMNS.queryResults(identifier)
   if success:
      print('Summary for ID #' + str(identifier) + ':')
      print(bson.json_util.dumps(summary, indent=3, sort_keys=True))

      myProbeMeasurementID   = summary['probeMeasurementID']
      myAgentMeasurementTime = summary['agentMeasurementTime']

      # ====== Find RIPE Atlas results ======================================
      print('RIPE Atlas Results for Measurement ID #' + str(myProbeMeasurementID) + ':')
      for ripeAtlasResult in ripeAtlasResults:
         sys.stdout.write(' * ')
         atlasMNS.dumpRIPEAtlasResult(ripeAtlasResult)

      # ====== Find HiPerConTracer results ==================================
      print('HiPerConTracer Results for Measurement Time ' + str(AtlasMNSTools.timeStampToDatetime(myAgentMeasurementTime)) + ':')
      isEmpty = True
      for hiPerConTracerResult in hiPerConTracerResults:
         sys.stdout.write(' * ')
         atlasMNS.dumpHiPerConTracerResult(hiPerConTracerResult)
         isEmpty = False
         # resultStr = bson.json_util.dumps(hiPerConTracerResult, indent=3, sort_keys=True)
         # resultStr = str(hiPerConTracerResult)
         # print(' * ' + resultStr)
      if isEmpty:
         print('-- No results, yet. Note, it may take some time until next importer cronjob run! --')

   else:
      print('No results found!')


# ###### Show help ##########################################################
def showHelp():
   print('Commands Overview:')
   print('')
   print('Agent Information')
   print('* list-agents')
   print('* purge-agents [minutes]')
   print('')
   print('Measurement Scheduling')
   print('* add-measurement    agent_host_ip agent_traffic_class agent_from_ip probe_id')
   print('* check-measurement  identifier')
   print('* remove-measurement agent_host_ip agent_traffic_class agent_from_ip probe_id')
   print('* add-measurements-from-json json_file')
   print('* list-measurements')
   print('* show-results identifier')
   print('')
   print('Miscellaneous')
   print('* exit')
   print('* help')


# ###### Input completer ####################################################
class SimpleCompleter(object):

   def __init__(self, options):
      self.options = sorted(options)
      return

   def complete(self, text, state):
      if text:
         self.matches = [s for s in self.options if s and s.startswith(text) ]
      else:
         self.matches = self.options[:]
      try:
         response = self.matches[state]
      except IndexError:
         response = None
      return response



# ###### Main program #######################################################

# ====== Initialise =========================================================
if len(sys.argv) == 1:
   configurationFile = os.path.expanduser('~/.atlasmns-database-configuration')
else:
   configurationFile = sys.argv[1]

atlasMNSLogger = AtlasMNSLogger.AtlasMNSLogger(AtlasMNSLogger.TRACE)
atlasMNS = AtlasMNS.AtlasMNS()
if not atlasMNS.loadConfiguration(configurationFile):
   sys.exit(1)

if not atlasMNS.connectToSchedulerDB():
   sys.exit(1)

if not atlasMNS.connectToResultsDB():
   sys.exit(1)


# ====== Initialise GNU Readline for comfortable input ======================
histfile = os.path.join(os.path.expanduser("~"), ".atlasmns-trace-controller_history")
try:
   readline.read_history_file(histfile)
   readline.set_history_length(1000)
except FileNotFoundError:
   pass

atexit.register(readline.write_history_file, histfile)
readline.parse_and_bind('set editing-mode vi')
readline.set_completer(SimpleCompleter([
   'list-agents',
   'purge-agents',
   'add-measurement',
   'check-measurement',
   'remove-measurement',
   'add-measurements-from-json',
   'list-measurements',
   'show-results',
   'exit',
   'help'
]).complete)
readline.set_completer_delims('\t')
readline.parse_and_bind('tab: complete')


# ====== Main loop ==========================================================
AtlasMNSLogger.info('Controller is ready!')
while True:
    try:
       line = input('? ')
    except EOFError:
        break

    argv = re.split('\s+', line)
    if len(argv) > 0:

       # ------ Newline -----------------------------------------------------
       if argv[0].strip() == '':
          continue

       # ------ Comment -----------------------------------------------------
       elif argv[0].strip().startswith('#'):
          continue

       # ------ "exit" ------------------------------------------------------
       elif argv[0] == 'exit':
          break

       # ------ "exit" ------------------------------------------------------
       elif ((argv[0] == 'help') or (argv[0] == '?')):
          showHelp()

       # ------ "exit" ------------------------------------------------------
       elif argv[0] == 'show-results':
          if len(argv) >= 2:
             try:
                identifier = int(argv[1])
             except Exception as e:
                print('Bad parameter for ' + argv[0] + ' given: ' + str(e) + '!')

             showResults(atlasMNS, identifier)
          else:
             print('Too few arguments for ' + argv[0] + ' given!')

       # ------ "list-measurements" -----------------------------------------
       elif argv[0] == 'list-measurements':
          listMeasurementRuns(atlasMNS)

       # ------ "list-agents" -----------------------------------------------
       elif argv[0] == 'list-agents':
          listAgents(atlasMNS)

       # ------ "purge-agents" ----------------------------------------------
       elif argv[0] == 'purge-agents':
          seconds = 24 * 3600
          if ((len(argv) >= 2) and (argv[1].strip() != "")):
             try:
                seconds = int(60 * float(argv[1]))
             except Exception as e:
                print('Bad parameter for ' + argv[0] + ' given: ' + str(e) + '!')
                continue

          print('Purging agents last seen more than ' + str(seconds / 60) + ' min ago ...')
          atlasMNS.purgeAgents(seconds)

       # ------ "add-measurement" and "remove-measurement" ------------------
       elif ((argv[0] == 'add-measurement') or
             (argv[0] == 'remove-measurement')):
          if len(argv) >= 5:
             try:
                agentHostIP       = ipaddress.ip_address(argv[1])
                agentTrafficClass = int(argv[2])
                agentFromIP       = ipaddress.ip_address(argv[3])
                probeID           = int(argv[4])
             except Exception as e:
                print('Bad parameter for ' + argv[0] + ' given: ' + str(e) + '!')
                continue

             if argv[0] == 'add-measurement':
                atlasMNS.addMeasurementRun(agentHostIP, agentTrafficClass, agentFromIP, probeID)
             elif argv[0] == 'remove-measurement':
                atlasMNS.removeMeasurementRun(agentHostIP, agentTrafficClass, agentFromIP, probeID)
             else:
                raise ValueError('Unexpected command')

          else:
             print('Too few arguments for ' + argv[0] + ' given!')

       # ------ "add-measurements-from-json" --------------------------------
       elif (argv[0] == 'add-measurements-from-json'):
          if len(argv) >= 2:
             addMeasurementRunsFromJSON(atlasMNS, argv[1])
          else:
             print('Too few arguments for ' + argv[0] + ' given!')

       # ------ "check-measurement" -----------------------------------------
       elif (argv[0] == 'check-measurement'):
          if len(argv) >= 2:
             try:
                identifier = int(argv[1])
             except Exception as e:
                print('Bad parameter for ' + argv[0] + ' given: ' + str(e) + '!')
                continue
             checkMeasurementRun(atlasMNS, identifier)
          else:
             print('Too few arguments for ' + argv[0] + ' given!')

       # ------ Unknown command ---------------------------------------------
       else:
          print('Unknown command: ' + argv[0])

print()


# ====== All done! ==========================================================
AtlasMNSLogger.info('Exiting!')
