from __future__ import print_function
from builtins import range
from builtins import object
import os
import re
import sys
import json
import argparse
import platform
import datetime
import logging

logging.getLogger('paramiko.transport').setLevel(logging.WARN)

datetime_formats = ['%Y-%m-%d %H:%M:%S',
                    '%a %b %d %H:%M:%S %Y']
TB_format1 = 'Traceback (most recent call last)'
SKIP_FILES = ['ifmap', '.swp', 'traceback']
MAX_BTS = 1000


class BTTracker(object):
    '''Search for tracebacks in /var/log/contrail

    path: Optional Folder to save the collected traceback data
    log_file_prefix: Optional prefix for log file names
    ignore_tbs_input_file: path of file which has ignorable bts

    search() returns a list of dicts of format
    [ {
        'file' : '/var/log/contrail/nova-console-supervisor-stdout.log',
        'exception_name' : 'DBConnectionError',
        'exception_text' : <Full Traceback>,
        'timestamp' : timestamp of exception (datetime object),
        'line_num'  : Line number in file
     },
     ...
    ]

    'file' key would have 'contrail-api-0-stdout.log' even if the filename is
    contrail-api-0-stdout.log.1 . This is so that multiple invocations of
    search() can be used to get only the newer exceptions

    Also creates tracebacks.log, ignored_tracebacks.log,
        traceback_parse_failures.log with suitable suffixes

    '''

    def __init__(self, path='.', log_file_prefix='',
                 ignore_tbs_input_file=None):
        self.folder = '/var/log/contrail'
        hostname = platform.uname()[1]
        path = os.path.abspath(path)
        if log_file_prefix:
            log_file_prefix = log_file_prefix + '-'
        self.ignored_tb_file = '%s/%s-%signored_tracebacks.log' % (path,
                                                                   hostname, log_file_prefix)
        self.tb_file = '%s/%s-%stracebacks.log' % (path,
                                                   hostname, log_file_prefix)
        self.parse_failure_file = '%s/%s-%straceback_parse_failures.log' % (path,
                                                                            hostname, log_file_prefix)

        self.ignore_tbs_input_file = ignore_tbs_input_file
        self.ignore_errors = {}
        if ignore_tbs_input_file:
            self._read_ignore_tbs_input_file()

        self._remove_result_files()
        print('Traceback details will be in %s' % (self.tb_file))
        print('Ignored Traceback details will be in %s' % (self.ignored_tb_file))
        print('Traceback parse errors will be in %s' % (self.parse_failure_file))

    def _read_ignore_tbs_input_file(self):
        if not self.ignore_tbs_input_file:
            return
        try:
            with open(self.ignore_tbs_input_file, 'r') as f_h:
                text = f_h.read()
                self.ignore_errors = json.loads(text)
        except IOError:
            pass

    # end _read_ignore_tbs_input_file

    def _remove_result_files(self):
        result_files = [self.ignored_tb_file, self.tb_file,
                        self.parse_failure_file]
        for f in result_files:
            try:
                os.remove(f)
            except OSError:
                pass
    # end _remove_result_files

    def _get_next_file(self):
        for dirname, dirs, files in os.walk(self.folder):
            for filename in files:
                if '.log' in filename or filename.endswith('.err'):
                    should_skip = False
                    for x in SKIP_FILES:
                        if x in filename:
                            should_skip = True
                            break
                    if not should_skip:
                        path = os.path.join(dirname, filename)
                        yield path
    # end _get_next_file

    def _get_tb_end_line(self, lines, tb_char_index, line_num):
        i = line_num + 1
        while i <= len(lines):
            if lines[i][tb_char_index] == ' ':
                i += 1
                continue
            else:
                # We have reached the last line of the tb
                tb_end_line_num = i
                exception_name = lines[i][tb_char_index:].split()[0]
                exception_name = re.match(r'(.*\w)', exception_name).group()
                break
        # end while
        return (exception_name, i + 1)
    # end _get_tb_end_line

    def is_date_time_str(self, string):
        for x in datetime_formats:
            try:
                if datetime.datetime.strptime(string.strip(), x):
                    return True
            except ValueError:
                pass
        return False
    # end is_date_time_str

    def parse_date_time_str(self, string):
        for x in datetime_formats:
            try:
                return datetime.datetime.strptime(string.strip(), x)
            except ValueError:
                pass
        # Unable to parse date
        raise ValueError
    # end parse_date_time_str

    def file_write(self, filename, tb_data, mode='a'):
        with open(filename, 'a') as f_h:
            f_h.write('File: %s, Line: %s\n' % (tb_data['file'],
                                                tb_data['line_num']))
            f_h.write('Error: %s, Timestamp: %s\n' % (
                tb_data['exception_name'], tb_data['timestamp']))
            text = ''.join(tb_data['exception_text'])
            f_h.write('Details:\n %s' % (text))
            f_h.write('-' * 80 + '\n')
    # end file_write

    def tb_parse_error(self, f, lines, line_num):
        '''
        Tracebacks which could not be easily parsed
        Ex : Missing log timestamps
        '''
        with open(self.parse_failure_file, 'a') as f_h:
            f_h.write('File: %s, Line: %s\n' % (f, line_num))
            text = ''.join(lines[line_num:line_num + 10])
            f_h.write('%s...\n...\n' % (text))
            f_h.write('-' * 80 + '\n')
    # end tb_parse_error

    def _get_tb_details_format_1(self, lines, line_num, file_name):
        '''
        For cgitb backtraces
        '''
        tb_string_line = lines[line_num]
        i = line_num - 1
        exception_name = None
        ts = None
        while i >= 0:
            # Get to a string which has <class '.sldfkj.abcError'>
            if (('<class \'' in lines[i] and '\'>' in lines[i]) or
                ('<type \'' in lines[i] and '\'>' in lines[i])) and \
                    lines[i + 1].startswith('Python') and \
                    self.is_date_time_str(lines[i + 2]):
                # if (lines[i].startswith('<class \'') and
                # lines[i].endswith('\'>')):
                match = re.search(r'<(class|type) \'(.*)\'>', lines[i])
                ts = None
                if match:
                    exception_name = match.group(2)
                    ts = self.parse_date_time_str(lines[i + 2])
                    tb_char_index = 0
                else:
                    # Timestamp could in that line itself
                    match = re.match(
                        r'(.*?) (.*?) (.*?).*?<(class|type) \'(.*)\'>', lines[i])
                    if match:
                        exception_name = match.group(5)
                        ts_str = '%s %s %s' % (match.group(1),
                                               match.group(2),
                                               match.group(3))
                        if self.is_date_time_str(ts_str):
                            ts = self.parse_date_time_str(ts_str)
                        else:
                            i -= 1
                            continue
                        tb_char_index = lines[line_num].index('Traceback')
                # endif
                if ts:
                    (ignore, tb_end_line_num) = self._get_tb_end_line(lines,
                                                                      tb_char_index,
                                                                      line_num)
                    return (exception_name, ts, i, tb_end_line_num)
            # end if
            i -= 1
        # end while
        self.tb_parse_error(file_name, lines, line_num)
        return (None, None, None, None)
    # end _get_tb_details_format_1

    def _get_tb_details_format_2(self, lines, line_num, file_name):
        '''
        Regular backtraces not logged using cgitb module
        '''
        exception_name = None
        ts = None
        tb_char_index = lines[line_num].index('Traceback')
        match = re.match(r'(.*?) (.*?) .*?Traceback', lines[line_num])
        if match:
            date_str = '%s %s' % (match.group(1), match.group(2))
        else:
            self.tb_parse_error(file_name, lines, line_num)
            return (None, None, None)

        if self.is_date_time_str(date_str):
            ts = self.parse_date_time_str(date_str)
            (exception_name, tb_end_line_num) = self._get_tb_end_line(lines,
                                                                      tb_char_index,
                                                                      line_num)
        else:
            self.tb_parse_error(file_name, lines, line_num)
            return (None, None, None)
        if not (exception_name and ts and tb_char_index):
            self.tb_parse_error(file_name, lines, line_num)
            return (None, None, None)
        return (exception_name, ts, tb_end_line_num)
    # end _get_tb_details_format_2

    def can_be_ignored(self, entry):
        if not self.ignore_errors:
            return False
        ignore = True
        for item in self.ignore_errors:
            f = item.get('file')
            if f and f not in entry['file']:
                ignore = False
            exception_name = item.get('exception_name')
            if exception_name and exception_name not in entry['exception_name']:
                ignore = False
            exception_text = item.get('exception_text')
            if exception_text:
                match = any(exception_text in x for x in entry[
                            'exception_text'])
                if not match:
                    ignore = False
        return ignore
    # end can_be_ignored

    def search(self):
        result = []
        for f in self._get_next_file():
            with open(f) as f_h:
                lines = f_h.readlines()
            for i in range(0, len(lines)):
                ts = None
                line = lines[i]
                if line.startswith(TB_format1):
                    (exception_name, ts, start_index, end_index) = \
                        self._get_tb_details_format_1(lines, i, f)
                elif TB_format1 in line:
                    start_index = i
                    (exception_name, ts, end_index) = \
                        self._get_tb_details_format_2(lines, i, f)
                if ts:
                    file_prefix = re.match(r'(.*.log).*', f).group(1)
                    entry = {'file': file_prefix,
                             'line_num': i,
                             'timestamp': ts,
                             'exception_name': exception_name,
                             'exception_text': lines[start_index:end_index]
                             }
                    if not self.can_be_ignored(entry):
                        result.append(entry)
                        self.file_write(self.tb_file, entry)
                    else:
                        self.file_write(self.ignored_tb_file, entry)
                    if len(result) > MAX_BTS:
                        print('Too many backtraces...giving up')
                        sys.exit()
        print('Done looking for tracebacks')
        return result
    # end search

if __name__ == "__main__":
    ap = argparse.ArgumentParser(
        description='Search for tracebacks in /var/log/contrail')
    ap.add_argument('-p', '--path', type=str,
                    help='Optional Folder to save the collected traceback data ',
                    default='.')
    ap.add_argument('-l', '--log_file_prefix', type=str,
                    help='Optional prefix for log file names',
                    default='')
    ap.add_argument('-i', '--ignore_tbs_input_file', type=str,
                    help='path of file which has ignorable bts',
                    default='Same location as this script')
    args = ap.parse_args()
    btt = BTTracker(args.path, args.log_file_prefix,
                    args.ignore_tbs_input_file)
    result = btt.search()
