use std::{collections::HashMap, sync::Arc};

use anyhow::{Result, anyhow};
use log::info;
use online_config::{ConfigChange, ConfigValue, OnlineConfig};
use tici_system::readable::{ReadableDuration, ReadableSchedule, ReadableSize};

use crate::{config::MetaServiceConfig, meta::MetaServiceCore};

lazy_static::lazy_static! {
    pub static ref METACONFIG_TYPED: ConfigChange = MetaServiceConfig::default().typed();
}

fn to_change_value(v: &str, typed: &ConfigValue) -> Result<ConfigValue> {
    let v = v.trim_matches('\"');
    let res = match typed {
        ConfigValue::Duration(_) => ConfigValue::from(
            v.parse::<ReadableDuration>()
                .map_err(|e| anyhow!("{}", e))?,
        ),
        ConfigValue::Size(_) => {
            ConfigValue::from(v.parse::<ReadableSize>().map_err(|e| anyhow!("{}", e))?)
        }
        ConfigValue::U64(_) => ConfigValue::from(v.parse::<u64>().map_err(|e| anyhow!("{}", e))?),
        ConfigValue::F64(_) => ConfigValue::from(v.parse::<f64>().map_err(|e| anyhow!("{}", e))?),
        ConfigValue::U32(_) => ConfigValue::from(v.parse::<u32>().map_err(|e| anyhow!("{}", e))?),
        ConfigValue::I32(_) => ConfigValue::from(v.parse::<i32>().map_err(|e| anyhow!("{}", e))?),
        ConfigValue::Usize(_) => {
            ConfigValue::from(v.parse::<usize>().map_err(|e| anyhow!("{}", e))?)
        }
        ConfigValue::Bool(_) => ConfigValue::from(v.parse::<bool>().map_err(|e| anyhow!("{}", e))?),
        ConfigValue::String(_) => ConfigValue::String(v.to_owned()),
        ConfigValue::Schedule(_) => {
            let schedule = v
                .parse::<ReadableSchedule>()
                .map_err(|e| anyhow!("{}", e))?;
            ConfigValue::from(schedule)
        }
        ConfigValue::Skip | ConfigValue::None | ConfigValue::Module(_) => unreachable!(),
    };
    Ok(res)
}

fn to_config_change(change: HashMap<String, String>) -> Result<ConfigChange> {
    fn helper(
        mut fields: Vec<String>,
        dst: &mut ConfigChange,
        typed: &ConfigChange,
        value: String,
    ) -> Result<()> {
        if let Some(field) = fields.pop() {
            return match typed.get(&field) {
                None => Err(anyhow!("unexpect fields: {}", field)),
                Some(ConfigValue::Skip) => Err(anyhow!("config {} can not be changed", field)),
                Some(ConfigValue::Module(m)) => {
                    if let ConfigValue::Module(n_dst) = dst
                        .entry(field)
                        .or_insert_with(|| ConfigValue::Module(HashMap::new()))
                    {
                        return helper(fields, n_dst, m, value);
                    }
                    panic!("unexpect config value");
                }
                Some(v) => {
                    if fields.is_empty() {
                        return match to_change_value(&value, v) {
                            Err(_) => Err(anyhow!("failed to parse: {}", value)),
                            Ok(v) => {
                                dst.insert(field, v);
                                Ok(())
                            }
                        };
                    }
                    let c: Vec<_> = fields.into_iter().rev().collect();
                    Err(anyhow!("unexpect fields: {}", c[..].join(".")))
                }
            };
        }
        Ok(())
    }
    let mut res = HashMap::new();
    for (name, value) in change {
        let fields: Vec<_> = name
            .as_str()
            .split('.')
            .map(|s| s.to_owned())
            .rev()
            .collect();
        helper(fields, &mut res, &METACONFIG_TYPED, value)?;
    }
    Ok(res)
}

impl MetaServiceCore {
    // TODO(P2) support persistence
    pub fn update_config(&self, changes: HashMap<String, String>) -> Result<()> {
        let diff = to_config_change(changes.clone())?;
        info!("update meta config, diff={:?}", diff);
        let mut incoming = (*self.config.load_full()).clone();
        incoming.update(diff).map_err(|e| anyhow!("{}", e))?;
        self.config.store(Arc::new(incoming));
        Ok(())
    }
}
