using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SVSim.Database;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.Database.Repositories.BuildDeck;
using SVSim.Database.Services;
using SVSim.EmulatedEntrypoint.Models.Dtos;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.BuildDeck;
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.BuildDeck;

namespace SVSim.EmulatedEntrypoint.Controllers;

/// <summary>
/// /build_deck/* — the in-game "Structure Deck" prebuilt-deck shop. Catalog +
/// purchase + per-product purchase counter refresh. See
/// docs/superpowers/specs/2026-05-26-prebuilt-decks-design.md.
/// </summary>
[Route("build_deck")]
public class BuildDeckController : SVSimController
{
    private readonly IBuildDeckRepository _repo;
    private readonly SVSimDbContext _db;
    private readonly RewardGrantService _rewards;

    public BuildDeckController(
        IBuildDeckRepository repo,
        SVSimDbContext db,
        RewardGrantService rewards)
    {
        _repo = repo;
        _db = db;
        _rewards = rewards;
    }

    /// <summary>
    /// Loads the viewer with the full cosmetic / inventory graph + BuildDeckPurchases. This is
    /// the single load /build_deck/buy makes; every subsequent mutation operates on the returned
    /// instance and the controller saves once at the end.
    /// </summary>
    private Task<Viewer> LoadViewerGraphAsync(long viewerId) => _db.Viewers
        .Include(v => v.Cards).ThenInclude(c => c.Card)
        .Include(v => v.LeaderSkins)
        .Include(v => v.Sleeves)
        .Include(v => v.Emblems)
        .Include(v => v.Degrees)
        .Include(v => v.MyPageBackgrounds)
        .Include(v => v.Items).ThenInclude(i => i.Item)
        .Include(v => v.BuildDeckPurchases)
        .AsSplitQuery()
        .FirstAsync(v => v.Id == viewerId);

    // The wire shape for /build_deck/info has `data` as a bare collection of series, not a
    // DTO with a `series_list` field. The client (BuildDeckPurchaseInfoTask.Parse) iterates
    // `data` directly via numeric indexer:
    //   for (int i = 0; i < data.Count; i++) data[i]["series_id"].ToInt();
    // So `data` must be either an array OR an object whose values are series. Wrapping in
    // `{series_list: [...]}` breaks the iteration: `data.Count` is 1 and `data[0]` is the
    // inner array, so `data[0]["series_id"]` throws "Instance of JsonData is not a dictionary".
    // We return a bare array — simpler than the dict-keyed-by-order_id shape prod emits, and
    // LitJson's numeric indexer iterates both shapes identically.
    [HttpPost("info")]
    public async Task<ActionResult<List<BuildDeckSeriesDto>>> Info(BuildDeckInfoRequest request)
    {
        if (!TryGetViewerId(out long viewerId)) return Unauthorized();

        var series = await _repo.GetEnabledCatalog(request.AddSeriesId);
        var purchases = await _repo.GetPurchasesForViewer(viewerId);

        return series.Select(s => ToSeriesDto(s, purchases)).ToList();
    }

    private static BuildDeckSeriesDto ToSeriesDto(
        BuildDeckSeriesEntry s,
        IReadOnlyDictionary<int, ViewerBuildDeckProductPurchase> purchases)
    {
        int totalSeriesPurchases = s.Products
            .Sum(p => purchases.TryGetValue(p.Id, out var v) ? v.PurchaseCount : 0);

        return new BuildDeckSeriesDto
        {
            SeriesId = s.Id,
            OrderId = s.OrderIndex,
            IsNew = s.IsNew,
            Products = s.Products
                .OrderBy(p => p.Id)
                .Select(p => ToProductDto(p, purchases))
                .ToList(),
            SeriesRewards = GroupSeriesRewards(s.SeriesRewards, totalSeriesPurchases),
        };
    }

    private static BuildDeckProductDto ToProductDto(
        BuildDeckProductEntry p,
        IReadOnlyDictionary<int, ViewerBuildDeckProductPurchase> purchases)
    {
        int current = purchases.TryGetValue(p.Id, out var v) ? v.PurchaseCount : 0;
        bool isFirstPrice = current == 0;
        int? priceCrystal = SelectPrice(isFirstPrice, p.IntroPriceCrystal, p.RegularPriceCrystal);
        int? priceRupy    = SelectPrice(isFirstPrice, p.IntroPriceRupy,    p.RegularPriceRupy);

        return new BuildDeckProductDto
        {
            ProductId = p.Id,
            ProductName = p.ProductNameKey,
            LeaderId = p.LeaderId,
            DeckCode = p.DeckCode,
            FeaturedCardId = p.FeaturedCardId,
            PurchaseNumMax = p.PurchaseNumMax,
            PurchaseNumCurrent = current,
            IsFirstPrice = isFirstPrice,
            PriceCrystal = priceCrystal,
            PriceRupy = priceRupy,
            Rewards = p.Rewards
                .OrderBy(r => r.RewardIndex)
                .Select(r => new BuildDeckProductRewardDto
                {
                    RewardType = r.RewardType,
                    RewardDetailId = r.RewardDetailId,
                    RewardNumber = r.RewardNumber,
                    MessageId = r.MessageId,
                }).ToList(),
        };
    }

    private static int? SelectPrice(bool isFirstPrice, int? intro, int? regular)
    {
        if (isFirstPrice) return intro ?? regular;   // fall back when only one tier known
        return regular ?? intro;
    }

    private static List<BuildDeckSeriesRewardTierDto> GroupSeriesRewards(
        IReadOnlyList<BuildDeckSeriesRewardEntry> rows,
        int totalSeriesPurchases)
    {
        return rows
            .GroupBy(r => r.TierIndex)
            .OrderBy(g => g.Key)
            .Select(g => new BuildDeckSeriesRewardTierDto
            {
                IsGet = totalSeriesPurchases >= g.Key,
                RewardList = g.OrderBy(r => r.ItemIndex).Select(r => new BuildDeckProductRewardDto
                {
                    RewardType = r.RewardType,
                    RewardDetailId = r.RewardDetailId,
                    RewardNumber = r.RewardNumber,
                    MessageId = r.MessageId,
                }).ToList(),
            }).ToList();
    }

    [HttpPost("buy")]
    public async Task<ActionResult<BuildDeckBuyResponse>> Buy(BuildDeckBuyRequest request)
    {
        if (!TryGetViewerId(out long viewerId)) return Unauthorized();

        var product = await _repo.GetProduct(request.ProductId);
        if (product is null) return NotFound(new { error = "unknown_product" });

        if (!product.IsEnabled || product.Series is not { IsEnabled: true })
            return BadRequest(new { error = "product_not_available" });

        if (request.SalesType is 3)
            return StatusCode(StatusCodes.Status501NotImplemented, new { error = "ticket_currency_path_not_implemented" });
        if (request.SalesType is < 0 or > 3)
            return BadRequest(new { error = "invalid_sales_type" });

        var purchases = await _repo.GetPurchasesForViewer(viewerId);
        int currentCount = purchases.TryGetValue(product.Id, out var pp) ? pp.PurchaseCount : 0;
        if (currentCount >= product.PurchaseNumMax)
            return BadRequest(new { error = "purchase_limit_reached" });

        bool isFirstPrice = currentCount == 0;
        int? priceCrystal = SelectPrice(isFirstPrice, product.IntroPriceCrystal, product.RegularPriceCrystal);
        int? priceRupy    = SelectPrice(isFirstPrice, product.IntroPriceRupy,    product.RegularPriceRupy);

        // Currency validation
        switch (request.SalesType)
        {
            case 0: // free
                if (!(product.IntroPriceCrystal == 0 && product.IntroPriceRupy == 0))
                    return BadRequest(new { error = "price_not_available_for_currency" });
                break;
            case 1: // crystal
                if (priceCrystal is null)
                    return BadRequest(new { error = "price_not_available_for_currency" });
                break;
            case 2: // rupy
                if (priceRupy is null)
                    return BadRequest(new { error = "price_not_available_for_currency" });
                break;
        }

        // Single viewer load with the full graph — every subsequent mutation (currency debit,
        // purchase counter, card grants, cosmetic grants) operates on this one in-memory instance
        // so we can save once at the end.
        var viewer = await LoadViewerGraphAsync(viewerId);
        var rewardList = new List<RewardListEntry>();

        // Debit + post-state currency entry
        if (request.SalesType == 1)
        {
            ulong cost = (ulong)priceCrystal!.Value;
            if (viewer.Currency.Crystals < cost)
                return BadRequest(new { error = "insufficient_crystals" });
            viewer.Currency.Crystals -= cost;
            rewardList.Add(new RewardListEntry { RewardType = 2, RewardId = 0, RewardNum = (int)viewer.Currency.Crystals });
        }
        else if (request.SalesType == 2)
        {
            ulong cost = (ulong)priceRupy!.Value;
            if (viewer.Currency.Rupees < cost)
                return BadRequest(new { error = "insufficient_rupees" });
            viewer.Currency.Rupees -= cost;
            rewardList.Add(new RewardListEntry { RewardType = 9, RewardId = 0, RewardNum = (int)viewer.Currency.Rupees });
        }
        // sales_type == 0 (free): no debit, no currency entry

        // Compute series purchase total BEFORE this buy
        int prevSeriesCount = product.Series!.Products
            .Sum(p => purchases.TryGetValue(p.Id, out var v) ? v.PurchaseCount : 0);
        int newSeriesCount = prevSeriesCount + 1;

        // Increment purchase counter directly on the tracked viewer (we already loaded
        // BuildDeckPurchases via LoadViewerGraphAsync). The repo's IncrementPurchaseCount would
        // re-attach to the same instance and trigger an extra save — inlining keeps the
        // controller's single-save model intact.
        var purchaseRow = viewer.BuildDeckPurchases.FirstOrDefault(p => p.ProductId == product.Id);
        if (purchaseRow is null)
            viewer.BuildDeckPurchases.Add(new ViewerBuildDeckProductPurchase { ProductId = product.Id, PurchaseCount = 1 });
        else
            purchaseRow.PurchaseCount += 1;

        // Grant the 40 deck cards. Bucket by CardId so duplicate (CardId, IsSpot) rows don't
        // emit redundant reward_list entries — ApplyAsync(Card, ...) runs the cosmetic cascade
        // and returns a post-state-total entry per call.
        var deckGrants = product.Cards
            .GroupBy(c => c.CardId)
            .Select(g => (UserGoodsType.Card, g.Key, g.Sum(c => c.Number)));
        await ApplyRewardsAsync(viewer, deckGrants, rewardList);

        // Per-buy rewards from the product catalog: sleeve, emblem, skin, sometimes extra cards
        // (Set 4 grants 3 copies of the featured card as a type=5 reward).
        await ApplyRewardsAsync(viewer, product.Rewards
            .OrderBy(r => r.RewardIndex)
            .Select(r => ((UserGoodsType)r.RewardType, r.RewardDetailId, r.RewardNumber)),
            rewardList);

        // Series-reward tier crossings: tiers where prevSeriesCount < TierIndex <= newSeriesCount.
        // Captured tiers include type 4 (Item), 5 (Card), 6 (Sleeve), 7 (Emblem) — granting them
        // all uniformly avoids the earlier card-only path that dropped non-card tier rewards.
        var crossedTiers = product.Series.SeriesRewards
            .Where(r => r.TierIndex > prevSeriesCount && r.TierIndex <= newSeriesCount)
            .GroupBy(r => r.TierIndex)
            .OrderBy(g => g.Key)
            .ToList();

        var seriesRewards = new List<BuildDeckProductRewardDto>();
        foreach (var tier in crossedTiers)
        {
            await ApplyRewardsAsync(viewer, tier
                .OrderBy(r => r.ItemIndex)
                .Select(r => ((UserGoodsType)r.RewardType, r.RewardDetailId, r.RewardNumber)),
                rewardList);

            foreach (var item in tier.OrderBy(r => r.ItemIndex))
            {
                seriesRewards.Add(new BuildDeckProductRewardDto
                {
                    RewardType = item.RewardType,
                    RewardDetailId = item.RewardDetailId,
                    RewardNumber = item.RewardNumber,
                    MessageId = item.MessageId,
                });
            }
        }

        await _db.SaveChangesAsync();

        return new BuildDeckBuyResponse
        {
            RewardList = rewardList,
            SeriesRewards = seriesRewards,
        };
    }

    /// <summary>
    /// Dispatches each (type, id, num) tuple through <see cref="RewardGrantService.ApplyAsync"/>
    /// and appends the resulting wire entries to <paramref name="rewardList"/>. Caller saves.
    /// </summary>
    private async Task ApplyRewardsAsync(
        Viewer viewer,
        IEnumerable<(UserGoodsType Type, long DetailId, int Number)> rewards,
        List<RewardListEntry> rewardList)
    {
        foreach (var (type, detailId, number) in rewards)
        {
            var granted = await _rewards.ApplyAsync(viewer, type, detailId, number);
            foreach (var g in granted)
            {
                rewardList.Add(new RewardListEntry
                {
                    RewardType = g.RewardType,
                    RewardId = g.RewardId,
                    RewardNum = g.RewardNum,
                });
            }
        }
    }

    [HttpPost("get_purchase_count")]
    public async Task<ActionResult<BuildDeckGetPurchaseCountResponse>> GetPurchaseCount(
        BuildDeckGetPurchaseCountRequest request)
    {
        if (!TryGetViewerId(out long viewerId)) return Unauthorized();

        var product = await _repo.GetProduct(request.ProductId);
        if (product is null) return NotFound(new { error = "unknown_product" });

        var purchases = await _repo.GetPurchasesForViewer(viewerId);
        int current = purchases.TryGetValue(request.ProductId, out var p) ? p.PurchaseCount : 0;

        return new BuildDeckGetPurchaseCountResponse
        {
            PurchaseNumCurrent = current,
            PurchaseNumMax = product.PurchaseNumMax,
        };
    }
}
