using System.Security.Cryptography;
using System.Text;
using Meezi.API.Models.Orders;
using Meezi.API.Services.Printing;
using Meezi.API.Models.Snappfood;
using Meezi.Core.Entities;
using Meezi.Core.Enums;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace Meezi.API.Services;

public interface ISnappfoodWebhookService
{
    bool VerifySignature(string rawBody, string? signatureHeader);
    Task<(bool Success, string? Error)> ProcessOrderAsync(SnappfoodWebhookOrder order, CancellationToken cancellationToken = default);
}

public class SnappfoodWebhookService : ISnappfoodWebhookService
{
    private readonly AppDbContext _db;
    private readonly IKdsNotifier _kdsNotifier;
    private readonly IConfiguration _configuration;
    private readonly ILogger<SnappfoodWebhookService> _logger;

    public SnappfoodWebhookService(
        AppDbContext db,
        IKdsNotifier kdsNotifier,
        IConfiguration configuration,
        ILogger<SnappfoodWebhookService> logger)
    {
        _db = db;
        _kdsNotifier = kdsNotifier;
        _configuration = configuration;
        _logger = logger;
    }

    public bool VerifySignature(string rawBody, string? signatureHeader)
    {
        var secret = _configuration["Snappfood:WebhookSecret"];
        if (string.IsNullOrWhiteSpace(secret))
            return true;

        if (string.IsNullOrWhiteSpace(signatureHeader))
            return false;

        var expected = ComputeHmac(rawBody, secret);
        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(expected),
            Encoding.UTF8.GetBytes(signatureHeader.Trim()));
    }

    public async Task<(bool Success, string? Error)> ProcessOrderAsync(
        SnappfoodWebhookOrder order,
        CancellationToken cancellationToken = default)
    {
        var cafe = await _db.Cafes
            .FirstOrDefaultAsync(c => c.SnappfoodVendorId == order.VendorId, cancellationToken);

        if (cafe is null)
            return (false, "Unknown vendor.");

        var existing = await _db.Orders
            .AnyAsync(o => o.CafeId == cafe.Id && o.SnappfoodOrderId == order.OrderId, cancellationToken);
        if (existing)
            return (true, null);

        var menuItems = await _db.MenuItems
            .Where(m => m.CafeId == cafe.Id && m.IsAvailable)
            .ToListAsync(cancellationToken);

        var orderItems = new List<OrderItem>();
        decimal subtotal = 0;

        foreach (var item in order.Items)
        {
            var menuItem = menuItems.FirstOrDefault(m =>
                m.Name.Equals(item.Name, StringComparison.OrdinalIgnoreCase) ||
                (m.NameEn != null && m.NameEn.Equals(item.Name, StringComparison.OrdinalIgnoreCase)));

            if (menuItem is null)
            {
                _logger.LogWarning("Snappfood item {Name} not matched for cafe {CafeId}", item.Name, cafe.Id);
                continue;
            }

            var lineTotal = item.UnitPrice * item.Quantity;
            subtotal += lineTotal;
            orderItems.Add(new OrderItem
            {
                MenuItemId = menuItem.Id,
                Quantity = item.Quantity,
                UnitPrice = item.UnitPrice,
                Notes = "Snappfood"
            });
        }

        if (orderItems.Count == 0)
            return (false, "No menu items matched.");

        var taxRate = await _db.Taxes
            .Where(t => t.CafeId == cafe.Id && t.IsDefault)
            .Select(t => t.Rate)
            .FirstOrDefaultAsync(cancellationToken);

        var taxTotal = Math.Round(subtotal * taxRate / 100m, 0);
        var total = order.Total > 0 ? order.Total : subtotal + taxTotal;

        var displayNumber = await AllocateDisplayNumberAsync(cafe.Id, cancellationToken);
        var meeziOrder = new Order
        {
            CafeId = cafe.Id,
            OrderType = OrderType.Delivery,
            Status = OrderStatus.Confirmed,
            DisplayNumber = displayNumber,
            SnappfoodOrderId = order.OrderId,
            Subtotal = subtotal,
            TaxTotal = taxTotal,
            Total = total,
            Items = orderItems
        };

        if (!string.IsNullOrWhiteSpace(order.CustomerPhone))
        {
            var customer = await _db.Customers
                .FirstOrDefaultAsync(c => c.CafeId == cafe.Id && c.Phone == order.CustomerPhone, cancellationToken);
            if (customer is null)
            {
                customer = new Customer
                {
                    CafeId = cafe.Id,
                    Name = order.CustomerName ?? "Snappfood",
                    Phone = order.CustomerPhone,
                    Group = CustomerGroup.New
                };
                _db.Customers.Add(customer);
                await _db.SaveChangesAsync(cancellationToken);
            }

            meeziOrder.CustomerId = customer.Id;
        }

        _db.Orders.Add(meeziOrder);
        await _db.SaveChangesAsync(cancellationToken);

        var loaded = await _db.Orders
            .Include(o => o.Items)
            .ThenInclude(i => i.MenuItem)
            .Include(o => o.Table)
            .FirstAsync(o => o.Id == meeziOrder.Id, cancellationToken);

        await _kdsNotifier.NotifyOrderCreatedAsync(cafe.Id, MapLive(loaded), cancellationToken);
        return (true, null);
    }

    private static string ComputeHmac(string body, string secret)
    {
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(body));
        return Convert.ToHexString(hash).ToLowerInvariant();
    }


    private async Task<int> AllocateDisplayNumberAsync(string cafeId, CancellationToken ct)
    {
        var max = await _db.Orders
            .Where(o => o.CafeId == cafeId)
            .MaxAsync(o => (int?)o.DisplayNumber, ct);
        return (max ?? 0) + 1;
    }    private static LiveOrderDto MapLive(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());
}
