import boto3
from botocore.config import Config
import logging
import datetime
import sys
import re
import os


config = Config(
    retries={
        'max_attempts': 10,
        'mode': 'adaptive'
    }
)

logs = boto3.client('logs', config=config)

live_interval = 3  # seconds

logging.basicConfig()
logger = logging.getLogger('cloudwatch_logs')
level = logging.getLevelName(os.getenv('LOGGING_LEVEL', "WARNING"))
logger.setLevel(level)  # override boto3 logging configuration
logging.getLogger('boto3').setLevel(logging.ERROR)
logging.getLogger('boto3').propagate = False
logging.getLogger('botocore').setLevel(logging.ERROR)
logging.getLogger('botocore').propagate = False


class LogStreamNotFoundError(Exception):
    pass


class TooManyLogStreamsMatched(Exception):
    pass


def logs_cat(log_group_name='', interleaved=True, log_stream_names=None, log_stream_name_prefix=None,
             start_time=None, end_time=None,
             filter_pattern='', print_log_stream_names=False, no_timestamp=False):
    """
    Function retrieves logs and prints to stdout
    """

    if log_stream_names is None:
        log_stream_names = []

    params = {
        'logGroupName': log_group_name,
        'interleaved': interleaved
    }

    if log_stream_name_prefix:
        params['logStreamNamePrefix'] = log_stream_name_prefix
    elif len(log_stream_names) > 0:
        params['logStreamNames'] = log_stream_names

    if start_time is not None:
        params['startTime'] = int(start_time.timestamp()) * 1000

    if end_time is not None:
        params['endTime'] = int(end_time.timestamp()) * 1000

    if len(filter_pattern) > 0:
        params['filterPattern'] = filter_pattern

    last_stream_name = None
    debug_call_start = datetime.datetime.now()
    debug_call_end = None
    paginator = logs.get_paginator('filter_log_events')
    iterator = paginator.paginate(**params)

    for page in iterator:
        if debug_call_end is None:
            debug_call_end = datetime.datetime.now()
            logger.debug("logs.filter_log_events({}) duration: {}".format(
                params, debug_call_end - debug_call_start))

        for event in page['events']:
            if print_log_stream_names and (last_stream_name is None or last_stream_name != event['logStreamName']):
                print("====== {} ======".format(event['logStreamName']))
                last_stream_name = event['logStreamName']
            if no_timestamp:
                print(event['message'].rstrip())
            else:
                print("{}: {}".format(datetime.datetime.fromtimestamp(
                    event['timestamp'] / 1000, datetime.timezone.utc), event['message'].rstrip()))

        sys.stdout.flush()


def logs_tail(log_group_name='', interleaved=True, log_stream_names=None, log_stream_name_prefix=None,
              start_time=None, filter_pattern='', last_event=None, seen_events=None,
              print_log_stream_names=False, tail=None, no_timestamp=False):
    """
    Function retrieves logs and prints to stdout. It returns last event object,
    which can be passed into next call to simulate tail command
    """

    if log_stream_names is None:
        log_stream_names = []

    params = {
        'logGroupName': log_group_name,
        'interleaved': interleaved
    }

    if log_stream_name_prefix:
        params['logStreamNamePrefix'] = log_stream_name_prefix
    elif len(log_stream_names) > 0:
        params['logStreamNames'] = log_stream_names

    if last_event is not None:
        params['startTime'] = last_event['timestamp'] - 3 * live_interval * 1000
    elif start_time is not None:
        params['startTime'] = int(start_time.timestamp()) * 1000

    if len(filter_pattern) > 0:
        params['filterPattern'] = filter_pattern

    tail_events = []

    tail_last_stream_name = (
        last_event['logStreamName'] if last_event is not None else None)
    last_stream_name = (last_event['logStreamName']
                        if last_event is not None else None)

    if seen_events:
        last_seen_event_minute = int(last_event['timestamp'] / 1000 / 60)
        del_seen_event_minute = None
        for seen_event_minute in seen_events:
            if int(seen_event_minute) < last_seen_event_minute - 3:
                del_seen_event_minute = seen_event_minute
                break

        if del_seen_event_minute:
            del seen_events[seen_event_minute]

    paginator = logs.get_paginator('filter_log_events')
    debug_call_start = datetime.datetime.now()
    debug_call_end = None
    iterator = paginator.paginate(**params)

    for page in iterator:
        if debug_call_end is None:
            debug_call_end = datetime.datetime.now()
            logger.debug("logs.filter_log_events({}) duration: {}".format(
                params, debug_call_end - debug_call_start))
        for event in page['events']:
            seen = False
            for seen_event_minute in seen_events:
                for seen_event in seen_events[seen_event_minute]:
                    if event['eventId'] == seen_event:
                        seen = True
                        break
                if seen:
                    break

            if seen:
                continue
            seen_event_minute = str(int(event['timestamp'] / 1000 / 60))
            if seen_event_minute not in seen_events:
                seen_events[seen_event_minute] = []
            seen_events[seen_event_minute].append(event['eventId'])

            if (tail is None):
                if print_log_stream_names and (last_stream_name is None or last_stream_name != event['logStreamName']):
                    print("====== {} ======".format(event['logStreamName']))
                    last_stream_name = event['logStreamName']

                if no_timestamp:
                    print(event['message'].rstrip())
                else:
                    print("{}: {}".format(datetime.datetime.fromtimestamp(
                        event['timestamp'] / 1000, datetime.timezone.utc), event['message'].rstrip()))
            else:
                tail_events.append(event)

            last_event = event

        sys.stdout.flush()

    if (tail is not None):
        for event in tail_events[-tail:]:
            if print_log_stream_names and (tail_last_stream_name is None
                                           or tail_last_stream_name != event['logStreamName']):
                print("====== {} ======".format(event['logStreamName']))
                tail_last_stream_name = event['logStreamName']

            if no_timestamp:
                print(event['message'].rstrip())
            else:
                print("{}: {}".format(datetime.datetime.fromtimestamp(
                    event['timestamp'] / 1000, datetime.timezone.utc), event['message'].rstrip()))
        sys.stdout.flush()

    return last_event, seen_events


def parse_common_params(parser):
    """
    Function adds common arguments to parser and return parsed args
    """

    parser.add_argument('logGroupName', type=str,
                        help="CloudWatch logs group name")

    parser.add_argument("--debug", type=int, help="Debug level: from 1 to 10")
    parser.add_argument("--logStreamNamePrefix", type=str,
                        help="Filter logs by the LogStream name prefix")
    parser.add_argument("--logStreamNames", type=str,
                        help="Filter logs by the LogStream names, comma-separated")
    parser.add_argument("--logStreamNames_regexp", type=re.compile,
                        help="Filter logs by the LogStream names matching regular expression")
    parser.add_argument("--filterPattern", type=str, help="Filter logs by filter pattern")
    parser.add_argument("-p", "--printLogStreamNames",
                        action='store_true', help="Print LogStream names")
    parser.add_argument("-t", "--no-timestamp",
                        action='store_true', help="Do not print event timestamp")

    args = parser.parse_args()

    if args.debug is not None:

        if args.debug > 0:
            logger.setLevel(logging.DEBUG)

        if args.debug >= 10:
            logging.getLogger('botocore').setLevel(logging.DEBUG)
            logging.getLogger('boto3').setLevel(logging.DEBUG)
            logging.getLogger('nose').setLevel(logging.DEBUG)

    params = {
        'log_group_name': args.logGroupName,
        'interleaved': True,
        'log_stream_names': []
    }

    if args.logStreamNamePrefix is not None:
        params['log_stream_name_prefix'] = args.logStreamNamePrefix

    elif args.logStreamNames is not None:
        params['log_stream_names'] = args.logStreamNames.split(',')

    elif args.logStreamNames_regexp is not None:
        debug_call_start = datetime.datetime.now()
        debug_call_end = None
        paginator = logs.get_paginator('describe_log_streams')
        iterator = paginator.paginate(
            logGroupName=args.logGroupName
        )

        matched_log_streams = []
        for page in iterator:
            if debug_call_end is None:
                debug_call_end = datetime.datetime.now()
                logger.debug("logs.describe_log_streams({}) duration: {}".format(
                    params, debug_call_end - debug_call_start))
            for log_stream in page['logStreams']:
                if args.logStreamNames_regexp.match(log_stream['logStreamName']):
                    logger.info("Adding logStream {} to list".format(
                        log_stream['logStreamName']))
                    matched_log_streams.append(log_stream['logStreamName'])

        if not matched_log_streams:
            raise LogStreamNotFoundError(
                "Could not find any log stream matching --logStreamNames_regexp param")

        params['log_stream_names'] += matched_log_streams

        if len(matched_log_streams) > 100:
            raise TooManyLogStreamsMatched(
                "Number of LogStreams matched by the regular expression exceeded 100, "
                "which is the top limit that can be passed to API. "
                "Use --logStreamNamePrefix instead.")

    if args.filterPattern is not None:
        params['filter_pattern'] = args.filterPattern

    params['print_log_stream_names'] = args.printLogStreamNames

    return params


def valid_time(timespec):
    if re.match(r"^[0-9]+$", timespec):
        return datetime.datetime.fromtimestamp(int(timespec))
    return datetime.datetime.strptime(timespec, "%Y-%m-%dT%H:%M:%S%z")
