//! Builder for creating path finders and directory scanners.

use crate::core::{
    AppType, ConfigCandidate, ConfigDiscovery, ConfigFileRule, ConfigRuleSet, ConfigTier,
    DiscoveryOptions, FilePattern, FragmentRule, PathStatus, RuleBasedDiscovery, RuleMatchResult,
    SourceType,
};
use crate::env::StdEnv;
use crate::platform::{DirectoryFinder, DirectoryInfo};
use crate::{Fs, StdFs};
use std::path::{Path, PathBuf};
use std::sync::Arc;

/// Builder for creating platform-specific path finders.
///
/// # Examples
///
/// ```
/// use cfgmatic_paths::{PathsBuilder, AppType};
///
/// // CLI application with XDG directories
/// let finder = PathsBuilder::new("myapp")
///     .app_type(AppType::Cli)
///     .build();
///
/// let user_dirs = finder.user_dirs();
/// ```
#[derive(Debug, Clone)]
pub struct PathsBuilder {
    /// Application name used in paths.
    app_name: String,
    /// Type of application (CLI, GUI, Service).
    app_type: AppType,
    /// Windows-specific company name.
    #[cfg(windows)]
    company_name: Option<String>,
    /// macOS GUI-specific bundle ID.
    #[cfg(all(target_os = "macos", feature = "macos-gui"))]
    bundle_id: Option<String>,
    /// Whether to include legacy `~/.apprc` fallback.
    legacy_rc: bool,
}

impl PathsBuilder {
    /// Create a new builder.
    pub fn new(app_name: impl Into<String>) -> Self {
        Self {
            app_name: app_name.into(),
            app_type: AppType::default(),
            #[cfg(windows)]
            company_name: None,
            #[cfg(all(target_os = "macos", feature = "macos-gui"))]
            bundle_id: None,
            legacy_rc: true,
        }
    }

    /// Set the application type.
    #[must_use]
    pub const fn app_type(mut self, app_type: AppType) -> Self {
        self.app_type = app_type;
        self
    }

    /// Enable/disable legacy `~/.apprc` fallback (Unix only).
    #[must_use]
    pub const fn legacy_rc(mut self, enabled: bool) -> Self {
        self.legacy_rc = enabled;
        self
    }

    /// Windows: set the company name.
    #[cfg(windows)]
    pub fn company_name(mut self, name: impl Into<String>) -> Self {
        self.company_name = Some(name.into());
        self
    }

    /// macOS GUI: set the bundle ID.
    #[cfg(all(target_os = "macos", feature = "macos-gui"))]
    #[must_use]
    pub fn bundle_id(mut self, id: impl Into<String>) -> Self {
        self.bundle_id = Some(id.into());
        self
    }

    /// Build the platform-specific directory finder.
    #[must_use]
    pub fn build(self) -> PathFinder {
        let dir_finder = self.build_directory_finder();
        PathFinder {
            dir_finder,
            fs: Arc::new(StdFs),
        }
    }

    /// Build the directory finder (internal).
    fn build_directory_finder(self) -> Box<dyn DirectoryFinder> {
        cfg_if::cfg_if! {
            if #[cfg(all(target_os = "macos", feature = "macos-gui"))] {
                use crate::platform::MacOSGuiDirectoryFinder;
                use crate::platform::UnixDirectoryFinder;
                if self.app_type == AppType::Gui {
                    let bundle_id = self.bundle_id.unwrap_or_else(|| {
                        format!("com.example.{}", self.app_name)
                    });
                    Box::new(MacOSGuiDirectoryFinder::new(bundle_id))
                } else {
                    // CLI on macOS uses XDG
                    Box::new(UnixDirectoryFinder::new(self.app_name).legacy_rc(self.legacy_rc))
                }
            } else if #[cfg(windows)] {
                use crate::platform::WindowsDirectoryFinder;
                let company_name = self.company_name.unwrap_or_else(|| {
                    self.app_name.clone()
                });
                Box::new(WindowsDirectoryFinder::new(self.app_name, company_name))
            } else {
                use crate::platform::UnixDirectoryFinder;
                // Unix/Linux or macOS CLI
                Box::new(UnixDirectoryFinder::new(self.app_name).legacy_rc(self.legacy_rc))
            }
        }
    }
}

/// Path finder for discovering configuration directories.
///
/// Provides methods to find configuration directories following
/// platform conventions (XDG on Unix, Known Folders on Windows,
/// Application Support on macOS).
pub struct PathFinder {
    /// Platform-specific directory finder.
    dir_finder: Box<dyn DirectoryFinder>,
    /// Filesystem abstraction.
    fs: Arc<dyn Fs>,
}

impl PathFinder {
    /// Get all user directories.
    #[must_use]
    pub fn user_dirs(&self) -> Vec<PathBuf> {
        self.dir_finder.user_dirs(&StdEnv)
    }

    /// Get all local directories.
    #[must_use]
    pub fn local_dirs(&self) -> Vec<PathBuf> {
        self.dir_finder.local_dirs(&StdEnv)
    }

    /// Get all system directories.
    #[must_use]
    pub fn system_dirs(&self) -> Vec<PathBuf> {
        self.dir_finder.system_dirs(&StdEnv)
    }

    /// Get all directories with their tiers.
    #[must_use]
    pub fn all_dirs(&self) -> Vec<DirectoryInfo> {
        [
            self.dirs_with_tier(self.user_dirs(), ConfigTier::User),
            self.dirs_with_tier(self.local_dirs(), ConfigTier::Local),
            self.dirs_with_tier(self.system_dirs(), ConfigTier::System),
        ]
        .into_iter()
        .flatten()
        .collect()
    }

    /// Get the primary user config directory (first user directory).
    #[must_use]
    pub fn user_config_dir(&self) -> Option<PathBuf> {
        self.user_dirs().into_iter().next()
    }

    /// Ensure the user config directory exists, creating it if necessary.
    ///
    /// Returns the path to the directory, or an error if creation fails.
    ///
    /// # Errors
    ///
    /// Returns an error if no user config directory is found or if creation fails.
    pub fn ensure_user_config_dir(&self) -> std::io::Result<PathBuf> {
        let path = self.user_config_dir().ok_or_else(|| {
            std::io::Error::new(
                std::io::ErrorKind::NotFound,
                "No user config directory found",
            )
        })?;
        self.fs.create_dir_all(&path)?;
        Ok(path)
    }

    /// Get the preferred user config path (without checking existence).
    ///
    /// Returns the first user config directory path, regardless of whether
    /// it exists. This is useful for determining where to create a new
    /// configuration file.
    ///
    /// # Examples
    ///
    /// ```
    /// use cfgmatic_paths::PathsBuilder;
    ///
    /// let finder = PathsBuilder::new("myapp").build();
    /// let path = finder.preferred_config_path();
    /// println!("Config would be at: {}", path.display());
    /// ```
    #[must_use]
    pub fn preferred_config_path(&self) -> PathBuf {
        self.user_dirs()
            .into_iter()
            .next()
            .unwrap_or_else(|| PathBuf::from(".config").join("myapp"))
    }

    /// Get the preferred config path with a filename appended.
    ///
    /// # Examples
    ///
    /// ```
    /// use cfgmatic_paths::PathsBuilder;
    ///
    /// let finder = PathsBuilder::new("myapp").build();
    /// let path = finder.preferred_config_file("config.toml");
    /// println!("Config file would be at: {}", path.display());
    /// ```
    #[must_use]
    pub fn preferred_config_file(&self, filename: impl AsRef<Path>) -> PathBuf {
        self.preferred_config_path().join(filename)
    }

    /// Discover configuration with full diagnostics.
    ///
    /// Searches all configuration locations and returns comprehensive
    /// information about what was found, including the preferred path
    /// for creating new configurations.
    ///
    /// # Examples
    ///
    /// ```
    /// use cfgmatic_paths::PathsBuilder;
    ///
    /// let finder = PathsBuilder::new("myapp").build();
    /// let discovery = finder.discover_config();
    ///
    /// println!("Preferred path: {}", discovery.preferred_path.display());
    /// if let Some(found) = &discovery.found_path {
    ///     println!("Found config at: {}", found.display());
    /// }
    ///
    /// for candidate in discovery.candidates {
    ///     println!("  - {:?}: {} ({:?})",
    ///         candidate.tier,
    ///         candidate.path.display(),
    ///         candidate.status
    ///     );
    /// }
    /// ```
    #[must_use]
    pub fn discover_config(&self) -> ConfigDiscovery {
        self.discover_config_with_options(&DiscoveryOptions::default())
    }

    /// Discover configuration with custom options.
    ///
    /// Allows customization of the discovery process including
    /// file patterns, fragment discovery, and legacy path inclusion.
    ///
    /// # Examples
    ///
    /// ```
    /// use cfgmatic_paths::{PathsBuilder, FilePattern, DiscoveryOptions};
    ///
    /// let finder = PathsBuilder::new("myapp").build();
    ///
    /// let options = DiscoveryOptions::new()
    ///     .with_pattern(FilePattern::extensions("config", &["toml", "yaml"]))
    ///     .with_fragments(true)
    ///     .with_fragment_dir("conf.d");
    ///
    /// let discovery = finder.discover_config_with_options(&options);
    /// ```
    #[must_use]
    pub fn discover_config_with_options(&self, options: &DiscoveryOptions) -> ConfigDiscovery {
        let mut candidates = Vec::new();
        let mut fragments = Vec::new();
        let mut found_path: Option<PathBuf> = None;

        // Collect directories with their tiers
        let tiers = [
            (self.dir_finder.user_dirs(&StdEnv), ConfigTier::User),
            (self.dir_finder.local_dirs(&StdEnv), ConfigTier::Local),
            (self.dir_finder.system_dirs(&StdEnv), ConfigTier::System),
        ];

        // Build candidates for each tier
        for (dirs, tier) in tiers {
            self.build_candidates_for_tier(
                &dirs,
                tier,
                options,
                &mut candidates,
                &mut fragments,
                &mut found_path,
            );
        }

        // Sort candidates by tier priority (highest first)
        candidates.sort_by(|a, b| b.tier.cmp(&a.tier));

        ConfigDiscovery {
            preferred_path: self.preferred_config_path(),
            found_path,
            candidates,
            fragments,
        }
    }

    /// Find all configuration files matching a pattern.
    ///
    /// Searches all configuration directories for files matching the
    /// given pattern and returns them as candidates with status information.
    ///
    /// # Examples
    ///
    /// ```
    /// use cfgmatic_paths::{PathsBuilder, FilePattern};
    ///
    /// let finder = PathsBuilder::new("myapp").build();
    /// let pattern = FilePattern::extensions("config", &["toml", "yaml", "json"]);
    ///
    /// let configs = finder.find_config_files(&pattern);
    /// for config in configs {
    ///     if config.exists() {
    ///         println!("Found: {}", config.path.display());
    ///     }
    /// }
    /// ```
    #[must_use]
    pub fn find_config_files(&self, pattern: &FilePattern) -> Vec<ConfigCandidate> {
        let mut candidates = Vec::new();

        let tiers = [
            (self.dir_finder.user_dirs(&StdEnv), ConfigTier::User),
            (self.dir_finder.local_dirs(&StdEnv), ConfigTier::Local),
            (self.dir_finder.system_dirs(&StdEnv), ConfigTier::System),
        ];

        for (dirs, tier) in tiers {
            self.find_files_in_dirs(&dirs, tier, pattern, &mut candidates);
        }

        candidates.sort_by(|a, b| b.tier.cmp(&a.tier));
        candidates
    }

    /// Find configuration fragments from conf.d-style directories.
    ///
    /// Searches for fragment directories (like `/etc/myapp/conf.d/`) and
    /// returns all matching configuration files within them.
    ///
    /// # Examples
    ///
    /// ```
    /// use cfgmatic_paths::{PathsBuilder, FilePattern};
    ///
    /// let finder = PathsBuilder::new("myapp").build();
    /// let pattern = FilePattern::glob("*.conf");
    ///
    /// let fragments = finder.find_fragments(&pattern, "conf.d");
    /// for frag in &fragments {
    ///     println!("Fragment: {}", frag.display());
    /// }
    /// ```
    #[must_use]
    pub fn find_fragments(&self, pattern: &FilePattern, fragment_dir_name: &str) -> Vec<PathBuf> {
        let mut fragments = Vec::new();

        // Search for conf.d directories in each tier
        let dirs_by_tier: [(Vec<PathBuf>, ConfigTier); 3] = [
            (self.dir_finder.user_dirs(&StdEnv), ConfigTier::User),
            (self.dir_finder.local_dirs(&StdEnv), ConfigTier::Local),
            (self.dir_finder.system_dirs(&StdEnv), ConfigTier::System),
        ];

        for (dirs, _tier) in dirs_by_tier {
            for base_dir in dirs {
                let conf_d = base_dir.join(fragment_dir_name);
                if self.fs.is_dir(&conf_d) {
                    // Find matching files in this directory
                    for entry in self.fs.read_dir(&conf_d) {
                        if pattern.matches(&entry) && self.fs.is_file(&entry) {
                            fragments.push(entry);
                        }
                    }
                }
            }
        }

        fragments.sort();
        fragments
    }

    /// Get all config directories where a file could be placed.
    ///
    /// Returns all directories in priority order where configuration
    /// files could be located, regardless of whether they exist.
    ///
    /// # Examples
    ///
    /// ```
    /// use cfgmatic_paths::PathsBuilder;
    ///
    /// let finder = PathsBuilder::new("myapp").build();
    /// let dirs = finder.config_directories();
    ///
    /// for dir in dirs {
    ///     println!("Config directory: {}", dir.display());
    /// }
    /// ```
    #[must_use]
    pub fn config_directories(&self) -> Vec<PathBuf> {
        [
            self.dir_finder.user_dirs(&StdEnv),
            self.dir_finder.local_dirs(&StdEnv),
            self.dir_finder.system_dirs(&StdEnv),
        ]
        .into_iter()
        .flatten()
        .collect()
    }

    /// Get the path status of a specific configuration path.
    ///
    /// # Examples
    ///
    /// ```
    /// use cfgmatic_paths::PathsBuilder;
    ///
    /// let finder = PathsBuilder::new("myapp").build();
    /// let path = finder.preferred_config_file("config.toml");
    ///
    /// let status = finder.path_status(&path);
    /// println!("Path status: {:?}", status);
    /// ```
    #[must_use]
    pub fn path_status(&self, path: &Path) -> PathStatus {
        if !self.fs.exists(path) {
            PathStatus::NotFound
        } else if self.fs.is_file(path) {
            PathStatus::File
        } else {
            PathStatus::Directory
        }
    }

    /// Build candidates for a specific tier.
    fn build_candidates_for_tier(
        &self,
        dirs: &[PathBuf],
        tier: ConfigTier,
        options: &DiscoveryOptions,
        candidates: &mut Vec<ConfigCandidate>,
        fragments: &mut Vec<PathBuf>,
        found_path: &mut Option<PathBuf>,
    ) {
        for dir in dirs {
            // Check the directory itself
            let status = if self.fs.exists(dir) {
                if self.fs.is_dir(dir) {
                    PathStatus::Directory
                } else {
                    PathStatus::File
                }
            } else {
                PathStatus::NotFound
            };

            // Determine source type (first is main, others might be legacy)
            let source_type = if candidates.is_empty() && found_path.is_none() {
                SourceType::MainFile
            } else {
                SourceType::Legacy
            };

            candidates.push(ConfigCandidate::new(dir.clone(), status, tier, source_type));

            // If this is the first found config, record it
            if found_path.is_none() && status.exists() {
                *found_path = Some(dir.clone());
            }

            // Check for config files in the directory
            if status == PathStatus::Directory {
                self.check_dir_for_configs(dir, tier, options, candidates, found_path);
            }

            // Check for fragments directory
            if options.include_fragments {
                let conf_d = dir.join(&options.fragment_dir);
                if self.fs.is_dir(&conf_d) {
                    candidates.push(ConfigCandidate::new(
                        conf_d.clone(),
                        PathStatus::Directory,
                        tier,
                        SourceType::FragmentsDir,
                    ));

                    // Collect fragment files
                    for entry in self.fs.read_dir(&conf_d) {
                        if self.fs.is_file(&entry)
                            && (fragments.is_empty() || !fragments.contains(&entry))
                        {
                            fragments.push(entry);
                        }
                    }
                }
            }
        }
    }

    /// Check a directory for configuration files.
    fn check_dir_for_configs(
        &self,
        dir: &Path,
        tier: ConfigTier,
        options: &DiscoveryOptions,
        candidates: &mut Vec<ConfigCandidate>,
        found_path: &mut Option<PathBuf>,
    ) {
        // Try concrete filenames from pattern
        if let Some(filenames) = options.pattern.concrete_filenames() {
            for filename in filenames {
                let file_path = dir.join(&filename);
                let status = self.path_status(&file_path);

                candidates.push(ConfigCandidate::new(
                    file_path.clone(),
                    status,
                    tier,
                    SourceType::MainFile,
                ));

                if found_path.is_none() && status.exists() {
                    *found_path = Some(file_path);
                }
            }
        }
    }

    /// Find files matching pattern in directories.
    fn find_files_in_dirs(
        &self,
        dirs: &[PathBuf],
        tier: ConfigTier,
        pattern: &FilePattern,
        candidates: &mut Vec<ConfigCandidate>,
    ) {
        for dir in dirs {
            if !self.fs.is_dir(dir) {
                // Check if the directory path itself matches as a file
                let status = self.path_status(dir);
                if pattern.matches(dir) {
                    candidates.push(ConfigCandidate::new(
                        dir.clone(),
                        status,
                        tier,
                        SourceType::MainFile,
                    ));
                }
                continue;
            }

            // Directory exists - search for matching files
            for entry in self.fs.read_dir(dir) {
                if pattern.matches(&entry) {
                    let status = self.path_status(&entry);
                    candidates.push(ConfigCandidate::new(
                        entry.clone(),
                        status,
                        tier,
                        SourceType::MainFile,
                    ));
                }
            }

            // Also check concrete filenames directly
            if let Some(filenames) = pattern.concrete_filenames() {
                for filename in filenames {
                    let file_path = dir.join(&filename);
                    if pattern.matches(&file_path) {
                        let status = self.path_status(&file_path);
                        candidates.push(ConfigCandidate::new(
                            file_path,
                            status,
                            tier,
                            SourceType::MainFile,
                        ));
                    }
                }
            }
        }
    }

    /// Convert paths to directory info with tier.
    fn dirs_with_tier(&self, paths: Vec<PathBuf>, tier: ConfigTier) -> Vec<DirectoryInfo> {
        paths
            .into_iter()
            .map(|path| DirectoryInfo {
                exists: self.fs.is_dir(&path),
                path,
                tier,
            })
            .collect()
    }

    /// Discover configuration files using a rule set.
    ///
    /// Uses the provided rule set to discover all configuration files,
    /// including main files and fragments. Returns a detailed result
    /// with all matched files grouped by rule.
    ///
    /// # Examples
    ///
    /// ```
    /// use cfgmatic_paths::{PathsBuilder, ConfigRuleSet, ConfigFileRule, FragmentRule};
    ///
    /// let finder = PathsBuilder::new("myapp").build();
    ///
    /// let rules = ConfigRuleSet::builder()
    ///     .main_file(ConfigFileRule::toml("config").required(true))
    ///     .main_file(ConfigFileRule::extensions("config", &["yaml"]))
    ///     .fragments(FragmentRule::new("conf.d", "*.conf"))
    ///     .build();
    ///
    /// let discovery = finder.discover_with_rules(&rules);
    ///
    /// // Process discovered files
    /// for result in &discovery.main_files {
    ///     println!("Rule: {:?}", result.rule.pattern);
    ///     for found in &result.matches {
    ///         println!("  Found: {:?}", found.path);
    ///     }
    /// }
    /// ```
    #[must_use]
    pub fn discover_with_rules(&self, rules: &ConfigRuleSet) -> RuleBasedDiscovery {
        let mut main_files = Vec::new();
        let mut fragments = Vec::new();

        // Process each main file rule
        for rule in &rules.main_files {
            let matches = self.find_by_rule(rule);
            main_files.push(RuleMatchResult {
                rule: rule.clone(),
                matches,
            });
        }

        // Process fragments if configured
        if let Some(fragment_rule) = &rules.fragments {
            fragments = self.find_fragments_by_rule(fragment_rule);
        }

        RuleBasedDiscovery {
            rules: rules.clone(),
            main_files,
            fragments,
        }
    }

    /// Find all files matching a single rule.
    fn find_by_rule(&self, rule: &ConfigFileRule) -> Vec<ConfigCandidate> {
        let mut candidates = Vec::new();

        // Iterate through tiers based on the rule's tier search mode
        for tier in rule.tiers.tiers() {
            let dirs = match tier {
                ConfigTier::User => self.dir_finder.user_dirs(&StdEnv),
                ConfigTier::Local => self.dir_finder.local_dirs(&StdEnv),
                ConfigTier::System => self.dir_finder.system_dirs(&StdEnv),
            };

            let source_type = if tier == ConfigTier::User {
                SourceType::MainFile
            } else {
                SourceType::Legacy
            };

            for dir in dirs {
                self.find_files_in_dirs_with_rule(
                    &[dir],
                    tier,
                    &rule.pattern,
                    &mut candidates,
                    source_type,
                );
            }
        }

        // Sort by tier priority (highest first)
        candidates.sort_by(|a, b| b.tier.cmp(&a.tier));
        candidates
    }

    /// Find files matching pattern in directories with a specific source type.
    fn find_files_in_dirs_with_rule(
        &self,
        dirs: &[PathBuf],
        tier: ConfigTier,
        pattern: &FilePattern,
        candidates: &mut Vec<ConfigCandidate>,
        source_type: SourceType,
    ) {
        for dir in dirs {
            if !self.fs.is_dir(dir) {
                // Check if the directory path itself matches as a file
                let status = self.path_status(dir);
                if pattern.matches(dir) {
                    candidates.push(ConfigCandidate::new(dir.clone(), status, tier, source_type));
                }
                continue;
            }

            // Directory exists - search for matching files
            for entry in self.fs.read_dir(dir) {
                if pattern.matches(&entry) {
                    let status = self.path_status(&entry);
                    candidates.push(ConfigCandidate::new(
                        entry.clone(),
                        status,
                        tier,
                        source_type,
                    ));
                }
            }

            // Also check concrete filenames directly
            if let Some(filenames) = pattern.concrete_filenames() {
                for filename in filenames {
                    let file_path = dir.join(&filename);
                    if pattern.matches(&file_path) {
                        let status = self.path_status(&file_path);
                        candidates.push(ConfigCandidate::new(file_path, status, tier, source_type));
                    }
                }
            }
        }
    }

    /// Find fragment files by a fragment rule.
    fn find_fragments_by_rule(&self, rule: &FragmentRule) -> Vec<RuleMatchResult> {
        let mut results = Vec::new();

        // Create a temporary rule for file matching
        let file_rule = ConfigFileRule {
            pattern: rule.pattern.clone(),
            tiers: rule.tiers,
            required: false,
        };

        // Iterate through tiers based on the fragment rule's tier search mode
        for tier in rule.tiers.tiers() {
            let dirs = match tier {
                ConfigTier::User => self.dir_finder.user_dirs(&StdEnv),
                ConfigTier::Local => self.dir_finder.local_dirs(&StdEnv),
                ConfigTier::System => self.dir_finder.system_dirs(&StdEnv),
            };

            for base_dir in dirs {
                let conf_d = base_dir.join(&rule.dir_name);
                if self.fs.is_dir(&conf_d) {
                    let mut matches = Vec::new();

                    // Find matching files in this directory
                    for entry in self.fs.read_dir(&conf_d) {
                        if rule.pattern.matches(&entry) && self.fs.is_file(&entry) {
                            let status = self.path_status(&entry);
                            matches.push(ConfigCandidate::new(
                                entry.clone(),
                                status,
                                tier,
                                SourceType::FragmentsDir,
                            ));
                        }
                    }

                    if !matches.is_empty() {
                        results.push(RuleMatchResult {
                            rule: file_rule.clone(),
                            matches,
                        });
                    }
                }
            }
        }

        results
    }
}

impl std::fmt::Debug for PathFinder {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("PathFinder")
            .field("user_dirs", &self.user_dirs())
            .field("local_dirs", &self.local_dirs())
            .field("system_dirs", &self.system_dirs())
            .finish()
    }
}
