using Meezi.API.Models.Orders;
using Meezi.API.Models.Public;
using Meezi.API.Services.Delivery;
using Meezi.API.Services.Printing;
using Meezi.Core.Entities;
using Meezi.Core.Enums;
using Meezi.Core.Utilities;
using Meezi.Core.Interfaces;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;

namespace Meezi.API.Services;

public record OrderServiceResult<T>(bool Success, T? Data, string? ErrorCode = null, string? Field = null);

public interface IOrderService
{
    Task<IReadOnlyList<OrderDto>> GetOrdersAsync(string cafeId, OrderStatus? status, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<OrderDto>> GetOpenOrdersAsync(string cafeId, string? search, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<LiveOrderDto>> GetLiveOrdersAsync(string cafeId, CancellationToken cancellationToken = default);
    Task<OrderDto?> GetOrderAsync(string cafeId, string orderId, CancellationToken cancellationToken = default);
    Task<OrderDto?> GetActiveOrderByTableAsync(string cafeId, string tableId, CancellationToken cancellationToken = default);
    Task<OrderServiceResult<OrderDto>> CreateOrderAsync(string cafeId, ITenantContext tenant, CreateOrderRequest request, CancellationToken cancellationToken = default);
    Task<OrderDto?> CreateGuestOrderAsync(
        string cafeId,
        CreateOrderRequest request,
        string? guestPhone,
        string? guestName,
        CancellationToken cancellationToken = default);
    Task<(GuestQrOrderPlacedDto? Data, string? ErrorCode, string? Message)> PlaceBranchGuestOrderAsync(
        string cafeId,
        string branchId,
        PlaceGuestOrderRequest request,
        CancellationToken cancellationToken = default);
    Task<OrderServiceResult<OrderDto>> AppendOrderItemsAsync(
        string cafeId,
        string orderId,
        AppendOrderItemsRequest request,
        CancellationToken cancellationToken = default);
    Task<OrderServiceResult<OrderDto>> UpdateOrderSessionAsync(
        string cafeId,
        string orderId,
        UpdateOrderSessionRequest request,
        CancellationToken cancellationToken = default);
    Task<OrderServiceResult<OrderDto>> VoidOrderItemAsync(
        string cafeId,
        string orderId,
        string itemId,
        string voidedByUserId,
        CancellationToken cancellationToken = default);
    Task<OrderServiceResult<OrderDto>> TransferTableAsync(
        string cafeId,
        string orderId,
        string targetTableId,
        CancellationToken cancellationToken = default);
    Task<OrderDto?> UpdateStatusAsync(string cafeId, string orderId, OrderStatus status, CancellationToken cancellationToken = default);
    Task<OrderServiceResult<OrderDto>> CancelOrderAsync(
        string cafeId,
        string orderId,
        string? reason,
        string? cancelledByEmployeeId,
        CancellationToken cancellationToken = default);
    Task<OrderServiceResult<IReadOnlyList<PaymentDto>>> RecordPaymentsAsync(
        string cafeId,
        string orderId,
        RecordPaymentsRequest request,
        string? userId,
        CancellationToken cancellationToken = default);
}

public class OrderService : IOrderService
{
    public static readonly OrderStatus[] LiveStatuses =
    [
        OrderStatus.Pending,
        OrderStatus.Confirmed,
        OrderStatus.Preparing,
        OrderStatus.Ready
    ];

    public static readonly OrderStatus[] OpenForPaymentStatuses =
    [
        OrderStatus.Pending,
        OrderStatus.Confirmed,
        OrderStatus.Preparing,
        OrderStatus.Ready
    ];

    private readonly AppDbContext _db;
    private readonly IKdsNotifier _kdsNotifier;
    private readonly ISnappfoodClient _snappfood;
    private readonly IDeliveryStatusSyncService _deliverySync;
    private readonly ILoyaltyService _loyalty;
    private readonly IShiftService _shiftService;
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly IOrderNotificationService _orderNotifications;
    private readonly IInventoryService _inventory;

    public OrderService(
        AppDbContext db,
        IKdsNotifier kdsNotifier,
        ISnappfoodClient snappfood,
        IDeliveryStatusSyncService deliverySync,
        IShiftService shiftService,
        IServiceScopeFactory scopeFactory,
        IOrderNotificationService orderNotifications,
        IInventoryService inventory,
        ILoyaltyService loyalty)
    {
        _db = db;
        _kdsNotifier = kdsNotifier;
        _snappfood = snappfood;
        _deliverySync = deliverySync;
        _shiftService = shiftService;
        _scopeFactory = scopeFactory;
        _orderNotifications = orderNotifications;
        _inventory = inventory;
        _loyalty = loyalty;
    }

    public async Task<IReadOnlyList<OrderDto>> GetOrdersAsync(string cafeId, OrderStatus? status, CancellationToken cancellationToken = default)
    {
        var query = ApplyOrderIncludes(_db.Orders.Where(o => o.CafeId == cafeId));

        if (status.HasValue)
            query = query.Where(o => o.Status == status.Value);

        var orders = await query.OrderByDescending(o => o.CreatedAt).ToListAsync(cancellationToken);
        return orders.Select(MapOrder).ToList();
    }

    public async Task<IReadOnlyList<OrderDto>> GetOpenOrdersAsync(
        string cafeId,
        string? search,
        CancellationToken cancellationToken = default)
    {
        var query = ApplyOrderIncludes(_db.Orders.Where(o => o.CafeId == cafeId))
            .Where(o => OpenForPaymentStatuses.Contains(o.Status));

        if (!string.IsNullOrWhiteSpace(search))
        {
            var term = search.Trim();
            query = query.Where(o =>
                o.Id.Contains(term)
                || (o.GuestName != null && o.GuestName.Contains(term))
                || (o.GuestPhone != null && o.GuestPhone.Contains(term))
                || (o.CustomerId != null && o.CustomerId == term)
                || (o.Customer != null && o.Customer.Phone.Contains(term))
                || (o.Table != null && o.Table.Number.Contains(term)));
        }

        var orders = await query.OrderByDescending(o => o.CreatedAt).ToListAsync(cancellationToken);
        return orders.Select(MapOrder).ToList();
    }

    public async Task<IReadOnlyList<LiveOrderDto>> GetLiveOrdersAsync(string cafeId, CancellationToken cancellationToken = default)
    {
        var orders = await _db.Orders
            .Include(o => o.Items)
            .ThenInclude(i => i.MenuItem)
            .Include(o => o.Table)
            .Where(o => o.CafeId == cafeId && LiveStatuses.Contains(o.Status))
            .OrderBy(o => o.CreatedAt)
            .ToListAsync(cancellationToken);

        return orders.Select(MapLiveOrder).ToList();
    }

    public async Task<OrderDto?> GetOrderAsync(string cafeId, string orderId, CancellationToken cancellationToken = default)
    {
        var order = await LoadOrderAsync(cafeId, orderId, cancellationToken);
        return order is null ? null : MapOrder(order);
    }

    public async Task<OrderDto?> GetActiveOrderByTableAsync(
        string cafeId,
        string tableId,
        CancellationToken cancellationToken = default)
    {
        var order = await FindOpenOrderForTableAsync(cafeId, tableId, cancellationToken);
        return order is null ? null : MapOrder(order);
    }

    public async Task<OrderDto?> CreateGuestOrderAsync(
        string cafeId,
        CreateOrderRequest request,
        string? guestPhone,
        string? guestName,
        CancellationToken cancellationToken = default)
    {
        var result = await CreateOrderCoreAsync(
            cafeId,
            request,
            employeeId: null,
            guestPhone,
            guestName,
            source: OrderSource.GuestQr,
            cancellationToken: cancellationToken);
        return result.Data;
    }

    public async Task<(GuestQrOrderPlacedDto? Data, string? ErrorCode, string? Message)> PlaceBranchGuestOrderAsync(
        string cafeId,
        string branchId,
        PlaceGuestOrderRequest request,
        CancellationToken cancellationToken = default)
    {
        if (request.Items.Count == 0)
            return (null, "VALIDATION_ERROR", "Order must include at least one item.");

        var table = await _db.Tables.FirstOrDefaultAsync(
            t => t.Id == request.TableId && t.CafeId == cafeId && t.BranchId == branchId && t.IsActive,
            cancellationToken);

        if (table is null)
            return (null, "TABLE_NOT_FOUND", "Table not found.");

        if (table.IsCleaning)
            return (null, "TABLE_CLEANING", "Table is being cleaned.");

        var branchOk = await _db.Branches.AnyAsync(
            b => b.Id == branchId && b.CafeId == cafeId && b.IsActive,
            cancellationToken);
        if (!branchOk)
            return (null, "BRANCH_NOT_FOUND", "Branch not found.");

        var menuItemIds = request.Items.Select(i => i.MenuItemId).Distinct().ToList();
        var menuItems = await _db.MenuItems
            .Where(m => m.CafeId == cafeId && menuItemIds.Contains(m.Id) && m.IsAvailable)
            .ToListAsync(cancellationToken);

        if (menuItems.Count != menuItemIds.Count)
            return (null, "INVALID_MENU_ITEMS", "One or more menu items are invalid.");

        var overrides = await _db.BranchMenuItemOverrides
            .Where(o => o.CafeId == cafeId && o.BranchId == branchId && menuItemIds.Contains(o.MenuItemId))
            .ToDictionaryAsync(o => o.MenuItemId, cancellationToken);

        foreach (var item in menuItems)
        {
            if (overrides.TryGetValue(item.Id, out var ov) && !ov.IsAvailable)
                return (null, "INVALID_MENU_ITEMS", "Item is not available at this branch.");
        }

        var priceByItem = menuItems.ToDictionary(
            m => m.Id,
            m =>
            {
                overrides.TryGetValue(m.Id, out var ov);
                return ov?.PriceOverride ?? m.Price;
            });

        var createRequest = new CreateOrderRequest(
            OrderType.DineIn,
            branchId,
            request.TableId,
            null,
            request.GuestName,
            request.GuestPhone,
            null,
            null,
            request.Items);

        var result = await CreateOrderCoreAsync(
            cafeId,
            createRequest,
            employeeId: null,
            request.GuestPhone,
            request.GuestName,
            source: OrderSource.GuestQr,
            unitPriceByMenuItemId: priceByItem,
            cancellationToken: cancellationToken);

        if (!result.Success || result.Data is null)
            return (null, result.ErrorCode ?? "ERROR", "Could not place order.");

        var itemCount = request.Items.Sum(i => i.Quantity);
        var orderNumber = ReceiptPrintFormatting.OrderNumberLabel(result.Data.DisplayNumber);
        var orderEntity = await _db.Orders
            .FirstOrDefaultAsync(o => o.Id == result.Data.Id && o.CafeId == cafeId, cancellationToken);
        if (orderEntity is null)
            return (null, "ORDER_NOT_FOUND", "Could not load placed order.");

        if (string.IsNullOrEmpty(orderEntity.GuestTrackingToken))
        {
            orderEntity.GuestTrackingToken = OrderTrackingHelper.NewTrackingToken();
            orderEntity.StatusUpdatedAt = DateTime.UtcNow;
            await _db.SaveChangesAsync(cancellationToken);
        }

        return (
            new GuestQrOrderPlacedDto(
                result.Data.Id,
                orderNumber,
                result.Data.Total,
                itemCount,
                result.Data.Status,
                orderEntity.GuestTrackingToken),
            null,
            null);
    }

    public async Task<OrderServiceResult<OrderDto>> CreateOrderAsync(
        string cafeId,
        ITenantContext tenant,
        CreateOrderRequest request,
        CancellationToken cancellationToken = default) =>
        await CreateOrderCoreAsync(
            cafeId,
            request,
            tenant.UserId,
            request.GuestPhone,
            request.GuestName,
            cancellationToken: cancellationToken);

    public async Task<OrderServiceResult<OrderDto>> AppendOrderItemsAsync(
        string cafeId,
        string orderId,
        AppendOrderItemsRequest request,
        CancellationToken cancellationToken = default)
    {
        await using var tx = await BeginTransactionIfSupportedAsync(cancellationToken);

        var order = await ApplyOrderIncludes(_db.Orders)
            .FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, cancellationToken);

        if (order is null)
            return new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_FOUND");

        if (!OpenForPaymentStatuses.Contains(order.Status))
            return new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_OPEN");

        var menuItemIds = request.Items.Select(i => i.MenuItemId).Distinct().ToList();
        var menuItems = await _db.MenuItems
            .Where(m => m.CafeId == cafeId && menuItemIds.Contains(m.Id) && m.IsAvailable)
            .ToListAsync(cancellationToken);

        if (menuItems.Count != menuItemIds.Count)
            return new OrderServiceResult<OrderDto>(false, null, "INVALID_ORDER");

        foreach (var line in request.Items)
        {
            var menuItem = menuItems.First(m => m.Id == line.MenuItemId);
            order.Items.Add(new OrderItem
            {
                OrderId = order.Id,
                MenuItemId = menuItem.Id,
                Quantity = line.Quantity,
                UnitPrice = menuItem.Price,
                Notes = line.Notes
            });
        }

        await RecalculateOrderTotalsAsync(order, cafeId, cancellationToken);
        await _db.SaveChangesAsync(cancellationToken);
        if (tx is not null)
            await tx.CommitAsync(cancellationToken);

        var loaded = await LoadOrderAsync(cafeId, order.Id, cancellationToken);
        if (loaded is not null)
        {
            await _kdsNotifier.NotifyOrderStatusChangedAsync(cafeId, order.Id, loaded.Status, cancellationToken);
            if (!string.IsNullOrEmpty(loaded.TableId))
                await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken);
            PrinterBackgroundJobs.QueueKitchenPrint(_scopeFactory, cafeId, order.Id);
        }

        return loaded is null
            ? new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_FOUND")
            : new OrderServiceResult<OrderDto>(true, MapOrder(loaded));
    }

    public async Task<OrderServiceResult<OrderDto>> UpdateOrderSessionAsync(
        string cafeId,
        string orderId,
        UpdateOrderSessionRequest request,
        CancellationToken cancellationToken = default)
    {
        var order = await _db.Orders
            .FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, cancellationToken);

        if (order is null)
            return new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_FOUND");

        if (!OpenForPaymentStatuses.Contains(order.Status))
            return new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_OPEN");

        if (request.GuestName is not null)
            order.GuestName = string.IsNullOrWhiteSpace(request.GuestName) ? null : request.GuestName.Trim();

        if (request.GuestPhone is not null)
            order.GuestPhone = NormalizeGuestPhone(request.GuestPhone);

        if (request.CustomerId is not null)
        {
            if (string.IsNullOrEmpty(request.CustomerId))
            {
                order.CustomerId = null;
            }
            else
            {
                var exists = await _db.Customers.AnyAsync(
                    c => c.Id == request.CustomerId && c.CafeId == cafeId,
                    cancellationToken);
                if (!exists)
                    return new OrderServiceResult<OrderDto>(false, null, "INVALID_ORDER", "customerId");
                order.CustomerId = request.CustomerId;
            }
        }

        if (!string.IsNullOrEmpty(order.GuestPhone) && string.IsNullOrEmpty(order.CustomerId))
            order.CustomerId = await ResolveCustomerIdFromPhoneAsync(
                cafeId,
                order.GuestPhone,
                order.GuestName,
                cancellationToken);

        await _db.SaveChangesAsync(cancellationToken);

        var loaded = await LoadOrderAsync(cafeId, orderId, cancellationToken);
        return loaded is null
            ? new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_FOUND")
            : new OrderServiceResult<OrderDto>(true, MapOrder(loaded));
    }

    public async Task<OrderServiceResult<OrderDto>> VoidOrderItemAsync(
        string cafeId,
        string orderId,
        string itemId,
        string voidedByUserId,
        CancellationToken cancellationToken = default)
    {
        var order = await _db.Orders
            .Include(o => o.Items)
            .FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, cancellationToken);

        if (order is null)
            return new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_FOUND");

        if (order.Status is OrderStatus.Delivered or OrderStatus.Cancelled)
            return new OrderServiceResult<OrderDto>(false, null, "ORDER_ALREADY_CLOSED");

        if (!OpenForPaymentStatuses.Contains(order.Status))
            return new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_OPEN");

        var item = order.Items.FirstOrDefault(i => i.Id == itemId);
        if (item is null)
            return new OrderServiceResult<OrderDto>(false, null, "ITEM_NOT_FOUND");

        if (item.IsVoided)
            return new OrderServiceResult<OrderDto>(false, null, "ITEM_ALREADY_VOIDED");

        item.IsVoided = true;
        item.VoidedAt = DateTime.UtcNow;
        item.VoidedByUserId = voidedByUserId;

        await RecalculateOrderTotalsAsync(order, cafeId, cancellationToken);
        await _db.SaveChangesAsync(cancellationToken);

        var loaded = await LoadOrderAsync(cafeId, orderId, cancellationToken);
        if (loaded is not null && !string.IsNullOrEmpty(loaded.TableId))
            await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken);

        return loaded is null
            ? new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_FOUND")
            : new OrderServiceResult<OrderDto>(true, MapOrder(loaded));
    }

    public async Task<OrderServiceResult<OrderDto>> TransferTableAsync(
        string cafeId,
        string orderId,
        string targetTableId,
        CancellationToken cancellationToken = default)
    {
        var order = await _db.Orders
            .FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, cancellationToken);

        if (order is null)
            return new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_FOUND");

        if (order.Status is OrderStatus.Delivered or OrderStatus.Cancelled)
            return new OrderServiceResult<OrderDto>(false, null, "ORDER_ALREADY_CLOSED");

        if (!OpenForPaymentStatuses.Contains(order.Status))
            return new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_OPEN");

        var targetTable = await _db.Tables
            .FirstOrDefaultAsync(t => t.Id == targetTableId && t.CafeId == cafeId, cancellationToken);

        if (targetTable is null)
            return new OrderServiceResult<OrderDto>(false, null, "TABLE_NOT_FOUND");

        if (targetTable.IsCleaning)
            return new OrderServiceResult<OrderDto>(false, null, "TABLE_CLEANING", "targetTableId");

        var targetOccupied = await _db.Orders.AnyAsync(
            o => o.TableId == targetTableId
                && o.CafeId == cafeId
                && OpenForPaymentStatuses.Contains(o.Status)
                && o.Id != orderId,
            cancellationToken);

        if (targetOccupied)
            return new OrderServiceResult<OrderDto>(false, null, "TABLE_OCCUPIED", "targetTableId");

        order.TableId = targetTableId;
        await _db.SaveChangesAsync(cancellationToken);

        await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken);

        var loaded = await LoadOrderAsync(cafeId, orderId, cancellationToken);
        return loaded is null
            ? new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_FOUND")
            : new OrderServiceResult<OrderDto>(true, MapOrder(loaded));
    }

    private async Task<OrderServiceResult<OrderDto>> CreateOrderCoreAsync(
        string cafeId,
        CreateOrderRequest request,
        string? employeeId,
        string? guestPhone,
        string? guestName,
        OrderSource source = OrderSource.Pos,
        IReadOnlyDictionary<string, decimal>? unitPriceByMenuItemId = null,
        CancellationToken cancellationToken = default)
    {
        if (request.Items.Count == 0)
            return new OrderServiceResult<OrderDto>(false, null, "INVALID_ORDER");

        await using var tx = await BeginTransactionIfSupportedAsync(cancellationToken);

        var tableId = request.TableId;
        TableReservation? reservation = null;

        if (!string.IsNullOrEmpty(request.ReservationId))
        {
            reservation = await _db.TableReservations
                .Include(r => r.Table)
                .FirstOrDefaultAsync(
                    r => r.Id == request.ReservationId && r.CafeId == cafeId,
                    cancellationToken);

            if (reservation is null
                || reservation.Status is ReservationStatus.Cancelled
                || reservation.Status is ReservationStatus.Completed)
                return new OrderServiceResult<OrderDto>(false, null, "INVALID_ORDER");

            if (string.IsNullOrEmpty(tableId) && !string.IsNullOrEmpty(reservation.TableId))
                tableId = reservation.TableId;

            if (string.IsNullOrWhiteSpace(guestPhone))
                guestPhone = reservation.GuestPhone;
            if (string.IsNullOrWhiteSpace(guestName))
                guestName = reservation.GuestName;

            reservation.Status = ReservationStatus.Seated;
        }

        if (!string.IsNullOrEmpty(tableId))
        {
            var table = await _db.Tables.FirstOrDefaultAsync(
                t => t.Id == tableId && t.CafeId == cafeId,
                cancellationToken);

            if (table is null)
                return new OrderServiceResult<OrderDto>(false, null, "INVALID_ORDER", "tableId");

            if (table.IsCleaning)
                return new OrderServiceResult<OrderDto>(false, null, "TABLE_NOT_AVAILABLE", "tableId");

            var existing = await FindOpenOrderForTableAsync(cafeId, tableId, cancellationToken);
            if (existing is not null)
            {
                if (source == OrderSource.GuestQr)
                    existing.Source = OrderSource.GuestQr;
                await AppendLinesToOrderAsync(
                    existing,
                    request.Items,
                    cafeId,
                    unitPriceByMenuItemId,
                    cancellationToken);
                ApplyGuestFields(existing, request, guestPhone, guestName);
                if (!string.IsNullOrEmpty(request.CustomerId))
                    existing.CustomerId = request.CustomerId;
                else if (!string.IsNullOrWhiteSpace(guestPhone))
                    existing.CustomerId = await ResolveCustomerIdFromPhoneAsync(
                        cafeId,
                        guestPhone.Trim(),
                        existing.GuestName ?? guestName,
                        cancellationToken);

                await RecalculateOrderTotalsAsync(existing, cafeId, cancellationToken);
                await _db.SaveChangesAsync(cancellationToken);

                await _inventory.DeductForOrderAsync(
                    cafeId,
                    existing.Id,
                    request.Items.Select(i => (i.MenuItemId, i.Quantity)).ToList(),
                    cancellationToken);

                if (tx is not null)
                    await tx.CommitAsync(cancellationToken);

                var merged = await LoadOrderAsync(cafeId, existing.Id, cancellationToken);
                if (merged is not null)
                {
                    if (source == OrderSource.GuestQr && string.IsNullOrEmpty(merged.GuestTrackingToken))
                    {
                        merged.GuestTrackingToken = OrderTrackingHelper.NewTrackingToken();
                        merged.StatusUpdatedAt = DateTime.UtcNow;
                        await _db.SaveChangesAsync(cancellationToken);
                    }

                    await _kdsNotifier.NotifyOrderStatusChangedAsync(cafeId, merged.Id, merged.Status, cancellationToken);
                    await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken);
                    if (source == OrderSource.GuestQr)
                    {
                        var entity = await LoadOrderAsync(cafeId, merged.Id, cancellationToken);
                        if (entity is not null)
                        {
                            await _orderNotifications.NotifyGuestOrderPlacedAsync(
                                entity, MapLiveOrder(entity), cancellationToken);
                        }
                    }
                }

                return merged is null
                    ? new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_FOUND")
                    : new OrderServiceResult<OrderDto>(true, MapOrder(merged));
            }
        }

        var createResult = await CreateNewOrderAsync(
            cafeId,
            request,
            employeeId,
            guestPhone,
            guestName,
            tableId,
            reservation,
            source,
            unitPriceByMenuItemId,
            cancellationToken);

        if (!createResult.Success)
            return createResult;

        if (tx is not null)
            await tx.CommitAsync(cancellationToken);
        return createResult;
    }

    private async Task<IDbContextTransaction?> BeginTransactionIfSupportedAsync(CancellationToken cancellationToken)
    {
        if (!_db.Database.IsRelational())
            return null;
        return await _db.Database.BeginTransactionAsync(cancellationToken);
    }

    private async Task<OrderServiceResult<OrderDto>> CreateNewOrderAsync(
        string cafeId,
        CreateOrderRequest request,
        string? employeeId,
        string? guestPhone,
        string? guestName,
        string? tableId,
        TableReservation? reservation,
        OrderSource source,
        IReadOnlyDictionary<string, decimal>? unitPriceByMenuItemId,
        CancellationToken cancellationToken)
    {
        var menuItemIds = request.Items.Select(i => i.MenuItemId).Distinct().ToList();
        var menuItems = await _db.MenuItems
            .Include(m => m.Category)
            .Where(m => m.CafeId == cafeId && menuItemIds.Contains(m.Id) && m.IsAvailable)
            .ToListAsync(cancellationToken);

        if (menuItems.Count != menuItemIds.Count)
            return new OrderServiceResult<OrderDto>(false, null, "INVALID_ORDER");

        string? orderBranchId = request.BranchId;
        if (!string.IsNullOrEmpty(orderBranchId))
        {
            var branchOk = await _db.Branches.AnyAsync(
                b => b.Id == orderBranchId && b.CafeId == cafeId,
                cancellationToken);
            if (!branchOk)
                orderBranchId = null;
        }

        var orderGuestName = string.IsNullOrWhiteSpace(request.GuestName)
            ? null
            : request.GuestName.Trim();
        if (orderGuestName is null && !string.IsNullOrWhiteSpace(guestName))
            orderGuestName = guestName.Trim();

        var orderGuestPhone = NormalizeGuestPhone(request.GuestPhone);
        if (orderGuestPhone is null)
            orderGuestPhone = NormalizeGuestPhone(guestPhone);

        var orderItems = new List<OrderItem>();
        foreach (var line in request.Items)
        {
            var menuItem = menuItems.First(m => m.Id == line.MenuItemId);
            var unitPrice = unitPriceByMenuItemId?.GetValueOrDefault(menuItem.Id) ?? menuItem.Price;
            orderItems.Add(new OrderItem
            {
                MenuItemId = menuItem.Id,
                Quantity = line.Quantity,
                UnitPrice = unitPrice,
                Notes = line.Notes
            });
        }

        string? customerId = request.CustomerId;
        if (string.IsNullOrEmpty(customerId) && !string.IsNullOrWhiteSpace(orderGuestPhone))
            customerId = await ResolveCustomerIdFromPhoneAsync(
                cafeId,
                orderGuestPhone,
                orderGuestName ?? guestName,
                cancellationToken);

        if (string.IsNullOrEmpty(orderBranchId) && !string.IsNullOrEmpty(tableId))
        {
            var tableBranch = await _db.Tables
                .Where(t => t.Id == tableId && t.CafeId == cafeId)
                .Select(t => t.BranchId)
                .FirstOrDefaultAsync(cancellationToken);
            if (!string.IsNullOrEmpty(tableBranch))
                orderBranchId = tableBranch;
        }

        var displayNumber = await AllocateDisplayNumberAsync(cafeId, cancellationToken);
        var order = new Order
        {
            CafeId = cafeId,
            BranchId = orderBranchId,
            TableId = tableId,
            ReservationId = request.ReservationId,
            GuestName = orderGuestName,
            GuestPhone = orderGuestPhone,
            CustomerId = customerId,
            EmployeeId = employeeId,
            OrderType = request.OrderType,
            Source = source,
            Status = OrderStatus.Pending,
            DisplayNumber = displayNumber,
            StatusUpdatedAt = DateTime.UtcNow,
            GuestTrackingToken = source == OrderSource.GuestQr ? OrderTrackingHelper.NewTrackingToken() : null,
            CouponId = request.CouponId,
            Items = orderItems
        };

        _db.Orders.Add(order);
        await RecalculateOrderTotalsAsync(order, cafeId, cancellationToken);
        await _db.SaveChangesAsync(cancellationToken);

        await _inventory.DeductForOrderAsync(
            cafeId,
            order.Id,
            orderItems.Select(i => (i.MenuItemId, i.Quantity)).ToList(),
            cancellationToken);

        var loaded = await LoadOrderAsync(cafeId, order.Id, cancellationToken);
        if (loaded is not null)
        {
            await _kdsNotifier.NotifyOrderCreatedAsync(cafeId, MapLiveOrder(loaded), cancellationToken);
            if (!string.IsNullOrEmpty(loaded.TableId))
                await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken);
            if (source == OrderSource.GuestQr)
                await _orderNotifications.NotifyGuestOrderPlacedAsync(loaded, MapLiveOrder(loaded), cancellationToken);
        }

        return loaded is null
            ? new OrderServiceResult<OrderDto>(false, null, "INVALID_ORDER")
            : new OrderServiceResult<OrderDto>(true, MapOrder(loaded));
    }

    private static void ApplyGuestFields(
        Order order,
        CreateOrderRequest request,
        string? guestPhone,
        string? guestName)
    {
        if (!string.IsNullOrWhiteSpace(request.GuestName))
            order.GuestName = request.GuestName.Trim();
        else if (!string.IsNullOrWhiteSpace(guestName))
            order.GuestName = guestName.Trim();

        var phone = NormalizeGuestPhone(request.GuestPhone) ?? NormalizeGuestPhone(guestPhone);
        if (phone is not null)
            order.GuestPhone = phone;
    }

    private static string? NormalizeGuestPhone(string? phone)
    {
        if (string.IsNullOrWhiteSpace(phone)) return null;
        var normalized = PhoneNormalizer.Normalize(phone);
        return PhoneNormalizer.IsValidIranMobile(normalized) ? normalized : null;
    }

    private async Task AppendLinesToOrderAsync(
        Order order,
        IReadOnlyList<CreateOrderItemRequest> lines,
        string cafeId,
        IReadOnlyDictionary<string, decimal>? unitPriceByMenuItemId,
        CancellationToken cancellationToken)
    {
        var menuItemIds = lines.Select(i => i.MenuItemId).Distinct().ToList();
        var menuItems = await _db.MenuItems
            .Where(m => m.CafeId == cafeId && menuItemIds.Contains(m.Id) && m.IsAvailable)
            .ToListAsync(cancellationToken);

        if (menuItems.Count != menuItemIds.Count)
            throw new InvalidOperationException("Invalid menu items.");

        foreach (var line in lines)
        {
            var menuItem = menuItems.First(m => m.Id == line.MenuItemId);
            var unitPrice = unitPriceByMenuItemId?.GetValueOrDefault(menuItem.Id) ?? menuItem.Price;
            order.Items.Add(new OrderItem
            {
                OrderId = order.Id,
                MenuItemId = menuItem.Id,
                Quantity = line.Quantity,
                UnitPrice = unitPrice,
                Notes = line.Notes
            });
        }
    }

    private async Task<string?> ResolveCustomerIdFromPhoneAsync(
        string cafeId,
        string phone,
        string? guestName,
        CancellationToken cancellationToken)
    {
        var existing = await _db.Customers.FirstOrDefaultAsync(
            c => c.CafeId == cafeId && c.Phone == phone,
            cancellationToken);

        if (existing is not null)
            return existing.Id;

        if (string.IsNullOrWhiteSpace(guestName))
            return null;

        var customer = new Customer
        {
            CafeId = cafeId,
            Name = guestName.Trim(),
            Phone = phone,
            Group = CustomerGroup.New
        };
        _db.Customers.Add(customer);
        await _db.SaveChangesAsync(cancellationToken);
        return customer.Id;
    }

    private async Task<decimal> ResolveEffectiveTaxRateAsync(
        string cafeId,
        string? branchId,
        CancellationToken cancellationToken)
    {
        var cafe = await _db.Cafes.AsNoTracking()
            .FirstOrDefaultAsync(c => c.Id == cafeId, cancellationToken);

        if (cafe is not null && cafe.DefaultTaxRate > 0)
        {
            if (!string.IsNullOrEmpty(branchId) && cafe.AllowBranchTaxOverride)
            {
                var branchRate = await _db.Branches.AsNoTracking()
                    .Where(b => b.Id == branchId && b.CafeId == cafeId)
                    .Select(b => b.TaxRate)
                    .FirstOrDefaultAsync(cancellationToken);
                if (branchRate is > 0)
                    return branchRate.Value;
            }

            return cafe.DefaultTaxRate;
        }

        var taxTableRate = await _db.Taxes
            .Where(t => t.CafeId == cafeId && t.IsDefault)
            .Select(t => t.Rate)
            .FirstOrDefaultAsync(cancellationToken);

        return taxTableRate > 0 ? taxTableRate : 9m;
    }

    private async Task RecalculateOrderTotalsAsync(
        Order order,
        string cafeId,
        CancellationToken cancellationToken)
    {
        await _db.Entry(order).Collection(o => o.Items).LoadAsync(cancellationToken);

        var subtotal = order.Items.Where(i => !i.IsVoided).Sum(i => i.UnitPrice * i.Quantity);
        var discountAmount = 0m;

        if (!string.IsNullOrEmpty(order.CouponId))
        {
            var coupon = await _db.Coupons.FirstOrDefaultAsync(
                c => c.Id == order.CouponId && c.CafeId == cafeId && c.IsActive,
                cancellationToken);

            if (coupon is not null
                && coupon.DeletedAt is null
                && (coupon.UsageLimit is null || coupon.UsedCount < coupon.UsageLimit)
                && (coupon.StartsAt is null || coupon.StartsAt <= DateTime.UtcNow)
                && (coupon.ExpiresAt is null || coupon.ExpiresAt >= DateTime.UtcNow)
                && (coupon.MinOrderAmount is null || subtotal >= coupon.MinOrderAmount))
            {
                discountAmount = CouponService.CalculateDiscount(coupon, subtotal);
            }
        }

        var defaultTaxRate = await ResolveEffectiveTaxRateAsync(cafeId, order.BranchId, cancellationToken);

        var taxable = subtotal - discountAmount;
        var taxTotal = Math.Round(taxable * defaultTaxRate / 100m, 0);

        order.Subtotal = subtotal;
        order.DiscountAmount = discountAmount;
        order.TaxTotal = taxTotal;
        order.Total = taxable + taxTotal;
    }

    private async Task<Order?> FindOpenOrderForTableAsync(
        string cafeId,
        string tableId,
        CancellationToken cancellationToken) =>
        await ApplyOrderIncludes(_db.Orders)
            .Where(o => o.CafeId == cafeId
                && o.TableId == tableId
                && OpenForPaymentStatuses.Contains(o.Status))
            .OrderBy(o => o.CreatedAt)
            .FirstOrDefaultAsync(cancellationToken);

    public async Task<OrderDto?> UpdateStatusAsync(string cafeId, string orderId, OrderStatus status, CancellationToken cancellationToken = default)
    {
        var order = await _db.Orders.FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, cancellationToken);
        if (order is null) return null;

        if (status == OrderStatus.Cancelled && !OpenForPaymentStatuses.Contains(order.Status))
            return null;

        order.Status = status;
        order.StatusUpdatedAt = DateTime.UtcNow;
        await _db.SaveChangesAsync(cancellationToken);

        await _kdsNotifier.NotifyOrderStatusChangedAsync(cafeId, orderId, status, cancellationToken);
        if (!string.IsNullOrEmpty(order.TableId))
            await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken);

        await _deliverySync.SyncInternalStatusAsync(cafeId, orderId, status, cancellationToken);

        var loaded = await LoadOrderAsync(cafeId, orderId, cancellationToken);
        if (loaded is not null)
            await _orderNotifications.NotifyOrderStatusChangedAsync(loaded, cancellationToken);

        return await GetOrderAsync(cafeId, orderId, cancellationToken);
    }

    public async Task<OrderServiceResult<OrderDto>> CancelOrderAsync(
        string cafeId,
        string orderId,
        string? reason,
        string? cancelledByEmployeeId,
        CancellationToken cancellationToken = default)
    {
        var order = await _db.Orders
            .Include(o => o.Payments)
            .FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, cancellationToken);

        if (order is null)
            return new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_FOUND");

        if (order.Status == OrderStatus.Cancelled)
            return new OrderServiceResult<OrderDto>(false, null, "ORDER_ALREADY_CANCELLED");

        if (!OpenForPaymentStatuses.Contains(order.Status))
            return new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_OPEN");

        // A paid order must be refunded through the payment flow first — cancelling it
        // here would silently strip the recorded money. Block and surface the reason.
        if (order.Payments.Any(p => p.DeletedAt == null))
            return new OrderServiceResult<OrderDto>(false, null, "ORDER_HAS_PAYMENTS");

        order.Status = OrderStatus.Cancelled;
        order.StatusUpdatedAt = DateTime.UtcNow;
        order.CancelledAt = DateTime.UtcNow;
        order.CancelReason = string.IsNullOrWhiteSpace(reason) ? null : reason.Trim();
        order.CancelledByEmployeeId = cancelledByEmployeeId;
        await _db.SaveChangesAsync(cancellationToken);

        await _kdsNotifier.NotifyOrderStatusChangedAsync(cafeId, orderId, OrderStatus.Cancelled, cancellationToken);
        if (!string.IsNullOrEmpty(order.TableId))
            await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken);

        await _deliverySync.SyncInternalStatusAsync(cafeId, orderId, OrderStatus.Cancelled, cancellationToken);

        var loaded = await LoadOrderAsync(cafeId, orderId, cancellationToken);
        if (loaded is not null)
            await _orderNotifications.NotifyOrderStatusChangedAsync(loaded, cancellationToken);

        return loaded is null
            ? new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_FOUND")
            : new OrderServiceResult<OrderDto>(true, MapOrder(loaded));
    }

    public async Task<OrderServiceResult<IReadOnlyList<PaymentDto>>> RecordPaymentsAsync(
        string cafeId,
        string orderId,
        RecordPaymentsRequest request,
        string? userId,
        CancellationToken cancellationToken = default)
    {
        var order = await _db.Orders
            .Include(o => o.Payments)
            .FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, cancellationToken);

        if (order is null)
            return new OrderServiceResult<IReadOnlyList<PaymentDto>>(false, null, "ORDER_NOT_FOUND");

        var branchId = await ResolveOrderBranchIdAsync(order, cafeId, cancellationToken);
        if (string.IsNullOrEmpty(branchId))
            return new OrderServiceResult<IReadOnlyList<PaymentDto>>(false, null, "NO_OPEN_SHIFT", "branchId");

        if (string.IsNullOrEmpty(order.BranchId))
        {
            order.BranchId = branchId;
        }

        var shiftCheck = await _shiftService.RequireOpenShiftForBranchAsync(
            cafeId, branchId, cancellationToken);
        if (!shiftCheck.Success)
            return new OrderServiceResult<IReadOnlyList<PaymentDto>>(false, null, shiftCheck.ErrorCode, shiftCheck.Field);

        if (request.LoyaltyPointsToRedeem is > 0)
        {
            var redeem = await _loyalty.RedeemOnOrderAsync(
                cafeId, order, request.LoyaltyPointsToRedeem.Value, cancellationToken);
            if (!redeem.Success)
            {
                return new OrderServiceResult<IReadOnlyList<PaymentDto>>(
                    false, null, redeem.ErrorCode ?? "LOYALTY_REDEEM_FAILED");
            }

            if (redeem.Data is { DiscountToman: > 0 })
            {
                order.DiscountAmount += redeem.Data.DiscountToman;
                var taxRate = await ResolveEffectiveTaxRateAsync(cafeId, order.BranchId, cancellationToken);
                var taxable = Math.Max(0, order.Subtotal - order.DiscountAmount);
                order.TaxTotal = Math.Round(taxable * taxRate / 100m, 0);
                order.Total = taxable + order.TaxTotal;
            }
        }

        var openShift = shiftCheck.Data!;
        var createdBy = userId ?? order.EmployeeId ?? openShift.OpenedByUserId;

        var payments = request.Payments.Select(p => new Payment
        {
            OrderId = orderId,
            Method = p.Method,
            Amount = p.Amount,
            Reference = p.Reference,
            Status = PaymentStatus.Completed
        }).ToList();

        _db.Payments.AddRange(payments);

        var paidTotal = order.Payments.Where(p => p.Status == PaymentStatus.Completed).Sum(p => p.Amount)
                        + payments.Sum(p => p.Amount);

        if (paidTotal >= order.Total)
        {
            order.Status = OrderStatus.Delivered;

            if (!string.IsNullOrEmpty(order.ReservationId))
            {
                var reservation = await _db.TableReservations.FirstOrDefaultAsync(
                    r => r.Id == order.ReservationId && r.CafeId == cafeId,
                    cancellationToken);
                if (reservation is not null)
                    reservation.Status = ReservationStatus.Completed;
            }
        }

        await _db.SaveChangesAsync(cancellationToken);

        foreach (var payment in payments)
        {
            await _shiftService.RecordTransactionAsync(
                cafeId,
                openShift.Id,
                CashTransactionType.OrderPayment,
                payment.Method,
                payment.Amount,
                createdBy,
                orderId,
                null,
                cancellationToken);
        }

        if (!string.IsNullOrEmpty(order.TableId))
            await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken);

        if (paidTotal >= order.Total)
        {
            PrinterBackgroundJobs.QueueReceiptPrint(_scopeFactory, cafeId, orderId);
            await _loyalty.ApplyEarnOnOrderPaidAsync(cafeId, order.CustomerId, paidTotal, cancellationToken);
            await _deliverySync.SyncInternalStatusAsync(cafeId, orderId, OrderStatus.Delivered, cancellationToken);
        }

        var dtos = payments.Select(p => new PaymentDto(p.Id, p.Method, p.Amount, p.Status, p.Reference)).ToList();
        return new OrderServiceResult<IReadOnlyList<PaymentDto>>(true, dtos);
    }

    private static IQueryable<Order> ApplyOrderIncludes(IQueryable<Order> query) =>
        query
            .Include(o => o.Items)
            .ThenInclude(i => i.MenuItem)
            .Include(o => o.Table)
            .Include(o => o.Customer)
            .Include(o => o.Reservation)
            .Include(o => o.Payments);

    private async Task<Order?> LoadOrderAsync(string cafeId, string orderId, CancellationToken cancellationToken) =>
        await ApplyOrderIncludes(_db.Orders)
            .FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, cancellationToken);

    private async Task<string?> ResolveOrderBranchIdAsync(
        Order order,
        string cafeId,
        CancellationToken cancellationToken)
    {
        if (!string.IsNullOrEmpty(order.BranchId))
            return order.BranchId;

        if (!string.IsNullOrEmpty(order.TableId))
        {
            var tableBranchId = await _db.Tables
                .Where(t => t.Id == order.TableId && t.CafeId == cafeId)
                .Select(t => t.BranchId)
                .FirstOrDefaultAsync(cancellationToken);
            if (!string.IsNullOrEmpty(tableBranchId))
                return tableBranchId;
        }

        return await _db.Branches
            .Where(b => b.CafeId == cafeId && b.IsActive)
            .OrderBy(b => b.CreatedAt)
            .Select(b => b.Id)
            .FirstOrDefaultAsync(cancellationToken);
    }

    private async Task<int> AllocateDisplayNumberAsync(string cafeId, CancellationToken cancellationToken)
    {
        var max = await _db.Orders
            .Where(o => o.CafeId == cafeId)
            .MaxAsync(o => (int?)o.DisplayNumber, cancellationToken);
        return (max ?? 0) + 1;
    }

    private static OrderDto MapOrder(Order o)
    {
        var paid = o.Payments
            .Where(p => p.Status == PaymentStatus.Completed)
            .Sum(p => p.Amount);

        return new OrderDto(
            o.Id,
            o.CafeId,
            o.BranchId,
            o.TableId,
            o.Table?.Number,
            o.GuestName,
            o.GuestPhone,
            o.Customer?.Name,
            o.Customer?.Phone,
            o.CustomerId,
            o.EmployeeId,
            o.OrderType,
            o.Source,
            o.Status,
            o.Subtotal,
            o.TaxTotal,
            o.DiscountAmount,
            o.Total,
            paid,
            o.CreatedAt,
            o.DisplayNumber > 0 ? o.DisplayNumber : ReceiptPrintFormatting.StableDisplayNumberFromId(o.Id),
            o.Items.Select(i => new OrderItemDto(
                i.Id,
                i.MenuItemId,
                i.MenuItem?.Name ?? "",
                i.Quantity,
                i.UnitPrice,
                i.Notes,
                i.IsVoided,
                i.VoidedAt)).ToList(),
            o.Payments.Select(p => new PaymentDto(p.Id, p.Method, p.Amount, p.Status, p.Reference)).ToList());
    }

    private static LiveOrderDto MapLiveOrder(Order o) => new(
        o.Id,
        o.DisplayNumber > 0 ? o.DisplayNumber : ReceiptPrintFormatting.StableDisplayNumberFromId(o.Id),
        o.Status,
        o.Table?.Number,
        o.OrderType,
        o.Total,
        o.CreatedAt,
        o.Items.Select(i => new OrderItemDto(
            i.Id,
            i.MenuItemId,
            i.MenuItem?.Name ?? "",
            i.Quantity,
            i.UnitPrice,
            i.Notes,
            i.IsVoided,
            i.VoidedAt)).ToList());
}
