using FruitBank.Common.Dtos;
using FruitBank.Common.Entities;
using FruitBank.Common.Enums;
using FruitBank.Common.Interfaces;
using Nop.Core.Domain.Catalog;
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
using Nop.Services.Catalog;
using Nop.Services.Orders;

namespace Nop.Plugin.Misc.FruitBankPlugin.Services;

/// <summary>
/// Extension methods for product replacement logic on PreOrderConversionService.
/// 
/// SETUP REQUIRED: Add this as a partial class by:
/// 1. Add `partial` keyword to PreOrderConversionService class declaration
/// 2. This file uses the same injected fields — no extra DI needed
/// </summary>
public partial class PreOrderConversionService
{
    // ── Product replacement ───────────────────────────────────────────────────

    /// <summary>
    /// Called from FruitBankDataController.UpdateShippingItem when ProductId changes.
    /// oldItem must be loaded BEFORE the DB update to capture the previous state.
    /// </summary>
    public async Task ReplaceShippingItemProductAsync(
        int shippingItemId,
        int newProductId,
        ShippingItem oldItem)
    {
        if (oldItem.ProductId == null || oldItem.ProductId.Value == newProductId) return;

        var oldProductId = oldItem.ProductId.Value;
        var qty          = oldItem.QuantityOnDocument;
        var isMeasured   = oldItem.IsMeasured;

        Console.WriteLine($"[ReplaceShippingItemProduct] #{shippingItemId}: {oldProductId}→{newProductId}, qty={qty}, measured={isMeasured}");

        // ── Guard: reject if any linked order is being measured ───────────────
        var affectedOrders = await GetAffectedOpenOrdersAsync(oldProductId);
        var startedOrders  = affectedOrders
            .Where(o => o.MeasuringStatus > MeasuringStatus.NotStarted)
            .ToList();

        if (startedOrders.Any())
            throw new InvalidOperationException(
                $"Mérés folyamatban a következő rendeléseken: " +
                $"#{string.Join(", #", startedOrders.Select(o => o.Id))}. " +
                $"Termékcsere nem lehetséges.");

        // ── Stock / IncomingQuantity swap ─────────────────────────────────────
        if (!isMeasured)
        {
            // Item not yet on the truck — only IncomingQuantity moves
            await SyncIncomingQuantityAsync(oldProductId, qty, 0);
            await SyncIncomingQuantityAsync(newProductId, 0, qty);
        }
        else
        {
            // Item already measured/received into stock — adjust actual stock
            await _dbContext.UpdateStockQuantityAndWeightAsync(
                oldProductId, -oldItem.MeasuredQuantity,
                $"Termék csere: shippingItem #{shippingItemId} — {oldProductId}→{newProductId}",
                oldItem.IsMeasurable ? -oldItem.MeasuredNetWeight : 0d);

            await _dbContext.UpdateStockQuantityAndWeightAsync(
                newProductId, +oldItem.MeasuredQuantity,
                $"Termék csere: shippingItem #{shippingItemId} — {oldProductId}→{newProductId}",
                oldItem.IsMeasurable ? +oldItem.MeasuredNetWeight : 0d);
        }

        // ── Swap order items ──────────────────────────────────────────────────
        var openOrders        = affectedOrders
            .Where(o => o.MeasuringStatus == MeasuringStatus.NotStarted)
            .OrderBy(o => o.Id)
            .ToList();

        var replacementBudget = qty;
        var affectedOrderIds  = new List<int>();

        foreach (var orderDto in openOrders)
        {
            if (replacementBudget <= 0) break;

            var order = await _dbContext.Orders.GetByIdAsync(orderDto.Id);
            if (order == null) continue;

            var allItems     = await _orderService.GetOrderItemsAsync(order.Id);
            var oldOrderItem = allItems.FirstOrDefault(oi => oi.ProductId == oldProductId);
            if (oldOrderItem == null) continue;

            var swapQty = Math.Min(oldOrderItem.Quantity, replacementBudget);

            // Remove old item — restores stock, deletes pallets/GAs, deletes item
            await _orderItemService.RemoveOrderItemFromOrderAsync(
                order, oldOrderItem,
                $"Termék csere: #{oldProductId}→#{newProductId}, rendelés #{order.Id}");

            // Add new item — direct insert without availability check
            var newProduct = await _productService.GetProductByIdAsync(newProductId);
            if (newProduct != null)
            {
                var newProductDto = await _dbContext.ProductDtos.GetByIdAsync(newProductId, true);
                var newOrderItem  = await _orderItemService.CreateOrderItemAsync(
                    newProduct, order,
                    productId                  : newProductId,
                    quantity                   : swapQty,
                    price                      : oldOrderItem.UnitPriceInclTax,
                    isMeasurable               : newProductDto?.IsMeasurable ?? false,
                    unitPricesIncludeDiscounts : false);

                await _orderItemService.AddOrderItemToOrderAsync(
                    order, newOrderItem,
                    $"Termék csere felvitel: #{newProductId}, rendelés #{order.Id}");
            }

            await _orderService.UpdateOrderAsync(order);
            await _orderItemService.InsertOrderNoteAsync(order.Id, false,
                $"Termék cserélve: #{oldProductId}→#{newProductId} ({swapQty} db), " +
                $"szállítói dok. #{oldItem.ShippingDocumentId}");

            replacementBudget -= swapQty;
            affectedOrderIds.Add(order.Id);
        }

        // ── Swap preorder items ───────────────────────────────────────────────
        if (affectedOrderIds.Any())
        {
            var preorders = (await _preorderDbContext.PreOrders.GetAll(false).ToListAsync())
                .Where(p => p.OrderId.HasValue && affectedOrderIds.Contains(p.OrderId.Value))
                .ToList();

            foreach (var preorder in preorders)
            {
                var piList = await _preorderDbContext.PreOrderItems
                    .GetAllByPreOrderIdAsync(preorder.Id)
                    .ToListAsync();

                foreach (var pi in piList.Where(i => i.ProductId == oldProductId))
                {
                    pi.ProductId = newProductId;
                    await _preorderDbContext.PreOrderItems.UpdateAsync(pi);
                }
            }
        }

        // ── Trigger conversion for new product ────────────────────────────────
        // New product may now have pending preorders that can be fulfilled
        await ConvertPreOrdersForProductsAsync(
            new List<int> { newProductId },
            oldItem.ShippingDocumentId);

        // TODO: SignalR notification to admin hub
        // TODO: SendPreOrderProductReplacedNotificationAsync per affected customer

        Console.WriteLine($"[ReplaceShippingItemProduct] Complete: " +
                          $"{affectedOrderIds.Count} orders swapped, budget remaining={replacementBudget}");
    }

    private async Task<List<OrderDto>> GetAffectedOpenOrdersAsync(int oldProductId)
    {
        // Orders that are referenced by a preorder AND contain the old product
        var preorderOrderIds = (await _preorderDbContext.PreOrders.GetAll(false).ToListAsync())
            .Where(p => p.OrderId.HasValue)
            .Select(p => p.OrderId!.Value)
            .ToHashSet();

        if (!preorderOrderIds.Any()) return new();

        var matchingOrderIds = await _dbContext.OrderItems.Table
            .Where(oi => oi.ProductId == oldProductId && preorderOrderIds.Contains(oi.OrderId))
            .Select(oi => oi.OrderId)
            .Distinct()
            .ToListAsync();

        if (!matchingOrderIds.Any()) return new();

        return await _dbContext.OrderDtos
            .GetAll(false)
            .Where(o => matchingOrderIds.Contains(o.Id) && !o.Deleted)
            .ToListAsync();
    }
}
