use std::{
    ffi::CStr,
    io::{Error, ErrorKind, IoSlice},
    mem,
    net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6},
    os::fd::{AsRawFd, FromRawFd, RawFd},
};

use byteorder::{ByteOrder, NetworkEndian};
use fehler::throws;
use libc::{c_char, iovec, writev, AF_INET, AF_INET6};
use socket2::{Domain, SockAddr, Socket, Type};
use tracing::{self, instrument};

pub mod kern_control;
pub mod sys;

use kern_control::SysControlSocket;

use super::address::{ensure_valid_ipv6_prefix, ipv6_prefix_octets, parse_addr_spec};
use super::{ifname_to_string, string_to_ifname};
use crate::TunOptions;

#[derive(Debug)]
pub struct TunInterface {
    pub(crate) socket: socket2::Socket,
}

impl TunInterface {
    #[throws]
    #[instrument]
    pub fn new() -> TunInterface {
        Self::new_with_options(TunOptions::new())?
    }

    #[throws]
    #[instrument]
    pub fn new_with_options(options: TunOptions) -> TunInterface {
        let ti = if options.tun_retrieve {
            TunInterface::retrieve().ok_or(Error::new(
                std::io::ErrorKind::NotFound,
                "No tun interface found",
            ))?
        } else {
            TunInterface::connect(0)?
        };
        ti.configure(options)?;
        ti
    }

    pub fn retrieve() -> Option<TunInterface> {
        (3..100)
            .filter_map(|fd| unsafe {
                let peer_addr = socket2::SockAddr::try_init(|storage, len| {
                    *len = mem::size_of::<sys::sockaddr_ctl>() as u32;
                    libc::getpeername(fd, storage as *mut _, len);
                    Ok(())
                })
                .map(|(_, addr)| (fd, addr));
                peer_addr.ok()
            })
            .filter(|(_fd, addr)| {
                let ctl_addr = unsafe { &*(addr.as_ptr() as *const libc::sockaddr_ctl) };
                addr.family() == libc::AF_SYSTEM as u8
                    && ctl_addr.ss_sysaddr == libc::AF_SYS_CONTROL as u16
            })
            .map(|(fd, _)| {
                let socket = unsafe { socket2::Socket::from_raw_fd(fd) };
                TunInterface { socket }
            })
            .next()
    }

    #[throws]
    fn configure(&self, options: TunOptions) {
        for spec in options.address {
            if let Some((addr, prefix_len)) = parse_addr_spec(&spec)? {
                match addr {
                    IpAddr::V4(addr) => self.set_ipv4_addr(addr)?,
                    IpAddr::V6(addr) => self.add_ipv6_addr(addr, prefix_len.unwrap_or(128))?,
                }
            }
        }
    }

    #[throws]
    #[instrument]
    fn connect(index: u32) -> TunInterface {
        use socket2::{Domain, Protocol, Socket, Type};

        let socket = Socket::new(
            Domain::from(sys::AF_SYSTEM),
            Type::DGRAM,
            Some(Protocol::from(sys::SYSPROTO_CONTROL)),
        )?;
        let addr = socket.resolve(sys::UTUN_CONTROL_NAME, index)?;
        socket.connect(&addr)?;

        TunInterface { socket }
    }

    #[throws]
    #[instrument]
    pub fn name(&self) -> String {
        let mut buf = [0 as c_char; sys::IFNAMSIZ];
        let mut len = buf.len() as sys::socklen_t;
        sys::syscall!(getsockopt(
            self.as_raw_fd(),
            sys::SYSPROTO_CONTROL,
            sys::UTUN_OPT_IFNAME,
            buf.as_mut_ptr() as *mut sys::c_void,
            &mut len,
        ))?;
        ifname_to_string(buf)
    }

    #[throws]
    #[instrument]
    fn ifreq(&self) -> sys::ifreq {
        let mut iff: sys::ifreq = unsafe { mem::zeroed() };
        iff.ifr_name = string_to_ifname(&self.name()?);
        iff
    }

    #[throws]
    #[instrument]
    fn in6_ifreq(&self) -> sys::in6_ifreq {
        let mut iff: sys::in6_ifreq = unsafe { mem::zeroed() };
        iff.ifr_name = string_to_ifname(&self.name()?);
        iff
    }

    #[throws]
    #[instrument]
    pub fn set_ipv4_addr(&self, addr: Ipv4Addr) {
        let addr = SockAddr::from(SocketAddrV4::new(addr, 0));
        let mut iff = self.ifreq()?;
        iff.ifr_ifru.ifru_addr = unsafe { *addr.as_ptr() };
        self.perform(|fd| unsafe { sys::if_set_addr(fd, &iff) })?;
        tracing::info!("ipv4_addr_set: {:?} (fd: {:?})", addr, self.as_raw_fd())
    }

    #[throws]
    #[instrument]
    pub fn ipv4_addr(&self) -> Ipv4Addr {
        let mut iff = self.ifreq()?;
        self.perform(|fd| unsafe { sys::if_get_addr(fd, &mut iff) })?;
        let addr = unsafe { *(&iff.ifr_ifru.ifru_addr as *const _ as *const sys::sockaddr_in) };
        Ipv4Addr::from(u32::from_be(addr.sin_addr.s_addr))
    }

    #[throws]
    #[instrument]
    pub fn add_ipv6_addr(&self, addr: Ipv6Addr, prefix_len: u8) {
        ensure_valid_ipv6_prefix(prefix_len)?;

        let mut req: sys::in6_aliasreq = unsafe { mem::zeroed() };
        req.ifra_name = string_to_ifname(&self.name()?);
        req.ifra_addr = ipv6_to_sockaddr(addr);
        req.ifra_prefixmask = ipv6_prefix_mask(prefix_len)?;
        self.perform6(|fd| unsafe { sys::if_add_addr6(fd, &req) })?;
        tracing::info!(
            "ipv6_addr_added: {:?}/{} (fd: {:?})",
            addr,
            prefix_len,
            self.as_raw_fd()
        );
    }

    #[throws]
    #[instrument]
    pub fn remove_ipv6_addr(&self, addr: Ipv6Addr, prefix_len: u8) {
        ensure_valid_ipv6_prefix(prefix_len)?;

        let mut iff = self.in6_ifreq()?;
        iff.ifr_ifru.ifru_addr = ipv6_to_sockaddr(addr);
        iff.ifr_ifru.ifru_prefixmask = ipv6_prefix_mask(prefix_len)?;
        self.perform6(|fd| unsafe { sys::if_del_addr6(fd, &iff) })?;
        tracing::info!(
            "ipv6_addr_removed: {:?}/{} (fd: {:?})",
            addr,
            prefix_len,
            self.as_raw_fd()
        );
    }

    #[throws]
    #[instrument]
    pub fn ipv6_addrs(&self) -> Vec<Ipv6Addr> {
        struct IfAddrs(*mut libc::ifaddrs);

        impl Drop for IfAddrs {
            fn drop(&mut self) {
                if !self.0.is_null() {
                    unsafe { libc::freeifaddrs(self.0) };
                }
            }
        }

        let mut ifaddrs = std::ptr::null_mut();
        unsafe {
            if libc::getifaddrs(&mut ifaddrs) != 0 {
                Err(Error::last_os_error())?;
            }
        }

        let guard = IfAddrs(ifaddrs);
        let interface_name = self.name()?;
        let mut cursor = guard.0;
        let mut result = Vec::new();

        while let Some(ifa) = unsafe { cursor.as_ref() } {
            if !ifa.ifa_addr.is_null()
                && unsafe { (*ifa.ifa_addr).sa_family as i32 } == AF_INET6
                && unsafe { CStr::from_ptr(ifa.ifa_name) }.to_string_lossy() == interface_name
            {
                let sockaddr = unsafe { *(ifa.ifa_addr as *const libc::sockaddr_in6) };
                result.push(Ipv6Addr::from(in6_addr_octets(sockaddr.sin6_addr)));
            }

            cursor = ifa.ifa_next;
        }

        result
    }

    #[throws]
    fn perform<R>(&self, perform: impl FnOnce(RawFd) -> Result<R, nix::Error>) -> R {
        let span = tracing::info_span!("perform", fd = self.as_raw_fd());
        let _enter = span.enter();

        let socket = Socket::new(Domain::IPV4, Type::DGRAM, None)?;
        perform(socket.as_raw_fd())?
    }

    #[throws]
    fn perform6<R>(&self, perform: impl FnOnce(RawFd) -> Result<R, nix::Error>) -> R {
        let span = tracing::info_span!("perform6", fd = self.as_raw_fd());
        let _enter = span.enter();

        let socket = Socket::new(Domain::IPV6, Type::DGRAM, None)?;
        perform(socket.as_raw_fd())?
    }

    #[throws]
    #[instrument]
    pub fn mtu(&self) -> i32 {
        let mut iff = self.ifreq()?;
        self.perform(|fd| unsafe { sys::if_get_mtu(fd, &mut iff) })?;
        let mtu = unsafe { iff.ifr_ifru.ifru_mtu };

        mtu
    }

    #[throws]
    #[instrument]
    pub fn set_mtu(&self, mtu: i32) {
        let mut iff = self.ifreq()?;
        iff.ifr_ifru.ifru_mtu = mtu;
        self.perform(|fd| unsafe { sys::if_set_mtu(fd, &iff) })?;
        tracing::info!("mtu_set: {:?} (fd: {:?})", mtu, self.as_raw_fd())
    }

    #[throws]
    #[instrument]
    pub fn netmask(&self) -> Ipv4Addr {
        let mut iff = self.ifreq()?;
        self.perform(|fd| unsafe { sys::if_get_netmask(fd, &mut iff) })?;

        let netmask =
            unsafe { *(&iff.ifr_ifru.ifru_netmask as *const _ as *const sys::sockaddr_in) };

        Ipv4Addr::from(u32::from_be(netmask.sin_addr.s_addr))
    }

    #[throws]
    #[instrument]
    pub fn set_netmask(&self, addr: Ipv4Addr) {
        let addr = SockAddr::from(SocketAddrV4::new(addr, 0));
        let mut iff = self.ifreq()?;
        iff.ifr_ifru.ifru_netmask = unsafe { *addr.as_ptr() };
        self.perform(|fd| unsafe { sys::if_set_netmask(fd, &iff) })?;
        tracing::info!(
            "netmask_set: {:?} (fd: {:?})",
            unsafe { iff.ifr_ifru.ifru_netmask },
            self.as_raw_fd()
        )
    }

    #[throws]
    #[instrument]
    pub fn send(&self, buf: &[u8]) -> usize {
        let proto = match buf[0] >> 4 {
            6 => Ok(AF_INET6),
            4 => Ok(AF_INET),
            _ => Err(Error::new(ErrorKind::InvalidInput, "Invalid IP version")),
        }?;
        let mut pbuf = [0; 4];
        NetworkEndian::write_i32(&mut pbuf, proto);

        let bufs = [IoSlice::new(&pbuf), IoSlice::new(buf)];
        let bytes_written: isize = unsafe {
            writev(
                self.as_raw_fd(),
                bufs.as_ptr() as *const iovec,
                bufs.len() as i32,
            )
        };
        bytes_written
            .try_into()
            .map_err(|_| Error::new(ErrorKind::Other, "Conversion error"))?
    }
}

#[inline]
fn in6_addr_octets(addr: libc::in6_addr) -> [u8; 16] {
    addr.s6_addr
}

fn ipv6_to_sockaddr(addr: Ipv6Addr) -> libc::sockaddr_in6 {
    let sockaddr = SockAddr::from(SocketAddrV6::new(addr, 0, 0, 0));
    unsafe { *(sockaddr.as_ptr() as *const libc::sockaddr_in6) }
}

#[throws]
fn ipv6_prefix_mask(prefix_len: u8) -> libc::sockaddr_in6 {
    let octets = ipv6_prefix_octets(prefix_len)?;
    ipv6_to_sockaddr(Ipv6Addr::from(octets))
}
