use tonic::metadata::{AsciiMetadataValue, MetadataMap};

use super::error::BigTableError;

/// gRPC metadata Resource prefix header
///
/// Generic across Google APIs. This "improves routing by the backend" as
/// described by other clients
const PREFIX_KEY: &str = "google-cloud-resource-prefix";

/// gRPC metadata Client information header
///
/// A `User-Agent` like header, likely its main use is for GCP's metrics
const METRICS_KEY: &str = "x-goog-api-client";

/// gRPC metadata Dynamic Routing header:
/// https://google.aip.dev/client-libraries/4222
///
/// See the googleapis protobuf for which routing header params are used for
/// each Spanner operation (under the `google.api.http` option).
///
/// https://github.com/googleapis/googleapis/blob/master/google/spanner/v1/spanner.proto
const ROUTING_KEY: &str = "x-goog-request-params";

/// gRPC metadata Leader Aware Routing header
///
/// Not well documented. Added to clients in early 2023 defaulting to disabled.
/// Clients have began defaulting it to enabled in late 2023.
///
/// "Enabling leader aware routing would route all requests in RW/PDML
/// transactions to the leader region." as described by other Spanner clients
const LEADER_AWARE_KEY: &str = "x-goog-spanner-route-to-leader";

/// The USER_AGENT string is a static value specified by Google.
/// Its meaning is not to be known to the uninitiated.
const USER_AGENT: &str = "gl-external/1.0 gccl/1.0";

/// Builds the [tonic::metadata::MetadataMap] for all db operations
#[derive(Default)]
pub struct MetadataBuilder<'a> {
    prefix: &'a str,
    routing_params: Vec<(&'a str, &'a str)>,
    route_to_leader: bool,
}

impl<'a> MetadataBuilder<'a> {
    /// Initialize a new builder with a [PREFIX_KEY] header for the given
    /// resource
    pub fn with_prefix(prefix: &'a str) -> Self {
        Self {
            prefix,
            ..Default::default()
        }
    }

    /// Add a [ROUTING_KEY] header
    /// This normally specifies the session name, but unlike spanner, bigtable does not appear to have one of those?
    pub fn routing_param(mut self, key: &'a str, value: &'a str) -> Self {
        self.routing_params.push((key, value));
        self
    }

    /// Toggle the [LEADER_AWARE_KEY] header
    pub fn route_to_leader(mut self, route_to_leader: bool) -> Self {
        self.route_to_leader = route_to_leader;
        self
    }

    /// Build the [MetadataMap]
    pub fn build(self) -> Result<MetadataMap, BigTableError> {
        let mut meta = MetadataMap::new();

        meta.insert(PREFIX_KEY, Self::value(self.prefix)?);
        meta.insert(METRICS_KEY, Self::value(USER_AGENT)?);
        if self.route_to_leader {
            meta.insert(LEADER_AWARE_KEY, Self::value("true")?);
        }
        if !self.routing_params.is_empty() {
            let routing_header = self.routing_header();
            meta.insert(ROUTING_KEY, Self::value(&routing_header)?);
        }
        Ok(meta)
    }

    fn value(val: &str) -> Result<AsciiMetadataValue, BigTableError> {
        AsciiMetadataValue::try_from(val)
            .map_err(|e| BigTableError::Config(format!("Invalid gRPC metadata value: {e}")))
    }

    fn routing_header(self) -> String {
        let mut ser = form_urlencoded::Serializer::new(String::new());
        for (key, val) in self.routing_params {
            ser.append_pair(key, val);
        }
        // python-spanner (python-api-core) doesn't encode '/':
        // https://github.com/googleapis/python-api-core/blob/6251eab/google/api_core/gapic_v1/routing_header.py#L85
        ser.finish().replace("%2F", "/")
    }
}

#[cfg(test)]
mod tests {
    use super::{
        LEADER_AWARE_KEY, METRICS_KEY, MetadataBuilder, PREFIX_KEY, ROUTING_KEY, USER_AGENT,
    };

    // Resource paths should not start with a "/"
    pub const DB: &str = "projects/foo/instances/bar/databases/gorp";
    pub const SESSION: &str = "projects/foo/instances/bar/databases/gorp/sessions/f00B4r_quuX";

    #[test]
    fn metadata_basic() {
        let meta = MetadataBuilder::with_prefix(DB)
            .routing_param("session", SESSION)
            .routing_param("foo", "bar baz")
            .build()
            .unwrap();

        assert_eq!(meta.len(), 3);
        assert_eq!(meta.get(PREFIX_KEY).unwrap().to_str().unwrap(), DB);
        assert_eq!(meta.get(METRICS_KEY).unwrap().to_str().unwrap(), USER_AGENT);
        assert_eq!(
            meta.get(ROUTING_KEY).unwrap().to_str().unwrap(),
            format!("session={SESSION}&foo=bar+baz")
        );
    }

    #[test]
    fn leader_aware() {
        let meta = MetadataBuilder::with_prefix(DB)
            .route_to_leader(true)
            .build()
            .unwrap();

        assert_eq!(meta.len(), 3);
        assert_eq!(
            meta.get(LEADER_AWARE_KEY).unwrap().to_str().unwrap(),
            "true"
        );
    }
}
