using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.Database.Models.Config;
using SVSim.Database.Services;

namespace SVSim.EmulatedEntrypoint.Services;

/// <summary>
/// Draws cards from a pack's pool using rates from <see cref="IGameConfigService"/>'s
/// <see cref="PackRateConfig"/>. Slot rarity selection is unified through one
/// <see cref="PickRarity"/> + <see cref="ResolveWeights"/> pair — what was previously a
/// hardcoded slot-1-7 vs slot-8 split now reads from <c>PackRateConfig.PerSlot</c>.
///
/// The "legendary-special slot-8 forced Legendary" rule stays in code (structural category
/// promise, not a tunable rate).
/// </summary>
public class PackOpenService
{
    private const int CardsPerPack = 8;

    private readonly PackRateConfig _rates;

    public PackOpenService(IGameConfigService config)
    {
        _rates = config.Get<PackRateConfig>();
    }

    public DrawResult Draw(
        PackConfigEntry pack,
        ICardPoolProvider pools,
        int packNumber,
        IReadOnlyCollection<long> excludeCardIds,
        IRandom rng)
    {
        var pool = pools.GetPool(pack);
        if (pool.Count == 0)
        {
            throw new InvalidOperationException(
                $"PackOpenService: pool for pack {pack.Id} (category {pack.PackCategory}) is empty.");
        }

        var poolByRarity = pool
            .Where(c => !excludeCardIds.Contains(c.Id))
            .GroupBy(c => c.Rarity)
            .ToDictionary(g => g.Key, g => g.ToList());

        bool isLegendarySpecial =
            pack.PackCategory == PackCategory.SpecialCardPack ||
            pack.PackCategory == PackCategory.LimitedSpecialCardPack;

        var slots = new List<DrawnCard>(packNumber * CardsPerPack);
        for (int p = 0; p < packNumber; p++)
        {
            for (int s = 0; s < CardsPerPack; s++)
            {
                int slotNum = s + 1;   // 1-based

                Rarity rarity;
                if (slotNum == CardsPerPack && isLegendarySpecial)
                {
                    // Structural category rule (not a tunable rate).
                    rarity = Rarity.Legendary;
                }
                else
                {
                    rarity = PickRarity(rng, ResolveWeights(slotNum));
                }

                var card = PickCardOfRarity(rarity, poolByRarity, rng);

                // Per-card, per-slot animated upgrade. Applies independently of rarity, slot
                // position, and pack category — including forced-Legendary slot-8 of specials.
                if (rng.NextDouble() < _rates.AnimatedRate)
                {
                    var foil = pools.TryGetFoilTwin(card.Id);
                    if (foil is not null) card = foil;   // silently keep base if no twin exists
                }

                slots.Add(new DrawnCard(card.Id, card.Rarity));
            }
        }
        return new DrawResult(slots);
    }

    /// <summary>
    /// Returns the rarity weights for the given 1-based slot. Looks for a per-slot override
    /// keyed by <c>Slot == slotNum.ToString()</c>; falls back to the global Default.
    ///
    /// NOTE: PerSlot is List&lt;SlotRarityWeights&gt; (not Dictionary) due to an EF Core 8
    /// jsonb-mapping limitation. Per-pack overrides would extend this resolver to check a
    /// per-pack collection first.
    /// </summary>
    private SlotRarityWeights ResolveWeights(int slotNum)
    {
        var slotKey = slotNum.ToString();
        var perSlot = _rates.PerSlot.FirstOrDefault(s => s.Slot == slotKey);
        return perSlot ?? _rates.Default;
    }

    private static Rarity PickRarity(IRandom rng, SlotRarityWeights w)
    {
        // Cumulative-band order: Legendary -> Gold -> Silver -> Bronze (catch-all).
        // - When weights sum to <1.0 (SV Classic Default = 0.9994), the slack absorbs into
        //   Bronze via the catch-all — preserves historic behavior.
        // - When weights sum to exactly 1.0 (SV Classic PerSlot[8] with Bronze=0), the catch-all
        //   never fires and Bronze=0 holds naturally.
        double r = rng.NextDouble();
        double cum = w.Legendary;        if (r < cum) return Rarity.Legendary;
        cum += w.Gold;                   if (r < cum) return Rarity.Gold;
        cum += w.Silver;                 if (r < cum) return Rarity.Silver;
        return Rarity.Bronze;
    }

    private static ShadowverseCardEntry PickCardOfRarity(
        Rarity rarity,
        Dictionary<Rarity, List<ShadowverseCardEntry>> poolByRarity,
        IRandom rng)
    {
        // Fallback if the rolled rarity has no cards: walk down (and up) through all rarities.
        // Order: rolled rarity first, then Legendary -> Gold -> Silver -> Bronze, deduped by
        // LINQ Distinct. This handles both "no Legendaries" (fall down) and sparse pools that
        // only contain a single rarity (fall up). Safety net for sparse master data.
        Rarity[] fallback = new[] { rarity, Rarity.Legendary, Rarity.Gold, Rarity.Silver, Rarity.Bronze }
            .Distinct().ToArray();
        foreach (var r in fallback)
        {
            if (poolByRarity.TryGetValue(r, out var list) && list.Count > 0)
            {
                return list[rng.Next(list.Count)];
            }
        }
        throw new InvalidOperationException("PackOpenService: pool empty after exclude filter.");
    }
}
