using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging;

namespace Inspectron.Epson;

public class DiscoveryService : IDiscoveryService
{
    private const int DiscoveryPort = 3289;
    private const string DiscoveryMessage = "EPSONQ";
    private const int MessageLength = 14;
    private readonly ILogger? _logger;

    public DiscoveryService(ILogger? logger = null)
    {
        _logger = logger;
    }

    /// <summary>
    /// Discovers Epson printers on the local network using UDP broadcast
    /// </summary>
    /// <param name="timeout">How long to wait for responses (default: 5 seconds)</param>
    /// <param name="onPrinterDiscovered">Optional callback invoked when a printer is discovered</param>
    /// <returns>List of discovered printer information</returns>
    public async Task<List<DiscoveredPrinter>> DiscoverPrintersAsync(
        TimeSpan? timeout = null,
        Action<DiscoveredPrinter>? onPrinterDiscovered = null)
    {
        timeout ??= TimeSpan.FromSeconds(5);

        using var udpClient = new UdpClient();
        udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, 0));

        // Send discovery broadcast message
        var msg = BuildDiscoveryMessage();
        var broadcastEndpoint = new IPEndPoint(IPAddress.Broadcast, DiscoveryPort);
        await udpClient.SendAsync(msg, msg.Length, broadcastEndpoint);

        // Receive responses
        var discoveredPrinters = new List<DiscoveredPrinter>();
        var endTime = DateTime.UtcNow.Add(timeout.Value);

        while (DateTime.UtcNow < endTime)
        {
            try
            {
                var remainingTime = endTime - DateTime.UtcNow;
                if (remainingTime <= TimeSpan.Zero)
                    break;

                var receiveTask = udpClient.ReceiveAsync();
                var timeoutTask = Task.Delay(remainingTime);
                var completedTask = await Task.WhenAny(receiveTask, timeoutTask);

                if (completedTask == receiveTask)
                {
                    var result = await receiveTask;

                    // Check if we've already discovered this printer
                    if (!discoveredPrinters.Any(p => p.IPAddress.Equals(result.RemoteEndPoint.Address)))
                    {
                        var ipAddress = result.RemoteEndPoint.Address.ToString();
                        var printer = new DiscoveredPrinter
                        {
                            IPAddress = ipAddress,
                            DiscoveredAt = DateTime.UtcNow
                        };

                        // Try to get printer model by connecting and querying
                        printer.ModelName = await GetPrinterModelAsync(ipAddress);

                        discoveredPrinters.Add(printer);
                        onPrinterDiscovered?.Invoke(printer);
                    }
                }
                else
                {
                    break; // Timeout reached
                }
            }
            catch (Exception)
            {
                // Ignore errors and continue waiting for other responses
                break;
            }
        }

        return discoveredPrinters;
    }

    private static byte[] BuildDiscoveryMessage()
    {
        // msg = b'EPSONQ' + bytes([0x00] * 8)
        var msg = new byte[MessageLength];
        Array.Copy(System.Text.Encoding.ASCII.GetBytes(DiscoveryMessage), msg, 6);
        msg[6] = 3;
        for (int i = 7; i < msg.Length; i++)
        {
            msg[i] = 0x00;
        }

        msg[10] = 0x10;
        return msg;
    }

    /// <summary>
    /// Connects to a printer and retrieves its model name
    /// </summary>
    private async Task<string?> GetPrinterModelAsync(string ipAddress, int port = 9100)
    {
        try
        {
            _logger?.LogDebug("Connecting to printer at {IP} to retrieve model", ipAddress);

            await using var epsonPrinter = new EpsonPrinter(_logger);
            await epsonPrinter.ConnectAsync(ipAddress, port, timeoutSeconds: 3);

            var printerId = await epsonPrinter.GetPrinterIdAsync();
            if (printerId.HasValue)
            {
                var modelName = GetModelNameFromId(printerId.Value);
                _logger?.LogDebug("Printer at {IP} identified as {Model} (ID: 0x{Id:X2})",
                    ipAddress, modelName, printerId.Value);
                return modelName;
            }

            _logger?.LogDebug("Could not retrieve printer ID from {IP}", ipAddress);
            return null;
        }
        catch (Exception ex)
        {
            _logger?.LogWarning(ex, "Failed to get printer model from {IP}", ipAddress);
            return null;
        }
    }

    /// <summary>
    /// Converts a printer ID byte to a model name string
    /// </summary>
    private static string GetModelNameFromId(byte printerId)
    {
        return printerId switch
        {
            0x01 => "TM-T30III",
            0x0D => "TM-U220II",
            0x13 => "TM-U220II",
            _ => $"Unknown (0x{printerId:X2})"
        };
    }
}


