/// <summary>
        /// Set the solve state of some puzzle state records. In the course of setting the state, instantiate any state records that are missing on the server.
        /// </summary>
        /// <param name="context">The puzzle DB context</param>
        /// <param name="eventObj">The event we are querying from</param>
        /// <param name="puzzle">
        ///     The puzzle; if null, get all puzzles in the event.
        /// </param>
        /// <param name="team">
        ///     The team; if null, get all the teams in the event.
        /// </param>
        /// <param name="value">The solve time (null if unsolving)</param>
        /// <param name="author"></param>
        /// <returns>
        ///     A task that can be awaited for the solve/unsolve operation
        /// </returns>
        public static async Task SetSolveStateAsync(
            PuzzleServerContext context,
            Event eventObj,
            Puzzle puzzle,
            Team team,
            DateTime?value,
            PuzzleUser author = null)
        {
            IQueryable <PuzzleStatePerTeam> statesQ = PuzzleStateHelper
                                                      .GetFullReadWriteQuery(context, eventObj, puzzle, team, author);

            List <PuzzleStatePerTeam> states = await statesQ.ToListAsync();

            for (int i = 0; i < states.Count; i++)
            {
                // Only allow solved time to be modified if it is being marked as unsolved (set to null) or if it is being solved for the first time
                if (value == null || states[i].SolvedTime == null)
                {
                    // Unlock puzzles when solving them
                    if (value != null && states[i].UnlockedTime == null)
                    {
                        states[i].UnlockedTime = value;
                    }

                    states[i].SolvedTime = value;
                }
            }

            // Award hint coins
            if (value != null && puzzle != null && puzzle.HintCoinsForSolve != 0)
            {
                if (team != null)
                {
                    team.HintCoinCount += puzzle.HintCoinsForSolve;
                }
                else
                {
                    var allTeams = from Team curTeam in context.Teams
                                   where curTeam.Event == eventObj
                                   select curTeam;
                    foreach (Team curTeam in allTeams)
                    {
                        curTeam.HintCoinCount += puzzle.HintCoinsForSolve;
                    }
                }
            }

            await context.SaveChangesAsync();

            // if this puzzle got solved, look for others to unlock
            if (puzzle != null && value != null)
            {
                await UnlockAnyPuzzlesThatThisSolveUnlockedAsync(context,
                                                                 eventObj,
                                                                 puzzle,
                                                                 team,
                                                                 value.Value);
            }
        }
        public static IQueryable <Puzzle> PuzzlesCausingGlobalLockout(
            PuzzleServerContext context,
            Event eventObj,
            Team team)
        {
            DateTime now = DateTime.UtcNow;

            return(PuzzleStateHelper.GetSparseQuery(context, eventObj, null, team)
                   .Where(state => state.SolvedTime == null && state.UnlockedTime != null && state.Puzzle.MinutesOfEventLockout != 0 && state.UnlockedTime.Value + TimeSpan.FromMinutes(state.Puzzle.MinutesOfEventLockout) > now)
                   .Select((s) => s.Puzzle));
        }
        public static async Task CheckForTimedUnlocksAsync(
            PuzzleServerContext context,
            Event eventObj,
            Team team)
        {
            DateTime expiry;

            lock (TimedUnlockExpiryCache)
            {
                // throttle this by an expiry interval before we do anything even remotely expensive
                if (TimedUnlockExpiryCache.TryGetValue(team.ID, out expiry) && expiry >= DateTime.UtcNow)
                {
                    return;
                }
            }

            DateTime now = DateTime.UtcNow;

            // do the unlocks in a loop.
            // The loop will catch cascading unlocks, e.g. if someone does not hit the site between 11:59 and 12:31, catch up to the 12:30 unlocks immediately.
            while (true)
            {
                var puzzlesToSolveByTime = await PuzzleStateHelper.GetSparseQuery(context, eventObj, null, team)
                                           .Where(state => state.SolvedTime == null && state.UnlockedTime != null && state.Puzzle.MinutesToAutomaticallySolve != null && state.UnlockedTime.Value + TimeSpan.FromMinutes(state.Puzzle.MinutesToAutomaticallySolve.Value) <= now)
                                           .Select((state) => new { Puzzle = state.Puzzle, UnlockedTime = state.UnlockedTime.Value })
                                           .ToListAsync();

                foreach (var state in puzzlesToSolveByTime)
                {
                    // mark solve time as when the puzzle was supposed to complete, so cascading unlocks work properly.
                    await PuzzleStateHelper.SetSolveStateAsync(context, eventObj, state.Puzzle, team, state.UnlockedTime + TimeSpan.FromMinutes(state.Puzzle.MinutesToAutomaticallySolve.Value));
                }

                // get out of the loop if we did nothing
                if (puzzlesToSolveByTime.Count == 0)
                {
                    break;
                }
            }

            lock (TimedUnlockExpiryCache)
            {
                // effectively, expiry = Math.Max(DateTime.UtcNow, LastGlobalExpiry) + ClosestExpirySpacing - if you could use Math.Max on DateTime
                expiry = DateTime.UtcNow;
                if (expiry < LastGlobalExpiry)
                {
                    expiry = LastGlobalExpiry;
                }
                expiry += ClosestExpirySpacing;

                TimedUnlockExpiryCache[team.ID] = expiry;
                LastGlobalExpiry = expiry;
            }
        }
        /// <summary>
        /// Set the unlock state of some puzzle state records. In the course of setting the state, instantiate any state records that are missing on the server.
        /// </summary>
        /// <param name="context">The puzzle DB context</param>
        /// <param name="eventObj">The event we are querying from</param>
        /// <param name="puzzle">The puzzle; if null, get all puzzles in the event.</param>
        /// <param name="team">The team; if null, get all the teams in the event.</param>
        /// <param name="value">The unlock time (null if relocking)</param>
        /// <returns>A task that can be awaited for the unlock/lock operation</returns>
        public static async Task SetUnlockStateAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team, DateTime?value, PuzzleUser author = null)
        {
            IQueryable <PuzzleStatePerTeam> statesQ = await PuzzleStateHelper.GetFullReadWriteQueryAsync(context, eventObj, puzzle, team, author);

            List <PuzzleStatePerTeam> states = await statesQ.ToListAsync();

            for (int i = 0; i < states.Count; i++)
            {
                states[i].UnlockedTime = value;
            }
            await context.SaveChangesAsync();
        }
        /// <summary>
        /// Set the solve state of some puzzle state records. In the course of setting the state, instantiate any state records that are missing on the server.
        /// </summary>
        /// <param name="context">The puzzle DB context</param>
        /// <param name="eventObj">The event we are querying from</param>
        /// <param name="puzzle">
        ///     The puzzle; if null, get all puzzles in the event.
        /// </param>
        /// <param name="team">
        ///     The team; if null, get all the teams in the event.
        /// </param>
        /// <param name="value">The solve time (null if unsolving)</param>
        /// <param name="author"></param>
        /// <returns>
        ///     A task that can be awaited for the solve/unsolve operation
        /// </returns>
        public static async Task SetSolveStateAsync(
            PuzzleServerContext context,
            Event eventObj,
            Puzzle puzzle,
            Team team,
            DateTime?value,
            PuzzleUser author = null)
        {
            IQueryable <PuzzleStatePerTeam> statesQ = await PuzzleStateHelper
                                                      .GetFullReadWriteQueryAsync(context, eventObj, puzzle, team, author);

            List <PuzzleStatePerTeam> states = await statesQ.ToListAsync();

            for (int i = 0; i < states.Count; i++)
            {
                states[i].SolvedTime = value;
            }

            // Award hint coins
            if (value != null && puzzle != null && puzzle.HintCoinsForSolve != 0)
            {
                if (team != null)
                {
                    team.HintCoinCount += puzzle.HintCoinsForSolve;
                }
                else
                {
                    var allTeams = from Team curTeam in context.Teams
                                   where curTeam.Event == eventObj
                                   select curTeam;
                    foreach (Team curTeam in allTeams)
                    {
                        curTeam.HintCoinCount += puzzle.HintCoinsForSolve;
                    }
                }
            }

            await context.SaveChangesAsync();

            // if this puzzle got solved, look for others to unlock
            if (value != null)
            {
                await UnlockAnyPuzzlesThatThisSolveUnlockedAsync(context,
                                                                 eventObj,
                                                                 puzzle,
                                                                 team,
                                                                 value.Value);
            }
        }
        /// <summary>
        /// Set the unlock state of some puzzle state records. In the course of setting the state, instantiate any state records that are missing on the server.
        /// </summary>
        /// <param name="context">The puzzle DB context</param>
        /// <param name="eventObj">The event we are querying from</param>
        /// <param name="puzzle">The puzzle; if null, get all puzzles in the event.</param>
        /// <param name="team">The team; if null, get all the teams in the event.</param>
        /// <param name="value">The unlock time (null if relocking)</param>
        /// <returns>A task that can be awaited for the unlock/lock operation</returns>
        public static async Task SetUnlockStateAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team, DateTime?value, PuzzleUser author = null)
        {
            IQueryable <PuzzleStatePerTeam> statesQ = PuzzleStateHelper.GetFullReadWriteQuery(context, eventObj, puzzle, team, author);
            List <PuzzleStatePerTeam>       states  = await statesQ.ToListAsync();

            for (int i = 0; i < states.Count; i++)
            {
                // Only allow unlock time to be modified if we were relocking it (setting it to null) or unlocking it for the first time
                if (value == null || states[i].UnlockedTime == null)
                {
                    states[i].UnlockedTime = value;
                }
            }
            await context.SaveChangesAsync();
        }
        public static async Task CheckForTimedUnlocksAsync(
            PuzzleServerContext context,
            Event eventObj,
            Team team)
        {
            DateTime expiry;

            lock (TimedUnlockExpiryCache)
            {
                // throttle this by an expiry interval before we do anything even remotely expensive
                if (TimedUnlockExpiryCache.TryGetValue(team.ID, out expiry) && expiry >= DateTime.UtcNow)
                {
                    return;
                }
            }

            DateTime now = DateTime.UtcNow;
            var      puzzlesToSolveByTime = await PuzzleStateHelper.GetSparseQuery(context, eventObj, null, team)
                                            .Where(state => state.SolvedTime == null && state.UnlockedTime != null && state.Puzzle.MinutesToAutomaticallySolve != null && state.UnlockedTime.Value + TimeSpan.FromMinutes(state.Puzzle.MinutesToAutomaticallySolve.Value) <= now)
                                            .Select(state => state.Puzzle)
                                            .ToListAsync();

            foreach (Puzzle puzzle in puzzlesToSolveByTime)
            {
                await PuzzleStateHelper.SetSolveStateAsync(context, eventObj, puzzle, team, DateTime.UtcNow);
            }

            lock (TimedUnlockExpiryCache)
            {
                // effectively, expiry = Math.Max(DateTime.UtcNow, LastGlobalExpiry) + ClosestExpirySpacing - if you could use Math.Max on DateTime
                expiry = DateTime.UtcNow;
                if (expiry < LastGlobalExpiry)
                {
                    expiry = LastGlobalExpiry;
                }
                expiry += ClosestExpirySpacing;

                TimedUnlockExpiryCache[team.ID] = expiry;
                LastGlobalExpiry = expiry;
            }
        }
        /// <summary>
        /// Set the email only mode of some puzzle state records. In the course
        /// of setting the state, instantiate any state records that are
        /// missing on the server.
        /// </summary>
        /// <param name="context">The puzzle DB context</param>
        /// <param name="eventObj">The event we are querying from</param>
        /// <param name="puzzle">
        ///     The puzzle; if null, get all puzzles in the event.
        /// </param>
        /// <param name="team">
        ///     The team; if null, get all the teams in the event.
        /// </param>
        /// <param name="value">The new email only state for the puzzle</param>
        /// <param name="author"></param>
        /// <returns>
        ///     A task that can be awaited for the lockout operation
        /// </returns>
        public static async Task SetEmailOnlyModeAsync(
            PuzzleServerContext context,
            Event eventObj,
            Puzzle puzzle,
            Team team,
            bool value,
            PuzzleUser author = null)
        {
            IQueryable <PuzzleStatePerTeam> statesQ = await PuzzleStateHelper
                                                      .GetFullReadWriteQueryAsync(context, eventObj, puzzle, team, author);

            List <PuzzleStatePerTeam> states = await statesQ.ToListAsync();

            for (int i = 0; i < states.Count; i++)
            {
                states[i].IsEmailOnlyMode = value;
                if (value == true)
                {
                    states[i].WrongSubmissionCountBuffer += 50;
                }
            }

            await context.SaveChangesAsync();
        }
        /// <summary>
        /// Unlock any puzzles that need to be unlocked due to the recent solve of a prerequisite.
        /// </summary>
        /// <param name="context">The puzzle DB context</param>
        /// <param name="eventObj">The event we are working in</param>
        /// <param name="puzzleJustSolved">The puzzle just solved; if null, all the puzzles in the event (which will make more sense once we add per author filtering)</param>
        /// <param name="team">The team that just solved; if null, all the teams in the event.</param>
        /// <param name="unlockTime">The time that the puzzle should be marked as unlocked.</param>
        /// <returns></returns>
        private static async Task UnlockAnyPuzzlesThatThisSolveUnlockedAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzleJustSolved, Team team, DateTime unlockTime)
        {
            // a simple query for all puzzle IDs in the event - will be used at least once below
            IQueryable <int> allPuzzleIDsQ = context.Puzzles.Where(p => p.Event == eventObj).Select(p => p.ID);

            // if we solved a group of puzzles, every puzzle needs an update.
            // if we solved a single puzzle, only update the puzzles that have that one as a prerequisite.
            IQueryable <int> needsUpdatePuzzleIDsQ =
                puzzleJustSolved == null ?
                allPuzzleIDsQ :
                context.Prerequisites.Where(pre => pre.Prerequisite == puzzleJustSolved).Select(pre => pre.PuzzleID).Distinct();

            // get the prerequisites for all puzzles that need an update
            // information we get per puzzle: { id, min count, prerequisite IDs }
            var prerequisiteDataForNeedsUpdatePuzzles = await context.Prerequisites
                                                        .Where(pre => needsUpdatePuzzleIDsQ.Contains(pre.PuzzleID))
                                                        .GroupBy(pre => pre.Puzzle)
                                                        .Select(g => new {
                PuzzleID = g.Key.ID,
                g.Key.MinPrerequisiteCount,
                PrerequisiteIDs = g.Select(pre => pre.PrerequisiteID)
            })
                                                        .ToListAsync();

            // Are we updating one team or all teams?
            List <Team> teamsToUpdate = team == null ? await context.Teams.Where(t => t.Event == eventObj).ToListAsync() : new List <Team>()
            {
                team
            };

            // Update teams one at a time
            foreach (Team t in teamsToUpdate)
            {
                // Collect the IDs of all solved/unlocked puzzles for this team
                // sparse lookup is fine since if the state is missing it isn't unlocked or solved!
                var puzzleStateForTeamT = await PuzzleStateHelper.GetSparseQuery(context, eventObj, null, t)
                                          .Select(state => new { state.PuzzleID, state.UnlockedTime, state.SolvedTime })
                                          .ToListAsync();

                // Make a hash set out of them for easy lookup in case we have several prerequisites to chase
                HashSet <int> unlockedPuzzleIDsForTeamT = new HashSet <int>();
                HashSet <int> solvedPuzzleIDsForTeamT   = new HashSet <int>();

                foreach (var puzzleState in puzzleStateForTeamT)
                {
                    if (puzzleState.UnlockedTime != null)
                    {
                        unlockedPuzzleIDsForTeamT.Add(puzzleState.PuzzleID);
                    }

                    if (puzzleState.SolvedTime != null)
                    {
                        solvedPuzzleIDsForTeamT.Add(puzzleState.PuzzleID);
                    }
                }

                // now loop through all puzzles and count up who needs to be unlocked
                foreach (var puzzleToUpdate in prerequisiteDataForNeedsUpdatePuzzles)
                {
                    // already unlocked? skip
                    if (unlockedPuzzleIDsForTeamT.Contains(puzzleToUpdate.PuzzleID))
                    {
                        continue;
                    }

                    // Enough puzzles unlocked by count? Let's unlock it
                    if (puzzleToUpdate.PrerequisiteIDs.Where(id => solvedPuzzleIDsForTeamT.Contains(id)).Count() >= puzzleToUpdate.MinPrerequisiteCount)
                    {
                        PuzzleStatePerTeam state = await context.PuzzleStatePerTeam.Where(s => s.PuzzleID == puzzleToUpdate.PuzzleID && s.Team == t).FirstOrDefaultAsync();

                        if (state == null)
                        {
                            context.PuzzleStatePerTeam.Add(new DataModel.PuzzleStatePerTeam()
                            {
                                PuzzleID = puzzleToUpdate.PuzzleID, Team = t, UnlockedTime = unlockTime
                            });
                        }
                        else
                        {
                            state.UnlockedTime = unlockTime;
                        }
                    }
                }
            }

            // after looping through all teams, send one update with all changes made
            await context.SaveChangesAsync();
        }
        /// <summary>
        /// Unlock any puzzles that need to be unlocked due to the recent solve of a prerequisite.
        /// </summary>
        /// <param name="context">The puzzle DB context</param>
        /// <param name="eventObj">The event we are working in</param>
        /// <param name="puzzleJustSolved">The puzzle just solved</param>
        /// <param name="team">The team that just solved; if null, all the teams in the event.</param>
        /// <param name="unlockTime">The time that the puzzle should be marked as unlocked.</param>
        /// <returns></returns>
        private static async Task UnlockAnyPuzzlesThatThisSolveUnlockedAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzleJustSolved, Team team, DateTime unlockTime)
        {
            // a simple query for all puzzle IDs in the event - will be used at least once below
            IQueryable <int> allPuzzleIDsQ = context.Puzzles.Where(p => p.Event == eventObj).Select(p => p.ID);


            // get the prerequisites for all puzzles that need an update
            // information we get per puzzle: { id, min count, number of solved prereqs including this one }
            var prerequisiteDataForNeedsUpdatePuzzles = (from possibleUnlock in context.Prerequisites
                                                         join unlockedBy in context.Prerequisites on possibleUnlock.PuzzleID equals unlockedBy.PuzzleID
                                                         join pspt in context.PuzzleStatePerTeam on unlockedBy.PrerequisiteID equals pspt.PuzzleID
                                                         join puz in context.Puzzles on unlockedBy.PrerequisiteID equals puz.ID
                                                         where possibleUnlock.Prerequisite == puzzleJustSolved && (team == null || pspt.TeamID == team.ID) && pspt.SolvedTime != null
                                                         group puz by new { unlockedBy.PuzzleID, unlockedBy.Puzzle.MinPrerequisiteCount, pspt.TeamID } into g
                                                         select new
            {
                PuzzleID = g.Key.PuzzleID,
                TeamID = g.Key.TeamID,
                g.Key.MinPrerequisiteCount,
                TotalPrerequisiteCount = g.Sum(p => (p.PrerequisiteWeight ?? 1))
            }).ToList();

            // Are we updating one team or all teams?
            List <Team> teamsToUpdate = team == null ? await context.Teams.Where(t => t.Event == eventObj).ToListAsync() : new List <Team>()
            {
                team
            };

            // Update teams one at a time
            foreach (Team t in teamsToUpdate)
            {
                // Collect the IDs of all solved/unlocked puzzles for this team
                // sparse lookup is fine since if the state is missing it isn't unlocked or solved!
                var puzzleStateForTeamT = await PuzzleStateHelper.GetSparseQuery(context, eventObj, null, t)
                                          .Select(state => new { state.PuzzleID, state.UnlockedTime, state.SolvedTime })
                                          .ToListAsync();

                // Make a hash set out of them for easy lookup in case we have several prerequisites to chase
                HashSet <int> unlockedPuzzleIDsForTeamT = new HashSet <int>();
                HashSet <int> solvedPuzzleIDsForTeamT   = new HashSet <int>();

                foreach (var puzzleState in puzzleStateForTeamT)
                {
                    if (puzzleState.UnlockedTime != null)
                    {
                        unlockedPuzzleIDsForTeamT.Add(puzzleState.PuzzleID);
                    }

                    if (puzzleState.SolvedTime != null)
                    {
                        solvedPuzzleIDsForTeamT.Add(puzzleState.PuzzleID);
                    }
                }

                // now loop through all puzzles and count up who needs to be unlocked
                foreach (var puzzleToUpdate in prerequisiteDataForNeedsUpdatePuzzles)
                {
                    // already unlocked? skip
                    if (unlockedPuzzleIDsForTeamT.Contains(puzzleToUpdate.PuzzleID))
                    {
                        continue;
                    }

                    // Enough puzzles unlocked by count? Let's unlock it
                    if (puzzleToUpdate.TeamID == t.ID && puzzleToUpdate.TotalPrerequisiteCount >= puzzleToUpdate.MinPrerequisiteCount)
                    {
                        PuzzleStatePerTeam state = await context.PuzzleStatePerTeam.Where(s => s.PuzzleID == puzzleToUpdate.PuzzleID && s.Team == t).FirstAsync();

                        state.UnlockedTime = unlockTime;
                    }
                }
            }

            // after looping through all teams, send one update with all changes made
            await context.SaveChangesAsync();
        }