from apiclient.discovery import build
from cal.constants import GOOGLE_CALENDAR_COLORS
from cal.helpers import EventCollection, TimeNode, TimeNodeChain, ensure_timezone_awareness, get_color, get_time_series
from datetime import datetime, timedelta
from django.contrib.auth.models import User
from django.conf import settings
from django.db import models
from django.utils import timezone
from django.utils.dateparse import parse_date, parse_datetime
from jsonfield import JSONField
from oauth2client.django_orm import CredentialsField, FlowField
from oauth2client.client import AccessTokenRefreshError

import httplib2
import pytz
import sys


class InvalidParameterException(Exception):
    pass


class MalformedDataException(Exception):
    pass


class Profile(models.Model):

    user = models.OneToOneField(User)
    google_id = models.CharField(null=True, max_length=25)
    picture_url = models.URLField(null=True, blank=True)
    locale = models.CharField(max_length=10, default='en')
    main_calendar = models.ForeignKey("GCalendar", null=True)
    authed = models.BooleanField(default=False, help_text="If the user's oauth credentials are currently valid")
    private_event_names = models.BooleanField(default=False, help_text="If the user's event names will return as dummy text.")
    analysis_start = models.DateTimeField(null=True, blank=True, help_text="When the analysis of the user's calendar will start")

    created_at = models.DateTimeField(auto_now_add=True)
    modified_at = models.DateTimeField(auto_now=True)

    @classmethod
    def get_or_create(cls, user):
        created = False
        try:
            profile = cls.objects.get(user=user)
        except cls.DoesNotExist:
            # Create a profile
            created = True
            profile = cls(user=user)
            profile.save()
        return profile, created

    def __str__(self):
        return "{}'s profile".format(self.user)

    def get_calendars_for_calendarids(self, calendar_ids=None):
        """
        Given a list of string calendar_id's, will always return a list of
        GCalendars on which you can execute a query.
        """
        calendars = []
        if calendar_ids:
            for calendar_str in calendar_ids:
                try:
                    c = GCalendar.objects.get(calendar_id=calendar_str, user=self.user)
                except GCalendar.DoesNotExist:
                    raise InvalidParameterException("Provided calendar {} does not exist".format(calendar_str))
                if c.user != self.user:
                    raise InvalidParameterException("That calendar doesn't belong to you!")
                calendars.append(c)
        else:
            # TODO take the default from preferences
            # TODO if no preferences specified, use main calendar
            calendars = GCalendar.objects.filter(user=self.user)
            if not calendars:
                raise Exception("User must sync")

        return calendars

    def create_calendars(self, only_primary=True):
        if only_primary and self.main_calendar:
            return False
        self.user.googlecredentials.import_calendars(only_primary)
        return self.main_calendar

    def clear_credentials(self):
        qs = GoogleCredentials.objects.filter(user=self.user)
        if qs:
            qs.last().delete()
        self.authed = False
        self.save()

    def generate_categories(self):
        """
        Generates a Category for every permutation of color and calendar that exists
        """
        def create_category_if_nonexistent(color_index, gcalendar=None):
            try:
                Category.objects.get(color_index=color_index, calendar=gcalendar)
            except Category.DoesNotExist:
                label = color_index
                if gcalendar:
                    label = gcalendar.summary
                cc = Category(
                                   calendar=gcalendar,
                                   user=self.user,
                                   color_index=color_index,
                                   label=label,
                                   )
                # Only create this color category if there are meaningful stats
                # for it
                if len(cc.query()) > 0:
                    cc.save()
                    print "Created color category {}".format(cc)

        # Create Color Categories
        for key in GEvent.EVENT_COLORS_KEYS:
            # Skip default colors
            if key == "1":
                continue
            qs = GEvent.objects.filter(calendar__user=self.user,
                                      color_index=key,
                                      all_day_event=False
                                      )
            if qs.count() > 0:
                create_category_if_nonexistent(color_index=key)

        # Create Calendar Categories
        for calendar in GCalendar.objects.filter(user=self.user):
            qs = GEvent.objects.filter(calendar__user=self.user,
                                       all_day_event=False)
            if qs.count() > 0:
                create_category_if_nonexistent(color_index="1", gcalendar=calendar)


class GCalendar(models.Model):

    """
    Represents a Google Calendar. The GEvents associated with this are designed to maintain
    the state of the User's Google Calendar.
    """

    CALENDAR_COLORS_KEYS = sorted(GOOGLE_CALENDAR_COLORS['event'].keys(), key=lambda x: int(x))
    CALENDAR_COLORS_TUPLES = [(k, GOOGLE_CALENDAR_COLORS['event'][k]['background']) for k in CALENDAR_COLORS_KEYS]

    user = models.ForeignKey(User, related_name='gcalendars')
    color_index = models.CharField(max_length=10, blank=False, choices=CALENDAR_COLORS_TUPLES)
    calendar_id = models.CharField(max_length=250)
    summary = models.CharField(max_length=250, help_text="Title of the calendar")
    next_sync_token = models.CharField(max_length=100, null=True, blank=True, help_text="The syncToken provided for this specific calendar")
    meta = JSONField(default="{}", blank=True)

    enabled_by_default = models.BooleanField(default=True)

    def __str__(self):
        return "{}'s calendar {}".format(self.user, self.calendar_id)

    @property
    def color(self):
        color = GOOGLE_CALENDAR_COLORS['calendar'].get(self.color_index)
        if color:
            return color
        else:
            # This handles when data isn't consistent for some reason.
            print "Warning: GEvent '{}' with id {} has an incorrect color_index value of '{}'".format(self.name, self.id, self.color_index)
            return GOOGLE_CALENDAR_COLORS['calendar']['1']

    def api_event_to_gevent(self, event):
        if event.get('status', 'confirmed') in set(['confirmed', 'tentative']):
            # Create or update the event

            try:
                g = GEvent.objects.get(google_id=event['id'])
            except GEvent.DoesNotExist:
                g = GEvent()
            g.name = event.get('summary', '')
            if event['start'].get('dateTime'):
                # This is a date time
                g.start = parse_datetime(event['start']['dateTime'])
                g.end = parse_datetime(event['end']['dateTime'])
            else:
                # This is a date, convert it to a datetime starting/ending at midnight
                g.start = datetime.combine(parse_date(event['start']['date']), datetime.min.time())
                g.end = g.start + timedelta(days=1)
                g.all_day_event = True
            g.location = event.get('location', '')

            g.updated = parse_datetime(event['updated'])

            try: # Deal with faulty creation dates (i.e. '0000-12-29T00:00:00.000Z')
                # Sometimes Google is stupid and fails to return the mandatory 'created' field
                if event.get('created'):
                    g.created = parse_datetime(event['created'])
                else:
                    g.created = g.updated
            except ValueError:
                g.created = g.updated

            g.calendar = self
            g.google_id = event['id']
            g.i_cal_uid = event['iCalUID']
            g.color_index = event.get('colorId', '')
            g.description = event.get('description', '')
            g.status = event.get('status', 'confirmed')
            g.transparency = event.get('transparency', 'opaque')
            g.all_day_event = True if event['start'].get('date', None) else False
            if not g.all_day_event:
                # Some events don't have timezones
                g.timezone = event['start'].get('timeZone')
            g.end_time_unspecified = event.get('endTimeUnspecified', False)
            g.recurrence = str(event.get('recurrence', ''))
            g.recurring_event_id = event.get('recurringEventId', '')
            g.save()
            if settings.VERBOSE_PRINT:
                print "Saved {} event: {}".format(g.start, g.name)
            return g

        else:
            if settings.VERBOSE_PRINT:
                print "Deleting event {}".format(event['id'])

            # Status is cancelled, create a DeletedEvent

            # 'event' looks like this:
            # {
            #     u'status': u'cancelled',
            #     u'kind': u'calendar#event',
            #     u'originalStartTime': {u'dateTime': u'2014-10-23T11:00:00-07:00'},
            #     u'etag': u'"2827884681552000"',
            #     u'recurringEventId': u'bpbt1o1k55c4hnv9ig9uet69ns',
            #     u'id': u'bpbt1o1k55c4hnv9ig9uet69ns_20141023T180000Z'
            # }
            if event.get('originalStartTime'):
                if event['originalStartTime'].get('dateTime'):
                    # This is a date time
                    original_start_time = parse_datetime(event['originalStartTime']['dateTime'])
                else:
                    # This is a date, convert it to a datetime
                    original_start_time = datetime.combine(parse_date(event['originalStartTime']['date']), datetime.min.time())
            else:
                original_start_time = None

            DeletedEvent.objects.get_or_create(
                calendar=self,
                google_id=event['id'],
                original_start_time=original_start_time,
                recurring_event_id=event.get('recurringEventId', ''))

    def sync(self, full_sync=False):
        """
        Syncs this calendar.
        Assumes that the list of calendars for this profile is correct
        """

        print "Syncing calendar {}".format(self.summary.encode('utf-8').strip())
        result = None
        creds = self.user.googlecredentials
        service = creds.get_service()

        default_list_args = {
                'calendarId': self.calendar_id,
                'singleEvents': True,
                'maxResults': 2500,
                }
        list_args_with_constraints = {
                # Only sync up to two months in the future
                'timeMax': ensure_timezone_awareness(datetime.now() + timedelta(days=60)).isoformat(),
                }
        list_args_with_constraints.update(default_list_args)

        next_page_token = None
        if full_sync:
            # Full sync - initial request without sync token or page token
            result = service.events().list(**list_args_with_constraints).execute()
            old_events = GEvent.objects.filter(calendar=self)
            for event in old_events:
                event.delete()
            # Delete the DeletedEvents and get a fresh copy from Google
            deleted_events = DeletedEvent.objects.filter(calendar=self)
            for event in deleted_events:
                event.delete()
        else:
            # Incremental sync, initial request needs syncToken
            try:
                # Google API doesn't accept constraints for the first request in incremental sync
                result = service.events().list(syncToken=self.next_sync_token, **default_list_args).execute()
            except Exception as e:
                t, v, tb = sys.exc_info()
                if hasattr(e, 'resp') and e.resp.status == 410:
                    # Sync token is no longer valid, perform full sync
                    print "Sync token is no longer valid, perform full sync for calendar {}".format(self.summary.encode('utf-8').strip())
                    result = service.events().list(**list_args_with_constraints).execute()
                else:
                    raise t, v, tb

        # Run the first iteration, for the first request
        for item in result['items']:
            self.api_event_to_gevent(item)

        # Paginate through the rest, if applicable
        while True:
            next_page_token = result.get('nextPageToken')

            if not next_page_token:
                # We've reached the last page. Store the sync token.
                self.next_sync_token = result['nextSyncToken']
                self.save()
                break

            result = service.events().list(pageToken=next_page_token, **list_args_with_constraints).execute()

            # Assume at this point it's a correctly formatted event
            for item in result['items']:
                self.api_event_to_gevent(item)

        # Make this calendar consistent with existing DeletedEvents
        deleted_events = DeletedEvent.objects.filter(calendar=self)
        for d_event in deleted_events:
            d_event.apply()

        print "Successfully synced calendar {}".format(self.summary.encode('utf-8').strip())

        # Some additional sanity checks
        # Check for non-duplicate events with the same recurring_event_id
        start_times = {}
        for recurrence in GEvent.objects.filter(calendar=self).exclude(recurrence=''):
            if recurrence.start in start_times.get(recurrence.recurring_event_id, set()):
                print "Error: Multiple GEvents found with the same start and recurring_event_id"
                dupe_ids = [str(dupe.id) for dupe in GEvent.objects.filter(recurring_event_id=recurrence.recurring_event_id,
                                                                start=recurrence.start)]
                print "IDs are {} at time {}".format(", ".join(dupe_ids), recurrence.start)

            if not start_times.get(recurrence.recurring_event_id, None):
                start_times[recurrence.recurring_event_id] = set()
            start_times[recurrence.recurring_event_id].add(recurrence.start)

    def find_gaps(self, start=None, end=None):
        qs = GEvent.objects.filter(calendar=self)
        if start:
            qs = qs.filter(start__gte=start)
        if end:
            qs = qs.filter(end__lte=end)
        qs.order_by('start')

        return qs

    def time_log(self):
        """
        Forms a TimeNodeChain based off events in this Calendar
        """
        return TimeNodeChain(GEvent.objects.filter(calendar=self).order_by('updated'))

    def update_meta(self):
        service = self.user.googlecredentials.get_service()
        try:
            result = service.calendars().get(calendarId=self.calendar_id).execute()
        except Exception as e:
            raise Exception("Could not update meta for GCalendar {}. Error: {}".format(self.calendar_id, e))
        # `result` looks like this:
        # {
        #     u'etag': u'[redacted]',
        #     u'id': u'maxfangx@gmail.com',
        #     u'kind': u'calendar#calendar',
        #     u'summary': u'maxfangx@gmail.com',
        #     u'timeZone': u'Asia/Shanghai'
        # }

        # Prune duplicate values
        if result and 'id' in result:
            result.pop('id')

        self.summary = result.get('summary').encode('utf-8').strip()
        self.meta = result
        self.save()

class Event(models.Model, TimeNode):

    name = models.CharField(max_length=150, default="(No title)", blank=True)
    start = models.DateTimeField(help_text="When the event started. 12AM for all day events")
    end = models.DateTimeField(help_text="When the event ended. 12AM the next day for all day events")
    location = models.CharField(max_length=500, blank=True, help_text="Geographic location as free form text")
    created = models.DateTimeField(help_text="When the event was created, on Google")
    updated = models.DateTimeField(help_text="When the event was last updated, on Google")

    class Meta:
        abstract = True

    def __init__(self, *args, **kwargs):

        TimeNode.__init__(self, start=kwargs.get('start'), end=kwargs.get('end'), id=kwargs.get('id'))

        super(Event, self).__init__(*args, **kwargs)


class GEvent(Event):

    """
    Represents a Google event.
    Reference:
    https://developers.google.com/google-apps/calendar/v3/reference/events#resource-representations
    """

    EVENT_COLORS_KEYS = sorted(GOOGLE_CALENDAR_COLORS['event'].keys(), key=lambda x: int(x))
    EVENT_COLORS_TUPLES = [(k, GOOGLE_CALENDAR_COLORS['event'][k]['background']) for k in EVENT_COLORS_KEYS]

    STATUS_CHOICES = (
        ('confirmed', 'Confirmed'),
        ('tentative', 'Tentative'),
        ('cancelled', 'Cancelled'),
    )

    TRANSPARENCY_CHOICES = (
        ('opaque', "Opaque - The event blocks time on the calendar"),
        ('transparent', "Transparent - The event does not block time on the calendar"),
    )

    # TODO support attendees for analyzing spending time with other people

    calendar = models.ForeignKey(GCalendar, related_name='gevents')
    google_id = models.CharField(max_length=1024, help_text="Unique id per calendar")
    i_cal_uid = models.CharField(max_length=1024, help_text="Unique id across calendaring systems. Only 1 per recurring event")
    color_index = models.CharField(max_length=10, blank=False, choices=EVENT_COLORS_TUPLES)
    description = models.TextField(max_length=20000, blank=True)
    status = models.CharField(max_length=50, default='confirmed', blank=True, choices=STATUS_CHOICES)
    transparency = models.CharField(max_length=50, default='opaque', blank=True, choices=TRANSPARENCY_CHOICES, help_text="Whether the event blocks time on the calendar.")

    all_day_event = models.BooleanField(default=False, blank=True)
    timezone = models.CharField(max_length=200, null=True, blank=True, help_text="IANA Time Zone Database Name")
    end_time_unspecified = models.BooleanField(default=False, help_text="If an end time is actually unspecified, since an end time is always specified for compatibility reasons")
    # Use ast.literal_eval(event.recurrence) to retrieve the list
    recurrence = models.CharField(max_length=1000, blank=True, help_text="string representation of list of RRULE, EXRULE, RDATE and EXDATE lines for a recurring event, as specified in RFC5545")
    recurring_event_id = models.CharField(max_length=1024, blank=True, help_text="For an instance of a recurring event, the id of the recurring event to which this instance belongs")
    # recurrences_filled_until = models.DateTimeField(null=True, help_text="How far in advance we have filled in recurrences")

    # TODO handle all_day_event not being counted in time
    # TODO handle transparency being counted in time

    def __str__(self):
        return "{} | {}".format(self.id, self.name.encode('utf-8').strip())

    @property
    def color(self):
        color = get_color(self.calendar, self.color_index)

        if color:
            return color
        else:
            # This handles when data isn't consistent for some reason.
            print "Warning: GEvent '{}' with id {} has an incorrect color_index value of '{}'".format(self.name, self.id, self.color_index)
            return GOOGLE_CALENDAR_COLORS['event']['1']

    def save(self, *args, **kwargs):
        if self.name is None:
            self.name = ""
        self.name = self.name[:150].encode('utf-8').strip()

        if self.description is None:
            self.description = ""
        self.description = self.description[:20000]

        # Validate the color_index field, correct if necessary
        if self.color_index is None or self.color_index == '':
            self.color_index = self.EVENT_COLORS_KEYS[0]
        try:
            assert int(self.color_index) in range(len(self.EVENT_COLORS_KEYS))
        except (ValueError, AssertionError):
            self.color_index = self.EVENT_COLORS_KEYS[0]

        made_aware = False
        if timezone.is_naive(self.start):
            made_aware = True

        self.start = ensure_timezone_awareness(self.start, self.timezone)
        self.end = ensure_timezone_awareness(self.end, self.timezone)

        super(GEvent, self).save(*args, **kwargs)

        if made_aware and settings.VERBOSE_PRINT:
            print "Made datetime timezone aware for GEvent {} with id {}".format(self.name, self.id)

    def conflicts_with(self, gevent):
        """
        Takes in another GEvent and returns True if the two events conflict, false otherwise.
        """
        # TODO incorporate this
        return False if self.end <= gevent.start or self.start >= gevent.end else True


class GRecurrence(models.Model):

    calendar = models.ForeignKey(GCalendar, related_name='recurrences')
    start = models.DateTimeField()
    end = models.DateTimeField()
    recurring_event_id = models.CharField(max_length=1024, help_text="For an instance of a recurring event, the id of the recurring event to which this instance belongs")

    def __getattr__(self, name):
        if name in set(['created', 'updated']):
            event = GEvent.objects.get(google_id=self.recurring_event_id)
            return getattr(event, 'name')
        else:
            return object.__dict__[name]

    @property
    def created(self):
        event = GEvent.objects.get(google_id=self.recurring_event_id)
        return event.created

    def save(self, *args, **kwargs):

        self.start = ensure_timezone_awareness(self.start)
        self.end = ensure_timezone_awareness(self.end)

        return super(GRecurrence, self).save(*args, **kwargs)


class DeletedEvent(models.Model):

    calendar = models.ForeignKey(GCalendar, related_name='deletedevents')
    original_start_time = models.DateTimeField(null=True)
    google_id = models.CharField(max_length=1024, blank=True, help_text="Unique id per calendar")
    recurring_event_id = models.CharField(max_length=1024, blank=True, help_text="For an instance of a recurring event, the id of the recurring event to which this instance belongs")

    def __str__(self):

        components = []
        if self.google_id:
            components.append("ID {}".format(self.google_id))
        if self.original_start_time:
            components.append("Start {}".format(self.original_start_time))
        if self.recurring_event_id:
            components.append("Recurring id {} ".format(self.recurring_event_id))

        return ", ".join(components)

    def apply(self):
        """
        Ensure that self.calendar is consistent with this DeletedEvent
        """

        if self.original_start_time:
            assert self.recurring_event_id, "If a DeletedEvent has an original_start_time, it must also have a recurring_event_id"
            # Need to add a one hour buffer for this query because this the implementation of RRULE using dateutil uses
            # naive datetimes and therefore doesn't account for daylight savings (not verified, but most likely problem)
            qs = GEvent.objects.filter(calendar=self.calendar,
                                       start__gte=self.original_start_time - timedelta(hours=1),
                                       start__lte=self.original_start_time + timedelta(hours=1),
                                       recurring_event_id=self.recurring_event_id)
            # qs = GEvent.objects.filter(calendar=self.calendar, start=self.original_start_time, recurring_event_id=self.recurring_event_id)
            for event in qs:
                event.delete()

        qs2 = GEvent.objects.filter(calendar=self.calendar, google_id=self.google_id)
        for event in qs2:
            event.delete()


class Category(models.Model, EventCollection):
    """
    Categories are slightly complex. There are two types:

    Calendar Categories - Categories based off of its affiliation with a
    *calendar* in Google Calendar. This is the typical case. These will have a
    non-null `calendar` field, and will ignore the color_index when making
    queries; i.e. it will return all events on a calendar regardless of if the
    event is the default color or not.

    Color Categories - Categories based off a *non-default color* set in Google
    Calendar, not tied to any particular calendar. In Google Calendar, default
    event colors actually have no value and defer to the color of the
    associated calendar, so this is why a Color Category must have a
    non-default color.

    To enforce the above, the `save` function will throw an error if both a
    calendar and a non-default color are specified. Accordingly, you can
    identify whether a Category is a Calendar Category or a Color Category by
    seeing whether the `calendar` field is set or the `color_index` is not
    equal to 1, respectively.

    Filtering by `calendar_ids` will have a different effect on both types
    as well.
    - For Calendar Categories, the calendar_ids parameter is ignored, because a
      Category tied to a specific calendar has nothing to do with multiple
      calendars.
    - For Color Categories, only events from the specified calendars in
      calendar_ids will be queried, much like the `calendar_ids` behavior for
      Tags.

    """

    calendar = models.ForeignKey(GCalendar, null=True, related_name='categories')
    user = models.ForeignKey(User, related_name='categories')
    color_index = models.CharField(max_length=100, help_text="str of the number of the event color in constants.py")
    label = models.CharField(max_length=100)

    def __str__(self):
        val = "{} by {}".format(self.label, self.user.username)
        if self.calendar:
            val += " for calendar {}".format(self.calendar.calendar_id)
        return val

    def save(self, *args, **kwargs):

        if self.calendar and self.color_index != "1":
            raise MalformedDataException("Category can only have non-null calendar field OR non-default color_index, but not both.")

        return super(Category, self).save(*args, **kwargs)

    def hours(self, calendar_ids=None, start=None, end=None):
        events = self.get_events(calendar_ids=calendar_ids, start=start, end=end)
        # round(...) necessary to display hours to two decimal points
        return round(EventCollection(lambda: events).total_time(start=start, end=end) / 3600.0, 2)

    def category_color(self):
        return get_color(self.calendar, self.color_index)['background']

    def get_events(self, calendar_ids=None, start=None, end=None):
        qs = self.query(calendar_ids, start, end)
        return set(qs)

    def query(self, calendar_ids=None, start=None, end=None):
        if self.calendar:  # Calendar Category
            # Added so deselecting calendars returns 0 hours for category
            if not calendar_ids or (calendar_ids and self.calendar.calendar_id in calendar_ids):
                calendars = [self.calendar]
            else:
                return []
        else:  # Color Category
            calendars = self.user.profile.get_calendars_for_calendarids(calendar_ids)

        querysets = [
                GEvent.objects
                .filter(calendar__user=self.user, calendar=calendar, color_index=self.color_index)
                .exclude(all_day_event=True)
                for calendar in calendars
                ]

        # Union over the querysets
        events_qs = reduce(lambda qs1, qs2: qs1 | qs2, querysets)

        if start:
            events_qs = events_qs.filter(end__gt=start)
        if end:
            events_qs = events_qs.filter(start__lt=end)
        else:
            events_qs = events_qs.filter(start__lt=datetime.now(pytz.utc))
        return events_qs.order_by('start')

    def get_time_series(self, timezone='UTC', time_step='week', calendar_ids=None, start=None, end=None):
        """
        Returns a list of date-hour tuples corresponding to the events in this Category.
        The dates are spaced out by the time_step.
        """
        return get_time_series(self, timezone, time_step, calendar_ids, start, end)


class TagGroup(models.Model):

    user = models.ForeignKey(User, related_name='taggroups')
    label = models.CharField(max_length=100, help_text="The name of this tag family")


class Tag(models.Model, EventCollection):

    user = models.ForeignKey(User, related_name='tags')
    label = models.CharField(max_length=100, help_text="The name of this tag")
    keywords = models.CharField(max_length=100, help_text="Comma-separated list of strings to search for")
    group = models.ForeignKey(TagGroup, null=True, default=None, related_name='tags')

    def __str__(self):
        return "<Tag '{}'>".format(self.label, self.keywords)

    def save(self, *args, **kwargs):
        # Remove beginning and ending spaces
        self.keywords = ",".join([k.strip() for k in self.keywords.split(',')])

        return super(Tag, self).save(*args, **kwargs)

    def hours(self, calendar_ids=None, start=None, end=None):
        events = self.get_events(calendar_ids=calendar_ids, start=start, end=end)
        # round(...) necessary to display hours to two decimal points
        return round(EventCollection(lambda: events).total_time(start=start, end=end) / 3600.0, 2)

    def get_events(self, calendar_ids=None, start=None, end=None):
        """
        Overrides EventCollection.get_events
        """

        # TODO handle edges here
        queryset = self.query(calendar_ids, start, end)

        return set(queryset)

    def query(self, calendar_ids=None, start=None, end=None):
        """
        Returns a QuerySet of events matching this Tag.
        Does not truncate at the edges.
        """
        calendars = self.user.profile.get_calendars_for_calendarids(calendar_ids)

        keywords = self.keywords.split(',')
        if not keywords:
            raise InvalidParameterException("No keywords defined for this tag")

        querysets = [
                GEvent.objects
                .filter(calendar=calendar, name__regex=r'\b(?i)[^a-zA-Z\d:]?'+keyword+r'(?i)[^a-zA-Z\d:]?\b')
                .exclude(all_day_event=True)
                for keyword in keywords
                for calendar in calendars
                ]

        # Union over the querysets
        events_qs = reduce(lambda qs1, qs2: qs1 | qs2, querysets)

        if start:
            events_qs = events_qs.filter(end__gt=start)
        if end:
            events_qs = events_qs.filter(start__lt=end)
        else:
            events_qs = events_qs.filter(start__lt=datetime.now(pytz.utc))

        return events_qs.order_by('start')

    def get_time_series(self, timezone='UTC', time_step='week', calendar_ids=None, start=None, end=None):
        """
        Returns a list of date-hour tuples corresponding to the events in this Tag.
        The dates are spaced out by the time_step.
        """
        return get_time_series(self, timezone, time_step, calendar_ids, start, end)


    def get_category_stats(self, categories, calendar_ids=None, start=None, end=None):
        """
        Returns a list of category-hour tuples corresponding to the number of hours per category
        that corresponds to this Tag.
        """
        category_hours = []
        keywords = self.keywords.split(',')

        for category in categories:
            if category.calendar and category.calendar.calendar_id in calendar_ids:
                querysets = [
                        GEvent.objects
                        .filter(calendar__user=category.user, calendar=category.calendar,
                                name__regex=r'\b(?i)[^a-zA-Z\d:]?'+keyword+r'(?i)[^a-zA-Z\d:]?\b',
                                color_index=category.color_index)
                        for keyword in keywords
                        ]

                events_qs = reduce(lambda qs1, qs2: qs1 | qs2, querysets)
                events_qs = events_qs.order_by('start')

                if start:
                    events_qs = events_qs.filter(end__gt=start)
                if end:
                    events_qs = events_qs.filter(start__lt=end)
                else:
                    events_qs = events_qs.filter(start__lt=datetime.now(pytz.utc))

                hours = round(EventCollection(lambda:set(events_qs)).total_time(start=start, end=end) / 3600.0, 2)
                for i in range(1, len(events_qs)):
                    if (events_qs[i - 1].end - events_qs[i].start).total_seconds() > 0:
                        # events overlap
                        hours -= round((events_qs[i - 1].end - events_qs[i].start).total_seconds() / 3600.0, 2)

                category_hours.append((category.label, category.category_color(), hours))

        return category_hours


class Statistic(models.Model):

    user = models.ForeignKey(User, related_name='statistics')
    name = models.CharField(max_length=100, help_text="The name of the statistic ")
    start_time = models.DateTimeField(null=True, help_text="The starting point for this statistic")
    end_time = models.DateTimeField(null=True, help_text="The ending point for this statistic")

    def query(self):
        qs = GEvent.objects.filter(user=self.user, name=self.name)
        if self.start_time:
            qs.filter(start_time__gt=self.start_time)
        if self.end_time:
            qs.filter(end_time__gt=self.end_time)
        return qs


class GoogleFlow(models.Model):
    """
    Represents a Google oauth flow.
    See: https://developers.google.com/api-client-library/python/guide/django
    """

    id = models.OneToOneField(User, primary_key=True)
    flow = FlowField()


class GoogleCredentials(models.Model):
    """
    Represents Google oauth credentials.
    See: https://developers.google.com/api-client-library/python/guide/django
    """

    user = models.OneToOneField(User, primary_key=True, related_name='googlecredentials')
    credential = CredentialsField()

    def get_service(self):
        http_auth = self.credential.authorize(httplib2.Http())

        # Try up to 3 times
        for _ in range(3):
            try:
                return build('calendar', 'v3', http=http_auth)
            except AccessTokenRefreshError:
                # TODO add logging to see this exception
                profile = Profile.get_or_create(self.user)[0]
                profile.authed = False
                profile.save()
                break
            except Exception:
                pass

        return None

    def import_calendars(self, only_primary=True):
        """
        Hits the CalendarList.list() endpoint and updates database with any calendars found.
        only_primary specifies if only the primary calendar is saved to the database.
        """

        service = self.get_service()
        result = service.calendarList().list().execute()

        # Example:
        # {
        #   "kind": "calendar#calendarList",
        #   "etag": etag,
        #   "nextPageToken": string,
        #   "nextSyncToken": string,  # Not sure what this is for
        #   "items": [
        #     {u'accessRole': u'owner',
        #      u'backgroundColor': u'#9fc6e7',
        #      u'colorId': u'15',
        #      u'defaultReminders': [{u'method': u'popup', u'minutes': 10}],
        #      u'etag': u'"1446592681590000"',
        #      u'foregroundColor': u'#000000',
        #      u'id': string,
        #      u'kind': u'calendar#calendarListEntry',
        #      u'notificationSettings': {u'notifications': [{u'method': u'email',
        #         u'type': u'eventCreation'},
        #        {u'method': u'email', u'type': u'eventChange'},
        #        {u'method': u'email', u'type': u'eventCancellation'},
        #        {u'method': u'email', u'type': u'eventResponse'}]},
        #      u'primary': True,
        #      u'selected': True,
        #      u'summary': u'maxfangx@gmail.com',
        #      u'timeZone': u'America/Los_Angeles'}
        #   ]
        # }

        assert 'items' in result, "import calendars failed"

        for item in result['items']:
            if item['accessRole'] != 'owner':
                # Incremental sync doesn't work for calendars belonging to others
                continue

            gcal, gcal_created = GCalendar.objects.get_or_create(user=self.user, calendar_id=item['id'])

            # This is the primary calendar, save it as such
            if item.get('primary', False):
                profile, _ = Profile.get_or_create(self.user)
                profile.main_calendar = gcal
                profile.save()

            # Update fields for each calendar
            gcal.summary = item.get('summary')
            gcal.color_index = item.get('colorId', '1')
            gcal.save()
            if gcal_created:
                gcal.update_meta()

        calendar_ids = [cal['id'] for cal in result['items'] if cal['accessRole'] == 'owner']
        inaccessible_calendars = GCalendar.objects.filter(user=self.user)\
                                                  .exclude(calendar_id__in=calendar_ids)
        for i in inaccessible_calendars:
            print "Deleting calendar '{}' because it is now inaccessible".format(i.summary)
            i.delete()

        self.save()
