using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.EntityFrameworkCore;
using sodoff.Attributes;
using sodoff.Model;
using sodoff.Schema;
using sodoff.Services;
using sodoff.Util;
using sodoff.Configuration;
using System.Globalization;

namespace sodoff.Controllers.Common;
public class ContentController : Controller {

    private readonly DBContext ctx;
    private KeyValueService keyValueService;
    private ItemService itemService;
    private MissionStoreSingleton missionStore;
    private MissionService missionService;
    private RoomService roomService;
    private AchievementService achievementService;
    private InventoryService inventoryService;
    private GameDataService gameDataService;
    private XmlDataService xmlDataService;
    private NeighborhoodService neighborhoodService;
    private WorldIdService worldIdService;
    private BuddyService buddyService;
    private MessagingService messagingService;
    private Random random = new Random();
    private readonly IOptions<ApiServerConfig> config;
    
    public ContentController(
        DBContext ctx,
        KeyValueService keyValueService,
        ItemService itemService,
        MissionStoreSingleton missionStore,
        MissionService missionService,
        RoomService roomService,
        AchievementService achievementService,
        InventoryService inventoryService,
        GameDataService gameDataService,
        XmlDataService xmlDataService,
        NeighborhoodService neighborhoodService,
        BuddyService buddyService,
        MessagingService messagingService,
        WorldIdService worldIdService,
        IOptions<ApiServerConfig> config
    ) {
        this.ctx = ctx;
        this.keyValueService = keyValueService;
        this.itemService = itemService;
        this.missionStore = missionStore;
        this.missionService = missionService;
        this.roomService = roomService;
        this.achievementService = achievementService;
        this.inventoryService = inventoryService;
        this.gameDataService = gameDataService;
        this.xmlDataService = xmlDataService;
        this.neighborhoodService = neighborhoodService;
        this.config = config;
        this.buddyService = buddyService;
        this.messagingService = messagingService;
        this.worldIdService = worldIdService;
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetRaisedPetGrowthStates")] // used by World Of Jumpstart 1.1
    public RaisedPetGrowthState[] GetRaisedPetGrowthStates()
    {
        return new RaisedPetGrowthState[] {
            new RaisedPetGrowthState {GrowthStateID = 0, Name = "none"},
            new RaisedPetGrowthState {GrowthStateID = 1, Name = "powerup"},
            new RaisedPetGrowthState {GrowthStateID = 2, Name = "find"},
            new RaisedPetGrowthState {GrowthStateID = 3, Name = "eggInHand"},
            new RaisedPetGrowthState {GrowthStateID = 4, Name = "hatching"},
            new RaisedPetGrowthState {GrowthStateID = 5, Name = "baby"},
            new RaisedPetGrowthState {GrowthStateID = 6, Name = "child"},
            new RaisedPetGrowthState {GrowthStateID = 7, Name = "teen"},
            new RaisedPetGrowthState {GrowthStateID = 8, Name = "adult"},
        };
    }
    
    [HttpPost]
    //[Produces("application/xml")]
    [Route("ContentWebService.asmx/GetProduct")] // used by World Of Jumpstart
    [VikingSession(UseLock=false)]
    public string? GetProduct(Viking viking, [FromForm] string apiKey) {
        return Util.SavedData.Get(
            viking,
            ClientVersion.GetVersion(apiKey)
        );
    }

    [HttpPost]
    //[Produces("application/xml")]
    [Route("ContentWebService.asmx/SetProduct")] // used by World Of Jumpstart
    [VikingSession(UseLock=true)]
    public bool SetProduct(Viking viking, [FromForm] string contentXml, [FromForm] string apiKey) {
        Util.SavedData.Set(
            viking,
            ClientVersion.GetVersion(apiKey),
            contentXml
        );
        ctx.SaveChanges();
        return true;
    }

    // NOTE: "Pet" (Petz) system (GetCurrentPetByUserID, GetCurrentPet, SetCurrentPet, DelCurrentPet) is a totally different system than "RaisedPet" (Dragons)

    [HttpPost]
    //[Produces("application/xml")]
    [Route("ContentWebService.asmx/GetCurrentPetByUserID")] // used by World Of Jumpstart
    public string GetCurrentPetByUserID([FromForm] Guid userId) {
        return GetCurrentPet(ctx.Vikings.FirstOrDefault(e => e.Uid == userId));
    }

    [HttpPost]
    //[Produces("application/xml")]
    [Route("ContentWebService.asmx/GetCurrentPet")] // used by World Of Jumpstart
    [VikingSession]
    public string GetCurrentPet(Viking viking) {
        string? ret = Util.SavedData.Get(
            viking,
            Util.SavedData.Pet()
        );
        if (ret is null)
            return XmlUtil.SerializeXml<PetData>(null);
        return ret;
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetCurrentPet")] // used by World Of Jumpstart
    [VikingSession]
    public bool SetCurrentPet(Viking viking, [FromForm] string? contentXml) {
        Util.SavedData.Set(
            viking,
            Util.SavedData.Pet(),
            contentXml
        );
        ctx.SaveChanges();
        return true;
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/DelCurrentPet")] // used by World Of Jumpstart
    [VikingSession]
    public bool DelCurrentPet(Viking viking) {
        return SetCurrentPet(viking, null);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetDefaultNameSuggestion")]
    [VikingSession(Mode=VikingSession.Modes.VIKING_OR_USER, UseLock=false)]
    public IActionResult GetDefaultNameSuggestion(User? user, Viking? viking) {
        string[] adjs = { //Adjectives used to generate suggested names
            "Adventurous", "Active", "Alert", "Attentive",
            "Beautiful", "Berkian", "Berserker", "Bold", "Brave",
            "Caring", "Cautious", "Creative", "Curious",
            "Dangerous", "Daring", "Defender",
            "Elder", "Exceptional", "Exquisite", 
            "Fearless", "Fighter", "Friendly",
            "Gentle", "Grateful", "Great",
            "Happy", "Honorable", "Hunter",
            "Insightful", "Intelligent",
            "Jolly", "Joyful", "Joyous",
            "Kind", "Kindly",
            "Legendary", "Lovable", "Lovely",
            "Marvelous", "Magnificent",
            "Noble", "Nifty", "Neat",
            "Outcast", "Outgoing", "Organized",
            "Planner", "Playful", "Pretty",
            "Quick", "Quiet",
            "Racer", "Random", "Resilient",
            "Scientist", "Seafarer", "Smart", "Sweet",
            "Thinker", "Thoughtful",
            "Unafraid", "Unique",
            "Valiant", "Valorous", "Victor", "Victorious", "Viking",
            "Winner", "Warrior", "Wise",
            "Young", "Youthful",
            "Zealous", "Zealot"
        };

        if (user is null)
            user = viking.User;
        string uname = user.Username;

        Random choice = new Random(); //Randomizer for selecting random adjectives
        
        List<string> suggestions = new();
        AddSuggestion(choice, uname, suggestions);

        for (int i = 0; i < 5; i++)
            AddSuggestion(choice, GetNameSuggestion(choice, uname, adjs), suggestions);

        return Ok(new DisplayNameUniqueResponse {
            Suggestions = new SuggestionResult {
                Suggestion = suggestions.ToArray()
            }
        });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/ValidateName")]
    public IActionResult ValidateName([FromForm] string nameValidationRequest) {
        // Check if name populated
        NameValidationRequest request = XmlUtil.DeserializeXml<NameValidationRequest>(nameValidationRequest);

        bool exists;
        switch (request.Category) {
            case NameCategory.Default:
                // This is an avatar we are checking
                // Check if viking exists
                exists = ctx.Vikings.Any(e => e.Name == request.Name);
                break;
            case NameCategory.Group:
                exists = ctx.Groups.Any(e => e.Name == request.Name);
                break;
            default: 
                // TODO: pets, default
                return Ok();
        }
        
        NameValidationResult result = exists ? NameValidationResult.NotUnique : NameValidationResult.Ok;
        return Ok(new NameValidationResponse { Result = result });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("/V2/ContentWebService.asmx/SetDisplayName")]
    [VikingSession]
    public IActionResult SetDisplayName(Viking viking, [FromForm] string request) {
        string newName = XmlUtil.DeserializeXml<SetDisplayNameRequest>(request).DisplayName;

        if (String.IsNullOrWhiteSpace(newName) || ctx.Vikings.Count(e => e.Name == newName) > 0) {
            return Ok(new SetAvatarResult {
                Success = false,
                StatusCode = AvatarValidationResult.AvatarDisplayNameInvalid
            });
        }

        viking.Name = newName;
        AvatarData avatarData = XmlUtil.DeserializeXml<AvatarData>(viking.AvatarSerialized);
        avatarData.DisplayName = newName;
        viking.AvatarSerialized = XmlUtil.SerializeXml(avatarData);
        ctx.SaveChanges();

        return Ok(new SetAvatarResult {
            Success = true,
            DisplayName = viking.Name,
            StatusCode = AvatarValidationResult.Valid
        });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetKeyValuePair")]
    [Route("ContentWebService.asmx/GetKeyValuePairByUserID")]
    [VikingSession(Mode=VikingSession.Modes.VIKING_OR_USER, UseLock=false)]
    public Schema.PairData? GetKeyValuePairByUserID(User? user, Viking? viking, [FromForm] int pairId, [FromForm] string? userId) {
        Model.PairData? pair = keyValueService.GetPairData(user, viking, userId, pairId);

        return keyValueService.ModelToSchema(pair);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetKeyValuePair")]
    [Route("ContentWebService.asmx/SetKeyValuePairByUserID")]
    [VikingSession(Mode=VikingSession.Modes.VIKING_OR_USER, UseLock=true)]
    public IActionResult SetKeyValuePairByUserID(User? user, Viking? viking, [FromForm] int pairId, [FromForm] string contentXML, [FromForm] string? userId) {
        Schema.PairData schemaData = XmlUtil.DeserializeXml<Schema.PairData>(contentXML);

        bool result = keyValueService.SetPairData(user, viking, userId, pairId, schemaData);

        return Ok(result);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetCommonInventory")]
    [VikingSession(Mode=VikingSession.Modes.VIKING_OR_USER, UseLock=false)]
    public IActionResult GetCommonInventory(User? user, Viking? viking) {
        if (viking != null) {
            return Ok( inventoryService.GetCommonInventoryData(viking) );
        } else {
            // TODO: placeholder - return 8 viking slot items
            return Ok(new CommonInventoryData {
                UserID = user.Id,
                Item = new UserItemData[] {
                    new UserItemData {
                        UserInventoryID = 0,
                        ItemID = 7971,
                        Quantity = 8,
                        Uses = -1,
                        ModifiedDate = new DateTime(DateTime.Now.Ticks),
                        Item = itemService.GetItem(7971)
                    }
                }
            });
        }
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetCommonInventoryByUserId")] // used by World Of Jumpstart (?)
    public IActionResult GetCommonInventoryByUserId([FromForm] Guid userId, [FromForm] int ContainerId)
    {
        Viking? viking = ctx.Vikings.FirstOrDefault(e => e.Uid == userId);
        return GetCommonInventory(null, viking);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/GetCommonInventory")]
    [VikingSession(UseLock=false)]
    public IActionResult GetCommonInventoryV2(Viking viking) {
        return Ok(inventoryService.GetCommonInventoryData(viking));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetCommonInventory")]
    [VikingSession]
    public IActionResult SetCommonInventory(Viking viking, [FromForm] string commonInventoryRequestXml) {
        CommonInventoryRequest[] request = XmlUtil.DeserializeXml<CommonInventoryRequest[]>(commonInventoryRequestXml);
        List<CommonInventoryResponseItem> responseItems = new();

        if (request is null) {
            return Ok(new CommonInventoryResponse {
                Success = false
            });
        }
        
        // SetCommonInventory can remove any number of items from the inventory, this checks if it's possible
        foreach (var req in request) {
            if (req.Quantity >= 0) continue;
            int inventorySum = viking.InventoryItems.Sum(e => {if (e.ItemId == req.ItemID) return e.Quantity; return 0;});
            if (inventorySum < -req.Quantity)
                return Ok(new CommonInventoryResponse { Success = false });
        }

        // Now that we know the request is valid, update the inventory
        foreach (var req in request) {
            if (req.ItemID == 0) continue; // Do not save a null item

            if (inventoryService.ItemNeedUniqueInventorySlot((int)req.ItemID)) {
                // if req.Quantity < 0 remove unique items
                for (int i=req.Quantity; i<0; ++i) {
                     InventoryItem? item = viking.InventoryItems.FirstOrDefault(e => e.ItemId == req.ItemID && e.Quantity>0);
                     item.Quantity--;
                }
                // if req.Quantity > 0 add unique items
                for (int i=0; i<req.Quantity; ++i) {
                    responseItems.Add(
                        inventoryService.AddItemToInventoryAndGetResponse(viking, (int)req.ItemID!, 1)
                    );
                }
            } else {
                var responseItem = inventoryService.AddItemToInventoryAndGetResponse(viking, (int)req.ItemID!, req.Quantity);
                if (req.Quantity > 0) {
                    responseItems.Add(responseItem);
                }
            }
        }

        CommonInventoryResponse response = new CommonInventoryResponse {
            Success = true,
            CommonInventoryIDs = responseItems.ToArray()
        };

        ctx.SaveChanges();
        return Ok(response);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetCommonInventoryAttribute")]
    [VikingSession]
    public IActionResult SetCommonInventoryAttribute(Viking viking, [FromForm] int commonInventoryID, [FromForm] string pairxml) {
        InventoryItem? item = viking.InventoryItems.FirstOrDefault(e => e.Id == commonInventoryID);

        List<Schema.Pair> itemAttributes;
        if (item.AttributesSerialized != null) {
            itemAttributes = XmlUtil.DeserializeXml<Schema.PairData>(item.AttributesSerialized).Pairs.ToList();
        } else {
            itemAttributes = new List<Schema.Pair>();
        }

        Schema.PairData newItemAttributes = XmlUtil.DeserializeXml<Schema.PairData>(pairxml);
        foreach (var p in newItemAttributes.Pairs) {
            var pairItem = itemAttributes.FirstOrDefault(e => e.PairKey == p.PairKey);
            if (pairItem != null){
                pairItem.PairValue = p.PairValue;
            } else {
                itemAttributes.Add(p);
            }
        }

        if (itemAttributes.Count > 0) {
            item.AttributesSerialized = XmlUtil.SerializeXml(
                new Schema.PairData{
                    Pairs = itemAttributes.ToArray()
                }
            );
        }

        ctx.SaveChanges();
        return Ok(true);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/UseInventory")]
    [VikingSession]
    public IActionResult UseInventory(Viking viking, [FromForm] int userInventoryId, [FromForm] int numberOfUses) {
        InventoryItem? item = viking.InventoryItems.FirstOrDefault(e => e.Id == userInventoryId);
        if (item is null)
            return Ok(false);
        if (item.Quantity < numberOfUses)
            return Ok(false);
        
        item.Quantity -= numberOfUses;
        ctx.SaveChanges();
        return Ok(true);
    }
    

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetAuthoritativeTime")]
    public IActionResult GetAuthoritativeTime() {
        return Ok(new DateTime(DateTime.Now.Ticks));
    }

    private int GetAvatarVersion(AvatarData avatarData) {
        foreach (AvatarDataPart part in avatarData.Part) {
            if (part.PartType == "Version") {
                return (int)part.Offsets[0].X * 100 + (int)part.Offsets[0].Y * 10 + (int)part.Offsets[0].Z;
            }
        }
        return 0;
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetAvatar")] // used by World Of Jumpstart
    [VikingSession(UseLock=false)]
    public IActionResult GetAvatar(Viking viking) {
        AvatarData avatarData = XmlUtil.DeserializeXml<AvatarData>(viking.AvatarSerialized);
        avatarData.Id = viking.Id;
        return Ok(avatarData);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetAvatarByUserID")] // used by World Of Jumpstart, only for public information
    public IActionResult GetAvatarByUserId([FromForm] Guid userId)
    {
        Viking? viking = ctx.Vikings.FirstOrDefault(e => e.Uid == userId);
        if (viking is null)
            return Ok(new AvatarData());

        AvatarData avatarData = XmlUtil.DeserializeXml<AvatarData>(viking.AvatarSerialized);
        if (avatarData is null)
            return Ok(new AvatarData());
        
        avatarData.Id = viking.Id;
        return Ok(avatarData);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetAvatar")] // used by World Of Jumpstart
    [VikingSession]
    public IActionResult SetAvatarV1(Viking viking, [FromForm] string contentXML) {
        if (viking.AvatarSerialized != null) {
            AvatarData dbAvatarData = XmlUtil.DeserializeXml<AvatarData>(viking.AvatarSerialized);
            AvatarData reqAvatarData = XmlUtil.DeserializeXml<AvatarData>(contentXML);

            int dbAvatarVersion = GetAvatarVersion(dbAvatarData);
            int reqAvatarVersion = GetAvatarVersion(reqAvatarData);

            if (dbAvatarVersion > reqAvatarVersion) {
                // do not allow override newer version avatar data by older version
                return Ok(false);
            }
        }

        viking.AvatarSerialized = contentXML;
        ctx.SaveChanges();

        return Ok(true);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/SetAvatar")]
    [VikingSession]
    public IActionResult SetAvatar(Viking viking, [FromForm] string contentXML) {
        if (viking.AvatarSerialized != null) {
            AvatarData dbAvatarData = XmlUtil.DeserializeXml<AvatarData>(viking.AvatarSerialized);
            AvatarData reqAvatarData = XmlUtil.DeserializeXml<AvatarData>(contentXML);

            int dbAvatarVersion = GetAvatarVersion(dbAvatarData);
            int reqAvatarVersion = GetAvatarVersion(reqAvatarData);

            if (dbAvatarVersion > reqAvatarVersion) {
                // do not allow override newer version avatar data by older version
                return Ok(new SetAvatarResult {
                    Success = false,
                    StatusCode = AvatarValidationResult.Error
                });
            }
        }

        viking.AvatarSerialized = contentXML;
        ctx.SaveChanges();

        return Ok(new SetAvatarResult {
            Success = true,
            DisplayName = viking.Name,
            StatusCode = AvatarValidationResult.Valid
        });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/CreateRaisedPet")] // used by SoD 1.6
    [VikingSession]
    public RaisedPetData? CreateRaisedPet([FromForm] string apiKey, Viking viking, int petTypeID) {
        // Update the RaisedPetData with the info
        String dragonId = Guid.NewGuid().ToString();
        
        var raisedPetData = new RaisedPetData();
        raisedPetData.IsPetCreated = true;
        raisedPetData.PetTypeID = petTypeID;
        raisedPetData.RaisedPetID = 0; // Initially make zero, so the db auto-fills
        raisedPetData.EntityID = Guid.Parse(dragonId);
        if (ClientVersion.GetGameID(apiKey) != ClientVersion.WoJS)
            raisedPetData.Name = string.Concat("Dragon-", dragonId.AsSpan(0, 8)); // Start off with a random name (if game isn't WoJS)
        raisedPetData.IsSelected = false; // The api returns false, not sure why
        raisedPetData.CreateDate = new DateTime(DateTime.Now.Ticks);
        raisedPetData.UpdateDate = new DateTime(DateTime.Now.Ticks);
        if (petTypeID == 2)
            raisedPetData.GrowthState = new RaisedPetGrowthState { Name = "BABY" };
        else
            raisedPetData.GrowthState = new RaisedPetGrowthState { Name = "POWERUP" };
        int imageSlot = (viking.Images.Select(i => i.ImageSlot).DefaultIfEmpty(-1).Max() + 1);
        raisedPetData.ImagePosition = imageSlot;
        // NOTE: Placing an egg into a hatchery slot calls CreatePet, but doesn't SetImage.
        // NOTE: We need to force create an image slot because hatching multiple eggs at once would create dragons with the same slot
        Image image = new Image {
            ImageType = "EggColor", // NOTE: The game doesn't seem to use anything other than EggColor.
            ImageSlot = imageSlot,
            Viking = viking,
        };
        // Save the dragon in the db
        Dragon dragon = new Dragon {
            EntityId = Guid.NewGuid(),
            Viking = viking,
            RaisedPetData = XmlUtil.SerializeXml(raisedPetData),
        };

        ctx.Dragons.Add(dragon);
        ctx.Images.Add(image);

        if (petTypeID != 2) {
            // Minisaurs should not be set as active pet
            viking.SelectedDragon = dragon;
            ctx.Update(viking);
        }
        ctx.SaveChanges();

        return GetRaisedPetDataFromDragon(dragon);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/CreatePet")]
    [VikingSession(UseLock = true)]
    public IActionResult CreatePet(Viking viking, [FromForm] string request) {
        RaisedPetRequest raisedPetRequest = XmlUtil.DeserializeXml<RaisedPetRequest>(request);
        // TODO: Investigate SetAsSelectedPet and UnSelectOtherPets - they don't seem to do anything

        // Update the RaisedPetData with the info
        string dragonId = Guid.NewGuid().ToString();
        raisedPetRequest.RaisedPetData.IsPetCreated = true;
        raisedPetRequest.RaisedPetData.RaisedPetID = 0; // Initially make zero, so the db auto-fills
        raisedPetRequest.RaisedPetData.EntityID = Guid.Parse(dragonId);
        if (string.IsNullOrEmpty(raisedPetRequest.RaisedPetData.Name))
            raisedPetRequest.RaisedPetData.Name = string.Concat("Dragon-", dragonId.AsSpan(0, 8)); // Start off with a random name
        raisedPetRequest.RaisedPetData.IsSelected = false; // The api returns false, not sure why
        raisedPetRequest.RaisedPetData.CreateDate = new DateTime(DateTime.Now.Ticks);
        raisedPetRequest.RaisedPetData.UpdateDate = new DateTime(DateTime.Now.Ticks);
        int imageSlot = (viking.Images.Select(i => i.ImageSlot).DefaultIfEmpty(-1).Max() + 1);
        raisedPetRequest.RaisedPetData.ImagePosition = imageSlot;
        // NOTE: Placing an egg into a hatchery slot calls CreatePet, but doesn't SetImage.
        // NOTE: We need to force create an image slot because hatching multiple eggs at once would create dragons with the same slot
        Image image = new Image {
            ImageType = "EggColor", // NOTE: The game doesn't seem to use anything other than EggColor.
            ImageSlot = imageSlot,
            Viking = viking,
        };
        // Save the dragon in the db
        Dragon dragon = new Dragon {
            EntityId = Guid.NewGuid(),
            Viking = viking,
            RaisedPetData = XmlUtil.SerializeXml(raisedPetRequest.RaisedPetData),
        };

        if (raisedPetRequest.SetAsSelectedPet == true) {
            viking.SelectedDragon = dragon;
        }
        ctx.Dragons.Add(dragon);
        ctx.Images.Add(image);
        ctx.SaveChanges();

        if (raisedPetRequest.CommonInventoryRequests is not null) {
            foreach (var req in raisedPetRequest.CommonInventoryRequests) {
                InventoryItem? item = viking.InventoryItems.FirstOrDefault(e => e.ItemId == req.ItemID);
                
                //Does the item exist in the user's inventory?
                if (item is null) continue; //If not, skip it.
                
                if (item.Quantity + req.Quantity >= 0 ) { //If so, can we update it appropriately?
                    //We can.  Do so.
                    item.Quantity += req.Quantity; //Note that we use += here because req.Quantity is negative.
                    ctx.SaveChanges();
                }
            }
        }

        return Ok(new CreatePetResponse {
            RaisedPetData = GetRaisedPetDataFromDragon(dragon)
        });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetRaisedPet")] // used by World Of Jumpstart and Math Blaster
    [VikingSession]
    public IActionResult SetRaisedPetv1(Viking viking, [FromForm] string raisedPetData) {
        RaisedPetData petData = XmlUtil.DeserializeXml<RaisedPetData>(raisedPetData);

        // Find the dragon
        Dragon? dragon = viking.Dragons.FirstOrDefault(e => e.Id == petData.RaisedPetID);
        if (dragon is null) {
            return Ok(false);
        }

        petData = UpdateDragon(dragon, petData);
        if (petData.Texture != null && petData.Texture.StartsWith("RS_SHARED/Larva.unity3d/LarvaTex") && petData.GrowthState.GrowthStateID > 4) {
            petData.Texture = "RS_SHARED/" + petData.PetTypeID switch {
                5 => "EyeClops.unity3d/EyeClopsBrainRedTex",           // EyeClops
                6 => "RodeoLizard.unity3d/BlueLizardTex",              // RodeoLizard
                7 => "MonsterAlien01.unity3d/BlasterMythieGreenTex",   // MonsterAlien01
                11 => "SpaceGriffin.unity3d/SpaceGriffinNormalBlueTex", // SpaceGriffin
                10 => "Tweeter.unity3d/TweeterMuttNormalPurple",        // Tweeter
                _ => "null" // Anything with any other ID shouldn't exist.
            };
        }
        dragon.RaisedPetData = XmlUtil.SerializeXml(petData);

        ctx.Update(dragon);
        ctx.SaveChanges();

        return Ok(true);
    }
    
    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/SetRaisedPet")] // used by Magic & Mythies
    [VikingSession]
    public IActionResult SetRaisedPetv2(Viking viking, [FromForm] string raisedPetData) {
        RaisedPetData petData = XmlUtil.DeserializeXml<RaisedPetData>(raisedPetData);

        // Find the dragon
        Dragon? dragon = viking.Dragons.FirstOrDefault(e => e.Id == petData.RaisedPetID);
        if (dragon is null) {
            return Ok(new SetRaisedPetResponse {
                RaisedPetSetResult = RaisedPetSetResult.Invalid
            });
        }

        dragon.RaisedPetData = XmlUtil.SerializeXml(UpdateDragon(dragon, petData));
        ctx.Update(dragon);
        ctx.SaveChanges();

        return Ok(new SetRaisedPetResponse {
            RaisedPetSetResult = RaisedPetSetResult.Success
        });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("v3/ContentWebService.asmx/SetRaisedPet")]
    [VikingSession]
    public IActionResult SetRaisedPet(Viking viking, [FromForm] string request, [FromForm] bool? import) {
         RaisedPetRequest raisedPetRequest = XmlUtil.DeserializeXml<RaisedPetRequest>(request);

        // Find the dragon
        Dragon? dragon = viking.Dragons.FirstOrDefault(e => e.Id == raisedPetRequest.RaisedPetData.RaisedPetID);
        if (dragon is null) {
            return Ok(new SetRaisedPetResponse {
                RaisedPetSetResult = RaisedPetSetResult.Invalid
            });
        }

        dragon.RaisedPetData = XmlUtil.SerializeXml(UpdateDragon(dragon, raisedPetRequest.RaisedPetData, import ?? false));
        ctx.Update(dragon);
        ctx.SaveChanges();

        // TODO: handle CommonInventoryRequests here too

        return Ok(new SetRaisedPetResponse {
            RaisedPetSetResult = RaisedPetSetResult.Success
        });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetRaisedPetInactive")] // used by World Of Jumpstart
    [VikingSession]
    public IActionResult SetRaisedPetInactive(Viking viking, [FromForm] int raisedPetID) {
        if (raisedPetID == viking.SelectedDragonId) {
            RaisedPetData dragonData = XmlUtil.DeserializeXml<RaisedPetData>(viking.SelectedDragon.RaisedPetData);
            RaisedPetAttribute? attribute = dragonData.Attributes.FirstOrDefault(a => a.Key == "GrowTime");
            if (attribute != null) {
                attribute.Value = DateTime.UtcNow.ToString("yyyy#M#d#H#m#s");
                viking.SelectedDragon.RaisedPetData = XmlUtil.SerializeXml(dragonData);
            }
            viking.SelectedDragonId = null;
        } else {
            Dragon? dragon = viking.Dragons.FirstOrDefault(e => e.Id == raisedPetID);
            if (dragon is null) {
                return Ok(false);
            }

            // check if Minisaurs - we real delete only Minisaurs
            RaisedPetData dragonData = XmlUtil.DeserializeXml<RaisedPetData>(dragon.RaisedPetData);
            if (dragonData.PetTypeID != 2) {
                return Ok(false);
            }

            viking.Dragons.Remove(dragon);
        }
        ctx.SaveChanges();
        return Ok(true);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetSelectedPet")]
    [VikingSession]
    public IActionResult SetSelectedPet(Viking viking, [FromForm] int raisedPetID) {
        // Find the dragon
        Dragon? dragon = viking.Dragons.FirstOrDefault(e => e.Id == raisedPetID);
        if (dragon is null) {
            return Ok(new SetRaisedPetResponse {
                RaisedPetSetResult = RaisedPetSetResult.Invalid
            });
        }

        // Set the dragon as selected
        viking.SelectedDragon = dragon;
        ctx.Update(viking);
        ctx.SaveChanges();

        return Ok(true); // RaisedPetSetResult.Success doesn't work, this does
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/GetAllActivePetsByuserId")]
    public RaisedPetData[]? GetAllActivePetsByuserId([FromForm] Guid userId, [FromForm] bool active) {
        // NOTE: this is public info (for mmo) - no session check
        Viking? viking = ctx.Vikings.FirstOrDefault(e => e.Uid == userId);
        if (viking is null)
            return null;

        RaisedPetData[] dragons = ctx.Dragons
            .Where(d => d.VikingId == viking.Id && d.RaisedPetData != null)
            .Select(d => GetRaisedPetDataFromDragon(d, viking.SelectedDragonId))
            .ToArray();

        if (dragons.Length == 0) {
            return null;
        }
        return dragons;
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetUnselectedPetByTypes")] // used by old SoD (e.g. 1.13)
    [VikingSession(UseLock=false)]
    public RaisedPetData[]? GetUnselectedPetByTypes(Viking viking, [FromForm] string? userId, [FromForm] string petTypeIDs, [FromForm] bool active) {
        // Get viking based on userId, or use request player's viking as a fallback.
        if (userId != null) {
            Guid userIdGuid = new Guid(userId);
            Viking? ownerViking = ctx.Vikings.FirstOrDefault(e => e.Uid == userIdGuid);
            if (ownerViking != null) viking = ownerViking;
        }
        
        RaisedPetData[] dragons = viking.Dragons
            .Where(d => d.RaisedPetData is not null)
            .Select(d => GetRaisedPetDataFromDragon(d, viking.SelectedDragonId))
            .ToArray();

        if (dragons.Length == 0) {
            return null;
        }

        List<RaisedPetData> filteredDragons = new List<RaisedPetData>();
        int[] petTypeIDsInt = Array.ConvertAll(petTypeIDs.Split(','), s => int.Parse(s));
        foreach (RaisedPetData dragon in dragons) {
            if (petTypeIDsInt.Contains(dragon.PetTypeID) && 
                // Don't send the selected dragon.
                viking.SelectedDragonId != dragon.RaisedPetID
            ) {
                filteredDragons.Add(dragon);
            }
        }

        if (filteredDragons.Count == 0) {
            return null;
        }

        return filteredDragons.ToArray();
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetActiveRaisedPet")] // used by World Of Jumpstart
    [VikingSession(UseLock=false)]
    public RaisedPetData[] GetActiveRaisedPet(Viking viking, [FromForm] string userId, [FromForm] int petTypeID) {
        if (petTypeID == 2) {
            // player can have multiple Minisaurs at the same time ... Minisaurs should never have been selected also ... so use GetUnselectedPetByTypes in this case
            return GetUnselectedPetByTypes(viking, userId, "2", false);
        }

        Dragon? dragon = viking.SelectedDragon;
        if (dragon is null) {
            return new RaisedPetData[0];
        }

        RaisedPetData dragonData = GetRaisedPetDataFromDragon(dragon);
        if (petTypeID != dragonData.PetTypeID)
            return new RaisedPetData[0];

        // NOTE: returned dragon PetTypeID should be equal value of pair 1967 → CurrentRaisedPetType
        return new RaisedPetData[] {dragonData};
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetActiveRaisedPetsByTypes")] // used by Math Blaster
    [VikingSession(UseLock=false)]
    public RaisedPetData[] GetActiveRaisedPet([FromForm] Guid userId, [FromForm] string petTypeIDs) {
        Viking? viking = ctx.Vikings.FirstOrDefault(e => e.Uid == userId);
        Dragon? dragon = viking.SelectedDragon;
        if (dragon is null) {
            return new RaisedPetData[0];
        }

        RaisedPetData dragonData = GetRaisedPetDataFromDragon(dragon);
        int[] petTypeIDsInt = Array.ConvertAll(petTypeIDs.Split(','), s => int.Parse(s));
        if (!petTypeIDsInt.Contains(dragonData.PetTypeID))
            return new RaisedPetData[0];

        return new RaisedPetData[] {dragonData};
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetSelectedRaisedPet")]
    [VikingSession(UseLock=false)]
    public RaisedPetData[]? GetSelectedRaisedPet(Viking viking, [FromForm] string userId, [FromForm] bool isActive) {
        Dragon? dragon = viking.SelectedDragon;
        if (dragon is null) {
            return null;
        }

        return new RaisedPetData[] {
            GetRaisedPetDataFromDragon(dragon)
        };
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetInactiveRaisedPet")] // used by World Of Jumpstart 1.1
    [VikingSession(UseLock=false)]
    public RaisedPetData[] GetInactiveRaisedPet(Viking viking, [FromForm] int petTypeID) {
        RaisedPetData[] dragons = viking.Dragons
            .Where(d => d.RaisedPetData is not null && d.Id != viking.SelectedDragonId)
            .Select(d => GetRaisedPetDataFromDragon(d, viking.SelectedDragonId))
            .ToArray();

        List<RaisedPetData> filteredDragons = new List<RaisedPetData>();
        foreach (RaisedPetData dragon in dragons) {
            if (petTypeID == dragon.PetTypeID) {
                filteredDragons.Add(dragon);
            }
        }

        if (filteredDragons.Count == 0) {
            return null;
        }

        return filteredDragons.ToArray();
    }
    
    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetImage")]
    [VikingSession(UseLock = true)]
    public bool SetImage(Viking viking, [FromForm] string ImageType, [FromForm] int ImageSlot, [FromForm] string contentXML, [FromForm] string imageFile) {
        // TODO: the other properties of contentXML
        ImageData data = XmlUtil.DeserializeXml<ImageData>(contentXML);

        bool newImage = false;
        Image? image = viking.Images.FirstOrDefault(e => e.ImageType == ImageType && e.ImageSlot == ImageSlot);
        if (image is null) {
            image = new Image {
                ImageType = ImageType,
                ImageSlot = ImageSlot,
                Viking = viking,
            };
            newImage = true;
        }

        // Save the image in the db
        image.ImageData = imageFile;
        image.TemplateName = data.TemplateName;

        if (newImage)
            ctx.Images.Add(image);
        ctx.SaveChanges();

        return true;
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetImage")]
    [VikingSession(UseLock=false)]
    public ImageData? GetImage(Viking viking, [FromForm] string ImageType, [FromForm] int ImageSlot) {
        return GetImageData(viking, ImageType, ImageSlot);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetImageByUserId")]
    public ImageData? GetImageByUserId([FromForm] Guid userId, [FromForm] string ImageType, [FromForm] int ImageSlot) {
        // NOTE: this is public info (for mmo) - no session check
        Viking? viking = ctx.Vikings.FirstOrDefault(e => e.Uid == userId);
        if (viking is null || viking.Images is null) {
            return null;
        }

        return GetImageData(viking, ImageType, ImageSlot);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/GetUserUpcomingMissionState")]
    public IActionResult GetUserUpcomingMissionState([FromForm] Guid apiToken, [FromForm] Guid userId, [FromForm] string apiKey) {
        Viking? viking = ctx.Vikings.FirstOrDefault(x => x.Uid == userId);
        if (viking is null)
            return Ok("error");
        
        uint gameVersion = ClientVersion.GetVersion(apiKey);
        UserMissionStateResult result = new UserMissionStateResult { Missions = new List<Mission>() };

        HashSet<int> upcomingMissionsSet = new(missionStore.GetUpcomingMissions(gameVersion));
        var toDiscardIds = new HashSet<int>(
            viking.MissionStates
                  .Where(x => x.MissionStatus != MissionStatus.Upcoming)
                  .Select(x => x.MissionId)
        );

        var toAddIds = new HashSet<int>(
            viking.MissionStates
                  .Where(x => x.MissionStatus == MissionStatus.Upcoming)
                  .Select(x => x.MissionId)
        );

        upcomingMissionsSet.UnionWith(toAddIds);
        upcomingMissionsSet.ExceptWith(toDiscardIds);

        foreach (var missionId in upcomingMissionsSet)
            result.Missions.Add(missionService.GetMissionWithProgress(missionId, viking.Id, gameVersion));

        result.UserID = viking.Uid;
        return Ok(result);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/GetUserActiveMissionState")]
    public IActionResult GetUserActiveMissionState([FromForm] Guid apiToken, [FromForm] Guid userId, [FromForm] string apiKey) {
        Viking? viking = ctx.Vikings.FirstOrDefault(x => x.Uid == userId);
        if (viking is null)
            return Ok("error");
        
        uint gameVersion = ClientVersion.GetVersion(apiKey);
        UserMissionStateResult result = new UserMissionStateResult { Missions = new List<Mission>()  };
        foreach (var mission in viking.MissionStates.Where(x => x.MissionStatus == MissionStatus.Active)) {
            Mission updatedMission = missionService.GetMissionWithProgress(mission.MissionId, viking.Id, gameVersion);
            if (mission.UserAccepted != null)
                updatedMission.Accepted = (bool)mission.UserAccepted;
            result.Missions.Add(updatedMission);
        }
        
        result.UserID = viking.Uid;
        return Ok(result);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/GetUserCompletedMissionState")]
    public IActionResult GetUserCompletedMissionState([FromForm] Guid apiToken, [FromForm] Guid userId, [FromForm] string apiKey) {
        Viking? viking = ctx.Vikings.FirstOrDefault(x => x.Uid == userId);
        if (viking is null)
            return Ok("error");

        uint gameVersion = ClientVersion.GetVersion(apiKey);
        UserMissionStateResult result = new UserMissionStateResult { Missions = new List<Mission>()  };
        foreach (var mission in viking.MissionStates.Where(x => x.MissionStatus == MissionStatus.Completed))
            result.Missions.Add(missionService.GetMissionWithProgress(mission.MissionId, viking.Id, gameVersion));

        result.UserID = viking.Uid;
        return Ok(result);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/AcceptMission")]
    [VikingSession]
    public IActionResult AcceptMission(Viking viking, [FromForm] Guid userId, [FromForm] int missionId, [FromForm] string apiKey) {
        if (viking.Uid != userId)
            return Unauthorized("Can't accept not owned mission");

        uint gameVersion = ClientVersion.GetVersion(apiKey);
        int[] upcomingMissions = missionStore.GetUpcomingMissions(gameVersion);

        MissionState? missionState = viking.MissionStates.FirstOrDefault(x => x.MissionId == missionId);
        if (!upcomingMissions.Contains(missionId) || (missionState is not null && missionState.MissionStatus != MissionStatus.Upcoming))
            return Ok(false);

        if (missionState is null) {
            viking.MissionStates.Add(new MissionState {
                MissionId = missionId,
                MissionStatus = MissionStatus.Active,
                UserAccepted = true
            });
        } else {
            missionState.MissionStatus = MissionStatus.Active;
            missionState.UserAccepted = true;
        }
        ctx.SaveChanges();
        return Ok(true);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetUserMissionState")] // used by SoD 1.13
    public IActionResult GetUserMissionStatev1([FromForm] Guid userId, [FromForm] string filter, [FromForm] string apiKey) {
        Viking? viking = ctx.Vikings.FirstOrDefault(x => x.Uid == userId);
        if (viking is null)
            return Ok("error");

        uint gameVersion = ClientVersion.GetVersion(apiKey);
        UserMissionStateResult result = new UserMissionStateResult { Missions = new List<Mission>()  };

        var missionStatesById = viking.MissionStates.ToDictionary(ms => ms.MissionId);
        HashSet<int> upcomingMissionIds = new(missionStore.GetUpcomingMissions(gameVersion));
        var combinedMissionIds = new HashSet<int>(missionStatesById.Keys);
        combinedMissionIds.UnionWith(upcomingMissionIds);

        foreach (var missionId in combinedMissionIds) {
            MissionState? missionState = missionStatesById.TryGetValue(missionId, out var ms) ? ms : null;
            Mission updatedMission = missionService.GetMissionWithProgress(missionId, viking.Id, gameVersion);

            // Only upcoming missions are missing in the database, so if db is null, mission must be upcoming
            MissionStatus status = missionState != null ? missionState.MissionStatus : MissionStatus.Upcoming;
            if (status == MissionStatus.Upcoming) {
                // NOTE: in old SoD job board mission must be send as non active and required accept
                //       (to avoid show all job board in journal and quest arrow pointing to job board)
                //       do this in this place (instead of update missions.xml) to avoid conflict with newer versions of SoD
                PrerequisiteItem prerequisite = updatedMission.MissionRule.Prerequisites.FirstOrDefault(x => x.Type == PrerequisiteRequiredType.Accept);
                if (prerequisite != null)
                    prerequisite.Value = "true";
            }

            if (missionState != null && missionState.UserAccepted != null)
                updatedMission.Accepted = (bool)missionState.UserAccepted;
            result.Missions.Add(updatedMission);
        }

        result.UserID = viking.Uid;
        return Ok(result);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/GetUserMissionState")]
    //[VikingSession(UseLock=false)]
    public IActionResult GetUserMissionState([FromForm] Guid userId, [FromForm] string filter, [FromForm] string apiKey) {
        MissionRequestFilterV2 filterV2 = XmlUtil.DeserializeXml<MissionRequestFilterV2>(filter);
        Viking? viking = ctx.Vikings.FirstOrDefault(x => x.Uid == userId);
        if (viking is null)
            return Ok("error");

        uint gameVersion = ClientVersion.GetVersion(apiKey);
        UserMissionStateResult result = new UserMissionStateResult { Missions = new List<Mission>()  };
        if (filterV2.MissionPair.Count > 0) {
            foreach (var m in filterV2.MissionPair)
                if (m.MissionID != null)
                    result.Missions.Add(missionService.GetMissionWithProgress((int)m.MissionID, viking.Id, gameVersion));
        } else {
            if (filterV2.GetCompletedMission ?? false) {
                foreach (var mission in viking.MissionStates.Where(x => x.MissionStatus == MissionStatus.Completed))
                    result.Missions.Add(missionService.GetMissionWithProgress(mission.MissionId, viking.Id, gameVersion));
            } else {
                var missionStatesById = viking.MissionStates.Where(x => x.MissionStatus != MissionStatus.Completed).ToDictionary(ms => ms.MissionId);
                HashSet<int> upcomingMissionIds = new(missionStore.GetUpcomingMissions(gameVersion));
                var combinedMissionIds = new HashSet<int>(missionStatesById.Keys);
                combinedMissionIds.UnionWith(upcomingMissionIds);
                foreach (var missionId in combinedMissionIds)
                    result.Missions.Add(missionService.GetMissionWithProgress(missionId, viking.Id, gameVersion));
            }
        }

        result.UserID = viking.Uid;
        return Ok(result);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetTaskState")] // used by SoD 1.13
    [VikingSession(UseLock=true)]
    public IActionResult SetTaskStatev1(Viking viking, [FromForm] Guid userId, [FromForm] int missionId, [FromForm] int taskId, [FromForm] bool completed, [FromForm] string xmlPayload, [FromForm] string apiKey) {
        if (viking.Uid != userId)
            return Unauthorized("Can't set not owned task");

        uint gameVersion = ClientVersion.GetVersion(apiKey);
        List<MissionCompletedResult> results = missionService.UpdateTaskProgress(missionId, taskId, viking.Id, completed, xmlPayload, gameVersion);

        SetTaskStateResult taskResult = new SetTaskStateResult {
            Success = true,
            Status = SetTaskStateStatus.TaskCanBeDone,
        };

        if (results.Count > 0)
            taskResult.MissionsCompleted = results.ToArray();

        return Ok(taskResult);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/SetTaskState")]
    [VikingSession]
    public IActionResult SetTaskState(Viking viking, [FromForm] Guid userId, [FromForm] int missionId, [FromForm] int taskId, [FromForm] bool completed, [FromForm] string xmlPayload, [FromForm] string commonInventoryRequestXml, [FromForm] string apiKey) {
        if (viking.Uid != userId)
            return Unauthorized("Can't set not owned task");

        uint gameVersion = ClientVersion.GetVersion(apiKey);
        List<MissionCompletedResult> results = missionService.UpdateTaskProgress(missionId, taskId, viking.Id, completed, xmlPayload, gameVersion);

        SetTaskStateResult taskResult = new SetTaskStateResult {
            Success = true,
            Status = SetTaskStateStatus.TaskCanBeDone,
        };

        if (commonInventoryRequestXml.Length > 44) { // avoid process inventory on empty xml request,
                                                     // NOTE: client do not set this on empty string when no inventory change request, but send <?xml version="1.0" encoding="utf-8"?>
            SetCommonInventory(viking, commonInventoryRequestXml);
            taskResult.CommonInvRes = new CommonInventoryResponse { Success = true };
        }

        if (results.Count > 0)
            taskResult.MissionsCompleted = results.ToArray();

        return Ok(taskResult);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/AddBuddy")]
    [VikingSession]
    public IActionResult AddBuddy(Viking viking, [FromForm] Guid buddyUserID)
    {
        // get buddy
        Viking? buddyViking = ctx.Vikings.FirstOrDefault(e => e.Uid == buddyUserID);

        if (buddyViking != null)
            return Ok(buddyService.CreateBuddyRelation(viking, buddyViking));
        else return Ok(new BuddyActionResult { Result = BuddyActionResultType.InvalidFriendCode });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/AddBuddyByFriendCode")]
    [VikingSession]
    public IActionResult AddBuddyByFriendCode(Viking viking, [FromForm] string friendCode, [FromForm] string apiKey)
    {
        // make sure viking doesn't add self as buddy
        if (viking.BuddyCode != null && viking.BuddyCode == friendCode) return Ok(new BuddyActionResult { Result = BuddyActionResultType.CannotAddSelf });

        // find viking
        Viking? buddyViking = ctx.Vikings.FirstOrDefault(e => e.BuddyCode == friendCode);

        if (buddyViking != null)
        {
            uint gameVersion = ClientVersion.GetVersion(apiKey);
            // check if game clients are different
            if (
            (buddyViking.GameVersion != gameVersion) &&
            !(buddyViking.GameVersion >= ClientVersion.Min_SoD && gameVersion >= ClientVersion.Min_SoD) &&
            !(buddyViking.GameVersion >= ClientVersion.WoJS && gameVersion >= ClientVersion.WoJS && buddyViking.GameVersion < ClientVersion.WoJS_NewAvatar && gameVersion < ClientVersion.WoJS_NewAvatar)
            ) return Ok(new BuddyActionResult { Result = BuddyActionResultType.InvalidFriendCode });
            else return Ok(buddyService.CreateBuddyRelation(viking, buddyViking));
        }
        else return Ok(new BuddyActionResult { Result = BuddyActionResultType.InvalidFriendCode });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/RemoveBuddy")]
    [VikingSession]
    public IActionResult RemoveBuddy(Viking viking, [FromForm] Guid buddyUserId)
    {
        // find buddy viking
        Viking? buddyViking = ctx.Vikings.FirstOrDefault(e => e.Uid == buddyUserId);

        if (buddyViking != null)
            return Ok(buddyService.RemoveBuddy(viking, buddyViking));
        else return Ok(false);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/BlockBuddy")]
    [VikingSession]
    public IActionResult BlockBuddy(Viking viking, [FromForm] Guid buddyUserId)
    {
        // find viking
        Viking? viking1 = ctx.Vikings.FirstOrDefault(e => e.Uid == buddyUserId);

        if (viking1 != null)
        {
            // check if a relation exists already
            Model.Buddy? buddy = ctx.Buddies.Where(e => e.VikingId == viking.Id || e.BuddyVikingId == viking.Id)
            .FirstOrDefault(e => e.BuddyVikingId == viking1.Id || e.VikingId == viking1.Id);

            if (buddy != null)
            {
                // update existing relation
                if (buddy.BuddyStatus1 == BuddyStatus.BlockedBySelf || buddy.BuddyStatus2 == BuddyStatus.BlockedByOther
                || buddy.BuddyStatus1 == BuddyStatus.BlockedByOther || buddy.BuddyStatus2 == BuddyStatus.BlockedBySelf)
                {
                    buddyService.UpdateBuddyRelation(viking, viking1, BuddyStatus.BlockedByBoth, BuddyStatus.BlockedByBoth);
                }
                else if (buddy.VikingId == viking.Id)
                    buddyService.UpdateBuddyRelation(viking, viking1, BuddyStatus.BlockedBySelf, BuddyStatus.BlockedByOther);
                else
                    buddyService.UpdateBuddyRelation(viking, viking1, BuddyStatus.BlockedByOther, BuddyStatus.BlockedBySelf);
            }
            else
            {
                if (viking1.Id == viking.Id)
                    buddyService.CreateBuddyRelation(viking, viking1, BuddyStatus.BlockedBySelf, BuddyStatus.BlockedByOther);
                else
                    buddyService.CreateBuddyRelation(viking, viking1, BuddyStatus.BlockedByOther, BuddyStatus.BlockedBySelf);
            }

            return Ok(true);
        }
        else return Ok(false);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/ApproveBuddy")]
    [VikingSession]
    public IActionResult ApproveBuddy(Viking viking, [FromForm] Guid buddyUserID)
    {
        // get buddy
        Viking? buddyViking = ctx.Vikings.FirstOrDefault(e => e.Uid == buddyUserID);

        if (buddyViking != null)
            return Ok(buddyService.UpdateBuddyRelation(viking, buddyViking, BuddyStatus.Approved, BuddyStatus.Approved));
        else return Ok(false);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/UpdateBestBuddy")]
    [VikingSession]
    public IActionResult SetBestBuddy(Viking viking, [FromForm] Guid buddyUserID, [FromForm] bool bestBuddy)
    {
        // get buddy
        Viking? buddyViking = ctx.Vikings.FirstOrDefault(e => e.Uid == buddyUserID);

        if (buddyViking != null)
            return Ok(buddyService.UpdateBestBuddy(viking, buddyViking, bestBuddy));
        else return Ok(false);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/InviteBuddy")]
    [VikingSession]
    public IActionResult InviteBuddy(Viking viking, [FromForm] Guid buddyUserId)
    {
        Viking? viking1 = ctx.Vikings.FirstOrDefault(e => e.Uid == buddyUserId);

        if (viking1 != null) messagingService.SendInvite(viking, viking1);
        return Ok(true);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetBuddyLocation")]
    public IActionResult GetBuddyLocation([FromForm] Guid buddyUserID)
    {
        Viking? viking = ctx.Vikings.FirstOrDefault(e => e.Uid == buddyUserID);
        if (viking != null) return Ok(buddyService.GetLocationOfBuddy(viking));
        else return Ok(new BuddyLocation());
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetBuddyList")]
    [VikingSession]
    public IActionResult GetBuddyList(Viking viking)
    {
        return Ok(buddyService.ConstructBuddyList(viking));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetFriendCode")]
    public IActionResult GetFriendCode([FromForm] Guid userId)
    {
        Viking? viking = ctx.Vikings.FirstOrDefault(e => e.Uid == userId);

        if (viking == null) return NotFound();
        else return Ok(buddyService.GetOrSetBuddyCode(viking));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("/ContentWebService.asmx/RedeemItems")]
    [VikingSession]
    public IActionResult RedeemItems(Viking viking, [FromForm] string request) {
        var req = XmlUtil.DeserializeXml<RedeemRequest>(request);
        Dictionary<int, int> inventoryItemsToAdd = [];

        // resolve items in the bundle
        ItemData bundleItem = itemService.GetItem(req.ItemID);
        foreach (var reward in bundleItem.Relationship.Where(e => e.Type == "Bundle")) {
            int quantity = itemService.GetItemQuantity(reward, 1);
            inventoryItemsToAdd.TryAdd(reward.ItemId, 0);
            inventoryItemsToAdd[reward.ItemId] += quantity;
        }

        // add items to the inventory (database) and build response
        return Ok(
            inventoryService.AddItemsToInventoryBulkAndGetResponse(
                viking, inventoryItemsToAdd, inventoryItemsToAdd, achievementService.GetUserCurrency(viking)
            )
        );
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("/ContentWebService.asmx/RedeemMysteryBoxItems")]
    [VikingSession]
    public IActionResult RedeemMysteryBoxItems(Viking viking, [FromForm] string request) {
        var req = XmlUtil.DeserializeXml<RedeemRequest>(request);

        // get and reduce quantity of box item
        InventoryItem? invItem = viking.InventoryItems.FirstOrDefault(e => e.ItemId == req.ItemID);
        if (invItem is null || invItem.Quantity < 1) {
            return Ok(new CommonInventoryResponse{ Success = false });
        }
        --invItem.Quantity;

        // get real item id (from box)
        Gender gender = XmlUtil.DeserializeXml<AvatarData>(viking.AvatarSerialized).GenderType;
        itemService.OpenBox(req.ItemID, gender, out int newItemId, out int quantity);
        ItemData newItem = itemService.GetItem(newItemId);
        CommonInventoryResponseItem newInvItem;

        // check if it is gems or coins bundle
        if (itemService.IsGemBundle(newItem.ItemID, out int gems)) {
            achievementService.AddAchievementPoints(viking, AchievementPointTypes.CashCurrency, gems);
            newInvItem = new CommonInventoryResponseItem {
                CommonInventoryID = 0,
                ItemID = newItem.ItemID,
                Quantity = 1
            };
        } else if (itemService.IsCoinBundle(newItem.ItemID, out int coins)) {
            achievementService.AddAchievementPoints(viking, AchievementPointTypes.GameCurrency, coins);
            newInvItem = new CommonInventoryResponseItem {
                CommonInventoryID = 0,
                ItemID = newItem.ItemID,
                Quantity = 1
            };
        // if not, add item to inventory
        } else {
            newInvItem = inventoryService.AddItemToInventoryAndGetResponse(viking, newItem.ItemID, quantity);
        }

        // prepare list of possible rewards for response
        List<ItemData> prizeItems = new List<ItemData>();
        prizeItems.Add(newItem);
        foreach (var reward in itemService.GetItem(req.ItemID).Relationship.Where(e => e.Type == "Prize")) {
            if (prizeItems.Count >= req.RedeemItemFetchCount)
                break;
            prizeItems.Add(itemService.GetItem(reward.ItemId));
        }

        return Ok(new CommonInventoryResponse{
            Success = true,
            CommonInventoryIDs = new CommonInventoryResponseItem[]{ newInvItem },
            PrizeItems = new List<PrizeItemResponse>{ new PrizeItemResponse{
                ItemID = req.ItemID,
                PrizeItemID = newItem.ItemID,
                MysteryPrizeItems = prizeItems,
            }},
            UserGameCurrency = achievementService.GetUserCurrency(viking)
        });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/PurchaseItems")]
    [VikingSession(UseLock = true)]
    public IActionResult PurchaseItems(Viking viking, [FromForm] string purchaseItemRequest) {
        PurchaseStoreItemRequest request = XmlUtil.DeserializeXml<PurchaseStoreItemRequest>(purchaseItemRequest);
        var itemsToPurchase = request.Items.GroupBy(id => id).ToDictionary(g => g.Key, g => g.Count());

        return Ok(PurchaseItemsImpl(viking, itemsToPurchase, request.AddMysteryBoxToInventory));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/PurchaseItems")]
    [VikingSession(UseLock = true)]
    public IActionResult PurchaseItemsV1(Viking viking, [FromForm] string itemIDArrayXml) {
        int[] itemIdArr = XmlUtil.DeserializeXml<int[]>(itemIDArrayXml);
        var itemsToPurchase = itemIdArr.GroupBy(id => id).ToDictionary(g => g.Key, g => g.Count());

        return Ok(PurchaseItemsImpl(viking, itemsToPurchase, false));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetUserRoomItemPositions")]
    public IActionResult GetUserRoomItemPositions([FromForm] Guid userId, [FromForm] string roomID, [FromForm] string apiKey) {
        // NOTE: this is public info (for mmo) - no session check
        Viking? viking = ctx.Vikings.FirstOrDefault(e => e.Uid == userId);

        if (roomID is null)
            roomID = "";
        Room? room = viking?.Rooms.FirstOrDefault(x => x.RoomId == roomID);
        if (room is null)
            return Ok(new UserItemPositionList { UserItemPosition = new UserItemPosition[0] });

        return Ok(roomService.GetUserItemPositionList(room, ClientVersion.GetVersion(apiKey)));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetUserRoomItemPositions")]
    [VikingSession]
    public IActionResult SetUserRoomItemPositions(Viking viking, [FromForm] string createXml, [FromForm] string updateXml, [FromForm] string removeXml, [FromForm] string roomID) {
        if (roomID is null)
            roomID = "";
        Room? room = viking.Rooms.FirstOrDefault(x => x.RoomId == roomID);
        if (room is null) {
            room = new Room {
                RoomId = roomID,
                Items = new List<RoomItem>()
            };
            viking.Rooms.Add(room);
            ctx.SaveChanges();
        }

        UserItemPositionSetRequest[] createItems = XmlUtil.DeserializeXml<UserItemPositionSetRequest[]>(createXml);
        UserItemPositionSetRequest[] updateItems = XmlUtil.DeserializeXml<UserItemPositionSetRequest[]>(updateXml);
        int[] deleteItems = XmlUtil.DeserializeXml<int[]>(removeXml);

        Tuple<int[], UserItemState[]> createData = roomService.CreateItems(createItems, room);
        UserItemState[] state = roomService.UpdateItems(updateItems, room);
        roomService.DeleteItems(deleteItems, room);

        UserItemPositionSetResponse response = new UserItemPositionSetResponse {
            Success = true,
            CreatedUserItemPositionIDs = createData.Item1,
            UserItemStates = createData.Item2,
            Result = ItemPositionValidationResult.Valid
        };

        if (state.Length > 0)
            response.UserItemStates = state;

        return Ok(response);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetUserRoomList")]
    public IActionResult GetUserRoomList([FromForm] string request) {
        // NOTE: this is public info (for mmo) - no session check
        // TODO: Categories are not supported
        UserRoomGetRequest userRoomRequest = XmlUtil.DeserializeXml<UserRoomGetRequest>(request);
        ICollection<Room>? rooms = ctx.Vikings.FirstOrDefault(x => x.Uid == userRoomRequest.UserID)?.Rooms;
        UserRoomResponse response = new UserRoomResponse { UserRoomList = new List<UserRoom>() };
        if (rooms is null)
            return Ok(response);
        foreach (var room in rooms) {
            if (room.RoomId == "MyRoomINT" || room.RoomId == "StaticFarmItems") continue;

            int itemID = 0;
            if (room.RoomId != "") {
                // farm expansion room: RoomId is Id for expansion item
                if (Int32.TryParse(room.RoomId, out int inventoryItemId)) {
                    InventoryItem? item = room.Viking.InventoryItems.FirstOrDefault(e => e.Id == inventoryItemId);
                    if (item != null) {
                        itemID = item.ItemId;
                    }
                }
            }

            UserRoom ur = new UserRoom {
                RoomID = room.RoomId,
                CategoryID = 541, // Placeholder
                CreativePoints = 0, // Placeholder
                ItemID = itemID,
                Name = room.Name
            };
            response.UserRoomList.Add(ur);
        }
        return Ok(response);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetUserRoom")]
    [VikingSession]
    public IActionResult SetUserRoom(Viking viking, [FromForm] string request) {
        UserRoom roomRequest = XmlUtil.DeserializeXml<UserRoom>(request);
        Room? room = viking.Rooms.FirstOrDefault(x => x.RoomId == roomRequest.RoomID);
        if (room is null) {
            // setting farm room name can be done before call SetUserRoomItemPositions
            room = new Room {
                RoomId = roomRequest.RoomID,
                Name = roomRequest.Name
            };
            viking.Rooms.Add(room);
        } else {
            room.Name = roomRequest.Name;
        }
        ctx.SaveChanges();
        return Ok(new UserRoomSetResponse {
            Success = true,
            StatusCode = UserRoomValidationResult.Valid
        });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetActiveParties")] // used by World Of Jumpstart
    public IActionResult GetActiveParties([FromForm] string apiKey)
    {
        List<Party> allParties = ctx.Parties.ToList();
        List<UserParty> userParties = new List<UserParty>();

        foreach(var party in allParties)
        {
            if(DateTime.UtcNow >= party.ExpirationDate)
            {
                ctx.Parties.Remove(party);
                ctx.SaveChanges();

                continue;
            }


            Viking viking = ctx.Vikings.FirstOrDefault(e => e.Id == party.VikingId);
            AvatarData avatarData = XmlUtil.DeserializeXml<AvatarData>(viking.AvatarSerialized);
            UserParty userParty = new UserParty
            {
                DisplayName = avatarData.DisplayName,
                UserName = avatarData.DisplayName,
                ExpirationDate = party.ExpirationDate,
                Icon = party.LocationIconAsset,
                Location = party.Location,
                PrivateParty = party.PrivateParty!.Value,
                UserID = viking.Uid
            };

            if (party.Location == "MyNeighborhood") userParty.DisplayName = $"{userParty.UserName}'s Block Party";
            if (party.Location == "MyVIPRoomInt") userParty.DisplayName = $"{userParty.UserName}'s VIP Party";
            if (party.Location == "MyPodInt") {
                // Only way to do this without adding another column to the table.
                if (party.AssetBundle == "RS_DATA/PfMyPodBirthdayParty.unity3d/PfMyPodBirthdayParty") {
                    userParty.DisplayName = $"{userParty.UserName}'s Pod Birthday Party";
                } else {
                    userParty.DisplayName = $"{userParty.UserName}'s Pod Party";
                }
            }

            uint gameID = ClientVersion.GetGameID(apiKey);
            // Send only JumpStart parties to JumpStart
            if (gameID == ClientVersion.WoJS
                && (party.Location == "MyNeighborhood"
                || party.Location == "MyVIPRoomInt")) {
                userParties.Add(userParty);
            // Send only Math Blaster parties to Math Blaster
            } else if (gameID == ClientVersion.MB
                && party.Location == "MyPodInt") {
                userParties.Add(userParty);
            }
        }

        return Ok(new UserPartyData { NonBuddyParties = userParties.ToArray() });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetPartiesByUserID")] // used by World Of Jumpstart
    public IActionResult GetPartiesByUserID([FromForm] Guid userId)
    {
        Viking? viking = ctx.Vikings.FirstOrDefault(e => e.Uid == userId);
        List<UserPartyComplete> parties = new List<UserPartyComplete>();

        if(viking is null)
        {
            return Ok(new ArrayOfUserPartyComplete());
        }

        bool needSave = false;
        foreach(var party in viking.Parties)
        {
            if (DateTime.UtcNow >= party.ExpirationDate)
            {
                viking.Parties.Remove(party);
                needSave = true;
                continue;
            }

            AvatarData avatarData = XmlUtil.DeserializeXml<AvatarData>(viking.AvatarSerialized);
            UserPartyComplete userPartyComplete = new UserPartyComplete
            {
                DisplayName = avatarData.DisplayName,
                UserName = avatarData.DisplayName,
                ExpirationDate = party.ExpirationDate,
                Icon = party.LocationIconAsset,
                Location = party.Location,
                PrivateParty = party.PrivateParty!.Value,
                UserID = viking.Uid,
                AssetBundle = party.AssetBundle
            };
            parties.Add(userPartyComplete);
        }

        if (needSave)
            ctx.SaveChanges();

        return Ok(new ArrayOfUserPartyComplete { UserPartyComplete = parties.ToArray() });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/PurchaseParty")] // used by World Of Jumpstart
    [VikingSession]
    public IActionResult PurchaseParty(Viking viking, [FromForm] int itemId, [FromForm] string apiKey)
    {
        ItemData itemData = itemService.GetItem(itemId);

        // create a party based on bought itemid
        Party party = new Party
        {
            PrivateParty = false
        };

        string? partyType = itemData.Attribute?.FirstOrDefault(a => a.Key == "PartyType").Value;

        if (partyType is null) {
            return Ok(null);
        }

        uint gameVersion = ClientVersion.GetVersion(apiKey);
        if (partyType == "Default") {
            if (gameVersion == ClientVersion.MB) {
                party.Location = "MyPodInt";
                party.LocationIconAsset = "RS_DATA/PfUiPartiesListMB.unity3d/IcoMbPartyDefault";
                party.AssetBundle = "RS_DATA/PfMyPodParty.unity3d/PfMyPodParty";
            } else {
                party.Location = "MyNeighborhood";
                party.LocationIconAsset = "RS_DATA/PfUiPartiesList.unity3d/IcoPartyLocationMyNeighborhood";
                party.AssetBundle = "RS_DATA/PfMyNeighborhoodParty.unity3d/PfMyNeighborhoodParty";
            }
        } else if (partyType == "VIPRoom") {
            party.Location = "MyVIPRoomInt";
            party.LocationIconAsset = "RS_DATA/PfUiPartiesList.unity3d/IcoPartyDefault";
            party.AssetBundle = "RS_DATA/PfMyVIPRoomIntPartyGroup.unity3d/PfMyVIPRoomIntPartyGroup";
        } else if (partyType == "Birthday") {
            party.Location = "MyPodInt";
            party.LocationIconAsset = "RS_DATA/PfUiPartiesListMB.unity3d/IcoMbPartyBirthday";
            party.AssetBundle = "RS_DATA/PfMyPodBirthdayParty.unity3d/PfMyPodBirthdayParty";
        } else {
            Console.WriteLine($"Unsupported partyType = {partyType}");
            return Ok(null);
        }

        party.ExpirationDate = DateTime.UtcNow.AddMinutes(
            Int32.Parse(itemData.Attribute.FirstOrDefault(a => a.Key == "Time").Value)
        );

        // check if party already exists
        if (viking.Parties.FirstOrDefault(e => e.Location == party.Location) != null) return Ok(null);

        // take away coins
        viking.AchievementPoints.FirstOrDefault(e => e.Type == (int)AchievementPointTypes.GameCurrency)!.Value -= itemData.Cost;

        viking.Parties.Add(party);
        ctx.SaveChanges();

        return Ok(true);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetUserActivityByUserID")]
    public IActionResult GetUserActivityByUserID() {
        // TODO: This is a placeholder
        return Ok(new ArrayOfUserActivity { UserActivity = new UserActivity[0] });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetNextItemState")]
    [VikingSession]
    public IActionResult SetNextItemState(Viking viking, [FromForm] string setNextItemStateRequest) {
        SetNextItemStateRequest request = XmlUtil.DeserializeXml<SetNextItemStateRequest>(setNextItemStateRequest);
        RoomItem? item = ctx.RoomItems.FirstOrDefault(x => x.Id == request.UserItemPositionID);
        if (item is null)
            return Ok();

        if (item.Room.Viking != viking)
            return Unauthorized("Can't set state not owned item");

        // NOTE: The game sets OverrideStateCriteria only if a speedup is used
        return Ok(roomService.NextItemState(item, request.OverrideStateCriteria));
    }

    [HttpPost]
    //[Produces("application/xml")]
    [Route("ContentWebService.asmx/GetDisplayNames")] // used by World Of Jumpstart
    [Route("ContentWebService.asmx/GetDisplayNamesByCategoryID")] // used by Math Blaster
    public IActionResult GetDisplayNames() {
        // TODO: This is a placeholder
        return Ok(XmlUtil.ReadResourceXmlString("displaynames"));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetDisplayNameByUserId")] // used by World Of Jumpstart
    public IActionResult GetDisplayNameByUserId([FromForm] Guid userId)
    {
        Viking? idViking = ctx.Vikings.FirstOrDefault(e => e.Uid == userId);
        if (idViking is null) return Ok("???");

        // return display name
        return Ok(XmlUtil.DeserializeXml<AvatarData>(idViking.AvatarSerialized!).DisplayName);
    }

    [HttpPost]
    //[Produces("application/xml")]
    [Route("ContentWebService.asmx/SetDisplayName")] // used by World Of Jumpstart
    [VikingSession]
    public IActionResult SetProduct(Viking viking, [FromForm] int firstNameID, [FromForm] int secondNameID, [FromForm] int thirdNameID) {
        AvatarData avatarData = XmlUtil.DeserializeXml<AvatarData>(viking.AvatarSerialized);
        avatarData.DisplayName = xmlDataService.GetDisplayName(firstNameID, secondNameID, thirdNameID);
        viking.AvatarSerialized = XmlUtil.SerializeXml(avatarData);
        ctx.SaveChanges();
        return Ok(true);
    }

    [HttpPost]
    //[Produces("application/xml")]
    [Route("ContentWebService.asmx/GetScene")] // used by World Of Jumpstart
    [VikingSession]
    public IActionResult GetScene(Viking viking, [FromForm] string sceneName) {
        SceneData? scene = viking.SceneData.FirstOrDefault(e => e.SceneName == sceneName);

        if (scene is not null) return Ok(scene.XmlData);
        else return Ok("");
    }

    [HttpPost]
    [Route("ContentWebSerivce.asmx/GetHouse")] // used by World Of Jumpstart
    [VikingSession]
    public IActionResult GetHouse(Viking viking) {
        string? ret = Util.SavedData.Get(
            viking,
            Util.SavedData.House()
        );
        if (ret != null)
            return Ok(ret);
        return Ok(XmlUtil.ReadResourceXmlString("defaulthouse"));
    }

    [HttpPost]
    [Route("ContentWebService.asmx/GetHouseByUserId")] // used by World Of Jumpstart
    public IActionResult GetHouseByUserId([FromForm] Guid userId)
    {
        return GetHouse(ctx.Vikings.FirstOrDefault(e => e.Uid == userId));
    }

    [HttpPost]
    //[Produces("application/xml")]
    [Route("ContentWebService.asmx/GetSceneByUserId")] // used by World Of Jumpstart
    public IActionResult GetSceneByUserId([FromForm] Guid userId, [FromForm] string sceneName) {
        SceneData? scene = ctx.Vikings.FirstOrDefault(e => e.Uid == userId)?.SceneData.FirstOrDefault(x => x.SceneName == sceneName);

        if (scene is not null) return Ok(scene.XmlData);
        else return Ok(null);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetScene")] // used by World of Jumpstart
    [VikingSession]
    public IActionResult SetScene(Viking viking, [FromForm] string sceneName, [FromForm] string contentXml) {
        SceneData? existingScene = viking.SceneData.FirstOrDefault(e => e.SceneName == sceneName);

        if(existingScene is not null)
        {
            existingScene.XmlData = contentXml;
            ctx.SaveChanges();
            return Ok(true);
        }
        else
        {
            SceneData sceneData = new SceneData
            {
                SceneName = sceneName,
                XmlData = contentXml
            };
            viking.SceneData.Add(sceneData);
            ctx.SaveChanges(); 
            return Ok(true);
        }
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetHouse")] // used by World Of Jumpstart
    [VikingSession]
    public IActionResult SetHouse(Viking viking, [FromForm] string contentXml) {
        Util.SavedData.Set(
            viking,
            Util.SavedData.House(),
            contentXml
        );
        ctx.SaveChanges();
        return Ok(true);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetNeighbor")] // used by World Of Jumpstart
    [VikingSession(UseLock=true)]
    public IActionResult SetNeighbor(Viking viking, string neighboruserid, int slot) {
        return Ok(neighborhoodService.SaveNeighbors(viking, neighboruserid, slot));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetNeighborsByUserID")] // used by World Of Jumpstart
    public IActionResult GetNeighborsByUserID(string userId) {
        return Ok(neighborhoodService.GetNeighbors(userId));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/GetGameData")]
    [VikingSession]
    public IActionResult GetGameData(Viking viking, [FromForm] string gameDataRequest) {
        GetGameDataRequest request = XmlUtil.DeserializeXml<GetGameDataRequest>(gameDataRequest);
        return Ok(gameDataService.GetGameDataForPlayer(viking, request));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetUserGameCurrency")]
    [VikingSession]
    public IActionResult GetUserGameCurrency(Viking viking) {
        return Ok(achievementService.GetUserCurrency(viking));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetGameCurrency")]
    [VikingSession]
    public IActionResult GetGameCurrency(Viking viking) {
        return Ok(achievementService.GetUserCurrency(viking).GameCurrency);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetGameCurrency")] // used by World Of Jumpstart
    [VikingSession]
    public IActionResult SetUserGameCurrency(Viking viking, [FromForm] int amount)
    {
        achievementService.AddAchievementPoints(viking, AchievementPointTypes.GameCurrency, amount);

        ctx.SaveChanges();
        return Ok(achievementService.GetUserCurrency(viking).GameCurrency ?? 0);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/RerollUserItem")]
    [VikingSession]
    public IActionResult RerollUserItem(Viking viking, [FromForm] string request) {
        RollUserItemRequest req = XmlUtil.DeserializeXml<RollUserItemRequest>(request);

        // get item
        InventoryItem? invItem = viking.InventoryItems.FirstOrDefault(e => e.Id == req.UserInventoryID);
        if (invItem is null)
            return Ok(new RollUserItemResponse { Status = Status.ItemNotFound });
        
        // get item data and stats
        ItemData itemData = itemService.GetItem(invItem.ItemId);
        ItemStatsMap itemStatsMap;
        if (invItem.StatsSerialized != null) {
            itemStatsMap = XmlUtil.DeserializeXml<ItemStatsMap>(invItem.StatsSerialized);
        } else {
            itemStatsMap = itemData.ItemStatsMap;
        }

        List<ItemStat> newStats;
        Status status = Status.Failure;
        
        // update stats
        if (req.ItemStatNames != null) {
            // reroll only one stat (from req.ItemStatNames)
            newStats = new List<ItemStat>();
            foreach (string name in req.ItemStatNames) {
                ItemStat itemStat = itemStatsMap.ItemStats.FirstOrDefault(e => e.Name == name);

                if (itemStat is null)
                    return Ok(new RollUserItemResponse { Status = Status.InvalidStatsMap });

                // draw new stats
                StatRangeMap rangeMap = itemData.PossibleStatsMap.Stats.FirstOrDefault(e => e.ItemStatsID == itemStat.ItemStatID).ItemStatsRangeMaps.FirstOrDefault(e => e.ItemTierID == (int)(itemStatsMap.ItemTier));
                int newVal = random.Next(rangeMap.StartRange, rangeMap.EndRange+1);

                // check draw results
                Int32.TryParse(itemStat.Value, out int oldVal);
                if (newVal > oldVal) {
                    itemStat.Value = newVal.ToString();
                    newStats.Add(itemStat);
                    status = Status.Success;
                }
            }
            // get shards
            inventoryService.AddItemToInventory(viking, InventoryService.Shards, -((int)(itemData.ItemRarity) + (int)(itemStatsMap.ItemTier) - 1));
        } else {
            // reroll full item
            newStats = itemService.CreateItemStats(itemData.PossibleStatsMap, (int)itemData.ItemRarity, (int)itemStatsMap.ItemTier);
            itemStatsMap.ItemStats = newStats.ToArray();
            status = Status.Success;
            // get shards
            int price = 0;
            switch (itemData.ItemRarity) {
                case ItemRarity.Common:
                    price = 5;
                    break;
                case ItemRarity.Rare:
                    price = 7;
                    break;
                case ItemRarity.Epic:
                    price = 10;
                    break;
                case ItemRarity.Legendary:
                    price = 20;
                    break;
            }
            switch (itemStatsMap.ItemTier) {
                case ItemTier.Tier2:
                    price = (int)Math.Floor(price * 1.5);
                    break;
                case ItemTier.Tier3:
                case ItemTier.Tier4:
                    price = price * 2;
                    break;
            }
            inventoryService.AddItemToInventory(viking, InventoryService.Shards, -price);
        }
 
        // save
        invItem.StatsSerialized = XmlUtil.SerializeXml(itemStatsMap);
        ctx.SaveChanges();

        // return results
        return Ok(new RollUserItemResponse {
            Status = status,
            ItemStats = newStats.ToArray() // we need return only updated stats, so can't `= itemStatsMap.ItemStats`
        });
    }
    
    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/FuseItems")]
    [VikingSession]
    public IActionResult FuseItems(Viking viking, [FromForm] string fuseItemsRequest) {
        FuseItemsRequest req = XmlUtil.DeserializeXml<FuseItemsRequest>(fuseItemsRequest);

        ItemData blueprintItem;
        try {
            if (req.BluePrintInventoryID != null) {
                blueprintItem = itemService.GetItem(
                    viking.InventoryItems.FirstOrDefault(e => e.Id == req.BluePrintInventoryID).ItemId
                );
            } else {
                blueprintItem = itemService.GetItem(req.BluePrintItemID ?? -1);
            }
        } catch(System.Collections.Generic.KeyNotFoundException) {
            return Ok(new FuseItemsResponse { Status = Status.BluePrintItemNotFound });
        }

        // TODO: check for blueprintItem.BluePrint.Deductibles and blueprintItem.BluePrint.Ingredients
        
        // remove items from DeductibleItemInventoryMaps and BluePrintFuseItemMaps
        foreach (var item in req.DeductibleItemInventoryMaps) {
            InventoryItem? invItem = viking.InventoryItems.FirstOrDefault(e => e.Id == item.UserInventoryID);
            if (invItem is null) {
                invItem = viking.InventoryItems.FirstOrDefault(e => e.ItemId == item.ItemID);
            }
            if (invItem is null || invItem.Quantity < item.Quantity) {
                return Ok(new FuseItemsResponse { Status = Status.ItemNotFound });
            }
            invItem.Quantity -= item.Quantity;
        }
        foreach (var item in req.BluePrintFuseItemMaps) {
            if (item.UserInventoryID < 0) {
                continue; // TODO: what we should do in this case?
            }
            InventoryItem? invItem = viking.InventoryItems.FirstOrDefault(e => e.Id == item.UserInventoryID);
            if (invItem is null)
                return Ok(new FuseItemsResponse { Status = Status.ItemNotFound });
            viking.InventoryItems.Remove(invItem);
        }
        // NOTE: we haven't saved any changes so far ... so we can safely interrupt "fusing" by return in loops above
        
        var resItemList = new List<InventoryItemStatsMap>();
        Gender gender = XmlUtil.DeserializeXml<AvatarData>(viking.AvatarSerialized).GenderType;
        foreach (BluePrintSpecification output in blueprintItem.BluePrint.Outputs) {
            if (output.ItemID is null)
                continue;

            itemService.CheckAndOpenBox((int)(output.ItemID), gender, out int newItemId, out int quantity);
            for (int i=0; i<quantity; ++i) {
                if (output.Tier is null)
                    throw new Exception($"Blueprint {blueprintItem.ItemID} hasn't output tier. Fix item definition: <bp> -> <OUT> -> <T>");
                resItemList.Add(
                    inventoryService.AddBattleItemToInventory(viking, newItemId, (int)output.Tier)
                );
            }
        }
        
        // NOTE: saved inside AddBattleItemToInventory
         
        // return response with new item info
        return Ok(new FuseItemsResponse {
            Status = Status.Success,
            InventoryItemStatsMaps = resItemList
        });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/SellItems")]
    [VikingSession]
    public IActionResult SellItems(Viking viking, [FromForm] string sellItemsRequest) {
        int shard = 0;
        int gold = 0;
        SellItemsRequest req = XmlUtil.DeserializeXml<SellItemsRequest>(sellItemsRequest);
        foreach (var invItemID in req.UserInventoryCommonIDs) {
            inventoryService.SellInventoryItem(viking, invItemID, ref gold, ref shard);
        }

        if (gold == 0 && shard == 0) { // NOTE: client sometimes call SellItems with invalid UserInventoryCommonIDs for unknown reasons
            return Ok(new CommonInventoryResponse { Success = false });
        }

        // apply shards reward
        CommonInventoryResponseItem resShardsItem = inventoryService.AddItemToInventoryAndGetResponse(viking, InventoryService.Shards, shard);
        
        // apply cash (gold) reward from sell items
        achievementService.AddAchievementPoints(viking, AchievementPointTypes.GameCurrency, gold);
        
        // save
        ctx.SaveChanges();

        // return success with shards reward
        return Ok(new CommonInventoryResponse {
            Success = true,
            CommonInventoryIDs = new CommonInventoryResponseItem[] {
                resShardsItem
            },
            UserGameCurrency = achievementService.GetUserCurrency(viking)
        });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/AddBattleItems")]
    [VikingSession]
    public IActionResult AddBattleItems(Viking viking, [FromForm] string request) {
        AddBattleItemsRequest req = XmlUtil.DeserializeXml<AddBattleItemsRequest>(request);
        
        var resItemList = new List<InventoryItemStatsMap>();
        foreach (BattleItemTierMap battleItemTierMap in req.BattleItemTierMaps) {
            for (int i=0; i<battleItemTierMap.Quantity; ++i) {
                resItemList.Add(
                    inventoryService.AddBattleItemToInventory(viking, battleItemTierMap.ItemID, (int)battleItemTierMap.Tier, battleItemTierMap.ItemStats)
                    // NOTE: battleItemTierMap.ItemStats is extension for importer
                );
            }
        }
        
        // NOTE: saved inside AddBattleItemToInventory
        
        return Ok(new AddBattleItemsResponse{
            Status = Status.Success,
            InventoryItemStatsMaps = resItemList
        });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/ProcessRewardedItems")]
    [VikingSession]
    public IActionResult ProcessRewardedItems(Viking viking, [FromForm] string request) {
        ProcessRewardedItemsRequest req = XmlUtil.DeserializeXml<ProcessRewardedItemsRequest>(request);
        
        if (req is null || req.ItemsActionMap is null)
            return Ok(new ProcessRewardedItemsResponse());
        
        int shard = 0;
        int gold = 0;
        bool soldInventoryItems = false;
        bool soldRewardBinItems = false;
        var itemsAddedToInventory = new List<CommonInventoryResponseRewardBinItem>();
        foreach (ItemActionTypeMap actionMap in req.ItemsActionMap) {
            switch (actionMap.Action) {
                case ActionType.MoveToInventory:
                    // item is in inventory in result of ApplyRewards ... only add to itemsAddedToInventory
                    itemsAddedToInventory.Add (new CommonInventoryResponseRewardBinItem {
                        ItemID = viking.InventoryItems.FirstOrDefault(e => e.Id == actionMap.ID).ItemId,
                        CommonInventoryID = actionMap.ID,
                        Quantity = 0,
                        UserItemStatsMapID = actionMap.ID
                    });
                    break;
                case ActionType.SellInventoryItem:
                    soldInventoryItems = true;
                    inventoryService.SellInventoryItem(viking, actionMap.ID, ref gold, ref shard);
                    break;
                case ActionType.SellRewardBinItem:
                    soldRewardBinItems = true;
                    inventoryService.SellInventoryItem(viking, actionMap.ID, ref gold, ref shard);
                    break;
            }
        }
        
        // apply shards reward from sell items
        InventoryItem item = inventoryService.AddItemToInventory(viking, InventoryService.Shards, shard);
        
        // NOTE: client expects multiple items each with quantity = 0
        var inventoryResponse = new CommonInventoryResponseItem[shard];
        for (int i=0; i<shard; ++i) {
            inventoryResponse[i] = new CommonInventoryResponseItem {
                CommonInventoryID = item.Id,
                ItemID = item.ItemId,
                Quantity = 0
            };
        }
        
        // apply cash (gold) reward from sell items
        achievementService.AddAchievementPoints(viking, AchievementPointTypes.GameCurrency, gold);
        
        // save
        ctx.SaveChanges();
        
        return Ok(new ProcessRewardedItemsResponse {
            SoldInventoryItems = soldInventoryItems,
            SoldRewardBinItems = soldRewardBinItems,
            MovedRewardBinItems = itemsAddedToInventory.ToArray(),
            CommonInventoryResponse = new CommonInventoryResponse {
                Success = false,
                CommonInventoryIDs = inventoryResponse,
                UserGameCurrency = achievementService.GetUserCurrency(viking)
            }
        });
    }
    
    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/ApplyRewards")]
    [VikingSession]
    public IActionResult ApplyRewards(Viking viking, [FromForm] string request) {
        ApplyRewardsRequest req = XmlUtil.DeserializeXml<ApplyRewardsRequest>(request);
        
        List<AchievementReward> achievementRewards = new List<AchievementReward>();
        UserItemStatsMap? rewardedBattleItem = null;
        CommonInventoryResponse? rewardedStandardItem = null;
        
        int rewardMultipler = 0;
        if (req.LevelRewardType == LevelRewardType.LevelFailure) {
            rewardMultipler = 1;
        } else if (req.LevelRewardType == LevelRewardType.LevelCompletion) {
            rewardMultipler = 2 * req.LevelDifficultyID;
        }
        
        if (rewardMultipler > 0) {
            // TODO: XP values and method of calculation is not grounded in anything ...

            // dragons XP
            if (req.RaisedPetEntityMaps != null) {
                int dragonXp = 40 * rewardMultipler;
                foreach (RaisedPetEntityMap petInfo in req.RaisedPetEntityMaps) {
                    Dragon? dragon = viking.Dragons.FirstOrDefault(e => e.Id == petInfo.RaisedPetID);
                    dragon.PetXP = (dragon.PetXP ?? 0) + dragonXp;
                    achievementRewards.Add(new AchievementReward{
                        EntityID = petInfo.EntityID,
                        PointTypeID = AchievementPointTypes.DragonXP,
                        Amount = dragonXp
                    });
                }
            }

            // player XP and gems
            achievementRewards.Add(
                achievementService.AddAchievementPoints(viking, AchievementPointTypes.PlayerXP, 60 * rewardMultipler)
            );
            achievementRewards.Add(
                achievementService.AddAchievementPoints(viking, AchievementPointTypes.CashCurrency, 2 * rewardMultipler)
            );
        }

        //  - battle backpack items, blueprints and other items
        if (req.LevelRewardType != LevelRewardType.LevelFailure) {
            Gender gender = XmlUtil.DeserializeXml<AvatarData>(viking.AvatarSerialized).GenderType;
            ItemData rewardItem = itemService.GetDTReward(gender);
            if (itemService.ItemHasCategory(rewardItem, 651) || rewardItem.PossibleStatsMap is null) {
                // blueprint or no battle item (including box)
                List<CommonInventoryResponseItem> standardItems = new List<CommonInventoryResponseItem>();
                itemService.CheckAndOpenBox(rewardItem.ItemID, gender, out int itemId, out int quantity);
                for (int i=0; i<quantity; ++i) {
                    standardItems.Add(inventoryService.AddItemToInventoryAndGetResponse(viking, itemId, 1));
                    // NOTE: client require single quantity items
                }
                rewardedStandardItem = new CommonInventoryResponse {
                    Success = true,
                    CommonInventoryIDs = standardItems.ToArray()
                };
            } else {
                // DT item
                InventoryItemStatsMap item = inventoryService.AddBattleItemToInventory(viking, rewardItem.ItemID, random.Next(1, 4));
                rewardedBattleItem = new UserItemStatsMap {
                    Item = item.Item,
                    ItemStats = item.ItemStatsMap.ItemStats,
                    ItemTier = item.ItemStatsMap.ItemTier,
                    UserItemStatsMapID = item.CommonInventoryID,
                    CreatedDate = new DateTime(DateTime.Now.Ticks)
                };
            }
        }

        // save
        ctx.SaveChanges();
        
        return Ok(new ApplyRewardsResponse {
            Status = Status.Success,
            AchievementRewards = achievementRewards.ToArray(),
            RewardedItemStatsMap = rewardedBattleItem,
            CommonInventoryResponse = rewardedStandardItem,
        });
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SendRawGameData")]
    [VikingSession(UseLock = true)]
    public IActionResult SendRawGameData(Viking viking, [FromForm] int gameId, bool isMultiplayer, int difficulty, int gameLevel, string xmlDocumentData, bool win, bool loss) {
        return Ok(gameDataService.SaveGameData(viking, gameId, isMultiplayer, difficulty, gameLevel, xmlDocumentData, win, loss));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetGameDataByGame")]
    [VikingSession(UseLock = true)]
    public IActionResult GetGameDataByGame(Viking viking, [FromForm] int gameId, bool isMultiplayer, int difficulty, int gameLevel, string key, int count, bool AscendingOrder, int score, bool buddyFilter, string apiKey) {
        return Ok(gameDataService.GetGameData(viking, gameId, isMultiplayer, difficulty, gameLevel, key, count, AscendingOrder, buddyFilter, apiKey));
    }
        
    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetGameDataByUser")] // used in My Scores
    [VikingSession(UseLock = true)]
    public IActionResult GetGameDataByUser(Viking viking, [FromForm] int gameId, bool isMultiplayer, int difficulty, int gameLevel, string key, int count, bool AscendingOrder, string apiKey) {
        return Ok(gameDataService.GetGameDataByUser(viking, gameId, isMultiplayer, difficulty, gameLevel, key, count, AscendingOrder, apiKey));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("V2/ContentWebService.asmx/GetGameDataByGameForDateRange")]
    [VikingSession(UseLock = true)]
    public IActionResult GetGameDataByGameForDateRange(Viking viking, [FromForm] int gameId, bool isMultiplayer, int difficulty, int gameLevel, string key, int count, bool AscendingOrder, int score, string startDate, string endDate, bool buddyFilter, string apiKey) {
        CultureInfo usCulture = new CultureInfo("en-US", false);
        return Ok(gameDataService.GetGameData(viking, gameId, isMultiplayer, difficulty, gameLevel, key, count, AscendingOrder, buddyFilter, apiKey, DateTime.Parse(startDate, usCulture), DateTime.Parse(endDate, usCulture)));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetPeriodicGameDataByGame")] // used by Math Blaster and WoJS (probably from 24 hours ago to now)
    [VikingSession(UseLock = true)]
    public IActionResult GetPeriodicGameDataByGame(Viking viking, [FromForm] int gameId, bool isMultiplayer, int difficulty, int gameLevel, string key, int count, bool AscendingOrder, int score, bool buddyFilter, string apiKey) {
        return Ok(gameDataService.GetGameData(viking, gameId, isMultiplayer, difficulty, gameLevel, key, count, AscendingOrder, buddyFilter, apiKey, DateTime.Now.AddHours(-24), DateTime.Now));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetGamePlayDataForDateRange")] // used by WoJS
    public IActionResult GetGamePlayDataForDateRange(Viking viking, string startDate, string endDate) {
        // stub, didn't work for some reason, even with the correct response
        return Ok(new ArrayOfGamePlayData());
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("MissionWebService.asmx/GetTreasureChest")] // used by Math Blaster
    public IActionResult GetTreasureChest() {
        // TODO: This is a placeholder
        return Ok(new TreasureChestData());
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("MissionWebService.asmx/GetWorldId")] // used by Math Blaster and WoJS Adventureland
    public IActionResult GetWorldId([FromForm] int gameId, [FromForm] string sceneName) {
        return Ok(xmlDataService.GetWorldID(sceneName));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("MissionWebService.asmx/GetMission")] // old ("step") missions - used by MB and WoJS lands
    public IActionResult GetMission([FromForm] int gameId, [FromForm] string name) {
        return Ok(missionStore.GetStepsMissions(gameId, name));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("MissionWebService.asmx/GetStep")] // old ("step") missions - used by MB and WoJS lands
    public IActionResult GetMissionStep([FromForm] int stepId) {
        return Ok(missionStore.GetStep(stepId));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetUserMission")] // old ("step") missions - used by MB and WoJS lands
    [VikingSession]
    public IActionResult GetUserMission(Viking viking, [FromForm] int worldId) {
        return Ok(missionService.GetUserMissionData(viking, worldId));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetUserMission")] // old ("step") missions - used by MB and WoJS lands
    [VikingSession(UseLock=true)]
    public IActionResult SetUserMission(Viking viking, [FromForm] int worldId, [FromForm] int missionId, [FromForm] int stepId, [FromForm] int taskId) {
        missionService.SetOrUpdateUserMissionData(viking, worldId, missionId, stepId, taskId);
        return Ok(true);
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetUserMissionComplete")] // old ("step") missions - used by MB and WoJS lands
    [VikingSession]
    public IActionResult SetUserMissionComplete(Viking viking, [FromForm] int worldId, [FromForm] int missionId) {
        return Ok(missionService.SetUserMissionCompleted(viking, worldId, missionId, true));
    }

    [HttpPost]
    //[Produces("application/xml")]
    [Route("MissionWebService.asmx/GetBadge")] // old ("step") missions - used by MB and WoJS lands
    public IActionResult GetBadge([FromForm] int gameId) {
        if (gameId == 1) return Ok(XmlUtil.ReadResourceXmlString("missions.badge_wojs_al"));
        return Ok();
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetUserBadgeComplete")] // old ("step") missions - used by MB and WoJS lands
    [VikingSession]
    public IActionResult SetUserBadgeComplete(Viking viking, [FromForm] int badgeId) {
        return Ok(missionService.SetUserBadgeComplete(viking, badgeId));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetUserBadgeComplete")] // old ("step") missions - used by MB and WoJS lands
    [VikingSession]
    public IActionResult GetUserBadgeComplete(Viking viking) {
        return Ok(missionService.GetUserBadgesCompleted(viking));
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetRevealIndex")] // used by World Of Jumpstart (Learning Games)
    public IActionResult GetRevealIndex()
    {
        // TODO - figure out proper way of doing this, if any
        return Ok(random.Next(1, 15));
    }

    [HttpPost]
    //[Produces("application/xml")]
    [Route("ContentWebService.asmx/GetGameProgress")] // used by Math Blaster (Ice Cubed)
    [VikingSession]
    public string GetGameProgress(Viking viking, [FromForm] int gameId) {
        string? ret = Util.SavedData.Get(
            viking,
            (uint)gameId
        );
        if (ret is null)
            return XmlUtil.SerializeXml<GameProgress>(null);
        return ret;
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/SetGameProgress")] // used by Math Blaster (Ice Cubed)
    [VikingSession]
    public IActionResult SetGameProgress(Viking viking, [FromForm] int gameId, [FromForm] string xmlDocumentData) {
        Util.SavedData.Set(
            viking,
            (uint)gameId,
            xmlDocumentData
        );
        ctx.SaveChanges();
        return Ok();
    }

    [HttpPost]
    [Produces("application/xml")]
    [Route("ContentWebService.asmx/GetTracksByUserID")]
    public IActionResult GetTracksByUserID()
    {
        // TODO
        return Ok(new TrackInfo());
    }

    private static RaisedPetData GetRaisedPetDataFromDragon (Dragon dragon, int? selectedDragonId = null) {
        if (selectedDragonId is null)
            selectedDragonId = dragon.Viking.SelectedDragonId;
        RaisedPetData data = XmlUtil.DeserializeXml<RaisedPetData>(dragon.RaisedPetData);
        data.RaisedPetID = dragon.Id;
        data.EntityID = dragon.EntityId;
        data.IsSelected = (selectedDragonId == dragon.Id);
        return data;
    }

    // Needs to merge newDragonData into dragonData
    private RaisedPetData UpdateDragon (Dragon dragon, RaisedPetData newDragonData, bool import = false) {
        RaisedPetData dragonData = XmlUtil.DeserializeXml<RaisedPetData>(dragon.RaisedPetData);

        // The simple attributes
        dragonData.IsPetCreated = newDragonData.IsPetCreated;
        if (newDragonData.ValidationMessage is not null) dragonData.ValidationMessage = newDragonData.ValidationMessage;
        if (newDragonData.EntityID is not null) dragonData.EntityID = newDragonData.EntityID;
        if (newDragonData.Name is not null) dragonData.Name = newDragonData.Name;
        dragonData.PetTypeID = newDragonData.PetTypeID;
        if (newDragonData.GrowthState is not null) {
            achievementService.DragonLevelUpOnAgeUp(dragon, dragonData.GrowthState, newDragonData.GrowthState);
            dragonData.GrowthState = newDragonData.GrowthState;
        }
        if (newDragonData.ImagePosition is not null) dragonData.ImagePosition = newDragonData.ImagePosition;
        if (newDragonData.Geometry is not null) dragonData.Geometry = newDragonData.Geometry;
        if (newDragonData.Texture is not null) dragonData.Texture = newDragonData.Texture;
        dragonData.Gender = newDragonData.Gender;
        if (newDragonData.Accessories is not null) dragonData.Accessories = newDragonData.Accessories;
        if (newDragonData.Colors is not null) dragonData.Colors = newDragonData.Colors;
        if (newDragonData.Skills is not null) dragonData.Skills = newDragonData.Skills;
        if (newDragonData.States is not null) dragonData.States = newDragonData.States;
        
        dragonData.IsSelected = newDragonData.IsSelected;
        dragonData.IsReleased = newDragonData.IsReleased;
        dragonData.UpdateDate = newDragonData.UpdateDate;

        if (import) dragonData.CreateDate = newDragonData.CreateDate;

        // Attributes is special - the entire list isn't re-sent, so we need to manually update each
        if (dragonData.Attributes is null) dragonData.Attributes = new RaisedPetAttribute[] { };
        List<RaisedPetAttribute> attribs = dragonData.Attributes.ToList();
        if (newDragonData.Attributes is not null) {
            foreach (RaisedPetAttribute newAttribute in newDragonData.Attributes) {
                RaisedPetAttribute? attribute = attribs.Find(a => a.Key == newAttribute.Key);
                if (attribute is null) {
                    attribs.Add(newAttribute);
                }
                else {
                    attribute.Value = newAttribute.Value;
                    attribute.Type = newAttribute.Type;
                }
            }
            dragonData.Attributes = attribs.ToArray();
        }

        return dragonData;
    }

    private ImageData? GetImageData (Viking viking, String ImageType, int ImageSlot) {
        Image? image = viking.Images.FirstOrDefault(e => e.ImageType == ImageType && e.ImageSlot == ImageSlot);
        if (image is null) {
            return null;
        }

        string imageUrl;
        if (String.IsNullOrEmpty(config.Value.ResponseURL)) {
            imageUrl = string.Format("{0}://{1}/RawImage/{2}/{3}/{4}.jpg", HttpContext.Request.Scheme, HttpContext.Request.Host, viking.Uid, ImageType, ImageSlot);
        } else {
            imageUrl = string.Format("{0}/RawImage/{1}/{2}/{3}.jpg", config.Value.ResponseURL, viking.Uid, ImageType, ImageSlot);
        }

        return new ImageData {
            ImageURL = imageUrl,
            TemplateName = image.TemplateName,
        };
    }

    private void AddSuggestion(Random rand, string name, List<string> suggestions) {
        if (ctx.Vikings.Any(x => x.Name == name) || suggestions.Contains(name)) {
            name += rand.Next(1, 5000);
            if (ctx.Vikings.Any(x => x.Name == name) || suggestions.Contains(name)) return;
        }
        suggestions.Add(name);
    }

    private string GetNameSuggestion(Random rand, string username, string[] adjectives) {
        string name = username;
        if (rand.NextDouble() >= 0.5)
            name = username + "The" + adjectives[rand.Next(adjectives.Length)];
        if (name == username || rand.NextDouble() >= 0.5)
            return adjectives[rand.Next(adjectives.Length)] + name;
        return name;
    }

    private CommonInventoryResponse PurchaseItemsImpl(Viking viking, Dictionary<int, int> itemsToPurchase, bool addAsMysteryBox) {
        // Viking information
        UserGameCurrency currency = achievementService.GetUserCurrency(viking);
        Gender gender = XmlUtil.DeserializeXml<AvatarData>(viking.AvatarSerialized).GenderType;

        // Purchase information
        int totalCoinCost = 0, totalGemCost = 0, coinsToAdd = 0, gemsToAdd = 0;
        Dictionary<int, int> inventoryItemsToAdd = new(); // dict of items to add to the inventory
        Dictionary<int, int> itemsToSendBack = new(); // dict of items that are sent back in the response

        foreach (var i in itemsToPurchase) {
            ItemData item = itemService.GetItem(i.Key);
            // Calculate cost
            totalCoinCost += (int)Math.Round(item.FinalDiscoutModifier * item.Cost) * i.Value;
            totalGemCost += (int)Math.Round(item.FinalDiscoutModifier * item.CashCost) * i.Value;

            // Resolve items to purchase
            if (addAsMysteryBox) {
                // add mystery box to inventory
                inventoryItemsToAdd.TryAdd(i.Key, 0);
                inventoryItemsToAdd[i.Key] += i.Value;
                itemsToSendBack.TryAdd(i.Key, 0);
                itemsToSendBack[i.Key] += i.Value;
            }
            else if (itemService.IsGemBundle(i.Key, out int gemValue)) {
                // get gem value
                gemsToAdd += gemValue * i.Value;
                itemsToSendBack.TryAdd(i.Key, 0);
                itemsToSendBack[i.Key] += i.Value;
            }
            else if (itemService.IsCoinBundle(i.Key, out int coinValue)) {
                // get coin value
                coinsToAdd += coinValue * i.Value;
                itemsToSendBack.TryAdd(i.Key, 0);
                itemsToSendBack[i.Key] += i.Value;
            }
            else if (itemService.IsBundleItem(i.Key)) {
                ItemData bundleItem = itemService.GetItem(i.Key);
                // resolve items in the bundle
                foreach (var reward in bundleItem.Relationship.Where(e => e.Type == "Bundle")) {
                    int quantity = itemService.GetItemQuantity(reward, i.Value);
                    inventoryItemsToAdd.TryAdd(reward.ItemId, 0);
                    inventoryItemsToAdd[reward.ItemId] += quantity;
                    itemsToSendBack.TryAdd(reward.ItemId, 0);
                    itemsToSendBack[reward.ItemId] += quantity;
                }
            }
            else if (itemService.IsBoxItem(i.Key)) {
                // open boxes individually
                for (int j = 0; j < i.Value; j++) {
                    itemService.OpenBox(i.Key, gender, out int itemId, out int quantity);
                    inventoryItemsToAdd.TryAdd(itemId, 0);
                    inventoryItemsToAdd[itemId] += quantity;
                    itemsToSendBack.TryAdd(itemId, 0);
                    itemsToSendBack[itemId] += quantity;
                }
            }
            else {
                // add item to inventory
                inventoryItemsToAdd.TryAdd(i.Key, 0);
                inventoryItemsToAdd[i.Key] += i.Value;
                itemsToSendBack.TryAdd(i.Key, 0);
                itemsToSendBack[i.Key] += i.Value;
            }
        }

        // check if the user can afford the purchase
        if (currency.GameCurrency - totalCoinCost < 0 && currency.CashCurrency - totalGemCost < 0) {
            return new CommonInventoryResponse {
                Success = false,
                CommonInventoryIDs = new CommonInventoryResponseItem[0],
                UserGameCurrency = achievementService.GetUserCurrency(viking)
            };
        }

        // deduct the cost of the purchase
        achievementService.AddAchievementPoints(viking, AchievementPointTypes.GameCurrency, -totalCoinCost + coinsToAdd);
        achievementService.AddAchievementPoints(viking, AchievementPointTypes.CashCurrency, -totalGemCost + gemsToAdd);

        // add items to the inventory (database) and build response
        return inventoryService.AddItemsToInventoryBulkAndGetResponse(
            viking, inventoryItemsToAdd, itemsToSendBack, achievementService.GetUserCurrency(viking)
        );
    }
}
