public async Task<IActionResult> GetVillageArmy(long villageId, int? morale)
        {
            if (!await CanReadVillage(villageId))
                return StatusCode(401);

            PreloadWorldData();

            var uploadHistory = await Profile("Get user upload history", () =>
                context.UserUploadHistory.Where(h => h.Uid == CurrentUserId).FirstOrDefaultAsync()
            );

            var validationInfo = UploadRestrictionsValidate.ValidateInfo.FromMapRestrictions(CurrentUser, uploadHistory);
            List<String> needsUpdateReasons = UploadRestrictionsValidate.GetNeedsUpdateReasons(CurrentServerTime, validationInfo);

            if (needsUpdateReasons != null && needsUpdateReasons.Any())
            {
                return StatusCode(423, needsUpdateReasons.Select(r => Translate(r)).ToList()); // Status code "Locked"
            }

            //  Start getting village data

            var currentVillage = await Profile("Get current village", () => (
                from cv in CurrentSets.CurrentVillage
                                    .Include(v => v.ArmyAtHome)
                                    .Include(v => v.ArmyOwned)
                                    .Include(v => v.ArmyStationed)
                                    .Include(v => v.ArmyTraveling)
                                    .Include(v => v.ArmyRecentLosses)
                                    .Include(v => v.CurrentBuilding)
                where cv.VillageId == villageId
                select cv
            ).FirstOrDefaultAsync());

            var commandsToVillage = await Profile("Get commands to village", () => (
                from command in CurrentSets.Command
                                            .Include(c => c.Army)
                where command.TargetVillageId == villageId
                where !command.IsReturning
                where command.LandsAt > CurrentServerTime
                select command
            ).ToListAsync());

            var latestConquerTimestamp = await Profile("Get latest conquer", () => (
                from conquer in CurrentSets.Conquer
                where conquer.VillageId == villageId
                orderby conquer.UnixTimestamp descending
                select conquer.UnixTimestamp
            ).FirstOrDefaultAsync());
            
            var jsonData = new JSON.VillageData();

            //  Return empty data if no data is available for the village
            if (currentVillage == null)
                return Ok(jsonData);

            Profile("Populate JSON data", () =>
            {
                if (currentVillage.ArmyOwned?.LastUpdated != null)
                {
                    jsonData.OwnedArmy = ArmyConvert.ArmyToJson(currentVillage.ArmyOwned);
                    jsonData.OwnedArmySeenAt = currentVillage.ArmyOwned.LastUpdated;
                }

                if (currentVillage.ArmyRecentLosses?.LastUpdated != null)
                {
                    jsonData.RecentlyLostArmy = ArmyConvert.ArmyToJson(currentVillage.ArmyRecentLosses);
                    jsonData.RecentlyLostArmySeenAt = currentVillage.ArmyRecentLosses.LastUpdated;
                }

                if (currentVillage.ArmyStationed?.LastUpdated != null)
                {
                    jsonData.StationedArmy = ArmyConvert.ArmyToJson(currentVillage.ArmyStationed);
                    jsonData.StationedSeenAt = currentVillage.ArmyStationed.LastUpdated;
                }

                if (currentVillage.ArmyTraveling?.LastUpdated != null)
                {
                    jsonData.TravelingArmy = ArmyConvert.ArmyToJson(currentVillage.ArmyTraveling);
                    jsonData.TravelingSeenAt = currentVillage.ArmyTraveling.LastUpdated;
                }

                if (currentVillage.ArmyAtHome?.LastUpdated != null)
                {
                    jsonData.AtHomeArmy = ArmyConvert.ArmyToJson(currentVillage.ArmyAtHome);
                    jsonData.AtHomeSeenAt = currentVillage.ArmyAtHome.LastUpdated;
                }

                jsonData.LastLoyalty = currentVillage.Loyalty;
                jsonData.LastLoyaltySeenAt = currentVillage.LoyaltyLastUpdated;

                if (currentVillage.Loyalty != null)
                {
                    var loyaltyCalculator = new LoyaltyCalculator(CurrentWorldSettings.GameSpeed);
                    jsonData.PossibleLoyalty = loyaltyCalculator.PossibleLoyalty(currentVillage.Loyalty.Value, CurrentServerTime - currentVillage.LoyaltyLastUpdated.Value);
                }

                jsonData.LastBuildings = BuildingConvert.CurrentBuildingToJson(currentVillage.CurrentBuilding);
                jsonData.LastBuildingsSeenAt = currentVillage.CurrentBuilding?.LastUpdated;

                if (currentVillage.CurrentBuilding?.LastUpdated != null)
                {
                    var constructionCalculator = new ConstructionCalculator();
                    jsonData.PossibleBuildings = constructionCalculator.CalculatePossibleBuildings(jsonData.LastBuildings, CurrentServerTime - currentVillage.CurrentBuilding.LastUpdated.Value);
                }

                if (currentVillage.ArmyStationed?.LastUpdated != null)
                {
                    var battleSimulator = new BattleSimulator();
                    short wallLevel = currentVillage.CurrentBuilding?.Wall ?? 20;
                    short hqLevel = currentVillage.CurrentBuilding?.Main ?? 20;

                    if (currentVillage.CurrentBuilding != null)
                        wallLevel += new ConstructionCalculator().CalculateLevelsInTimeSpan(BuildingType.Wall, hqLevel, wallLevel, CurrentServerTime - currentVillage.CurrentBuilding.LastUpdated.Value);

                    var nukeEstimation = battleSimulator.EstimateRequiredNukes(jsonData.StationedArmy, wallLevel, CurrentWorldSettings.ArchersEnabled, morale ?? 100);

                    jsonData.NukesRequired = nukeEstimation.NukesRequired;
                    jsonData.LastNukeLossPercent = (int)(nukeEstimation.LastNukeLossesPercent);
                }

                //  Might have CurrentArmy entries but they're just empty/null - not based on any report data
                if (jsonData.OwnedArmy != null && (jsonData.OwnedArmySeenAt == null || jsonData.OwnedArmy.Count == 0))
                {
                    jsonData.OwnedArmy = null;
                    jsonData.OwnedArmySeenAt = null;
                }

                if (jsonData.StationedArmy != null && (jsonData.StationedSeenAt == null || jsonData.StationedArmy.Count == 0))
                {
                    jsonData.StationedArmy = null;
                    jsonData.StationedSeenAt = null;
                }

                if (jsonData.TravelingArmy != null && (jsonData.TravelingSeenAt == null || jsonData.TravelingArmy.Count == 0))
                {
                    jsonData.TravelingArmy = null;
                    jsonData.TravelingSeenAt = null;
                }

                if (jsonData.RecentlyLostArmy != null && (jsonData.RecentlyLostArmySeenAt == null || jsonData.RecentlyLostArmy.Count == 0))
                {
                    jsonData.RecentlyLostArmy = null;
                    jsonData.RecentlyLostArmySeenAt = null;
                }


                var armyCalculator = new RecruitmentCalculator(2, jsonData.LastBuildings);
                DateTime? localArmyLastSeenAt = null;
                int? availableArmyPopulation = null;

                if (jsonData.StationedArmy != null)
                {
                    localArmyLastSeenAt = jsonData.StationedSeenAt.Value;
                    var existingPop = ArmyStats.CalculateTotalPopulation(jsonData.StationedArmy);
                    availableArmyPopulation = Math.Max(0, armyCalculator.MaxPopulation - existingPop);
                }

                var conquerTime = DateTimeOffset.FromUnixTimeMilliseconds(latestConquerTimestamp).UtcDateTime;

                bool useConquer = false;
                if (localArmyLastSeenAt == null)
                    useConquer = true;
                else
                    useConquer = conquerTime > localArmyLastSeenAt.Value;

                if (useConquer)
                {
                    localArmyLastSeenAt = conquerTime;
                    availableArmyPopulation = armyCalculator.MaxPopulation;
                }

                //  Add recruitment estimations
                if (localArmyLastSeenAt != null)
                {
                    var timeSinceSeen = CurrentServerTime - localArmyLastSeenAt.Value;
                    armyCalculator.MaxPopulation = availableArmyPopulation.Value;

                    //  No point in estimating troops if there's been 2 weeks since we saw stationed troops
                    if (timeSinceSeen.TotalDays < 14)
                    {
                        jsonData.PossibleRecruitedOffensiveArmy = armyCalculator.CalculatePossibleOffenseRecruitment(timeSinceSeen);
                        jsonData.PossibleRecruitedDefensiveArmy = armyCalculator.CalculatePossibleDefenseRecruitment(timeSinceSeen);
                    }
                }

                //  Add command summaries
                jsonData.DVs = new Dictionary<long, int>();
                jsonData.Fakes = new List<long>();
                jsonData.Nukes = new List<long>();
                jsonData.Nobles = new List<long>();

                jsonData.Players = commandsToVillage.Select(c => c.SourcePlayerId).Distinct().ToList();

                foreach (var command in commandsToVillage.Where(c => c.Army != null))
                {
                    var army = ArmyConvert.ArmyToJson(command.Army);
                    var offensivePop = ArmyStats.CalculateTotalPopulation(army.OfType(JSON.UnitBuild.Offensive));
                    var defensivePop = ArmyStats.CalculateTotalPopulation(army.OfType(JSON.UnitBuild.Defensive));
                    var isNoble = command.Army.Snob > 0 && command.IsAttack;

                    bool isFake = false;
                    bool isNuke = false;
                    if (!army.Values.Any(cnt => cnt > 1) && !isNoble)
                    {
                        isFake = true;
                    }
                    else if (command.IsAttack && offensivePop > 10000)
                    {
                        isNuke = true;
                    }

                    if (isFake)
                        jsonData.Fakes.Add(command.CommandId);
                    else if (isNuke)
                        jsonData.Nukes.Add(command.CommandId);
                    else if (defensivePop > 3000 && !command.IsAttack)
                        jsonData.DVs.Add(command.CommandId, defensivePop);

                    if (isNoble)
                        jsonData.Nobles.Add(command.CommandId);
                }
            });

            return Ok(jsonData);
        }
        public async Task<IActionResult> GetCommandsRegardingVillage(long villageId)
        {
            if (!await CanReadVillage(villageId))
                return StatusCode(401);

            var uploadHistory = await context.UserUploadHistory.Where(u => u.Uid == CurrentUserId).FirstOrDefaultAsync();
            var validationInfo = UploadRestrictionsValidate.ValidateInfo.FromMapRestrictions(CurrentUser, uploadHistory);
            var needsUpdateReasons = UploadRestrictionsValidate.GetNeedsUpdateReasons(CurrentServerTime, validationInfo);

            if (needsUpdateReasons != null && needsUpdateReasons.Any())
            {
                return StatusCode(423, needsUpdateReasons.Select(r => Translate(r)).ToList()); // Status code "Locked"
            }

            var commandsFromVillage = await Profile("Get commands from village", () => (
                    from command in CurrentSets.Command
                                               .FromWorld(CurrentWorldId)
                                               .Include(c => c.Army)
                    where command.SourceVillageId == villageId
                    where command.ReturnsAt > CurrentServerTime
                    orderby command.ReturnsAt ascending
                    select command
                ).ToListAsync());

            foreach (var command in commandsFromVillage.Where(c => !c.IsReturning))
            {
                if (command.LandsAt <= CurrentServerTime)
                    command.IsReturning = true;
            }

            await context.SaveChangesAsync();
            

            var targetVillageIds = commandsFromVillage.Select(c => c.TargetVillageId).Distinct();
            var targetVillages = await Profile("Get other villages", () => (
                    from village in CurrentSets.Village
                    where targetVillageIds.Contains(village.VillageId)
                    select village
                ).ToListAsync());

            var targetVillagesById = targetVillages.ToDictionary(v => v.VillageId, v => v);

            var result = new JSON.VillageCommandSet();

            if (commandsFromVillage != null && commandsFromVillage.Count > 0)
            {
                result.CommandsFromVillage = new List<JSON.VillageCommand>();

                foreach (var command in commandsFromVillage)
                {
                    var commandData = new JSON.VillageCommand();
                    commandData.CommandId = command.CommandId;
                    commandData.LandsAt = command.LandsAt;
                    commandData.ReturnsAt = command.ReturnsAt.Value;
                    commandData.Army = ArmyConvert.ArmyToJson(command.Army);
                    commandData.IsReturning = command.IsReturning;
                    commandData.TroopType = command.TroopType;
                    commandData.OtherVillageId = command.TargetVillageId;

                    var otherVillage = targetVillagesById[command.TargetVillageId];
                    commandData.OtherVillageName = otherVillage.VillageName.UrlDecode();
                    commandData.OtherVillageCoords = $"{otherVillage.X}|{otherVillage.Y}";

                    result.CommandsFromVillage.Add(commandData);
                }
            }

            return Ok(result);
        }
        public async Task<IActionResult> GetMapTags(int x, int y, int width, int height)
        {
            var uploadHistory = await context.UserUploadHistory.Where(u => u.Uid == CurrentUserId).FirstOrDefaultAsync();
            var validationInfo = UploadRestrictionsValidate.ValidateInfo.FromMapRestrictions(CurrentUser, uploadHistory);
            var needsUpdateReasons = UploadRestrictionsValidate.GetNeedsUpdateReasons(CurrentServerTime, validationInfo);

            if (needsUpdateReasons != null && needsUpdateReasons.Any())
            {
                return StatusCode(423, needsUpdateReasons.Select(r => Translate(r)).ToList()); // Status code "Locked"
            }

            var vaultTribes = await Profile("Get tribe IDs", () => (
                from user in CurrentSets.User
                join player in CurrentSets.Player on user.PlayerId equals player.PlayerId
                where player.TribeId != null && user.Enabled
                select player.TribeId.Value
            ).Distinct().ToListAsync());

            var enemyTribes = await CurrentSets.EnemyTribe.Select(et => et.EnemyTribeId).ToListAsync();

            var villageData = await Profile("Get village data", () => (
                from currentVillage in CurrentSets.CurrentVillage
                                                  .Include(cv => cv.ArmyOwned)
                                                  .Include(cv => cv.ArmyTraveling)
                                                  .Include(cv => cv.ArmyStationed)
                                                  .Include(cv => cv.CurrentBuilding)

                join village in CurrentSets.Village on currentVillage.VillageId equals village.VillageId
                join player in CurrentSets.Player on village.PlayerId equals player.PlayerId
                where CurrentUserIsAdmin || player.TribeId == null || !vaultTribes.Contains(player.TribeId.Value)

                // Right now we're not doing lazy-loading of data in the script, so this is just a potentially-costly
                // and pointless calculation.
                //where village.X >= x && village.Y >= y && village.X <= x + width && village.Y <= y + height
                select new { CurrentVillage = currentVillage, player.PlayerId, player.TribeId }
            ).ToListAsync());

            var ownVillageData = await Profile("Get own village data", () => (
                from village in CurrentSets.Village
                join currentVillage in CurrentSets.CurrentVillage on village.VillageId equals currentVillage.VillageId
                where village.PlayerId == CurrentPlayerId
                select new { Village = village, currentVillage.ArmyAtHome }
            ).ToListAsync());

            var validVillages = villageData.Where(vd => vd.PlayerId != CurrentPlayerId).ToList();
            var validVillageIds = villageData.Select(d => d.CurrentVillage.VillageId).ToList();

            var returningCommandData = await Profile("Get returning command data", () => (
                from command in CurrentSets.Command
                where command.ReturnsAt > CurrentServerTime && command.ArmyId != null && command.IsReturning
                select new { command.SourceVillageId, command.TargetVillageId, command.Army }
            ).ToListAsync());

            var sendingCommandData = await Profile("Get sent command data", () => (
                from command in CurrentSets.Command
                where command.ReturnsAt > CurrentServerTime && command.ArmyId != null && !command.IsReturning
                select new { command.SourceVillageId, command.TargetVillageId, command.Army }
            ).ToListAsync());

            var returningCommandsBySourceVillageId = validVillageIds.ToDictionary(id => id, id => new List<Scaffold.CommandArmy>());
            foreach (var command in returningCommandData)
            {
                if (returningCommandsBySourceVillageId.ContainsKey(command.SourceVillageId))
                    returningCommandsBySourceVillageId[command.SourceVillageId].Add(command.Army);
            }

            var commandsByTargetVillageId = validVillageIds.ToDictionary(id => id, id => new List<Scaffold.CommandArmy>());
            foreach (var command in sendingCommandData)
            {
                if (commandsByTargetVillageId.ContainsKey(command.TargetVillageId))
                    commandsByTargetVillageId[command.TargetVillageId].Add(command.Army);
            }

            var result = new Dictionary<long, JSON.VillageTags>();

            var tribeIds = validVillages.Select(vv => vv.TribeId).Where(t => t != null).Select(t => t.Value).Distinct();
            var tribeNames = await Profile("Get tribe names", () => (
                from tribe in CurrentSets.Ally
                where tribeIds.Contains(tribe.TribeId)
                select new { tribe.Tag, tribe.TribeId }
            ).ToListAsync());

            var tribeNamesById = tribeIds.ToDictionary(tid => tid, tid => tribeNames.First(tn => tn.TribeId == tid).Tag.UrlDecode());

            Profile("Generate JSON tags", () =>
            {
                foreach (var data in validVillages)
                {
                    var village = data.CurrentVillage;
                    var tag = new JSON.VillageTags();
                    tag.TribeName = data.TribeId == null ? null : tribeNamesById[data.TribeId.Value];
                    tag.IsEnemyTribe = data.TribeId == null ? false : enemyTribes.Contains(data.TribeId.Value);

                    tag.WallLevel = data.CurrentVillage.CurrentBuilding?.Wall;
                    tag.WallLevelSeenAt = data.CurrentVillage.CurrentBuilding?.LastUpdated;

                    tag.WatchtowerLevel = data.CurrentVillage.CurrentBuilding?.Watchtower;
                    tag.WatchtowerSeenAt = data.CurrentVillage.CurrentBuilding?.LastUpdated;

                    bool IsNuke(Scaffold.IScaffoldArmy army)
                    {
                        var jsonArmy = ArmyConvert.ArmyToJson(army);
                        if (BattleSimulator.TotalAttackPower(jsonArmy) < 3.5e5)
                            return false;

                        if (ArmyStats.CalculateTotalPopulation(jsonArmy, TroopType.Axe, TroopType.Light, TroopType.Marcher) < 4000)
                            return false;

                        //  Check HC nuke
                        if (army.Light < 100)
                        {
                            return ArmyStats.CalculateTotalPopulation(jsonArmy, TroopType.Axe, TroopType.Heavy, TroopType.Marcher) > 15000 && army.Axe > army.Heavy;
                        }
                        else
                        {
                            //  13k pop, ie 5k axe, 2k lc
                            return ArmyStats.CalculateTotalPopulation(jsonArmy, TroopType.Axe, TroopType.Light, TroopType.Marcher) > 13000;
                        }
                    }

                    tag.ReturningTroopsPopulation = returningCommandsBySourceVillageId[data.CurrentVillage.VillageId].Sum((army) => ArmyStats.CalculateTotalPopulation(ArmyConvert.ArmyToJson(army)));
                    tag.NumTargettingNukes = commandsByTargetVillageId[data.CurrentVillage.VillageId].Count(army => IsNuke(army));

                    if (village.ArmyStationed?.LastUpdated != null)
                    {
                        // 1 DV is approx. 1.7m total defense power
                        var stationed = village.ArmyStationed;
                        var defensePower = BattleSimulator.TotalDefensePower(ArmyConvert.ArmyToJson(stationed));
                        tag.IsStacked = defensePower > 1.7e6;
                        tag.StackDVs = defensePower / (float)1.7e6;
                        tag.StackSeenAt = village.ArmyStationed.LastUpdated;
                    }

                    var validArmies = new[] {
                        village.ArmyOwned,
                        village.ArmyTraveling,
                        village.ArmyStationed
                    }.Where(a => a?.LastUpdated != null && !ArmyConvert.ArmyToJson(a).IsEmpty()).ToList();

                    var nukeArmy = (
                            from army in validArmies
                            where IsNuke(army)
                            orderby army.LastUpdated.Value descending
                            select army
                        ).FirstOrDefault();

                    var nobleArmy = (
                            from army in validArmies
                            where army.Snob > 0
                            orderby army.LastUpdated.Value descending
                            select army
                        ).FirstOrDefault();

                    if (nukeArmy != null)
                    {
                        tag.HasNuke = true;
                        tag.NukeSeenAt = nukeArmy.LastUpdated;
                    }

                    if (nobleArmy != null)
                    {
                        tag.HasNobles = true;
                        tag.NoblesSeenAt = nobleArmy.LastUpdated;
                    }

                    result.Add(village.VillageId, tag);
                }
            });

            return Ok(result);
        }
        public async Task <IActionResult> GetIncomingTags([FromBody] List <long> incomingsIds)
        {
            // Preload world data since we need world settings within queries below
            PreloadWorldData();
            //  Lots of data read but only updating some of it; whenever we do SaveChanges it checks
            //  for changes against all queried objects. Disable tracking by default and track explicitly if necessary
            context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

            var incomingData = await Profile("Get existing commands", () => (
                                                 from command in CurrentSets.Command
                                                 .Include(c => c.SourceVillage)
                                                 let currentVillage = command.SourceVillage.CurrentVillage.FirstOrDefault(cv => cv.AccessGroupId == CurrentAccessGroupId)
                                                                      where incomingsIds.Contains(command.CommandId)
                                                                      select new { Command = command, CurrentVillage = currentVillage }
                                                 ).ToListAsync()
                                             );

            if (incomingData == null)
            {
                return(NotFound());
            }

            //  Load in actual CurrentArmy data for incomings
            //  (Didn't need this previously but EF Core can be dumb, .Include on a `join .. into` doesn't actually include the given properties)
            {
                IEnumerable <long> SelectCurrentArmyIds(Scaffold.CurrentVillage currentVillage)
                {
                    if (currentVillage == null)
                    {
                        yield break;
                    }

                    if (currentVillage.ArmyOwnedId != null)
                    {
                        yield return(currentVillage.ArmyOwnedId.Value);
                    }

                    if (currentVillage.ArmyStationedId != null)
                    {
                        yield return(currentVillage.ArmyStationedId.Value);
                    }

                    if (currentVillage.ArmyTravelingId != null)
                    {
                        yield return(currentVillage.ArmyTravelingId.Value);
                    }
                }

                var currentArmyIds = incomingData.SelectMany(d => SelectCurrentArmyIds(d.CurrentVillage)).ToList();
                var currentArmies  = await CurrentSets.CurrentArmy.Where(army => currentArmyIds.Contains(army.ArmyId)).ToDictionaryAsync(a => a.ArmyId, a => a);

                foreach (var village in incomingData.Select(d => d.CurrentVillage).Where(v => v != null))
                {
                    if (village.ArmyOwnedId != null)
                    {
                        village.ArmyOwned = currentArmies[village.ArmyOwnedId.Value];
                    }
                    if (village.ArmyStationedId != null)
                    {
                        village.ArmyStationed = currentArmies[village.ArmyStationedId.Value];
                    }
                    if (village.ArmyTravelingId != null)
                    {
                        village.ArmyTraveling = currentArmies[village.ArmyTravelingId.Value];
                    }
                }
            }

            var uploadHistory = await Profile("Get user upload history", () =>
                                              context.UserUploadHistory.Where(h => h.Uid == CurrentUserId).FirstOrDefaultAsync()
                                              );

            var           validationInfo     = UploadRestrictionsValidate.ValidateInfo.FromTaggingRestrictions(CurrentUser, uploadHistory);
            List <String> needsUpdateReasons = UploadRestrictionsValidate.GetNeedsUpdateReasons(CurrentServerTime, validationInfo);

            if (needsUpdateReasons != null && needsUpdateReasons.Any())
            {
                return(StatusCode(423, needsUpdateReasons.Select(r => Translate(r)).ToList())); // Status code "Locked"
            }

            //  NOTE - We pull data for all villas requested but only return data for villas not in vaultOwnedVillages,
            //  should stop querying that other data at some point
            var commandSourceVillageIds = incomingData.Select(inc => inc.Command.SourceVillageId).Distinct().ToList();
            var commandTargetVillageIds = incomingData.Select(inc => inc.Command.TargetVillageId).Distinct().ToList();

            var relevantVillages = await(
                from village in CurrentSets.Village
                where commandSourceVillageIds.Contains(village.VillageId) || commandTargetVillageIds.Contains(village.VillageId)
                select new { village.PlayerId, village.VillageId, village.VillageName, X = village.X.Value, Y = village.Y.Value }
                ).ToDictionaryAsync(v => v.VillageId, v => v);

            var sourcePlayerIds = relevantVillages.Values.Where(v => commandSourceVillageIds.Contains(v.VillageId)).Select(v => v.PlayerId ?? 0).ToList();

            //  Don't do any tagging for villages owned by players registered with the vault (so players in other tribes
            //  also using the vault can't infer villa builds)
            var vaultOwnedVillages = await Profile("Get villages owned by vault users", () => (
                                                       from user in CurrentSets.User
                                                       join village in CurrentSets.Village on user.PlayerId equals village.PlayerId
                                                       where user.Enabled
                                                       select village.VillageId
                                                       ).ToListAsync());

            var sourcePlayerNames = await Profile("Get player names", () => (
                                                      from player in CurrentSets.Player
                                                      where sourcePlayerIds.Contains(player.PlayerId)
                                                      select new { player.PlayerId, player.PlayerName }
                                                      ).ToDictionaryAsync(p => p.PlayerId, p => p.PlayerName));

            var countsByVillage = await Profile("Get command counts", () => (
                                                    from command in CurrentSets.Command
                                                    where !command.IsReturning && command.LandsAt > CurrentServerTime
                                                    group command by command.SourceVillageId into villageCommands
                                                    select new { VillageId = villageCommands.Key, Count = villageCommands.Count() }
                                                    ).ToDictionaryAsync(vc => vc.VillageId, vc => vc.Count));

            var travelCalculator = new Features.Simulation.TravelCalculator(CurrentWorldSettings.GameSpeed, CurrentWorldSettings.UnitSpeed);

            DateTime CommandLaunchedAt(Scaffold.Command command) => command.LandsAt - travelCalculator.CalculateTravelTime(
                (command.TroopType ?? "ram").ToTroopType(),
                relevantVillages[command.SourceVillageId].X, relevantVillages[command.SourceVillageId].Y,
                relevantVillages[command.TargetVillageId].X, relevantVillages[command.TargetVillageId].Y
                );

            var earliestLaunchTime = incomingData.Select(inc => CommandLaunchedAt(inc.Command)).DefaultIfEmpty(CurrentServerTime).Min();

            var commandsReturningByVillageId = await Profile("Process returning commands for all source villages", async() =>
            {
                var commandSeenThreshold = earliestLaunchTime - TimeSpan.FromDays(1);

                var sentCommands = await Profile("Query returning commands for all source villages", () => (
                                                     from command in CurrentSets.Command.AsTracking()
                                                     .Include(c => c.Army)
                                                     where command.FirstSeenAt > commandSeenThreshold
                                                     where command.Army != null
                                                     where commandSourceVillageIds.Contains(command.SourceVillageId)
                                                     select command
                                                     ).ToListAsync());

                bool updatedCommands = false;
                var result           = commandSourceVillageIds.ToDictionary(vid => vid, vid => new List <Scaffold.Command>());

                Profile("Update command returning and sort into dictionary", () =>
                {
                    foreach (var cmd in sentCommands)
                    {
                        if (cmd.LandsAt <= CurrentServerTime)
                        {
                            if (!cmd.IsReturning)
                            {
                                updatedCommands = true;
                                cmd.IsReturning = true;
                            }

                            result[cmd.SourceVillageId].Add(cmd);
                        }
                    }
                });

                if (updatedCommands)
                {
                    await Profile("Save commands now set to returning", () => context.SaveChangesAsync());
                }

                return(result);
            });

            var otherTargetedVillageIds = commandsReturningByVillageId.SelectMany(kvp => kvp.Value).Select(c => c.TargetVillageId).Distinct().Except(relevantVillages.Keys);
            var otherTargetVillages     = await Profile("Get other villages targeted by inc source villas", () =>
                                                        CurrentSets.Village.Where(v => otherTargetedVillageIds.Contains(v.VillageId)).ToListAsync()
                                                        );

            foreach (var id in otherTargetedVillageIds)
            {
                var village = otherTargetVillages.First(v => v.VillageId == id);
                relevantVillages.Add(id, new
                {
                    village.PlayerId, village.VillageId, village.VillageName, X = village.X.Value, Y = village.Y.Value
                });
            }

            var launchTimesByCommandId = commandsReturningByVillageId.SelectMany(kvp => kvp.Value).Where(cmd => !vaultOwnedVillages.Contains(cmd.SourceVillageId)).ToDictionary(
                cmd => cmd.CommandId,
                cmd => CommandLaunchedAt(cmd)
                );

            IEnumerable <Scaffold.Command> RelevantCommandsForIncoming(Scaffold.Command incoming)
            {
                if (!relevantVillages.ContainsKey(incoming.SourceVillageId))
                {
                    return(Enumerable.Empty <Scaffold.Command>());
                }

                var launchTime        = CommandLaunchedAt(incoming);
                var returningCommands = commandsReturningByVillageId.GetValueOrDefault(incoming.SourceVillageId);

                if (returningCommands == null)
                {
                    return(Enumerable.Empty <Scaffold.Command>());
                }

                return(returningCommands.Where(cmd => cmd.ReturnsAt > launchTime || (launchTimesByCommandId.ContainsKey(cmd.CommandId) && launchTimesByCommandId[cmd.CommandId] > launchTime)));
            }

            var duplicates = incomingData.GroupBy(i => i.Command.CommandId).Where(g => g.Count() > 1).ToDictionary(g => g.Key, g => g.ToList());

            Dictionary <long, JSON.IncomingTag> resultTags = new Dictionary <long, JSON.IncomingTag>();

            Profile("Make incomings tags", () =>
            {
                foreach (var data in incomingData)
                {
                    var incoming        = data.Command;
                    var sourceVillageId = incoming.SourceVillageId;
                    if (vaultOwnedVillages.Contains(sourceVillageId))
                    {
                        continue;
                    }

                    var sourceCurrentVillage = data.CurrentVillage;
                    var commandsReturning    = RelevantCommandsForIncoming(incoming);

                    var armyOwned     = sourceCurrentVillage?.ArmyOwned;
                    var armyTraveling = sourceCurrentVillage?.ArmyTraveling;
                    var armyStationed = sourceCurrentVillage?.ArmyStationed;

                    //  TODO - Make this a setting
                    var maxUpdateTime = TimeSpan.FromDays(4);

                    if (armyOwned?.LastUpdated != null && (CurrentServerTime - armyOwned.LastUpdated.Value > maxUpdateTime))
                    {
                        armyOwned = null;
                    }

                    if (armyTraveling?.LastUpdated != null && (CurrentServerTime - armyTraveling.LastUpdated.Value > maxUpdateTime))
                    {
                        armyTraveling = null;
                    }

                    if (armyStationed?.LastUpdated != null && (CurrentServerTime - armyStationed.LastUpdated.Value > maxUpdateTime))
                    {
                        armyStationed = null;
                    }

                    if (armyOwned != null && armyOwned.IsEmpty())
                    {
                        armyOwned = null;
                    }
                    if (armyTraveling != null && armyTraveling.IsEmpty())
                    {
                        armyTraveling = null;
                    }
                    if (armyStationed != null && armyStationed.IsEmpty())
                    {
                        armyStationed = null;
                    }

                    var troopsReturning = new JSON.Army();
                    if (commandsReturning != null)
                    {
                        foreach (var command in commandsReturning)
                        {
                            troopsReturning += ArmyConvert.ArmyToJson(command.Army);
                        }
                    }

                    Scaffold.CurrentArmy effectiveArmy = null;
                    bool isConfidentArmy = true;
                    if (armyOwned != null)
                    {
                        effectiveArmy = armyOwned;
                    }
                    else if (armyTraveling != null)
                    {
                        effectiveArmy = armyTraveling;
                    }
                    else if (armyStationed != null)
                    {
                        effectiveArmy   = armyStationed;
                        isConfidentArmy = false;
                    }

                    var tag            = new JSON.IncomingTag();
                    tag.CommandId      = incoming.CommandId;
                    tag.OriginalTag    = incoming.UserLabel;
                    tag.NumFromVillage = countsByVillage.GetValueOrDefault(sourceVillageId);
                    tag.TroopType      = TroopTypeConvert.StringToTroopType(incoming.TroopType);

                    var sourceVillage       = relevantVillages[incoming.SourceVillageId];
                    var targetVillage       = relevantVillages[incoming.TargetVillageId];
                    tag.SourceVillageCoords = $"{sourceVillage.X}|{sourceVillage.Y}";
                    tag.TargetVillageCoords = $"{targetVillage.X}|{targetVillage.Y}";
                    tag.SourcePlayerName    = sourcePlayerNames.GetValueOrDefault(incoming.SourcePlayerId, Translate("UNKNOWN")).UrlDecode();
                    tag.SourceVillageName   = sourceVillage.VillageName.UrlDecode();
                    tag.TargetVillageName   = targetVillage.VillageName.UrlDecode();
                    tag.Distance            = new Coordinate {
                        X = sourceVillage.X, Y = sourceVillage.Y
                    }.DistanceTo(targetVillage.X, targetVillage.Y);

                    if (effectiveArmy != null)
                    {
                        //  TODO - Make this a setting
                        bool isOffense = ArmyStats.IsOffensive(effectiveArmy);

                        tag.VillageType = isOffense ? Translate("OFFENSE") : Translate("DEFENSE");

                        if (!isOffense && isConfidentArmy && (effectiveArmy.Snob == null || effectiveArmy.Snob == 0) && incoming.TroopType != JSON.TroopType.Snob.ToTroopString())
                        {
                            tag.DefiniteFake = true;
                        }

                        var offensiveArmy = effectiveArmy.OfType(JSON.UnitBuild.Offensive);
                        var jsonArmy      = ArmyConvert.ArmyToJson(offensiveArmy);
                        var pop           = Native.ArmyStats.CalculateTotalPopulation(jsonArmy);

                        var returningOffensiveArmy = troopsReturning.OfType(JSON.UnitBuild.Offensive);
                        var returningPop           = Native.ArmyStats.CalculateTotalPopulation(returningOffensiveArmy);

                        tag.OffensivePopulation = pop - returningPop;
                        if (tag.OffensivePopulation < 0)
                        {
                            tag.OffensivePopulation = 0;
                        }

                        if ((tag.OffensivePopulation > 100 || returningPop > 5000) && tag.OffensivePopulation < 5000 && isConfidentArmy)
                        {
                            tag.DefiniteFake = true;
                        }

                        tag.ReturningPopulation = returningPop;

                        tag.NumCats = effectiveArmy.Catapult;
                    }

                    resultTags.Add(incoming.CommandId, tag);
                }
            });

            return(Ok(resultTags));
        }