__filename__ = "happening.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.2.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
__module_group__ = "Core"

import os
from uuid import UUID
from datetime import datetime
from datetime import timedelta

from utils import isPublicPost
from utils import loadJson
from utils import saveJson
from utils import locatePost
from utils import hasObjectDict
from utils import acctDir


def _validUuid(testUuid: str, version: int = 4):
    """Check if uuid_to_test is a valid UUID
    """
    try:
        uuid_obj = UUID(testUuid, version=version)
    except ValueError:
        return False

    return str(uuid_obj) == testUuid


def _removeEventFromTimeline(eventId: str, tlEventsFilename: str) -> None:
    """Removes the given event Id from the timeline
    """
    if eventId + '\n' not in open(tlEventsFilename).read():
        return
    with open(tlEventsFilename, 'r') as fp:
        eventsTimeline = fp.read().replace(eventId + '\n', '')
        try:
            with open(tlEventsFilename, 'w+') as fp2:
                fp2.write(eventsTimeline)
        except BaseException:
            print('ERROR: unable to save events timeline')
            pass


def saveEventPost(baseDir: str, handle: str, postId: str,
                  eventJson: {}) -> bool:
    """Saves an event to the calendar and/or the events timeline
    If an event has extra fields, as per Mobilizon,
    Then it is saved as a separate entity and added to the
    events timeline
    See https://framagit.org/framasoft/mobilizon/-/blob/
    master/lib/federation/activity_stream/converter/event.ex
    """
    if not os.path.isdir(baseDir + '/accounts/' + handle):
        print('WARN: Account does not exist at ' +
              baseDir + '/accounts/' + handle)
    calendarPath = baseDir + '/accounts/' + handle + '/calendar'
    if not os.path.isdir(calendarPath):
        os.mkdir(calendarPath)

    # get the year, month and day from the event
    eventTime = datetime.strptime(eventJson['startTime'],
                                  "%Y-%m-%dT%H:%M:%S%z")
    eventYear = int(eventTime.strftime("%Y"))
    if eventYear < 2020 or eventYear >= 2100:
        return False
    eventMonthNumber = int(eventTime.strftime("%m"))
    if eventMonthNumber < 1 or eventMonthNumber > 12:
        return False
    eventDayOfMonth = int(eventTime.strftime("%d"))
    if eventDayOfMonth < 1 or eventDayOfMonth > 31:
        return False

    if eventJson.get('name') and eventJson.get('actor') and \
       eventJson.get('uuid') and eventJson.get('content'):
        if not _validUuid(eventJson['uuid']):
            return False
        print('Mobilizon type event')
        # if this is a full description of an event then save it
        # as a separate json file
        eventsPath = baseDir + '/accounts/' + handle + '/events'
        if not os.path.isdir(eventsPath):
            os.mkdir(eventsPath)
        eventsYearPath = \
            baseDir + '/accounts/' + handle + '/events/' + str(eventYear)
        if not os.path.isdir(eventsYearPath):
            os.mkdir(eventsYearPath)
        eventId = str(eventYear) + '-' + eventTime.strftime("%m") + '-' + \
            eventTime.strftime("%d") + '_' + eventJson['uuid']
        eventFilename = eventsYearPath + '/' + eventId + '.json'

        saveJson(eventJson, eventFilename)
        # save to the events timeline
        tlEventsFilename = baseDir + '/accounts/' + handle + '/events.txt'

        if os.path.isfile(tlEventsFilename):
            _removeEventFromTimeline(eventId, tlEventsFilename)
            try:
                with open(tlEventsFilename, 'r+') as tlEventsFile:
                    content = tlEventsFile.read()
                    if eventId + '\n' not in content:
                        tlEventsFile.seek(0, 0)
                        tlEventsFile.write(eventId + '\n' + content)
            except Exception as e:
                print('WARN: Failed to write entry to events file ' +
                      tlEventsFilename + ' ' + str(e))
                return False
        else:
            with open(tlEventsFilename, 'w+') as tlEventsFile:
                tlEventsFile.write(eventId + '\n')

    # create a directory for the calendar year
    if not os.path.isdir(calendarPath + '/' + str(eventYear)):
        os.mkdir(calendarPath + '/' + str(eventYear))

    # calendar month file containing event post Ids
    calendarFilename = calendarPath + '/' + str(eventYear) + \
        '/' + str(eventMonthNumber) + '.txt'

    # Does this event post already exist within the calendar month?
    if os.path.isfile(calendarFilename):
        if postId in open(calendarFilename).read():
            # Event post already exists
            return False

    # append the post Id to the file for the calendar month
    with open(calendarFilename, 'a+') as calendarFile:
        calendarFile.write(postId + '\n')

    # create a file which will trigger a notification that
    # a new event has been added
    calendarNotificationFilename = \
        baseDir + '/accounts/' + handle + '/.newCalendar'
    with open(calendarNotificationFilename, 'w+') as calendarNotificationFile:
        notifyStr = \
            '/calendar?year=' + str(eventYear) + '?month=' + \
            str(eventMonthNumber) + '?day=' + str(eventDayOfMonth)
        calendarNotificationFile.write(notifyStr)
    return True


def _isHappeningEvent(tag: {}) -> bool:
    """Is this tag an Event or Place ActivityStreams type?
    """
    if not tag.get('type'):
        return False
    if tag['type'] != 'Event' and tag['type'] != 'Place':
        return False
    return True


def _isHappeningPost(postJsonObject: {}) -> bool:
    """Is this a post with tags?
    """
    if not postJsonObject:
        return False
    if not hasObjectDict(postJsonObject):
        return False
    if not postJsonObject['object'].get('tag'):
        return False
    return True


def getTodaysEvents(baseDir: str, nickname: str, domain: str,
                    currYear: int = None, currMonthNumber: int = None,
                    currDayOfMonth: int = None) -> {}:
    """Retrieves calendar events for today
    Returns a dictionary of lists containing Event and Place activities
    """
    now = datetime.now()
    if not currYear:
        year = now.year
    else:
        year = currYear
    if not currMonthNumber:
        monthNumber = now.month
    else:
        monthNumber = currMonthNumber
    if not currDayOfMonth:
        dayNumber = now.day
    else:
        dayNumber = currDayOfMonth

    calendarFilename = \
        acctDir(baseDir, nickname, domain) + \
        '/calendar/' + str(year) + '/' + str(monthNumber) + '.txt'
    events = {}
    if not os.path.isfile(calendarFilename):
        return events

    calendarPostIds = []
    recreateEventsFile = False
    with open(calendarFilename, 'r') as eventsFile:
        for postId in eventsFile:
            postId = postId.replace('\n', '').replace('\r', '')
            postFilename = locatePost(baseDir, nickname, domain, postId)
            if not postFilename:
                recreateEventsFile = True
                continue

            postJsonObject = loadJson(postFilename)
            if not _isHappeningPost(postJsonObject):
                continue

            publicEvent = isPublicPost(postJsonObject)

            postEvent = []
            dayOfMonth = None
            for tag in postJsonObject['object']['tag']:
                if not _isHappeningEvent(tag):
                    continue
                # this tag is an event or a place
                if tag['type'] == 'Event':
                    # tag is an event
                    if not tag.get('startTime'):
                        continue
                    eventTime = \
                        datetime.strptime(tag['startTime'],
                                          "%Y-%m-%dT%H:%M:%S%z")
                    if int(eventTime.strftime("%Y")) == year and \
                       int(eventTime.strftime("%m")) == monthNumber and \
                       int(eventTime.strftime("%d")) == dayNumber:
                        dayOfMonth = str(int(eventTime.strftime("%d")))
                        if '#statuses#' in postId:
                            # link to the id so that the event can be
                            # easily deleted
                            tag['postId'] = postId.split('#statuses#')[1]
                            tag['sender'] = postId.split('#statuses#')[0]
                            tag['sender'] = tag['sender'].replace('#', '/')
                            tag['public'] = publicEvent
                        postEvent.append(tag)
                else:
                    # tag is a place
                    postEvent.append(tag)
            if postEvent and dayOfMonth:
                calendarPostIds.append(postId)
                if not events.get(dayOfMonth):
                    events[dayOfMonth] = []
                events[dayOfMonth].append(postEvent)

    # if some posts have been deleted then regenerate the calendar file
    if recreateEventsFile:
        with open(calendarFilename, 'w+') as calendarFile:
            for postId in calendarPostIds:
                calendarFile.write(postId + '\n')

    return events


def dayEventsCheck(baseDir: str, nickname: str, domain: str, currDate) -> bool:
    """Are there calendar events for the given date?
    """
    year = currDate.year
    monthNumber = currDate.month
    dayNumber = currDate.day

    calendarFilename = \
        acctDir(baseDir, nickname, domain) + \
        '/calendar/' + str(year) + '/' + str(monthNumber) + '.txt'
    if not os.path.isfile(calendarFilename):
        return False

    eventsExist = False
    with open(calendarFilename, 'r') as eventsFile:
        for postId in eventsFile:
            postId = postId.replace('\n', '').replace('\r', '')
            postFilename = locatePost(baseDir, nickname, domain, postId)
            if not postFilename:
                continue

            postJsonObject = loadJson(postFilename)
            if not _isHappeningPost(postJsonObject):
                continue

            for tag in postJsonObject['object']['tag']:
                if not _isHappeningEvent(tag):
                    continue
                # this tag is an event or a place
                if tag['type'] != 'Event':
                    continue
                # tag is an event
                if not tag.get('startTime'):
                    continue
                eventTime = \
                    datetime.strptime(tag['startTime'],
                                      "%Y-%m-%dT%H:%M:%S%z")
                if int(eventTime.strftime("%d")) != dayNumber:
                    continue
                if int(eventTime.strftime("%m")) != monthNumber:
                    continue
                if int(eventTime.strftime("%Y")) != year:
                    continue
                eventsExist = True
                break

    return eventsExist


def getThisWeeksEvents(baseDir: str, nickname: str, domain: str) -> {}:
    """Retrieves calendar events for this week
    Returns a dictionary indexed by day number of lists containing
    Event and Place activities
    Note: currently not used but could be with a weekly calendar screen
    """
    now = datetime.now()
    endOfWeek = now + timedelta(7)
    year = now.year
    monthNumber = now.month

    calendarFilename = \
        acctDir(baseDir, nickname, domain) + \
        '/calendar/' + str(year) + '/' + str(monthNumber) + '.txt'

    events = {}
    if not os.path.isfile(calendarFilename):
        return events

    calendarPostIds = []
    recreateEventsFile = False
    with open(calendarFilename, 'r') as eventsFile:
        for postId in eventsFile:
            postId = postId.replace('\n', '').replace('\r', '')
            postFilename = locatePost(baseDir, nickname, domain, postId)
            if not postFilename:
                recreateEventsFile = True
                continue

            postJsonObject = loadJson(postFilename)
            if not _isHappeningPost(postJsonObject):
                continue

            postEvent = []
            weekDayIndex = None
            for tag in postJsonObject['object']['tag']:
                if not _isHappeningEvent(tag):
                    continue
                # this tag is an event or a place
                if tag['type'] == 'Event':
                    # tag is an event
                    if not tag.get('startTime'):
                        continue
                    eventTime = \
                        datetime.strptime(tag['startTime'],
                                          "%Y-%m-%dT%H:%M:%S%z")
                    if eventTime >= now and eventTime <= endOfWeek:
                        weekDayIndex = (eventTime - now).days()
                        postEvent.append(tag)
                else:
                    # tag is a place
                    postEvent.append(tag)
            if postEvent and weekDayIndex:
                calendarPostIds.append(postId)
                if not events.get(weekDayIndex):
                    events[weekDayIndex] = []
                events[weekDayIndex].append(postEvent)

    # if some posts have been deleted then regenerate the calendar file
    if recreateEventsFile:
        with open(calendarFilename, 'w+') as calendarFile:
            for postId in calendarPostIds:
                calendarFile.write(postId + '\n')

    return events


def getCalendarEvents(baseDir: str, nickname: str, domain: str,
                      year: int, monthNumber: int) -> {}:
    """Retrieves calendar events
    Returns a dictionary indexed by day number of lists containing
    Event and Place activities
    """
    calendarFilename = \
        acctDir(baseDir, nickname, domain) + \
        '/calendar/' + str(year) + '/' + str(monthNumber) + '.txt'

    events = {}
    if not os.path.isfile(calendarFilename):
        return events

    calendarPostIds = []
    recreateEventsFile = False
    with open(calendarFilename, 'r') as eventsFile:
        for postId in eventsFile:
            postId = postId.replace('\n', '').replace('\r', '')
            postFilename = locatePost(baseDir, nickname, domain, postId)
            if not postFilename:
                recreateEventsFile = True
                continue

            postJsonObject = loadJson(postFilename)
            if not _isHappeningPost(postJsonObject):
                continue

            postEvent = []
            dayOfMonth = None
            for tag in postJsonObject['object']['tag']:
                if not _isHappeningEvent(tag):
                    continue
                # this tag is an event or a place
                if tag['type'] == 'Event':
                    # tag is an event
                    if not tag.get('startTime'):
                        continue
                    eventTime = \
                        datetime.strptime(tag['startTime'],
                                          "%Y-%m-%dT%H:%M:%S%z")
                    if int(eventTime.strftime("%Y")) == year and \
                       int(eventTime.strftime("%m")) == monthNumber:
                        dayOfMonth = str(int(eventTime.strftime("%d")))
                        postEvent.append(tag)
                else:
                    # tag is a place
                    postEvent.append(tag)

            if postEvent and dayOfMonth:
                calendarPostIds.append(postId)
                if not events.get(dayOfMonth):
                    events[dayOfMonth] = []
                events[dayOfMonth].append(postEvent)

    # if some posts have been deleted then regenerate the calendar file
    if recreateEventsFile:
        with open(calendarFilename, 'w+') as calendarFile:
            for postId in calendarPostIds:
                calendarFile.write(postId + '\n')

    return events


def removeCalendarEvent(baseDir: str, nickname: str, domain: str,
                        year: int, monthNumber: int, messageId: str) -> None:
    """Removes a calendar event
    """
    calendarFilename = \
        acctDir(baseDir, nickname, domain) + \
        '/calendar/' + str(year) + '/' + str(monthNumber) + '.txt'
    if not os.path.isfile(calendarFilename):
        return
    if '/' in messageId:
        messageId = messageId.replace('/', '#')
    if messageId not in open(calendarFilename).read():
        return
    lines = None
    with open(calendarFilename, 'r') as f:
        lines = f.readlines()
    if not lines:
        return
    with open(calendarFilename, 'w+') as f:
        for line in lines:
            if messageId not in line:
                f.write(line)