#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]

use std::{
    collections::HashMap,
    net::SocketAddr,
    time::{Duration, Instant},
};

use eyre::bail;
pub use mdns_sd::DaemonEvent;
use mdns_sd::{Receiver, ResolvedService, ServiceDaemon, ServiceEvent, ServiceInfo};

pub const LANSPREAD_SERVICE_TYPE: &str = "_lanspread._udp.local.";
pub type MdnsMonitor = Receiver<DaemonEvent>;

pub struct MdnsAdvertiser {
    daemon: ServiceDaemon,
    service_info: ServiceInfo,
    pub monitor: Receiver<DaemonEvent>,
}

impl MdnsAdvertiser {
    pub fn new(
        service_type: &str,
        instance_name: &str,
        address: SocketAddr,
        properties: Option<HashMap<String, String>>,
    ) -> eyre::Result<Self> {
        let host_name = format!("{}.local.", address.ip());
        let daemon = ServiceDaemon::new()?;
        let service_info = ServiceInfo::new(
            service_type,
            instance_name,
            &host_name,
            address.ip(),
            address.port(),
            properties,
        )?;

        let monitor = daemon.monitor()?;

        // Register the service
        daemon.register(service_info.clone())?;

        Ok(Self {
            daemon,
            service_info,
            monitor,
        })
    }
}

impl Drop for MdnsAdvertiser {
    fn drop(&mut self) {
        let _ = self.daemon.unregister(self.service_info.get_fullname());
        let _ = self.daemon.shutdown();
    }
}

pub struct MdnsBrowser {
    daemon: ServiceDaemon,
    receiver: Receiver<ServiceEvent>,
    service_type: String,
}

#[derive(Debug, Clone)]
pub struct MdnsService {
    pub addr: SocketAddr,
    pub fullname: String,
    pub hostname: String,
    pub properties: HashMap<String, String>,
}

#[derive(Debug, Clone)]
pub enum MdnsServicePoll {
    Service(MdnsService),
    Timeout,
    Closed,
}

impl MdnsBrowser {
    pub fn new(service_type: &str) -> eyre::Result<Self> {
        let daemon = ServiceDaemon::new()?;
        let receiver = daemon.browse(service_type)?;
        Ok(Self {
            daemon,
            receiver,
            service_type: service_type.to_string(),
        })
    }

    pub fn next_service(
        &self,
        ignore_addr: Option<SocketAddr>,
    ) -> eyre::Result<Option<MdnsService>> {
        loop {
            match self.receiver.recv() {
                Ok(event) => {
                    if let Some(service) = self.service_from_event(event, ignore_addr) {
                        return Ok(Some(service));
                    }
                }
                Err(err) => {
                    log::error!("mDNS browse channel closed: {err}");
                    return Ok(None);
                }
            }
        }
    }

    pub fn next_service_timeout(
        &self,
        ignore_addr: Option<SocketAddr>,
        timeout: Duration,
    ) -> eyre::Result<MdnsServicePoll> {
        let deadline = Instant::now() + timeout;

        loop {
            let remaining = deadline.saturating_duration_since(Instant::now());
            if remaining.is_zero() {
                return Ok(MdnsServicePoll::Timeout);
            }

            match self.receiver.recv_timeout(remaining) {
                Ok(event) => {
                    if let Some(service) = self.service_from_event(event, ignore_addr) {
                        return Ok(MdnsServicePoll::Service(service));
                    }
                }
                Err(err) if self.receiver.is_disconnected() => {
                    log::error!("mDNS browse channel closed: {err}");
                    return Ok(MdnsServicePoll::Closed);
                }
                Err(err) => {
                    log::trace!("mDNS browse timeout: {err}");
                    return Ok(MdnsServicePoll::Timeout);
                }
            }
        }
    }

    pub fn next_address(
        &self,
        ignore_addr: Option<SocketAddr>,
    ) -> eyre::Result<Option<SocketAddr>> {
        Ok(self.next_service(ignore_addr)?.map(|service| service.addr))
    }

    fn service_from_event(
        &self,
        event: ServiceEvent,
        ignore_addr: Option<SocketAddr>,
    ) -> Option<MdnsService> {
        match event {
            ServiceEvent::ServiceResolved(info) => self.service_from_resolved(&info, ignore_addr),
            other_event => {
                log::trace!("mdns unrelated event: {other_event:?}");
                None
            }
        }
    }

    fn service_from_resolved(
        &self,
        info: &ResolvedService,
        ignore_addr: Option<SocketAddr>,
    ) -> Option<MdnsService> {
        log::trace!("mdns ServiceResolved event: {info:?}");

        if info.ty_domain != self.service_type {
            log::trace!(
                "Got mDNS with uninteresting service type: {} (expected: {})",
                info.ty_domain,
                self.service_type,
            );
            return None;
        }

        let mut ignored_match = false;
        for address in info.get_addresses() {
            let addr = SocketAddr::new(address.to_ip_addr(), info.get_port());

            if ignore_addr.is_some_and(|ignore| ignore == addr) {
                ignored_match = true;
                log::trace!("Ignoring mDNS advertisement for local server at {addr}");
                continue;
            }

            log::info!("Found server at {addr}");
            let properties = info.get_properties().clone().into_property_map_str();
            return Some(MdnsService {
                addr,
                fullname: info.get_fullname().to_string(),
                hostname: info.get_hostname().to_string(),
                properties,
            });
        }

        if ignored_match {
            log::trace!(
                "Only saw ignored mDNS advertisements (probably ourselves) for {:?}",
                info.get_fullname()
            );
            return None;
        }

        log::error!("No address found in mDNS response: {info:?}");
        None
    }
}

impl Drop for MdnsBrowser {
    fn drop(&mut self) {
        let _ = self.daemon.shutdown();
    }
}

pub fn discover_service(
    service_type: &str,
    ignore_addr: Option<SocketAddr>,
) -> eyre::Result<SocketAddr> {
    // Currently unused; kept for potential one-off discovery callers that just need a single address.
    let browser = MdnsBrowser::new(service_type)?;
    match browser.next_address(ignore_addr)? {
        Some(addr) => Ok(addr),
        None => bail!("No server found."),
    }
}
