use std::{fs, path::Path};

use base64::{
    alphabet,
    display::Base64Display,
    engine::{self, general_purpose, general_purpose::STANDARD},
};
use regex::Regex;
use sha2::{
    digest::generic_array::{typenum::*, GenericArray},
    Digest, Sha256, Sha384, Sha512,
};
use simplelog::info;

type HasherSha256 =
    GenericArray<u8, UInt<UInt<UInt<UInt<UInt<UInt<UTerm, B1>, B0>, B0>, B0>, B0>, B0>>;
type HasherSha384 =
    GenericArray<u8, UInt<UInt<UInt<UInt<UInt<UInt<UTerm, B1>, B1>, B0>, B0>, B0>, B0>>;
type HasherSha512 =
    GenericArray<u8, UInt<UInt<UInt<UInt<UInt<UInt<UInt<UTerm, B1>, B0>, B0>, B0>, B0>, B0>, B0>>;

enum HasherGeneratedHash {
    Sha256(HasherSha256),
    Sha384(HasherSha384),
    Sha512(HasherSha512),
}

impl AsRef<[u8]> for HasherGeneratedHash {
    fn as_ref(&self) -> &[u8] {
        match self {
            HasherGeneratedHash::Sha256(hash) => hash.as_slice(),
            HasherGeneratedHash::Sha384(hash) => hash.as_slice(),
            HasherGeneratedHash::Sha512(hash) => hash.as_slice(),
        }
    }
}

const CUSTOM_ENGINE: engine::GeneralPurpose =
    engine::GeneralPurpose::new(&alphabet::URL_SAFE, general_purpose::NO_PAD);

pub struct Hasher {
    input: String,
    output_path: String,
    pub hashed_file: String,
    pub integrity: String,
}

impl Hasher {
    pub fn new(input: &str, output_path: &str, hash_algorithm: Option<&str>) -> Self {
        let algorithm = match hash_algorithm {
            Some("sha256") => "sha256",
            Some("sha512") => "sha512",
            _ => "sha384",
        };
        let hash = Self::hash(fs::read(input).unwrap(), algorithm);

        Self {
            input: input.to_string(),
            integrity: Self::integrity(&hash, algorithm),
            output_path: output_path.to_string(),
            hashed_file: Self::hashed_filename(
                &hash,
                Path::new(input).file_name().unwrap().to_str().unwrap(),
            ),
        }
    }

    // hash contents with specified algorithm
    fn hash(contents: Vec<u8>, algorithm: &str) -> HasherGeneratedHash {
        match algorithm {
            "sha256" => HasherGeneratedHash::Sha256(Sha256::digest(contents)),
            "sha512" => HasherGeneratedHash::Sha512(Sha512::digest(contents)),
            _ => HasherGeneratedHash::Sha384(Sha384::digest(contents)),
        }
    }

    // return hashed filename
    fn hashed_filename(hash: impl AsRef<[u8]>, filename: &str) -> String {
        Regex::new(r"\.(?<ext>[^\.]+)$")
            .unwrap()
            .replace(filename, &format!(".{}.$ext", &Self::url_safe_hash(hash)))
            .to_string()
    }

    // return hash string for integrity attribute
    fn integrity(hash: impl AsRef<[u8]>, algorithm: &str) -> String {
        format!(
            "{}-{}",
            &algorithm,
            Base64Display::new(hash.as_ref(), &STANDARD)
        )
    }

    // return url safe display of hash
    fn url_safe_hash(hash: impl AsRef<[u8]>) -> String {
        Base64Display::new(hash.as_ref(), &CUSTOM_ENGINE).to_string()
    }

    /// Copies input file to output asset file
    pub fn copy(&self, quiet: Option<bool>) {
        let output_asset_file = self.output_asset_file();
        fs::copy(&self.input, &output_asset_file).unwrap();

        if !quiet.unwrap_or(false) {
            info!("Copied {} to {}", &self.input, &output_asset_file);
        }
    }

    /// Provides output path for hashed file
    pub fn output_asset_file(&self) -> String {
        format!("{}/{}", &self.output_path, &self.hashed_file)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_hasher_new() {
        let hasher = Hasher::new("tests/data/index.html", "tests/output", None);

        assert_eq!(
            hasher.hashed_file,
            "index.oHWiVETdiyvzd41lHOpS3gAmG-RacWi8NTM9gZ3SMQsHi8NV4zGzXOXIP6SKmEwg.html"
        );
        assert_eq!(
            hasher.integrity,
            "sha384-oHWiVETdiyvzd41lHOpS3gAmG+RacWi8NTM9gZ3SMQsHi8NV4zGzXOXIP6SKmEwg"
        );
    }

    #[test]
    fn test_hash_sha256() {
        assert_eq!(
            Base64Display::new(
                Hasher::hash("testing hashing sha256".as_bytes().to_vec(), "sha256").as_ref(),
                &STANDARD
            )
            .to_string(),
            "JKkagevjdRmsUwF7x/pc37K5QyAtJ4YxsTmnPB4ModU="
        );
    }

    #[test]
    fn test_hash_sha512() {
        assert_eq!(
            Base64Display::new(
                Hasher::hash("testing hashing sha512".as_bytes().to_vec(), "sha512").as_ref(),
                &STANDARD
            )
            .to_string(),
            "vrUJr13O5Y8EMCriDrcm8k8Rit5mVxj8Jl74ErnewIQu/rWRVDbHascpkM67sJPZLMHVkjItCCwSRGf0fpU9qQ=="
        );
    }

    #[test]
    fn test_hashed_filename() {
        assert_eq!(
            Hasher::hashed_filename(
                Hasher::hash("this is a test".as_bytes().to_vec(), "sha384").as_ref(),
                "index.html",
            ),
            "index.QzgqjMZQkEZ1ydYteFeG42jzqZ25muqqe3awJTBncVTQnAtr0uIbQyn9QVQ7mnhb.html"
        );
    }

    #[test]
    fn test_integrity() {
        assert_eq!(
            Hasher::integrity(
                Hasher::hash("this is another test".as_bytes().to_vec(), "sha384").as_ref(),
                "sha384"
            ),
            "sha384-fMp5lYuYh0sptnzKMvATiRBx+bU8Xe4jm3SQSEIOwCsrujQsSEHukIpiL5rXGQoR"
        );
    }
}
