import argparse
import boto3
import json
import os
import pytz
import re
import sys
import time

from datetime import datetime, timezone
from dateutil import parser
from tzlocal import get_localzone

from app.utils import op_dev_service, OPLogFormatter, parse_log_msg


def parse_datetime_str(value, tz):
  d = parser.parse(value)
  if (d.tzinfo is None) or (d.tzinfo.utcoffset(d) is None):
    d = (tz or get_localzone()).localize(d)
  return d


def datetime_to_timestamp(tz_aware_datetime):
  return int((tz_aware_datetime - datetime.fromtimestamp(0, tz=timezone.utc)).total_seconds() * 1000.0)


def timestamp_to_datetime(ms_since_epoch):
  return datetime.fromtimestamp(ms_since_epoch / 1000, tz=timezone.utc)


def eval_filter(filter_fn, request_data):
  try:
    return filter_fn(request_data)
  except:
    return False


def run(parser, args):
  tz = None
  if args.timezone:
    tz = pytz.timezone(args.timezone)

  now = datetime.now(timezone.utc)

  from_datetime = parse_datetime_str(args.from_datetime, tz) if args.from_datetime else now
  if (from_datetime > now):
    sys.exit("The value for '--from' cannot be in the future")

  to_datetime = parse_datetime_str(args.to_datetime, tz) if args.to_datetime else None
  if to_datetime and (to_datetime > now):
    sys.exit("The value for '--to' cannot be in the future (omit it to stream logs)")

  if to_datetime and (to_datetime < from_datetime):
    sys.exit("The value for '--to' cannot come before the value for '--from'")

  time_format = None
  if not args.iso_format:
    # Infer the time format based on the range in question - if we're only looking at a few minutes around now, it's
    # much simpler to read a local time rather than a full ISO string
    today = now.date()
    from_date = from_datetime.astimezone(None).date()
    to_date = to_datetime.astimezone(None).date() if to_datetime else None
    if (from_date == today):
      # Dealing with events from today (presumably we know what day it is...)
      time_format = '%-I:%M:%S %p'
    elif (today - from_date).days < 7:
      # Dealing with events from days in the past week, so the day is sufficient to distinguish them
      time_format = '%A %-I:%M:%S %p'
    else:
      # Dealing with events from beyond the past week, so we need the actual date (but the year is probably overkill)
      time_format = '%-m/%-d %-I:%M:%S %p'

  credentials = op_dev_service.call('get-log-credentials', { 'url': args.url })
  session = boto3.Session(**credentials['aws_config'])
  logs = session.client('logs')

  if args.filter_requests:
    if args.filter_requests == True:
      filter_fn = lambda x: True
    else:
      filter_fn = eval(args.filter_requests)

  formatter = OPLogFormatter(tz=tz, time_format=time_format)
  from_timestamp = datetime_to_timestamp(from_datetime)
  while True:
    kwargs = { 'logGroupName': credentials['log_group'], 'startTime': from_timestamp, 'interleaved': True }
    if to_datetime:
      kwargs['endTime'] = datetime_to_timestamp(to_datetime)
    for response in logs.get_paginator('filter_log_events').paginate(**kwargs):
      for event in response['events']:
        t = event['timestamp']

        # TODO: this is not a perfectly reliable way to do pagination - if two messages have exactly the same
        # timestamp, it's possible that we might miss the latter message if they happen to cross a page boundary
        from_timestamp = (t + 1)

        dt = timestamp_to_datetime(t)
        msg = parse_log_msg(event['message'])

        if args.levels and (msg.get('l') not in args.levels):
          continue

        if args.name and (msg.get('n') not in args.name):
          continue

        if args.service_instance and (msg.get('i') not in args.service_instance):
          continue

        if msg.get('n') == 'op-request':
          try:
            request_data = json.loads(msg['m'])
          except:
            msg['n'] = 'op-request:legacy'

        if args.filter_requests:
          if msg.get('n') != 'op-request':
            continue
          if not eval_filter(filter_fn, { 't': dt, 'si': msg['i'], **request_data }):
            continue

        if args.format_requests:
          if msg.get('n') == 'op-request':
            if request_data['_'] == 0:
              # If we're using a custom formatter, then disregard the "Started request" messages
              continue
            # Make sure we have all the properties available so the filter formatting string doesn't break
            complete_request_data = {
              'c': None,
              'p': None,
              'pv': None,
              'i': None,
              'iv': None,
              'o': None,
              **request_data,
              't': formatter.format_time(dt),
              'si': msg['i']
            }
            print(args.format_requests % complete_request_data)
            continue

        print(formatter.format(dt, msg))

    if to_datetime:
      break

    time.sleep(3)
