use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::Result;
use serde::{Deserialize, Serialize};

pub fn is_valid_dbus_name(name: &str) -> bool {
    if name.is_empty() || name.len() > 255 {
        return false;
    }

    if !name.contains('.') {
        return false;
    }

    name.split('.').all(|element| {
        let mut chars = element.chars();
        match chars.next() {
            Some(c) if !c.is_ascii_digit() => {
                chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
            }
            _ => false,
        }
    })
}

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "kebab-case")]
pub struct BuildOptions {
    #[serde(default)]
    pub env: HashMap<String, String>,
    #[serde(default)]
    pub config_opts: Vec<String>,
    pub prepend_path: Option<String>,
    pub append_path: Option<String>,
    pub prepend_ld_library_path: Option<String>,
    pub append_ld_library_path: Option<String>,
    pub prepend_pkg_config_path: Option<String>,
    pub append_pkg_config_path: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum Module {
    #[serde(rename_all = "kebab-case")]
    Object {
        name: String,
        #[serde(default)]
        buildsystem: Option<String>,
        #[serde(default)]
        config_opts: Option<Vec<String>>,
        #[serde(default)]
        build_commands: Option<Vec<String>>,
        #[serde(default)]
        build_options: Option<BuildOptions>,
        #[serde(default)]
        post_install: Option<Vec<String>>,
        #[serde(default)]
        sources: Vec<serde_json::Value>,
    },
    Reference(String),
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Manifest {
    #[serde(alias = "app-id")]
    pub id: String,
    pub sdk: String,
    pub runtime: String,
    pub runtime_version: String,
    pub command: String,
    #[serde(default)]
    pub x_run_args: Option<Vec<String>>,
    #[serde(default)]
    pub modules: Vec<Module>,
    #[serde(default)]
    pub finish_args: Vec<String>,
    #[serde(default)]
    pub build_options: BuildOptions,
    #[serde(default)]
    pub sdk_extensions: Vec<String>,
    #[serde(default)]
    pub cleanup: Vec<String>,
}

impl Manifest {
    pub fn from_file(path: &Path) -> Result<Self> {
        let content = fs::read_to_string(path)?;
        let manifest: Manifest = match path.extension().and_then(|s| s.to_str()) {
            Some("json") => serde_json::from_str(&content)?,
            Some("yaml") | Some("yml") => serde_saphyr::from_str(&content)?,
            _ => return Err(anyhow::anyhow!("Unsupported manifest format")),
        };
        if !is_valid_dbus_name(&manifest.id) {
            return Err(anyhow::anyhow!("Invalid application ID: {}", manifest.id));
        }
        Ok(manifest)
    }

    pub fn finish_args_filtered(&self) -> Vec<String> {
        self.finish_args
            .iter()
            .filter(|arg| {
                let key = arg.split('=').next().unwrap_or("");
                key != "--metadata" && key != "--require-version"
            })
            .cloned()
            .collect()
    }

    pub fn merged_config_opts<'a>(
        &'a self,
        module_config_opts: Option<&'a [String]>,
    ) -> Vec<&'a str> {
        self.build_options
            .config_opts
            .iter()
            .chain(module_config_opts.into_iter().flatten())
            .map(|s| s.as_str())
            .collect()
    }

    pub fn merged_env(
        &self,
        module_build_options: Option<&BuildOptions>,
    ) -> HashMap<String, String> {
        let mut merged = self.build_options.env.clone();
        if let Some(mbo) = module_build_options {
            merged.extend(mbo.env.clone());
        }
        merged
    }

    pub fn path_overrides(&self, module_build_options: Option<&BuildOptions>) -> Vec<String> {
        let mut overrides = Vec::new();

        let mpp = module_build_options.and_then(|b| b.prepend_path.as_deref());
        let mpa = module_build_options.and_then(|b| b.append_path.as_deref());
        let mlp = module_build_options.and_then(|b| b.prepend_ld_library_path.as_deref());
        let mla = module_build_options.and_then(|b| b.append_ld_library_path.as_deref());
        let mppc = module_build_options.and_then(|b| b.prepend_pkg_config_path.as_deref());
        let mapc = module_build_options.and_then(|b| b.append_pkg_config_path.as_deref());

        if let Some(arg) = Self::build_path_override(
            "PATH",
            &["/app/bin", "/usr/bin"],
            self.build_options.prepend_path.as_deref(),
            mpp,
            self.build_options.append_path.as_deref(),
            mpa,
        ) {
            overrides.push(arg);
        }

        if let Some(arg) = Self::build_path_override(
            "LD_LIBRARY_PATH",
            &["/app/lib"],
            self.build_options.prepend_ld_library_path.as_deref(),
            mlp,
            self.build_options.append_ld_library_path.as_deref(),
            mla,
        ) {
            overrides.push(arg);
        }

        if let Some(arg) = Self::build_path_override(
            "PKG_CONFIG_PATH",
            &[
                "/app/lib/pkgconfig",
                "/app/share/pkgconfig",
                "/usr/lib/pkgconfig",
                "/usr/share/pkgconfig",
            ],
            self.build_options.prepend_pkg_config_path.as_deref(),
            mppc,
            self.build_options.append_pkg_config_path.as_deref(),
            mapc,
        ) {
            overrides.push(arg);
        }

        overrides
    }

    fn build_path_override(
        var: &str,
        defaults: &[&str],
        manifest_prepend: Option<&str>,
        module_prepend: Option<&str>,
        manifest_append: Option<&str>,
        module_append: Option<&str>,
    ) -> Option<String> {
        let host_value = env::var(var).ok();
        let parts: Vec<&str> = manifest_prepend
            .into_iter()
            .chain(module_prepend)
            .chain(host_value.as_deref())
            .chain(defaults.iter().copied())
            .chain(manifest_append)
            .chain(module_append)
            .collect();
        if parts.is_empty() {
            return None;
        }
        Some(format!("--env={var}={}", parts.join(":")))
    }
}

/// Recursively finds manifest files in the given path, optionally excluding a prefix subtree.
/// Returns a sorted Vec of manifest file paths, prioritizing ".Devel." manifests and shallower paths.
pub fn find_manifests_in_path(path: &Path, exclude_prefix: Option<&Path>) -> Result<Vec<PathBuf>> {
    use walkdir::WalkDir;

    let mut manifests = vec![];

    let path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
    let exclude_prefix =
        exclude_prefix.map(|p| p.canonicalize().unwrap_or_else(|_| p.to_path_buf()));

    for entry in WalkDir::new(&path)
        .into_iter()
        .filter_entry(|e| {
            if e.depth() == 0 {
                return true;
            }
            if e.file_name().to_str().is_some_and(|s| s.starts_with('.')) {
                return false;
            }
            if let Some(prefix) = &exclude_prefix
                && e.path().starts_with(prefix)
            {
                return false;
            }
            true
        })
        .filter_map(|e| e.ok())
        .filter(|e| e.file_type().is_file())
        .filter(|e| {
            matches!(
                e.path().extension().and_then(|s| s.to_str()),
                Some("json") | Some("yaml") | Some("yml")
            )
        })
        .filter(|e| Manifest::from_file(e.path()).is_ok())
    {
        manifests.push(entry.into_path());
    }

    manifests.sort_by(|a, b| {
        let a_is_devel = a.to_str().unwrap_or("").contains(".Devel.");
        let b_is_devel = b.to_str().unwrap_or("").contains(".Devel.");
        b_is_devel
            .cmp(&a_is_devel)
            .then_with(|| a.components().count().cmp(&b.components().count()))
    });

    Ok(manifests)
}

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

    #[test]
    fn test_is_valid_dbus_name() {
        assert!(is_valid_dbus_name("org.example.App"));
        assert!(is_valid_dbus_name("com.github.user.Application"));
        assert!(is_valid_dbus_name("io.github.user_name.app-name"));

        assert!(!is_valid_dbus_name(""));
        assert!(!is_valid_dbus_name("single"));
        assert!(!is_valid_dbus_name("org.123invalid"));
        assert!(!is_valid_dbus_name("org..double"));
        assert!(!is_valid_dbus_name(".org.example"));
        assert!(!is_valid_dbus_name("org.example."));

        let long_name = format!("org.{}.App", "a".repeat(250));
        assert!(!is_valid_dbus_name(&long_name));
    }

    #[test]
    fn test_manifest_parsing_json() {
        use std::io::Write;
        use tempfile::NamedTempFile;

        let manifest_content = r#"{
            "app-id": "org.example.TestApp",
            "sdk": "org.gnome.Sdk",
            "runtime": "org.gnome.Platform",
            "runtime-version": "47",
            "command": "test-app",
            "modules": [
                {
                    "name": "test-module",
                    "buildsystem": "meson",
                    "sources": []
                }
            ],
            "finish-args": [
                "--share=network"
            ]
        }"#;

        let mut temp_file = NamedTempFile::with_suffix(".json").unwrap();
        temp_file.write_all(manifest_content.as_bytes()).unwrap();
        temp_file.flush().unwrap();

        let manifest = Manifest::from_file(temp_file.path()).unwrap();
        assert_eq!(manifest.id, "org.example.TestApp");
        assert_eq!(manifest.sdk, "org.gnome.Sdk");
        assert_eq!(manifest.runtime, "org.gnome.Platform");
        assert_eq!(manifest.runtime_version, "47");
        assert_eq!(manifest.command, "test-app");
        assert_eq!(manifest.finish_args.len(), 1);
        assert_eq!(manifest.modules.len(), 1);
    }

    #[test]
    fn test_manifest_parsing_yaml() {
        use std::io::Write;
        use tempfile::NamedTempFile;

        let manifest_content = r#"
app-id: org.example.TestApp
sdk: org.gnome.Sdk
runtime: org.gnome.Platform
runtime-version: "47"
command: test-app
modules:
  - name: test-module
    buildsystem: meson
    sources: []
finish-args:
  - --share=network
"#;

        let mut temp_file = NamedTempFile::with_suffix(".yaml").unwrap();
        temp_file.write_all(manifest_content.as_bytes()).unwrap();
        temp_file.flush().unwrap();

        let manifest = Manifest::from_file(temp_file.path()).unwrap();
        assert_eq!(manifest.id, "org.example.TestApp");
        assert_eq!(manifest.sdk, "org.gnome.Sdk");
        assert_eq!(manifest.command, "test-app");
    }

    #[test]
    fn test_manifest_invalid_app_id() {
        use std::io::Write;
        use tempfile::NamedTempFile;

        let manifest_content = r#"{
            "app-id": "invalid",
            "sdk": "org.gnome.Sdk",
            "runtime": "org.gnome.Platform",
            "runtime-version": "47",
            "command": "test-app",
            "modules": []
        }"#;

        let mut temp_file = NamedTempFile::with_suffix(".json").unwrap();
        temp_file.write_all(manifest_content.as_bytes()).unwrap();
        temp_file.flush().unwrap();

        let result = Manifest::from_file(temp_file.path());
        assert!(result.is_err());
        assert!(
            result
                .unwrap_err()
                .to_string()
                .contains("Invalid application ID")
        );
    }
}
