using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.Mastery;
using NeonSprawl.Server.Game.Skills;

namespace NeonSprawl.Server.Game.Crafting;

/// <summary>
/// Orchestrates craft side effects: recipe resolve, inventory pre-flight, input removal, output grant, refine XP (NEO-69).
/// Craft HTTP: <see cref="PlayerCraftApi"/> (NEO-70). NEO-71 telemetry hook sites: <see cref="TryCraft"/> success path and <see cref="Deny"/>.
/// </summary>
public static class CraftOperations
{
    private static readonly CraftIoApplied[] EmptyIo = [];

    /// <summary>
    /// Resolves one craft for <paramref name="playerId"/> and <paramref name="recipeId"/> at batch <paramref name="quantity"/>.
    /// Mutations commit in order: remove inputs → add outputs → refine XP; compensating rollback on output or XP failure.
    /// </summary>
    public static CraftResult TryCraft(
        string playerId,
        string recipeId,
        int quantity,
        IRecipeDefinitionRegistry recipeRegistry,
        IItemDefinitionRegistry itemRegistry,
        IPlayerInventoryStore inventoryStore,
        ISkillDefinitionRegistry skillRegistry,
        IPlayerSkillProgressionStore xpStore,
        ISkillLevelCurve levelCurve,
        PerkUnlockEngine perkUnlockEngine)
    {
        if (quantity <= 0)
        {
            return Deny(CraftReasonCodes.InvalidQuantity);
        }

        var recipeKey = recipeId.Trim();
        if (recipeKey.Length == 0 || !recipeRegistry.TryGetDefinition(recipeKey, out var recipe))
        {
            return Deny(CraftReasonCodes.UnknownRecipe);
        }

        if (!inventoryStore.TryGetSnapshot(playerId, out var snapshot))
        {
            return Deny(CraftReasonCodes.InventoryStoreMissing);
        }

        var scaledInputs = ScaleIoRows(recipe.Inputs, quantity);
        var scaledOutputs = ScaleIoRows(recipe.Outputs, quantity);

        foreach (var input in scaledInputs)
        {
            var held = PlayerInventoryOperations.GetHeldQuantity(snapshot, input.ItemId, itemRegistry);
            if (held < input.Quantity)
            {
                return Deny(CraftReasonCodes.InsufficientMaterials);
            }
        }

        var simulated = CloneSnapshot(snapshot);
        foreach (var input in scaledInputs)
        {
            if (!PlayerInventoryOperations.TrySimulateRemoveStack(ref simulated, input.ItemId, input.Quantity, itemRegistry))
            {
                return Deny(CraftReasonCodes.InsufficientMaterials);
            }
        }

        foreach (var output in scaledOutputs)
        {
            if (!PlayerInventoryOperations.TrySimulateAddStack(ref simulated, output.ItemId, output.Quantity, itemRegistry))
            {
                return Deny(CraftReasonCodes.InventoryFull);
            }
        }

        var inputsRemoved = new List<CraftIoApplied>(scaledInputs.Length);
        foreach (var input in scaledInputs)
        {
            var removeOutcome = PlayerInventoryOperations.TryRemoveStack(
                playerId,
                input.ItemId,
                input.Quantity,
                itemRegistry,
                inventoryStore);

            if (removeOutcome.Kind == PlayerInventoryMutationKind.StoreMissing)
            {
                CompensatingRestoreInputs(playerId, inputsRemoved, itemRegistry, inventoryStore);
                return Deny(CraftReasonCodes.InventoryStoreMissing);
            }

            if (removeOutcome.Kind == PlayerInventoryMutationKind.Denied)
            {
                CompensatingRestoreInputs(playerId, inputsRemoved, itemRegistry, inventoryStore);
                return Deny(CraftReasonCodes.InsufficientMaterials);
            }

            inputsRemoved.Add(input);
        }

        var outputsApplied = new List<CraftIoApplied>(scaledOutputs.Length);
        foreach (var output in scaledOutputs)
        {
            var addOutcome = PlayerInventoryOperations.TryAddStack(
                playerId,
                output.ItemId,
                output.Quantity,
                itemRegistry,
                inventoryStore);

            if (addOutcome.Kind == PlayerInventoryMutationKind.StoreMissing)
            {
                CompensatingRemoveOutputs(playerId, outputsApplied, itemRegistry, inventoryStore);
                CompensatingRestoreInputs(playerId, scaledInputs, itemRegistry, inventoryStore);
                return Deny(CraftReasonCodes.InventoryStoreMissing);
            }

            if (addOutcome.Kind == PlayerInventoryMutationKind.Denied)
            {
                CompensatingRemoveOutputs(playerId, outputsApplied, itemRegistry, inventoryStore);
                CompensatingRestoreInputs(playerId, scaledInputs, itemRegistry, inventoryStore);
                return Deny(CraftReasonCodes.InventoryFull);
            }

            outputsApplied.Add(output);
        }

        var xpOutcome = SkillProgressionGrantOperations.TryApplyGrant(
            playerId,
            RefineSkillXpConstants.RefineSkillId,
            RefineSkillXpConstants.ActivityXpPerCraftRefineCompletion,
            RefineSkillXpConstants.ActivitySourceKind,
            skillRegistry,
            xpStore,
            levelCurve,
            perkUnlockEngine);

        if (xpOutcome.Kind != SkillProgressionGrantApplyKind.Granted)
        {
            CompensatingRemoveOutputs(playerId, outputsApplied, itemRegistry, inventoryStore);
            CompensatingRestoreInputs(playerId, scaledInputs, itemRegistry, inventoryStore);

            var xpReason = xpOutcome.Kind == SkillProgressionGrantApplyKind.ProgressionStoreMissing
                ? CraftReasonCodes.ProgressionStoreMissing
                : xpOutcome.Response?.ReasonCode ?? CraftReasonCodes.ProgressionStoreMissing;

            return Deny(xpReason);
        }

        // --- Telemetry hook site (NEO-71): future E9.M1 catalog event `item_crafted` ---
        // TODO(E9.M1): catalog emit — on every successful craft (inputs removed, outputs granted, refine XP applied).
        // Planned payload fields: playerId, recipeId, quantity (batch), inputsConsumed, outputsGranted, xpGrantSummary
        // (skillId, amount, sourceKind). No ingest or ILogger here (comments-only). Successful output grants also pass
        // through PlayerInventoryOperations (`item_created` hook, NEO-56). PlayerCraftApi delegates here (no duplicate hooks).

        return new CraftResult(
            Success: true,
            ReasonCode: null,
            InputsConsumed: scaledInputs,
            OutputsGranted: outputsApplied,
            XpGrantSummary: new CraftXpGrantSummary(
                RefineSkillXpConstants.RefineSkillId,
                RefineSkillXpConstants.ActivityXpPerCraftRefineCompletion,
                RefineSkillXpConstants.ActivitySourceKind));
    }

    private static CraftIoApplied[] ScaleIoRows(IReadOnlyList<RecipeIoRow> rows, int quantity)
    {
        var scaled = new CraftIoApplied[rows.Count];
        for (var i = 0; i < rows.Count; i++)
        {
            var row = rows[i];
            scaled[i] = new CraftIoApplied(row.ItemId, row.Quantity * quantity);
        }

        return scaled;
    }

    private static PlayerInventorySnapshot CloneSnapshot(PlayerInventorySnapshot snapshot) =>
        new()
        {
            BagSlots = PlayerInventorySnapshot.CloneSlots(snapshot.BagSlots),
            EquipmentSlots = PlayerInventorySnapshot.CloneSlots(snapshot.EquipmentSlots),
        };

    private static void CompensatingRestoreInputs(
        string playerId,
        IReadOnlyList<CraftIoApplied> inputs,
        IItemDefinitionRegistry itemRegistry,
        IPlayerInventoryStore inventoryStore)
    {
        foreach (var input in inputs)
        {
            _ = PlayerInventoryOperations.TryAddStack(
                playerId,
                input.ItemId,
                input.Quantity,
                itemRegistry,
                inventoryStore);
        }
    }

    private static void CompensatingRemoveOutputs(
        string playerId,
        IReadOnlyList<CraftIoApplied> outputs,
        IItemDefinitionRegistry itemRegistry,
        IPlayerInventoryStore inventoryStore)
    {
        foreach (var output in outputs)
        {
            _ = PlayerInventoryOperations.TryRemoveStack(
                playerId,
                output.ItemId,
                output.Quantity,
                itemRegistry,
                inventoryStore);
        }
    }

    private static CraftResult Deny(string reasonCode)
    {
        // --- Telemetry hook site (NEO-71): future E9.M1 catalog event `craft_failed` ---
        // TODO(E9.M1): catalog emit — on every structured craft deny with stable reasonCode (CraftReasonCodes).
        // Planned payload fields: playerId, recipeId, quantity, reasonCode — from enclosing TryCraft scope at call sites.
        // No ingest or ILogger here (comments-only). PlayerCraftApi delegates here (no duplicate hooks).

        return new CraftResult(
            Success: false,
            ReasonCode: reasonCode,
            InputsConsumed: EmptyIo,
            OutputsGranted: EmptyIo,
            XpGrantSummary: null);
    }
}
