// check if we're authorized and if we have a calendar id, and prompt the user to set up either if needed
        // returns true if we're authorized and have a calendar id, returns false if either checks are false
        public CalendarSyncStatus CheckIfSyncPossible(DiscordServer server)
        {
            // check if we have credentials for google apiitem
            if (server.GoogleUserCredential == null)
            {
                return(CalendarSyncStatus.NullCredentials);
            }

            if (server.CalendarId == "")
            {
                return(CalendarSyncStatus.NullCalendarId);
            }

            if (server.CalendarId == null)
            {
                return(CalendarSyncStatus.EmptyCalendarId);
            }

            // if server object is assigned, the bot is connected, but the bot is not connected to this server, we're probably kicked
            if (server.DiscordServerObject != null && server.DiscordServerObject.Available &&
                ((SocketGuild)server.DiscordServerObject).IsConnected == false)
            {
                // DEBUG
                Task.Run((async() =>
                {
                    Logger.Log(LogLevel.Debug, $"DEBUG - Name: {server.DiscordServerObject.Name} - Available: {server.DiscordServerObject.Available} " +
                               $"Connected: {((SocketGuild)server.DiscordServerObject).IsConnected} - WE SHOULD NOT SEE THIS. THIS SHOULD BE HANDLED AT THE START OF A TIMER TICK.");
                }));
                return(CalendarSyncStatus.ServerUnavailable);
            }


            return(CalendarSyncStatus.OK);
        }
        // log in to all servers
        public async Task Login(DiscordServer server)
        {
            var credentialPath = $@"{_credentialPathPrefix}/{server.ServerId}";

            using (var stream = new FileStream(_filePath, FileMode.Open, FileAccess.Read))
            {
                // build code flow manager to authenticate token
                var flowManager = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
                {
                    ClientSecrets = GoogleClientSecrets.Load(stream).Secrets,
                    Scopes        = _scopes,
                    DataStore     = new FileDataStore(credentialPath, true)
                });

                var fileDataStore = new FileDataStore(credentialPath, true);
                var token         = await fileDataStore.GetAsync <TokenResponse>("token");

                // load token from file
                // var token = await flowManager.LoadTokenAsync(_userId, CancellationToken.None).ConfigureAwait(false);

                // check if we need to get a new token
                if (flowManager.ShouldForceTokenRetrieval() || token == null ||
                    token.RefreshToken == null && token.IsExpired(flowManager.Clock))
                {
                    return;
                }

                // set credentials to use for syncing
                server.GoogleUserCredential = new UserCredential(flowManager, _userId, token);
            }
        }
        // send or modify embed messages listing upcoming events from the raid calendar
        public async Task SendEvents(DiscordServer server)
        {
            // build embed
            var embed = BuildEventsEmbed(server);

            // check if we haven't set an embed message yet
            if (server.EventEmbedMessage == null)
            {
                // try to get a pre-existing event embed
                var oldEmbedMessage = await GetPreviousEmbed(server);

                // if we found a pre-existing event embed, set it as our current event embed message
                // and edit it
                if (oldEmbedMessage != null)
                {
                    server.EventEmbedMessage = oldEmbedMessage;
                    await server.EventEmbedMessage.ModifyAsync(m => { m.Embed = embed; });
                }
                // otherwise, send a new one and set it as our current event embed message
                else
                {
                    // send embed
                    var message = await server.ReminderChannel.SendMessageAsync(null, false, embed);

                    // store message id
                    server.EventEmbedMessage = message;
                }
            } // if we have set a current event embed message, edit it
            else
            {
                await server.EventEmbedMessage.ModifyAsync(m => { m.Embed = embed; });
            }
        }
        // searches the _reminderChannel for a message from the bot containing the passed param
        // (this should be the title of an event for which we are looking for a remindermessage to edit)
        // if it finds one, return that message to the calling method to be modified
        private async Task <IUserMessage> GetPreviousReminderMessage(DiscordServer server, string messageContains)
        {
            // get all messages in reminder channel
            var messages = await server.ReminderChannel.GetMessagesAsync().FlattenAsync();

            // try to get a pre-existing message matching messageContains (so {eventtitle})
            //return the results or null
            var reminderMsg = messages.Where(msg => msg.Author.Id == _discord.CurrentUser.Id).FirstOrDefault(msg => msg.Content.Contains(messageContains));

            return((IUserMessage)reminderMsg);
        }
        // called whenever .sync command is used, and at first program launch
        public async Task <bool> ManualSync(DiscordServer server = null, SocketCommandContext context = null)
        {
            // if server is null, context is not null - we're calling via command, so get the right server via context
            if (server == null && context != null)
            {
                server = Servers.ServerList.Find(x => x.DiscordServerObject == context.Guild);
            }

            // check if we're authenticated and have a calendar id to sync from
            var syncStatus = CheckIfSyncPossible(server);

            if (syncStatus != CalendarSyncStatus.OK)
            {
                if (server == null && context != null)
                {
                    await context.Channel.SendMessageAsync($"Sync failed: {SyncFailedReason(syncStatus)}");
                }
                else
                {
                    await server.ConfigChannel.SendMessageAsync($"Sync failed: {SyncFailedReason(syncStatus)}");
                }

                return(false);
            }

            // perform the actual sync
            var success = SyncFromGoogleCalendar(server);

            // handle sync success or failure
            if (success)
            {
                // send message reporting we've synced calendar events
                string resultMessage = $":calendar: Synced {server.Events.Count} calendar events.";
                if (context != null) // we only want to send a message announcing sync success if the user sent the command
                {
                    await _interactiveService.ReplyAndDeleteAsync(context, resultMessage);
                }
            }
            else
            {
                // send message reporting there were no calendar events to sync
                string resultMessage = ":calendar: No events found in calendar.";
                if (context != null)
                {
                    await _interactiveService.ReplyAndDeleteAsync(context, resultMessage);
                }
            }

            // send/modify events embed in reminders to reflect newly synced values
            await _scheduleService.SendEvents(server);

            return(true);
        }
        // searches the _reminderChannel for a message from the bot containing an embed (how else can we filter this - title?)
        // if it finds one, return that message to the calling method to be set as _eventEmbedMessage
        private async Task <IUserMessage> GetPreviousEmbed(DiscordServer server)
        {
            // get all messages in reminder channel
            var messages = await server.ReminderChannel.GetMessagesAsync().FlattenAsync();

            // try to get a pre-existing embed message matching our usual event embed parameters
            // return the results
            try
            {
                var embedMsg = messages.Where(msg => msg.Author.Id == _discord.CurrentUser.Id)
                               .Where(msg => msg.Embeds.Count > 0)
                               .Where(msg => msg.Embeds.First().Title == "Schedule").ToList().First();
                return((IUserMessage)embedMsg);
            }
            catch
            {
                return(null);
            }
        }
        // logic for pulling data from api and adding it to CalendarEvents list, returns bool representing
        // if calendar had events or not
        public bool SyncFromGoogleCalendar(DiscordServer server)
        {
            // Set the timespan of events to sync
            var min = TimezoneAdjustedDateTime.Now.Invoke();
            var max = TimezoneAdjustedDateTime.Now.Invoke().AddMonths(1);

            // pull events from the specified google calendar
            // string is the calendar id of the calendar to sync with
            var events = GetCalendarEvents(server.GoogleUserCredential, server.CalendarId, min, max);

            // declare events to use for list comparisons
            List <CalendarEvent> oldEventsList = new List <CalendarEvent>();
            List <CalendarEvent> newEventsList = new List <CalendarEvent>();

            oldEventsList.AddRange(server.Events);
            server.Events.Clear();

            // if there are events, iterate through and add them to our calendarevents list
            if (events.Any())
            {
                // build a list of the events we pulled from gcal
                foreach (var eventItem in events)
                {
                    // api wrapper will always pull times in local time aka eastern because it sucks
                    // so just subtract 3 hours to get pacific time
                    eventItem.Start.DateTime = eventItem.Start.DateTime - TimeSpan.FromHours(7);
                    eventItem.End.DateTime   = eventItem.End.DateTime - TimeSpan.FromHours(7);

                    // don't add items from the past
                    if (eventItem.End.DateTime < TimezoneAdjustedDateTime.Now.Invoke())
                    {
                        continue;
                    }

                    DateTime startDate;
                    DateTime endDate;

                    if (eventItem.Start.DateTime.HasValue == false || eventItem.End.DateTime.HasValue == false)
                    {
                        startDate = DateTime.Parse(eventItem.Start.Date);
                        endDate   = DateTime.Parse(eventItem.End.Date);
                    }
                    else
                    {
                        startDate = eventItem.Start.DateTime.Value;
                        endDate   = eventItem.End.DateTime.Value;
                    }

                    // build calendar event to be added to our list
                    var calendarEvent = new CalendarEvent()
                    {
                        Name      = eventItem.Summary,
                        StartDate = startDate,
                        EndDate   = endDate,
                        Timezone  = "PST",
                        UniqueId  = eventItem.Id
                    };

                    newEventsList.Add(calendarEvent);
                }

                // build our working list of calendarevents, mixing old event items (if any) and new ones
                if (oldEventsList.Count == 0)
                {
                    // if calendarevents list (and thus oldeventslist) is empty, we're running for the first time
                    // so just add newEventsList to calendarevents and be done
                    server.Events.AddRange(newEventsList);
                }
                else
                {
                    // match events we just pulled from google to events we have stored already, by start date
                    // store new name (this doesn't matter), start and endgames from new list into CalendarEvents
                    // keep existing alert flags
                    var oldEventsDict = oldEventsList.ToDictionary(n => n.UniqueId);
                    foreach (var n in newEventsList)
                    {
                        CalendarEvent o;
                        if (oldEventsDict.TryGetValue(n.UniqueId, out o))
                        {
                            var calendarEvent = new CalendarEvent();
                            calendarEvent.Name         = n.Name;
                            calendarEvent.Timezone     = o.Timezone;
                            calendarEvent.AlertMessage = o.AlertMessage;
                            calendarEvent.UniqueId     = o.UniqueId;

                            // if this event's been manually adjusted, keep the old values
                            if (o.ManuallyAdjusted)
                            {
                                calendarEvent.StartDate = o.StartDate;
                                calendarEvent.EndDate   = o.EndDate;
                            }
                            else // else accept the new values
                            {
                                calendarEvent.StartDate = n.StartDate;
                                calendarEvent.EndDate   = n.EndDate;
                            }

                            server.Events.Add(calendarEvent);
                        }

                        else
                        {
                            server.Events.Add(n);
                        }
                    }
                }
                return(true); // calendar had events, and we added them
            }
            return(false);    // calendar did not have events
        }
        // send or modify messages alerting the user that an event will be starting soon
        public async Task HandleReminders(DiscordServer server)
        {
            var firstCalendarEvent = server.Events[0];

            // look for any existing reminder messages in the reminders channel that contain the first event's name
            // if one exists, set that message as the first event's alert message
            // only set the first one so repeated event names don't all get assigned this message
            var oldReminderMessage = await GetPreviousReminderMessage(server, firstCalendarEvent.Name);

            if (oldReminderMessage != null && firstCalendarEvent.AlertMessage == null)
            {
                firstCalendarEvent.AlertMessage = oldReminderMessage;
            }

            foreach (var calendarEvent in server.Events)
            {
                // get amount of time between the calendarevent start time and the current time
                // and round it to the nearest 5m interval, so usually on the 5m interval
                var timeStartDelta = RoundToNearestMinutes(calendarEvent.StartDate - TimezoneAdjustedDateTime.Now.Invoke(), 5);

                // if it's less than an hour but more than fifteen minutes, and we haven't sent an alert message, send an alert message
                if (timeStartDelta.TotalHours < 1 && timeStartDelta.TotalMinutes > 15)
                {
                    var messageContents =
                        $"{calendarEvent.Name} is starting in {(int)timeStartDelta.TotalMinutes} minutes.";

                    // if there's an alert message already, edit it
                    if (calendarEvent.AlertMessage != null)
                    {
                        await calendarEvent.AlertMessage.ModifyAsync(m => m.Content = messageContents);

                        Logger.Log(LogLevel.Debug, $"DEBUG - {server.ServerName} - An event is between 15m and 1h from now and we did have an alert message, editing it.");
                    }
                    // if there wasn't an alert message, send a new message
                    else
                    {
                        var msg = await server.ReminderChannel.SendMessageAsync(messageContents);

                        calendarEvent.AlertMessage = msg;
                        Logger.Log(LogLevel.Debug, $"DEBUG - {server.ServerName} - An event is between 15m and 1h from now and we did not have an alert message, sending one.");
                    }
                }

                // if it's less than an hour and less or equal to fifteen minutes, try to modify an existing alert message or send a new one
                if (timeStartDelta.TotalHours < 1 && timeStartDelta.TotalMinutes <= 15)
                {
                    var messageContents = $"{calendarEvent.Name} is starting shortly. Look for a party finder soon.";

                    // if there's an alert message already, edit it
                    if (calendarEvent.AlertMessage != null)
                    {
                        await calendarEvent.AlertMessage.ModifyAsync(m => m.Content = messageContents);

                        Logger.Log(LogLevel.Debug, $"DEBUG - {server.ServerName} - The event is less than 15m from now and we did have an alert message, editing it.");
                    }
                    // if there wasn't an alert message, send a new message
                    else
                    {
                        var msg = await server.ReminderChannel.SendMessageAsync(messageContents);

                        calendarEvent.AlertMessage = msg;
                        Logger.Log(LogLevel.Debug, $"DEBUG - {server.ServerName} - The event is less than 15m from now and we did not have an alert message, sending one.");
                    }
                }

                // if the event is currently active (after start date but before end date)
                // update the alert message to reflect how much time is left until the event is over
                if (calendarEvent.StartDate < TimezoneAdjustedDateTime.Now.Invoke() &&
                    calendarEvent.EndDate > TimezoneAdjustedDateTime.Now.Invoke())
                {
                    // get amount of time between the current time and the calendarevent end time
                    var timeEndDelta = RoundToNearestMinutes(calendarEvent.EndDate - TimezoneAdjustedDateTime.Now.Invoke(), 5);

                    var messageContents = $"{calendarEvent.Name} is underway, ending in" + GetTimeDeltaFormatting(timeEndDelta) + ".";

                    // if there's an alert message already, edit it
                    if (calendarEvent.AlertMessage != null)
                    {
                        await calendarEvent.AlertMessage.ModifyAsync(m => m.Content = messageContents);

                        Logger.Log(LogLevel.Debug, $"DEBUG - {server.ServerName} - The event is underway and we had an alert message, editing it.");
                    }
                    // if there wasn't an alert message, send a new message
                    else
                    {
                        var msg = await server.ReminderChannel.SendMessageAsync(messageContents);

                        calendarEvent.AlertMessage = msg;
                        Logger.Log(LogLevel.Debug, $"DEBUG - {server.ServerName} - The event is underway and we did not have an alert message, sending one.");
                    }
                }

                // if the event is almost past, delete the alertmessage
                if (calendarEvent.EndDate < TimezoneAdjustedDateTime.Now.Invoke() + TimeSpan.FromMinutes(5))
                {
                    await calendarEvent.AlertMessage.DeleteAsync();

                    calendarEvent.AlertMessage = null;
                    Logger.Log(LogLevel.Debug, $"DEBUG - {server.ServerName} - The event end date is less than 5 mins from now, deleting alert message.");
                }

                // if the event is over an hour from now and an alert message exists, delete it.
                if (calendarEvent.StartDate > TimezoneAdjustedDateTime.Now.Invoke() + TimeSpan.FromMinutes(60) && calendarEvent.AlertMessage != null)
                {
                    // await calendarEvent.AlertMessage.DeleteAsync();

                    calendarEvent.AlertMessage = null;
                    Logger.Log(LogLevel.Debug, $"DEBUG - {server.ServerName} - The event start date is over an hour away, we would have deleted the alert message.");
                }

                if (calendarEvent.AlertMessage != null)
                {
                    Logger.Log(LogLevel.Debug, $"DEBUG - msg ID: {calendarEvent.AlertMessage.Id} - edited: {calendarEvent.AlertMessage.EditedTimestamp} - contents: {calendarEvent.AlertMessage.Content}");
                }
            }
        }
        // put together the events embed & return it to calling method
        private Embed BuildEventsEmbed(DiscordServer server)
        {
            EmbedBuilder embedBuilder = new EmbedBuilder();

            // if there are no items in CalendarEvents, build a field stating so
            if (server.Events.Count == 0)
            {
                embedBuilder.AddField("No raids scheduled.", _textMemeService.GetMemeTextForNoEvents());
            }

            // iterate through each calendar event and build strings from them
            // if there are no events, the foreach loop is skipped, so no need to check
            foreach (var calendarEvent in server.Events)
            {
                // don't add items from the past
                if (calendarEvent.EndDate < TimezoneAdjustedDateTime.Now.Invoke())
                {
                    continue;
                }

                // get the time difference between the event and now
                // roundtonearestminutes wrapper will round it to closest 5m interval
                TimeSpan timeDelta;

                // holy f*****g formatting batman
                StringBuilder stringBuilder = new StringBuilder();

                // if event hasn't started yet
                if (calendarEvent.StartDate > TimezoneAdjustedDateTime.Now.Invoke())
                {
                    stringBuilder.AppendLine($"Starts on {calendarEvent.StartDate,0:M/dd} at {calendarEvent.StartDate,0: h:mm tt} {calendarEvent.Timezone} and ends at {calendarEvent.EndDate,0: h:mm tt} {calendarEvent.Timezone}");
                    stringBuilder.Append(":watch: Starts in");
                    timeDelta = RoundToNearestMinutes(calendarEvent.StartDate - TimezoneAdjustedDateTime.Now.Invoke(), 5);
                }

                // if event has started but hasn't finished
                else if (calendarEvent.StartDate < TimezoneAdjustedDateTime.Now.Invoke() &&
                         calendarEvent.EndDate > TimezoneAdjustedDateTime.Now.Invoke())
                {
                    stringBuilder.AppendLine($"Currently underway, ending at {calendarEvent.EndDate,0: h:mm tt} {calendarEvent.Timezone}");
                    stringBuilder.Append(":watch: Ends in");
                    timeDelta = RoundToNearestMinutes(calendarEvent.EndDate - TimezoneAdjustedDateTime.Now.Invoke(), 5);
                }


                // get formatting for timedelta
                stringBuilder.Append(GetTimeDeltaFormatting(timeDelta));

                stringBuilder.Append(".");

                // bundle it all together into a line for the embed
                embedBuilder.AddField($"{calendarEvent.Name}", stringBuilder.ToString());
            }

            // add the extra little embed bits
            embedBuilder.WithTitle("Schedule")
            .WithColor(Color.Blue)
            .WithFooter("Synced: ")
            // set the actual datetime value since discord timestamps
            // are timezone-aware (?)
            .WithTimestamp(DateTime.Now);

            // roll it all up and send it to the channel
            var embed = embedBuilder.Build();

            return(embed);
        }
 // converts stored IDs from database into ulongs (mongo can't store ulong ha ha) and use them to
 // assign our discord objects
 public void SetServerDiscordObjects(DiscordServer server)
 {
     server.DiscordServerObject = _discord.GetGuild(Convert.ToUInt64(server.ServerId));
     server.ConfigChannel       = _discord.GetChannel(Convert.ToUInt64(server.ConfigChannelId)) as ITextChannel;
     server.ReminderChannel     = _discord.GetChannel(Convert.ToUInt64(server.ReminderChannelId)) as ITextChannel;
 }