{# The final Rust code is autogenerated, but this template is not. Please file bugs if there is a problem! #}
//! This Server Events crate encapsulates the core functionality related to
//! emitting Glean server metrics.

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// AUTOGENERATED BY glean_parser v{{ parser_version }}. DO NOT EDIT.

// Required imports
use chrono::Utc;
use serde::{Deserialize, Serialize};
use serde_json;
use std::collections::HashMap;
use uuid::Uuid;

// log type string used to identify logs to process in the Moz Data Pipeline
const GLEAN_EVENT_MOZLOG_TYPE: &str = "glean-server-event";

// Code below is static, regardless of what is defined in `metrics.yaml`:

/// The GleanEventsLogger produces output in the required format for Glean to ingest.
/// Glean ingestion requires the output to be written to stdout. Writing to a different
/// output will require the consumer to handle any closing as appropriate for the Writer.
pub struct GleanEventsLogger {
    /// Application Id to identify application per Glean standards
    pub app_id: String,
    /// Version of application emitting the event
    pub app_display_version: String,
    /// Channel to differentiate logs from prod/beta/staging/devel
    pub app_channel: String,
}

/// Struct containing request metadata. Record calls can be made with this being left empty.
/// Default impl empty values will be omitted in json from ping struct definition.
#[derive(Default, Serialize, Deserialize)]
pub struct RequestInfo {
    pub user_agent: String,
    pub ip_address: String,
}

/// Struct encapsulating client application data to construct Glean ping.
#[derive(Serialize, Deserialize, Debug)]
pub struct ClientInfo {
    telemetry_sdk_build: String,
    first_run_date: String,
    os: String,
    os_version: String,
    architecture: String,
    app_build: String,
    app_display_version: String,
    app_channel: String,
}

/// Ping metadata struct.
#[derive(Serialize, Deserialize, Debug)]
pub struct PingInfo {
    seq: u32,
    start_time: String,
    end_time: String,
}

impl Default for PingInfo {
    fn default() -> Self {
        // times are ISO-8601 strings, e.g. "2023-12-19T22:09:17.440Z"
        let now = Utc::now().to_rfc3339();
        PingInfo {
            seq: 0,
            start_time: now.clone(),
            end_time: now,
        }
    }
}

/// Struct containing ping metadata.
#[derive(Serialize, Deserialize, Debug)]
pub struct Ping {
    document_namespace: String,
    document_type: String,
    document_version: String,
    document_id: String,
    user_agent: Option<String>,
    ip_address: Option<String>,
    payload: String,
}

/// Glean Metrics type expressed by a String key of the supported metric types
/// ("string", "quantity", "event", "datetime", "boolean") and a HashMap
/// of each metric (defined in `metrics.yaml`) corresponding to its
/// serialized value.
type Metrics = HashMap<String, HashMap<String, serde_json::Value>>;

/// Struct defining the `Event` metric type.
#[derive(Debug, Serialize, Deserialize)]
pub struct GleanEvent {
    category: String,
    name: String,
    timestamp: i64,
    extra: HashMap<String, String>,
}

pub fn new_glean_event(
    category: &str,
    name: &str,
    extra: HashMap<String, String>,
) -> GleanEvent {
    GleanEvent {
        category: category.to_owned(),
        name: name.to_owned(),
        timestamp: 0,
        extra,
    }
}

/// Struct encapsulating the telemetry payload, including the metrics and events,
/// in addition to client and ping metadata.
#[derive(Serialize, Deserialize, Debug)]
struct PingPayload {
    client_info: ClientInfo,
    ping_info: PingInfo,
    metrics: Metrics,
    events: Vec<GleanEvent>,
}

/// Logging envelope that is serialized for emission to stdout.
#[derive(Serialize, Deserialize)]
struct LogEnvelope {
    // MozLog compliant format. https://wiki.mozilla.org/Firefox/Services/Logging
    #[serde(rename = "Type")]
    log_type: String,
    #[serde(rename = "Fields")]
    fields: Ping,
}

impl GleanEventsLogger {
    /// Create ClientInfo struct from values defined in GleanEventsLogger.
    fn create_client_info(&self) -> ClientInfo {
        // Fields with default values are required in the Glean schema, but not used in server context.
        ClientInfo {
            telemetry_sdk_build: "glean_parser v15.0.2.dev17+g81fec69a".to_owned(),
            first_run_date: "Unknown".to_owned(),
            os: "Unknown".to_owned(),
            os_version: "Unknown".to_owned(),
            architecture: "Unknown".to_owned(),
            app_build: "Unknown".to_owned(),
            app_display_version: self.app_display_version.clone(),
            app_channel: self.app_channel.clone(),
        }
    }

    /// Method used to encapsulate ping metadata and PingPayload.
    fn create_ping(
        &self,
        document_type: &str,
        config: &RequestInfo,
        payload: &PingPayload,
    ) -> Ping {
        let payload_json =
            serde_json::to_string(payload).expect("unable to marshal payload to json.");
        let document_id = Uuid::new_v4().to_string();
        Ping {
            document_namespace: self.app_id.clone(),
            document_type: document_type.to_owned(),
            document_version: "1".to_owned(),
            document_id,
            user_agent: Some(config.user_agent.clone()),
            ip_address: Some(config.ip_address.clone()),
            payload: payload_json,
        }
    }

    /// Method called by each ping-specific record method.
    /// The goal is to construct the ping, wrap it in the envelope and print to stdout.
    fn record(
        &self,
        document_type: &str,
        request_info: &RequestInfo,
        metrics: Metrics,
        events: Vec<GleanEvent>,
    ) {
        let telemetry_payload: PingPayload = PingPayload {
            client_info: self.create_client_info(),
            ping_info: PingInfo::default(),
            metrics,
            events,
        };

        let ping: Ping = self.create_ping(document_type, request_info, &telemetry_payload);

        let envelope: LogEnvelope = LogEnvelope {
            log_type: GLEAN_EVENT_MOZLOG_TYPE.to_owned(),
            fields: ping,
        };
        let envelope_json =
            serde_json::to_string(&envelope).expect("unable to marshal payload to json.");
        println!("{}", envelope_json);
    }
}

// Code below is generated based on the provided `metrics.yaml` file:

// Metrics of the `event` type. Anything defined in `extra_keys` has it's own struct field.
// The appended `Event` term to any metric of the event type implies the ping event.
{# if any ping has an event metric, create methods and types for them #}
{% for ping, metrics_by_type in pings.items() %}
{% if metrics_by_type['event'] %}
{# Loop over each event associated with the current ping #}
{# each event has a specific type and method to create a gleanEvent #}
{% for event in metrics_by_type['event'] %}
/// Struct containing metadata defined in `extra_keys` if they are defined. Otherwise empty.
pub struct {{ event|event_type_name }} {
    // Metadata for event in `extra_keys`
    {% for extra, metadata in event.extra_keys.items() %}
    {% if extra|event_extra_name == "event" %}
    /// {{ metadata.description|clean_string }}
    pub {{ extra|event_extra_name|snake_case }}: Box<dyn {{ metadata.type|rust_metric_type }}>,
    {% else %}
    /// {{ metadata.description|clean_string }}
    pub {{ extra|event_extra_name|snake_case }}: {{ metadata.type|rust_metric_type }},
    {% endif %}
    {% endfor %}
}

// Implementing the {{ ping|ping_events_type_name }} trait for the generated struct {{ event|event_type_name }}
impl {{ ping|ping_events_type_name }} for {{ event|event_type_name }} {
    /// Create a GleanEvent for the above-defined Event struct ({{ event|event_type_name }}).
    /// Any metadata `extra` values are passed into the extra HashMap.
    fn glean_event(&self) -> GleanEvent {
        // Any `extra_keys` will be output below to be inserted into `extra`.
        // If there are none, an empty, immutable HashMap is created.
        {% if event.extra_keys.items()|length == 0 %}
        let extra: HashMap<String, String> = HashMap::new();
        {% else %}
        let mut extra: HashMap<String, String> = HashMap::new();
        {% endif %}

        {% for extra, metadata in event.extra_keys.items() %}
        {# convert all extra fields to string for submission #}
        {% if metadata.type == 'boolean' %}
        extra.insert("{{ extra }}".to_owned(), self.{{ extra|event_extra_name|snake_case }}.to_string());
        {% elif metadata.type == 'quantity' %}
        extra.insert("{{ extra }}".to_owned(), self.{{ extra|event_extra_name|snake_case }}.to_string());
        {% else %}
        extra.insert("{{ extra }}".to_owned(), self.{{ extra|event_extra_name|snake_case }}.to_string());
        {% endif %}
        {% endfor %}

        new_glean_event(
            "{{ event.category }}",
            "{{ event.name }}",
            extra
        )
    }
}

{% endfor %}
{% endif %}
{% endfor %}
{# struct & methods for submitting pings #}
{% for ping, metrics_by_type in pings.items() %}
{% if metrics_by_type['event'] %}

{# interface that eligible events for this ping will implement #}
/// Marker trait for events per ping.
pub trait {{ ping|ping_events_type_name }} {
    fn glean_event(&self) -> GleanEvent;
}
{% endif %}

/// Struct containing defined metrics and event(s) from `metrics.yaml`.
/// Encompasses the core Glean Ping Event and its data.
pub struct {{ ping|ping_type_name }} {
    {% for metric_type, metrics in metrics_by_type.items() %}
    {% if metric_type != 'event' %}
    {% for metric in metrics %}
    /// {{ metric.description|clean_string }}
    pub {{ metric|metric_argument_name|snake_case }}: {{ metric_type|rust_metric_type}},
    {% endfor %}
    {% endif %}
    {% endfor %}
    {% if metrics_by_type['event'] %}
    /// valid event of  `{{ ping|ping_events_type_name }}` for this ping
    pub event: Option<Box<dyn {{ ping|ping_events_type_name }}>>,
    {% endif %}
}

// Record and submit `{{ ping }}` ping
impl GleanEventsLogger {
    /// General `record_events_ping` function for core Glean Ping Event - Record and submit `events` ping.
    /// Collects a HashMap of parametrized key value pairs and events to be recorded.
    pub fn record_{{ ping|ping_type_name|snake_case }}(&self, request_info: &RequestInfo, params: &{{ ping|ping_type_name}}) {
        // Define the outer `Metrics` map that holds the metric type.
        let mut metrics = Metrics::new();
        // Create corresponding metric value maps to insert into `Metrics`.
        {% for metric_type, metrics in metrics_by_type.items() %}
        {% if metric_type != 'event' %}
        let mut {{metric_type}}_map: HashMap<String, serde_json::Value> = HashMap::new();
        {% for metric in metrics %}
        {{metric_type}}_map.insert(
            {% if metric_type == 'datetime' %}
		    "{{ metric|metric_name }}".to_owned(),
            serde_json::Value::String(params.{{ metric|metric_argument_name|snake_case }}.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()),
            {% elif metric_type == 'quantity' %}
            "{{ metric|metric_name }}".to_owned(),
            serde_json::Value::Number(params.{{ metric|metric_argument_name|snake_case }}.into()),
            {% elif metric_type == 'boolean' %}
            "{{ metric|metric_name }}".to_owned(),
            serde_json::Value::Bool(params.{{ metric|metric_argument_name|snake_case }}.into()),
            {% else %}
            "{{ metric|metric_name }}".to_owned(),
            serde_json::Value::String(params.{{ metric|metric_argument_name|snake_case }}.to_string()),
            {% endif %}
        );
        {% endfor %}
        metrics.insert("{{ metric_type }}".to_owned(), {{metric_type}}_map);

        {% endif %}
        {% endfor %}

        {% if metrics_by_type['event'] %}
        let mut events: Vec<GleanEvent> = Vec::new();
        if let Some(event) = &params.event {
            events.push(event.glean_event());
        }

        {% else %}
        let events: Vec<GleanEvent> = Vec::new();
        {% endif %}
        self.record("{{ ping }}", request_info, metrics, events);
    }
}

impl GleanEventsLogger {
    /// Record and submit `events` ping while omitting user request info.
    pub fn record_{{ ping|ping_type_name|snake_case }}_without_user_info(
    &self,
    params: &{{ ping|ping_type_name}}
    ) {
        self.record_{{ ping|ping_type_name|snake_case }}(&RequestInfo::default(), params)
    }
}
{% endfor %}
