using Theriapolis.Core.Data;
using Theriapolis.Core.Rules.Character;
using Theriapolis.Core.Util;

namespace Theriapolis.Core.Items;

/// <summary>
/// Phase 7 M2 — central dispatch for "the player consumed item X". The
/// inventory UI's "Use" button routes here; quest effects and dialogue
/// effects that consume items also route here. Centralising the dispatch
/// keeps the per-kind handlers (healing potion, scent mask, …) testable
/// in one place and makes the Hybrid Medical Incompatibility scaling
/// (Phase 6.5 M4 carryover) apply uniformly across all heal sources.
///
/// Returns a <see cref="ConsumeResult"/> describing what happened so the
/// UI can format a message and the inventory caller can decrement /
/// remove the consumed instance.
///
/// Adding a new consumable kind: add the value to
/// <see cref="ItemDef.ConsumableKind"/> in JSON, then add a
/// <c>case</c> branch below. <see cref="ConsumeResult"/> carries the
/// outcome a caller cares about.
/// </summary>
public static class ConsumableHandler
{
    /// <summary>
    /// Consume <paramref name="item"/> for <paramref name="pc"/>, deriving
    /// any randomness from <paramref name="seed"/> (e.g. healing-dice rolls).
    /// The caller is responsible for actually removing the item from the
    /// inventory once a non-rejected result is returned.
    ///
    /// <paramref name="seed"/> should be deterministic per usage —
    /// callers typically derive it from
    /// <c>worldSeed ^ characterCreationMs ^ usageIndex</c> or similar so
    /// save / load round-trips reproduce the roll.
    /// </summary>
    public static ConsumeResult Consume(ItemDef item, Character pc, ulong seed)
    {
        if (item is null) throw new System.ArgumentNullException(nameof(item));
        if (pc   is null) throw new System.ArgumentNullException(nameof(pc));
        if (item.Kind != "consumable")
            return ConsumeResult.Rejected($"'{item.Id}' is not a consumable (kind='{item.Kind}').");

        return item.ConsumableKind switch
        {
            "healing"     => ConsumeHealingPotion(item, pc, seed),
            "scent_mask"  => ConsumeScentMask(item, pc),
            _             => ConsumeResult.Unrecognized(item.Id),
        };
    }

    private static ConsumeResult ConsumeHealingPotion(ItemDef item, Character pc, ulong seed)
    {
        // Healing dice — items.json field "healing" = e.g. "2d4+2".
        if (string.IsNullOrEmpty(item.Healing))
            return ConsumeResult.Rejected($"healing potion '{item.Id}' has no healing dice expression.");

        var roll = Rules.Combat.DamageRoll.Parse(item.Healing, Rules.Stats.DamageType.Bludgeoning);
        var rng  = new SeededRng(seed);
        // Average each die roll independently — same shape as the
        // resolver's DamageRoll roll path. We don't have a delegate hook
        // here; just sum the dice directly.
        int rolled = roll.FlatMod;
        for (int i = 0; i < roll.DiceCount; i++)
            rolled += rng.NextInt(1, roll.DiceSides + 1);
        if (rolled < 0) rolled = 0;

        // Phase 6.5 M4 carryover — Hybrid Medical Incompatibility scales
        // potion healing at 0.75× (round down, min 1). Same handler the
        // Field Repair / Lay on Paws paths use; centralising here means
        // every future healing source gets it automatically.
        int delivered = HybridDetriments.ScaleHealForHybrid(pc, rolled);

        // Apply to PC HP, capped to MaxHp.
        int before = pc.CurrentHp;
        pc.CurrentHp = System.Math.Min(pc.MaxHp, pc.CurrentHp + delivered);
        int actualHealed = pc.CurrentHp - before;

        return ConsumeResult.Healed(actualHealed,
            wasScaledForHybrid: pc.IsHybrid && delivered != rolled);
    }

    private static ConsumeResult ConsumeScentMask(ItemDef item, Character pc)
    {
        var tier = ParseScentMaskTier(item.Id);
        if (tier == ScentMaskTier.None)
            return ConsumeResult.Rejected($"unknown scent-mask tier on '{item.Id}'.");

        // Hybrid PCs are the use case; non-hybrids consuming a mask is
        // mechanically a no-op (no detriments to suppress) but we still
        // accept the consume so the UI doesn't error out — flavoured as
        // "you put on a mask; nothing in particular happens".
        if (pc.Hybrid is null)
            return ConsumeResult.MaskApplied(tier, hadEffect: false);

        pc.Hybrid.ActiveMaskTier = tier;
        return ConsumeResult.MaskApplied(tier, hadEffect: true);
    }

    private static ScentMaskTier ParseScentMaskTier(string itemId) => itemId switch
    {
        "scent_mask_basic"      => ScentMaskTier.Basic,
        "scent_mask_military"   => ScentMaskTier.Military,
        "scent_mask_deep_cover" => ScentMaskTier.DeepCover,
        _                       => ScentMaskTier.None,
    };
}

/// <summary>
/// Phase 7 M2 — outcome of a <see cref="ConsumableHandler.Consume"/> call.
/// Tagged-union shape: exactly one of <see cref="HealedAmount"/>,
/// <see cref="MaskTier"/>, <see cref="RejectedReason"/>, or
/// <see cref="UnrecognizedItemId"/> carries the meaningful payload, keyed
/// by <see cref="Kind"/>.
/// </summary>
public sealed record ConsumeResult
{
    public enum ResultKind : byte { Healed, MaskApplied, Rejected, Unrecognized }

    public ResultKind Kind { get; init; }
    public int        HealedAmount { get; init; }
    public bool       WasScaledForHybrid { get; init; }
    public ScentMaskTier MaskTier { get; init; }
    public bool       MaskHadEffect { get; init; }
    public string     RejectedReason { get; init; } = "";
    public string     UnrecognizedItemId { get; init; } = "";

    public bool IsSuccess => Kind == ResultKind.Healed || Kind == ResultKind.MaskApplied;

    public static ConsumeResult Healed(int amount, bool wasScaledForHybrid)
        => new() { Kind = ResultKind.Healed, HealedAmount = amount, WasScaledForHybrid = wasScaledForHybrid };

    public static ConsumeResult MaskApplied(ScentMaskTier tier, bool hadEffect)
        => new() { Kind = ResultKind.MaskApplied, MaskTier = tier, MaskHadEffect = hadEffect };

    public static ConsumeResult Rejected(string reason)
        => new() { Kind = ResultKind.Rejected, RejectedReason = reason };

    public static ConsumeResult Unrecognized(string itemId)
        => new() { Kind = ResultKind.Unrecognized, UnrecognizedItemId = itemId };
}
