use crate::observability::config::ObservabilityConfig;
use crate::observability::config_file::{
    reload_observability_filters, validate_observability_config,
};
use crate::observability::init::{init_observability, ObservabilityGuards};
use crate::observability::reload::ObservabilityReloadHandle;
use anyhow::{bail, Context, Result};
use pi_config::{Config, Value};

/// Reads and validates an observability configuration subtree from `pi_config`.
///
/// `path` must identify a table containing the same `log`, `trace`, and
/// optional `metrics` structure accepted by [`crate::observability::init_observability`].
/// The complete merged subtree is read with `Config::sub_value`, so values from
/// TOML, CLI, environment, defaults, and overlays are already resolved.
pub fn observability_config_from_pi_config(
    config: &mut Config,
    path: &str,
) -> Result<ObservabilityConfig> {
    let value = config
        .sub_value(path)
        .with_context(|| format!("failed to read pi_config subtree `{path}`"))?;
    let value = pi_value_to_toml(value, path)?;
    let config: ObservabilityConfig = value
        .try_into()
        .with_context(|| format!("failed to parse pi_config subtree `{path}`"))?;
    validate_observability_config(&config)?;
    Ok(config)
}

/// Initializes observability from a merged `pi_config` subtree.
///
/// This initializes output topology once. Later configuration notifications
/// can call [`reload_observability_filters_from_pi_config`] to update only
/// local, remote, and trace filter rules. Output enable flags, endpoints,
/// formats, file paths, and metrics settings require process restart.
pub fn init_observability_from_pi_config(
    config: &mut Config,
    path: &str,
) -> Result<(ObservabilityReloadHandle, ObservabilityGuards)> {
    init_observability(observability_config_from_pi_config(config, path)?)
}

/// Applies reloadable observability filters from a merged `pi_config` subtree.
///
/// The complete subtree is parsed and validated before any active filter is
/// replaced. Invalid updates leave current filters unchanged. Switching remote
/// logs between a shared local filter and an independent remote filter requires
/// process restart and returns an error.
pub fn reload_observability_filters_from_pi_config(
    reload: &ObservabilityReloadHandle,
    config: &mut Config,
    path: &str,
) -> Result<()> {
    let config = observability_config_from_pi_config(config, path)?;
    reload_observability_filters(reload, &config)
}

fn pi_value_to_toml(value: Value, path: &str) -> Result<toml::Value> {
    Ok(match value {
        Value::Bool(value) => toml::Value::Boolean(value),
        Value::Int(value) => toml::Value::Integer(value),
        Value::Float(value) => toml::Value::Float(value),
        Value::Str(value) => toml::Value::String(value),
        Value::Array(values) => toml::Value::Array(
            values
                .into_iter()
                .enumerate()
                .map(|(index, value)| pi_value_to_toml(value, &format!("{path}[{index}]")))
                .collect::<Result<Vec<_>>>()?,
        ),
        Value::Table(values) => toml::Value::Table(
            values
                .into_iter()
                .map(|(key, value)| {
                    let child_path = format!("{path}.{key}");
                    Ok((key, pi_value_to_toml(value, &child_path)?))
                })
                .collect::<Result<toml::map::Map<_, _>>>()?,
        ),
        other => bail!(
            "unsupported pi_config value at `{path}` for observability configuration: {other:?}"
        ),
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::observability::filter::SharedOrderedFilter;
    use crate::observability::reload::FilterReloadHandle;
    use pi_config::ConfigBuilder;
    use std::collections::BTreeMap;

    #[test]
    fn parses_complete_observability_subtree() {
        let mut config = config_with_observability("info", Some("off"));
        let parsed = observability_config_from_pi_config(&mut config, "observability").unwrap();

        assert_eq!(parsed.log.filter.default_level, "info");
        assert_eq!(
            parsed.log.remote.filter.as_ref().unwrap().default_level,
            "off"
        );
    }

    #[test]
    fn missing_subtree_reports_path() {
        let mut config = ConfigBuilder::new().build().unwrap();
        let error = observability_config_from_pi_config(&mut config, "observability").unwrap_err();

        assert!(format!("{error:#}").contains("observability"));
    }

    #[test]
    fn reloads_filters_from_updated_pi_config_subtree() {
        let local = reload_scope("local", "info");
        let remote = reload_scope("remote", "off");
        let trace = reload_scope("trace", "warn");
        let reload = ObservabilityReloadHandle::new(local, Some(remote), Some(trace), false);
        let mut config = config_with_observability("debug", Some("error"));
        let Value::Table(root) = config.sub_value("observability").unwrap() else {
            panic!("observability must be a table");
        };
        let mut root = root;
        let Value::Table(trace) = root.get_mut("trace").unwrap() else {
            panic!("trace must be a table");
        };
        trace.insert("filter".to_string(), filter("trace"));
        let mut config =
            ConfigBuilder::from_base_value(table([("observability", Value::Table(root))]))
                .build()
                .unwrap();

        reload_observability_filters_from_pi_config(&reload, &mut config, "observability").unwrap();

        assert_eq!(
            reload.local().current_filter_config().default_level,
            "debug"
        );
        assert_eq!(
            reload
                .remote()
                .unwrap()
                .current_filter_config()
                .default_level,
            "error"
        );
        assert_eq!(
            reload
                .trace()
                .unwrap()
                .current_filter_config()
                .default_level,
            "trace"
        );
    }

    #[test]
    fn invalid_pi_config_update_keeps_active_filters() {
        let reload =
            ObservabilityReloadHandle::new(reload_scope("local", "info"), None, None, false);
        let mut config = config_with_observability("verbose", None);

        assert!(
            reload_observability_filters_from_pi_config(&reload, &mut config, "observability")
                .is_err()
        );
        assert_eq!(reload.local().current_filter_config().default_level, "info");
    }

    fn reload_scope(scope: &'static str, level: &str) -> FilterReloadHandle {
        FilterReloadHandle::new(
            true,
            scope,
            SharedOrderedFilter::new(crate::observability::LogFilterConfig {
                default_level: level.to_string(),
                overrides: Vec::new(),
            })
            .unwrap(),
        )
    }

    fn config_with_observability(local_level: &str, remote_level: Option<&str>) -> Config {
        let remote_filter = remote_level.map(filter);
        let remote = table([
            ("enabled", Value::Bool(false)),
            ("endpoint", Value::Str(String::new())),
        ]);
        let Value::Table(mut remote) = remote else {
            unreachable!("remote test configuration must be a table");
        };
        if let Some(filter) = remote_filter {
            remote.insert("filter".to_string(), filter);
        }
        let remote = Value::Table(remote);
        let log = table([
            ("filter", filter(local_level)),
            (
                "local",
                table([
                    ("enabled", Value::Bool(false)),
                    ("file_dir", Value::Str("logs".to_string())),
                    ("file_name", Value::Str("app.log".to_string())),
                ]),
            ),
            ("remote", remote),
            ("dynamic", table([("enabled", Value::Bool(true))])),
        ]);
        let trace = table([
            ("enabled", Value::Bool(false)),
            ("exporter", Value::Str("otlp".to_string())),
            ("endpoint", Value::Str(String::new())),
            ("service_name", Value::Str("test".to_string())),
        ]);
        let root = table([("observability", table([("log", log), ("trace", trace)]))]);
        ConfigBuilder::from_base_value(root).build().unwrap()
    }

    fn filter(level: &str) -> Value {
        table([
            ("default_level", Value::Str(level.to_string())),
            ("overrides", Value::Array(Vec::new())),
        ])
    }

    fn table<const N: usize>(entries: [(&str, Value); N]) -> Value {
        Value::Table(
            entries
                .into_iter()
                .map(|(key, value)| (key.to_string(), value))
                .collect::<BTreeMap<_, _>>(),
        )
    }
}