use crate::{Error, Result};
use crev_data::proof;
use serde::{Deserialize, Serialize};
use std::{
    collections::HashMap,
    fs,
    io::Write,
    path::{Path, PathBuf},
};

#[derive(Serialize, Deserialize, Debug)]
pub struct StagingPathInfo {
    blake_hash: Vec<u8>,
}

pub struct Staging {
    root_path: PathBuf,
    file_path: PathBuf,
    pub entries: HashMap<PathBuf, StagingPathInfo>,
}

const STAGING_FILE_NAME: &str = "staging";

impl Staging {
    pub fn wipe(&mut self) -> Result<()> {
        fs::remove_file(&self.file_path)?;
        Ok(())
    }

    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    pub fn save(&mut self) -> Result<()> {
        self.write_to_file(&self.file_path)
    }

    pub fn open(repo_path: &Path) -> Result<Self> {
        let path = repo_path.join(super::CREV_DOT_NAME).join(STAGING_FILE_NAME);
        if !path.exists() {
            return Ok(Self {
                root_path: repo_path.to_owned(),
                file_path: path,
                entries: Default::default(),
            });
        }

        let file = fs::File::open(&path)?;

        let path_info: HashMap<PathBuf, StagingPathInfo> = serde_cbor::from_reader(&file)?;

        Ok(Self {
            root_path: repo_path.to_owned(),
            file_path: path,
            entries: path_info,
        })
    }

    fn write_to_file(&self, path: &Path) -> Result<()> {
        let tmp_path = path.with_extension("tmp");
        let mut file = fs::File::create(&tmp_path)?;
        serde_cbor::to_writer(&mut file, &self.entries)?;
        file.flush()?;
        drop(file);
        fs::rename(tmp_path, path)?;
        Ok(())
    }

    pub fn insert(&mut self, path: &Path) -> Result<()> {
        let full_path = path.canonicalize()?;

        let path = full_path
            .strip_prefix(&self.root_path)
            .map_err(|_| Error::PathNotInStageRootPath)?
            .to_owned();
        println!("Adding {}", path.display());
        self.entries.insert(
            path,
            StagingPathInfo {
                blake_hash: crev_common::blake2b256sum_file(&full_path)?,
            },
        );

        Ok(())
    }

    pub fn remove(&mut self, path: &Path) -> Result<()> {
        let full_path = path.canonicalize()?;

        let path = full_path
            .strip_prefix(&self.root_path)
            .map_err(|_| Error::PathNotInStageRootPath)?
            .to_owned();
        println!("Removing {}", path.display());

        self.entries.remove(&path);

        Ok(())
    }

    pub fn to_review_files(&self) -> Vec<proof::review::code::File> {
        self.entries
            .iter()
            .map(|(k, v)| proof::review::code::File {
                path: k.to_owned(),
                digest: v.blake_hash.clone(),
                digest_type: "blake2b".into(),
            })
            .collect()
    }

    pub fn enforce_current(&self) -> Result<()> {
        for (rel_path, info) in self.entries.iter() {
            let path = self.root_path.join(rel_path);
            if crev_common::blake2b256sum_file(&path)? != info.blake_hash {
                return Err(Error::FileNotCurrent(rel_path.as_path().into()));
            }
        }

        Ok(())
    }
}
