/// Stable low-cardinality fields that can be attached to request logs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogFields {
    service: String,
    transport: Option<String>,
    route: Option<String>,
    method: Option<String>,
    request_id: Option<String>,
    trace_id: Option<String>,
    span_id: Option<String>,
    status: Option<String>,
}

impl LogFields {
    /// Creates fields for a logical service.
    pub fn new(service: impl Into<String>) -> Self {
        Self {
            service: service.into(),
            transport: None,
            route: None,
            method: None,
            request_id: None,
            trace_id: None,
            span_id: None,
            status: None,
        }
    }

    /// Sets the transport label.
    pub fn with_transport(mut self, value: impl Into<String>) -> Self {
        self.transport = Some(value.into());
        self
    }

    /// Sets the route or RPC method pattern.
    pub fn with_route(mut self, value: impl Into<String>) -> Self {
        self.route = Some(value.into());
        self
    }

    /// Sets the operation method.
    pub fn with_method(mut self, value: impl Into<String>) -> Self {
        self.method = Some(value.into());
        self
    }

    /// Sets the request id.
    pub fn with_request_id(mut self, value: impl Into<String>) -> Self {
        self.request_id = Some(value.into());
        self
    }

    /// Sets the trace id.
    pub fn with_trace_id(mut self, value: impl Into<String>) -> Self {
        self.trace_id = Some(value.into());
        self
    }

    /// Sets the span id.
    pub fn with_span_id(mut self, value: impl Into<String>) -> Self {
        self.span_id = Some(value.into());
        self
    }

    /// Sets the status or code label.
    pub fn with_status(mut self, value: impl Into<String>) -> Self {
        self.status = Some(value.into());
        self
    }

    /// Returns all populated fields as key/value pairs.
    pub fn as_pairs(&self) -> Vec<(String, String)> {
        let mut pairs = vec![("service".to_string(), self.service.clone())];
        push_optional(&mut pairs, "transport", &self.transport);
        push_optional(&mut pairs, "route", &self.route);
        push_optional(&mut pairs, "method", &self.method);
        push_optional(&mut pairs, "request_id", &self.request_id);
        push_optional(&mut pairs, "trace_id", &self.trace_id);
        push_optional(&mut pairs, "span_id", &self.span_id);
        push_optional(&mut pairs, "status", &self.status);
        pairs
    }
}

fn push_optional(pairs: &mut Vec<(String, String)>, key: &str, value: &Option<String>) {
    if let Some(value) = value
        && !value.is_empty()
    {
        pairs.push((key.to_string(), value.clone()));
    }
}
