use core::ffi::c_void;
use std::alloc::alloc_zeroed;
use std::alloc::dealloc;
use std::alloc::Layout;
use std::ffi::CString;
use std::mem::size_of;
use std::mem::MaybeUninit;
use std::os::raw::c_char;
use std::os::raw::c_ulong;
use std::ptr;
use std::ptr::addr_of;
use std::ptr::NonNull;

use libbpf_sys::bpf_link;
use libbpf_sys::bpf_map;
use libbpf_sys::bpf_map_skeleton;
use libbpf_sys::bpf_object;
use libbpf_sys::bpf_object_skeleton;
use libbpf_sys::bpf_prog_skeleton;
use libbpf_sys::bpf_program;

use crate::error::IntoError as _;
use crate::util;
use crate::AsRawLibbpf;
use crate::Error;
use crate::Object;
use crate::ObjectBuilder;
use crate::OpenObject;
use crate::Result;

#[derive(Debug)]
struct MapSkelConfig {
    name: String,
    map: Box<*mut bpf_map>,
    mmaped: Option<Box<*mut c_void>>,
}

#[derive(Debug)]
struct ProgSkelConfig {
    name: String,
    prog: Box<*mut bpf_program>,
    link: Box<*mut bpf_link>,
}

#[expect(missing_docs)]
#[derive(Debug)]
pub struct ObjectSkeletonConfigBuilder<'dat> {
    data: &'dat [u8],
    obj: Box<*mut bpf_object>,
    name: Option<String>,
    maps: Vec<MapSkelConfig>,
    progs: Vec<ProgSkelConfig>,
}

fn str_to_cstring_and_pool(s: &str, pool: &mut Vec<CString>) -> Result<*const c_char> {
    let cname = util::str_to_cstring(s)?;
    let p = cname.as_ptr();
    pool.push(cname);

    Ok(p)
}

impl<'dat> ObjectSkeletonConfigBuilder<'dat> {
    /// Construct a new instance
    ///
    /// `object_data` is the contents of the `.o` from clang
    ///
    /// `p` is a reference to the pointer where `libbpf_sys::bpf_object` should be
    /// stored/retrieved
    pub fn new(object_data: &'dat [u8]) -> Self {
        Self {
            data: object_data,
            obj: Box::new(ptr::null_mut()),
            name: None,
            maps: Vec::new(),
            progs: Vec::new(),
        }
    }

    #[expect(missing_docs)]
    pub fn name<T: AsRef<str>>(&mut self, name: T) -> &mut Self {
        self.name = Some(name.as_ref().to_string());
        self
    }

    /// Adds a map to the config
    ///
    /// Set `mmaped` to `true` if the map is mmap'able to userspace
    pub fn map<T: AsRef<str>>(&mut self, name: T, mmaped: bool) -> &mut Self {
        let m = if mmaped {
            Some(Box::new(ptr::null_mut()))
        } else {
            None
        };

        self.maps.push(MapSkelConfig {
            name: name.as_ref().to_string(),
            map: Box::new(ptr::null_mut()),
            mmaped: m,
        });

        self
    }

    /// Adds a prog to the config
    pub fn prog<T: AsRef<str>>(&mut self, name: T) -> &mut Self {
        self.progs.push(ProgSkelConfig {
            name: name.as_ref().to_string(),
            prog: Box::new(ptr::null_mut()),
            link: Box::new(ptr::null_mut()),
        });

        self
    }

    fn build_maps(
        maps: &mut [MapSkelConfig],
        s: &mut bpf_object_skeleton,
        string_pool: &mut Vec<CString>,
    ) -> Option<Layout> {
        if maps.is_empty() {
            return None;
        }

        s.map_cnt = maps.len() as i32;
        s.map_skel_sz = size_of::<bpf_map_skeleton>() as i32;

        let layout = Layout::array::<bpf_map_skeleton>(maps.len())
            .expect("Failed to allocate memory for maps skeleton");

        unsafe {
            s.maps = alloc_zeroed(layout) as *mut bpf_map_skeleton;
            for (i, map) in maps.iter_mut().enumerate() {
                let current_map = s.maps.add(i);

                // Opt to panic on error here. We've already allocated memory and we'd rather not
                // leak. Extremely unlikely to have invalid unicode anyways.
                (*current_map).name = str_to_cstring_and_pool(&map.name, string_pool)
                    .expect("Invalid unicode in map name");
                (*current_map).map = &mut *map.map;
                (*current_map).mmaped = if let Some(ref mut mmaped) = map.mmaped {
                    &mut **mmaped
                } else {
                    ptr::null_mut()
                };
            }
        }

        Some(layout)
    }

    fn build_progs(
        progs: &mut [ProgSkelConfig],
        s: &mut bpf_object_skeleton,
        string_pool: &mut Vec<CString>,
    ) -> Option<Layout> {
        if progs.is_empty() {
            return None;
        }

        s.prog_cnt = progs.len() as i32;
        s.prog_skel_sz = size_of::<bpf_prog_skeleton>() as i32;

        let layout = Layout::array::<bpf_prog_skeleton>(progs.len())
            .expect("Failed to allocate memory for progs skeleton");

        unsafe {
            s.progs = alloc_zeroed(layout) as *mut bpf_prog_skeleton;
            for (i, prog) in progs.iter_mut().enumerate() {
                let current_prog = s.progs.add(i);

                // See above for `expect()` rationale
                (*current_prog).name = str_to_cstring_and_pool(&prog.name, string_pool)
                    .expect("Invalid unicode in prog name");
                (*current_prog).prog = &mut *prog.prog;
                (*current_prog).link = &mut *prog.link;
            }
        }

        Some(layout)
    }

    #[expect(missing_docs)]
    pub fn build(mut self) -> Result<ObjectSkeletonConfig<'dat>> {
        // Holds `CString`s alive so pointers to them stay valid
        let mut string_pool = Vec::new();

        let mut s = libbpf_sys::bpf_object_skeleton {
            sz: size_of::<bpf_object_skeleton>() as c_ulong,
            ..Default::default()
        };

        if let Some(ref n) = self.name {
            s.name = str_to_cstring_and_pool(n, &mut string_pool)?;
        }

        // libbpf_sys will use it as const despite the signature
        s.data = self.data.as_ptr() as *mut c_void;
        s.data_sz = self.data.len() as c_ulong;

        // Give s ownership over the box
        s.obj = Box::into_raw(self.obj);

        let maps_layout = Self::build_maps(&mut self.maps, &mut s, &mut string_pool);
        let progs_layout = Self::build_progs(&mut self.progs, &mut s, &mut string_pool);

        Ok(ObjectSkeletonConfig {
            inner: s,
            maps: self.maps,
            progs: self.progs,
            maps_layout,
            progs_layout,
            _data: self.data,
            _string_pool: string_pool,
        })
    }
}

/// Helper struct that wraps a `libbpf_sys::bpf_object_skeleton`.
///
/// This struct will:
/// * ensure lifetimes are valid for dependencies (pointers, data buffer)
/// * free any allocated memory on drop
///
/// This struct can be moved around at will. Upon drop, all allocated resources will be freed
#[derive(Debug)]
pub struct ObjectSkeletonConfig<'dat> {
    inner: bpf_object_skeleton,
    maps: Vec<MapSkelConfig>,
    progs: Vec<ProgSkelConfig>,
    /// Layout necessary to `dealloc` memory
    maps_layout: Option<Layout>,
    /// Same as above
    progs_layout: Option<Layout>,
    /// Hold this reference so that compiler guarantees buffer lives as long as us
    _data: &'dat [u8],
    /// Hold strings alive so pointers to them stay valid
    _string_pool: Vec<CString>,
}

impl ObjectSkeletonConfig<'_> {
    /// Returns the `mmaped` pointer for a map at the specified `index`.
    ///
    /// The index is determined by the order in which the map was passed to
    /// `ObjectSkeletonConfigBuilder::map`. Index starts at 0.
    ///
    /// Warning: the returned pointer is only valid while the `ObjectSkeletonConfig` is alive.
    pub fn map_mmap_ptr(&self, index: usize) -> Result<*mut c_void> {
        if index >= self.maps.len() {
            return Err(Error::with_invalid_data(format!(
                "Invalid map index: {index}"
            )));
        }

        let p = self.maps[index]
            .mmaped
            .as_ref()
            .ok_or_invalid_data(|| "Map does not have mmaped ptr")?;
        Ok(**p)
    }

    /// Returns the link pointer for a prog at the specified `index`.
    ///
    /// The index is determined by the order in which the prog was passed to
    /// `ObjectSkeletonConfigBuilder::prog`. Index starts at 0.
    ///
    /// Warning: the returned pointer is only valid while the `ObjectSkeletonConfig` is alive.
    pub fn prog_link_ptr(&self, index: usize) -> Result<*mut bpf_link> {
        if index >= self.progs.len() {
            return Err(Error::with_invalid_data(format!(
                "Invalid prog index: {index}"
            )));
        }

        Ok(*self.progs[index].link)
    }
}

impl AsRawLibbpf for ObjectSkeletonConfig<'_> {
    type LibbpfType = libbpf_sys::bpf_object_skeleton;

    /// Retrieve the underlying [`libbpf_sys::bpf_object_skeleton`].
    fn as_libbpf_object(&self) -> NonNull<Self::LibbpfType> {
        // SAFETY: A reference is always a valid pointer.
        unsafe { NonNull::new_unchecked(addr_of!(self.inner).cast_mut()) }
    }
}

impl Drop for ObjectSkeletonConfig<'_> {
    // Note we do *not* run `libbpf_sys::bpf_object__destroy_skeleton` here.
    //
    // Couple reasons:
    //
    // 1) We did not allocate `libbpf_sys::bpf_object_skeleton` on the heap and
    //    `libbpf_sys::bpf_object__destroy_skeleton` will try to free from heap
    //
    // 2) `libbpf_object_skeleton` assumes it "owns" the object and everything inside it.
    //    libbpf-cargo's generated skeleton instead gives ownership of the object to
    //    libbpf-rs::*Object. The destructors in libbpf-rs::*Object will know when and how to do
    //    cleanup.
    fn drop(&mut self) {
        assert_eq!(self.maps_layout.is_none(), self.inner.maps.is_null());
        assert_eq!(self.progs_layout.is_none(), self.inner.progs.is_null());

        if let Some(layout) = self.maps_layout {
            unsafe {
                dealloc(self.inner.maps as _, layout);
            }
        }

        if let Some(layout) = self.progs_layout {
            unsafe {
                dealloc(self.inner.progs as _, layout);
            }
        }

        let () = drop(unsafe { Box::from_raw(self.inner.obj) });
    }
}

/// A trait for skeleton builder.
pub trait SkelBuilder<'obj> {
    /// Define that when BPF object is opened, the returned type should implement the [`OpenSkel`]
    /// trait
    type Output: OpenSkel<'obj>;

    /// Open eBPF object and return [`OpenSkel`]
    fn open(self, object: &'obj mut MaybeUninit<OpenObject>) -> Result<Self::Output>;

    /// Open eBPF object with [`libbpf_sys::bpf_object_open_opts`] and return [`OpenSkel`]
    fn open_opts(
        self,
        open_opts: libbpf_sys::bpf_object_open_opts,
        object: &'obj mut MaybeUninit<OpenObject>,
    ) -> Result<Self::Output>;

    /// Get a reference to [`ObjectBuilder`]
    fn object_builder(&self) -> &ObjectBuilder;

    /// Get a mutable reference to [`ObjectBuilder`]
    fn object_builder_mut(&mut self) -> &mut ObjectBuilder;
}

/// A trait for opened skeleton.
///
/// In addition to the methods defined in this trait, skeletons that implement this trait will also
/// have bespoke implementations of a few additional methods to facilitate access to global
/// variables of the BPF program. These methods will be named `bss()`, `data()`, and `rodata()`.
/// Each corresponds to the variables stored in the BPF ELF program section of the same name.
/// However if your BPF program lacks one of these sections the corresponding rust method will not
/// be generated.
///
/// The type of the value returned by each of these methods will be specific to your BPF program.
/// A common convention is to define a single global variable in the BPF program with a struct type
/// containing a field for each configuration parameter <sup>\[[source]\]</sup>. libbpf-rs
/// auto-generates this pattern for you without you having to define such a struct type in your BPF
/// program. It does this by examining each of the global variables in your BPF program's `.bss`,
/// `.data`, and `.rodata` sections and then creating Rust struct types. Since these struct types
/// are specific to the layout of your BPF program, they are not documented in this crate. However
/// you can see documentation for them by running `cargo doc` in your own project and looking at
/// the `imp` module. You can also view their implementation by looking at the generated skeleton
/// rust source file. The use of these methods can also be seen in the examples 'capable',
/// 'runqslower', and 'tproxy'.
///
/// If you ever doubt whether libbpf-rs has placed a particular variable in the correct struct
/// type, you can see which section each global variable is stored in by examining the output of
/// the following command (after a successful build):
///
/// ```sh
/// bpf-objdump --syms ./target/bpf/*.bpf.o
/// ```
///
/// [source]: https://nakryiko.com/posts/bcc-to-libbpf-howto-guide/#application-configuration
pub trait OpenSkel<'obj> {
    /// Define that when BPF object is loaded, the returned type should implement the [`Skel`] trait
    type Output: Skel<'obj>;

    /// Load BPF object and return [`Skel`].
    fn load(self) -> Result<Self::Output>;

    /// Get a reference to [`OpenObject`].
    fn open_object(&self) -> &OpenObject;

    /// Get a mutable reference to [`OpenObject`].
    fn open_object_mut(&mut self) -> &mut OpenObject;
}

/// A trait for loaded skeleton.
pub trait Skel<'obj> {
    /// Attach BPF object.
    fn attach(&mut self) -> Result<()> {
        unimplemented!()
    }
    /// Get a reference to [`Object`].
    fn object(&self) -> &Object;

    /// Get a mutable reference to [`Object`].
    fn object_mut(&mut self) -> &mut Object;
}
