using System.Text.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SVSim.Database;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.Database.Repositories.Viewer;
using SVSim.EmulatedEntrypoint.Infrastructure;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Admin;
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Admin;

namespace SVSim.EmulatedEntrypoint.Controllers;

/// <summary>
/// Util endpoints for bootstrapping the dev environment. Anonymous-allowed today — security
/// audit pending (don't expose these to the public internet).
/// </summary>
public class AdminController : SVSimController
{
    private readonly IViewerRepository _viewerRepository;
    private readonly SVSimDbContext _dbContext;

    public AdminController(IViewerRepository viewerRepository, SVSimDbContext dbContext)
    {
        _viewerRepository = viewerRepository;
        _dbContext = dbContext;
    }

    /// <summary>
    /// Upsert a viewer from external data (typically captured from the live game via the
    /// SVSimLoader dump). Matches existing viewers by SteamId; creates a new one if missing.
    /// Only essential fields are imported today — extend as needed.
    /// </summary>
    [AllowAnonymous]
    [HttpPost("import_viewer")]
    public async Task<ActionResult<ImportViewerResponse>> ImportViewer(ImportViewerRequest request)
    {
        if (request.SteamId == 0)
        {
            return BadRequest("steam_id is required");
        }

        // SocialAccountConnection is [Owned]-by-Viewer — can't query the owned table directly;
        // look up the Viewer with a matching owned connection instead.
        var existing = await _dbContext.Viewers
            .AsNoTracking()
            .FirstOrDefaultAsync(v => v.SocialAccountConnections.Any(sac =>
                sac.AccountType == SocialAccountType.Steam && sac.AccountId == request.SteamId));

        long viewerId;
        bool wasCreated;

        if (existing is null)
        {
            var created = await _viewerRepository.RegisterViewer(
                request.DisplayName ?? "Imported Viewer",
                SocialAccountType.Steam,
                request.SteamId);
            viewerId = created.Id;
            wasCreated = true;
        }
        else
        {
            viewerId = existing.Id;
            wasCreated = false;
        }

        // Reload with all the nav properties we need to mutate. RegisterViewer SaveChanges'd
        // already, so we re-fetch with full graph and apply the updates. AsSplitQuery to avoid
        // the cartesian-explosion across all the many-to-many cosmetic collections.
        var viewer = await _dbContext.Viewers
            .AsSplitQuery()
            .Include(v => v.Info).ThenInclude(i => i.SelectedEmblem)
            .Include(v => v.Info).ThenInclude(i => i.SelectedDegree)
            .Include(v => v.Currency)
            .Include(v => v.MissionData)
            .Include(v => v.Classes).ThenInclude(c => c.Class)
            .Include(v => v.Sleeves)
            .Include(v => v.Emblems)
            .Include(v => v.Degrees)
            .Include(v => v.LeaderSkins)
            .Include(v => v.MyPageBackgrounds)
            .FirstAsync(v => v.Id == viewerId);

        if (request.DisplayName is not null) viewer.DisplayName = request.DisplayName;
        if (request.CountryCode is not null) viewer.Info.CountryCode = request.CountryCode;
        if (request.TutorialState.HasValue) viewer.MissionData.TutorialState = request.TutorialState.Value;

        if (request.Currency is not null)
        {
            if (request.Currency.Crystals.HasValue) viewer.Currency.Crystals = request.Currency.Crystals.Value;
            if (request.Currency.Rupees.HasValue) viewer.Currency.Rupees = request.Currency.Rupees.Value;
            if (request.Currency.RedEther.HasValue) viewer.Currency.RedEther = request.Currency.RedEther.Value;
        }

        if (request.SelectedEmblemId.HasValue)
        {
            var emblem = await _dbContext.Emblems.FindAsync(request.SelectedEmblemId.Value);
            if (emblem is not null) viewer.Info.SelectedEmblem = emblem;
        }
        if (request.SelectedDegreeId.HasValue)
        {
            var degree = await _dbContext.Degrees.FindAsync(request.SelectedDegreeId.Value);
            if (degree is not null) viewer.Info.SelectedDegree = degree;
        }

        await ReplaceOwned(viewer.Sleeves, request.OwnedSleeveIds, _dbContext.Sleeves);
        await ReplaceOwned(viewer.Emblems, request.OwnedEmblemIds, _dbContext.Emblems);
        await ReplaceOwned(viewer.Degrees, request.OwnedDegreeIds, _dbContext.Degrees);
        await ReplaceOwned(viewer.LeaderSkins, request.OwnedLeaderSkinIds, _dbContext.LeaderSkins);
        await ReplaceOwned(viewer.MyPageBackgrounds, request.OwnedMyPageBackgroundIds, _dbContext.MyPageBackgrounds);

        if (request.Classes is not null)
        {
            foreach (var importClass in request.Classes)
            {
                var existingClass = viewer.Classes.FirstOrDefault(c => c.Class.Id == importClass.ClassId);
                if (existingClass is not null)
                {
                    existingClass.Level = importClass.Level;
                    existingClass.Exp = importClass.Exp;
                }
            }
        }

        // Clone the 8 starter decks into the viewer when freshly created — workaround for a
        // client-side NRE in the deck-edit menu (DeckListUI.IsVisibleCreateNewButton at
        // decompile Wizard/DeckListUI.cs:316 unconditionally reads `_deckGroup.DeckFormat`, but
        // _deckGroup is null when GetCustomDeckGroup() finds no matching CustomDeck group in
        // DeckGroupDataBase — which is exactly what happens for a fresh viewer). Prod players
        // acquire decks via tutorial; we shortcut by seeding the 8 defaults at import time.
        // See docs/audits/deck-edit-empty-decklist-nre-2026-05-23.md for the full background.
        if (wasCreated)
        {
            await CloneDefaultDecksToViewerAsync(viewer);
        }

        await _dbContext.SaveChangesAsync();

        return new ImportViewerResponse
        {
            ViewerId = viewer.Id,
            ShortUdid = viewer.ShortUdid,
            WasCreated = wasCreated
        };
    }

    /// <summary>
    /// Replaces the owned-collection with the master rows matching the supplied ids.
    /// Null `ids` is a no-op (preserve existing). Empty list clears the collection.
    /// </summary>
    private async Task ReplaceOwned<TEntity>(List<TEntity> owned, List<int>? ids, DbSet<TEntity> table)
        where TEntity : class
    {
        if (ids is null) return;
        owned.Clear();
        if (ids.Count == 0) return;

        var rows = await table.Where(e => ids.Contains(EF.Property<int>(e, "Id"))).ToListAsync();
        owned.AddRange(rows);
    }

    /// <summary>
    /// Default sleeve id used for cloned starter decks. Matches prod's wire shape — every
    /// default_deck_list entry on /deck/info has <c>sleeve_id: 3000011</c>.
    /// </summary>
    private const long DefaultSleeveId = 3000011L;

    /// <summary>
    /// Formats we clone the starter decks into. Each format the player can open the deck-edit
    /// menu for needs at least one CustomDeck group in <c>Data.DeckGroupDataBase</c>, otherwise
    /// the client NREs on <c>_deckGroup.DeckFormat</c> in DeckListUI.IsVisibleCreateNewButton.
    /// Rotation / Unlimited / MyRotation are the always-active base formats; PreRotation /
    /// Crossover / Avatar are seasonal and gated by UI state — leave them empty for now (see
    /// docs/audits/deck-edit-empty-decklist-nre-2026-05-23.md follow-ups).
    /// </summary>
    private static readonly Format[] SeededDeckFormats = { Format.Rotation, Format.Unlimited, Format.MyRotation };

    /// <summary>
    /// Materialize the 8 default decks into the viewer's deck collection, once per seeded format.
    /// The tracked <paramref name="viewer"/> instance gets new ShadowverseDeckEntry rows added to
    /// its Decks navigation; EF picks them up on the caller's SaveChangesAsync.
    /// </summary>
    private async Task CloneDefaultDecksToViewerAsync(Viewer viewer)
    {
        var defaultDecks = await _dbContext.DefaultDecks.AsNoTracking().OrderBy(d => d.Id).ToListAsync();
        if (defaultDecks.Count == 0) return;

        // Resolve nav-property entities once. Classes need LeaderSkins included for the
        // DefaultLeaderSkin nav lookup. Cards are fetched in one bulk query keyed by id.
        var classes = await _dbContext.Classes.Include(c => c.LeaderSkins).ToDictionaryAsync(c => c.Id);
        var defaultSleeve = await _dbContext.Sleeves.FindAsync((int)DefaultSleeveId);

        var allCardIds = defaultDecks
            .SelectMany(d => JsonSerializer.Deserialize<List<long>>(d.CardIdArray, JsonbReadOptions.Instance) ?? new List<long>())
            .Distinct()
            .ToList();
        var cards = await _dbContext.Cards.Where(c => allCardIds.Contains(c.Id)).ToDictionaryAsync(c => c.Id);

        // Seeded MyRotation placeholder decks need a real rotation_id, otherwise the client's
        // DeckData.GetMyRotationClassName NREs on `info.LastPackText` when the user clicks one
        // (info is null because Data.MyRotationAllInfo.Get(null) returns null). Pick the highest
        // rotation id available — it includes the most recent pack and therefore covers every
        // class (including class_id=8 Nemesis, which requires last_pack >= 10007).
        var latestMyRotationId = (await _dbContext.MyRotationSettings.AsNoTracking()
            .Select(s => (int?)s.Id)
            .OrderByDescending(id => id)
            .FirstOrDefaultAsync())?.ToString();

        foreach (var format in SeededDeckFormats)
        {
            int slot = 1;
            foreach (var d in defaultDecks)
            {
                if (!classes.TryGetValue(d.ClassId, out var classEntry)) continue;
                var leaderSkin = classEntry.DefaultLeaderSkin ?? classEntry.LeaderSkins.FirstOrDefault();
                if (leaderSkin is null || defaultSleeve is null) continue;

                var cardIdArray = JsonSerializer.Deserialize<List<long>>(d.CardIdArray, JsonbReadOptions.Instance) ?? new List<long>();
                var deckCards = cardIdArray
                    .GroupBy(id => id)
                    .Where(g => cards.ContainsKey(g.Key))
                    .Select(g => new DeckCard { Card = cards[g.Key], Count = g.Count() })
                    .ToList();

                viewer.Decks.Add(new ShadowverseDeckEntry
                {
                    Name = d.DeckName,
                    Number = slot++,
                    Format = format,
                    Class = classEntry,
                    Sleeve = defaultSleeve,
                    LeaderSkin = leaderSkin,
                    RandomLeaderSkin = false,
                    Cards = deckCards,
                    MyRotationId = format == Format.MyRotation ? latestMyRotationId : null,
                });
            }
        }
    }
}
