//! Configuration file support for Hotspots
//!
//! Loads project-specific configuration from JSON files.
//!
//! Search order:
//! 1. Explicit path (--config CLI flag)
//! 2. `.hotspotsrc.json` in project root
//! 3. `hotspots.config.json` in project root
//! 4. `"hotspots"` key in `package.json`
//!
//! All fields are optional. CLI flags take precedence over config file values.

use anyhow::{Context, Result};
use globset::{Glob, GlobSet, GlobSetBuilder};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};

/// Default exclude patterns applied when no config is specified
const DEFAULT_EXCLUDES: &[&str] = &[
    "**/*.test.ts",
    "**/*.test.tsx",
    "**/*.test.js",
    "**/*.test.jsx",
    "**/*.spec.ts",
    "**/*.spec.tsx",
    "**/*.spec.js",
    "**/*.spec.jsx",
    "**/node_modules/**",
    "**/__tests__/**",
    "**/__mocks__/**",
    "**/dist/**",
    "**/build/**",
    // Python test file conventions
    "**/test_*.py",
    "**/*_test.py",
    // Go test file convention
    "**/*_test.go",
];

/// Hotspots configuration loaded from a JSON config file
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HotspotsConfig {
    /// Glob patterns for files to include (default: all supported extensions)
    #[serde(default)]
    pub include: Vec<String>,

    /// Glob patterns for files to exclude (default: test files, node_modules, dist)
    #[serde(default)]
    pub exclude: Vec<String>,

    /// Custom risk band thresholds
    #[serde(default)]
    pub thresholds: Option<ThresholdConfig>,

    /// Custom metric weights for LRS calculation
    #[serde(default)]
    pub weights: Option<WeightConfig>,

    /// Warning thresholds for proactive alerts
    #[serde(default)]
    pub warning_thresholds: Option<WarningThresholdConfig>,

    /// Minimum LRS to report (default: 0.0, report all)
    #[serde(default)]
    pub min_lrs: Option<f64>,

    /// Maximum number of results to show
    #[serde(default)]
    pub top: Option<usize>,

    /// Activity risk scoring weights
    #[serde(default)]
    pub scoring: Option<ScoringWeightsConfig>,

    /// Number of days back to look for co-change pairs (default: 90)
    #[serde(default)]
    pub co_change_window_days: Option<u64>,

    /// Minimum number of co-changes required to report a pair (default: 3)
    #[serde(default)]
    pub co_change_min_count: Option<usize>,

    /// Use per-function git log -L for touch metrics (default: true).
    /// Warm runs use the on-disk cache and are as fast as file-level.
    /// Set to false to always use file-level batching (faster cold start, less accurate).
    #[serde(default)]
    pub per_function_touches: Option<bool>,

    /// Percentile threshold for driving dimension detection (1–99, default: 75).
    /// A function must exceed this percentile in a metric to trigger that driver label.
    /// Lower values = more functions get specific labels; higher = only extreme outliers.
    #[serde(default)]
    pub driver_threshold_percentile: Option<u8>,

    /// Node count above which betweenness centrality switches from exact to approximate
    /// (default: 2000). Below this threshold the exact O(N²) Brandes algorithm runs;
    /// above it, k-source pivot sampling is used instead.
    #[serde(default)]
    pub betweenness_exact_threshold: Option<usize>,

    /// Number of pivot sources for approximate betweenness centrality (default: 256).
    /// Higher values improve accuracy at proportional cost. Only used when the graph
    /// exceeds `betweenness_exact_threshold`. Must be at least 1.
    #[serde(default)]
    pub betweenness_approx_k: Option<usize>,

    /// Pattern detection thresholds. Overrides defaults from `docs/patterns.md`.
    #[serde(default)]
    pub patterns: Option<PatternThresholdsConfig>,
}

/// Custom risk band thresholds
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ThresholdConfig {
    /// LRS threshold for moderate risk (default: 3.0)
    pub moderate: Option<f64>,
    /// LRS threshold for high risk (default: 6.0)
    pub high: Option<f64>,
    /// LRS threshold for critical risk (default: 9.0)
    pub critical: Option<f64>,
}

/// Custom metric weights for LRS calculation
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WeightConfig {
    /// Weight for cyclomatic complexity (default: 1.0)
    pub cc: Option<f64>,
    /// Weight for nesting depth (default: 0.8)
    pub nd: Option<f64>,
    /// Weight for fan-out (default: 0.6)
    pub fo: Option<f64>,
    /// Weight for non-structured exits (default: 0.7)
    pub ns: Option<f64>,
}

/// Weights for activity-weighted risk scoring
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ScoringWeightsConfig {
    /// Weight for churn factor (default: 0.5)
    pub churn: Option<f64>,
    /// Weight for touch frequency factor (default: 0.3)
    pub touch: Option<f64>,
    /// Weight for recency factor (default: 0.2)
    pub recency: Option<f64>,
    /// Weight for fan-in factor (default: 0.4)
    pub fan_in: Option<f64>,
    /// Weight for SCC penalty (default: 0.3)
    pub scc: Option<f64>,
    /// Weight for dependency depth penalty (default: 0.1)
    pub depth: Option<f64>,
    /// Weight for neighbor churn factor (default: 0.2)
    pub neighbor_churn: Option<f64>,
}

/// Pattern detection thresholds — override defaults from `docs/patterns.md`
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PatternThresholdsConfig {
    pub complex_branching_cc: Option<usize>,
    pub complex_branching_nd: Option<usize>,
    pub deeply_nested_nd: Option<usize>,
    pub exit_heavy_ns: Option<usize>,
    pub god_function_loc: Option<usize>,
    pub god_function_fo: Option<usize>,
    pub long_function_loc: Option<usize>,
    pub churn_magnet_churn: Option<usize>,
    pub churn_magnet_cc: Option<usize>,
    pub cyclic_hub_scc: Option<usize>,
    pub cyclic_hub_fan_in: Option<usize>,
    pub hub_function_fan_in: Option<usize>,
    pub hub_function_cc: Option<usize>,
    pub middle_man_fan_in: Option<usize>,
    pub middle_man_fo: Option<usize>,
    pub middle_man_cc_max: Option<usize>,
    pub neighbor_risk_churn: Option<usize>,
    pub neighbor_risk_fo: Option<usize>,
    pub shotgun_target_fan_in: Option<usize>,
    pub shotgun_target_churn: Option<usize>,
    pub stale_complex_cc: Option<usize>,
    pub stale_complex_loc: Option<usize>,
    pub stale_complex_days: Option<u32>,
}

/// Warning thresholds for proactive alerts
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WarningThresholdConfig {
    /// Watch threshold minimum - approaching moderate (default: 2.5)
    pub watch_min: Option<f64>,
    /// Watch threshold maximum - approaching moderate (default: 3.0)
    pub watch_max: Option<f64>,
    /// Attention threshold minimum - approaching high (default: 5.5)
    pub attention_min: Option<f64>,
    /// Attention threshold maximum - approaching high (default: 6.0)
    pub attention_max: Option<f64>,
    /// Rapid growth threshold - percent increase (default: 50.0)
    pub rapid_growth_percent: Option<f64>,
}

/// Resolved configuration with compiled glob patterns
#[derive(Debug)]
pub struct ResolvedConfig {
    /// Compiled include patterns (empty means include all)
    pub include: Option<GlobSet>,
    /// Compiled exclude patterns
    pub exclude: GlobSet,
    /// Risk band thresholds
    pub moderate_threshold: f64,
    pub high_threshold: f64,
    pub critical_threshold: f64,
    /// LRS weights
    pub weight_cc: f64,
    pub weight_nd: f64,
    pub weight_fo: f64,
    pub weight_ns: f64,
    /// Warning thresholds
    pub watch_min: f64,
    pub watch_max: f64,
    pub attention_min: f64,
    pub attention_max: f64,
    pub rapid_growth_percent: f64,
    /// Filters
    pub min_lrs: Option<f64>,
    pub top_n: Option<usize>,
    /// Co-change mining parameters
    pub co_change_window_days: u64,
    pub co_change_min_count: usize,
    /// Whether to use per-function git log -L for touch metrics
    pub per_function_touches: bool,
    /// Percentile threshold for driving dimension detection (1–99)
    pub driver_threshold_percentile: u8,
    /// Node count above which betweenness switches to approximate algorithm
    pub betweenness_exact_threshold: usize,
    /// Number of pivot sources for approximate betweenness
    pub betweenness_approx_k: usize,
    /// Activity risk scoring weights
    pub scoring_weights: crate::scoring::ScoringWeights,
    /// Pattern detection thresholds
    pub pattern_thresholds: crate::patterns::Thresholds,
    /// Path the config was loaded from (None if defaults)
    pub config_path: Option<PathBuf>,
}

impl HotspotsConfig {
    /// Validate the configuration for logical errors
    pub fn validate(&self) -> Result<()> {
        if let Some(ref t) = self.thresholds {
            validate_thresholds(t)?;
        }
        if let Some(ref w) = self.weights {
            validate_weights(w)?;
        }
        if let Some(ref wt) = self.warning_thresholds {
            validate_warning_thresholds(wt)?;
        }
        if let Some(ref s) = self.scoring {
            validate_scoring(s)?;
        }
        if let Some(ref p) = self.patterns {
            validate_pattern_thresholds(p)?;
        }
        validate_scalar_fields(self)?;
        validate_glob_patterns(&self.include, &self.exclude)
    }
}

fn validate_scalar_fields(c: &HotspotsConfig) -> Result<()> {
    if let Some(min) = c.min_lrs {
        if min < 0.0 {
            anyhow::bail!("min_lrs must be non-negative (got {})", min);
        }
    }
    if let Some(w) = c.co_change_window_days {
        if w == 0 {
            anyhow::bail!("co_change_window_days must be at least 1");
        }
    }
    if let Some(m) = c.co_change_min_count {
        if m == 0 {
            anyhow::bail!("co_change_min_count must be at least 1");
        }
    }
    if let Some(p) = c.driver_threshold_percentile {
        if p == 0 || p >= 100 {
            anyhow::bail!(
                "driver_threshold_percentile must be between 1 and 99 (got {})",
                p
            );
        }
    }
    if let Some(k) = c.betweenness_approx_k {
        if k == 0 {
            anyhow::bail!("betweenness_approx_k must be at least 1");
        }
    }
    Ok(())
}

fn validate_glob_patterns(include: &[String], exclude: &[String]) -> Result<()> {
    for pattern in include {
        Glob::new(pattern).with_context(|| format!("invalid include pattern: {}", pattern))?;
    }
    for pattern in exclude {
        Glob::new(pattern).with_context(|| format!("invalid exclude pattern: {}", pattern))?;
    }
    Ok(())
}

fn validate_thresholds(t: &ThresholdConfig) -> Result<()> {
    let moderate = t.moderate.unwrap_or(3.0);
    let high = t.high.unwrap_or(6.0);
    let critical = t.critical.unwrap_or(9.0);
    if moderate <= 0.0 {
        anyhow::bail!("thresholds.moderate must be positive (got {})", moderate);
    }
    if high <= 0.0 {
        anyhow::bail!("thresholds.high must be positive (got {})", high);
    }
    if critical <= 0.0 {
        anyhow::bail!("thresholds.critical must be positive (got {})", critical);
    }
    if moderate >= high {
        anyhow::bail!(
            "thresholds.moderate ({}) must be less than thresholds.high ({})",
            moderate,
            high
        );
    }
    if high >= critical {
        anyhow::bail!(
            "thresholds.high ({}) must be less than thresholds.critical ({})",
            high,
            critical
        );
    }
    Ok(())
}

fn validate_weights(w: &WeightConfig) -> Result<()> {
    for (name, val) in [("cc", w.cc), ("nd", w.nd), ("fo", w.fo), ("ns", w.ns)] {
        if let Some(v) = val {
            if v < 0.0 {
                anyhow::bail!("weights.{} must be non-negative (got {})", name, v);
            }
            if v > 10.0 {
                anyhow::bail!("weights.{} must be at most 10.0 (got {})", name, v);
            }
        }
    }
    Ok(())
}

fn validate_warning_thresholds(wt: &WarningThresholdConfig) -> Result<()> {
    let watch_min = wt.watch_min.unwrap_or(2.5);
    let watch_max = wt.watch_max.unwrap_or(3.0);
    let attention_min = wt.attention_min.unwrap_or(5.5);
    let attention_max = wt.attention_max.unwrap_or(6.0);
    let rapid_growth = wt.rapid_growth_percent.unwrap_or(50.0);
    if watch_min <= 0.0 {
        anyhow::bail!(
            "warning_thresholds.watch_min must be positive (got {})",
            watch_min
        );
    }
    if watch_max <= 0.0 {
        anyhow::bail!(
            "warning_thresholds.watch_max must be positive (got {})",
            watch_max
        );
    }
    if attention_min <= 0.0 {
        anyhow::bail!(
            "warning_thresholds.attention_min must be positive (got {})",
            attention_min
        );
    }
    if attention_max <= 0.0 {
        anyhow::bail!(
            "warning_thresholds.attention_max must be positive (got {})",
            attention_max
        );
    }
    if rapid_growth <= 0.0 {
        anyhow::bail!(
            "warning_thresholds.rapid_growth_percent must be positive (got {})",
            rapid_growth
        );
    }
    if watch_min >= watch_max {
        anyhow::bail!(
            "warning_thresholds.watch_min ({}) must be less than watch_max ({})",
            watch_min,
            watch_max
        );
    }
    if attention_min >= attention_max {
        anyhow::bail!(
            "warning_thresholds.attention_min ({}) must be less than attention_max ({})",
            attention_min,
            attention_max
        );
    }
    Ok(())
}

fn validate_scoring(s: &ScoringWeightsConfig) -> Result<()> {
    for (name, val) in [
        ("churn", s.churn),
        ("touch", s.touch),
        ("recency", s.recency),
        ("fan_in", s.fan_in),
        ("scc", s.scc),
        ("depth", s.depth),
        ("neighbor_churn", s.neighbor_churn),
    ] {
        if let Some(v) = val {
            if v < 0.0 {
                anyhow::bail!("scoring.{} must be non-negative (got {})", name, v);
            }
            if v > 10.0 {
                anyhow::bail!("scoring.{} must be at most 10.0 (got {})", name, v);
            }
        }
    }
    Ok(())
}

fn validate_pattern_thresholds(p: &PatternThresholdsConfig) -> Result<()> {
    // All thresholds must be at least 1 when specified
    let usize_fields: &[(&str, Option<usize>)] = &[
        ("complex_branching_cc", p.complex_branching_cc),
        ("complex_branching_nd", p.complex_branching_nd),
        ("deeply_nested_nd", p.deeply_nested_nd),
        ("exit_heavy_ns", p.exit_heavy_ns),
        ("god_function_loc", p.god_function_loc),
        ("god_function_fo", p.god_function_fo),
        ("long_function_loc", p.long_function_loc),
        ("churn_magnet_churn", p.churn_magnet_churn),
        ("churn_magnet_cc", p.churn_magnet_cc),
        ("cyclic_hub_scc", p.cyclic_hub_scc),
        ("cyclic_hub_fan_in", p.cyclic_hub_fan_in),
        ("hub_function_fan_in", p.hub_function_fan_in),
        ("hub_function_cc", p.hub_function_cc),
        ("middle_man_fan_in", p.middle_man_fan_in),
        ("middle_man_fo", p.middle_man_fo),
        ("middle_man_cc_max", p.middle_man_cc_max),
        ("neighbor_risk_churn", p.neighbor_risk_churn),
        ("neighbor_risk_fo", p.neighbor_risk_fo),
        ("shotgun_target_fan_in", p.shotgun_target_fan_in),
        ("shotgun_target_churn", p.shotgun_target_churn),
        ("stale_complex_cc", p.stale_complex_cc),
        ("stale_complex_loc", p.stale_complex_loc),
    ];
    for (name, val) in usize_fields {
        if let Some(v) = val {
            if *v == 0 {
                anyhow::bail!("patterns.{} must be at least 1 (got 0)", name);
            }
        }
    }
    if let Some(v) = p.stale_complex_days {
        if v == 0 {
            anyhow::bail!("patterns.stale_complex_days must be at least 1 (got 0)");
        }
    }
    Ok(())
}

impl HotspotsConfig {
    /// Resolve config into compiled form ready for use
    pub fn resolve(&self) -> Result<ResolvedConfig> {
        self.validate()?;

        // Compile include patterns
        let include = if self.include.is_empty() {
            None
        } else {
            let mut builder = GlobSetBuilder::new();
            for pattern in &self.include {
                builder.add(Glob::new(pattern)?);
            }
            Some(builder.build()?)
        };

        // Compile exclude patterns (merge with defaults if user didn't specify any)
        let exclude = {
            let mut builder = GlobSetBuilder::new();
            if self.exclude.is_empty() {
                // Use defaults when no excludes specified
                for pattern in DEFAULT_EXCLUDES {
                    builder.add(Glob::new(pattern)?);
                }
            } else {
                for pattern in &self.exclude {
                    builder.add(Glob::new(pattern)?);
                }
            }
            builder.build()?
        };

        let (moderate, high, critical) = match &self.thresholds {
            Some(t) => (
                t.moderate.unwrap_or(3.0),
                t.high.unwrap_or(6.0),
                t.critical.unwrap_or(9.0),
            ),
            None => (3.0, 6.0, 9.0),
        };

        let (w_cc, w_nd, w_fo, w_ns) = match &self.weights {
            Some(w) => (
                w.cc.unwrap_or(1.0),
                w.nd.unwrap_or(0.8),
                w.fo.unwrap_or(0.6),
                w.ns.unwrap_or(0.7),
            ),
            None => (1.0, 0.8, 0.6, 0.7),
        };

        let (watch_min, watch_max, attention_min, attention_max, rapid_growth_percent) =
            match &self.warning_thresholds {
                Some(wt) => (
                    wt.watch_min.unwrap_or(2.5),
                    wt.watch_max.unwrap_or(3.0),
                    wt.attention_min.unwrap_or(5.5),
                    wt.attention_max.unwrap_or(6.0),
                    wt.rapid_growth_percent.unwrap_or(50.0),
                ),
                None => (2.5, 3.0, 5.5, 6.0, 50.0),
            };

        let scoring_weights = match &self.scoring {
            Some(s) => {
                let defaults = crate::scoring::ScoringWeights::default();
                crate::scoring::ScoringWeights {
                    churn: s.churn.unwrap_or(defaults.churn),
                    touch: s.touch.unwrap_or(defaults.touch),
                    recency: s.recency.unwrap_or(defaults.recency),
                    fan_in: s.fan_in.unwrap_or(defaults.fan_in),
                    scc: s.scc.unwrap_or(defaults.scc),
                    depth: s.depth.unwrap_or(defaults.depth),
                    neighbor_churn: s.neighbor_churn.unwrap_or(defaults.neighbor_churn),
                }
            }
            None => crate::scoring::ScoringWeights::default(),
        };

        let pattern_thresholds = match &self.patterns {
            Some(p) => {
                let d = crate::patterns::Thresholds::default();
                crate::patterns::Thresholds {
                    complex_branching_cc: p.complex_branching_cc.unwrap_or(d.complex_branching_cc),
                    complex_branching_nd: p.complex_branching_nd.unwrap_or(d.complex_branching_nd),
                    deeply_nested_nd: p.deeply_nested_nd.unwrap_or(d.deeply_nested_nd),
                    exit_heavy_ns: p.exit_heavy_ns.unwrap_or(d.exit_heavy_ns),
                    god_function_loc: p.god_function_loc.unwrap_or(d.god_function_loc),
                    god_function_fo: p.god_function_fo.unwrap_or(d.god_function_fo),
                    long_function_loc: p.long_function_loc.unwrap_or(d.long_function_loc),
                    churn_magnet_churn: p.churn_magnet_churn.unwrap_or(d.churn_magnet_churn),
                    churn_magnet_cc: p.churn_magnet_cc.unwrap_or(d.churn_magnet_cc),
                    cyclic_hub_scc: p.cyclic_hub_scc.unwrap_or(d.cyclic_hub_scc),
                    cyclic_hub_fan_in: p.cyclic_hub_fan_in.unwrap_or(d.cyclic_hub_fan_in),
                    hub_function_fan_in: p.hub_function_fan_in.unwrap_or(d.hub_function_fan_in),
                    hub_function_cc: p.hub_function_cc.unwrap_or(d.hub_function_cc),
                    middle_man_fan_in: p.middle_man_fan_in.unwrap_or(d.middle_man_fan_in),
                    middle_man_fo: p.middle_man_fo.unwrap_or(d.middle_man_fo),
                    middle_man_cc_max: p.middle_man_cc_max.unwrap_or(d.middle_man_cc_max),
                    neighbor_risk_churn: p.neighbor_risk_churn.unwrap_or(d.neighbor_risk_churn),
                    neighbor_risk_fo: p.neighbor_risk_fo.unwrap_or(d.neighbor_risk_fo),
                    shotgun_target_fan_in: p
                        .shotgun_target_fan_in
                        .unwrap_or(d.shotgun_target_fan_in),
                    shotgun_target_churn: p.shotgun_target_churn.unwrap_or(d.shotgun_target_churn),
                    stale_complex_cc: p.stale_complex_cc.unwrap_or(d.stale_complex_cc),
                    stale_complex_loc: p.stale_complex_loc.unwrap_or(d.stale_complex_loc),
                    stale_complex_days: p.stale_complex_days.unwrap_or(d.stale_complex_days),
                }
            }
            None => crate::patterns::Thresholds::default(),
        };

        Ok(ResolvedConfig {
            include,
            exclude,
            moderate_threshold: moderate,
            high_threshold: high,
            critical_threshold: critical,
            weight_cc: w_cc,
            weight_nd: w_nd,
            weight_fo: w_fo,
            weight_ns: w_ns,
            watch_min,
            watch_max,
            attention_min,
            attention_max,
            rapid_growth_percent,
            min_lrs: self.min_lrs,
            top_n: self.top,
            scoring_weights,
            pattern_thresholds,
            co_change_window_days: self.co_change_window_days.unwrap_or(90),
            co_change_min_count: self.co_change_min_count.unwrap_or(3),
            per_function_touches: self.per_function_touches.unwrap_or(true),
            driver_threshold_percentile: self.driver_threshold_percentile.unwrap_or(75),
            betweenness_exact_threshold: self.betweenness_exact_threshold.unwrap_or(2000),
            betweenness_approx_k: self.betweenness_approx_k.unwrap_or(256),
            config_path: None,
        })
    }
}

impl ResolvedConfig {
    /// Check if a file path should be included based on include/exclude patterns
    pub fn should_include(&self, path: &Path) -> bool {
        let path_str = path.to_string_lossy();

        // Check exclude first
        if self.exclude.is_match(path_str.as_ref()) {
            return false;
        }

        // If include patterns exist, file must match at least one
        if let Some(ref include) = self.include {
            return include.is_match(path_str.as_ref());
        }

        true
    }

    /// Build a ResolvedConfig with all defaults (no config file)
    pub fn defaults() -> Result<Self> {
        HotspotsConfig::default().resolve()
    }
}

/// Discover and load a config file from the project root
///
/// Search order:
/// 1. `.hotspotsrc.json`
/// 2. `hotspots.config.json`
/// 3. `"hotspots"` key in `package.json`
///
/// Returns `None` if no config file is found (use defaults).
pub fn discover_config(project_root: &Path) -> Result<Option<(HotspotsConfig, PathBuf)>> {
    // 1. .hotspotsrc.json
    let rc_path = project_root.join(".hotspotsrc.json");
    if rc_path.exists() {
        let config = load_config_file(&rc_path)?;
        return Ok(Some((config, rc_path)));
    }

    // 2. hotspots.config.json
    let config_path = project_root.join("hotspots.config.json");
    if config_path.exists() {
        let config = load_config_file(&config_path)?;
        return Ok(Some((config, config_path)));
    }

    // 3. package.json "hotspots" key
    let pkg_path = project_root.join("package.json");
    if pkg_path.exists() {
        if let Some(config) = load_from_package_json(&pkg_path)? {
            return Ok(Some((config, pkg_path)));
        }
    }

    Ok(None)
}

/// Load config from an explicit file path
pub fn load_config_file(path: &Path) -> Result<HotspotsConfig> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("failed to read config file: {}", path.display()))?;

    let config: HotspotsConfig = serde_json::from_str(&content)
        .with_context(|| format!("failed to parse config file: {}", path.display()))?;

    config
        .validate()
        .with_context(|| format!("invalid config in: {}", path.display()))?;

    Ok(config)
}

/// Load hotspots config from the "hotspots" key in package.json
fn load_from_package_json(path: &Path) -> Result<Option<HotspotsConfig>> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("failed to read {}", path.display()))?;

    let pkg: serde_json::Value = serde_json::from_str(&content)
        .with_context(|| format!("failed to parse {}", path.display()))?;

    match pkg.get("hotspots") {
        Some(hotspots_value) => {
            let config: HotspotsConfig = serde_json::from_value(hotspots_value.clone())
                .with_context(|| format!("invalid hotspots config in {}", path.display()))?;
            config
                .validate()
                .with_context(|| format!("invalid hotspots config in {}", path.display()))?;
            Ok(Some(config))
        }
        None => Ok(None),
    }
}

/// Load and resolve config for a project
///
/// If `config_path` is provided, loads from that file.
/// Otherwise, discovers config from the project root.
/// Returns default config if nothing is found.
pub fn load_and_resolve(project_root: &Path, config_path: Option<&Path>) -> Result<ResolvedConfig> {
    let (config, source_path) = if let Some(path) = config_path {
        let config = load_config_file(path)?;
        (config, Some(path.to_path_buf()))
    } else {
        match discover_config(project_root)? {
            Some((config, path)) => (config, Some(path)),
            None => (HotspotsConfig::default(), None),
        }
    };

    let mut resolved = config.resolve()?;
    resolved.config_path = source_path;
    Ok(resolved)
}

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

    #[test]
    fn test_default_config_is_valid() {
        let config = HotspotsConfig::default();
        config.validate().expect("default config should be valid");
        let resolved = config.resolve().expect("default config should resolve");
        assert!(resolved.include.is_none());
        assert_eq!(resolved.weight_cc, 1.0);
        assert_eq!(resolved.weight_nd, 0.8);
        assert_eq!(resolved.weight_fo, 0.6);
        assert_eq!(resolved.weight_ns, 0.7);
        assert_eq!(resolved.moderate_threshold, 3.0);
        assert_eq!(resolved.high_threshold, 6.0);
        assert_eq!(resolved.critical_threshold, 9.0);
    }

    #[test]
    fn test_parse_minimal_config() {
        let json = r#"{}"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        config.validate().unwrap();
    }

    #[test]
    fn test_parse_full_config() {
        let json = r#"{
            "include": ["src/**/*.ts", "src/**/*.tsx"],
            "exclude": ["**/*.test.ts", "**/node_modules/**"],
            "thresholds": {
                "moderate": 4.0,
                "high": 7.0,
                "critical": 10.0
            },
            "weights": {
                "cc": 1.2,
                "nd": 0.9,
                "fo": 0.5,
                "ns": 0.8
            },
            "min_lrs": 2.0,
            "top": 20
        }"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        config.validate().unwrap();
        let resolved = config.resolve().unwrap();
        assert!(resolved.include.is_some());
        assert_eq!(resolved.moderate_threshold, 4.0);
        assert_eq!(resolved.high_threshold, 7.0);
        assert_eq!(resolved.critical_threshold, 10.0);
        assert_eq!(resolved.weight_cc, 1.2);
        assert_eq!(resolved.min_lrs, Some(2.0));
        assert_eq!(resolved.top_n, Some(20));
    }

    #[test]
    fn test_reject_unknown_fields() {
        let json = r#"{"unknown_field": true}"#;
        let result: Result<HotspotsConfig, _> = serde_json::from_str(json);
        assert!(result.is_err(), "unknown fields should be rejected");
    }

    #[test]
    fn test_reject_negative_weight() {
        let json = r#"{"weights": {"cc": -1.0}}"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_reject_weight_over_10() {
        let json = r#"{"weights": {"cc": 11.0}}"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_reject_negative_threshold() {
        let json = r#"{"thresholds": {"moderate": -1.0}}"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_reject_unordered_thresholds() {
        let json = r#"{"thresholds": {"moderate": 6.0, "high": 3.0, "critical": 9.0}}"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_reject_invalid_glob_pattern() {
        let json = r#"{"include": ["[invalid"]}"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_should_include_default_excludes() {
        let resolved = ResolvedConfig::defaults().unwrap();
        assert!(!resolved.should_include(Path::new("src/foo.test.ts")));
        assert!(!resolved.should_include(Path::new("node_modules/pkg/index.js")));
        assert!(!resolved.should_include(Path::new("dist/bundle.js")));
        assert!(resolved.should_include(Path::new("src/api.ts")));
        assert!(resolved.should_include(Path::new("src/components/Button.tsx")));
    }

    #[test]
    fn test_should_include_custom_patterns() {
        let config: HotspotsConfig = serde_json::from_str(
            r#"{
            "include": ["src/**/*.ts"],
            "exclude": ["src/generated/**"]
        }"#,
        )
        .unwrap();
        let resolved = config.resolve().unwrap();
        assert!(resolved.should_include(Path::new("src/api.ts")));
        assert!(!resolved.should_include(Path::new("lib/util.ts")));
        assert!(!resolved.should_include(Path::new("src/generated/types.ts")));
    }

    #[test]
    fn test_discover_hotspotsrc() {
        let dir = tempfile::tempdir().unwrap();
        let config_path = dir.path().join(".hotspotsrc.json");
        fs::write(&config_path, r#"{"min_lrs": 5.0}"#).unwrap();

        let result = discover_config(dir.path()).unwrap();
        assert!(result.is_some());
        let (config, path) = result.unwrap();
        assert_eq!(config.min_lrs, Some(5.0));
        assert_eq!(path, config_path);
    }

    #[test]
    fn test_discover_hotspots_config_json() {
        let dir = tempfile::tempdir().unwrap();
        let config_path = dir.path().join("hotspots.config.json");
        fs::write(&config_path, r#"{"top": 10}"#).unwrap();

        let result = discover_config(dir.path()).unwrap();
        assert!(result.is_some());
        let (config, _) = result.unwrap();
        assert_eq!(config.top, Some(10));
    }

    #[test]
    fn test_discover_package_json() {
        let dir = tempfile::tempdir().unwrap();
        let pkg_path = dir.path().join("package.json");
        fs::write(
            &pkg_path,
            r#"{
            "name": "my-project",
            "version": "1.0.0",
            "hotspots": {
                "exclude": ["**/*.test.ts"],
                "min_lrs": 3.0
            }
        }"#,
        )
        .unwrap();

        let result = discover_config(dir.path()).unwrap();
        assert!(result.is_some());
        let (config, _) = result.unwrap();
        assert_eq!(config.min_lrs, Some(3.0));
        assert_eq!(config.exclude, vec!["**/*.test.ts"]);
    }

    #[test]
    fn test_discover_package_json_without_hotspots_key() {
        let dir = tempfile::tempdir().unwrap();
        let pkg_path = dir.path().join("package.json");
        fs::write(&pkg_path, r#"{"name": "my-project", "version": "1.0.0"}"#).unwrap();

        let result = discover_config(dir.path()).unwrap();
        assert!(result.is_none());
    }

    #[test]
    fn test_discover_priority_order() {
        let dir = tempfile::tempdir().unwrap();

        // Create both config files - .hotspotsrc.json should win
        fs::write(dir.path().join(".hotspotsrc.json"), r#"{"min_lrs": 1.0}"#).unwrap();
        fs::write(
            dir.path().join("hotspots.config.json"),
            r#"{"min_lrs": 2.0}"#,
        )
        .unwrap();

        let result = discover_config(dir.path()).unwrap();
        let (config, _) = result.unwrap();
        assert_eq!(
            config.min_lrs,
            Some(1.0),
            ".hotspotsrc.json should take priority"
        );
    }

    #[test]
    fn test_no_config_returns_none() {
        let dir = tempfile::tempdir().unwrap();
        let result = discover_config(dir.path()).unwrap();
        assert!(result.is_none());
    }

    #[test]
    fn test_load_and_resolve_defaults() {
        let dir = tempfile::tempdir().unwrap();
        let resolved = load_and_resolve(dir.path(), None).unwrap();
        assert!(resolved.config_path.is_none());
        assert_eq!(resolved.weight_cc, 1.0);
    }

    #[test]
    fn test_load_and_resolve_explicit_path() {
        let dir = tempfile::tempdir().unwrap();
        let config_path = dir.path().join("custom.json");
        fs::write(&config_path, r#"{"weights": {"cc": 2.0}}"#).unwrap();

        let resolved = load_and_resolve(dir.path(), Some(&config_path)).unwrap();
        assert_eq!(resolved.weight_cc, 2.0);
        assert_eq!(resolved.config_path, Some(config_path));
    }

    #[test]
    fn test_partial_weights_use_defaults_for_rest() {
        let json = r#"{"weights": {"cc": 2.0}}"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        let resolved = config.resolve().unwrap();
        assert_eq!(resolved.weight_cc, 2.0);
        assert_eq!(resolved.weight_nd, 0.8); // default
        assert_eq!(resolved.weight_fo, 0.6); // default
        assert_eq!(resolved.weight_ns, 0.7); // default
    }

    #[test]
    fn test_partial_thresholds_use_defaults_for_rest() {
        let json = r#"{"thresholds": {"critical": 12.0}}"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        let resolved = config.resolve().unwrap();
        assert_eq!(resolved.moderate_threshold, 3.0); // default
        assert_eq!(resolved.high_threshold, 6.0); // default
        assert_eq!(resolved.critical_threshold, 12.0);
    }

    #[test]
    fn test_default_warning_thresholds() {
        let config = HotspotsConfig::default();
        let resolved = config.resolve().unwrap();
        assert_eq!(resolved.watch_min, 2.5);
        assert_eq!(resolved.watch_max, 3.0);
        assert_eq!(resolved.attention_min, 5.5);
        assert_eq!(resolved.attention_max, 6.0);
        assert_eq!(resolved.rapid_growth_percent, 50.0);
    }

    #[test]
    fn test_parse_warning_thresholds() {
        let json = r#"{
            "warning_thresholds": {
                "watch_min": 2.0,
                "watch_max": 3.5,
                "attention_min": 5.0,
                "attention_max": 7.0,
                "rapid_growth_percent": 75.0
            }
        }"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        config.validate().unwrap();
        let resolved = config.resolve().unwrap();
        assert_eq!(resolved.watch_min, 2.0);
        assert_eq!(resolved.watch_max, 3.5);
        assert_eq!(resolved.attention_min, 5.0);
        assert_eq!(resolved.attention_max, 7.0);
        assert_eq!(resolved.rapid_growth_percent, 75.0);
    }

    #[test]
    fn test_partial_warning_thresholds_use_defaults() {
        let json = r#"{"warning_thresholds": {"rapid_growth_percent": 100.0}}"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        let resolved = config.resolve().unwrap();
        assert_eq!(resolved.watch_min, 2.5); // default
        assert_eq!(resolved.watch_max, 3.0); // default
        assert_eq!(resolved.attention_min, 5.5); // default
        assert_eq!(resolved.attention_max, 6.0); // default
        assert_eq!(resolved.rapid_growth_percent, 100.0);
    }

    #[test]
    fn test_reject_negative_watch_min() {
        let json = r#"{"warning_thresholds": {"watch_min": -1.0}}"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_reject_negative_rapid_growth() {
        let json = r#"{"warning_thresholds": {"rapid_growth_percent": -10.0}}"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_reject_unordered_watch_thresholds() {
        let json = r#"{"warning_thresholds": {"watch_min": 3.0, "watch_max": 2.0}}"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_reject_unordered_attention_thresholds() {
        let json = r#"{"warning_thresholds": {"attention_min": 7.0, "attention_max": 5.0}}"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_full_config_with_warnings() {
        let json = r#"{
            "thresholds": {
                "moderate": 3.0,
                "high": 6.0,
                "critical": 9.0
            },
            "warning_thresholds": {
                "watch_min": 2.5,
                "watch_max": 3.0,
                "attention_min": 5.5,
                "attention_max": 6.0,
                "rapid_growth_percent": 50.0
            }
        }"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        config.validate().unwrap();
        let resolved = config.resolve().unwrap();
        assert_eq!(resolved.watch_min, 2.5);
        assert_eq!(resolved.watch_max, 3.0);
        assert_eq!(resolved.attention_min, 5.5);
        assert_eq!(resolved.attention_max, 6.0);
        assert_eq!(resolved.rapid_growth_percent, 50.0);
    }

    #[test]
    fn test_scoring_weights_defaults() {
        let config = HotspotsConfig::default();
        let resolved = config.resolve().unwrap();
        let defaults = crate::scoring::ScoringWeights::default();
        assert_eq!(resolved.scoring_weights.churn, defaults.churn);
        assert_eq!(resolved.scoring_weights.touch, defaults.touch);
        assert_eq!(resolved.scoring_weights.recency, defaults.recency);
        assert_eq!(resolved.scoring_weights.fan_in, defaults.fan_in);
        assert_eq!(resolved.scoring_weights.scc, defaults.scc);
        assert_eq!(resolved.scoring_weights.depth, defaults.depth);
        assert_eq!(
            resolved.scoring_weights.neighbor_churn,
            defaults.neighbor_churn
        );
    }

    #[test]
    fn test_scoring_weights_from_config() {
        let json = r#"{
            "scoring": {
                "churn": 0.8,
                "touch": 0.5,
                "fan_in": 0.6
            }
        }"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        config.validate().unwrap();
        let resolved = config.resolve().unwrap();
        assert_eq!(resolved.scoring_weights.churn, 0.8);
        assert_eq!(resolved.scoring_weights.touch, 0.5);
        assert_eq!(resolved.scoring_weights.fan_in, 0.6);
        // Unspecified fields fall back to defaults
        let defaults = crate::scoring::ScoringWeights::default();
        assert_eq!(resolved.scoring_weights.recency, defaults.recency);
        assert_eq!(resolved.scoring_weights.scc, defaults.scc);
    }

    #[test]
    fn test_reject_negative_scoring_weight() {
        let json = r#"{"scoring": {"churn": -0.1}}"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_reject_scoring_weight_over_10() {
        let json = r#"{"scoring": {"fan_in": 11.0}}"#;
        let config: HotspotsConfig = serde_json::from_str(json).unwrap();
        assert!(config.validate().is_err());
    }
}
