// SPDX-License-Identifier: MPL-2.0
// Copyright (C) 2026 Petr Hodina <petr.hodina@protonmail.com>

//! Thin wrapper around `AF_QIPCRTR` sockets.
//!
//! We talk to the kernel via raw `libc` syscalls because nix's
//! `AddressFamily` enum doesn't include `AF_QIPCRTR` and its `SockaddrLike`
//! trait is sealed against external impls.
//!
//! Reference: linux/include/uapi/linux/qrtr.h
//! Reference: Linaro qrtr-tools (qrtr-cfg, qrtr-ns, qrtr-lookup).

use std::mem::{size_of, MaybeUninit};
use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd};
use std::time::Duration;

use tracing::{debug, trace};

use crate::error::{Error, Result};

pub const AF_QIPCRTR: libc::c_int = 42;
pub const QRTR_PORT_CTRL: u32 = 0xFFFF_FFFE;
pub const QRTR_NODE_BCAST: u32 = 0xFFFF_FFFF;

/// Control-pkt cmd values. Match `QRTR_TYPE_*` from linux/qrtr.h exactly.
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum CtrlPkt {
    Data = 1,
    Hello = 2,
    Bye = 3,
    NewServer = 4,
    DelServer = 5,
    DelClient = 6,
    ResumeTx = 7,
    Exit = 8,
    Ping = 9,
    NewLookup = 10,
    DelLookup = 11,
}

/// Control-plane packet layout shared by NEW_LOOKUP, NEW_SERVER, DEL_SERVER.
/// Five little-endian u32 fields = exactly 20 bytes — must match the kernel's
/// `struct qrtr_ctrl_pkt` byte-for-byte. qrtr-ns drops oversized packets.
#[repr(C)]
#[derive(Debug, Default, Clone, Copy)]
struct CtrlMsg {
    cmd: u32,
    service: u32,
    instance: u32, // upper 16 bits = version, lower 16 bits = instance
    node: u32,
    port: u32,
}

impl CtrlMsg {
    pub fn to_bytes(self) -> [u8; size_of::<CtrlMsg>()] {
        // SAFETY: repr(C) POD, six u32 fields, no padding, no niches.
        unsafe { std::mem::transmute(self) }
    }

    fn from_bytes(buf: &[u8]) -> Option<Self> {
        if buf.len() < size_of::<CtrlMsg>() {
            return None;
        }
        let mut out = CtrlMsg::default();
        // SAFETY: target is plain POD, source length checked above.
        unsafe {
            std::ptr::copy_nonoverlapping(buf.as_ptr(), &mut out as *mut _ as *mut u8, size_of::<CtrlMsg>());
        }
        Some(out)
    }
}

/// `sockaddr_qrtr` from linux/qrtr.h.
///
/// Layout: u16 family, [u8; 2] padding (alignment), u32 node, u32 port = 12B.
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct SockaddrQrtr {
    sq_family: libc::sa_family_t,
    sq_node: u32,
    sq_port: u32,
}

impl SockaddrQrtr {
    pub fn new(node: u32, port: u32) -> Self {
        Self { sq_family: AF_QIPCRTR as libc::sa_family_t, sq_node: node, sq_port: port }
    }

    pub fn node(&self) -> u32 {
        self.sq_node
    }

    pub fn port(&self) -> u32 {
        self.sq_port
    }
}

pub struct QrtrSocket {
    fd: OwnedFd,
}

impl QrtrSocket {
    pub fn open() -> Result<Self> {
        // SAFETY: socket(2) is always safe to call; we check the return.
        let raw = unsafe { libc::socket(AF_QIPCRTR, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0) };
        if raw < 0 {
            let e = std::io::Error::last_os_error();
            return Err(match e.raw_os_error() {
                Some(libc::EAFNOSUPPORT) => Error::Qrtr("kernel does not support AF_QIPCRTR (CONFIG_QRTR=n?)".into()),
                _ => Error::Io(e),
            });
        }
        // SAFETY: socket(2) returned a valid fd; we take exclusive ownership.
        let fd = unsafe { OwnedFd::from_raw_fd(raw) };

        // No explicit bind. The kernel's `qrtr_bind` rejects any sq_node that
        // doesn't match `qrtr_local_nid` (often set by qrtr-ns to something
        // other than 0), so manual bind is fragile. `qrtr_sendmsg` calls
        // `qrtr_autobind` on first send, which uses the correct local nid.
        Ok(Self { fd })
    }

    /// Discover our local node id via `getsockname()`. Only meaningful after
    /// autobind has run (i.e. after the first `send_to`).
    pub fn local_node(&self) -> Result<u32> {
        let mut addr = MaybeUninit::<SockaddrQrtr>::zeroed();
        let mut addrlen = size_of::<SockaddrQrtr>() as libc::socklen_t;
        // SAFETY: kernel writes up to addrlen bytes into addr.
        let rc = unsafe { libc::getsockname(self.fd.as_raw_fd(), addr.as_mut_ptr() as *mut libc::sockaddr, &mut addrlen) };
        if rc != 0 {
            return Err(Error::Io(std::io::Error::last_os_error()));
        }
        // SAFETY: kernel populated the AF_QIPCRTR address.
        Ok(unsafe { addr.assume_init() }.sq_node)
    }

    pub fn set_recv_timeout(&self, dur: Duration) -> Result<()> {
        let tv = libc::timeval { tv_sec: dur.as_secs() as libc::time_t, tv_usec: dur.subsec_micros() as libc::suseconds_t };
        // SAFETY: setsockopt with valid timeval and matching length.
        let rc = unsafe {
            libc::setsockopt(
                self.fd.as_raw_fd(),
                libc::SOL_SOCKET,
                libc::SO_RCVTIMEO,
                &tv as *const _ as *const libc::c_void,
                size_of::<libc::timeval>() as libc::socklen_t,
            )
        };
        if rc != 0 {
            return Err(Error::Io(std::io::Error::last_os_error()));
        }
        Ok(())
    }

    pub fn send_to(&self, buf: &[u8], dst: SockaddrQrtr) -> Result<()> {
        // SAFETY: pointers and lengths match what the kernel expects.
        let rc = unsafe {
            libc::sendto(
                self.fd.as_raw_fd(),
                buf.as_ptr() as *const libc::c_void,
                buf.len(),
                0,
                &dst as *const _ as *const libc::sockaddr,
                size_of::<SockaddrQrtr>() as libc::socklen_t,
            )
        };
        if rc < 0 {
            return Err(Error::Io(std::io::Error::last_os_error()));
        }
        Ok(())
    }

    pub fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SockaddrQrtr)> {
        let mut addr = MaybeUninit::<SockaddrQrtr>::zeroed();
        let mut addrlen = size_of::<SockaddrQrtr>() as libc::socklen_t;
        // SAFETY: kernel writes up to addrlen bytes into addr; we read after.
        let rc = unsafe {
            libc::recvfrom(
                self.fd.as_raw_fd(),
                buf.as_mut_ptr() as *mut libc::c_void,
                buf.len(),
                0,
                addr.as_mut_ptr() as *mut libc::sockaddr,
                &mut addrlen,
            )
        };
        if rc < 0 {
            return Err(Error::Io(std::io::Error::last_os_error()));
        }
        // SAFETY: kernel always populates AF_QIPCRTR addresses fully.
        let addr = unsafe { addr.assume_init() };
        Ok((rc as usize, addr))
    }

    /// Ask the QRTR name service to publish all servers matching
    /// `(service, instance)`. Existing servers are streamed back as
    /// `NEW_SERVER` ctrl messages until a sentinel (all-zero node/port) is
    /// received.
    ///
    /// Returns the first matching `(node, port)` found, or
    /// [`Error::ServiceNotFound`] on timeout.
    pub fn lookup_service(&self, service: u32, instance: u32, timeout: Duration) -> Result<SockaddrQrtr> {
        self.set_recv_timeout(timeout)?;

        let req = CtrlMsg { cmd: CtrlPkt::NewLookup as u32, service, instance, ..Default::default() };
        // qrtr-lookup pattern (verified via strace): send NEW_LOOKUP
        // unicast to our own node's CTRL port. The kernel populates
        // `sq_node` at socket-creation time so getsockname works without
        // any prior send.
        let local = self.local_node()?;
        debug!(service, instance, local_node = local, "sending NEW_LOOKUP");
        self.send_to(&req.to_bytes(), SockaddrQrtr::new(local, QRTR_PORT_CTRL))?;

        let mut buf = [0u8; 256];
        loop {
            let (n, src) = match self.recv_from(&mut buf) {
                Ok(v) => v,
                Err(Error::Io(e)) if e.kind() == std::io::ErrorKind::WouldBlock => {
                    return Err(Error::ServiceNotFound);
                }
                Err(e) => return Err(e),
            };
            let pkt = CtrlMsg::from_bytes(&buf[..n]).ok_or_else(|| Error::Qrtr(format!("short ctrl pkt ({n} bytes)")))?;
            trace!(
                cmd = pkt.cmd,
                service = pkt.service,
                node = pkt.node,
                port = pkt.port,
                from_node = src.node(),
                from_port = src.port(),
                "ctrl rx"
            );
            if pkt.cmd != CtrlPkt::NewServer as u32 {
                continue;
            }
            // Sentinel "no more servers" packet has node=0 port=0.
            if pkt.node == 0 && pkt.port == 0 {
                return Err(Error::ServiceNotFound);
            }
            if pkt.service == service {
                let addr = SockaddrQrtr::new(pkt.node, pkt.port);
                self.drain_pending();
                return Ok(addr);
            }
        }
    }

    /// Drain any control packets still queued (e.g. the all-zero NEW_SERVER
    /// sentinel sent by qrtr-ns after the matching one) so they don't bleed
    /// into the QMI traffic on this socket.
    fn drain_pending(&self) {
        // Use a tight non-blocking timeout to flush whatever's there.
        let _ = self.set_recv_timeout(Duration::from_millis(50));
        let mut buf = [0u8; 256];
        while let Ok((n, src)) = self.recv_from(&mut buf) {
            trace!(n, from_node = src.node(), from_port = src.port(), "drained pending");
        }
    }
}
