/*!
The PMOMusic SSDP client is a *control point*.
It must **not** bind to UDP port 1900.

Reason:

* The SSDP *server* (UPnP device mode) must listen on 0.0.0.0:1900 for M-SEARCH discovery.
* The SSDP *client* only needs to send M-SEARCH and receive unicast HTTP/200 replies.
* If both client and server bind on 1900 (even with SO_REUSEPORT) the kernel load-balances
  incoming datagrams between sockets. As a result, NOTIFY and HTTP/200 messages are lost
  randomly by the client.

Therefore:

* SSDP server → bind(0.0.0.0:1900), join multicast, answer M-SEARCH.
* SSDP client → bind(0.0.0.0:0), use an ephemeral port, send M-SEARCH, receive replies.

The client may still join the multicast group for debugging, but NOTIFY reception is optional.
*/
//! Client SSDP pour la découverte des devices UPnP

use super::{MAX_AGE, SSDP_MULTICAST_ADDR, SSDP_PORT};
use socket2::{Domain, Protocol, Socket, Type};
use std::collections::HashMap;
use std::net::{SocketAddr, UdpSocket};
use std::sync::Arc;
use std::time::Duration;
use tracing::{debug, info, trace, warn};

/// Événements SSDP intéressants pour un control point
#[derive(Debug, Clone)]
pub enum SsdpEvent {
    Alive {
        usn: String,
        nt: String,
        location: String,
        server: String,
        max_age: u32,
        from: SocketAddr,
    },
    ByeBye {
        usn: String,
        nt: String,
        from: SocketAddr,
    },
    SearchResponse {
        usn: String,
        st: String,
        location: String,
        server: String,
        max_age: u32,
        from: SocketAddr,
    },
}

/// Client SSDP pour envoyer des M-SEARCH et écouter les annonces
#[derive(Clone)]
pub struct SsdpClient {
    socket: Arc<UdpSocket>,
}

impl SsdpClient {
    /// Crée un nouveau client SSDP
    pub fn new() -> std::io::Result<Self> {
        let addr = format!("{}:{}", SSDP_MULTICAST_ADDR, SSDP_PORT);

        let socket2 = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
        socket2.set_reuse_address(true)?;

        #[cfg(windows)]
        {
            debug!("✅ SsdpClient SO_REUSEADDR enabled (Windows - SO_REUSEPORT not needed)");
        }

        let bind_addr: SocketAddr = "0.0.0.0:0".parse().unwrap();
        socket2.bind(&bind_addr.into())?;

        let socket: UdpSocket = socket2.into();
        socket.set_read_timeout(Some(Duration::from_secs(1)))?;
        socket.set_multicast_loop_v4(true)?; // utile en dev local

        let multicast_addr = SSDP_MULTICAST_ADDR.parse().unwrap();
        for iface in get_if_addrs::get_if_addrs()? {
            if let std::net::IpAddr::V4(ipv4) = iface.ip() {
                if !ipv4.is_loopback() {
                    match socket.join_multicast_v4(&multicast_addr, &ipv4) {
                        Ok(()) => {
                            debug!("SSDP: joined {} on {}", SSDP_MULTICAST_ADDR, ipv4);
                        }
                        Err(e) => {
                            warn!(
                                "SSDP: failed to join {} on {}: {}",
                                SSDP_MULTICAST_ADDR, ipv4, e
                            );
                        }
                    }
                }
            }
        }

        // Sur macOS, chaque join_multicast_v4 écrase IP_MULTICAST_IF.
        // On remet explicitement l'interface de sortie sur l'IP principale
        // pour que send_to vers 239.255.255.250 utilise la bonne interface.
        let local_ip: std::net::Ipv4Addr = pmoutils::guess_local_ip()
            .parse()
            .unwrap_or("0.0.0.0".parse().unwrap());
        let socket2 = Socket::from(socket);
        socket2.set_multicast_if_v4(&local_ip)?;
        debug!(
            "SSDP client: multicast outgoing interface set to {}",
            local_ip
        );
        let socket: UdpSocket = socket2.into();

        info!("✅ SSDP client ready on {}", addr);

        Ok(Self {
            socket: Arc::new(socket),
        })
    }

    /// Envoie un M-SEARCH pour un type donné
    pub fn send_msearch(&self, st: &str, mx: u32) -> std::io::Result<()> {
        let mx = mx.max(1); // MX doit être >= 1
        let msg = format!(
            "M-SEARCH * HTTP/1.1\r\n\
             HOST: {}:{}\r\n\
             MAN: \"ssdp:discover\"\r\n\
             MX: {}\r\n\
             ST: {}\r\n\
             USER-AGENT: PMOMusic SSDP Client\r\n\
             \r\n",
            SSDP_MULTICAST_ADDR, SSDP_PORT, mx, st
        );

        let addr: SocketAddr = format!("{}:{}", SSDP_MULTICAST_ADDR, SSDP_PORT)
            .parse()
            .unwrap();

        match self.socket.send_to(msg.as_bytes(), addr) {
            Ok(_) => {
                info!("📤 M-SEARCH sent (ST={}, MX={})", st, mx);
                debug!(
                    "📨 M-SEARCH payload\n<details>\n\n```\n{}\n```\n</details>\n",
                    msg
                );
                Ok(())
            }
            Err(e) => {
                warn!("❌ Failed to send M-SEARCH: {}", e);
                Err(e)
            }
        }
    }

    /// Boucle de réception bloquante pour traiter les événements SSDP
    pub fn run_event_loop<F>(&self, mut on_event: F) -> !
    where
        F: FnMut(SsdpEvent) + Send + 'static,
    {
        let socket = Arc::clone(&self.socket);
        let mut buf = [0u8; 8192];
        loop {
            match socket.recv_from(&mut buf) {
                Ok((n, from)) => {
                    let data = String::from_utf8_lossy(&buf[..n]);
                    if let Some(event) = parse_message(&data, from) {
                        debug!("📥 SSDP event from {}: {:?}", from, event);
                        on_event(event);
                    }
                }
                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
                    // Timeout, recommencer
                    continue;
                }
                Err(e) => {
                    warn!("❌ SSDP client read error: {}", e);
                }
            }
        }
    }
}

fn parse_message(data: &str, from: SocketAddr) -> Option<SsdpEvent> {
    let mut lines = data.lines();
    let first_line = lines.next()?.trim();
    let upper = first_line.to_ascii_uppercase();
    let headers = parse_headers(lines);

    let result = if upper.starts_with("NOTIFY ") {
        handle_notify(&headers, from)
    } else if upper.starts_with("HTTP/") && upper.contains(" 200 ") {
        handle_search_response(&headers, from)
    } else if upper.starts_with("M-SEARCH ") {
        // Another control point querying us; we are not a device, so we ignore.
        None
    } else {
        trace!("Unknown SSDP message type from {}: {}", from, first_line);
        None
    };

    if result.is_none() {
        trace!("SSDP message from {} could not be parsed:\n{}", from, data);
    }

    result
}

fn handle_notify(headers: &HashMap<String, String>, from: SocketAddr) -> Option<SsdpEvent> {
    // Critical headers: NTS, NT, USN (required by UPnP spec)
    let nts = headers.get("NTS")?.to_ascii_lowercase();
    let nt = headers.get("NT")?.to_string();
    let usn = headers.get("USN")?.to_string();

    if nts == "ssdp:alive" {
        // LOCATION is required for alive notifications
        let location = match headers.get("LOCATION") {
            Some(loc) => loc.to_string(),
            None => {
                trace!(
                    "NOTIFY ssdp:alive from {} missing LOCATION header, ignoring",
                    from
                );
                return None;
            }
        };

        // Non-critical headers: SERVER, CACHE-CONTROL
        let server = headers
            .get("SERVER")
            .map(|s| s.to_string())
            .unwrap_or_else(|| {
                trace!("NOTIFY from {} has no SERVER header, using 'Unknown'", from);
                "Unknown".to_string()
            });
        let max_age = parse_max_age(headers.get("CACHE-CONTROL"));

        Some(SsdpEvent::Alive {
            usn,
            nt,
            location,
            server,
            max_age,
            from,
        })
    } else if nts == "ssdp:byebye" {
        Some(SsdpEvent::ByeBye { usn, nt, from })
    } else {
        trace!("Unknown NTS value from {}: {}", from, nts);
        None
    }
}

fn handle_search_response(
    headers: &HashMap<String, String>,
    from: SocketAddr,
) -> Option<SsdpEvent> {
    // Critical headers: ST, USN, LOCATION (required by UPnP spec)
    let st = match headers.get("ST") {
        Some(s) => s.to_string(),
        None => {
            trace!(
                "M-SEARCH response from {} missing ST header, ignoring",
                from
            );
            return None;
        }
    };
    let usn = match headers.get("USN") {
        Some(u) => u.to_string(),
        None => {
            trace!(
                "M-SEARCH response from {} missing USN header, ignoring",
                from
            );
            return None;
        }
    };
    let location = match headers.get("LOCATION") {
        Some(loc) => loc.to_string(),
        None => {
            trace!(
                "M-SEARCH response from {} missing LOCATION header, ignoring",
                from
            );
            return None;
        }
    };

    // Non-critical headers: SERVER, CACHE-CONTROL
    let server = headers
        .get("SERVER")
        .map(|s| s.to_string())
        .unwrap_or_else(|| {
            trace!(
                "M-SEARCH response from {} has no SERVER header, using 'Unknown'",
                from
            );
            "Unknown".to_string()
        });
    let max_age = parse_max_age(headers.get("CACHE-CONTROL"));

    Some(SsdpEvent::SearchResponse {
        usn,
        st,
        location,
        server,
        max_age,
        from,
    })
}

fn parse_headers<'a, I>(lines: I) -> HashMap<String, String>
where
    I: Iterator<Item = &'a str>,
{
    let mut headers = HashMap::new();
    for line in lines {
        let line = line.trim();

        // Empty line marks end of headers
        if line.is_empty() {
            break;
        }

        // Split on first ':' only (values may contain ':')
        if let Some(colon_pos) = line.find(':') {
            let (name, value_with_colon) = line.split_at(colon_pos);
            let value = &value_with_colon[1..]; // Skip the ':'

            let name = name.trim().to_ascii_uppercase();
            let value = value.trim().to_string();

            // Skip empty header names or values
            if !name.is_empty() && !value.is_empty() {
                headers.insert(name, value);
            } else {
                trace!("Skipping malformed header: '{}'", line);
            }
        } else {
            // Line without ':' is invalid, skip it
            trace!("Skipping line without colon: '{}'", line);
        }
    }
    headers
}

fn parse_max_age(value: Option<&String>) -> u32 {
    if let Some(v) = value {
        // Try case-insensitive search for "max-age=<number>"
        let lower = v.to_ascii_lowercase();
        if let Some(idx) = lower.find("max-age") {
            // Extract everything after "max-age"
            let after_key = &v[idx + 7..];
            // Skip any whitespace and '=' characters
            let after_eq = after_key.trim_start().trim_start_matches('=').trim_start();
            // Try to parse the first sequence of digits
            let digits: String = after_eq
                .chars()
                .take_while(|c| c.is_ascii_digit())
                .collect();
            if let Ok(age) = digits.parse::<u32>() {
                return age;
            }
        }
        trace!(
            "Could not parse max-age from CACHE-CONTROL: '{}', using default {}",
            v, MAX_AGE
        );
    }
    MAX_AGE
}
