using System.Net;
using System.Net.Sockets;
using System.Text;

namespace Inspectron.Epson;

/// <summary>
/// Service Location Protocol (SLP) Discovery for Epson Printers
/// Implements SLPv2 (RFC 2608) for printer discovery on port 427
/// </summary>
public class EpsonPrinterDiscovery
{
    private const int SLP_PORT = 427;
    private const int DISCOVER_TIMEOUT_MS = 5000;
    private const string SLP_MULTICAST_ADDRESS = "239.255.255.253"; // SLP multicast group

    // SLP Message Function IDs (SLPv2)
    private const byte FUNC_SRVRQST = 1;  // Service Request
    private const byte FUNC_SRVRPLY = 2;  // Service Reply
    private const byte FUNC_DAADVERT = 8; // Directory Agent Advertisement
    private const byte FUNC_ATTRRQST = 6; // Attribute Request
    private const byte FUNC_ATTRRPLY = 7; // Attribute Reply

    private static ushort _nextXid = 1;

    /// <summary>
    /// Discovers printers using SLP (Service Location Protocol)
    /// </summary>
    /// <param name="serviceType">Service type to search for (default: "service:printer")</param>
    /// <param name="scope">Scope to search in (default: "DEFAULT")</param>
    /// <param name="timeoutMs">Timeout in milliseconds</param>
    /// <returns>List of discovered printers</returns>
    public static List<DiscoveredPrinter> DiscoverPrinters(
        string serviceType = "service:printer",
        string scope = "DEFAULT",
        int timeoutMs = DISCOVER_TIMEOUT_MS)
    {
        var discoveredPrinters = new List<DiscoveredPrinter>();
        var printerUrls = new HashSet<string>();

        using (var udpClient = new UdpClient())
        {
            try
            {
                // Bind to local port
                udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
                udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, 0));

                // Join multicast group
                var multicastAddress = IPAddress.Parse(SLP_MULTICAST_ADDRESS);
                udpClient.JoinMulticastGroup(multicastAddress);

                // Set receive timeout
                udpClient.Client.ReceiveTimeout = timeoutMs;

                // Build and send Service Request
                var xid = _nextXid++;
                byte[] serviceRequest = BuildServiceRequest(serviceType, scope, xid);

                var multicastEndpoint = new IPEndPoint(multicastAddress, SLP_PORT);
                Console.WriteLine($"[SLP Discovery] Sending Service Request to {multicastEndpoint}...");
                Console.WriteLine($"[SLP Discovery] Service Type: {serviceType}, Scope: {scope}");

                int bytesSent = udpClient.Send(serviceRequest, serviceRequest.Length, multicastEndpoint);
                Console.WriteLine($"[SLP Discovery] Sent {bytesSent} bytes");

                // Wait for responses
                var startTime = DateTime.Now;
                int receiveCount = 0;

                while ((DateTime.Now - startTime).TotalMilliseconds < timeoutMs)
                {
                    try
                    {
                        IPEndPoint remoteEndpoint = null;
                        byte[] responseData = udpClient.Receive(ref remoteEndpoint);

                        receiveCount++;
                        Console.WriteLine($"[SLP Discovery] Received {responseData.Length} bytes from {remoteEndpoint.Address}");

                        // Parse SLP response
                        if (IsValidSlpResponse(responseData, xid))
                        {
                            var printers = ParseServiceReply(responseData, remoteEndpoint.Address.ToString());

                            foreach (var printer in printers)
                            {
                                if (!printerUrls.Contains(printer.ServiceUrl))
                                {
                                    discoveredPrinters.Add(printer);
                                    printerUrls.Add(printer.ServiceUrl);
                                    Console.WriteLine($"[SLP Discovery] ✓ Found printer: {printer.ServiceUrl}");
                                }
                            }
                        }
                    }
                    catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
                    {
                        // Timeout - no more responses
                        break;
                    }
                }

                Console.WriteLine($"[SLP Discovery] Discovery complete. Found {discoveredPrinters.Count} printer(s)");

                // Leave multicast group
                udpClient.DropMulticastGroup(multicastAddress);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[SLP Discovery] Error: {ex.Message}");
            }
        }

        return discoveredPrinters;
    }

    /// <summary>
    /// Discovers printers asynchronously
    /// </summary>
    public static async Task<List<DiscoveredPrinter>> DiscoverPrintersAsync(
        string serviceType = "service:printer",
        string scope = "DEFAULT",
        int timeoutMs = DISCOVER_TIMEOUT_MS)
    {
        return await Task.Run(() => DiscoverPrinters(serviceType, scope, timeoutMs));
    }

    /// <summary>
    /// Builds an SLP Service Request packet (SLPv2 - RFC 2608)
    /// </summary>
    private static byte[] BuildServiceRequest(string serviceType, string scope, ushort xid)
    {
        using (var ms = new System.IO.MemoryStream())
        using (var writer = new System.IO.BinaryWriter(ms))
        {
            // SLP Header (14 bytes for SLPv2)
            writer.Write((byte)2);              // Version (SLPv2)
            writer.Write(FUNC_SRVRQST);         // Function: Service Request

            // Length placeholder (will be updated at end)
            long lengthPosition = ms.Position;
            writer.Write((byte)0);
            writer.Write((ushort)0);            // Length (3 bytes, big-endian)

            // Flags (2 bytes)
            ushort flags = 0x2000;              // Request Multicast flag
            writer.Write((byte)((flags >> 8) & 0xFF));
            writer.Write((byte)(flags & 0xFF));

            // Next Extension Offset (3 bytes) - 0 for no extensions
            writer.Write((byte)0);
            writer.Write((ushort)0);

            // XID (Transaction ID) - 2 bytes
            writer.Write((byte)((xid >> 8) & 0xFF));
            writer.Write((byte)(xid & 0xFF));

            // Language Tag (2 + length bytes) - "en"
            string langTag = "en";
            writer.Write((ushort)langTag.Length);
            writer.Write(Encoding.ASCII.GetBytes(langTag));

            // --- Service Request Body ---

            // Previous Responder List (2 bytes for count)
            writer.Write((ushort)0);            // No previous responders

            // Service Type (2 + length bytes)
            writer.Write((ushort)serviceType.Length);
            writer.Write(Encoding.ASCII.GetBytes(serviceType));

            // Scope List (2 + length bytes)
            writer.Write((ushort)scope.Length);
            writer.Write(Encoding.ASCII.GetBytes(scope));

            // Predicate (2 + length bytes) - Empty for "all"
            writer.Write((ushort)0);            // No predicate (match all)

            // SLP SPI String (2 + length bytes) - Empty (no authentication)
            writer.Write((ushort)0);

            // Update length field
            byte[] packet = ms.ToArray();
            int totalLength = packet.Length;

            // Write length in big-endian format (3 bytes starting at position 2)
            packet[2] = (byte)((totalLength >> 16) & 0xFF);
            packet[3] = (byte)((totalLength >> 8) & 0xFF);
            packet[4] = (byte)(totalLength & 0xFF);

            return packet;
        }
    }

    /// <summary>
    /// Validates if response is a proper SLP Service Reply
    /// </summary>
    private static bool IsValidSlpResponse(byte[] response, ushort expectedXid)
    {
        if (response == null || response.Length < 14)
            return false;

        // Check version (must be 2)
        if (response[0] != 2)
            return false;

        // Check function (should be Service Reply = 2)
        if (response[1] != FUNC_SRVRPLY)
            return false;

        // Check XID matches
        ushort xid = (ushort)((response[12] << 8) | response[13]);
        if (xid != expectedXid)
            return false;

        return true;
    }

    /// <summary>
    /// Parses SLP Service Reply packet
    /// </summary>
    private static List<DiscoveredPrinter> ParseServiceReply(byte[] response, string sourceIp)
    {
        var printers = new List<DiscoveredPrinter>();

        try
        {
            int offset = 14; // Skip SLP header

            // Read Language Tag
            ushort langTagLen = ReadUInt16(response, ref offset);
            string langTag = ReadString(response, ref offset, langTagLen);

            // Read Error Code (2 bytes)
            ushort errorCode = ReadUInt16(response, ref offset);
            if (errorCode != 0)
            {
                Console.WriteLine($"[SLP Parser] Error code in response: {errorCode}");
                return printers;
            }

            // Read URL Entry Count
            ushort urlCount = ReadUInt16(response, ref offset);
            Console.WriteLine($"[SLP Parser] Found {urlCount} URL(s) in response");

            // Parse each URL entry
            for (int i = 0; i < urlCount; i++)
            {
                try
                {
                    var printer = new DiscoveredPrinter
                    {
                        DiscoveredAt = DateTime.Now,
                        RawResponse = response
                    };

                    // Reserved byte
                    offset++;

                    // Lifetime (2 bytes)
                    printer.Lifetime = ReadUInt16(response, ref offset);

                    // URL Length and URL
                    ushort urlLen = ReadUInt16(response, ref offset);
                    printer.ServiceUrl = ReadString(response, ref offset, urlLen);

                    // Parse URL to extract IP and port
                    ParseServiceUrl(printer.ServiceUrl, printer);

                    // Auth Block Count (1 byte)
                    byte authBlockCount = response[offset++];

                    // Skip auth blocks if present
                    for (int j = 0; j < authBlockCount; j++)
                    {
                        // Skip authentication blocks (not typically used)
                        ushort blockLen = ReadUInt16(response, ref offset);
                        offset += blockLen;
                    }

                    printers.Add(printer);
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"[SLP Parser] Error parsing URL entry {i}: {ex.Message}");
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"[SLP Parser] Error parsing response: {ex.Message}");
        }

        return printers;
    }

    /// <summary>
    /// Parses a service URL to extract IP address and port
    /// Examples:
    ///   service:printer:ipp://192.168.1.100:631/printers/lp
    ///   service:printer:lpr://192.168.1.100/queue
    /// </summary>
    private static void ParseServiceUrl(string serviceUrl, DiscoveredPrinter printer)
    {
        try
        {
            // Extract service type
            if (serviceUrl.StartsWith("service:"))
            {
                var parts = serviceUrl.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
                if (parts.Length >= 2)
                {
                    printer.ServiceType = parts[1]; // e.g., "printer"
                }
            }

            // Extract IP and port from URL
            // Pattern: protocol://ip:port/path
            var protocolIndex = serviceUrl.IndexOf("://");
            if (protocolIndex > 0)
            {
                var afterProtocol = serviceUrl.Substring(protocolIndex + 3);
                var slashIndex = afterProtocol.IndexOf('/');
                var hostPort = slashIndex > 0 ?
                    afterProtocol.Substring(0, slashIndex) :
                    afterProtocol;

                // Check for port
                var colonIndex = hostPort.LastIndexOf(':');
                if (colonIndex > 0 && colonIndex < hostPort.Length - 1)
                {
                    printer.IPAddress = hostPort.Substring(0, colonIndex);
                    if (int.TryParse(hostPort.Substring(colonIndex + 1), out int port))
                    {
                        printer.Port = port;
                    }
                }
                else
                {
                    printer.IPAddress = hostPort;
                    // Default ports based on protocol
                    if (serviceUrl.Contains("ipp://"))
                        printer.Port = 631;
                    else if (serviceUrl.Contains("lpr://"))
                        printer.Port = 515;
                    else
                        printer.Port = 9100; // Default ESC/POS
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"[SLP Parser] Error parsing service URL: {ex.Message}");
        }
    }

    /// <summary>
    /// Reads a 16-bit unsigned integer in big-endian format
    /// </summary>
    private static ushort ReadUInt16(byte[] data, ref int offset)
    {
        if (offset + 2 > data.Length)
            throw new InvalidOperationException("Not enough data to read UInt16");

        ushort value = (ushort)((data[offset] << 8) | data[offset + 1]);
        offset += 2;
        return value;
    }

    /// <summary>
    /// Reads a string of specified length
    /// </summary>
    private static string ReadString(byte[] data, ref int offset, int length)
    {
        if (offset + length > data.Length)
            throw new InvalidOperationException("Not enough data to read string");

        string value = Encoding.UTF8.GetString(data, offset, length);
        offset += length;
        return value;
    }

    /// <summary>
    /// Prints hex dump of byte array for debugging
    /// </summary>
    public static void PrintHexDump(byte[] data, int bytesPerLine = 16)
    {
        Console.WriteLine("\n=== SLP Hex Dump ===");
        for (int i = 0; i < data.Length; i += bytesPerLine)
        {
            Console.Write($"{i:X4}: ");

            for (int j = 0; j < bytesPerLine; j++)
            {
                if (i + j < data.Length)
                    Console.Write($"{data[i + j]:X2} ");
                else
                    Console.Write("   ");
            }

            Console.Write(" | ");

            for (int j = 0; j < bytesPerLine && i + j < data.Length; j++)
            {
                byte b = data[i + j];
                char c = (b >= 32 && b < 127) ? (char)b : '.';
                Console.Write(c);
            }

            Console.WriteLine();
        }
        Console.WriteLine("===================\n");
    }
}