using System.Net.Sockets;
using Meezi.API.Models.Orders;
using Meezi.API.Models.Printing;
using Meezi.Core.Entities;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace Meezi.API.Services.Printing;

public record PrintResult(bool Success, string? ErrorCode, string? ErrorDetail = null)
{
    public static PrintResult Ok() => new(true, null);
    public static PrintResult Fail(string code, string? detail = null) => new(false, code, detail);
}

public interface IPrinterService
{
    Task<PrintResult> PrintReceiptAsync(string cafeId, string orderId, CancellationToken ct = default);
    Task<PrintResult> PrintKitchenTicketAsync(
        string cafeId,
        string orderId,
        CancellationToken ct = default);
    Task<PrintResult> TestPrintAsync(string printerIp, int port, CancellationToken ct = default);
}

public class NetworkPrinterService : IPrinterService
{
    private readonly AppDbContext _db;
    private readonly IOrderService _orders;
    private readonly ReceiptBuilder _receiptBuilder;
    private readonly ILogger<NetworkPrinterService> _logger;

    public NetworkPrinterService(
        AppDbContext db,
        IOrderService orders,
        ReceiptBuilder receiptBuilder,
        ILogger<NetworkPrinterService> logger)
    {
        _db = db;
        _orders = orders;
        _receiptBuilder = receiptBuilder;
        _logger = logger;
    }

    public async Task<PrintResult> PrintReceiptAsync(string cafeId, string orderId, CancellationToken ct = default)
    {
        var ctx = await BuildContextAsync(cafeId, orderId, ct);
        if (ctx is null)
            return PrintResult.Fail("ORDER_NOT_FOUND");

        if (string.IsNullOrWhiteSpace(ctx.Value.branch.ReceiptPrinterIp))
            return PrintResult.Fail("PRINTER_NOT_CONFIGURED");

        var bytes = _receiptBuilder.BuildReceipt(ctx.Value.printCtx);
        return await SendToPrinterAsync(
            ctx.Value.branch.ReceiptPrinterIp!,
            ctx.Value.branch.ReceiptPrinterPort ?? 9100,
            bytes,
            ct);
    }

    public async Task<PrintResult> PrintKitchenTicketAsync(
        string cafeId,
        string orderId,
        CancellationToken ct = default)
    {
        var ctx = await BuildContextAsync(cafeId, orderId, ct);
        if (ctx is null)
            return PrintResult.Fail("ORDER_NOT_FOUND");

        var order = ctx.Value.printCtx.Order;
        var activeItems = order.Items.Where(i => !i.IsVoided).ToList();
        if (activeItems.Count == 0)
            return PrintResult.Ok();

        var menuItemIds = activeItems.Select(i => i.MenuItemId).Distinct().ToList();
        var categoryStations = await (
            from m in _db.MenuItems.AsNoTracking()
            join c in _db.MenuCategories.AsNoTracking() on m.CategoryId equals c.Id
            where menuItemIds.Contains(m.Id) && m.CafeId == cafeId
            select new { m.Id, c.KitchenStationId }
        ).ToListAsync(ct);

        var stationIds = categoryStations
            .Select(x => x.KitchenStationId)
            .Where(id => !string.IsNullOrEmpty(id))
            .Distinct()
            .ToList();

        var stations = stationIds.Count == 0
            ? []
            : await _db.KitchenStations
                .AsNoTracking()
                .Where(s => stationIds.Contains(s.Id) && s.CafeId == cafeId)
                .ToListAsync(ct);

        var groups = activeItems
            .GroupBy(item =>
            {
                var cat = categoryStations.FirstOrDefault(c => c.Id == item.MenuItemId);
                return cat?.KitchenStationId;
            })
            .ToList();

        PrintResult? lastFail = null;
        var anyPrinted = false;

        foreach (var group in groups)
        {
            var station = string.IsNullOrEmpty(group.Key)
                ? null
                : stations.FirstOrDefault(s => s.Id == group.Key);

            string? ip;
            int port;
            string? stationLabel = null;

            if (station is not null && !string.IsNullOrWhiteSpace(station.PrinterIp))
            {
                ip = station.PrinterIp;
                port = station.PrinterPort;
                stationLabel = station.Name;
            }
            else if (!string.IsNullOrWhiteSpace(ctx.Value.branch.KitchenPrinterIp))
            {
                ip = ctx.Value.branch.KitchenPrinterIp;
                port = ctx.Value.branch.KitchenPrinterPort ?? 9100;
            }
            else
            {
                lastFail = PrintResult.Fail("KITCHEN_PRINTER_NOT_CONFIGURED");
                continue;
            }

            var itemsOnly = group.ToList();
            var bytes = _receiptBuilder.BuildKitchenTicket(
                ctx.Value.printCtx with { StationName = stationLabel },
                itemsOnly);
            var result = await SendToPrinterAsync(ip!, port, bytes, ct);
            if (result.Success)
                anyPrinted = true;
            else
                lastFail = result;
        }

        return anyPrinted ? PrintResult.Ok() : lastFail ?? PrintResult.Fail("KITCHEN_PRINTER_NOT_CONFIGURED");
    }

    public async Task<PrintResult> TestPrintAsync(string printerIp, int port, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(printerIp))
            return PrintResult.Fail("PRINTER_NOT_CONFIGURED");

        var bytes = _receiptBuilder.BuildTestPage();
        return await SendToPrinterAsync(printerIp.Trim(), port, bytes, ct);
    }

    private async Task<(Branch branch, ReceiptPrintContext printCtx)?> BuildContextAsync(
        string cafeId,
        string orderId,
        CancellationToken ct)
    {
        var order = await _orders.GetOrderAsync(cafeId, orderId, ct);
        if (order is null || string.IsNullOrEmpty(order.BranchId))
            return null;

        var branch = await _db.Branches
            .AsNoTracking()
            .Include(b => b.Cafe)
            .FirstOrDefaultAsync(b => b.Id == order.BranchId && b.CafeId == cafeId, ct);

        if (branch is null)
            return null;

        var print = new ReceiptPrintContext(
            order,
            branch.Cafe.Name,
            branch.Name,
            branch.ReceiptHeader,
            branch.ReceiptFooter,
            branch.WifiPassword,
            branch.PaperWidthMm is 58 or 80 ? branch.PaperWidthMm : 80,
            branch.AutoCutEnabled);

        return (branch, printCtx: print);
    }

    private async Task<PrintResult> SendToPrinterAsync(
        string ip,
        int port,
        byte[] data,
        CancellationToken ct)
    {
        try
        {
            using var client = new TcpClient();
            using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
            timeout.CancelAfter(TimeSpan.FromSeconds(5));
            await client.ConnectAsync(ip, port, timeout.Token);
            await using var stream = client.GetStream();
            await stream.WriteAsync(data, timeout.Token);
            await stream.FlushAsync(timeout.Token);

            _logger.LogInformation("Printed {Bytes} bytes to {Ip}:{Port}", data.Length, ip, port);
            return PrintResult.Ok();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Print failed to {Ip}:{Port}", ip, port);
            return PrintResult.Fail("PRINTER_CONNECTION_FAILED", ex.Message);
        }
    }
}

public static class PrinterBackgroundJobs
{
    public static void QueueReceiptPrint(IServiceScopeFactory scopeFactory, string cafeId, string orderId)
    {
        _ = Task.Run(async () =>
        {
            await using var scope = scopeFactory.CreateAsyncScope();
            var logger = scope.ServiceProvider.GetRequiredService<ILogger<NetworkPrinterService>>();
            try
            {
                var printer = scope.ServiceProvider.GetRequiredService<IPrinterService>();
                var result = await printer.PrintReceiptAsync(cafeId, orderId, CancellationToken.None);
                if (!result.Success)
                    logger.LogWarning("Auto-print receipt failed for {OrderId}: {Code}", orderId, result.ErrorCode);
            }
            catch (Exception ex)
            {
                logger.LogWarning(ex, "Auto-print receipt failed for order {OrderId}", orderId);
            }
        });
    }

    public static void QueueKitchenPrint(IServiceScopeFactory scopeFactory, string cafeId, string orderId)
    {
        _ = Task.Run(async () =>
        {
            await using var scope = scopeFactory.CreateAsyncScope();
            var logger = scope.ServiceProvider.GetRequiredService<ILogger<NetworkPrinterService>>();
            try
            {
                var printer = scope.ServiceProvider.GetRequiredService<IPrinterService>();
                var result = await printer.PrintKitchenTicketAsync(cafeId, orderId, CancellationToken.None);
                if (!result.Success)
                    logger.LogWarning("Kitchen print failed for {OrderId}: {Code}", orderId, result.ErrorCode);
            }
            catch (Exception ex)
            {
                logger.LogWarning(ex, "Kitchen print failed for order {OrderId}", orderId);
            }
        });
    }
}
