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

namespace SVSim.EmulatedEntrypoint.Controllers;

/// <summary>
/// Tutorial-scoped gift endpoints. We do NOT implement a generic gift system here —
/// only the /tutorial/gift_top and /tutorial/gift_receive aliases needed for the
/// step 31 → 41 reward flow. A full gift inbox is future work; if/when needed,
/// add /gift/top and /gift/receive_gift aliases to this controller.
/// </summary>
public class GiftController : SVSimController
{
    /// <summary>The hardcoded tutorial gift bundle every fresh viewer sees at step 31.</summary>
    public static readonly IReadOnlyList<PresentDto> TutorialGifts = new[]
    {
        new PresentDto { PresentId = "71478626", RewardType = "1", RewardDetailId = "0",     RewardCount = "400", Message = "For completing the tutorial" },
        new PresentDto { PresentId = "71478627", RewardType = "9", RewardDetailId = "0",     RewardCount = "100", Message = "For completing the tutorial" },
        new PresentDto { PresentId = "71478628", RewardType = "4", RewardDetailId = "1",     RewardCount = "3",   Message = "For completing the tutorial", ItemType = 1 },
        new PresentDto { PresentId = "71478629", RewardType = "4", RewardDetailId = "80001", RewardCount = "40",  Message = "For completing the tutorial", ItemType = 2 },
        new PresentDto { PresentId = "71478630", RewardType = "4", RewardDetailId = "90001", RewardCount = "1",   Message = "For completing the tutorial", ItemType = 2 },
    };

    private readonly SVSimDbContext _db;
    private readonly RewardGrantService _rewards;

    public GiftController(SVSimDbContext db, RewardGrantService rewards)
    {
        _db = db;
        _rewards = rewards;
    }

    [HttpPost("/tutorial/gift_top")]
    public async Task<ActionResult<GiftTopResponse>> TutorialGiftTop([FromBody] GiftTopRequest request)
    {
        if (!TryGetViewerId(out long viewerId)) return Unauthorized();

        var claimedList = await _db.ViewerClaimedTutorialGifts
            .Where(g => g.ViewerId == viewerId)
            .Select(g => g.PresentId)
            .ToListAsync();
        var claimed = new HashSet<string>(claimedList);

        var nowString = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss");
        var presents = TutorialGifts
            .Where(p => !claimed.Contains(p.PresentId))
            .Select(p => Clone(p, nowString))
            .ToList();
        var history = TutorialGifts
            .Where(p => claimed.Contains(p.PresentId))
            .Select(p => Clone(p, nowString))
            .ToList();

        return new GiftTopResponse
        {
            PresentList = presents,
            PresentHistoryList = history,
            LimitOverPresentList = new(),
        };
    }

    [HttpPost("/tutorial/gift_receive")]
    public async Task<ActionResult<GiftReceiveResponse>> TutorialGiftReceive([FromBody] GiftReceiveRequest request)
    {
        if (!TryGetViewerId(out long viewerId)) return Unauthorized();

        var requestedIds = request.PresentIdArray.ToHashSet();

        // Load viewer with the collections RewardGrantService mutates. Crystals/Rupees live on
        // viewer.Currency (owned, auto-loads); Items live on viewer.Items (owned collection).
        // MissionData is an owned type and auto-loads, but Include is listed explicitly to match
        // the pattern in TutorialController.Update and to make the intent clear.
        // AsSplitQuery is the default-safe pattern when including viewer collections
        // (project memory: project_ef_split_query).
        //
        // ThenInclude(i => i.Item) is load-bearing: OwnedItemEntry.Item is a separate non-owned
        // entity whose default initialiser is `new ItemEntry()` (Id=0). Without the explicit
        // ThenInclude, RewardGrantService.ApplyAsync's `FirstOrDefault(i => i.Item.Id == ...)`
        // never matches a pre-existing row → falls through to add a duplicate → (ViewerId, ItemId)
        // unique index throws on SaveChanges (project_ef_nav_include_pitfall).
        var viewer = await _db.Viewers
            .Include(v => v.Items).ThenInclude(i => i.Item)
            .Include(v => v.MissionData)
            .AsSplitQuery()
            .FirstAsync(v => v.Id == viewerId);

        // Resolve which of the requested ids are still claimable for this viewer.
        var alreadyClaimedList = await _db.ViewerClaimedTutorialGifts
            .Where(g => g.ViewerId == viewerId && requestedIds.Contains(g.PresentId))
            .Select(g => g.PresentId)
            .ToListAsync();
        var alreadyClaimed = new HashSet<string>(alreadyClaimedList);

        var toClaim = TutorialGifts
            .Where(p => requestedIds.Contains(p.PresentId) && !alreadyClaimed.Contains(p.PresentId))
            .ToList();

        // Apply grants via the canonical primitive. Pass the loaded viewer, NOT viewerId.
        foreach (var p in toClaim)
        {
            var goodsType = WireRewardTypeToUserGoodsType(int.Parse(p.RewardType));
            await _rewards.ApplyAsync(viewer, goodsType, long.Parse(p.RewardDetailId), int.Parse(p.RewardCount));
        }

        // Advance tutorial state from 31 → 41 as a side-effect of this claim (no separate
        // /tutorial/update → 41 call appears in the prod capture). Preserve max — don't downgrade
        // viewers who are already past step 41.
        const int GiftReceiveTutorialStep = 41;
        if (viewer.MissionData.TutorialState < GiftReceiveTutorialStep)
        {
            viewer.MissionData.TutorialState = GiftReceiveTutorialStep;
        }

        // Persist claim receipts in the same transaction.
        var now = DateTime.UtcNow;
        foreach (var p in toClaim)
        {
            _db.ViewerClaimedTutorialGifts.Add(new SVSim.Database.Models.ViewerClaimedTutorialGift
            {
                ViewerId = viewerId,
                PresentId = p.PresentId,
                ClaimedAt = now,
            });
        }
        await _db.SaveChangesAsync();

        var nowString = now.ToString("yyyy-MM-dd HH:mm:ss");
        var allClaimedList = await _db.ViewerClaimedTutorialGifts
            .Where(g => g.ViewerId == viewerId)
            .Select(g => g.PresentId)
            .ToListAsync();
        var allClaimed = new HashSet<string>(allClaimedList);

        // Derive presentList/historyList up front so IsUnreceivedPresent can read the count
        // without re-filtering. unclaimedPresents are the gifts still on offer after this call;
        // claimedPresents are everything the viewer has ever received (this call + prior calls).
        var unclaimedPresents = TutorialGifts
            .Where(p => !allClaimed.Contains(p.PresentId))
            .Select(p => Clone(p, nowString))
            .ToList();
        var claimedPresents = TutorialGifts
            .Where(p => allClaimed.Contains(p.PresentId))
            .Select(p => Clone(p, nowString))
            .ToList();

        return new GiftReceiveResponse
        {
            CardList = new(),
            // Echo only the ids actually granted by THIS call. Building this from `requestedIds`
            // would falsely confirm a re-grant on idempotent retries: the client would re-show
            // the "received N gifts" popup and direct-assign the same post-state totals it already
            // applied, double-toasting the user. Sort ascending to match the prod-capture order.
            ReceivedIds = toClaim
                .Select(p => p.PresentId)
                .OrderBy(x => x)
                .ToList(),
            // Same idempotency contract: only the gifts granted in THIS call belong in the
            // per-reward summary list. The client uses this to drive the +N popups.
            TotalReceiveCountList = toClaim
                .Select(p => new TotalReceiveCountDto
                {
                    RewardType = int.Parse(p.RewardType),
                    RewardDetailId = long.Parse(p.RewardDetailId),
                    RewardCount = long.Parse(p.RewardCount),
                    ItemType = p.ItemType ?? 0,
                    IsUsable = true,
                }).ToList(),
            PresentList = unclaimedPresents,
            PresentHistoryList = claimedPresents,
            // True when there are still unclaimed gifts on offer — drives the inbox badge state.
            // Hardcoding false hid the badge after partial claims even though present_list still
            // carried unclaimed entries.
            IsUnreceivedPresent = unclaimedPresents.Count > 0,
            // reward_list entries must carry POST-STATE TOTALS, not gift deltas.
            // The client's PlayerStaticData.UpdateHaveUserGoodsNumByJsonData does direct
            // assignment on each entry's reward_num — emitting the delta would clobber
            // the client-side cached balance down to the gift amount until the next /load/index.
            // See project memory: project_wire_reward_list_post_state.
            //
            // Iterate `toClaim` so idempotent re-receive doesn't re-emit post-state entries
            // the client would direct-assign again (no-op on currency, but redundant traffic
            // and risk of misinterpretation on item counts).
            RewardList = toClaim
                .Select(p => new GiftRewardListEntry
                {
                    RewardType = p.RewardType,
                    RewardId = p.RewardDetailId,
                    RewardNum = ResolvePostStateRewardNum(p, viewer),
                })
                .ToList(),
            // Echo the persisted state, not a hardcoded 41. The state may already be past 41
            // for replay/edge-case calls (the Math.Max-preserve block above keeps it stable);
            // emitting 41 anyway would surface a regressed step to the client and desync the
            // tutorial-state machine.
            TutorialStep = viewer.MissionData.TutorialState,
        };
    }

    /// <summary>
    /// Returns the post-grant viewer balance for the given gift entry, not the gift delta.
    /// reward_list on wire carries post-state totals (client does direct assignment).
    /// </summary>
    private static string ResolvePostStateRewardNum(PresentDto gift, SVSim.Database.Models.Viewer viewer)
    {
        switch (gift.RewardType)
        {
            case "1":  // Crystal
                return ((long)viewer.Currency.Crystals).ToString(System.Globalization.CultureInfo.InvariantCulture);
            case "9":  // Rupy
                return ((long)viewer.Currency.Rupees).ToString(System.Globalization.CultureInfo.InvariantCulture);
            case "4":  // Item
            {
                int itemId = int.Parse(gift.RewardDetailId, System.Globalization.CultureInfo.InvariantCulture);
                var owned = viewer.Items.FirstOrDefault(i => i.Item.Id == itemId);
                return ((long)(owned?.Count ?? 0)).ToString(System.Globalization.CultureInfo.InvariantCulture);
            }
            default:
                return gift.RewardCount;  // unknown type — fall back to gift count (better than 0)
        }
    }

    private static UserGoodsType WireRewardTypeToUserGoodsType(int wireType) => wireType switch
    {
        1 => UserGoodsType.Crystal,
        4 => UserGoodsType.Item,
        9 => UserGoodsType.Rupy,
        _ => throw new InvalidOperationException($"Unmapped gift wire reward_type {wireType}"),
    };

    private static PresentDto Clone(PresentDto p, string createTime) => new()
    {
        PresentId = p.PresentId,
        RewardType = p.RewardType,
        RewardDetailId = p.RewardDetailId,
        RewardCount = p.RewardCount,
        ConditionNumber = p.ConditionNumber,
        PresentLimitType = p.PresentLimitType,
        RewardLimitTime = p.RewardLimitTime,
        CreateTime = createTime,
        ItemType = p.ItemType,
        Message = p.Message,
    };
}
