use std::fs;
use std::path::PathBuf;

use dsx_common::config::DsxConfig;
use serde::{Deserialize, Serialize};

use crate::{DATA_DIR, error::ApiError};

// stored as a Config.toml above the repository root.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageMeta {
    pub id: String,

    #[serde(default)]
    pub latest_build_date: Option<String>,
    #[serde(default)]
    pub latest_build_status: Option<String>,
    #[serde(default)]
    pub latest_build_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Package {
    pub config: DsxConfig,
    pub meta: PackageMeta,
    pub files: Vec<FileObj>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileObj {
    pub name: String,
    pub path: String,
    pub is_dir: bool,
    pub size: u64,
    pub extension: String,
}

impl Package {
    pub fn load(name: &str) -> Result<Self, ApiError> {
        let repo_path = DATA_DIR.join("repos").join(name);

        let config_contents = fs::read_to_string(repo_path.join("repo/Dsx.toml"))
            .map_err(|e| {
                warn!("unable to read config for repo, {e}");
                ApiError::InternalServerError(())
            })?;

        let config: DsxConfig = toml::from_str(&config_contents).map_err(|e| {
            warn!("Invalid config file for repo! {e}");
            ApiError::InternalServerError(())
        })?;

        let meta_contents =
            fs::read_to_string(repo_path.join("Package.toml")).map_err(|e| {
                warn!("unable to read config for repo, {e}");
                ApiError::InternalServerError(())
            })?;

        let meta: PackageMeta = toml::from_str(&meta_contents).map_err(|e| {
            warn!("Invalid meta file for repo! {e}");
            ApiError::InternalServerError(())
        })?;

        let dir = fs::read_dir(repo_path.join("repo")).map_err(|e| {
            warn!("unable to read files for repo, {e}");
            ApiError::InternalServerError(())
        })?;

        let mut files = Vec::new();
        for entry in dir {
            let entry = entry.map_err(|e| {
                warn!("unable to read file entry for repo, {e}");
                ApiError::InternalServerError(())
            })?;
            let path = entry.path();
            let metadata = fs::metadata(&path).map_err(|e| {
                warn!("unable to read file metadata for repo, {e}");
                ApiError::InternalServerError(())
            })?;
            let is_dir = metadata.is_dir();
            let size = metadata.len();
            let extension = path
                .extension()
                .map_or(String::new(), |ext| ext.to_string_lossy().to_string());
            files.push(FileObj {
                path: path.to_string_lossy().to_string(),
                name: path.file_name().unwrap().to_string_lossy().to_string(),
                is_dir,
                size,
                extension,
            });
        }

        Ok(Self {
            config,
            meta,
            files,
        })
    }

    pub fn unpack(archive: &std::path::Path) -> Result<Self, ApiError> {
        let repo_name = archive.file_name().unwrap().to_str().unwrap();
        let dest = DATA_DIR.join("repos").join(repo_name).join("repo");
        unpack_tarball(archive, &dest)?;
        let package = Self::load(repo_name)?;
        Ok(package)
    }

    pub fn tarball(&self) -> Result<Vec<u8>, ApiError> {
        let src_dir = self.path().join("repo");
        pack_tarball(&src_dir)
    }

    pub fn path(&self) -> PathBuf {
        DATA_DIR.join("repos").join(&self.meta.id)
    }
}

// ── Tar helpers ───────────────────────────────────────────────────────────────

use flate2::read::GzDecoder;
use flate2::{Compression, write::GzEncoder};
use std::fs::File;
use tar::Builder;

fn unpack_tarball(
    archive: &std::path::Path,
    dest: &std::path::Path,
) -> Result<(), ApiError> {
    let file = File::open(archive)?;
    let gz = GzDecoder::new(file);
    let mut tar = tar::Archive::new(gz);
    tar.unpack(dest)?;
    Ok(())
}

fn pack_tarball(src_dir: &std::path::Path) -> Result<Vec<u8>, ApiError> {
    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()?)
}
