use std::io;
use std::path::{Path, PathBuf};

use log::trace;
use tempfile::TempDir;

use crate::error::Error;
use crate::metadata::{Metadata, get_crate_metadata};
use crate::utils::{self, DirectoryContents, get_contents};
use crate::vcsinfo::{CargoVcsInfo, vcs_info_from_root};

// these files are expected to be different in published crates
const FILTERED_FILE_NAMES: [&str; 4] = ["Cargo.toml", "Cargo.toml.orig", "Cargo.lock", ".cargo_vcs_info.json"];

/// Crate archive
#[derive(Debug)]
#[non_exhaustive]
pub struct Crate {
    pub(crate) metadata: Metadata,
    pub(crate) root: PathBuf,
    pub(crate) vcs_info: Option<CargoVcsInfo>,
    pub(crate) _temp: Option<TempDir>,
}

impl Crate {
    /// Load crate sources from local ".crate" file.
    ///
    /// ```rust,no_run
    /// use ingredients::Crate;
    ///
    /// let _krate = Crate::local("ingredients", "0.1.0", "./ingredients-0.1.0.crate").unwrap();
    /// ```
    ///
    /// # Errors
    ///
    /// * The specified path does not exist.
    /// * The file at the specified path is not a crate archive (.tar.gz format).
    /// * The crate cannot be unpacked in a temporary location.
    /// * The crate does not contain valid crate metadata.
    pub fn local<P: AsRef<Path>>(name: &str, version: &str, path: P) -> Result<Self, Error> {
        let temp = utils::unpack_tar_gz(path)?;

        let root = temp.path().join(format!("{name}-{version}"));
        let metadata = get_crate_metadata(root.clone())?;
        let vcs_info = vcs_info_from_root(&root)?;

        Ok(Crate {
            metadata,
            root,
            vcs_info,
            _temp: Some(temp),
        })
    }

    /// Name of the crate
    #[must_use]
    pub fn name(&self) -> &str {
        &self.metadata.inner.name
    }

    /// Version of the crate
    #[must_use]
    pub fn version(&self) -> String {
        self.metadata.inner.version.to_string()
    }

    pub(crate) fn cargo_toml_orig(&self) -> PathBuf {
        self.root.join("Cargo.toml.orig")
    }

    pub(crate) fn file_contents(&self) -> Result<DirectoryContents, Error> {
        let mut contents = get_contents(&self.root, &self.root)?;

        contents.files.retain(|f| {
            f.file_name()
                .is_some_and(|s| !FILTERED_FILE_NAMES.iter().any(|f| *f == s))
        });

        Ok(contents)
    }

    pub(crate) fn read_entry_to_bytes<P: AsRef<Path>>(&self, path: P) -> io::Result<Vec<u8>> {
        trace!("Reading bytes from file: {}", path.as_ref().to_string_lossy());
        utils::read_to_bytes(self.root.join(path))
    }

    pub(crate) fn read_entry_to_string<P: AsRef<Path>>(&self, path: P) -> io::Result<String> {
        trace!("Reading text from file: {}", path.as_ref().to_string_lossy());
        std::fs::read_to_string(self.root.join(path))
    }
}
