use std::path::{Path, PathBuf};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigPathError {
    #[error("Could not determine config directory for this platform")]
    NoConfigDir,
    #[error("Could not determine data directory for this platform")]
    NoDataDir,
    #[error("Failed to create directory {path}: {source}")]
    CreateDir {
        path: PathBuf,
        source: std::io::Error,
    },
    #[error("Failed to create symlink from {from} to {to}: {source}")]
    Symlink {
        from: PathBuf,
        to: PathBuf,
        source: std::io::Error,
    },
}

/// Platform-native paths for dirigent configuration and data.
///
/// Environment variable overrides (highest priority):
/// - `DIRIGENT_CONFIG_DIR` -- overrides the config directory
/// - `DIRIGENT_DATA_DIR` -- overrides the data directory
///
/// Platform-native defaults (fallback):
///
/// | Platform | Config Dir | Data Dir |
/// |----------|-----------|----------|
/// | Linux | `$XDG_CONFIG_HOME/dirigent/` | `$XDG_DATA_HOME/dirigent/` |
/// | macOS | `~/.config/dirigent/` | `~/Library/Application Support/dirigent/` |
/// | Windows | `%APPDATA%\dirigent\` | `%LOCALAPPDATA%\dirigent\` |
#[derive(Debug, Clone)]
pub struct DirigentPaths {
    config_dir: PathBuf,
    data_dir: PathBuf,
}

impl DirigentPaths {
    /// Resolve platform-native paths.
    pub fn resolve() -> Result<Self, ConfigPathError> {
        let config_dir = Self::resolve_config_dir()?;
        let data_dir = Self::resolve_data_dir()?;
        Ok(Self {
            config_dir,
            data_dir,
        })
    }

    /// Create directories and symlink if they don't exist.
    pub fn ensure_dirs(&self) -> Result<(), ConfigPathError> {
        std::fs::create_dir_all(&self.config_dir).map_err(|e| ConfigPathError::CreateDir {
            path: self.config_dir.clone(),
            source: e,
        })?;

        std::fs::create_dir_all(&self.data_dir).map_err(|e| ConfigPathError::CreateDir {
            path: self.data_dir.clone(),
            source: e,
        })?;

        let noproject = self.noproject_home_dir();
        std::fs::create_dir_all(&noproject).map_err(|e| ConfigPathError::CreateDir {
            path: noproject,
            source: e,
        })?;

        // Create symlink on Linux/macOS: config_dir/data -> data_dir
        #[cfg(unix)]
        {
            let symlink_path = self.config_dir.join("data");
            if symlink_path.symlink_metadata().is_err() {
                std::os::unix::fs::symlink(&self.data_dir, &symlink_path).map_err(|e| {
                    ConfigPathError::Symlink {
                        from: symlink_path,
                        to: self.data_dir.clone(),
                        source: e,
                    }
                })?;
            }
        }

        Ok(())
    }

    /// Where `dirigent.toml` lives.
    pub fn config_dir(&self) -> &Path {
        &self.config_dir
    }

    /// Where archives, projects, caches live.
    pub fn data_dir(&self) -> &Path {
        &self.data_dir
    }

    /// Archive storage directory.
    pub fn archive_dir(&self) -> PathBuf {
        self.data_dir.join("archives")
    }

    /// Project storage directory.
    pub fn projects_dir(&self) -> PathBuf {
        self.data_dir.join("projects")
    }

    /// Default working directory for connectors when no project is active.
    /// Lives under the data directory alongside other runtime artifacts.
    pub fn noproject_home_dir(&self) -> PathBuf {
        self.data_dir.join("noproject_home")
    }

    /// Task output storage directory.
    pub fn tasks_dir(&self) -> PathBuf {
        self.data_dir.join("tasks")
    }

    /// Log storage directory.
    pub fn logs_dir(&self) -> PathBuf {
        self.data_dir.join("logs")
    }

    /// Main config file path.
    pub fn config_file(&self) -> PathBuf {
        self.config_dir.join("dirigent.toml")
    }

    fn resolve_config_dir() -> Result<PathBuf, ConfigPathError> {
        // Environment variable override (highest priority)
        if let Ok(dir) = std::env::var("DIRIGENT_CONFIG_DIR") {
            if !dir.trim().is_empty() {
                // Absolutize relative paths against CWD to prevent nested resolution
                // when subprocesses run from different working directories.
                let path = PathBuf::from(&dir);
                if path.is_relative() {
                    return Ok(std::env::current_dir()
                        .map_err(|_| ConfigPathError::NoConfigDir)?
                        .join(path));
                }
                return Ok(path);
            }
        }

        // Platform-native fallback
        #[cfg(target_os = "macos")]
        {
            dirs::home_dir()
                .map(|d| d.join(".config").join("dirigent"))
                .ok_or(ConfigPathError::NoConfigDir)
        }
        #[cfg(not(target_os = "macos"))]
        {
            dirs::config_dir()
                .map(|d| d.join("dirigent"))
                .ok_or(ConfigPathError::NoConfigDir)
        }
    }

    fn resolve_data_dir() -> Result<PathBuf, ConfigPathError> {
        // Environment variable override (highest priority)
        if let Ok(dir) = std::env::var("DIRIGENT_DATA_DIR") {
            if !dir.trim().is_empty() {
                // Absolutize relative paths against CWD to prevent nested resolution
                // when subprocesses run from different working directories.
                let path = PathBuf::from(&dir);
                if path.is_relative() {
                    return Ok(std::env::current_dir()
                        .map_err(|_| ConfigPathError::NoDataDir)?
                        .join(path));
                }
                return Ok(path);
            }
        }

        // Platform-native fallback
        #[cfg(target_os = "windows")]
        {
            dirs::data_local_dir()
                .map(|d| d.join("dirigent"))
                .ok_or(ConfigPathError::NoDataDir)
        }
        #[cfg(not(target_os = "windows"))]
        {
            dirs::data_dir()
                .map(|d| d.join("dirigent"))
                .ok_or(ConfigPathError::NoDataDir)
        }
    }
}

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

    #[test]
    fn test_resolve_returns_paths() {
        let paths = DirigentPaths::resolve().expect("should resolve paths");
        assert!(paths.config_dir().to_string_lossy().contains("dirigent"));
        assert!(paths.data_dir().to_string_lossy().contains("dirigent"));
    }

    #[test]
    fn test_config_file_path() {
        let paths = DirigentPaths::resolve().unwrap();
        let config = paths.config_file();
        assert!(config.to_string_lossy().ends_with("dirigent.toml"));
    }

    #[test]
    fn test_archive_dir() {
        let paths = DirigentPaths::resolve().unwrap();
        let archive = paths.archive_dir();
        assert!(archive.to_string_lossy().ends_with("archives"));
    }

    #[test]
    fn test_projects_dir() {
        let paths = DirigentPaths::resolve().unwrap();
        let projects = paths.projects_dir();
        assert!(projects.to_string_lossy().ends_with("projects"));
    }

    #[test]
    fn test_config_file_under_config_dir() {
        let paths = DirigentPaths::resolve().unwrap();
        assert!(paths.config_file().starts_with(paths.config_dir()));
    }

    #[test]
    fn test_subdirs_under_data_dir() {
        let paths = DirigentPaths::resolve().unwrap();
        assert!(paths.archive_dir().starts_with(paths.data_dir()));
        assert!(paths.projects_dir().starts_with(paths.data_dir()));
        assert!(paths.logs_dir().starts_with(paths.data_dir()));
    }

    use serial_test::serial;

    #[test]
    #[serial]
    fn test_config_dir_override_from_env() {
        let tmp = std::env::temp_dir().join("dirigent_test_config_override");
        unsafe { std::env::set_var("DIRIGENT_CONFIG_DIR", &tmp) };
        let paths = DirigentPaths::resolve().expect("should resolve with env override");
        assert_eq!(paths.config_dir(), tmp.as_path());
        unsafe { std::env::remove_var("DIRIGENT_CONFIG_DIR") };
    }

    #[test]
    #[serial]
    fn test_data_dir_override_from_env() {
        let tmp = std::env::temp_dir().join("dirigent_test_data_override");
        unsafe { std::env::set_var("DIRIGENT_DATA_DIR", &tmp) };
        let paths = DirigentPaths::resolve().expect("should resolve with env override");
        assert_eq!(paths.data_dir(), tmp.as_path());
        unsafe { std::env::remove_var("DIRIGENT_DATA_DIR") };
    }

    #[test]
    fn test_noproject_home_under_data_dir() {
        let paths = DirigentPaths::resolve().unwrap();
        assert!(paths.noproject_home_dir().starts_with(paths.data_dir()));
    }

    #[test]
    #[serial]
    fn test_both_dir_overrides_from_env() {
        let tmp_config = std::env::temp_dir().join("dirigent_test_both_config");
        let tmp_data = std::env::temp_dir().join("dirigent_test_both_data");
        unsafe { std::env::set_var("DIRIGENT_CONFIG_DIR", &tmp_config) };
        unsafe { std::env::set_var("DIRIGENT_DATA_DIR", &tmp_data) };
        let paths = DirigentPaths::resolve().expect("should resolve with both overrides");
        assert_eq!(paths.config_dir(), tmp_config.as_path());
        assert_eq!(paths.data_dir(), tmp_data.as_path());
        assert!(paths.archive_dir().starts_with(&tmp_data));
        assert!(paths.projects_dir().starts_with(&tmp_data));
        unsafe { std::env::remove_var("DIRIGENT_CONFIG_DIR") };
        unsafe { std::env::remove_var("DIRIGENT_DATA_DIR") };
    }
}
