use std::fs;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;

use chrono::Utc;
use dsx_common::builder::{self, BuildContext};
use dsx_common::config::DsxConfig;
use serde::{Deserialize, Serialize};
use walkdir::WalkDir;

pub static DATA_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
    PathBuf::from(std::env::var("DATA_DIR").unwrap_or("./data".to_string()))
});

use crate::error::{ApiError, DsxError, ErrorType};

// stored as a Package.toml above the repository root.
// not directly modifiable by the user/maintainer.
#[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>,

    #[serde(default)]
    pub detected_languages: Option<Vec<String>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Package {
    pub config: DsxConfig,
    pub language: String,
    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,
}

enum BuildStatus {
    // if we don't know the build state, or no build has run before.
    None,

    // waiting for build
    Pending,

    // build
    Building,

    // build result
    Success,
    Failure,
    Error,
}

pub struct PackageHandle {
    // id of the package
    pub package_id: String,

    config: Option<DsxConfig>,
    meta: Option<PackageMeta>,

    // build info
    build_state: BuildStatus,
}

impl PackageHandle {
    const META_PATH: &'static str = "Package.toml";
    const CONFIG_PATH: &'static str = "repo/Dsx.toml";
    const ARTIFACTS_DIR: &'static str = "artifacts";

    pub fn new(package_id: impl Into<String>) -> Self {
        PackageHandle {
            package_id: package_id.into(),
            config: None,
            meta: None,
            build_state: BuildStatus::None,
        }
    }

    pub fn load(&mut self) -> Result<&mut Self, DsxError> {
        self.get_config()?;
        self.get_meta()?;
        Ok(self)
    }

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

    pub fn unpack(&mut self, archive: &Path) -> Result<&mut Self, DsxError> {
        let dest = DATA_DIR.join("repos").join(&self.package_id).join("repo");
        unpack_tarball(archive, &dest)?;
        self.load()
    }

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

    pub fn build(&mut self) -> Result<&mut Self, DsxError> {
        let ctx = BuildContext {
            project_dir: self.path().join("repo"),
            build_dir: self.path().join("build"),
            artifact_dir: self.path().join("artifacts"),
        };

        self.build_state = BuildStatus::Building;

        let id = self.package_id.clone();
        tokio::task::spawn_blocking(move || {
            let id = id;

            let res = match builder::build_project(ctx) {
                Ok(_) => "success",
                Err(_) => "failure",
            };

            let mut handle = PackageHandle::new(&id);
            let mut meta = handle.get_meta().unwrap();
            meta.latest_build_date =
                Some(Utc::now().format("%Y-%m-%d %H:%M:%S").to_string());
            meta.latest_build_status = Some(res.to_string());
            meta.latest_build_id = Some(id);
            handle.set_meta(meta)
        });

        Ok(self)
    }

    pub fn get_config(&mut self) -> Result<DsxConfig, DsxError> {
        let config_path = self.path().join(Self::CONFIG_PATH);

        let config = fs::read_to_string(&config_path).map_err(|e| {
            warn!("unable to read Dsx.toml, {e}");
            DsxError::with_context("Unable to read Dsx.toml", ErrorType::IoError)
        })?;

        let config: DsxConfig = toml::from_str(&config).map_err(|e| {
            warn!("unable to parse Dsx.toml: {e}");
            DsxError::new("Unable to parse Dsx.toml")
        })?;

        self.config = Some(config.clone());
        Ok(config)
    }

    pub fn get_meta(&mut self) -> Result<PackageMeta, DsxError> {
        let meta_path = self.path().join(Self::META_PATH);

        let meta = fs::read_to_string(&meta_path).map_err(|e| {
            warn!("unable to read Package.toml, {e}");
            DsxError::with_context("Unable to read Package.toml", ErrorType::IoError)
        })?;

        let meta: PackageMeta = toml::from_str(&meta).map_err(|e| {
            warn!("unable to parse Package.toml: {e}");
            DsxError::new("Unable to parse Package.toml")
        })?;

        self.meta = Some(meta.clone());
        Ok(meta)
    }

    pub fn set_meta(&mut self, meta: PackageMeta) -> Result<(), DsxError> {
        self.meta = Some(meta);
        self.save_meta()
    }

    fn save_meta(&self) -> Result<(), DsxError> {
        let meta_path = self.path().join(Self::META_PATH);
        let str = toml::to_string(&self.meta.as_ref().unwrap()).unwrap();

        fs::write(&meta_path, str).map_err(|e| {
            warn!("unable to write Package.toml, {e}");
            DsxError::with_context("Unable to write Package.toml", ErrorType::IoError)
        })?;
        Ok(())
    }

    pub fn get_languages(&mut self) -> Result<Vec<String>, DsxError> {
        self.get_meta()?;

        if let Some(languages) = &self.meta.as_ref().unwrap().detected_languages {
            return Ok(languages.clone());
        }

        let mut langs = Vec::new();

        for entry in WalkDir::new(self.path().join("repo/src"))
            .into_iter()
            .flatten()
        {
            if entry.file_type().is_file() {
                let path = entry.path();
                let extension = path.extension().and_then(|ext| ext.to_str());
                if let Some(ext) = extension {
                    match ext {
                        "dsa" | "dsc" => langs.push(ext.to_string()),
                        _ => {}
                    }
                }
            }
        }

        self.meta.as_mut().unwrap().detected_languages = Some(langs.clone());

        Ok(langs)
    }

    pub fn get_artifact(&self) -> Result<Vec<u8>, DsxError> {
        println!(
            "{}",
            self.path()
                .join(Self::ARTIFACTS_DIR)
                .join("out.dsb")
                .display()
        );

        let artifact_path = self.path().join(Self::ARTIFACTS_DIR).join("out.dsb");
        fs::read(artifact_path).map_err(|e| {
            warn!("unable to read artifact for repo: {e}");
            DsxError::new("Unable to read artifact")
        })
    }

    pub fn file_tree(&self, subpath: impl AsRef<Path>) -> Result<Vec<FileObj>, DsxError> {
        let repo = self.path().join("repo").join(subpath);

        let dir = fs::read_dir(repo).map_err(|e| {
            warn!("unable to read files for repo, {e}");
            DsxError::new("Unable to read files in repository")
        })?;

        let mut files = Vec::new();

        // for entry in WalkDir::new(repo_path.join("repo")).max_depth(1) {
        for entry in dir {
            let entry = entry.map_err(|e| {
                warn!("unable to read file entry for repo, {e}");
                DsxError::new("Internal error")
            })?;

            // remove root of DATA_DIR/repos/<name>/repo
            let path = entry.path();

            let metadata = fs::metadata(&path).map_err(|e| {
                warn!("unable to read file metadata for repo, {e}");
                DsxError::new("Internal error")
            })?;
            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
                    .strip_prefix(self.path().join("repo"))
                    .unwrap()
                    .to_string_lossy()
                    .to_string(),
                name: path.file_name().unwrap().to_string_lossy().to_string(),
                is_dir,
                size,
                extension,
            });
        }

        Ok(files)
    }
}

// ── 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<(), 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()?)
}
