use crate::{
    arq::{
        self, BackupFolder, BackupPlan, BackupRecord, BlobLoc, KeySet, Node, Tree,
        decrypt_lz4decompress,
    },
    errors::ArqError,
};
use glob::glob;
use log::trace;
use std::{
    cell::RefCell,
    fs,
    io::{self, Read, Seek, SeekFrom},
    path::{Component, Path, PathBuf},
    str::{self},
};

pub struct Repository {
    path: PathBuf,
    pub uuid: String,
    key_set: KeySet,

    tree_pack_blob_locs: RefCell<Vec<BlobLoc>>,
    data_blob_locs: RefCell<Vec<BlobLoc>>,
}

impl Repository {
    pub fn build(path: &Path, password: &str) -> arq::Result<Self> {
        let key_set = KeySet::decrypt_parse_key_set(
            fs::File::open(path.join("encryptedkeyset.dat"))?,
            password,
        )?;

        let plan =
            BackupPlan::from_reader(fs::File::open(path.join("backupplan.json"))?, &key_set)?;

        Ok(Repository {
            path: path.to_path_buf(),
            uuid: plan.uuid,
            key_set,
            tree_pack_blob_locs: vec![].into(),
            data_blob_locs: vec![].into(),
        })
    }

    // Returns a list of UUIDs found under backupfolders/
    // This does NOT check if there is a valid backupfolder.json inside!
    fn backup_folder_uuids(&self) -> arq::Result<Vec<String>> {
        Ok(
            fs::read_dir(PathBuf::from(&self.path).join("backupfolders/"))?
                .try_fold(Vec::new(), |mut acc, entry| -> arq::Result<Vec<String>> {
                    let entry: fs::DirEntry = entry?;
                    if let Ok(file_type) = entry.file_type()
                        && !file_type.is_dir()
                    {
                        return Ok(acc);
                    }

                    if let Some(uuid) = entry.file_name().to_str() {
                        acc.push(uuid.to_string());
                    }

                    Ok(acc)
                })?
                .into_iter()
                .collect(),
        )
    }

    // Returns BackupFolders
    pub fn backup_folders(&self) -> arq::Result<Vec<BackupFolder>> {
        self.backup_folder_uuids()?
            .into_iter()
            .map(|uuid| self.load_folder(&uuid))
            .collect()
    }

    pub fn backup_record_timestamps(&self, backup_folder: &str) -> arq::Result<Vec<u64>> {
        let pattern = self
            .path
            .join("backupfolders")
            .join(backup_folder)
            .join("backuprecords/**/*.backuprecord");

        let Some(pattern) = pattern.to_str() else {
            panic!("Glob pattern not a UTF-8 string!?");
        };

        let mut records: Vec<u64> = vec![];

        for entry in glob(pattern).expect("Failed to read glob pattern") {
            match entry {
                Ok(path) => {
                    if let Some(ts) = Self::path_to_record(path) {
                        records.push(ts as u64);
                    }
                }
                Err(e) => println!("{:?}", e),
            }
        }

        Ok(records)
    }

    pub fn find_subtree(
        &self,
        base: &Path,
        start: Node,
        search_path: &Path,
    ) -> arq::Result<Option<(Node, PathBuf)>> {
        if search_path.is_relative() {
            return Err(ArqError::Subtree(format!(
                "Search path ({}) must be absolute!",
                search_path.display(),
            )));
        }

        if base.is_relative() {
            panic!("Backup base path must be absolute!");
        }

        if !start.is_tree {
            panic!("Start node must be a directory!");
        }

        if base == search_path {
            return Ok(Some((start, search_path.to_path_buf())));
        }

        let base = PathBuf::from(base);
        let mut search_components = search_path.components();

        // if search path is a prefix to the backup local path, we start
        // at the top-most node we can, which is the base path.
        if base.starts_with(search_path) {
            return Ok(Some((start, base)));
        } else if search_path.starts_with(&base) {
            for component in base.components() {
                match search_components.next() {
                    Some(Component::RootDir) => continue,
                    Some(current @ Component::Normal(_)) => {
                        if current != component {
                            return Ok(None);
                        }
                    }
                    Some(Component::CurDir) | Some(Component::ParentDir) => {
                        return Err(ArqError::Subtree(
                            "subtree does not support references like ./ or ../".to_owned(),
                        ));
                    }
                    Some(Component::Prefix(_)) => {
                        return Err(ArqError::Subtree("Windows paths not supported".to_owned()));
                    }
                    // TODO: This should not happen, right? Figure out exactly why and give appropriate panic/assertion
                    None => panic!("subtree"),
                }
            }
        }

        let Some(mut current_comp) = search_components.next() else {
            return Ok(None);
        };
        let mut base = base;
        let mut node = start;

        loop {
            let Some(tree_loc) = node.tree_loc.as_ref() else {
                return Err(ArqError::MissingTreeLoc);
            };

            match self.tree_from_pack(tree_loc) {
                Ok(tree) => {
                    if let Some(dir) = tree
                        .nodes
                        .into_iter()
                        .filter(|n| n.is_tree)
                        .find(|d| Component::Normal(d.name.as_ref()) == current_comp)
                    {
                        if let Some(next_comp) = search_components.next() {
                            current_comp = next_comp;
                            base.push(dir.name.clone());
                            node = dir;

                            continue;
                        } else {
                            node = dir;
                            return Ok(Some((node, base)));
                        }
                    }
                }
                Err(e) => return Err(e),
            }

            break;
        }

        Ok(None)
    }

    pub fn used_tree_pack_files(&self) -> Vec<String> {
        let mut files: Vec<String> = self
            .tree_pack_blob_locs
            .borrow()
            .iter()
            .map(|loc| loc.relative_path.clone())
            .collect();

        files.sort();
        files.dedup();

        files
    }

    pub fn used_data_pack_files(&self) -> Vec<String> {
        let mut paths: Vec<String> = self
            .data_blob_locs
            .borrow()
            .iter()
            .map(|loc| loc.relative_path.clone())
            .collect();

        paths.sort();
        paths.dedup();

        paths
    }

    pub fn used_data_pack_bytes(&self) -> u64 {
        self.data_blob_locs
            .borrow()
            .iter()
            .fold(0, |acc, loc| acc + loc.length)
    }

    fn folder_to_path<P>(folder_uuid: P) -> PathBuf
    where
        P: AsRef<Path>,
    {
        PathBuf::from("backupfolders/").join(folder_uuid)
    }

    fn folder_and_record_to_path(folder_uuid: &str, record: &u32) -> PathBuf {
        const SPLIT: u32 = 10_000_000;

        Self::folder_to_path(folder_uuid)
            .join("backuprecords")
            .join(format!("{:0>5}", record / SPLIT))
            .join(format!("{:0>7}.backuprecord", record % SPLIT))
    }

    /// Returns the timestamp of a backup record, based on its file path.
    /// For example `DD994AEF-F9CB-4D39-B12D-1EF5305103C8/backuprecords/00176/3107319.backuprecord`
    /// returns `1763107319`.
    fn path_to_record<P>(path: P) -> Option<u32>
    where
        P: AsRef<Path>,
    {
        let path = path.as_ref();

        // File part: e.g. "3107319"
        let file = path.file_name()?.to_str()?;
        let file_ts = file.strip_suffix(".backuprecord")?;

        // Parent folder part: e.g. "00176"
        let folder_ts = path.parent()?.file_name()?.to_str()?;

        format!("{folder_ts}{file_ts}").parse().ok()
    }

    // TODO: This should accept either a str (UUID) or a Path (backup folder location). The UUID
    //       case can directly load the BackupFolder, for the Path we have to lookup the UUID first.
    pub fn load_folder(&self, uuid: &str) -> arq::Result<BackupFolder> {
        BackupFolder::from_reader(
            self.open_relative(Self::folder_to_path(uuid).join("backupfolder.json"))?,
            &self.key_set,
        )
    }

    pub fn load_record(&self, uuid: &str, record: &u32) -> arq::Result<BackupRecord> {
        BackupRecord::from_reader(
            self.open_relative(Self::folder_and_record_to_path(uuid, record))?,
            &self.key_set,
        )
    }

    fn open_relative<P>(&self, relative_path: P) -> io::Result<fs::File>
    where
        P: AsRef<Path>,
    {
        fs::File::open(self.to_real_path(relative_path))
    }

    pub fn load_plan(&self) -> arq::Result<BackupPlan> {
        BackupPlan::from_reader(self.open_relative("/backupplan.json")?, &self.key_set)
    }

    /// Converts a file at a path, "forced" relative to the repository's path.
    ///
    /// * /backupplan.json will be relative to the repositories root.
    /// * additionally the repository UUID will be removed as a prefix,
    ///   /D7E09473-74BF-4E6D-86C3-43A033570D80/backupplan.json will be
    ///   $repositoryPath/backupplan.json.
    ///
    /// Removing the UUID prefix is needed, because in Arq there is no
    /// concept of a "repository", only storage locations with one or more
    /// backup plans. For some reason, some paths in Arq objects like the path
    /// to blobs, will also contain the plan UUID.
    fn to_real_path<P>(&self, relative_path: P) -> PathBuf
    where
        P: AsRef<Path>,
    {
        let mut relative_path = relative_path.as_ref();

        if relative_path.is_absolute() {
            relative_path = relative_path.strip_prefix("/").unwrap_or(relative_path);
        }

        self.path.join(
            if let Ok(stripped) =
                relative_path.strip_prefix(PathBuf::from(format!("{}/", &self.uuid)))
            {
                stripped
            } else {
                relative_path
            },
        )
    }

    pub fn data_from_pack(&self, blob_loc: &BlobLoc) -> arq::Result<Vec<u8>> {
        let data = self.load_from_pack(blob_loc)?;

        self.data_blob_locs.borrow_mut().push(blob_loc.clone());

        Ok(data)
    }

    pub fn tree_from_pack(&self, blob_loc: &BlobLoc) -> arq::Result<Tree> {
        let data = self.load_from_pack(blob_loc)?;

        self.tree_pack_blob_locs.borrow_mut().push(blob_loc.clone());

        Tree::from_binary(&*data)
    }

    fn load_from_pack(&self, blob_loc: &BlobLoc) -> arq::Result<Vec<u8>> {
        trace!("Loading from packfile: {:?}", blob_loc);
        let mut f = self.open_relative(&blob_loc.relative_path)?;

        f.seek(SeekFrom::Start(blob_loc.offset))?;
        let mut buffer = vec![0u8; blob_loc.length as usize];
        f.read_exact(&mut buffer)?;

        if blob_loc.compression_type == BlobLoc::COMPRESSION_TYPE_LZ4 {
            decrypt_lz4decompress(buffer.as_slice(), &self.key_set)
        } else {
            Err(ArqError::UnsupportedCompressionType(
                blob_loc.compression_type,
            ))
        }
    }
}
