use {
    crate::{
        tar::{Error as TarError, Node, Owner},
        Entry,
    },
    deku::DekuError,
    sha2::{Digest, Sha256},
    std::{
        ffi::OsStr,
        fmt::{self, Write},
        fs,
        io::{self, Read},
        os::unix::fs::MetadataExt,
        path::{Path, PathBuf},
    },
};

#[derive(Clone, Debug)]
/// An intermediate type used in the creation of a package archive
pub struct Item {
    pub entry: Entry,
    pub data: Vec<u8>,
}

impl Item {
    pub fn try_create(path: &Path) -> Result<Self, Error> {
        let path = fix_path(path);
        let meta = fs::metadata(&path)?;
        let filename = format!("{}", path.display());
        let owner = Some(Owner::default());
        if meta.is_file() {
            let mut data = vec![];
            let mut fd = fs::File::open(path)?;
            let path = PathBuf::from(&filename);
            let size = fd.read_to_end(&mut data)?;
            let mut sha256sum = String::new();
            let mut hasher = Sha256::new();
            hasher.update(&data);
            let res = hasher.finalize();
            for c in res {
                write!(sha256sum, "{c:02x}")?;
            }
            let mode = meta.mode();
            Ok(Self {
                entry: Entry::File {
                    path,
                    sha256sum,
                    mode,
                    size,
                },
                data: Node::read_data_to_tar(&data, &filename, &meta, owner)?.to_vec()?,
            })
        } else if meta.is_dir() {
            let mode = meta.mode();
            let path = PathBuf::from(&filename);
            Ok(Self {
                entry: Entry::Directory { path, mode },
                data: Node::read_data_to_tar(&[0; 0], &filename, &meta, owner)?.to_vec()?,
            })
        } else if meta.is_symlink() {
            let target = fs::read_link(path)?;
            let path = PathBuf::from(&filename);
            Ok(Self {
                entry: Entry::Link { path, target },
                data: Node::read_data_to_tar(&[0; 0], &filename, &meta, owner)?.to_vec()?,
            })
        } else {
            unreachable!();
        }
    }
}

#[derive(Debug)]
pub enum Error {
    Io(io::Error),
    Fmt(fmt::Error),
    Tar(TarError),
    Deku(DekuError),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{self:?}")
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Io(e) => Some(e),
            Self::Fmt(e) => Some(e),
            Self::Tar(e) => Some(e),
            Self::Deku(e) => Some(e),
        }
    }
}

impl From<io::Error> for Error {
    fn from(value: io::Error) -> Self {
        Self::Io(value)
    }
}

impl From<fmt::Error> for Error {
    fn from(value: fmt::Error) -> Self {
        Self::Fmt(value)
    }
}

impl From<TarError> for Error {
    fn from(value: TarError) -> Self {
        Self::Tar(value)
    }
}

impl From<DekuError> for Error {
    fn from(value: DekuError) -> Self {
        Self::Deku(value)
    }
}

fn fix_path(path: &Path) -> PathBuf {
    let path = if let Ok(p) = path.strip_prefix("./") {
        p
    } else {
        path
    };
    if path.is_file() || path.is_symlink() {
        match path.ancestors().last().and_then(Path::to_str) {
            Some("etc" | "var") => {
                let ext = if let Some(x) = path.extension().and_then(OsStr::to_str) {
                    format!("{x}.new")
                } else {
                    "new".to_string()
                };
                let mut path = path.to_path_buf();
                path.set_extension(ext);
                path
            }
            _ => path.to_path_buf(),
        }
    } else {
        path.to_path_buf()
    }
}
