using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SVSim.Database.Enums;
using SVSim.Database.Models;

namespace SVSim.Database.Services;

/// <summary>
/// Wire-shape entry returned by <see cref="RewardGrantService.ApplyAsync"/>. Field names match
/// the <c>reward_list</c> entries used by <c>/pack/open</c>, <c>/basic_puzzle/finish</c>, and
/// <c>/story/*/finish</c>. reward_num is a POST-STATE TOTAL for currencies and a count for
/// collection grants — see <see cref="Models.RewardListEntry"/>.
/// </summary>
public sealed record GrantedReward(int RewardType, long RewardId, int RewardNum);

/// <summary>
/// Single canonical grant primitive for every <see cref="UserGoodsType"/> the server hands to a
/// viewer. Switch on the type, mutate the appropriate viewer collection / <see cref="ViewerCurrency"/>
/// field, return the wire-shape entries to embed in the response's <c>reward_list</c>.
///
/// <para>
/// <b>DO NOT reimplement reward dispatch in a controller or new helper.</b> This service handles
/// RedEther, Crystal, SpotCardPoint, Item, Card (with <see cref="CardCosmeticReward"/> cascade),
/// Sleeve, Emblem, Degree, Rupy, Skin, MyPageBG — everything except the dead-letter SpotCard /
/// SpotCardOnlyLatestCardPack slots (use Card=5 instead). Endpoint code that takes a
/// list of <c>(type, id, num)</c> tuples should iterate and call <see cref="ApplyAsync"/>
/// per tuple — never switch on type yourself, never filter to "only card-typed rewards", never
/// build a second dispatch table. Past duplicate implementations (ICardAcquisitionService in the
/// EmulatedEntrypoint project, the first pass at /build_deck/buy) all silently dropped subsets of
/// types and produced the same bug: wire reward visible but viewer's collection unchanged. When a
/// new reward type comes up, add a case here. See <c>feedback_reward_grant_service</c> memory.
/// </para>
///
/// Card grants additionally run the <see cref="CardCosmeticReward"/> cascade: any cosmetic
/// associated with the granted card that the viewer doesn't yet own is granted too, and produces
/// an additional entry in the returned list. That's why the return type is a list: most types
/// produce one entry, Card produces 1 + N.
///
/// Caller is responsible for <see cref="SVSimDbContext.SaveChangesAsync(System.Threading.CancellationToken)"/> —
/// this service only mutates the in-memory graph so a controller can stack several grants in
/// a single transaction.
/// </summary>
public sealed class RewardGrantService
{
    private readonly SVSimDbContext _db;
    private readonly ILogger<RewardGrantService> _log;

    public RewardGrantService(SVSimDbContext db, ILogger<RewardGrantService> log)
    {
        _db = db;
        _log = log;
    }

    public async Task<IReadOnlyList<GrantedReward>> ApplyAsync(
        Viewer viewer, UserGoodsType type, long detailId, int num, CancellationToken ct = default)
    {
        switch (type)
        {
            case UserGoodsType.Sleeve:
                AddCosmeticIfMissing(viewer.Sleeves, detailId, _db.Sleeves);
                return Single(type, detailId, 1);

            case UserGoodsType.Emblem:
                AddCosmeticIfMissing(viewer.Emblems, detailId, _db.Emblems);
                return Single(type, detailId, 1);

            case UserGoodsType.Skin: // LeaderSkin in our schema
                AddCosmeticIfMissing(viewer.LeaderSkins, detailId, _db.LeaderSkins);
                return Single(type, detailId, 1);

            case UserGoodsType.Degree:
                AddCosmeticIfMissing(viewer.Degrees, detailId, _db.Degrees);
                return Single(type, detailId, 1);

            case UserGoodsType.MyPageBG:
                AddCosmeticIfMissing(viewer.MyPageBackgrounds, detailId, _db.MyPageBackgrounds);
                return Single(type, detailId, 1);

            case UserGoodsType.Rupy:
                viewer.Currency.Rupees += (ulong)num;
                return Single(type, detailId, checked((int)viewer.Currency.Rupees));

            case UserGoodsType.Crystal:
                viewer.Currency.Crystals += (ulong)num;
                return Single(type, detailId, checked((int)viewer.Currency.Crystals));

            case UserGoodsType.RedEther:
                viewer.Currency.RedEther += (ulong)num;
                return Single(type, detailId, checked((int)viewer.Currency.RedEther));

            case UserGoodsType.SpotCardPoint:
                viewer.Currency.SpotPoints += (ulong)num;
                return Single(type, detailId, checked((int)viewer.Currency.SpotPoints));

            case UserGoodsType.Item:
            {
                var owned = viewer.Items.FirstOrDefault(i => i.Item.Id == (int)detailId);
                if (owned is null)
                {
                    var item = _db.Items.Find((int)detailId)
                               ?? throw new InvalidOperationException($"Item {detailId} not in catalog");
                    viewer.Items.Add(new OwnedItemEntry { Item = item, Count = num, Viewer = viewer });
                    return Single(type, detailId, num);
                }
                owned.Count += num;
                return Single(type, detailId, owned.Count);
            }

            case UserGoodsType.Card:
                return await ApplyCardAsync(viewer, detailId, num, ct);

            case UserGoodsType.SpotCard:
            case UserGoodsType.SpotCardOnlyLatestCardPack:
                // Spot-card-typed grants don't appear in captures — emitters always use Card=5
                // with the spot-card-specific id. These two enum slots remain unimplemented; if a
                // capture ever shows one in a reward_list we'll know to wire them up here.
                throw new NotSupportedException(
                    $"{type} rewards are not yet supported — emitters use Card=5 instead.");

            default:
                throw new NotSupportedException($"UserGoodsType {type} not yet handled by RewardGrantService");
        }
    }

    private async Task<IReadOnlyList<GrantedReward>> ApplyCardAsync(
        Viewer viewer, long cardId, int num, CancellationToken ct)
    {
        // Find-or-add OwnedCardEntry. Mirrors the primitive that used to live in
        // IPackRepository.GrantCardsToViewer — now inline so we own the in-memory-only contract.
        var owned = viewer.Cards.FirstOrDefault(c => c.Card.Id == cardId);
        int postCount;
        if (owned is null)
        {
            var card = await _db.Cards.FirstOrDefaultAsync(c => c.Id == cardId, ct)
                       ?? throw new InvalidOperationException($"Card {cardId} not in catalog");
            owned = new OwnedCardEntry { Card = card, Count = num, IsProtected = false };
            viewer.Cards.Add(owned);
            postCount = num;
        }
        else
        {
            owned.Count += num;
            postCount = owned.Count;
        }

        var results = new List<GrantedReward>
        {
            new((int)UserGoodsType.Card, cardId, postCount),
        };

        // Cascade: cosmetic mappings live on the non-foil row. If the granted card is foil
        // (card_id ends in 1, IsFoil=true), look up cascade against cardId - 1.
        long lookupId = owned.Card.IsFoil ? cardId - 1 : cardId;

        var cascade = await _db.CardCosmeticRewards
            .Where(r => r.CardId == lookupId)
            .ToListAsync(ct);

        foreach (var reward in cascade)
        {
            if (TryAddCascadeCosmetic(viewer, reward, lookupId))
            {
                // CosmeticType numeric values are identical to UserGoodsType — direct cast is safe.
                results.Add(new GrantedReward((int)reward.Type, reward.CosmeticId, 1));
            }
        }

        return results;
    }

    private static IReadOnlyList<GrantedReward> Single(UserGoodsType type, long id, int num)
        => new[] { new GrantedReward((int)type, id, num) };

    private bool TryAddCascadeCosmetic(Viewer viewer, CardCosmeticReward reward, long forCardId)
    {
        try
        {
            return reward.Type switch
            {
                CosmeticType.Sleeve   => AddCosmeticIfMissing(viewer.Sleeves,           reward.CosmeticId, _db.Sleeves),
                CosmeticType.Emblem   => AddCosmeticIfMissing(viewer.Emblems,           reward.CosmeticId, _db.Emblems),
                CosmeticType.Skin     => AddCosmeticIfMissing(viewer.LeaderSkins,       reward.CosmeticId, _db.LeaderSkins),
                CosmeticType.Degree   => AddCosmeticIfMissing(viewer.Degrees,           reward.CosmeticId, _db.Degrees),
                CosmeticType.MyPageBG => AddCosmeticIfMissing(viewer.MyPageBackgrounds, reward.CosmeticId, _db.MyPageBackgrounds),
                _ => false,
            };
        }
        catch (InvalidOperationException ex)
        {
            _log.LogWarning(ex,
                "Card cascade: cosmetic {Type} {Id} for card {CardId} skipped (master row missing)",
                reward.Type, reward.CosmeticId, forCardId);
            return false;
        }
    }

    private static bool AddCosmeticIfMissing<T>(List<T> collection, long detailId, DbSet<T> catalog) where T : class
    {
        bool alreadyOwned = collection.Any(e => GetId(e) == detailId);
        if (alreadyOwned) return false;

        var entity = catalog.Find(checked((int)detailId))
                     ?? throw new InvalidOperationException(
                         $"Cosmetic id {detailId} not in catalog for type {typeof(T).Name}");
        collection.Add(entity);
        return true;
    }

    /// <summary>
    /// Reflectively reads an entity's Id property — works for both <c>BaseEntity&lt;int&gt;</c>
    /// (cosmetics) and <c>BaseEntity&lt;long&gt;</c> (e.g. Viewer/Card) without forcing two
    /// non-generic overloads of <see cref="AddCosmeticIfMissing"/>.
    /// </summary>
    private static long GetId<T>(T e)
    {
        var prop = typeof(T).GetProperty("Id")
                   ?? throw new InvalidOperationException($"Type {typeof(T).Name} missing Id property");
        var val = prop.GetValue(e);
        return val switch { long l => l, int i => i, _ => 0 };
    }
}
