using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using SVSim.Database;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.Database.Repositories.Deck;
using SVSim.Database.Repositories.Globals;
using SVSim.EmulatedEntrypoint.Configuration;
using SVSim.EmulatedEntrypoint.Constants;
using SVSim.EmulatedEntrypoint.Extensions;
using SVSim.EmulatedEntrypoint.Models.Dtos;
using SVSim.EmulatedEntrypoint.Models.Dtos.Common;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Common;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Deck;
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Deck;

namespace SVSim.EmulatedEntrypoint.Controllers;

public class DeckController : SVSimController
{
    private readonly IDeckRepository _deckRepository;
    private readonly IGlobalsRepository _globalsRepository;
    private readonly SVSimDbContext _dbContext;
    private readonly DeckOptions _deckOptions;

    private static readonly System.Text.Json.JsonSerializerOptions JsonbReadOptions = new()
    {
        PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.SnakeCaseLower,
        NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString,
    };

    public DeckController(IDeckRepository deckRepository, IGlobalsRepository globalsRepository, SVSimDbContext dbContext, IOptions<DeckOptions> deckOptions)
    {
        _deckRepository = deckRepository;
        _globalsRepository = globalsRepository;
        _dbContext = dbContext;
        _deckOptions = deckOptions.Value;
    }

    /// <summary>
    /// Pads a viewer's real deck list with empty-slot placeholders up to <see cref="DeckOptions.MaxDeckSlots"/>.
    /// Required because the client's <c>DeckUI.DeckViewData.CreateDeckViewList</c> only renders
    /// a "New Deck" tile when the response contains an entry whose <c>card_id_array</c> is empty —
    /// without padding, the player cannot create additional decks once any exist.
    /// </summary>
    private List<UserDeck> PadEmptySlots(List<UserDeck> realDecks)
    {
        var taken = realDecks.Select(d => d.DeckNumber).ToHashSet();
        var result = new List<UserDeck>(realDecks);
        for (int slot = 1; slot <= _deckOptions.MaxDeckSlots; slot++)
        {
            if (!taken.Contains(slot))
            {
                result.Add(UserDeck.CreateEmptySlot(slot));
            }
        }
        return result;
    }

    // Request deck_format fields arrive as wire ints (MessagePack-CSharp doesn't honor STJ
    // converters on request DTOs, so request DTO properties stay typed as int). Route through
    // FromApi here so controllers always work in internal Format space when comparing /
    // persisting.
    private static Format AsFormat(int apiValue) => FormatExtensions.FromApi(apiValue);

    [HttpPost("info")]
    public async Task<ActionResult<DeckListResponse>> Info(DeckInfoRequest request)
    {
        if (!TryGetViewerId(out long viewerId)) return Unauthorized();
        return await BuildDeckListResponseAsync(viewerId, AsFormat(request.DeckFormat));
    }

    [HttpPost("my_list")]
    public async Task<ActionResult<DeckListResponse>> MyList(DeckFormatRequest request)
    {
        if (!TryGetViewerId(out long viewerId)) return Unauthorized();
        return await BuildDeckListResponseAsync(viewerId, AsFormat(request.DeckFormat));
    }

    /// <summary>
    /// Shared hydration for <c>/deck/info</c> and <c>/deck/my_list</c> — both endpoints return the
    /// same <see cref="DeckListResponse"/> DTO and the client's DeckInfoTask.Parse / DeckMyListTask.Parse
    /// are identical (both call <c>DeckGroupListData(jsonData, format)</c>).
    ///
    /// Wire shape swaps based on the request format. When the client asks for All-format
    /// (<c>deck_format=0</c>), prod emits per-format keys (<c>user_deck_rotation</c>, etc.);
    /// for a specific format request, prod emits a single <c>user_deck_list</c>. The client's
    /// <c>DeckListUtility.ParseDeckInfoResponceData</c> branches on these two shapes, so the
    /// controller mirrors it exactly.
    /// </summary>
    private async Task<DeckListResponse> BuildDeckListResponseAsync(long viewerId, Format requestFormat)
    {
        var defaultDecks = await _globalsRepository.GetDefaultDecks();

        // user_leader_skin_setting_list is PER-VIEWER (the wire `user_` prefix is honest, despite
        // the misleading docstring on DefaultLeaderSkinSetting). Source it from the viewer's
        // ViewerClassData rows, matching how /load/index's user_class_list reads them. The global
        // DefaultLeaderSkinSettings table is now used only as initial seed values for fresh
        // viewers (ViewerRepository.RegisterViewer); the per-class current skin is on
        // viewer.Classes[i].LeaderSkin and gets mutated by /leader_skin/update.
        var viewerClasses = await _dbContext.Viewers
            .Where(v => v.Id == viewerId)
            .SelectMany(v => v.Classes)
            .Select(c => new { c.Class.Id, LeaderSkinId = c.LeaderSkin.Id })
            .ToListAsync();

        var response = new DeckListResponse
        {
            DefaultDeckList = defaultDecks.ToDictionary(
                d => d.Id.ToString(),
                d => new DefaultDeck
                {
                    DeckNo = d.DeckNo,
                    ClassId = d.ClassId,
                    SleeveId = d.SleeveId,
                    LeaderSkinId = d.LeaderSkinId,
                    DeckName = d.DeckName,
                    CardIdArray = System.Text.Json.JsonSerializer.Deserialize<List<long>>(d.CardIdArray, JsonbReadOptions) ?? new(),
                    // TODO(deck-stub): wire from real per-deck state once user maintenance / availability tracking lands.
                    // Prod emits is_complete_deck=1, is_available_deck=1, maintenance_card_ids=[] for the 8 starter decks.
                    IsCompleteDeck = 1,
                    IsAvailableDeck = 1,
                    MaintenanceCardIds = new(),
                }),
            UserLeaderSkinSettingList = viewerClasses.ToDictionary(
                vc => vc.Id.ToString(),
                vc => new UserLeaderSkinSetting
                {
                    ClassId = vc.Id,
                    IsRandomLeaderSkin = 0,   // random-skin mode (per-class shuffle pool) not yet persisted
                    LeaderSkinId = vc.LeaderSkinId,
                }),
            MaintenanceCardList = new(), // sourced from same place as /load/index when wired
        };

        if (requestFormat == Format.All)
        {
            // Prod's All-format response emits these three per-format lists (each [] for fresh viewers).
            // The PreRotation / Crossover / Avatar siblings exist in client code but prod omits them
            // for our profile; we mirror that omission and leave the nullable DTO fields unset.
            var formats = new[] { Format.Rotation, Format.Unlimited, Format.MyRotation };
            var byFormat = await _deckRepository.GetDecksByFormats(viewerId, formats);
            response.UserDeckRotation = PadEmptySlots(byFormat[Format.Rotation].Select(d => new UserDeck(d)).ToList());
            response.UserDeckUnlimited = PadEmptySlots(byFormat[Format.Unlimited].Select(d => new UserDeck(d)).ToList());
            response.UserDeckMyRotation = PadEmptySlots(byFormat[Format.MyRotation].Select(d => new UserDeck(d)).ToList());
            // trial_deck_list is prod-emitted on /deck/info (All format) but omitted on /deck/my_list
            // (specific format). Empty array in the 2026-05-23 prod capture.
            response.TrialDeckList = new();
        }
        else
        {
            var decks = await _deckRepository.GetDecks(viewerId, requestFormat);
            response.UserDeckList = PadEmptySlots(decks.Select(d => new UserDeck(d)).ToList());
        }

        return response;
    }

    [HttpPost("get_empty_deck_number")]
    public async Task<ActionResult<EmptyDeckNumberResponse>> GetEmptyDeckNumber(DeckFormatRequest request)
    {
        if (!TryGetViewerId(out long viewerId)) return Unauthorized();
        return new EmptyDeckNumberResponse
        {
            EmptyDeckNum = await _deckRepository.GetEmptyDeckNumber(viewerId, AsFormat(request.DeckFormat))
        };
    }

    [HttpPost("update")]
    public async Task<ActionResult<DeckUpdateResponse>> Update(DeckUpdateRequest request)
    {
        if (!TryGetViewerId(out long viewerId)) return Unauthorized();
        var format = AsFormat(request.DeckFormat);

        if (request.IsDelete == 1)
        {
            await _deckRepository.DeleteDecks(viewerId, format, new[] { request.DeckNo });
        }
        else
        {
            var cls = await _dbContext.Classes.FindAsync(request.ClassId);
            var sleeve = await _dbContext.Sleeves.FindAsync((int)request.SleeveId);
            var skin = await _dbContext.LeaderSkins.FindAsync(request.LeaderSkinId);
            var cards = await ResolveDeckCards(request.CardIdArray);

            await _deckRepository.UpsertDeck(viewerId, format, request.DeckNo, deck =>
            {
                deck.Name = request.DeckName ?? string.Empty;
                if (cls is not null) deck.Class = cls;
                if (sleeve is not null) deck.Sleeve = sleeve;
                if (skin is not null) deck.LeaderSkin = skin;
                deck.RandomLeaderSkin = request.IsRandomLeaderSkin;
                deck.Cards = cards;
                // Clear stale rotation_id if the deck moved to a non-MyRotation format;
                // otherwise persist the chosen period so it survives the next /load/index.
                deck.MyRotationId = format == Format.MyRotation ? request.RotationId : null;
            });
        }

        var decks = await _deckRepository.GetDecks(viewerId, format);
        return new DeckUpdateResponse
        {
            UserDeckList = PadEmptySlots(decks.Select(d => new UserDeck(d)).ToList())
        };
    }

    [HttpPost("update_name")]
    public async Task<ActionResult<SingleDeckResponse>> UpdateName(DeckUpdateNameRequest request)
    {
        if (!TryGetViewerId(out long viewerId)) return Unauthorized();
        var deck = await _deckRepository.UpsertDeck(viewerId, AsFormat(request.DeckFormat), request.DeckNo,
            d => d.Name = request.DeckName ?? string.Empty);
        return new SingleDeckResponse { UserDeck = new UserDeck(deck) };
    }

    [HttpPost("update_sleeve")]
    public async Task<ActionResult<SingleDeckResponse>> UpdateSleeve(DeckUpdateSleeveRequest request)
    {
        if (!TryGetViewerId(out long viewerId)) return Unauthorized();
        var sleeve = await _dbContext.Sleeves.FindAsync((int)request.SleeveId);
        if (sleeve is null) return BadRequest($"Unknown sleeve {request.SleeveId}");

        var deck = await _deckRepository.UpsertDeck(viewerId, AsFormat(request.DeckFormat), request.DeckNo,
            d => d.Sleeve = sleeve);
        return new SingleDeckResponse { UserDeck = new UserDeck(deck) };
    }

    [HttpPost("update_leader_skin")]
    public async Task<ActionResult<SingleDeckResponse>> UpdateLeaderSkin(DeckUpdateLeaderSkinRequest request)
    {
        if (!TryGetViewerId(out long viewerId)) return Unauthorized();
        var skin = await _dbContext.LeaderSkins.FindAsync(request.LeaderSkinId);
        if (skin is null) return BadRequest($"Unknown leader skin {request.LeaderSkinId}");

        var deck = await _deckRepository.UpsertDeck(viewerId, AsFormat(request.DeckFormat), request.DeckNo,
            d =>
            {
                d.LeaderSkin = skin;
                d.RandomLeaderSkin = false;
            });
        return new SingleDeckResponse { UserDeck = new UserDeck(deck) };
    }

    // TODO: schema doesn't yet model the random-leader-skin pool — we just pick one and persist
    // that. Add a join table (DeckLeaderSkinPool) when ranked play / random skins become a real
    // feature. For now the UI flow still works (server returns a single chosen skin per spec).
    [HttpPost("update_random_leader_skin")]
    public async Task<ActionResult<SingleDeckResponse>> UpdateRandomLeaderSkin(DeckUpdateRandomLeaderSkinRequest request)
    {
        if (!TryGetViewerId(out long viewerId)) return Unauthorized();
        var pool = request.LeaderSkinIdList ?? new List<int>();
        if (pool.Count == 0) return BadRequest("leader_skin_id_list must contain at least one id");

        int chosenId = pool[Random.Shared.Next(pool.Count)];
        var skin = await _dbContext.LeaderSkins.FindAsync(chosenId);
        if (skin is null) return BadRequest($"Unknown leader skin {chosenId}");

        var deck = await _deckRepository.UpsertDeck(viewerId, AsFormat(request.DeckFormat), request.DeckNo,
            d =>
            {
                d.LeaderSkin = skin;
                d.RandomLeaderSkin = true;
            });
        return new SingleDeckResponse { UserDeck = new UserDeck(deck) };
    }

    [HttpPost("update_order")]
    public async Task<ActionResult<EmptyResponse>> UpdateOrder(DeckOrderRequest request)
    {
        if (!TryGetViewerId(out long viewerId)) return Unauthorized();
        // Deck `Number` IS the slot order — the client sends the same slot numbers in a new
        // sequence. Today we don't model "display order" separately from "slot number", so
        // reordering is a no-op server-side. When a separate Order column lands, persist here.
        return new EmptyResponse();
    }

    [HttpPost("delete_deck_list")]
    public async Task<ActionResult<EmptyResponse>> DeleteDeckList(DeckDeleteListRequest request)
    {
        if (!TryGetViewerId(out long viewerId)) return Unauthorized();
        var nos = request.DeckNoList ?? new List<int>();
        if (nos.Count > 0) await _deckRepository.DeleteDecks(viewerId, AsFormat(request.DeckFormat), nos);
        return new EmptyResponse();
    }

    // /deck/set_deck_redis — server side is a Redis-cached "active deck per class" hint for
    // matchmaking. We don't model matchmaking yet; acknowledge the call and move on (real
    // server may not persist this either; the `_redis` suffix suggests cache-only).
    [HttpPost("set_deck_redis")]
    public Task<ActionResult<EmptyResponse>> SetDeckRedis(SetDeckRedisRequest request)
    {
        if (!TryGetViewerId(out long _)) return Task.FromResult<ActionResult<EmptyResponse>>(Unauthorized());
        return Task.FromResult<ActionResult<EmptyResponse>>(new EmptyResponse());
    }

    /// <summary>
    /// Convert a flat `card_id_array` (cards repeated for count) into a grouped DeckCard list.
    /// Cards not in the DB are silently dropped — until CardImport lands the result is always
    /// empty, which is acceptable for the deck-editing flow (UI saves what it can).
    /// </summary>
    private async Task<List<DeckCard>> ResolveDeckCards(List<long>? cardIdArray)
    {
        if (cardIdArray is null || cardIdArray.Count == 0) return new List<DeckCard>();

        var grouped = cardIdArray.GroupBy(id => id).Select(g => new { Id = g.Key, Count = g.Count() }).ToList();
        var ids = grouped.Select(g => g.Id).ToList();
        var cards = await _dbContext.Cards.Where(c => ids.Contains(c.Id)).ToDictionaryAsync(c => c.Id);

        return grouped
            .Where(g => cards.ContainsKey(g.Id))
            .Select(g => new DeckCard { Card = cards[g.Id], Count = g.Count })
            .ToList();
    }
}
