using System.Net.Mime;
using System.Text.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Prospect.Server.Api.Config;
using Prospect.Server.Api.Models.Client;
using Prospect.Server.Api.Models.Client.Data;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.Auth.User;
using Prospect.Server.Api.Services.Database;
using Prospect.Server.Api.Services.Database.Models;
using Prospect.Server.Api.Services.UserData;
using Prospect.Steam;

namespace Prospect.Server.Api.Controllers;

[ApiController]
[Route("Client")]
public class ClientController : Controller
{
    private const int AppIdDefault = 480;
    private const int AppIdCycleBeta = 1600361;
    private const int AppIdCycle = 868270;

    private readonly ILogger<ClientController> _logger;
    private readonly PlayFabSettings _settings;
    private readonly AuthTokenService _authTokenService;
    private readonly DbUserService _userService;
    private readonly DbEntityService _entityService;
    private readonly UserDataService _userDataService;
    private readonly TitleDataService _titleDataService;
    private readonly Prospect.Server.Api.Services.Steam.SteamWebApiService _steamWebApi;

    public ClientController(ILogger<ClientController> logger,
        IOptions<PlayFabSettings> settings,
        AuthTokenService authTokenService,
        DbUserService userService,
        DbEntityService entityService,
        UserDataService userDataService,
        TitleDataService titleDataService,
        Prospect.Server.Api.Services.Steam.SteamWebApiService steamWebApi)
    {
        _settings = settings.Value;
        _logger = logger;
        _authTokenService = authTokenService;
        _userService = userService;
        _entityService = entityService;
        _userDataService = userDataService;
        _titleDataService = titleDataService;
        _steamWebApi = steamWebApi;
    }

    [AllowAnonymous]
    [HttpPost("LoginWithSteam")]
    [Produces(MediaTypeNames.Application.Json)]
    public async Task<IActionResult> LoginWithSteam(FLoginWithSteamRequest request)
    {
        if (!string.IsNullOrEmpty(request.SteamTicket))
        {
            var ticket = AppTicket.Parse(request.SteamTicket);
            if (ticket.IsValid && ticket.HasValidSignature && (ticket.AppId == AppIdDefault || ticket.AppId == AppIdCycleBeta || ticket.AppId == AppIdCycle))
            {
                var userSteamId = ticket.SteamId.ToString();

                var user = await _userService.FindOrCreateAsync(PlayFabUserAuthType.Steam, userSteamId);

                // Backfill the display name from the Steam persona when we don't have a real
                // one yet (accounts are created as "Unknown"; the client never sends a name).
                if (string.IsNullOrEmpty(user.DisplayName) || user.DisplayName == "Unknown")
                {
                    var persona = await _steamWebApi.GetPersonaNameAsync(userSteamId);
                    if (!string.IsNullOrWhiteSpace(persona))
                    {
                        await _userService.UpdateDisplayNameAsync(user.Id, persona);
                        user.DisplayName = persona;
                    }
                }

                var entity = await _entityService.FindOrCreateAsync(user.Id);

                var userTicket = _authTokenService.GenerateUser(entity);
                var entityTicket = _authTokenService.GenerateEntity(entity);

                await _userDataService.InitAsync(user.Id);

                return Ok(new ClientResponse<FServerLoginResult>
                {
                    Code = 200,
                    Status = "OK",
                    Data = new FServerLoginResult
                    {
                        EntityToken = new FEntityTokenResponse
                        {
                            Entity = new FEntityKey
                            {
                                Id = entity.Id,
                                Type = "title_player_account",
                                TypeString = "title_player_account"
                            },
                            EntityToken = entityTicket,
                            TokenExpiration = DateTime.UtcNow.AddDays(6), // TODO:
                        },
                        InfoResultPayload = new FGetPlayerCombinedInfoResultPayload
                        {
                            CharacterInventories = new List<object>(),
                            PlayerProfile = new FPlayerProfileModel
                            {
                                DisplayName = user.DisplayName,
                                PlayerId = user.Id,
                                PublisherId = _settings.PublisherId,
                                TitleId = _settings.TitleId
                            },
                            UserDataVersion = 0,
                            UserInventory = new List<object>(),
                            UserReadOnlyDataVersion = 0
                        },
                        LastLoginTime = DateTime.UtcNow, // TODO:
                        NewlyCreated = false, // TODO:
                        PlayFabId = user.Id,
                        SessionTicket = userTicket,
                        SettingsForUser = new FUserSettings
                        {
                            GatherDeviceInfo = true,
                            GatherFocusInfo = true,
                            NeedsAttribution = false,
                        },
                        TreatmentAssignment = new FTreatmentAssignment
                        {
                            Variables = new List<FVariable>(),
                            Variants = new List<string>()
                        }
                    }
                });
            }

            _logger.LogWarning("Invalid steam ticket specified, IsExpired {IsExpired}, IsValid {IsValid}, HasValidSignature {Sig}, AppId {AppId}",
                ticket.IsExpired,
                ticket.IsValid,
                ticket.HasValidSignature,
                ticket.AppId);
        }

        return BadRequest(new ClientResponse
        {
            Code = 400,
            Status = "BadRequest",
            Error = "InvalidSteamTicket",
            ErrorCode = 1010,
            ErrorMessage = "Steam API AuthenticateUserTicket error response .."
        });
    }

    [HttpPost("AddGenericID")]
    [Produces(MediaTypeNames.Application.Json)]
    [Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
    public async Task<IActionResult> AddGenericId(FAddGenericIDRequest request)
    {
        // Persist the linked generic service id instead of dropping it (was a no-op stub).
        var userId = User.FindAuthUserId();
        if (request.GenericId != null && !string.IsNullOrEmpty(request.GenericId.ServiceName))
        {
            await _userDataService.UpdateAsync(userId, userId, new Dictionary<string, string>
            {
                ["GenericId_" + request.GenericId.ServiceName] = request.GenericId.UserId ?? ""
            });
        }

        return Ok(new ClientResponse<object>
        {
            Code = 200,
            Status = "OK",
            Data = new {}
        });
    }

    [HttpPost("GetStoreItems")]
    [Produces(MediaTypeNames.Application.Json)]
    [Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
    public IActionResult GetStoreItems(FGetStoreItems request)
    {
        // The vendors (Korolev / ICA / Osiris / QuickShop) and other stores are defined
        // inside the Blueprints title data: each item lists a per-store entry under
        // ItemShopsCraftingData keyed by the store id, with the currency price in its
        // recipe ingredients. This endpoint was previously a stub returning {}, which is
        // why every shop showed up empty. We build the store on the fly from that data.
        var storeId = request.StoreId ?? "";
        var store = new List<FStoreItem>();
        try
        {
            var titleData = _titleDataService.Find(new List<string> { "Blueprints" });
            var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
            if (blueprints != null && !string.IsNullOrEmpty(storeId))
            {
                uint position = 0;
                foreach (var (itemId, blueprint) in blueprints)
                {
                    if (blueprint.ItemShopsCraftingData == null) continue;
                    if (!blueprint.ItemShopsCraftingData.TryGetValue(storeId, out var shopData)) continue;
                    if (shopData?.ItemRecipeIngredients == null) continue;

                    // Only currency ingredients form a purchasable price. Entries whose
                    // recipe is item-based (crafting) are handled by the crafting UI from
                    // the blueprint data the client already has, not by the buy store.
                    var prices = new Dictionary<string, uint>();
                    foreach (var ingredient in shopData.ItemRecipeIngredients)
                    {
                        var code = ingredient.Currency switch
                        {
                            "SoftCurrency" => "SC",
                            "Aurum" => "AU",
                            "InsuranceCurrency" => "IN",
                            _ => null,
                        };
                        if (code == null || ingredient.Amount < 0) continue;
                        prices[code] = prices.TryGetValue(code, out var current)
                            ? current + (uint)ingredient.Amount
                            : (uint)ingredient.Amount;
                    }
                    if (prices.Count == 0) continue;

                    store.Add(new FStoreItem
                    {
                        ItemId = itemId,
                        VirtualCurrencyPrices = prices,
                        RealCurrencyPrices = new Dictionary<string, uint>(),
                        DisplayPosition = position++,
                    });
                }
            }
        }
        catch (Exception e)
        {
            _logger.LogWarning(e, "Failed to build store items for store {StoreId}", storeId);
        }

        // Cosmetic / premium stores are backed by the Vanities title data (not Blueprints):
        // each vanity carries its price in StoreData.Amount, paid in Aurum. AurumShop lists
        // the whole priced catalogue; the daily/weekly rotations (ids from RequestStore
        // RotationData) show a deterministic slice so they change over time but stay stable
        // within a period.
        var isAurumShop = storeId == "AurumShop";
        var isDailyShop = storeId.StartsWith("DailyShop_");
        var isWeeklyShop = storeId.StartsWith("WeeklyShop_");
        if (store.Count == 0 && (isAurumShop || isDailyShop || isWeeklyShop))
        {
            try
            {
                var vanityData = _titleDataService.Find(new List<string> { "Vanities" });
                var vanities = JsonSerializer.Deserialize<List<TitleDataVanityInfo>>(vanityData["Vanities"]);
                var priced = vanities?
                    .Where(v => v.StoreData != null && v.StoreData.Amount > 0)
                    .OrderBy(v => v.Name, StringComparer.Ordinal)
                    .ToList() ?? new List<TitleDataVanityInfo>();

                IEnumerable<TitleDataVanityInfo> selection = priced;
                if ((isDailyShop || isWeeklyShop) && priced.Count > 0)
                {
                    var size = isWeeklyShop ? 12 : 6;
                    int.TryParse(storeId[(storeId.IndexOf('_') + 1)..], out var rotation);
                    var start = (int)(((long)rotation * size) % priced.Count);
                    selection = Enumerable.Range(0, Math.Min(size, priced.Count))
                        .Select(i => priced[(start + i) % priced.Count]);
                }

                uint position = 0;
                foreach (var vanity in selection)
                {
                    store.Add(new FStoreItem
                    {
                        ItemId = vanity.Name,
                        VirtualCurrencyPrices = new Dictionary<string, uint> { ["AU"] = (uint)vanity.StoreData.Amount },
                        RealCurrencyPrices = new Dictionary<string, uint>(),
                        DisplayPosition = position++,
                    });
                }
            }
            catch (Exception e)
            {
                _logger.LogWarning(e, "Failed to build vanity store {StoreId}", storeId);
            }
        }

        _logger.LogInformation("GetStoreItems '{StoreId}': {Count} item(s)", storeId, store.Count);
        return Ok(new ClientResponse<FGetStoreItemsResult>
        {
            Code = 200,
            Status = "OK",
            Data = new FGetStoreItemsResult
            {
                StoreId = string.IsNullOrEmpty(storeId) ? null : storeId,
                CatalogVersion = "StaticItems",
                Store = store,
            }
        });
    }

    [HttpPost("UpdateUserTitleDisplayName")]
    [Produces(MediaTypeNames.Application.Json)]
    [Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
    public async Task<IActionResult> UpdateUserTitleDisplayName(FUpdateUserTitleDisplayNameRequest request)
    {
        // Persist the rename, otherwise it is echoed back but lost on next login.
        var userId = User.FindAuthUserId();
        await _userService.UpdateDisplayNameAsync(userId, request.DisplayName);

        return Ok(new ClientResponse<FUpdateUserTitleDisplayNameResult>
        {
            Code = 200,
            Status = "OK",
            Data = new FUpdateUserTitleDisplayNameResult
            {
                DisplayName = request.DisplayName
            }
        });
    }

    [HttpPost("UpdateUserData")]
    [Produces(MediaTypeNames.Application.Json)]
    [Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
    public async Task<IActionResult> UpdateUserData(FUpdateUserDataRequest request)
    {
        var userId = User.FindAuthUserId();
        await _userDataService.UpdateAsync(userId, request.PlayFabId, request.Data);

        return Ok(new ClientResponse<FUpdateUserDataResult>
        {
            Code = 200,
            Status = "OK",
            Data = new FUpdateUserDataResult
            {
                DataVersion = 0
            }
        });
    }

    [HttpPost("GetUserData")]
    [Produces(MediaTypeNames.Application.Json)]
    [Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
    public async Task<IActionResult> GetUserData(FGetUserDataRequest request)
    {
        var userId = User.FindAuthUserId();
        var userData = await _userDataService.FindAsync(userId, request.PlayFabId, request.Keys);

        return Ok(new ClientResponse<FGetUserDataResult>
        {
            Code = 200,
            Status = "OK",
            Data = new FGetUserDataResult
            {
                Data = userData,
                DataVersion = 0
            }
        });
    }

    [HttpPost("GetUserReadOnlyData")]
    [Produces(MediaTypeNames.Application.Json)]
    [Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
    public async Task<IActionResult> GetUserReadOnlyData(FGetUserDataRequest request)
    {
        var userId = User.FindAuthUserId();
        var userData = await _userDataService.FindAsync(userId, request.PlayFabId, request.Keys);

        return Ok(new ClientResponse<FGetUserDataResult>
        {
            Code = 200,
            Status = "OK",
            Data = new FGetUserDataResult
            {
                Data = userData,
                DataVersion = 0
            }
        });
    }

    [HttpPost("GetUserInventory")]
    [Produces(MediaTypeNames.Application.Json)]
    [Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
    public async Task<IActionResult> GetUserInventory(FGetUserInventoryRequest request)
    {
        var userId = User.FindAuthUserId();
        var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"Balance", "Inventory", "VanityItems", "CharacterVanity", "GlobalVanity"});
        var inventory = JsonSerializer.Deserialize<FYCustomItemInfo[]>(userData["Inventory"].Value);
        // Per-user vanity ownership: the player owns the cosmetics they've bought (VanityItems)
        // plus whatever is currently equipped (CharacterVanity / GlobalVanity), so the starter
        // outfit stays owned. Previously the whole Vanities catalogue was returned as owned,
        // which made every item in the Aurum shop show up as already "owned".
        var ownedVanityIds = CollectOwnedVanityIds(userData);
        var titleData = _titleDataService.Find(new List<string>{"Blueprints"});
        var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
        List<FItemInstance> items = new List<FItemInstance>();
        foreach (var item in inventory) {
            if (!blueprints.ContainsKey(item.BaseItemId)) {
                _logger.LogWarning("Failed to find item with ID {ItemID}", item.BaseItemId);
                continue;
            }
            var blueprintData = blueprints[item.BaseItemId];
            items.Add(new FItemInstance {
                ItemId = item.BaseItemId,
                ItemInstanceId = item.ItemId,
                ItemClass = blueprintData.Kind,
                // PurchaseDate = DateTime.Now, // TODO
                // UnitPrice = 0,
                CatalogVersion = "StaticItems",
                CustomData = new Dictionary<string, string> {
                    ["mods"] = JsonSerializer.Serialize(item.ModData),
                    // ["insurance"] = "None", // TODO
                    ["vanity"] = $"{{\"p\":{item.PrimaryVanityId},\"s\":{item.SecondaryVanityId}}}", // p - primary, s - secondary
                    ["coreData"] = $"{{\"a\":{item.Amount},\"d\":{item.Durability}}}", // a - amount, d - durability
                    ["origin"] = JsonSerializer.Serialize(item.Origin),
                    ["rolledPerks"] = JsonSerializer.Serialize(item.RolledPerks),
                }
            });
        }

        foreach (var vanityId in ownedVanityIds) {
            items.Add(new FItemInstance {
                ItemId = vanityId,
                // ItemInstanceId is not required since it's not a unique item.
                ItemClass = "Vanity",
                // PurchaseDate = DateTime.Now, // TODO
                CatalogVersion = "StaticItems",
                CustomData = new Dictionary<string, string> {
                    ["coreData"] = "{}", // Vanity items don't seem to have any coreData. But it's still required since it's an inventory item.
                    ["origin"] = "{}",
                }
            });
        }

        return Ok(new ClientResponse<FGetUserInventoryResult>
        {
            Code = 200,
            Status = "OK",
            Data = new FGetUserInventoryResult
            {
                Inventory = items,
                VirtualCurrency = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value),
                VirtualCurrencyRechargeTimes = new Dictionary<string, FVirtualCurrencyRechargeTime>()
            }
        });
    }

    // Collects the vanity ids a player actually owns: everything they've purchased
    // (VanityItems) plus everything currently equipped (CharacterVanity slots and the
    // GlobalVanity sprays / banner / emotes / drop pod). Parsed defensively with
    // JsonDocument so malformed / partial data never breaks the inventory response.
    private static HashSet<string> CollectOwnedVanityIds(Dictionary<string, FUserDataRecord> userData)
    {
        var owned = new HashSet<string>(StringComparer.Ordinal);
        void AddId(string? id) { if (!string.IsNullOrEmpty(id)) owned.Add(id); }

        if (userData.TryGetValue("VanityItems", out var viRec) && !string.IsNullOrWhiteSpace(viRec.Value))
        {
            try
            {
                using var doc = JsonDocument.Parse(viRec.Value);
                if (doc.RootElement.ValueKind == JsonValueKind.Array)
                    foreach (var el in doc.RootElement.EnumerateArray())
                        if (el.ValueKind == JsonValueKind.Object && el.TryGetProperty("baseItemId", out var b))
                            AddId(b.GetString());
            }
            catch { /* ignore malformed VanityItems */ }
        }

        if (userData.TryGetValue("CharacterVanity", out var cvRec) && !string.IsNullOrWhiteSpace(cvRec.Value))
        {
            try
            {
                using var doc = JsonDocument.Parse(cvRec.Value);
                if (doc.RootElement.ValueKind == JsonValueKind.Object)
                    foreach (var prop in doc.RootElement.EnumerateObject())
                        if (prop.Value.ValueKind == JsonValueKind.Object && prop.Value.TryGetProperty("id", out var id))
                            AddId(id.GetString());
            }
            catch { /* ignore malformed CharacterVanity */ }
        }

        if (userData.TryGetValue("GlobalVanity", out var gvRec) && !string.IsNullOrWhiteSpace(gvRec.Value))
        {
            try
            {
                using var doc = JsonDocument.Parse(gvRec.Value);
                var root = doc.RootElement;
                if (root.ValueKind == JsonValueKind.Object)
                {
                    if (root.TryGetProperty("activeGlobalVanityIds", out var arr) && arr.ValueKind == JsonValueKind.Array)
                        foreach (var el in arr.EnumerateArray()) AddId(el.GetString());
                    if (root.TryGetProperty("droppodId", out var dp)) AddId(dp.GetString());
                }
            }
            catch { /* ignore malformed GlobalVanity */ }
        }

        return owned;
    }

    [HttpPost("GetTitleData")]
    [Produces(MediaTypeNames.Application.Json)]
    [Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
    public IActionResult GetTitleData(FGetTitleDataRequest request)
    {
        return Ok(new ClientResponse<FGetTitleDataResult>
        {
            Code = 200,
            Status = "OK",
            Data = new FGetTitleDataResult()
            {
                Data = _titleDataService.Find(request.Keys)
            }
        });
    }
}