use serde::Deserialize;
use std::collections::HashMap;
use uforia_ms_types::{Asset, CmError, Horizon};

#[derive(Debug, Clone, Deserialize)]
pub struct AppConfig {
    pub general: GeneralConfig,
    pub feeds: FeedsConfig,
    pub bar: BarConfig,
    pub costs: HashMap<String, CostConfig>,
    pub features: FeaturesConfig,
    pub regime: RegimeConfig,
    pub model: ModelConfig,
    pub risk: RiskConfig,
    pub storage: StorageConfig,
    #[serde(default)]
    pub api: ApiConfig,
}

#[derive(Debug, Clone, Deserialize)]
pub struct GeneralConfig {
    #[serde(default = "default_assets")]
    pub assets: Vec<String>,
    #[serde(default = "default_log_level")]
    pub log_level: String,
    #[serde(default = "default_metrics_port")]
    pub metrics_port: u16,
}

fn default_assets() -> Vec<String> {
    vec!["btc".into(), "eth".into()]
}

fn default_log_level() -> String {
    "info".into()
}

fn default_metrics_port() -> u16 {
    9090
}

#[derive(Debug, Clone, Deserialize)]
pub struct FeedsConfig {
    #[serde(default)]
    pub binance_spot: FeedEndpoint,
    #[serde(default)]
    pub binance_futures: FeedEndpoint,
    #[serde(default)]
    pub bybit_spot: FeedEndpoint,
    #[serde(default)]
    pub bybit_perps: FeedEndpoint,
    #[serde(default)]
    pub deribit: FeedEndpoint,
}

#[derive(Debug, Clone, Deserialize)]
pub struct FeedEndpoint {
    #[serde(default = "default_true")]
    pub enabled: bool,
    #[serde(default)]
    pub ws_url: String,
    #[serde(default)]
    pub rest_url: String,
    #[serde(default = "default_reconnect_base_ms")]
    pub reconnect_base_ms: u64,
    #[serde(default = "default_reconnect_max_ms")]
    pub reconnect_max_ms: u64,
}

impl Default for FeedEndpoint {
    fn default() -> Self {
        Self {
            enabled: true,
            ws_url: String::new(),
            rest_url: String::new(),
            reconnect_base_ms: 1000,
            reconnect_max_ms: 60000,
        }
    }
}

fn default_true() -> bool {
    true
}

fn default_reconnect_base_ms() -> u64 {
    1000
}

fn default_reconnect_max_ms() -> u64 {
    60000
}

#[derive(Debug, Clone, Deserialize)]
pub struct BarConfig {
    #[serde(default = "default_bar_interval_secs")]
    pub interval_secs: u64,
    #[serde(default = "default_flush_delay_ms")]
    pub flush_delay_ms: u64,
    #[serde(default = "default_gap_threshold_secs")]
    pub gap_threshold_secs: u64,
}

fn default_bar_interval_secs() -> u64 {
    60
}

fn default_flush_delay_ms() -> u64 {
    200
}

fn default_gap_threshold_secs() -> u64 {
    30
}

#[derive(Debug, Clone, Deserialize)]
pub struct CostConfig {
    pub fee_bps: f64,
    pub spread_bps: f64,
    pub slip_bps: f64,
    #[serde(default = "default_impact_bps")]
    pub impact_bps: f64,
    #[serde(default = "default_safety_factor")]
    pub safety_factor: f64,
}

fn default_impact_bps() -> f64 {
    0.5
}

fn default_safety_factor() -> f64 {
    1.5
}

#[derive(Debug, Clone, Deserialize)]
pub struct FeaturesConfig {
    #[serde(default = "default_book_levels")]
    pub book_levels: usize,
    #[serde(default = "default_flow_windows")]
    pub flow_windows_secs: Vec<u64>,
    #[serde(default = "default_vol_windows")]
    pub vol_windows_mins: Vec<u64>,
    #[serde(default = "default_normalizer_window")]
    pub normalizer_window: usize,
}

fn default_book_levels() -> usize {
    5
}

fn default_flow_windows() -> Vec<u64> {
    vec![30, 60, 180]
}

fn default_vol_windows() -> Vec<u64> {
    vec![5, 15, 30]
}

fn default_normalizer_window() -> usize {
    10080
}

#[derive(Debug, Clone, Deserialize)]
pub struct RegimeConfig {
    #[serde(default = "default_lookback_days")]
    pub lookback_days: Vec<u64>,
    #[serde(default = "default_persistence_k")]
    pub persistence_k: u32,
    #[serde(default = "default_enter_pct")]
    pub enter_pct: f64,
    #[serde(default = "default_exit_pct")]
    pub exit_pct: f64,
    #[serde(default = "default_stressed_vol_pct")]
    pub stressed_vol_pct: f64,
    #[serde(default = "default_stressed_spread_pct")]
    pub stressed_spread_pct: f64,
    #[serde(default = "default_stressed_depth_pct")]
    pub stressed_depth_pct: f64,
    #[serde(default = "default_vol_expansion_vol_pct")]
    pub vol_expansion_vol_pct: f64,
    #[serde(default = "default_vol_expansion_spread_pct")]
    pub vol_expansion_spread_pct: f64,
    #[serde(default = "default_calm_trend_trend_pct")]
    pub calm_trend_trend_pct: f64,
    #[serde(default = "default_calm_trend_vol_pct")]
    pub calm_trend_vol_pct: f64,
}

fn default_lookback_days() -> Vec<u64> {
    vec![7, 30, 90]
}

fn default_persistence_k() -> u32 {
    3
}

fn default_enter_pct() -> f64 {
    85.0
}

fn default_exit_pct() -> f64 {
    75.0
}

fn default_stressed_vol_pct() -> f64 {
    85.0
}

fn default_stressed_spread_pct() -> f64 {
    85.0
}

fn default_stressed_depth_pct() -> f64 {
    15.0
}

fn default_vol_expansion_vol_pct() -> f64 {
    70.0
}

fn default_vol_expansion_spread_pct() -> f64 {
    60.0
}

fn default_calm_trend_trend_pct() -> f64 {
    60.0
}

fn default_calm_trend_vol_pct() -> f64 {
    60.0
}

#[derive(Debug, Clone, Deserialize)]
pub struct ModelConfig {
    #[serde(default = "default_weights_dir")]
    pub weights_dir: String,
    #[serde(default = "default_reload_interval_secs")]
    pub reload_interval_secs: u64,
    pub delta_base: Option<HashMap<String, f64>>,
    #[serde(default = "default_p_flat_cap")]
    pub p_flat_cap: f64,
}

fn default_weights_dir() -> String {
    "models".into()
}

fn default_reload_interval_secs() -> u64 {
    300
}

fn default_p_flat_cap() -> f64 {
    0.6
}

#[derive(Debug, Clone, Deserialize)]
pub struct RiskConfig {
    #[serde(default = "default_max_daily_loss")]
    pub max_daily_loss: f64,
    #[serde(default = "default_max_position_size")]
    pub max_position_size: f64,
    #[serde(default = "default_max_concurrent")]
    pub max_concurrent_positions: usize,
    #[serde(default = "default_risk_budget")]
    pub risk_budget: f64,
    #[serde(default = "default_drawdown_kill")]
    pub drawdown_kill_pct: f64,
    #[serde(default = "default_vol_stop_multiplier")]
    pub vol_stop_multiplier: f64,
}

fn default_max_daily_loss() -> f64 {
    1000.0
}

fn default_max_position_size() -> f64 {
    0.1
}

fn default_max_concurrent() -> usize {
    4
}

fn default_risk_budget() -> f64 {
    100.0
}

fn default_drawdown_kill() -> f64 {
    5.0
}

fn default_vol_stop_multiplier() -> f64 {
    2.0
}

#[derive(Debug, Clone, Deserialize)]
pub struct ApiConfig {
    #[serde(default = "default_api_enabled")]
    pub enabled: bool,
    #[serde(default = "default_rest_port")]
    pub rest_port: u16,
    #[serde(default = "default_grpc_port")]
    pub grpc_port: u16,
    #[serde(default = "default_true")]
    pub ws_enabled: bool,
    #[serde(default)]
    pub cors_origins: Vec<String>,
    #[serde(default)]
    pub dashboard_dir: Option<String>,
}

impl Default for ApiConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            rest_port: 8080,
            grpc_port: 50051,
            ws_enabled: true,
            cors_origins: vec![],
            dashboard_dir: None,
        }
    }
}

fn default_api_enabled() -> bool {
    true
}

fn default_rest_port() -> u16 {
    8080
}

fn default_grpc_port() -> u16 {
    50051
}

#[derive(Debug, Clone, Deserialize)]
pub struct StorageConfig {
    pub postgres: PostgresStorageConfig,
    #[serde(default)]
    pub clickhouse: Option<ClickHouseConfig>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct PostgresStorageConfig {
    #[serde(default)]
    pub dsn: String,
    #[serde(default = "default_postgres_schema")]
    pub schema: String,
    #[serde(default = "default_batch_size")]
    pub buffer_size: usize,
}

fn default_postgres_schema() -> String {
    "uforia".into()
}

fn default_batch_size() -> usize {
    1000
}

#[derive(Debug, Clone, Deserialize)]
pub struct ClickHouseConfig {
    #[serde(default = "default_ch_url")]
    pub url: String,
    #[serde(default = "default_ch_db")]
    pub database: String,
    #[serde(default = "default_ch_batch")]
    pub batch_size: usize,
    #[serde(default = "default_ch_flush")]
    pub flush_interval_secs: u64,
}

fn default_ch_url() -> String {
    "http://localhost:8123".into()
}

fn default_ch_db() -> String {
    "crypto_microstructure".into()
}

fn default_ch_batch() -> usize {
    5000
}

fn default_ch_flush() -> u64 {
    10
}

impl AppConfig {
    pub fn apply_env_overrides(&mut self) {
        if let Ok(val) = std::env::var("CM_LOG_LEVEL") {
            self.general.log_level = val;
        }
        if let Ok(val) = std::env::var("CM_METRICS_PORT") {
            if let Ok(port) = val.parse() {
                self.general.metrics_port = port;
            }
        }
        if let Ok(val) = std::env::var("UFORIA_DB_DSN") {
            self.storage.postgres.dsn = val;
        } else if let Ok(val) = std::env::var("CM_POSTGRES_DSN") {
            self.storage.postgres.dsn = val;
        }
        if let Ok(val) = std::env::var("UFORIA_DB_SCHEMA") {
            self.storage.postgres.schema = val;
        } else if let Ok(val) = std::env::var("CM_POSTGRES_SCHEMA") {
            self.storage.postgres.schema = val;
        }
        if let Ok(val) = std::env::var("CM_POSTGRES_BATCH_SIZE") {
            if let Ok(size) = val.parse() {
                self.storage.postgres.buffer_size = size;
            }
        }
        if let Ok(val) = std::env::var("CM_CLICKHOUSE_URL") {
            if let Some(ref mut ch) = self.storage.clickhouse {
                ch.url = val;
            }
        }
        if let Ok(val) = std::env::var("CM_WEIGHTS_DIR") {
            self.model.weights_dir = val;
        }
        if let Ok(val) = std::env::var("CM_MAX_DAILY_LOSS") {
            if let Ok(v) = val.parse() {
                self.risk.max_daily_loss = v;
            }
        }
        if let Ok(val) = std::env::var("CM_API_REST_PORT") {
            if let Ok(port) = val.parse() {
                self.api.rest_port = port;
            }
        }
        if let Ok(val) = std::env::var("CM_API_GRPC_PORT") {
            if let Ok(port) = val.parse() {
                self.api.grpc_port = port;
            }
        }
    }

    pub fn validate(&self) -> Result<(), CmError> {
        if self.general.assets.is_empty() {
            return Err(CmError::Config("No assets configured".into()));
        }
        for asset_str in &self.general.assets {
            if asset_str != "btc" && asset_str != "eth" {
                return Err(CmError::Config(format!("Unknown asset: {}", asset_str)));
            }
        }
        if self.risk.max_daily_loss <= 0.0 {
            return Err(CmError::Config("max_daily_loss must be positive".into()));
        }
        if self.risk.risk_budget <= 0.0 {
            return Err(CmError::Config("risk_budget must be positive".into()));
        }
        Ok(())
    }

    pub fn parsed_assets(&self) -> Vec<Asset> {
        self.general
            .assets
            .iter()
            .filter_map(|s| match s.as_str() {
                "btc" => Some(Asset::Btc),
                "eth" => Some(Asset::Eth),
                _ => None,
            })
            .collect()
    }

    pub fn cost_for_asset(&self, asset: Asset) -> Option<&CostConfig> {
        self.costs.get(&asset.to_string())
    }

    pub fn tau_h(&self, asset: Asset, horizon: Horizon) -> f64 {
        let horizon_multiplier = match horizon {
            Horizon::M1 => 2.0,
            Horizon::M5 => 1.5,
            Horizon::M15 => 1.2,
            Horizon::M30 => 1.0,
        };
        if let Some(cost) = self.cost_for_asset(asset) {
            let cost_bps = cost.fee_bps + cost.spread_bps + cost.slip_bps + cost.impact_bps;
            cost_bps * 1e-4 * cost.safety_factor * horizon_multiplier
        } else {
            10.0 * 1e-4 * horizon_multiplier
        }
    }

    pub fn delta_base(&self, horizon: Horizon) -> f64 {
        if let Some(ref deltas) = self.model.delta_base {
            if let Some(v) = deltas.get(&horizon.to_string()) {
                return *v;
            }
        }
        match horizon {
            Horizon::M1 => 0.04,
            Horizon::M5 => 0.03,
            Horizon::M15 => 0.02,
            Horizon::M30 => 0.02,
        }
    }
}

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

    fn sample_toml() -> &'static str {
        r#"
[general]
assets = ["btc", "eth"]
log_level = "info"
metrics_port = 9090

[feeds.binance_spot]
enabled = true
ws_url = "wss://stream.binance.com:9443/ws"
rest_url = "https://api.binance.com"

[feeds.binance_futures]
enabled = true
ws_url = "wss://fstream.binance.com/ws"
rest_url = "https://fapi.binance.com"

[feeds.bybit_spot]
enabled = true
ws_url = "wss://stream.bybit.com/v5/public/spot"
rest_url = "https://api.bybit.com"

[feeds.bybit_perps]
enabled = true
ws_url = "wss://stream.bybit.com/v5/public/linear"
rest_url = "https://api.bybit.com"

[feeds.deribit]
enabled = true
ws_url = "wss://www.deribit.com/ws/api/v2"
rest_url = "https://www.deribit.com/api/v2"

[bar]
interval_secs = 60
flush_delay_ms = 200
gap_threshold_secs = 30

[costs.btc]
fee_bps = 2.0
spread_bps = 1.0
slip_bps = 1.0
impact_bps = 0.5
safety_factor = 1.5

[costs.eth]
fee_bps = 2.0
spread_bps = 1.5
slip_bps = 1.5
impact_bps = 1.0
safety_factor = 1.5

[features]
book_levels = 5
flow_windows_secs = [30, 60, 180]
vol_windows_mins = [5, 15, 30]
normalizer_window = 10080

[regime]
lookback_days = [7, 30, 90]
persistence_k = 3
enter_pct = 85.0
exit_pct = 75.0

[model]
weights_dir = "models"
reload_interval_secs = 300
p_flat_cap = 0.6

[model.delta_base]
"1m" = 0.04
"5m" = 0.03
"15m" = 0.02
"30m" = 0.02

[risk]
max_daily_loss = 1000.0
max_position_size = 0.1
max_concurrent_positions = 4
risk_budget = 100.0
drawdown_kill_pct = 5.0
vol_stop_multiplier = 2.0

[storage.postgres]
dsn = "postgresql://localhost/uforia"
schema = "uforia"
buffer_size = 1000

[storage.clickhouse]
url = "http://localhost:8123"
database = "crypto_microstructure"
batch_size = 5000
flush_interval_secs = 10
"#
    }

    #[test]
    fn test_parse_default_config() {
        let config: AppConfig = toml::from_str(sample_toml()).unwrap();
        assert_eq!(config.general.assets, vec!["btc", "eth"]);
        assert_eq!(config.bar.interval_secs, 60);
        assert!(config.costs.contains_key("btc"));
        assert!(config.costs.contains_key("eth"));
        assert_eq!(config.regime.persistence_k, 3);
    }

    #[test]
    fn test_validate_valid_config() {
        let config: AppConfig = toml::from_str(sample_toml()).unwrap();
        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_validate_empty_assets() {
        let mut config: AppConfig = toml::from_str(sample_toml()).unwrap();
        config.general.assets.clear();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_validate_unknown_asset() {
        let mut config: AppConfig = toml::from_str(sample_toml()).unwrap();
        config.general.assets.push("sol".into());
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_tau_h() {
        let config: AppConfig = toml::from_str(sample_toml()).unwrap();
        let tau = config.tau_h(Asset::Btc, Horizon::M1);
        // (2+1+1+0.5) * 1e-4 * 1.5 * 2.0 (M1 multiplier) = 4.5 * 1.5e-4 * 2.0 = 1.35e-3
        assert!((tau - 1.35e-3).abs() < 1e-10);
    }

    #[test]
    fn test_tau_h_horizon_aware() {
        let config: AppConfig = toml::from_str(sample_toml()).unwrap();
        let tau_m1 = config.tau_h(Asset::Btc, Horizon::M1);
        let tau_m5 = config.tau_h(Asset::Btc, Horizon::M5);
        let tau_m15 = config.tau_h(Asset::Btc, Horizon::M15);
        let tau_m30 = config.tau_h(Asset::Btc, Horizon::M30);

        // Shorter horizons should have higher thresholds
        assert!(tau_m1 > tau_m5);
        assert!(tau_m5 > tau_m15);
        assert!(tau_m15 > tau_m30);

        // Verify exact multipliers: M1=2.0, M5=1.5, M15=1.2, M30=1.0
        let base = tau_m30; // M30 multiplier is 1.0
        assert!((tau_m1 / base - 2.0).abs() < 1e-10);
        assert!((tau_m5 / base - 1.5).abs() < 1e-10);
        assert!((tau_m15 / base - 1.2).abs() < 1e-10);
    }

    #[test]
    fn test_delta_base() {
        let config: AppConfig = toml::from_str(sample_toml()).unwrap();
        assert!((config.delta_base(Horizon::M1) - 0.04).abs() < 1e-10);
        assert!((config.delta_base(Horizon::M5) - 0.03).abs() < 1e-10);
    }

    #[test]
    fn test_parsed_assets() {
        let config: AppConfig = toml::from_str(sample_toml()).unwrap();
        let assets = config.parsed_assets();
        assert_eq!(assets, vec![Asset::Btc, Asset::Eth]);
    }
}
