#![allow(clippy::similar_names)]
use crate::tar::Error;
use deku::prelude::*;
use std::{
    env,
    ffi::{CStr, OsStr},
    fmt::{self, Write},
    fs::{self, Metadata},
    io,
    ops::Deref,
    os::{linux::fs::MetadataExt, unix::fs::FileTypeExt},
    path::PathBuf,
};

#[repr(u8)]
pub enum FileType {
    Normal = 0x30,
    Hardlink = 0x31,
    Symlink = 0x32,
    Char = 0x33,
    Block = 0x34,
    Dir = 0x35,
    FIFO = 0x36,
    Unknown = 0x00,
}

impl<T> From<T> for FileType
where
    T: Deref<Target = Metadata>,
{
    fn from(meta: T) -> Self {
        if meta.is_dir() {
            return FileType::Dir;
        }

        let file_type = meta.file_type();
        if file_type.is_fifo() {
            return FileType::FIFO;
        } else if file_type.is_char_device() {
            return FileType::Char;
        } else if file_type.is_block_device() {
            return FileType::Block;
        } else if file_type.is_symlink() {
            return FileType::Symlink;
        } else if file_type.is_file() {
            return FileType::Normal;
        }

        FileType::Unknown
    }
}

#[derive(Clone)]
pub struct Owner {
    pub uid: u32,
    pub gid: u32,
    pub username: String,
    pub groupname: String,
}

impl Default for Owner {
    fn default() -> Self {
        Self {
            uid: 0,
            gid: 0,
            username: "root".into(),
            groupname: "root".into(),
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, DekuRead, DekuWrite)]
#[deku(endian = "little")]
pub struct Header {
    pub(crate) fname: [u8; 100],
    pub(crate) mode: [u8; 8],
    pub(crate) uid: [u8; 8],
    pub(crate) gid: [u8; 8],
    pub(crate) size: [u8; 12],
    pub(crate) mtime: [u8; 12],
    pub(crate) header_checksum: [u8; 8],
    pub(crate) link_indicator: [u8; 1],
    pub(crate) link_name: [u8; 100],
    pub(crate) ustar_magic: [u8; 6],
    pub(crate) ustar_version: [u8; 2],
    pub(crate) username: [u8; 32],
    pub(crate) groupname: [u8; 32],
    pub(crate) device_major: [u8; 8],
    pub(crate) device_minor: [u8; 8],
    pub(crate) file_prefix: [u8; 155],
    pub(crate) reserved: [u8; 12],
}

impl Default for Header {
    fn default() -> Self {
        Self {
            fname: [0; 100],
            mode: [0; 8],
            uid: [0; 8],
            gid: [0; 8],
            size: [0; 12],
            mtime: [0; 12],
            header_checksum: [0x20; 8],
            link_indicator: [0; 1],
            link_name: [0; 100],
            ustar_magic: [0x75, 0x73, 0x74, 0x61, 0x72, 0x20],
            ustar_version: [0x20, 0x00],
            username: [0; 32],
            groupname: [0; 32],
            device_major: [0; 8],
            device_minor: [0; 8],
            file_prefix: [0; 155],
            reserved: [0; 12],
        }
    }
}

impl Header {
    /// Get the filename of this archive member
    ///
    /// # Example
    ///
    /// ```
    /// use hpk::tar::Header;
    ///
    /// let header = Header::new("test/1.txt").unwrap();
    /// let filename = header.filename().unwrap();
    /// assert_eq!(filename.as_str(), "test/1.txt");
    /// ```
    pub fn filename(&self) -> Result<String, fmt::Error> {
        let mut s = String::new();
        for c in self.fname {
            if c == 0 {
                break;
            }
            write!(s, "{}", char::from(c))?;
        }
        Ok(s)
    }

    /// Gets the Unix mode of this archive member
    ///
    /// # Example
    /// ```
    /// use hpk::tar::Header;
    ///
    /// let header = Header::new("test/1.txt").unwrap();
    /// let mode = header.mode().unwrap();
    /// assert_eq!(mode, 420);
    /// ```
    pub fn mode(&self) -> Result<u32, Error> {
        let mut s = String::new();
        for c in self.mode {
            if c == 0 {
                break;
            }
            write!(s, "{}", char::from(c))?;
        }
        let mode = u32::from_str_radix(&s, 8)?;
        Ok(mode)
    }

    fn uid(&self) -> Result<u32, Error> {
        let mut s = String::new();
        for c in self.mode {
            if c == 0 {
                break;
            }
            write!(s, "{}", char::from(c))?;
        }
        let uid = u32::from_str_radix(&s, 8)?;
        Ok(uid)
    }

    fn gid(&self) -> Result<u32, Error> {
        let mut s = String::new();
        for c in self.mode {
            if c == 0 {
                break;
            }
            write!(s, "{}", char::from(c))?;
        }
        let gid = u32::from_str_radix(&s, 8)?;
        Ok(gid)
    }

    fn username(&self) -> Result<String, fmt::Error> {
        let mut s = String::new();
        for c in self.username {
            if c == 0 {
                break;
            }
            write!(s, "{}", char::from(c))?;
        }
        Ok(s)
    }

    fn groupname(&self) -> Result<String, fmt::Error> {
        let mut s = String::new();
        for c in self.groupname {
            if c == 0 {
                break;
            }
            write!(s, "{}", char::from(c))?;
        }
        Ok(s)
    }

    pub fn owner(&self) -> Result<Owner, Error> {
        let uid = self.uid()?;
        let gid = self.gid()?;
        let username = self.username()?;
        let groupname = self.groupname()?;
        Ok(Owner {
            uid,
            gid,
            username,
            groupname,
        })
    }

    /// Gets the path to the file minus it's final component
    /// > Note: the ustar format only uses this field if the
    /// > complete filename would be longer than 100 bytes.
    /// > If the combined filename is shorter, this function
    /// > returns `None`.
    ///
    /// # Example
    /// ```
    /// use hpk::tar::Header;
    ///
    /// let header = Header::new("test/1.txt").unwrap();
    /// let prefix = header.prefix();
    /// assert!(prefix.is_none());
    /// ```
    pub fn prefix(&self) -> Option<String> {
        let mut s = String::new();
        for c in self.file_prefix {
            if c == 0 {
                break;
            }
            write!(s, "{}", char::from(c)).ok()?;
        }
        if s.is_empty() {
            None
        } else {
            Some(s)
        }
    }

    /// Gets the full file path to this archive member.
    ///
    /// # Example
    ///
    /// ```
    /// use hpk::tar::Header;
    /// use std::path::PathBuf;
    ///
    /// let header = Header::new("test/1.txt").unwrap();
    /// let path = header.file_path().unwrap();
    /// assert_eq!(PathBuf::from("test/1.txt"), path);
    /// ```
    pub fn file_path(&self) -> Result<PathBuf, fmt::Error> {
        let mut path = match self.prefix() {
            Some(p) => PathBuf::from(&p),
            None => PathBuf::new(),
        };
        let name = self.filename()?;
        path.push(&name);
        Ok(path)
    }

    #[allow(clippy::missing_panics_doc)]
    pub fn new(filename: &str) -> Result<Self, Error> {
        let mut header = Header::default();
        let meta = fs::symlink_metadata(filename)?;
        let (filename, prefix) = {
            // Original tar has a maximum file name length of 100 bytes. The ustar
            // revision allows storing the path prefix separately if the combined
            // length is more than 100 bytes.
            if filename.len() > 100 {
                let path = PathBuf::from(&filename);
                let name = match path.file_name().and_then(OsStr::to_str) {
                    Some(n) => n.to_string(),
                    None => {
                        return Err(Error::Io(io::Error::new(
                            io::ErrorKind::Other,
                            "Cannot get file name",
                        )))
                    }
                };
                let dir = path.parent().map(|x| format!("{}", x.display()));
                (name, dir)
            } else {
                (filename.to_string(), None)
            }
        };

        /* Fill in metadata */
        header.fname[..filename.len()].copy_from_slice(filename.as_bytes());
        let mode = format!("{:07o}", (meta.st_mode() & 0o777));
        header.mode[..mode.len()].copy_from_slice(mode.as_bytes());
        let user = format!("{:07o}", meta.st_uid());
        header.uid[..user.len()].copy_from_slice(user.as_bytes());
        let group = format!("{:07o}", meta.st_gid());
        header.gid[..group.len()].copy_from_slice(group.as_bytes());
        let size = format!("{:011o}", meta.st_size());
        header.size[..size.len()].copy_from_slice(size.as_bytes());
        let mtime = format!("{:011o}", meta.st_mtime());
        header.mtime[..mtime.len()].copy_from_slice(mtime.as_bytes());
        if let Some(prefix) = prefix {
            header.file_prefix[..prefix.len()].copy_from_slice(prefix.as_bytes());
        }

        /* Get the file type and conditional metadata */
        header.link_indicator[0] = FileType::from(&meta) as u8;
        if header.link_indicator[0] == FileType::Symlink as u8 {
            let link = fs::read_link(filename)?.to_str().unwrap().to_string();
            header.link_name[..link.len()].copy_from_slice(link.as_bytes());
        } else if header.link_indicator[0] == FileType::Block as u8 {
            let major = format!("{:07o}", meta.st_dev());
            header.device_major[..major.len()].copy_from_slice(major.as_bytes());
            let minor = format!("{:07o}", meta.st_rdev());
            header.device_minor[..minor.len()].copy_from_slice(minor.as_bytes());
        }

        /* TODO: Find better way to get username */
        let key = "USER";
        if let Ok(val) = env::var(key) {
            header.username[..val.len()].copy_from_slice(val.as_bytes());
        }
        /* TODO: Find way to get groupname */

        /* Update the header checksum value */
        header.update_checksum()?;

        Ok(header)
    }

    #[allow(clippy::missing_panics_doc)]
    pub fn new_from_meta(
        filename: &str,
        meta: &Metadata,
        owner: Option<Owner>,
    ) -> Result<Self, Error> {
        let mut header = Header::default();
        let (filename, prefix) = {
            // Original tar has a maximum file name length of 100 bytes. The ustar
            // revision allows storing the path prefix separately if the combined
            // length is more than 100 bytes.
            if filename.len() > 100 {
                let path = PathBuf::from(&filename);
                let name = match path.file_name().and_then(OsStr::to_str) {
                    Some(n) => n.to_string(),
                    None => {
                        return Err(Error::Io(io::Error::new(
                            io::ErrorKind::Other,
                            "Cannot get file name",
                        )))
                    }
                };
                let dir = path.parent().map(|x| format!("{}", x.display()));
                (name, dir)
            } else {
                (filename.to_string(), None)
            }
        };
        header.fname[..filename.len()].copy_from_slice(filename.as_bytes());
        let mode = format!("{:07o}", meta.st_mode());
        header.mode[..mode.len()].copy_from_slice(mode.as_bytes());
        let owner = match owner {
            Some(o) => o,
            None => Owner {
                uid: meta.st_uid(),
                gid: meta.st_gid(),
                username: get_username_for_uid(meta.st_uid())?.into(),
                groupname: get_groupname_for_gid(meta.st_gid())?.into(),
            },
        };
        let uid = format!("{:07o}", owner.uid);
        header.uid[..uid.len()].copy_from_slice(uid.as_bytes());
        let gid = format!("{:07o}", owner.gid);
        header.gid[..gid.len()].copy_from_slice(gid.as_bytes());
        let size = format!("{:011o}", meta.len());
        header.size[..size.len()].copy_from_slice(size.as_bytes());
        let mtime = format!("{:011o}", meta.st_mtime());
        header.mtime[..mtime.len()].copy_from_slice(mtime.as_bytes());
        if let Some(prefix) = prefix {
            header.file_prefix[..prefix.len()].copy_from_slice(prefix.as_bytes());
        }
        header.link_indicator[0] = FileType::from(meta) as u8;
        if header.link_indicator[0] == FileType::Symlink as u8 {
            let link = fs::read_link(filename)?.to_str().unwrap().to_string();
            header.link_name[..link.len()].copy_from_slice(link.as_bytes());
        } else if header.link_indicator[0] == FileType::Block as u8 {
            let major = format!("{:07o}", meta.st_dev());
            header.device_major[..major.len()].copy_from_slice(major.as_bytes());
            let minor = format!("{:07o}", meta.st_rdev());
            header.device_minor[..minor.len()].copy_from_slice(minor.as_bytes());
        }
        header.username[..owner.username.len()].copy_from_slice(owner.username.as_bytes());
        header.groupname[..owner.groupname.len()].copy_from_slice(owner.groupname.as_bytes());
        header.update_checksum()?;
        Ok(header)
    }

    /// Validates that the magic value received matches the magic value required in the Tar specification.
    ///
    /// # Example
    ///
    /// ```
    /// use hpk::tar::Header;
    /// let header = Header::default();
    /// if !header.validate_magic() {
    ///     println!("Magic value is invalid");
    /// }
    /// ```
    pub fn validate_magic(self) -> bool {
        self.ustar_magic == "ustar ".as_bytes()
    }

    /// Validates the header checksum computes to the expected value.
    ///
    /// # Example
    ///
    /// ```
    /// use hpk::tar::Header;
    /// let header = Header::default();
    /// if header.validate_checksum().unwrap() {
    ///     println!("Checksum is valid");
    /// }
    /// ```
    pub fn validate_checksum(self) -> Result<bool, Error> {
        let mut test = self;
        let mut new = [0x20u8; 8];
        test.header_checksum.copy_from_slice(&[0x20; 8]);

        let tmp = format!("{:06o}\x00", test.calc_checksum()?);
        new[..tmp.len()].copy_from_slice(tmp.as_bytes());

        Ok(self.header_checksum == new)
    }

    /// Updates the header checksum value.
    ///
    /// # Example
    ///
    /// ```
    /// use hpk::tar::Header;
    /// let mut header = Header::default();
    ///
    /// /* Fill in header information */
    ///
    /// header.update_checksum();
    /// ```
    pub fn update_checksum(&mut self) -> Result<(), Error> {
        let checksum = format!("{:06o}\x00", self.calc_checksum()?);
        self.header_checksum[..checksum.len()].copy_from_slice(checksum.as_bytes());
        Ok(())
    }

    fn calc_checksum(self) -> Result<usize, Error> {
        let out = self.to_bytes()?;
        let mut checksum = 0;
        for i in out {
            checksum += i as usize;
        }
        Ok(checksum)
    }
}

fn get_username_for_uid<'a>(uid: u32) -> Result<&'a str, std::str::Utf8Error> {
    let user = unsafe {
        let pw = libc::getpwuid(uid);
        let name = (*pw).pw_name;
        CStr::from_ptr(name)
    };
    user.to_str()
}

fn get_groupname_for_gid<'a>(gid: u32) -> Result<&'a str, std::str::Utf8Error> {
    let group = unsafe {
        let gr = libc::getgrgid(gid);
        let name = (*gr).gr_name;
        CStr::from_ptr(name)
    };
    group.to_str()
}
