//! Network addresses and URLs that combine an endpoint with a node identity.
//!
//! In NWEP every address binds a network endpoint (IP + port) to a [`NodeId`].
//! This binding prevents address spoofing: a peer connecting to an address must
//! prove possession of the corresponding private key during the mutual-auth
//! TLS handshake.
//!
//! The base58-encoded [`Addr`] packs IP, port, and [`NodeId`] into a compact
//! token that can be shared as a connection target. [`Url`] extends this with a
//! path component, forming `web://<base58-addr>/<path>` URLs.

use crate::ffi;
use crate::error::{check, Error, ERR_IDENTITY_INVALID_ADDR};
use crate::types::{NodeId, BASE58_ADDR_LEN, URL_MAX_LEN};
use std::ffi::CString;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

/// `Addr` combines a network endpoint (IP + port) with a cryptographic node identity.
///
/// The IP is stored internally as 16 bytes in IPv4-mapped IPv6 format
/// (`::ffff:x.x.x.x`) for IPv4 addresses; pure IPv6 addresses are stored as-is.
/// Use [`Addr::new_ipv4`] / [`Addr::new_ipv6`] to construct addresses from standard
/// Rust IP types, and [`Addr::encode`] / [`Addr::decode`] to convert to/from the
/// compact base58 wire format.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Addr {
    /// 16-byte IP address in IPv4-mapped IPv6 form (`::ffff:x.x.x.x` for IPv4).
    pub ip: [u8; 16],
    /// The 32-byte node identifier of the peer at this address.
    pub node_id: NodeId,
    /// UDP port number.
    pub port: u16,
}

impl Addr {
    /// `new_ipv4` creates an `Addr` from an IPv4 address, storing it as IPv4-mapped IPv6.
    pub fn new_ipv4(ipv4: Ipv4Addr, node_id: NodeId, port: u16) -> Self {
        let mut ip = [0u8; 16];
        // IPv4-mapped IPv6: ::ffff:x.x.x.x
        ip[10] = 0xff;
        ip[11] = 0xff;
        let octets = ipv4.octets();
        ip[12..16].copy_from_slice(&octets);
        Addr { ip, node_id, port }
    }

    /// `new_ipv6` creates an `Addr` from a pure IPv6 address.
    pub fn new_ipv6(ipv6: Ipv6Addr, node_id: NodeId, port: u16) -> Self {
        Addr { ip: ipv6.octets(), node_id, port }
    }

    /// `ip_addr` returns the address as a standard [`IpAddr`], converting IPv4-mapped IPv6 back to `V4`.
    pub fn ip_addr(&self) -> IpAddr {
        // Check if IPv4-mapped
        if self.ip[..10].iter().all(|&b| b == 0) && self.ip[10] == 0xff && self.ip[11] == 0xff {
            let octets = [self.ip[12], self.ip[13], self.ip[14], self.ip[15]];
            IpAddr::V4(Ipv4Addr::from(octets))
        } else {
            IpAddr::V6(Ipv6Addr::from(self.ip))
        }
    }

    /// `encode` serializes the address to a base58 string for use as a connection target.
    ///
    /// # Errors
    ///
    /// Returns `Err(ERR_IDENTITY_INVALID_ADDR)` if the IP/port/node_id combination is invalid.
    pub fn encode(&self) -> Result<String, Error> {
        let ffi_addr = self.to_ffi();
        let mut buf = vec![0u8; BASE58_ADDR_LEN + 2];
        let n = unsafe { ffi::nwep_addr_encode(buf.as_mut_ptr() as *mut i8, buf.len(), &ffi_addr) };
        if n == 0 {
            Err(Error::from_code(ERR_IDENTITY_INVALID_ADDR))
        } else {
            Ok(String::from_utf8_lossy(&buf[..n]).into_owned())
        }
    }

    /// `decode` parses a base58-encoded address string produced by [`encode`](Addr::encode).
    ///
    /// # Errors
    ///
    /// Returns `Err(ERR_IDENTITY_INVALID_ADDR)` if `encoded` is not a valid NWEP address.
    pub fn decode(encoded: &str) -> Result<Self, Error> {
        let c = CString::new(encoded).map_err(|_| Error::from_code(ERR_IDENTITY_INVALID_ADDR))?;
        let mut ffi_addr = ffi::nwep_addr { ip: [0u8; 16], nodeid: ffi::nwep_nodeid { data: [0u8; 32] }, port: 0 };
        check(unsafe { ffi::nwep_addr_decode(&mut ffi_addr, c.as_ptr()) })?;
        Ok(Addr::from_ffi(&ffi_addr))
    }

    pub(crate) fn to_ffi(&self) -> ffi::nwep_addr {
        ffi::nwep_addr {
            ip: self.ip,
            nodeid: ffi::nwep_nodeid { data: self.node_id.0 },
            port: self.port,
        }
    }

    pub(crate) fn from_ffi(a: &ffi::nwep_addr) -> Self {
        Addr { ip: a.ip, node_id: NodeId(a.nodeid.data), port: a.port }
    }
}

/// `Url` is a NWEP URL combining an [`Addr`] with a path component.
///
/// The string form is `web://<base58-addr>/<path>`. Use [`Url::parse`] to parse
/// a URL string and [`Url::format`] (or the [`Display`](std::fmt::Display) impl)
/// to serialize it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Url {
    /// The network address and node identity of the target.
    pub addr: Addr,
    /// The request path (e.g. `"/hello"`).
    pub path: String,
}

impl Url {
    /// `parse` parses a `web://` URL string into a `Url`.
    ///
    /// # Errors
    ///
    /// Returns `Err(ERR_IDENTITY_INVALID_ADDR)` if `s` is not a valid NWEP URL.
    pub fn parse(s: &str) -> Result<Self, Error> {
        let c = CString::new(s).map_err(|_| Error::from_code(ERR_IDENTITY_INVALID_ADDR))?;
        let mut ffi_url = unsafe { std::mem::zeroed::<ffi::nwep_url>() };
        check(unsafe { ffi::nwep_url_parse(&mut ffi_url, c.as_ptr()) })?;
        let path = unsafe {
            std::ffi::CStr::from_ptr(ffi_url.path.as_ptr()).to_string_lossy().into_owned()
        };
        Ok(Url { addr: Addr::from_ffi(&ffi_url.addr), path })
    }

    /// `format` serializes the URL to a `web://` string.
    ///
    /// # Errors
    ///
    /// Returns `Err(ERR_IDENTITY_INVALID_ADDR)` if the embedded address is invalid.
    pub fn format(&self) -> Result<String, Error> {
        let ffi_url = self.to_ffi_url();
        let mut buf = vec![0u8; URL_MAX_LEN];
        let n = unsafe { ffi::nwep_url_format(buf.as_mut_ptr() as *mut i8, buf.len(), &ffi_url) };
        if n == 0 {
            Err(Error::from_code(ERR_IDENTITY_INVALID_ADDR))
        } else {
            Ok(String::from_utf8_lossy(&buf[..n]).into_owned())
        }
    }

    fn to_ffi_url(&self) -> ffi::nwep_url {
        self.to_ffi()
    }

    pub(crate) fn to_ffi(&self) -> ffi::nwep_url {
        let mut ffi_url = unsafe { std::mem::zeroed::<ffi::nwep_url>() };
        ffi_url.addr = self.addr.to_ffi();
        let path_bytes = self.path.as_bytes();
        let len = path_bytes.len().min(255);
        // path is [i8; 256] in C
        for (i, &b) in path_bytes[..len].iter().enumerate() {
            ffi_url.path[i] = b as i8;
        }
        ffi_url
    }
}

impl std::fmt::Display for Url {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.format() {
            Ok(s) => f.write_str(&s),
            Err(_) => write!(f, "web://[invalid]:{}{}", self.addr.port, self.path),
        }
    }
}
