using Microsoft.AspNetCore.SignalR;
using Meezi.API.Hubs;
using Meezi.API.Models.Notifications;
using Meezi.Core.Entities;
using Meezi.Core.Enums;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace Meezi.API.Services;

public record IngredientDto(
    string Id,
    string Name,
    string Unit,
    decimal QuantityOnHand,
    decimal ReorderLevel,
    decimal UnitCost,
    decimal ParLevel,
    decimal LowStockWarningPercent,
    decimal WarningThreshold,
    decimal StockValueToman,
    bool IsLowStock);

public record CreateIngredientRequest(
    string Name,
    string Unit,
    decimal QuantityOnHand,
    decimal ReorderLevel,
    decimal UnitCost,
    decimal ParLevel,
    decimal LowStockWarningPercent,
    decimal? TotalPaidToman = null,
    string? BranchId = null);

public record UpdateIngredientRequest(
    string? Name,
    string? Unit,
    decimal? ReorderLevel,
    decimal? UnitCost,
    decimal? ParLevel,
    decimal? LowStockWarningPercent);

public record AdjustStockRequest(
    decimal Delta,
    string? Note,
    decimal? TotalPaidToman = null,
    string? BranchId = null);

public record InventoryPurchaseDto(
    string Id,
    string IngredientId,
    string IngredientName,
    decimal Delta,
    string Unit,
    decimal TotalPaidToman,
    decimal UnitCostAfter,
    DateTime CreatedAt,
    string? ExpenseId);

public record InventoryPurchasesSummaryDto(
    decimal TotalPaidToman,
    int PurchaseCount,
    IReadOnlyList<InventoryPurchaseDto> Recent);

public record RecipeLineDto(
    string Id,
    string IngredientId,
    string IngredientName,
    string Unit,
    decimal QuantityPerUnit);

public record MenuItemRecipeDto(
    string MenuItemId,
    string MenuItemName,
    IReadOnlyList<RecipeLineDto> Lines,
    decimal MaterialCostPerUnitToman);

public record SetRecipeLineRequest(string IngredientId, decimal QuantityPerUnit);

public record SetMenuItemRecipeRequest(IReadOnlyList<SetRecipeLineRequest> Lines);

public record OrderDeductionResult(
    bool Applied,
    IReadOnlyList<string> LowStockIngredientNames);

public interface IInventoryService
{
    Task<IReadOnlyList<IngredientDto>> ListAsync(string cafeId, CancellationToken ct = default);
    Task<IReadOnlyList<IngredientDto>> LowStockAsync(string cafeId, CancellationToken ct = default);
    Task<IngredientDto?> CreateAsync(string cafeId, CreateIngredientRequest request, CancellationToken ct = default);
    Task<IngredientDto?> UpdateAsync(string cafeId, string ingredientId, UpdateIngredientRequest request, CancellationToken ct = default);
    Task<IngredientDto?> AdjustAsync(
        string cafeId,
        string ingredientId,
        AdjustStockRequest request,
        string? userId,
        CancellationToken ct = default);
    Task<InventoryPurchasesSummaryDto> GetPurchasesSummaryAsync(
        string cafeId,
        string branchId,
        DateOnly from,
        DateOnly to,
        CancellationToken ct = default);
    Task<MenuItemRecipeDto?> GetRecipeAsync(string cafeId, string menuItemId, CancellationToken ct = default);
    Task<MenuItemRecipeDto?> SetRecipeAsync(string cafeId, string menuItemId, SetMenuItemRecipeRequest request, CancellationToken ct = default);
    Task<OrderDeductionResult> DeductForOrderAsync(
        string cafeId,
        string orderId,
        IReadOnlyList<(string MenuItemId, int Quantity)> lines,
        CancellationToken ct = default);
}

public class InventoryService : IInventoryService
{
    private readonly AppDbContext _db;
    private readonly IHubContext<KdsHub> _kdsHub;

    public InventoryService(AppDbContext db, IHubContext<KdsHub> kdsHub)
    {
        _db = db;
        _kdsHub = kdsHub;
    }

    public async Task<IReadOnlyList<IngredientDto>> ListAsync(string cafeId, CancellationToken ct = default)
    {
        var rows = await _db.Ingredients.AsNoTracking()
            .Where(i => i.CafeId == cafeId)
            .OrderBy(i => i.Name)
            .ToListAsync(ct);
        return rows.Select(ToDto).ToList();
    }

    public async Task<IReadOnlyList<IngredientDto>> LowStockAsync(string cafeId, CancellationToken ct = default)
    {
        var rows = await _db.Ingredients.Where(i => i.CafeId == cafeId).ToListAsync(ct);
        return rows.Where(IsLowStock).Select(ToDto).ToList();
    }

    public async Task<IngredientDto?> CreateAsync(string cafeId, CreateIngredientRequest request, CancellationToken ct = default)
    {
        var par = request.ParLevel > 0 ? request.ParLevel : request.QuantityOnHand;
        var unitCost = ResolveUnitCost(request.QuantityOnHand, request.UnitCost, request.TotalPaidToman);
        var entity = new Ingredient
        {
            Id = $"ing_{Guid.NewGuid():N}"[..24],
            CafeId = cafeId,
            Name = request.Name.Trim(),
            Unit = string.IsNullOrWhiteSpace(request.Unit) ? "عدد" : request.Unit.Trim(),
            QuantityOnHand = request.QuantityOnHand,
            ReorderLevel = request.ReorderLevel,
            UnitCost = unitCost,
            ParLevel = par,
            LowStockWarningPercent = ClampPercent(request.LowStockWarningPercent)
        };
        _db.Ingredients.Add(entity);
        if (request.QuantityOnHand != 0)
        {
            var movement = NewMovement(
                cafeId,
                entity.Id,
                request.QuantityOnHand,
                request.TotalPaidToman > 0 ? StockMovementKind.Purchase : StockMovementKind.Manual,
                null,
                request.TotalPaidToman > 0 ? "خرید اولیه" : "موجودی اولیه",
                request.TotalPaidToman,
                request.BranchId);
            _db.StockMovements.Add(movement);

            if (request.TotalPaidToman > 0 && !string.IsNullOrWhiteSpace(request.BranchId))
            {
                var expense = await TryCreatePurchaseExpenseAsync(
                    cafeId,
                    request.BranchId,
                    request.TotalPaidToman.Value,
                    $"خرید انبار: {entity.Name} ({request.QuantityOnHand:N0} {entity.Unit})",
                    userId: null,
                    ct);
                if (expense is not null)
                    movement.ExpenseId = expense.Id;
            }
        }

        await _db.SaveChangesAsync(ct);
        return ToDto(entity);
    }

    public async Task<IngredientDto?> UpdateAsync(
        string cafeId,
        string ingredientId,
        UpdateIngredientRequest request,
        CancellationToken ct = default)
    {
        var entity = await _db.Ingredients.FirstOrDefaultAsync(i => i.Id == ingredientId && i.CafeId == cafeId, ct);
        if (entity is null) return null;

        if (!string.IsNullOrWhiteSpace(request.Name)) entity.Name = request.Name.Trim();
        if (!string.IsNullOrWhiteSpace(request.Unit)) entity.Unit = request.Unit.Trim();
        if (request.ReorderLevel.HasValue) entity.ReorderLevel = request.ReorderLevel.Value;
        if (request.UnitCost.HasValue) entity.UnitCost = request.UnitCost.Value;
        if (request.ParLevel.HasValue) entity.ParLevel = request.ParLevel.Value;
        if (request.LowStockWarningPercent.HasValue)
            entity.LowStockWarningPercent = ClampPercent(request.LowStockWarningPercent.Value);

        await _db.SaveChangesAsync(ct);
        return ToDto(entity);
    }

    public async Task<IngredientDto?> AdjustAsync(
        string cafeId,
        string ingredientId,
        AdjustStockRequest request,
        string? userId,
        CancellationToken ct = default)
    {
        var entity = await _db.Ingredients.FirstOrDefaultAsync(i => i.Id == ingredientId && i.CafeId == cafeId, ct);
        if (entity is null) return null;

        if (request.Delta > 0)
        {
            if (request.TotalPaidToman is null or <= 0)
                throw new InvalidOperationException("TOTAL_PAID_REQUIRED");
            if (string.IsNullOrWhiteSpace(request.BranchId))
                throw new InvalidOperationException("BRANCH_ID_REQUIRED");

            var oldQty = entity.QuantityOnHand;
            var oldValue = oldQty * entity.UnitCost;
            entity.QuantityOnHand += request.Delta;
            entity.UnitCost = entity.QuantityOnHand > 0
                ? (oldValue + request.TotalPaidToman.Value) / entity.QuantityOnHand
                : request.TotalPaidToman.Value / request.Delta;

            var movement = NewMovement(
                cafeId,
                ingredientId,
                request.Delta,
                StockMovementKind.Purchase,
                null,
                request.Note?.Trim() ?? "خرید / ورود به انبار",
                request.TotalPaidToman,
                request.BranchId);
            _db.StockMovements.Add(movement);

            var expense = await TryCreatePurchaseExpenseAsync(
                cafeId,
                request.BranchId!,
                request.TotalPaidToman.Value,
                $"خرید انبار: {entity.Name} ({request.Delta:N0} {entity.Unit})",
                userId,
                ct);
            if (expense is not null)
                movement.ExpenseId = expense.Id;
        }
        else
        {
            entity.QuantityOnHand += request.Delta;
            _db.StockMovements.Add(NewMovement(
                cafeId,
                ingredientId,
                request.Delta,
                StockMovementKind.Manual,
                null,
                request.Note?.Trim() ?? "تنظیم دستی",
                null));
        }

        await _db.SaveChangesAsync(ct);

        if (IsLowStock(entity))
            await NotifyLowStockAsync(cafeId, [entity], ct);

        return ToDto(entity);
    }

    public async Task<InventoryPurchasesSummaryDto> GetPurchasesSummaryAsync(
        string cafeId,
        string branchId,
        DateOnly from,
        DateOnly to,
        CancellationToken ct = default)
    {
        var utcStart = from.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
        var utcEnd = to.AddDays(1).ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);

        var movements = await _db.StockMovements.AsNoTracking()
            .Include(m => m.Ingredient)
            .Where(m => m.CafeId == cafeId
                && m.BranchId == branchId
                && m.TotalCostToman != null
                && m.TotalCostToman > 0
                && m.CreatedAt >= utcStart
                && m.CreatedAt < utcEnd)
            .OrderByDescending(m => m.CreatedAt)
            .Take(50)
            .ToListAsync(ct);

        var total = movements.Sum(m => m.TotalCostToman ?? 0);
        var recent = movements.Select(m => new InventoryPurchaseDto(
            m.Id,
            m.IngredientId,
            m.Ingredient.Name,
            m.Delta,
            m.Ingredient.Unit,
            m.TotalCostToman ?? 0,
            m.Ingredient.UnitCost,
            m.CreatedAt,
            m.ExpenseId)).ToList();

        return new InventoryPurchasesSummaryDto(total, recent.Count, recent);
    }

    public async Task<MenuItemRecipeDto?> GetRecipeAsync(string cafeId, string menuItemId, CancellationToken ct = default)
    {
        var item = await _db.MenuItems.AsNoTracking()
            .FirstOrDefaultAsync(m => m.Id == menuItemId && m.CafeId == cafeId, ct);
        if (item is null) return null;

        var lines = await _db.MenuItemIngredients.AsNoTracking()
            .Include(r => r.Ingredient)
            .Where(r => r.CafeId == cafeId && r.MenuItemId == menuItemId)
            .ToListAsync(ct);

        return BuildRecipeDto(item, lines);
    }

    public async Task<MenuItemRecipeDto?> SetRecipeAsync(
        string cafeId,
        string menuItemId,
        SetMenuItemRecipeRequest request,
        CancellationToken ct = default)
    {
        var item = await _db.MenuItems.FirstOrDefaultAsync(m => m.Id == menuItemId && m.CafeId == cafeId, ct);
        if (item is null) return null;

        var existing = await _db.MenuItemIngredients
            .Where(r => r.CafeId == cafeId && r.MenuItemId == menuItemId)
            .ToListAsync(ct);
        _db.MenuItemIngredients.RemoveRange(existing);

        foreach (var line in request.Lines.Where(l => l.QuantityPerUnit > 0))
        {
            var ingOk = await _db.Ingredients.AnyAsync(i => i.Id == line.IngredientId && i.CafeId == cafeId, ct);
            if (!ingOk) continue;

            _db.MenuItemIngredients.Add(new MenuItemIngredient
            {
                Id = $"mii_{Guid.NewGuid():N}"[..24],
                CafeId = cafeId,
                MenuItemId = menuItemId,
                IngredientId = line.IngredientId,
                QuantityPerUnit = line.QuantityPerUnit
            });
        }

        await _db.SaveChangesAsync(ct);
        return await GetRecipeAsync(cafeId, menuItemId, ct);
    }

    public async Task<OrderDeductionResult> DeductForOrderAsync(
        string cafeId,
        string orderId,
        IReadOnlyList<(string MenuItemId, int Quantity)> lines,
        CancellationToken ct = default)
    {
        if (lines.Count == 0)
            return new OrderDeductionResult(false, []);

        var menuItemIds = lines.Select(l => l.MenuItemId).Distinct().ToList();
        var recipes = await _db.MenuItemIngredients
            .Where(r => r.CafeId == cafeId && menuItemIds.Contains(r.MenuItemId))
            .ToListAsync(ct);

        if (recipes.Count == 0)
            return new OrderDeductionResult(false, []);

        var usage = new Dictionary<string, decimal>(StringComparer.Ordinal);
        foreach (var line in lines)
        {
            foreach (var recipe in recipes.Where(r => r.MenuItemId == line.MenuItemId))
            {
                var amount = recipe.QuantityPerUnit * line.Quantity;
                usage[recipe.IngredientId] = usage.GetValueOrDefault(recipe.IngredientId) + amount;
            }
        }

        if (usage.Count == 0)
            return new OrderDeductionResult(false, []);

        var ingredientIds = usage.Keys.ToList();
        var ingredients = await _db.Ingredients
            .Where(i => i.CafeId == cafeId && ingredientIds.Contains(i.Id))
            .ToListAsync(ct);

        foreach (var ing in ingredients)
        {
            if (!usage.TryGetValue(ing.Id, out var deduct)) continue;
            ing.QuantityOnHand -= deduct;
            _db.StockMovements.Add(NewMovement(
                cafeId,
                ing.Id,
                -deduct,
                StockMovementKind.OrderDeduction,
                orderId,
                $"سفارش {orderId[..Math.Min(8, orderId.Length)]}",
                null));
        }

        await _db.SaveChangesAsync(ct);

        var lowStock = ingredients.Where(IsLowStock).ToList();
        if (lowStock.Count > 0)
            await NotifyLowStockAsync(cafeId, lowStock, ct);

        return new OrderDeductionResult(
            true,
            lowStock.Select(i => i.Name).ToList());
    }

    private async Task NotifyLowStockAsync(string cafeId, IReadOnlyList<Ingredient> items, CancellationToken ct)
    {
        if (items.Count == 0) return;

        var names = string.Join("، ", items.Select(i => $"{i.Name} ({FormatQty(i)})"));
        var notification = new CafeNotification
        {
            CafeId = cafeId,
            Type = "inventory_low_stock",
            Title = "کمبود مواد اولیه",
            Body = names
        };
        _db.CafeNotifications.Add(notification);
        await _db.SaveChangesAsync(ct);

        var dto = new CafeNotificationDto(
            notification.Id,
            notification.Type,
            notification.Title,
            notification.Body,
            notification.ReferenceId,
            notification.TableNumber,
            notification.IsRead,
            notification.CreatedAt);

        await _kdsHub.Clients.Group(KdsHub.GroupName(cafeId))
            .SendAsync("NotificationReceived", dto, ct);
    }

    private async Task<Expense?> TryCreatePurchaseExpenseAsync(
        string cafeId,
        string branchId,
        decimal amount,
        string note,
        string? userId,
        CancellationToken ct)
    {
        var branch = await _db.Branches.FirstOrDefaultAsync(
            b => b.Id == branchId && b.CafeId == cafeId && b.IsActive,
            ct);
        if (branch is null) return null;

        var expense = new Expense
        {
            Id = $"exp_{Guid.NewGuid():N}"[..24],
            CafeId = cafeId,
            BranchId = branchId,
            Category = ExpenseCategory.Supplies,
            Amount = amount,
            Note = note,
            CreatedByUserId = userId ?? "system",
            CreatedAt = DateTime.UtcNow
        };
        _db.Expenses.Add(expense);
        return expense;
    }

    private static decimal ResolveUnitCost(decimal qty, decimal unitCost, decimal? totalPaid)
    {
        if (totalPaid is > 0 && qty > 0)
            return totalPaid.Value / qty;
        return unitCost;
    }

    private static StockMovement NewMovement(
        string cafeId,
        string ingredientId,
        decimal delta,
        StockMovementKind kind,
        string? orderId,
        string? note,
        decimal? totalCostToman,
        string? branchId = null) => new()
    {
        Id = $"stk_{Guid.NewGuid():N}"[..24],
        CafeId = cafeId,
        IngredientId = ingredientId,
        BranchId = branchId,
        Delta = delta,
        Kind = kind,
        OrderId = orderId,
        Note = note,
        TotalCostToman = totalCostToman > 0 ? totalCostToman : null
    };

    private static MenuItemRecipeDto BuildRecipeDto(MenuItem item, List<MenuItemIngredient> lines)
    {
        var recipeLines = lines.Select(r => new RecipeLineDto(
            r.Id,
            r.IngredientId,
            r.Ingredient.Name,
            r.Ingredient.Unit,
            r.QuantityPerUnit)).ToList();

        var cost = lines.Sum(r => r.QuantityPerUnit * r.Ingredient.UnitCost);
        return new MenuItemRecipeDto(item.Id, item.Name, recipeLines, cost);
    }

    private static bool IsLowStock(Ingredient i)
    {
        var threshold = WarningThreshold(i);
        return i.QuantityOnHand <= threshold;
    }

    private static decimal WarningThreshold(Ingredient i)
    {
        if (i.ParLevel > 0 && i.LowStockWarningPercent > 0)
            return i.ParLevel * (i.LowStockWarningPercent / 100m);
        return i.ReorderLevel;
    }

    private static IngredientDto ToDto(Ingredient i)
    {
        var threshold = WarningThreshold(i);
        return new IngredientDto(
            i.Id,
            i.Name,
            i.Unit,
            i.QuantityOnHand,
            i.ReorderLevel,
            i.UnitCost,
            i.ParLevel,
            i.LowStockWarningPercent,
            threshold,
            i.QuantityOnHand * i.UnitCost,
            i.QuantityOnHand <= threshold);
    }

    private static decimal ClampPercent(decimal p) => Math.Clamp(p, 1m, 100m);

    private static string FormatQty(Ingredient i) =>
        $"{i.QuantityOnHand:N0} {i.Unit}";
}
