use anyhow::{Context, Result};
use directories::ProjectDirs;
use serde::Deserialize;
use std::collections::{BTreeMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;

#[derive(Debug, Deserialize)]
pub struct Config {
    pub environments: Vec<Environment>,
}

#[derive(Debug, Deserialize, Clone)]
pub struct Environment {
    pub name: String,
    pub env_yaml_dir: PathBuf,
    #[serde(default)]
    pub env_yaml_dir_extra: BTreeMap<String, PathBuf>,
    pub kubectl_context: String,
    pub gcp_project: Option<String>,
    pub protected: Option<bool>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct YamlSource {
    pub name: String,
    pub root: PathBuf,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ServiceSource {
    pub name: String,
    pub kind: String,
    pub image_path: String,
    pub container_name: String,
    pub source_name: String,
    pub source_root: std::path::PathBuf,
    pub yaml_path: std::path::PathBuf,
    pub namespace: Option<String>,
    pub selector: Option<String>,
}

impl Environment {
    pub fn yaml_sources(&self) -> Vec<YamlSource> {
        let mut sources = vec![YamlSource {
            name: "main".to_string(),
            root: self.env_yaml_dir.clone(),
        }];

        sources.extend(self.env_yaml_dir_extra.iter().map(|(name, root)| YamlSource {
            name: name.clone(),
            root: root.clone(),
        }));

        sources
    }

    pub fn list_services(&self) -> Result<Vec<ServiceSource>> {
        let mut services = HashSet::new();

        for source in self.yaml_sources() {
            if !source.root.exists() {
                continue;
            }

            for entry in WalkDir::new(&source.root)
                .into_iter()
                .filter_entry(|e| {
                    if e.depth() == 0 {
                        return true;
                    }
                    !e.file_name()
                        .to_str()
                        .map(|s| s.starts_with('.'))
                        .unwrap_or(false)
                })
                .filter_map(|e| e.ok())
            {
                let path = entry.path();
                if path.is_file() {
                    if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
                        if ext == "yaml" || ext == "yml" {
                            if let Ok(content) = fs::read_to_string(path) {
                                let deserializer = serde_yaml::Deserializer::from_str(&content);
                                for document in deserializer {
                                    match serde_yaml::Value::deserialize(document) {
                                        Ok(resource) => {
                                            if let Some(source) =
                                                self.extract_gcr_service(&source, &resource, path)
                                            {
                                                services.insert(source);
                                            }
                                        }
                                        Err(e) => {
                                            let err_msg = e.to_string();
                                            if !err_msg.contains("more than one document") {
                                                eprintln!(
                                                    "Failed to parse YAML doc in {:?}: {}",
                                                    path, e
                                                );
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        let mut sorted_services = services.into_iter().collect::<Vec<_>>();
        sorted_services.sort_by(|a, b| a.name.cmp(&b.name));
        Ok(sorted_services)
    }

    fn extract_gcr_service(
        &self,
        source: &YamlSource,
        resource: &serde_yaml::Value,
        yaml_path: &std::path::Path,
    ) -> Option<ServiceSource> {
        let kind = resource.get("kind")?.as_str()?;
        let metadata = resource.get("metadata")?;
        let name = metadata.get("name")?.as_str()?;

        let microservice_kinds = ["Deployment", "StatefulSet", "DaemonSet", "Job", "CronJob"];
        if !microservice_kinds.contains(&kind) {
            return None;
        }

        // Search for images in the spec
        if let Some(spec) = resource.get("spec") {
            if let Some((image_path, container_name)) = self.find_gcr_image(spec) {
                let namespace = metadata
                    .get("namespace")
                    .and_then(|v| v.as_str())
                    .map(|s| s.to_string());
                let mut selector = None;

                // Extract app label selector
                if let Some(sel) = spec.get("selector") {
                    if let Some(match_labels) = sel.get("matchLabels") {
                        if let Some(app) = match_labels.get("app") {
                            if let Some(app_str) = app.as_str() {
                                selector = Some(format!("app={}", app_str));
                            }
                        }
                    }
                }

                return Some(ServiceSource {
                    name: name.to_string(),
                    kind: kind.to_string(),
                    image_path,
                    container_name,
                    source_name: source.name.clone(),
                    source_root: source.root.clone(),
                    yaml_path: yaml_path.to_path_buf(),
                    namespace,
                    selector,
                });
            }
        }

        None
    }

    fn find_gcr_image(&self, value: &serde_yaml::Value) -> Option<(String, String)> {
        if let Some(map) = value.as_mapping() {
            // Check if this mapping is a container definition
            if let Some(image_val) = map.get(&serde_yaml::Value::String("image".to_string())) {
                if let Some(img_str) = image_val.as_str() {
                    if img_str.contains("gcr.io") || img_str.contains("pkg.dev") {
                        let container_name = map
                            .get(&serde_yaml::Value::String("name".to_string()))
                            .and_then(|v| v.as_str())
                            .unwrap_or("default")
                            .to_string();
                        return Some((img_str.to_string(), container_name));
                    }
                }
            }

            for (_k, v) in map {
                if let Some(found) = self.find_gcr_image(v) {
                    return Some(found);
                }
            }
        }

        if let Some(seq) = value.as_sequence() {
            for v in seq {
                if let Some(found) = self.find_gcr_image(v) {
                    return Some(found);
                }
            }
        }

        None
    }
}

impl Config {
    pub fn load() -> Result<Self> {
        let config_path = Self::get_config_path()?;

        if !config_path.exists() {
            return Err(anyhow::anyhow!(
                "Config file not found at {}. Please create it based on documentation.",
                config_path.display()
            ));
        }

        let content = fs::read_to_string(&config_path)
            .with_context(|| format!("Failed to read config file at {}", config_path.display()))?;

        let config: Config = toml::from_str(&content)
            .with_context(|| format!("Failed to parse TOML config at {}", config_path.display()))?;

        config.validate()?;

        Ok(config)
    }

    pub fn get_config_path() -> Result<PathBuf> {
        if let Ok(path) = std::env::var("DAVIT_CONFIG") {
            return Ok(PathBuf::from(path));
        }

        let proj_dirs = ProjectDirs::from("com", "davit", "davit")
            .context("Could not determine project directories")?;

        let mut config_path = proj_dirs.config_dir().to_path_buf();
        config_path.push("config.toml");

        Ok(config_path)
    }

    fn validate(&self) -> Result<()> {
        for env in &self.environments {
            env.validate()?;
        }

        Ok(())
    }
}

impl Environment {
    fn validate(&self) -> Result<()> {
        if self.env_yaml_dir_extra.contains_key("main") {
            return Err(anyhow::anyhow!(
                "Environment '{}' uses reserved extra source name 'main'",
                self.name
            ));
        }

        let mut seen_paths = HashSet::new();
        for source in self.yaml_sources() {
            let normalized = normalize_source_path(&source.root);
            if !seen_paths.insert(normalized.clone()) {
                return Err(anyhow::anyhow!(
                    "Environment '{}' defines duplicate YAML source path '{}' (source '{}')",
                    self.name,
                    normalized.display(),
                    source.name
                ));
            }
        }

        Ok(())
    }
}

fn normalize_source_path(path: &Path) -> PathBuf {
    path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn test_list_services_gcr_filter() -> Result<()> {
        let dir = tempdir()?;
        let env_yaml_dir = dir.path().to_path_buf();

        // 1. Valid Deployment with GCR image
        let service1_dir = env_yaml_dir.join("service1");
        fs::create_dir(&service1_dir)?;
        fs::write(
            service1_dir.join("deploy.yaml"),
            r#"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gcr-service
  namespace: test-ns
spec:
  selector:
    matchLabels:
      app: gcr-service-app
  template:
    metadata:
      labels:
        app: gcr-service-app
    spec:
      containers:
      - name: gcr-container
        image: gcr.io/my-project/my-image:latest
"#,
        )?;

        // 2. Valid StatefulSet with Artifact Registry image
        let service2_dir = env_yaml_dir.join("service2");
        fs::create_dir(&service2_dir)?;
        fs::write(
            service2_dir.join("statefulset.yaml"),
            r#"
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: pkg-service
spec:
  template:
    spec:
      containers:
      - name: main
        image: europe-west1-docker.pkg.dev/my-project/my-repo/my-image:v1
"#,
        )?;

        // 3. Invalid Kind (Service)
        fs::write(
            env_yaml_dir.join("service.yaml"),
            r#"
apiVersion: v1
kind: Service
metadata:
  name: not-a-microservice
spec:
  ports:
  - port: 80
"#,
        )?;

        // 4. Invalid Image (Docker Hub)
        let service3_dir = env_yaml_dir.join("service3");
        fs::create_dir(&service3_dir)?;
        fs::write(
            service3_dir.join("deploy.yaml"),
            r#"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dockerhub-service
spec:
  template:
    spec:
      containers:
      - name: main
        image: nginx:latest
"#,
        )?;

        let env = Environment {
            name: "test".to_string(),
            env_yaml_dir,
            env_yaml_dir_extra: BTreeMap::new(),
            kubectl_context: "test".to_string(),
            gcp_project: None,
            protected: None,
        };

        let services = env.list_services()?;
        assert_eq!(services.len(), 2);

        let gcr_service = services.iter().find(|s| s.name == "gcr-service").unwrap();
        assert_eq!(gcr_service.kind, "Deployment");
        assert_eq!(gcr_service.image_path, "gcr.io/my-project/my-image:latest");
        assert_eq!(gcr_service.container_name, "gcr-container");
        assert!(
            gcr_service
                .yaml_path
                .to_str()
                .unwrap()
                .contains("deploy.yaml")
        );
        assert_eq!(
            gcr_service.selector,
            Some("app=gcr-service-app".to_string())
        );
        assert_eq!(gcr_service.namespace, Some("test-ns".to_string()));

        let pkg_service = services.iter().find(|s| s.name == "pkg-service").unwrap();
        assert_eq!(pkg_service.kind, "StatefulSet");
        assert_eq!(
            pkg_service.image_path,
            "europe-west1-docker.pkg.dev/my-project/my-repo/my-image:v1"
        );
        assert!(
            pkg_service
                .yaml_path
                .to_str()
                .unwrap()
                .contains("statefulset.yaml")
        );

        assert!(!services.iter().any(|s| s.name == "not-a-microservice"));
        assert!(!services.iter().any(|s| s.name == "dockerhub-service"));

        Ok(())
    }

    #[test]
    fn test_list_services_includes_named_extra_sources() -> Result<()> {
        let dir = tempdir()?;
        let env_yaml_dir = dir.path().join("main");
        let extra_yaml_dir = dir.path().join("demo");
        fs::create_dir_all(&env_yaml_dir)?;
        fs::create_dir_all(&extra_yaml_dir)?;

        fs::write(
            env_yaml_dir.join("main.yaml"),
            r#"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: shared-service
  namespace: default
spec:
  template:
    spec:
      containers:
      - name: main
        image: gcr.io/my-project/shared:main
"#,
        )?;

        fs::write(
            extra_yaml_dir.join("demo.yaml"),
            r#"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: shared-service
  namespace: default
spec:
  template:
    spec:
      containers:
      - name: demo
        image: gcr.io/my-project/shared:demo
"#,
        )?;

        let env = Environment {
            name: "test".to_string(),
            env_yaml_dir: env_yaml_dir.clone(),
            env_yaml_dir_extra: BTreeMap::from([("demo".to_string(), extra_yaml_dir.clone())]),
            kubectl_context: "test".to_string(),
            gcp_project: None,
            protected: None,
        };

        let services = env.list_services()?;
        assert_eq!(services.len(), 2);

        let main_service = services
            .iter()
            .find(|s| s.source_name == "main")
            .expect("main source service");
        assert_eq!(main_service.source_root, env_yaml_dir);

        let demo_service = services
            .iter()
            .find(|s| s.source_name == "demo")
            .expect("demo source service");
        assert_eq!(demo_service.source_root, extra_yaml_dir);

        Ok(())
    }
}
