/// <summary>
 /// Gets the job arguments used by the RatingChangeJob execution. 
 /// Call this before running the job.
 /// </summary>
 /// <param name="bracket">The bracket to pull leadboard for.</param>
 /// <param name="region">The region to pull leadboard for, defaults to US.</param>
 /// <returns>Dictionary populated with job keys and properties for execution.</returns>
 public static Dictionary<string, object> GetRatingChangeJobArguments(Bracket bracket, Region region = Region.US)
 {
     return new Dictionary<string, object>
     {
         { BRACKET_KEY, bracket },
         { REGION_KEY, region }
     };
 }
        /// <summary>
        /// Executes the team clustering and db insert of this job:
        /// - Checks and resets the baseline stats 
        /// - Sorts pvp stats into team members by faction and winners/losers
        /// - Clusters the team members into team based off rating change and current rating
        /// - Inserts successful clusters into database
        /// </summary>
        /// <param name="stats">The current stats pull to cluster.</param>
        /// <param name="region">The region for this pull.</param>
        /// <param name="bracket">The arena bracket for this pull.</param>
        private void ExecuteCluster(List<PvpStats> stats, Region region, Bracket bracket)
        {
            var key = new Tuple<Region, Bracket>(region, bracket);
            var baselineStats = GetPvpStatsCache(region, bracket);
            if (baselineStats.Count == 0)  // first pass, no baseline yet
            {
                SetPvpStatsCache(key, stats);
                return;
            }

            var allyWinners = new List<TeamMember>();
            var allyLosers = new List<TeamMember>(); // faction = 0
            var hordeWinners = new List<TeamMember>();
            var hordeLosers = new List<TeamMember>(); // factionId = 1

            // sort by faction and into winners and losers
            foreach (var stat in stats)
            {
                var baseStat = baselineStats.FirstOrDefault(b => b.Name.Equals(stat.Name) &&
                                                                 b.RealmSlug.Equals(stat.RealmSlug));
                if (baseStat == null)
                    continue; // player isn't in the baseline, nothing to compare against

                if (stat.Rating == baseStat.Rating)
                    continue; // no change

                var ratingChange = stat.Rating - baseStat.Rating;
                //if (ratingChange == 0) continue; // no rating change, ignore

                var character = GetCharacter(stat, region);
                var teamMember = new TeamMember // create db model
                {
                    RatingChangeValue = ratingChange,
                    CurrentRating = stat.Rating,
                    CharacterID = character.CharacterID,
                    SpecID = character.SpecID,
                    RaceID = character.RaceID,
                    FactionID = character.FactionID,
                    GenderID = character.GenderID,
                    ModifiedDate = DateTime.Now,
                    ModifiedStatus = "I",
                    ModifiedUserID = 0,
                };

                var isAlly = stat.FactionId == 0;
                var wonGame = ratingChange > 0;

                if (isAlly)
                {
                    if (wonGame)
                        allyWinners.Add(teamMember);
                    else
                        allyLosers.Add(teamMember);
                }
                else // horde
                {
                    if (wonGame)
                        hordeWinners.Add(teamMember);
                    else
                        hordeLosers.Add(teamMember);
                }
            }

            // current stats are baseline for next pass
            SetPvpStatsCache(key, stats);

            // ensure that each group has enough players to fill at least 1 team
            var teamSize = GetTeamSize(bracket);

            if (allyWinners.Count >= teamSize)
                ClusterAndInsertDb(allyWinners, teamSize, bracket);

            if (allyLosers.Count >= teamSize)
                ClusterAndInsertDb(allyLosers, teamSize, bracket);

            if (hordeWinners.Count >= teamSize)
                ClusterAndInsertDb(hordeWinners, teamSize, bracket);

            if (hordeLosers.Count >= teamSize)
                ClusterAndInsertDb(hordeLosers, teamSize, bracket);
        }
        private DAL.Character GetCharacter(PvpStats pvpStats, Region region)
        {
            // have to use the realm ID from the DB, not the pvp stats object
            DAL.Character character = null;
            var realm = DbManager.GetRealmByName(pvpStats.RealmName, region);
            if (realm != null)
                character = DbManager.GetCharacter(pvpStats.Name, realm.RealmID);

            if (character == null)
            {
                // no matching character, insert into db
                DbManager.InsertCharacter(pvpStats, region);

                // realm id will have been resolved after character insert
                realm = DbManager.GetRealmByName(pvpStats.RealmName, region);

                // refetch after insert
                character = DbManager.GetCharacter(pvpStats.Name, realm.RealmID);
            }

            if (character == null)
                throw new ArgumentException(nameof(character)); // if happens, something failed at db layer

            // need to update or insert the pvp stat on each pass
            var dbPvpStat = DbManager.GetPvpStatByCharacterId(character.CharacterID);
            if (dbPvpStat == null)
                DbManager.InsertPvpStats(pvpStats, character.CharacterID);
            else
                DbManager.UpdatePvpStats(pvpStats, dbPvpStat);

            return character;
        }
        /// <summary>
        /// Gets the cached baseline PvpStats list from the dictionary.
        /// </summary>
        private List<PvpStats> GetPvpStatsCache(Region region, Bracket bracket)
        {
            var key = new Tuple<Region, Bracket>(region, bracket);

            if (_baselineStatsDictionary.ContainsKey(key))
                return _baselineStatsDictionary[key];

            // key doesn't exist, add new pvp stats list
            try
            {
                if (!_baselineStatsDictionary.TryAdd(key, new List<PvpStats>()))
                {
                    LoggingUtil.LogMessage(DateTime.Now, $"Throwing new exception, unable to add {key} to stats cache.");
                    throw new Exception($"unable to add { key } to stats cache.");
                }
            }
            catch (ArgumentNullException e)
            {
                LoggingUtil.LogMessage(DateTime.Now, "Null argument exception caught: " + e);
            }
            catch (OverflowException ex)
            {
                LoggingUtil.LogMessage(DateTime.Now, "Overflow exception caught: " + ex);
            }

            return _baselineStatsDictionary[key];
        }