//! Configuration loading and parsing logic.

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

use serde::Deserialize;

use crate::extractors::DeclarativeExtractorDef;
use crate::AphoriaError;

use super::types::{AphoriaConfig, ExtractorConfig};

/// Wrapper for deserializing extractor files that use `[[extractors.declarative]]` format.
#[derive(Deserialize)]
struct ExtractorFileWrapper {
    #[serde(default)]
    extractors: ExtractorFileContent,
}

#[derive(Deserialize, Default)]
struct ExtractorFileContent {
    #[serde(default)]
    declarative: Vec<DeclarativeExtractorDef>,
}

impl AphoriaConfig {
    /// Load configuration from a TOML file.
    ///
    /// After parsing the main config, this also scans `.aphoria/extractors/` for
    /// additional `.toml` files containing declarative extractor definitions and
    /// merges them into the config.
    pub fn from_file(path: &Path) -> Result<Self, AphoriaError> {
        if !path.exists() {
            return Err(AphoriaError::ConfigNotFound(path.to_path_buf()));
        }

        let content = std::fs::read_to_string(path)?;
        let mut config: AphoriaConfig = toml::from_str(&content)?;

        // Resolve .aphoria/ directory from config file location:
        // - ".aphoria/config.toml" → parent is ".aphoria/"
        // - "aphoria.toml"         → look for sibling ".aphoria/"
        let aphoria_dir = if let Some(parent) = path.parent() {
            if parent.file_name().map(|n| n == ".aphoria").unwrap_or(false) {
                parent.to_path_buf()
            } else {
                parent.join(".aphoria")
            }
        } else {
            PathBuf::from(".aphoria")
        };

        let extractors_dir = aphoria_dir.join("extractors");
        if extractors_dir.is_dir() {
            load_extractor_files(&mut config.extractors, &extractors_dir);
        }

        Ok(config)
    }
}

/// Load declarative extractor definitions from `.toml` files in a directory.
///
/// Merges them into `config.extractors.declarative`. Invalid files produce
/// warnings but don't fail the load — one bad file should not break the scan.
fn load_extractor_files(extractors: &mut ExtractorConfig, dir: &Path) {
    let entries = match std::fs::read_dir(dir) {
        Ok(e) => e,
        Err(e) => {
            tracing::warn!(dir = %dir.display(), error = %e, "Failed to read extractors directory");
            return;
        }
    };

    for entry in entries.flatten() {
        let path = entry.path();
        if path.extension().and_then(|e| e.to_str()) != Some("toml") || !path.is_file() {
            continue;
        }

        let content = match std::fs::read_to_string(&path) {
            Ok(c) => c,
            Err(e) => {
                tracing::warn!(file = %path.display(), error = %e, "Failed to read extractor file");
                continue;
            }
        };

        match toml::from_str::<ExtractorFileWrapper>(&content) {
            Ok(wrapper) => {
                let count = wrapper.extractors.declarative.len();
                if count > 0 {
                    tracing::debug!(
                        file = %path.display(),
                        count,
                        "Loaded declarative extractors from file"
                    );
                    extractors.declarative.extend(wrapper.extractors.declarative);
                }
            }
            Err(e) => {
                tracing::warn!(
                    file = %path.display(),
                    error = %e,
                    "Failed to parse extractor file"
                );
            }
        }
    }
}
