use dsx_common::config::DsxConfig;
use flate2::read::GzDecoder;
use flate2::{Compression, write::GzEncoder};
use reqwest::Url;
use std::fs::{self, File};
use std::path::Path;
use tar::Builder;

pub fn push(repo_dir: &Path) -> Result<(), DsxError> {
    let config_file = fs::read_to_string(repo_dir.join("Dsx.toml"))?;
    let config: DsxConfig = toml::from_str(&config_file).expect("Failed to parse config");

    let mut repo_url =
        Url::parse(&config.remote_url.expect(
            "Repository URL is not set in Dsx.toml, set it with the key 'remote'",
        ))
        .unwrap();
    repo_url.path_segments_mut().unwrap().push("push");

    let client = reqwest::blocking::Client::new();
    let response = client
        .post(repo_url)
        .body(pack_tarball(repo_dir)?)
        .header("Content-Type", "application/octet-stream")
        .send()
        .map_err(|e| {
            DsxError::with_context(
                format!("failed to stream to client: {}", e),
                ErrorType::IoError,
            )
        })?;

    if response.status().is_success() {
        Ok(())
    } else {
        Err(DsxError::with_context(
            response.text().unwrap(),
            ErrorType::IoError,
        ))
    }
}

pub fn pull(repo_dir: &Path) -> Result<(), DsxError> {
    let config_file = fs::read_to_string(repo_dir.join("Dsx.toml"))?;
    let config: DsxConfig = toml::from_str(&config_file).expect("Failed to parse config");

    let repo_url =
        Url::parse(&config.remote_url.expect(
            "Repository URL is not set in Dsx.toml, set it with the key 'remote'",
        ))
        .unwrap();

    clone(repo_dir, &repo_url)
}

pub fn clone(repo_dir: &Path, url: &Url) -> Result<(), DsxError> {
    let mut url = url.clone();
    url.path_segments_mut().unwrap().push("pull");

    let client = reqwest::blocking::Client::new();
    let response = client.get(url).send().map_err(|e| {
        DsxError::with_context(
            format!("failed to stream from client: {e}"),
            ErrorType::IoError,
        )
    })?;

    if !response.status().is_success() {
        return Err(DsxError::with_context(
            response.text().unwrap(),
            ErrorType::IoError,
        ));
    }

    let tmp_file = std::env::temp_dir().join("dsx-pull.tar.gz");
    std::fs::write(&tmp_file, response.bytes().unwrap())
        .map_err(|e| DsxError::with_context(e.to_string(), ErrorType::IoError))?;
    unpack_tarball(&tmp_file, repo_dir)?;
    fs::remove_file(tmp_file)?;
    Ok(())
}

fn unpack_tarball(
    archive: &std::path::Path,
    dest: &std::path::Path,
) -> Result<(), DsxError> {
    let file = File::open(archive)
        .map_err(|e| DsxError::with_context(e.to_string(), ErrorType::IoError))?;

    fs::create_dir_all(dest)?;

    let gz = GzDecoder::new(file);
    let mut tar = tar::Archive::new(gz);
    tar.unpack(dest)
        .map_err(|e| DsxError::with_context(e.to_string(), ErrorType::TarError))?;

    Ok(())
}

fn pack_tarball(src_dir: &std::path::Path) -> Result<Vec<u8>, DsxError> {
    let buf = Vec::new();
    let gz = GzEncoder::new(buf, Compression::default());
    let mut tar = Builder::new(gz);
    tar.append_dir_all(".", src_dir)?;
    let gz = tar.into_inner()?;
    Ok(gz.finish()?)
}

#[derive(Debug)]
pub struct DsxError {
    pub message: String,
    pub r#type: ErrorType,
}

impl DsxError {
    pub fn new(message: impl AsRef<str>) -> Self {
        DsxError {
            message: message.as_ref().to_string(),
            r#type: ErrorType::Generic,
        }
    }

    pub fn with_context(message: impl AsRef<str>, r#type: ErrorType) -> Self {
        DsxError {
            message: message.as_ref().to_string(),
            r#type,
        }
    }
}

impl From<std::io::Error> for DsxError {
    fn from(err: std::io::Error) -> Self {
        Self::with_context(err.to_string(), ErrorType::IoError)
    }
}

#[derive(Default, Debug)]
pub enum ErrorType {
    BuildFailed,
    IoError,

    #[default]
    Generic,
    TarError,
}
