#!/usr/bin/env python3
# Copyright (c) 2011 Arista Networks, Inc.  All rights reserved.
# Arista Networks, Inc. Confidential and Proprietary.

# pylint: disable=consider-using-f-string
# pylint: disable=consider-using-sys-exit

import sys, optparse, re # pylint: disable=deprecated-module
import calendar
from datetime import datetime, date, time, timedelta

# Disable pylint warning for multi-line statements
# pylint: disable-msg=C0321

usage = "Usage: %prog [options] LogFile"
timeFormat = "[[yyyy] [Mmm] [d|dd]] | [yyyy-mm-dd] [hh:mm:ss]\n"
# dict { "Jan":1, "Feb":2, "Mar:3", ... }
monthMap = {   calendar.month_abbr[ i ]: i
             for i in range( 1, 13 )  }

# Parse the timestamp string input by user into a datetime object
def parseDate( dateStr ):
   dtStr = ""
   dtFormat = ""
   yearSeen = monthSeen = daySeen = hmsSeen = ymdSeen = False

   year = str( date.today().year )
   month = date.today().month
   month = calendar.month_abbr[ month ]
   day = str( date.today().day )
   hms = None

   dateFields = dateStr.split()
   for dateField in dateFields:
      if re.match( r'\d{4}$', dateField ) is not None:
         if yearSeen:
            print( "Error: Year specified more than once in '%s'\n" % ( dateStr ) )
            return None
         year = dateField
         yearSeen = True
      elif re.match( "[a-zA-Z]{3}$", dateField ) is not None:
         if monthSeen:
            print( "Error: Month specified more than once in '%s'\n" % ( dateStr ) )
            return None
         month = dateField
         monthSeen = True
      elif re.match( r'\d{1,2}$', dateField ) is not None:
         if daySeen:
            print( "Error: Day specified more than once in '%s'\n" % ( dateStr ) )
            return None
         day = dateField
         daySeen = True
      elif re.match( r'\d{1,2}:\d{1,2}:\d{1,2}$', dateField ) is not None:
         if hmsSeen:
            print( "Error: HH:MM:SS specified more than once in '%s'\n" %
                     ( dateStr ) )
            return None
         hms = dateField
         hmsSeen = True
      else:
         m = re.match( r'(\d{4})-(\d{1,2})-(\d{1,2})$', dateField )
         if m is not None:
            if yearSeen or monthSeen or daySeen:
               print( "Error: yyyy-mm-dd combined with another "
                      "specification of Year, Month or Day in '%s'\n" % ( dateStr ) )
               return None
            if ymdSeen:
               print( "Error: yyyy-mm-dd specified more than once "
                      "in '%s'\n" % ( dateStr ) )
               return None
            ymdSeen = True
            year = m.group( 1 )
            if int( m.group( 2 ) ) > 12:
               print( "Error: Invalid Month '%s' in '%s'\n" %
                       ( m.group( 2 ), dateStr ) )
               return None
            month = calendar.month_abbr[ int( m.group( 2 ) ) ]
            day = m.group( 3 )
         else:
            print( "Error: Date '%s' does not match format %s" %
                   ( dateStr, timeFormat ) )
            return None

   dtStr = dtStr + year + "-"
   dtFormat = dtFormat + "%Y-"

   dtStr = dtStr + month + "-"
   dtFormat = dtFormat + "%b-"

   dtStr = dtStr + day
   dtFormat = dtFormat + "%d"

   if hms is not None:
      dtStr = dtStr + " " + hms
      dtFormat = dtFormat + " %H:%M:%S"
   try:
      dt = datetime.strptime( dtStr, dtFormat )
   except ValueError:
      print( "Error: Invalid time '%s'\n" % ( dtStr ) )
      dt = None
   return dt

# Compare two dates of the form "Mmm dd hh:mm:ss".
# Dont bother with basic checks as the date comes from log messages
# raised by rsyslog.
def dateGreater( date1, date2, orEqual=True ):
   month1str, day1str, time1str = date1.split()
   month2str, day2str, time2str = date2.split()

   m1 = monthMap.get( month1str )
   m2 = monthMap.get( month2str )
   if m1 is None or m2 is None:
      return False
   d1 = int( day1str )
   d2 = int( day2str )

   hms = time1str.split(':')
   time1 = time( int( hms[ 0 ] ), int ( hms[ 1 ] ), int ( hms[ 2 ] ) )
   hms = time2str.split(':')
   time2 = time( int( hms[ 0 ] ), int ( hms[ 1 ] ), int ( hms[ 2 ] ) )

   if m1 == m2:
      if d1 == d2:
         if orEqual:
            return time1 >= time2
         else:
            return time1 > time2
      else:
         if orEqual:
            return d1 >= d2
         else:
            return d1 > d2
   return m1 > m2

def main():

   parser = optparse.OptionParser( usage=usage, add_help_option=False )
   parser.add_option( '--help', action='help',
                      help='Show this message and exit' )
   parser.add_option( '-b', '--begin',
                      help='Start time, specified as ' + timeFormat )
   parser.add_option( '-e', '--end',
                      help='End time, specified as ' + timeFormat )
   parser.add_option( '-s', '--seconds', type='int',
                      help='display log messages in last N seconds' )
   parser.add_option( '-m', '--minutes', type='int',
                      help='display log messages in last N minutes' )
   parser.add_option( '-h', '--hours', type='int',
                      help='display log messages in last N hours' )
   parser.add_option( '-d', '--days', type='int',
                      help='display log messages in last N days' )
   parser.add_option( '-q', '--quiet', action='store_true', default=False,
                      help='Suppress verbose error messages' )

   options, args = parser.parse_args()

   if len( args ) > 1:
      print( "Error: Unexpected LogFile arg: " + " ".join( args ) )
      if not options.quiet: parser.print_help()
      exit()

   range_opt_count = 0
   dt_start = dt_end = None
   if options.begin is not None:
      range_opt_count = range_opt_count + 1
      dt_start = parseDate( options.begin )

      if dt_start is None:
         if not options.quiet: parser.print_help()
         exit()

   if options.end is not None:
      range_opt_count = range_opt_count + 1
      dt_end = parseDate( options.end )

      if dt_end is None:
         if not options.quiet: parser.print_help()
         exit()

   last_opt_count = 0
   if options.seconds is not None:
      last_opt_count = last_opt_count + 1
      try:
         time_delta = timedelta( seconds=options.seconds )
      except (TypeError, ValueError, OverflowError):
         print( "Invalid seconds '" + str(options.seconds) + "'\n" )
         if not options.quiet: parser.print_help()
         exit()
   if options.minutes is not None:
      last_opt_count = last_opt_count + 1
      try:
         time_delta = timedelta( minutes=options.minutes )
      except (TypeError, ValueError, OverflowError):
         print( "Invalid minutes '" + str(options.minutes) + "'\n" )
         if not options.quiet: parser.print_help()
         exit()
   if options.hours is not None:
      last_opt_count = last_opt_count + 1
      try:
         time_delta = timedelta( hours=options.hours )
      except (TypeError, ValueError, OverflowError):
         print( "Invalid hours '" + str(options.hours) + "'\n" )
         if not options.quiet: parser.print_help()
         exit()
   if options.days is not None:
      last_opt_count = last_opt_count + 1
      try:
         time_delta = timedelta( days=options.days )
      except (TypeError, ValueError, OverflowError):
         print( "Invalid days '" + str(options.days) + "'\n" )
         if not options.quiet: parser.print_help()
         exit()

   if options.begin is None and options.end is None and last_opt_count == 0:
      print( "Error: Either (-b, -e) or (-s, -m, -h, -d) options "
               "must be specified\n" )
      if not options.quiet: parser.print_help()
      exit()

   if range_opt_count and last_opt_count:
      print ( "Error: Option sets (-b, -e) and (-s, -m, -h, -d) are "
              "mutually exclusive\n" )
      if not options.quiet: parser.print_help()
      exit()

   if last_opt_count > 1:
      print( "Error: Options [-s] [-m] [-h] and [-d] are mutually exclusive\n" )
      if not options.quiet: parser.print_help()
      exit()

   if last_opt_count == 1:
      dt_end = datetime.now()
      dt_start = dt_end - time_delta

   if dt_start is not None and dt_end is not None and dt_start > dt_end:
      print( "Error: Start time (%s) must be earlier than end time (%s)\n" %
            ( datetime.strftime( dt_start, '%Y %b %d %H:%M:%S' ),
              datetime.strftime( dt_end, '%Y %b %d %H:%M:%S' ) ) )

      if not options.quiet:
         parser.print_help()
      exit()

   if args:
      logFile = args[ 0 ]
      try:
         fd = open( logFile ) # pylint: disable=consider-using-with
      except OSError:
         print( "Error: Unable to open File: " + logFile + "\n" )
         exit()
   else:
      fd = sys.stdin
      logFile = "<stdin>"

   # pylint: disable=too-many-nested-blocks
   try:
      dt_prev = None
      raise_warn = raise_err = 0
      dt_start_str = dt_end_str = None
      year_start_str = year_end_str = None
      excludeRange = False
      dateRangeOverYear = False

      # Convert the date input by user into "Mmm dd", ignoring the year
      if dt_start:
         dt_start_str = datetime.strftime( dt_start, "%b %d %H:%M:%S" )
         year_start_str = datetime.strftime( dt_start, "%Y" )

      if dt_end:
         dt_end_str = datetime.strftime( dt_end, "%b %d %H:%M:%S" )
         year_end_str = datetime.strftime( dt_end, "%Y" )

      # Following checks/vars used only when timestamp does not have year
      if dt_start and dt_end:
         year_start = int( year_start_str )
         year_end = int( year_end_str )
         if year_start != year_end:
            # Date range includes full year. So if timestamp has no year
            # we just print all logs that has no year in timestamp
            dateRangeOverYear = year_end - year_start > 1
            # For Nov 12 2011 - Mar 12 2012, split filter ranges as
            # Nov 12 - Dec 31 and Jan 1 - Mar 12. or exclude all logs
            # that are between Mar 12 and Nov 12.
            excludeRange = not dateRangeOverYear

      # BUG148975 - Using just the leap day 29 Feb without any year causes
      # datetime.strptime() to report validation errors because internally the
      # default year is set to 1900.
      # To workaround the issue, passing the default year of 1904 explicitly
      # to datetime.strptime() where ever year is missing.
      defaultYear = "1904"
      dateFormat = "%Y %b %d %H:%M:%S"
      for logMessage in fd:
         logMessage = logMessage.rstrip()
         logFields = logMessage.split()
         if not logFields:
            continue
         if len( logFields[ 0 ] ) == 3:
            # Timestamp is in RFC 3164 format (Mmm dd hh:mm:ss)

            # Use only the month and day for filtering, ignore the year.
            # This will result in including logs from previous year(s)
            monthDay = " ".join( logFields[ 0:3 ] )

            # Validate the date string in the log
            try:
               dt = datetime.strptime( defaultYear + " " + monthDay,
                  dateFormat )
            except ( ValueError, TypeError ):
               raise_err = 1
               date_err = monthDay
               # Ignore the log entry and keep going
               continue

            # Warn user if we detect year roll over
            if dt_prev and dateGreater( dt_prev, monthDay, orEqual=False ):
               raise_warn = 1
            dt_prev = monthDay

            # Filter the log
            if not dateRangeOverYear:
               if excludeRange:
                  # End date is in next year of start date
                  if ( dt_end_str and dateGreater( monthDay, dt_end_str ) and
                       dt_start_str and dateGreater( dt_start_str, monthDay ) ):
                     continue
               else:
                  # Start and end date are within same year
                  if dt_start_str and not dateGreater( monthDay, dt_start_str ):
                     continue
                  if dt_end_str and not dateGreater( dt_end_str, monthDay ):
                     continue

         elif len( logFields[ 0 ] ) == 4:
            # Timestamp is in RFE 3164 format with year displayed.
            year = logFields[ 0 ]
            # Check for year validity.
            if year_start_str is not None and year < year_start_str:
               continue
            if year_end_str is not None and year > year_end_str:
               continue
            monthDay = " ".join( logFields[ 1:4 ] )

            # Validate the date string in the log
            try:
               dt = datetime.strptime( defaultYear + " " + monthDay,
                  dateFormat )
            except ( ValueError, TypeError ):
               raise_err = 1
               date_err = monthDay
               # Ignore the log entry and keep going
               continue

            dt_prev = monthDay

            # Filter the log
            # Earlier than the start time.
            if dt_start_str and not dateGreater( monthDay, dt_start_str ):
               if year_start_str and year_start_str == year:
                  continue
            # Later than the end time.
            if dt_end_str and not dateGreater( dt_end_str, monthDay ):
               if year_end_str and year_end_str == year:
                  continue
         else:
            # Timestamp is in RFC 3339 format (yyyy-mm-ddThh:mm:ss.ffffffTZ)
            timestamp = logFields[ 0 ]
            # strptime() does not have a %format for the timezone.
            # Timezone can be '(z|Z) | + | - hh:mm'
            dateUtc = re.match( r'(.*)[zZ\-\+].*', timestamp )
            if dateUtc is None:
               raise_err = 1
               date_err = timestamp
               # Ignore the log entry and keep going
               continue
            try:
               dt = datetime.strptime( dateUtc.group( 1 ), "%Y-%m-%dT%H:%M:%S.%f" )
            except ( ValueError, TypeError ):
               raise_err = 1
               date_err = dateUtc.group( 1 )
               # Ignore the log entry and keep going
               continue

            # Filter the log
            if dt_start is not None and dt < dt_start:
               continue
            if dt_end is not None and dt > dt_end:
               continue
         print( logMessage )
   finally:
      if raise_warn: # pylint: disable=used-before-assignment
         print( "Warning: Results may include log messages from previous year(s) "
                "because some logs do not have a year in the timestamp." )
      if raise_err: # pylint: disable=used-before-assignment
         print( "Error: Some log messages were ignored because they had invalid "
                # pylint: disable-next=used-before-assignment
                "timestamps (%s)." % date_err )
      fd.close()

if __name__ == '__main__':
   main()
