//! Data File Discovery Module
//!
//! Discovers Parquet and DBN market data files for ML training jobs.
//! Supports multi-asset discovery with automatic format detection and
//! preference for Parquet format over DBN.
//!
//! # Features
//! - Parquet file discovery (primary format)
//! - DBN file fallback (secondary format)
//! - Multi-asset batch discovery
//! - Automatic format preference (Parquet > DBN)
//! - File validation and error handling
//! - Performance: <5ms for 4 assets
//!
//! # Usage
//! ```rust,no_run
//! use ml_training_service::data_file_discovery::DataFileDiscovery;
//! use std::path::Path;
//!
//! let discovery = DataFileDiscovery::new(Path::new("test_data"));
//! let assets = vec!["ES_FUT".to_string(), "NQ_FUT".to_string()];
//! let files = discovery.discover(&assets)?;
//! # Ok::<(), anyhow::Error>(())
//! ```

use anyhow::Result;
use std::path::{Path, PathBuf};
use tracing::debug;
/// Data format types supported for ML training
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DataFormat {
    /// Parquet format (preferred for performance)
    Parquet,
    /// DBN format (Databento native format)
    DBN,
}

/// Discovered data file with metadata
#[derive(Debug, Clone)]
pub struct DataFile {
    /// Full path to the data file
    pub path: PathBuf,
    /// Format of the data file
    pub format: DataFormat,
    /// Asset symbol (e.g., "ES_FUT", "NQ_FUT")
    pub asset: String,
}

impl DataFile {
    /// Create a new DataFile
    pub fn new(path: PathBuf, format: DataFormat, asset: String) -> Self {
        Self {
            path,
            format,
            asset,
        }
    }
}

/// Data file discovery engine
pub struct DataFileDiscovery {
    /// Base directory to search for data files
    base_path: PathBuf,
}

impl DataFileDiscovery {
    /// Create a new data file discovery instance
    ///
    /// # Arguments
    /// * `base_path` - Base directory containing data files (e.g., "test_data/")
    ///
    /// # Example
    /// ```rust,no_run
    /// use ml_training_service::data_file_discovery::DataFileDiscovery;
    /// use std::path::Path;
    ///
    /// let discovery = DataFileDiscovery::new(Path::new("test_data"));
    /// ```
    pub fn new(base_path: impl Into<PathBuf>) -> Self {
        Self {
            base_path: base_path.into(),
        }
    }

    /// Discover data files for the given assets
    ///
    /// For each asset, the discovery process:
    /// 1. Checks for Parquet files (preferred): `{SYMBOL}_180d.parquet`, `{SYMBOL}_90d.parquet`, `{SYMBOL}_small.parquet`
    /// 2. Falls back to DBN files: `{SYMBOL}_180d.dbn`, `{SYMBOL}_90d.dbn`, etc.
    /// 3. Returns error if no valid file is found
    ///
    /// # Arguments
    /// * `assets` - List of asset symbols to discover files for
    ///
    /// # Returns
    /// * `Ok(Vec<DataFile>)` - Discovered data files (one per asset)
    /// * `Err` - If any asset has no valid data file
    ///
    /// # Example
    /// ```rust,no_run
    /// # use ml_training_service::data_file_discovery::DataFileDiscovery;
    /// # use std::path::Path;
    /// let discovery = DataFileDiscovery::new(Path::new("test_data"));
    /// let assets = vec!["ES_FUT".to_string(), "NQ_FUT".to_string()];
    /// let files = discovery.discover(&assets)?;
    /// # Ok::<(), anyhow::Error>(())
    /// ```
    pub fn discover(&self, assets: &[String]) -> Result<Vec<DataFile>> {
        let mut discovered_files = Vec::with_capacity(assets.len());

        for asset in assets {
            debug!("Discovering data file for asset: {}", asset);

            // Try Parquet first (preferred format)
            if let Some(parquet_path) = self.find_parquet(asset) {
                debug!("Found Parquet file for {}: {:?}", asset, parquet_path);
                discovered_files.push(DataFile::new(
                    parquet_path,
                    DataFormat::Parquet,
                    asset.clone(),
                ));
                continue;
            }

            // Fall back to DBN
            if let Some(dbn_path) = self.find_dbn(asset) {
                debug!("Found DBN file for {}: {:?}", asset, dbn_path);
                discovered_files.push(DataFile::new(dbn_path, DataFormat::DBN, asset.clone()));
                continue;
            }

            // No file found - return error
            return Err(anyhow::anyhow!(
                "No data file found for asset '{}' in directory '{}'",
                asset,
                self.base_path.display()
            ));
        }

        Ok(discovered_files)
    }

    /// Find a Parquet file for the given asset
    ///
    /// Searches for files matching patterns:
    /// - `{SYMBOL}_180d.parquet`
    /// - `{SYMBOL}_90d.parquet`
    /// - `{SYMBOL}_small.parquet`
    ///
    /// # Arguments
    /// * `asset` - Asset symbol (e.g., "ES_FUT")
    ///
    /// # Returns
    /// * `Some(PathBuf)` - Path to the Parquet file if found
    /// * `None` - If no Parquet file exists
    pub fn find_parquet(&self, asset: &String) -> Option<PathBuf> {
        // Try common Parquet file patterns
        let patterns = [
            format!("{}_180d.parquet", asset),
            format!("{}_90d.parquet", asset),
            format!("{}_small.parquet", asset),
            // Also try without suffix
            format!("{}.parquet", asset),
        ];

        for pattern in &patterns {
            let candidate = self.base_path.join(pattern);
            if candidate.exists() && candidate.is_file() {
                // Validate it's actually a Parquet file by extension
                if let Some(ext) = candidate.extension() {
                    if ext == "parquet" {
                        return Some(candidate);
                    }
                }
            }
        }

        None
    }

    /// Find a DBN file for the given asset
    ///
    /// Searches for files matching patterns:
    /// - `{SYMBOL}_180d.dbn`
    /// - `{SYMBOL}_90d.dbn`
    /// - `{SYMBOL}_180d_decompressed.dbn`
    /// - `{SYMBOL}_180d_uncompressed.dbn`
    ///
    /// # Arguments
    /// * `asset` - Asset symbol (e.g., "ES_FUT")
    ///
    /// # Returns
    /// * `Some(PathBuf)` - Path to the DBN file if found
    /// * `None` - If no DBN file exists
    pub fn find_dbn(&self, asset: &String) -> Option<PathBuf> {
        // Try common DBN file patterns
        let patterns = [
            format!("{}_180d.dbn", asset),
            format!("{}_90d.dbn", asset),
            format!("{}_180d_decompressed.dbn", asset),
            format!("{}_180d_uncompressed.dbn", asset),
            format!("{}_90d_clean.dbn", asset),
            // Also try without suffix
            format!("{}.dbn", asset),
        ];

        for pattern in &patterns {
            let candidate = self.base_path.join(pattern);
            if candidate.exists() && candidate.is_file() {
                // Validate it's actually a DBN file by extension
                if let Some(ext) = candidate.extension() {
                    if ext == "dbn" {
                        return Some(candidate);
                    }
                }
            }
        }

        None
    }

    /// Get the base path for this discovery instance
    pub fn base_path(&self) -> &Path {
        &self.base_path
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
    use super::*;
    use std::fs::File;
    use tempfile::TempDir;

    #[test]
    fn test_new_discovery() {
        let discovery = DataFileDiscovery::new("/tmp/test_data");
        assert_eq!(discovery.base_path(), Path::new("/tmp/test_data"));
    }

    #[test]
    fn test_data_file_creation() {
        let path = PathBuf::from("/tmp/test.parquet");
        let file = DataFile::new(path.clone(), DataFormat::Parquet, "ES_FUT".to_string());

        assert_eq!(file.path, path);
        assert_eq!(file.format, DataFormat::Parquet);
        assert_eq!(file.asset, "ES_FUT");
    }

    #[test]
    fn test_data_format_equality() {
        assert_eq!(DataFormat::Parquet, DataFormat::Parquet);
        assert_eq!(DataFormat::DBN, DataFormat::DBN);
        assert_ne!(DataFormat::Parquet, DataFormat::DBN);
    }

    #[test]
    fn test_find_parquet_with_real_file() {
        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Create a test Parquet file
        let test_file = base_path.join("ES_FUT_180d.parquet");
        File::create(&test_file).unwrap();

        let discovery = DataFileDiscovery::new(base_path);
        let result = discovery.find_parquet(&"ES_FUT".to_string());

        assert!(result.is_some());
        assert_eq!(result.unwrap(), test_file);
    }

    #[test]
    fn test_find_dbn_with_real_file() {
        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Create a test DBN file
        let test_file = base_path.join("NQ_FUT_180d.dbn");
        File::create(&test_file).unwrap();

        let discovery = DataFileDiscovery::new(base_path);
        let result = discovery.find_dbn(&"NQ_FUT".to_string());

        assert!(result.is_some());
        assert_eq!(result.unwrap(), test_file);
    }

    #[test]
    fn test_discover_empty_assets() {
        let temp_dir = TempDir::new().unwrap();
        let discovery = DataFileDiscovery::new(temp_dir.path());

        let result = discovery.discover(&[]);
        assert!(result.is_ok());
        assert_eq!(result.unwrap().len(), 0);
    }
}
