use serde::{Deserialize, Serialize};
use serde_json::Value;

/// Observability module configuration.
///
/// This is the top-level object consumed by `init_observability`. It configures
/// local logging, optional remote OpenTelemetry logs, and optional tracing
/// export. Log filtering is shared by all log outputs.
#[derive(Debug, Clone, Deserialize)]
pub struct ObservabilityConfig {
    /// Log output and shared filtering configuration.
    pub log: LogConfig,
    /// OpenTelemetry trace export configuration.
    pub trace: TraceConfig,
    /// OpenTelemetry metrics export configuration.
    ///
    /// Missing value disables metrics export.
    pub metrics: Option<MetricsConfig>,
}

/// Log subsystem configuration.
///
/// `filter` is intentionally shared by all outputs. Use `console`, `local`
/// and `remote` only to decide where already accepted events are written.
#[derive(Debug, Clone, Deserialize)]
pub struct LogConfig {
    /// Console and local file format. Missing value defaults to `json`.
    pub format: Option<LogFormat>,
    /// Shared ordered filter configuration.
    pub filter: LogFilterConfig,
    /// Console output configuration.
    pub console: Option<LogConsoleConfig>,
    /// Local rolling file output configuration.
    pub local: LogLocalConfig,
    /// Remote OpenTelemetry log output configuration.
    pub remote: LogRemoteConfig,
    /// Runtime reload configuration.
    pub dynamic: DynamicLogConfig,
}

/// Console and local file log format.
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum LogFormat {
    /// Structured JSON output.
    Json,
    /// Compact text output.
    Compact,
    /// Full text output.
    Full,
    /// Pretty multi-line text output.
    Pretty,
}

impl Default for LogFormat {
    fn default() -> Self {
        Self::Json
    }
}

/// Console output configuration.
#[derive(Debug, Clone, Deserialize)]
pub struct LogConsoleConfig {
    /// Enables stdout output. Missing value defaults to false.
    pub enabled: Option<bool>,
}

/// Shared log filter configuration.
///
/// Filtering uses a default level plus optional override rules. Events that do
/// not match any override are filtered by `default_level`. Events that match
/// one or more overrides use the highest-priority override, including that
/// override's level and optional field rules.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LogFilterConfig {
    /// Default minimum level for modules that do not match any override.
    ///
    /// Supported values are `trace`, `debug`, `info`, `warn`, `error`, and
    /// `off`. For example, `info` accepts `info`, `warn`, and `error` events.
    pub default_level: String,
    /// Target/span override rules.
    ///
    /// Overrides are useful for temporarily changing a group of modules to
    /// `debug` or `trace`, or for making noisy modules stricter with `warn` or
    /// `error`. When multiple overrides match, the one with the highest
    /// `priority` wins.
    #[serde(default)]
    pub overrides: Vec<LogFilterOverrideConfig>,
}

/// Target/span-specific log filter override.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LogFilterOverrideConfig {
    /// Human-readable rule name used for configuration diagnostics.
    ///
    /// Names must be unique within one `LogFilterConfig`. Runtime reload APIs
    /// can use this value to enable or disable a specific override.
    pub name: String,
    /// Enables this override. Missing value defaults to true.
    pub enabled: Option<bool>,
    /// Minimum level used when this override matches.
    ///
    /// Supported values are `trace`, `debug`, `info`, `warn`, `error`, and
    /// `off`. This value replaces `default_level` for matching events.
    pub level: String,
    /// Rule priority. Higher values win when multiple overrides match.
    #[serde(default)]
    pub priority: i64,
    /// Target/span fuzzy matchers that select the modules or call chains this
    /// override applies to. An empty list means the override can match all
    /// targets, so use that form only for intentional global overrides.
    #[serde(default)]
    pub fuzzy_rules: Vec<FuzzyRuleConfig>,
    /// Event field rules applied after this override matches.
    ///
    /// Multiple field rules use AND semantics. These rules do not affect
    /// modules that do not match this override.
    #[serde(default)]
    pub field_rules: Vec<FieldRuleConfig>,
}

/// Local rolling file output configuration.
#[derive(Debug, Clone, Deserialize)]
pub struct LogLocalConfig {
    /// Enables local file output. Missing value defaults to true.
    pub enabled: Option<bool>,
    /// Directory where rolling log files are written.
    pub file_dir: String,
    /// Rolling log file name.
    pub file_name: String,
}

/// Remote OpenTelemetry log output configuration.
#[derive(Debug, Clone, Deserialize)]
pub struct LogRemoteConfig {
    /// Enables remote OpenTelemetry logs. Missing value defaults to false.
    pub enabled: Option<bool>,
    /// OTLP transport. Missing value defaults to `http_binary`.
    #[serde(default)]
    pub protocol: OtlpProtocol,
    /// Collector endpoint.
    ///
    /// HTTP transport appends `/v1/logs` when not already present. gRPC uses
    /// the endpoint unchanged, normally with Collector port `4317`.
    pub endpoint: String,
    /// Optional remote-only ordered filter configuration.
    ///
    /// When this value is present, remote OpenTelemetry logs use this filter
    /// instead of the shared `[log.filter]` used by console and local file
    /// outputs. When omitted, remote logs keep the previous behavior and reuse
    /// the shared filter.
    pub filter: Option<LogFilterConfig>,
}

/// Dynamic reload configuration.
#[derive(Debug, Clone, Deserialize)]
pub struct DynamicLogConfig {
    /// Enables runtime reload methods on the returned handle.
    pub enabled: bool,
}

/// OpenTelemetry trace export configuration.
#[derive(Debug, Clone, Deserialize)]
pub struct TraceConfig {
    /// Enables trace export.
    pub enabled: bool,
    /// Exporter kind. Only `otlp` is currently implemented.
    pub exporter: String,
    /// OTLP transport. Missing value defaults to `http_binary`.
    #[serde(default)]
    pub protocol: OtlpProtocol,
    /// Collector endpoint.
    ///
    /// HTTP transport appends `/v1/traces` when not already present. gRPC uses
    /// the endpoint unchanged, normally with Collector port `4317`.
    pub endpoint: String,
    /// OpenTelemetry `service.name`.
    pub service_name: String,
    /// Optional OpenTelemetry `service.version`.
    pub service_version: Option<String>,
    /// Optional trace-only ordered filter configuration.
    ///
    /// When omitted, traces use `trace` as the default level so existing
    /// configurations continue exporting all spans and trace events.
    pub filter: Option<LogFilterConfig>,
}

/// OpenTelemetry metrics export configuration.
#[derive(Debug, Clone, Deserialize)]
pub struct MetricsConfig {
    /// Enables metrics export.
    pub enabled: bool,
    /// Exporter kind. Only `otlp` is currently implemented.
    pub exporter: String,
    /// OTLP transport. Missing value defaults to `http_binary`.
    #[serde(default)]
    pub protocol: OtlpProtocol,
    /// Collector endpoint.
    ///
    /// HTTP transport appends `/v1/metrics` when not already present. gRPC uses
    /// the endpoint unchanged, normally with Collector port `4317`.
    pub endpoint: String,
    /// OpenTelemetry `service.name`.
    pub service_name: String,
    /// Optional OpenTelemetry `service.version`.
    pub service_version: Option<String>,
}

/// Transport used by an OTLP exporter.
#[derive(Debug, Clone, Copy, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum OtlpProtocol {
    /// OTLP Protobuf over HTTP, normally using Collector port `4318`.
    #[default]
    HttpBinary,
    /// OTLP over gRPC, normally using Collector port `4317`.
    Grpc,
}

/// Fuzzy rule target.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FuzzyRuleKind {
    /// Match against `metadata.target()`.
    Target,
    /// Match against active span names.
    Span,
}

/// Fuzzy match operator.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FuzzyMatchType {
    /// Exact string equality.
    Exact,
    /// Prefix match.
    Prefix,
    /// Substring match.
    Contains,
    /// `*` and `?` glob match.
    Glob,
    /// Regular expression match.
    Regex,
}

/// Deserializable fuzzy filter rule.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FuzzyRuleConfig {
    /// Target/span selector.
    pub kind: FuzzyRuleKind,
    /// Match operation.
    pub match_type: FuzzyMatchType,
    /// Pattern used by the match operation.
    pub pattern: String,
    /// Optional level marker retained for diagnostics and future extensions.
    pub level: Option<String>,
}

/// Event field filter operation.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FieldRuleOp {
    /// Equal.
    Eq,
    /// Not equal.
    Ne,
    /// String contains.
    Contains,
    /// String starts with.
    StartsWith,
    /// String ends with.
    EndsWith,
    /// String regular expression.
    Regex,
    /// Value is in array.
    In,
    /// Field exists.
    Exists,
    /// Field does not exist.
    NotExists,
    /// Greater than.
    Gt,
    /// Greater than or equal.
    Gte,
    /// Less than.
    Lt,
    /// Less than or equal.
    Lte,
}

/// Deserializable event field rule.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FieldRuleConfig {
    /// Event field name.
    pub field: String,
    /// Operation to apply to the field.
    pub op: FieldRuleOp,
    /// Comparison value. Not required for `exists` and `not_exists`.
    #[serde(default)]
    pub value: Value,
}

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

    #[test]
    fn toml_config_parses_filter_overrides_and_remote_filter() {
        let config: ObservabilityConfig = toml::from_str(
            r#"
            [log]
            format = "json"

            [log.filter]
            default_level = "info"

            [[log.filter.overrides]]
            name = "decoder_debug"
            enabled = true
            level = "debug"
            priority = 100

            [[log.filter.overrides.fuzzy_rules]]
            kind = "target"
            match_type = "contains"
            pattern = "decoder"

            [[log.filter.overrides.field_rules]]
            field = "id"
            op = "eq"
            value = 100

            [log.console]
            enabled = true

            [log.local]
            enabled = false
            file_dir = "logs"
            file_name = "app.log"

            [log.remote]
            enabled = true
            protocol = "grpc"
            endpoint = "http://127.0.0.1:4318/v1/logs"

            [log.remote.filter]
            default_level = "off"

            [[log.remote.filter.overrides]]
            name = "remote_id_100"
            enabled = true
            level = "info"
            priority = 200

            [[log.remote.filter.overrides.fuzzy_rules]]
            kind = "target"
            match_type = "exact"
            pattern = "app::remote"

            [[log.remote.filter.overrides.field_rules]]
            field = "id"
            op = "eq"
            value = 100

            [log.dynamic]
            enabled = true

            [trace]
            enabled = true
            exporter = "otlp"
            endpoint = "http://127.0.0.1:4318/v1/traces"
            service_name = "pi_logger_test"
            service_version = "1.0.0"

            [trace.filter]
            default_level = "off"

            [[trace.filter.overrides]]
            name = "trace_test_on"
            enabled = true
            level = "info"
            priority = 100

            [[trace.filter.overrides.fuzzy_rules]]
            kind = "target"
            match_type = "contains"
            pattern = "trace_test"

            [metrics]
            enabled = true
            exporter = "otlp"
            protocol = "grpc"
            endpoint = "http://127.0.0.1:4318/v1/metrics"
            service_name = "pi_logger_test"
            service_version = "1.0.0"
            "#,
        )
        .unwrap();

        assert_eq!(config.log.format, Some(LogFormat::Json));
        assert_eq!(config.log.filter.default_level, "info");
        assert_eq!(config.log.filter.overrides.len(), 1);
        assert_eq!(config.log.filter.overrides[0].name, "decoder_debug");
        assert_eq!(config.log.filter.overrides[0].enabled, Some(true));
        assert_eq!(
            config.log.filter.overrides[0].fuzzy_rules[0].kind,
            FuzzyRuleKind::Target
        );
        assert_eq!(
            config.log.filter.overrides[0].fuzzy_rules[0].match_type,
            FuzzyMatchType::Contains
        );
        assert_eq!(
            config.log.filter.overrides[0].field_rules[0].op,
            FieldRuleOp::Eq
        );

        let remote_filter = config.log.remote.filter.as_ref().unwrap();
        assert_eq!(remote_filter.default_level, "off");
        assert_eq!(remote_filter.overrides[0].name, "remote_id_100");
        assert_eq!(remote_filter.overrides[0].priority, 200);

        assert_eq!(config.log.console.unwrap().enabled, Some(true));
        assert_eq!(config.log.local.enabled, Some(false));
        assert_eq!(config.log.remote.enabled, Some(true));
        assert_eq!(config.log.remote.protocol, OtlpProtocol::Grpc);
        assert!(config.log.dynamic.enabled);
        assert!(config.trace.enabled);
        assert_eq!(config.trace.protocol, OtlpProtocol::HttpBinary);
        assert_eq!(config.trace.service_name, "pi_logger_test");
        assert_eq!(config.trace.service_version.as_deref(), Some("1.0.0"));
        assert_eq!(
            config.trace.filter.as_ref().unwrap().overrides[0].name,
            "trace_test_on"
        );
        let metrics = config.metrics.as_ref().unwrap();
        assert!(metrics.enabled);
        assert_eq!(metrics.exporter, "otlp");
        assert_eq!(metrics.protocol, OtlpProtocol::Grpc);
        assert_eq!(metrics.service_name, "pi_logger_test");
        assert_eq!(metrics.service_version.as_deref(), Some("1.0.0"));
    }

    #[test]
    fn toml_override_defaults_empty_rule_lists_and_priority() {
        let config: LogFilterConfig = toml::from_str(
            r#"
            default_level = "warn"

            [[overrides]]
            name = "global_info"
            level = "info"
            "#,
        )
        .unwrap();

        assert_eq!(config.default_level, "warn");
        assert_eq!(config.overrides.len(), 1);
        assert_eq!(config.overrides[0].enabled, None);
        assert_eq!(config.overrides[0].priority, 0);
        assert!(config.overrides[0].fuzzy_rules.is_empty());
        assert!(config.overrides[0].field_rules.is_empty());
    }
}
