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

namespace Inspectron.Epson;

/// <summary>
/// Bonjour/mDNS (Multicast DNS) Service Discovery
/// Implements RFC 6762 (mDNS) and RFC 6763 (DNS-SD)
/// Port 5353, Multicast 224.0.0.251 (IPv4) / FF02::FB (IPv6)
/// </summary>
public class BonjourDiscovery
{
    private const int MDNS_PORT = 5353;
    private const string MDNS_MULTICAST_IPV4 = "224.0.0.251";
    //private const string MDNS_MULTICAST_IPV4 = "255.255.255.255";
    private const string MDNS_MULTICAST_IPV6 = "FF02::FB";
    private const int DEFAULT_TIMEOUT_MS = 10000;

    private static ushort _nextTransactionId = 1;

    /// <summary>
    /// Well-known service types for Bonjour/mDNS
    /// </summary>
    public static class ServiceTypes
    {
        public const string PRINTER_IPP = "_ipp._tcp.local.";
        public const string PRINTER_IPP_TLS = "_ipps._tcp.local.";
        public const string PRINTER_LPD = "_printer._tcp.local.";
        public const string PRINTER_PDL_DATASTREAM = "_pdl-datastream._tcp.local.";
        public const string SCANNER = "_scanner._tcp.local.";
        public const string HTTP = "_http._tcp.local.";
        public const string HTTPS = "_https._tcp.local.";
        public const string SSH = "_ssh._tcp.local.";
        public const string FTP = "_ftp._tcp.local.";
        public const string SFTP = "_sftp-ssh._tcp.local.";
        public const string SMB = "_smb._tcp.local.";
        public const string AFP = "_afpovertcp._tcp.local.";
        public const string NFS = "_nfs._tcp.local.";
        public const string AIRPLAY = "_airplay._tcp.local.";
        public const string AIRPRINT = "_ipp._tcp.local.";
        public const string RAOP = "_raop._tcp.local.";
        public const string HOMEKIT = "_hap._tcp.local.";
        public const string CHROMECAST = "_googlecast._tcp.local.";
        public const string SPOTIFY_CONNECT = "_spotify-connect._tcp.local.";
        public const string MQTT = "_mqtt._tcp.local.";
        public const string WORKSTATION = "_workstation._tcp.local.";
        public const string SERVICES_DNS_SD = "_services._dns-sd._udp.local.";
    }

    // _dosvc._tcp.local - discovery of all devices

    /// <summary>
    /// Discovers services of specified type via mDNS/Bonjour
    /// </summary>
    public static List<BonjourService> DiscoverServices(
        string serviceType = "_ipp._tcp.local.",
        int timeoutMs = DEFAULT_TIMEOUT_MS,
        bool useIPv6 = false)
    {
        var discoveredServices = new List<BonjourService>();
        var serviceKeys = new HashSet<string>();

        using (var udpClient = new UdpClient())
        {
            try
            {
                // Configure socket
                udpClient.Client.SetSocketOption(
                    SocketOptionLevel.Socket,
                    SocketOptionName.ReuseAddress,
                    true);

                if (useIPv6)
                {
                    udpClient.Client.Bind(new IPEndPoint(IPAddress.IPv6Any, MDNS_PORT));
                    var multicastAddress = IPAddress.Parse(MDNS_MULTICAST_IPV6);
                    udpClient.JoinMulticastGroup(multicastAddress);
                }
                else
                {
                    udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, MDNS_PORT));
                    var multicastAddress = IPAddress.Parse(MDNS_MULTICAST_IPV4);
                    udpClient.JoinMulticastGroup(multicastAddress);
                }

                udpClient.Client.ReceiveTimeout = timeoutMs;

                // Build and send mDNS query
                var transactionId = _nextTransactionId++;
                byte[] query = BuildMdnsQuery(serviceType, transactionId);

                var multicastEndpoint = new IPEndPoint(
                    useIPv6 ? IPAddress.Parse(MDNS_MULTICAST_IPV6) : IPAddress.Parse(MDNS_MULTICAST_IPV4),
                    MDNS_PORT);

                Console.WriteLine($"[mDNS] Querying: {serviceType}");
                udpClient.Send(query, query.Length, multicastEndpoint);

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

                        if (IsValidMdnsResponse(responseData))
                        {
                            var services = ParseMdnsResponse(responseData, remoteEndpoint.Address.ToString());

                            foreach (var service in services)
                            {
                                string key = $"{service.ServiceName}.{service.ServiceType}";
                                if (!serviceKeys.Contains(key))
                                {
                                    discoveredServices.Add(service);
                                    serviceKeys.Add(key);
                                    Console.WriteLine($"[mDNS] ✓ Found: {service.ToString()}");
                                    
                                }
                            }
                        }
                    }
                    catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
                    {
                        break;
                    }
                }

                Console.WriteLine($"[mDNS] Found {discoveredServices.Count} service(s)");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[mDNS] Error: {ex.Message}");
            }
        }

        return discoveredServices;
    }

    /// <summary>
    /// Discovers services asynchronously
    /// </summary>
    public static async Task<List<BonjourService>> DiscoverServicesAsync(
        string serviceType = "_ipp._tcp.local.",
        int timeoutMs = DEFAULT_TIMEOUT_MS,
        bool useIPv6 = false)
    {
        return await Task.Run(() => DiscoverServices(serviceType, timeoutMs, useIPv6));
    }

    /// <summary>
    /// Discovers all available service types on the network
    /// </summary>
    public static List<string> DiscoverServiceTypes(int timeoutMs = DEFAULT_TIMEOUT_MS)
    {
        Console.WriteLine("[mDNS] Discovering available service types...");

        var services = DiscoverServices("_services._dns-sd._udp.local.", timeoutMs);
        var serviceTypes = new HashSet<string>();

        foreach (var service in services)
        {
            if (!string.IsNullOrEmpty(service.ServiceType))
            {
                serviceTypes.Add(service.ServiceType);
            }
        }

        return serviceTypes.ToList();
    }

    /// <summary>
    /// Builds an mDNS query packet
    /// </summary>
    private static byte[] BuildMdnsQuery(string serviceType, ushort transactionId)
    {
        using (var ms = new System.IO.MemoryStream())
        using (var writer = new System.IO.BinaryWriter(ms))
        {
            // DNS Header
            writer.Write((byte)((transactionId >> 8) & 0xFF));
            writer.Write((byte)(transactionId & 0xFF));

            // Flags: Standard query (0x0000)
            writer.Write((byte)0x00);
            writer.Write((byte)0x00);

            // Question count: 1
            writer.Write((byte)0x00);
            writer.Write((byte)0x01);

            // Answer count: 0
            writer.Write((byte)0x00);
            writer.Write((byte)0x00);

            // Authority count: 0
            writer.Write((byte)0x00);
            writer.Write((byte)0x00);

            // Additional count: 0
            writer.Write((byte)0x00);
            writer.Write((byte)0x00);

            // Question section
            WriteQName(writer, serviceType);

            // Type: PTR (12)
            writer.Write((byte)0x00);
            writer.Write((byte)0x0C);

            // Class: IN (1)
            writer.Write((byte)0x00);
            writer.Write((byte)0x01);

            return ms.ToArray();
        }
    }

    /// <summary>
    /// Writes a DNS name in QNAME format
    /// </summary>
    private static void WriteQName(System.IO.BinaryWriter writer, string name)
    {
        var labels = name.TrimEnd('.').Split('.');

        foreach (var label in labels)
        {
            if (string.IsNullOrEmpty(label))
                continue;

            writer.Write((byte)label.Length);
            writer.Write(Encoding.ASCII.GetBytes(label));
        }

        writer.Write((byte)0x00); // Null terminator
    }

    /// <summary>
    /// Validates if response is a proper mDNS response
    /// </summary>
    private static bool IsValidMdnsResponse(byte[] response)
    {
        if (response == null || response.Length < 12)
            return false;

        // Check if it's a response (QR bit set)
        return (response[2] & 0x80) != 0;
    }

    /// <summary>
    /// Parses mDNS response packet
    /// </summary>
    private static List<BonjourService> ParseMdnsResponse(byte[] response, string sourceIp)
    {
        var services = new List<BonjourService>();

        try
        {
            int offset = 0;

            // Parse header
            ushort transactionId = ReadUInt16(response, ref offset);
            ushort flags = ReadUInt16(response, ref offset);
            ushort questionCount = ReadUInt16(response, ref offset);
            ushort answerCount = ReadUInt16(response, ref offset);
            ushort authorityCount = ReadUInt16(response, ref offset);
            ushort additionalCount = ReadUInt16(response, ref offset);

            // Skip questions
            for (int i = 0; i < questionCount; i++)
            {
                SkipQName(response, ref offset);
                offset += 4; // Type and Class
            }

            // Parse answers
            var ptrRecords = new Dictionary<string, List<string>>();
            var srvRecords = new Dictionary<string, SrvRecord>();
            var txtRecords = new Dictionary<string, Dictionary<string, string>>();
            var aRecords = new Dictionary<string, string>();

            // Parse all record sections
            int totalRecords = answerCount + authorityCount + additionalCount;

            for (int i = 0; i < totalRecords; i++)
            {
                try
                {
                    string name = ReadQName(response, ref offset);
                    ushort type = ReadUInt16(response, ref offset);
                    ushort rclass = ReadUInt16(response, ref offset);
                    uint ttl = ReadUInt32(response, ref offset);
                    ushort dataLength = ReadUInt16(response, ref offset);
                    int dataStart = offset;

                    switch (type)
                    {
                        case 12: // PTR
                            string ptrTarget = ReadQName(response, ref offset);
                            if (!ptrRecords.ContainsKey(name))
                                ptrRecords[name] = new List<string>();
                            ptrRecords[name].Add(ptrTarget);
                            break;

                        case 33: // SRV
                            ushort priority = ReadUInt16(response, ref offset);
                            ushort weight = ReadUInt16(response, ref offset);
                            ushort port = ReadUInt16(response, ref offset);
                            string target = ReadQName(response, ref offset);
                            srvRecords[name] = new SrvRecord
                            {
                                Port = port,
                                Target = target,
                                Priority = priority,
                                Weight = weight,
                                TTL = ttl
                            };
                            break;

                        case 16: // TXT
                            var txtData = ParseTxtRecord(response, offset, dataLength);
                            txtRecords[name] = txtData;
                            offset += dataLength;
                            break;

                        case 1: // A (IPv4)
                            if (dataLength == 4)
                            {
                                var ip = new IPAddress(new[]
                                {
                                        response[offset],
                                        response[offset + 1],
                                        response[offset + 2],
                                        response[offset + 3]
                                    });
                                aRecords[name] = ip.ToString();
                                offset += 4;
                            }
                            break;

                        case 28: // AAAA (IPv6)
                            if (dataLength == 16)
                            {
                                byte[] ipBytes = new byte[16];
                                Array.Copy(response, offset, ipBytes, 0, 16);
                                var ip = new IPAddress(ipBytes);
                                aRecords[name] = ip.ToString();
                                offset += 16;
                            }
                            break;

                        default:
                            offset += dataLength;
                            break;
                    }

                    // Ensure we're at the right position
                    offset = dataStart + dataLength;
                }
                catch
                {
                    break;
                }
            }

            // Build service objects from collected records
            foreach (var ptrEntry in ptrRecords)
            {
                foreach (var instanceName in ptrEntry.Value)
                {
                    var service = new BonjourService
                    {
                        DiscoveredAt = DateTime.Now,
                        RawResponse = response
                    };

                    // Parse instance name (e.g., "My Printer._ipp._tcp.local.")
                    ParseServiceName(instanceName, service);

                    // Get SRV record
                    if (srvRecords.ContainsKey(instanceName))
                    {
                        var srv = srvRecords[instanceName];
                        service.Port = srv.Port;
                        service.HostName = srv.Target;
                        service.TTL = srv.TTL;

                        // Get IP address from A/AAAA record
                        if (aRecords.ContainsKey(srv.Target))
                        {
                            service.IPAddress = aRecords[srv.Target];
                        }
                    }

                    // Get TXT record
                    if (txtRecords.ContainsKey(instanceName))
                    {
                        service.TxtRecords = txtRecords[instanceName];
                    }

                    services.Add(service);
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"[mDNS Parser] Error: {ex.Message}");
        }

        return services;
    }

    /// <summary>
    /// Parses service name components
    /// </summary>
    private static void ParseServiceName(string fullName, BonjourService service)
    {
        service.FullServiceName = fullName;

        var parts = fullName.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);

        if (parts.Length >= 3)
        {
            service.ServiceName = parts[0];
            service.ServiceType = $"{parts[1]}.{parts[2]}";

            if (parts.Length >= 4)
            {
                service.Domain = string.Join(".", parts.Skip(3));
            }
            else
            {
                service.Domain = "local";
            }
        }
    }

    /// <summary>
    /// Parses TXT record key-value pairs
    /// </summary>
    private static Dictionary<string, string> ParseTxtRecord(byte[] data, int offset, int length)
    {
        var result = new Dictionary<string, string>();
        int end = offset + length;

        while (offset < end)
        {
            byte len = data[offset++];
            if (len == 0 || offset + len > end)
                break;

            string txt = Encoding.UTF8.GetString(data, offset, len);
            offset += len;

            var eqIndex = txt.IndexOf('=');
            if (eqIndex > 0)
            {
                string key = txt.Substring(0, eqIndex);
                string value = txt.Substring(eqIndex + 1);
                result[key] = value;
            }
            else
            {
                result[txt] = "";
            }
        }

        return result;
    }

    /// <summary>
    /// Reads a DNS name from the packet (handles compression)
    /// </summary>
    private static string ReadQName(byte[] data, ref int offset)
    {
        var labels = new List<string>();
        int jumps = 0;
        int maxJumps = 10;
        bool jumped = false;
        int returnOffset = -1;

        while (offset < data.Length)
        {
            byte length = data[offset];

            // Check for compression pointer
            if ((length & 0xC0) == 0xC0)
            {
                if (!jumped)
                    returnOffset = offset + 2;

                int pointer = ((length & 0x3F) << 8) | data[offset + 1];
                offset = pointer;
                jumped = true;
                jumps++;

                if (jumps > maxJumps)
                    break;

                continue;
            }

            offset++;

            if (length == 0)
                break;

            if (offset + length > data.Length)
                break;

            string label = Encoding.ASCII.GetString(data, offset, length);
            labels.Add(label);
            offset += length;
        }

        if (jumped && returnOffset != -1)
            offset = returnOffset;

        return string.Join(".", labels);
    }

    /// <summary>
    /// Skips a DNS name in the packet
    /// </summary>
    private static void SkipQName(byte[] data, ref int offset)
    {
        while (offset < data.Length)
        {
            byte length = data[offset++];

            if (length == 0)
                break;

            if ((length & 0xC0) == 0xC0)
            {
                offset++;
                break;
            }

            offset += length;
        }
    }

    /// <summary>
    /// Reads 16-bit unsigned integer (big-endian)
    /// </summary>
    private static ushort ReadUInt16(byte[] data, ref int offset)
    {
        ushort value = (ushort)((data[offset] << 8) | data[offset + 1]);
        offset += 2;
        return value;
    }

    /// <summary>
    /// Reads 32-bit unsigned integer (big-endian)
    /// </summary>
    private static uint ReadUInt32(byte[] data, ref int offset)
    {
        uint value = (uint)((data[offset] << 24) | (data[offset + 1] << 16) |
                           (data[offset + 2] << 8) | data[offset + 3]);
        offset += 4;
        return value;
    }

    /// <summary>
    /// SRV record data structure
    /// </summary>
    private class SrvRecord
    {
        public ushort Priority { get; set; }
        public ushort Weight { get; set; }
        public ushort Port { get; set; }
        public string Target { get; set; }
        public uint TTL { get; set; }
    }
}