{# The final Go code is autogenerated, but this template is not. Please file bugs! #}
package glean

// 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
import (
{% if transport == "pubsub" %}
    "bytes"
    "compress/gzip"
    "encoding/json"
    "fmt"
    "io"
    "sync"
    "time"

    pubsub "cloud.google.com/go/pubsub/v2"
    "github.com/google/uuid"
{% else %}
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "strconv"
    "time"

    "github.com/google/uuid"
{% endif %}
)

{% if transport == "pubsub" %}
// GleanEventsBuilder constructs Pub/Sub messages carrying Glean pings.
// Builder is stateless beyond its app-identity fields; callers own the
// pubsub.Client, the pubsub.Publisher, batching settings, retries, shutdown sequencing, and any
// publish-result metrics
type GleanEventsBuilder struct {
    AppID             string // Application ID to identify application per Glean standards
    AppDisplayVersion string // Version of application emitting the event
    AppChannel        string // Channel to differentiate logs from prod/beta/staging/devel
}

// gzipPool reuses gzip.Writer instances across calls. gzip.NewWriter()
// allocates ~800 KB internally; pooling + Reset() drops per-message
// allocations to near zero at high publish volumes.
var gzipPool = sync.Pool{
    New: func() interface{} {
        return gzip.NewWriter(io.Discard)
    },
}

// compressPayload gzips data using the package-level writer pool. The
// returned slice is caller-owned; only the *gzip.Writer is returned to the
// pool.
func compressPayload(data []byte) ([]byte, error) {
    var buf bytes.Buffer
    // Optimistic preallocation: gzip on small JSON typically achieves ~2x ratio.
    buf.Grow(len(data) / 2)

    gz := gzipPool.Get().(*gzip.Writer)
    gz.Reset(&buf)

    if _, err := gz.Write(data); err != nil {
        gzipPool.Put(gz)
        return nil, fmt.Errorf("gzip write failed: %w", err)
    }
    if err := gz.Close(); err != nil {
        gzipPool.Put(gz)
        return nil, fmt.Errorf("gzip close failed: %w", err)
    }
    gzipPool.Put(gz)
    return buf.Bytes(), nil
}
{% else %}
// log type string used to identify logs to process in the Moz Data Pipeline
var gleanEventMozlogType string = "glean-server-event"

// A GleanEventsLogger produces output in the required format for Glean to ingest.
// Glean ingestion requires the output to be written to os.Stdout. Writing to a different
// output will require the consumer to handle any closing as appropriate for the Writer.
// e.g. if writing to a file.
type GleanEventsLogger struct {
    AppID             string // Application Id to identify application per Glean standards
    AppDisplayVersion string // Version of application emitting the event
    AppChannel        string // Channel to differentiate logs from prod/beta/staging/devel
    Writer            io.Writer // Writer to output to. Normal operation expects os.Stdout
}
{% endif %}

// exported type for public method parameters
type RequestInfo struct {
    UserAgent string
    IpAddress string
}

// default empty values will be omitted in json from ping struct definition
var defaultRequestInfo = RequestInfo{
    UserAgent: "",
    IpAddress: "",
}

// structs to construct the glean ping
type clientInfo struct {
    TelemetrySDKBuild string `json:"telemetry_sdk_build"`
    FirstRunDate      string `json:"first_run_date"`
    OS                string `json:"os"`
    OSVersion         string `json:"os_version"`
    Architecture      string `json:"architecture"`
    AppBuild          string `json:"app_build"`
    AppDisplayVersion string `json:"app_display_version"`
    AppChannel        string `json:"app_channel"`
}

type pingInfo struct {
    Seq       int    `json:"seq"`
    StartTime string `json:"start_time"`
    EndTime   string `json:"end_time"`
}

{% if transport == "logging" %}
type ping struct {
    DocumentNamespace string `json:"document_namespace"`
    DocumentType      string `json:"document_type"`
    DocumentVersion   string `json:"document_version"`
    DocumentID        string `json:"document_id"`
    UserAgent         string `json:"user_agent,omitempty"`
    IpAddress         string `json:"ip_address,omitempty"`
    Payload           string `json:"payload"`
}
{% endif %}

type metrics map[string]map[string]any

type pingPayload struct {
    ClientInfo clientInfo   `json:"client_info"`
    PingInfo   pingInfo     `json:"ping_info"`
    Metrics    metrics      `json:"metrics"`
    Events     []gleanEvent `json:"events"`
}

type gleanEvent struct {
    Category  string            `json:"category"`
    Name      string            `json:"name"`
    Timestamp int64             `json:"timestamp"`
    Extra     map[string]string `json:"extra"`
}

{% if transport == "logging" %}
type logEnvelope struct {
    Timestamp string
    Logger    string
    Type      string
    Fields    ping
}
{% endif %}

{% if transport == "pubsub" %}
func (g GleanEventsBuilder) createClientInfo() clientInfo {
{% else %}
func (g GleanEventsLogger) createClientInfo() clientInfo {
{% endif %}
    // Fields with default values are required in the Glean schema, but not used in server context
    return clientInfo{
        TelemetrySDKBuild: "glean_parser v{{ parser_version }}",
        FirstRunDate:      "Unknown",
        OS:                "Unknown",
        OSVersion:         "Unknown",
        Architecture:      "Unknown",
        AppBuild:          "Unknown",
        AppDisplayVersion: g.AppDisplayVersion,
        AppChannel:        g.AppChannel,
    }
}

func createPingInfo() pingInfo {
    {# times are ISO-8601 strings, e.g. "2023-12-19T22:09:17.440Z" #}
    now := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
    return pingInfo{
        Seq:       0,
        StartTime: now,
        EndTime:   now,
    }
}

{% if transport == "logging" %}
func (g GleanEventsLogger) createPing(documentType string, config RequestInfo, payload pingPayload) (ping, error) {
    payloadJson, err := json.Marshal(payload)
    if err != nil {
        return ping{}, err
    }

    documentID, err := uuid.NewRandom()
    if err != nil {
        return ping{}, err
    }

    return ping{
        DocumentNamespace: g.AppID,
        DocumentType:      documentType,
        DocumentVersion:   "1",
        DocumentID:        documentID.String(),
        UserAgent:         config.UserAgent,
        IpAddress:         config.IpAddress,
        Payload:           string(payloadJson),
    }, nil
}

// method called by each ping-specific record method.
// construct the ping, wrap it in the envelope, and print to stdout
func (g GleanEventsLogger) record(
    documentType string,
    requestInfo RequestInfo,
    metrics metrics,
    events []gleanEvent,
) error {
    if g.Writer == nil {
        return errors.New("writer not specified")
    }

    telemetryPayload := pingPayload{
        ClientInfo: g.createClientInfo(),
        PingInfo:   createPingInfo(),
        Metrics:    metrics,
        Events:     events,
    }

    ping, err := g.createPing(documentType, requestInfo, telemetryPayload)
    if err != nil {
        return err
    }

    envelope := logEnvelope{
        Timestamp: strconv.FormatInt(time.Now().UnixNano(), 10),
        Logger:    "glean",
        Type:      gleanEventMozlogType,
        Fields:    ping,
    }
    envelopeJson, err := json.Marshal(envelope)
    if err != nil {
        return err
    }

    fmt.Fprintln(g.Writer, string(envelopeJson))
    return nil
}
{% endif %}
{# if any ping has an event metric, create methods and types for them #}
{% if events %}

func newGleanEvent(category, name string, extra map[string]string) gleanEvent {
    return gleanEvent{
        Category:  category,
        Name:      name,
        Timestamp: 0,
        Extra: extra,
    }
}
{# each event has a type and method to create a gleanEvent #}
{% for event in events %}

type {{ event|event_type_name }} struct {
    {% for extra, metadata in event.extra_keys.items() %}
    {{ extra|event_extra_name }} {{ metadata.type|go_metric_type }} // {{ metadata.description|clean_string }}
    {% endfor %}
}

func (e {{ event|event_type_name }}) gleanEvent() gleanEvent {
    return newGleanEvent(
        "{{ event.category }}",
        "{{ event.name }}",
        map[string]string{
        {% for extra, metadata in event.extra_keys.items() %}
            {# convert all extra fields to string for submission #}
            {% if metadata.type == 'boolean' %}
            "{{ extra }}": fmt.Sprintf("%t", e.{{ extra|event_extra_name }}),
            {% elif metadata.type == 'quantity' %}
            "{{ extra }}": fmt.Sprintf("%d", e.{{ extra|event_extra_name }}),
            {% else %}
            "{{ extra }}": e.{{ extra|event_extra_name }},
            {% endif %}
        {% endfor %}
        },
    )
}
{% endfor %}
{% endif %}
{# Generate struct types for labeled_boolean metrics #}
{% if labeled_booleans %}
{% for metric in labeled_booleans %}

// {{ metric.description|clean_string }}
type {{ metric|metric_argument_name }} struct {
    {% for label in metric.ordered_labels %}
    {{ label|event_extra_name }} *bool `json:"{{ label }},omitempty"`
    {% endfor %}
}
{% endfor %}
{% endif %}
{# 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 #}
type {{ ping|ping_events_type_name }} interface {
    is{{ping|ping_events_type_name }}()
    gleanEvent() gleanEvent
}

{# create functions for valid events for this ping #}
{% for metric in metrics_by_type['event'] %}
func (e {{ metric|event_type_name}}) is{{ ping|ping_events_type_name }}() {}
{% endfor %}
{% endif %}

type {{ ping|ping_type_name }} struct {
    {% for metric_type, metrics in metrics_by_type.items() %}
    {% if metric_type != 'event' %}
    {% for metric in metrics %}
    {% if metric_type == 'labeled_boolean' %}
    {{ metric|metric_argument_name }} {{ metric|metric_argument_name }} // {{ metric.description|clean_string }}
    {% else %}
    {{ metric|metric_argument_name }} {{ metric_type|go_metric_type}} // {{ metric.description|clean_string }}
    {% endif %}
    {% endfor %}
    {% endif %}
    {% endfor %}
    {% if metrics_by_type['event'] %}
    Event {{ ping|ping_events_type_name }} // valid event for this ping
    {% endif %}
}

{% if transport == "pubsub" %}
// Build{{ ping|ping_type_name }}Message constructs a Pub/Sub message carrying
// the given `{{ ping }}` ping. The caller publishes the returned message
// (e.g., topic.Publish(ctx, msg)) and owns batching, retries, shutdown
// sequencing, and any publish-result metrics.
//
// Wire-format contract: see
// docs/architecture/decoder_service_specification.md in mozilla/gcp-ingestion.
func (g GleanEventsBuilder) Build{{ ping|ping_type_name }}Message(
    requestInfo RequestInfo,
    params {{ ping|ping_type_name }},
) (*pubsub.Message, error) {
    {% if metrics_by_type['string_list'] %}
    {% for metric in metrics_by_type['string_list'] %}
    // Ensure nil string_list metrics serialize as empty arrays, not null
    if params.{{ metric|metric_argument_name }} == nil {
        params.{{ metric|metric_argument_name }} = []string{}
    }
    {% endfor %}
    {% endif %}
    metrics := metrics{
    {% for metric_type, metrics in metrics_by_type.items() %}
    {% if metric_type != 'event' %}
        "{{ metric_type }}": {
        {% for metric in metrics %}
            {% if metric_type == 'datetime' %}
            "{{ metric|metric_name }}": params.{{ metric|metric_argument_name }}.Format("2006-01-02T15:04:05.000Z"),
            {% else %}
            "{{ metric|metric_name }}": params.{{ metric|metric_argument_name }},
            {% endif %}
        {% endfor %}
        },
    {% endif %}
    {% endfor %}
    }

    events := []gleanEvent{}
    {% if metrics_by_type['event'] %}
    if params.Event != nil {
        events = append(events, params.Event.gleanEvent())
    }
    {% endif %}

    payload := pingPayload{
        ClientInfo: g.createClientInfo(),
        PingInfo:   createPingInfo(),
        Metrics:    metrics,
        Events:     events,
    }

    payloadJSON, err := json.Marshal(payload)
    if err != nil {
        return nil, fmt.Errorf("marshal ping payload: %w", err)
    }
    compressed, err := compressPayload(payloadJSON)
    if err != nil {
        return nil, fmt.Errorf("compress payload: %w", err)
    }

    documentID, err := uuid.NewRandom()
    if err != nil {
        return nil, fmt.Errorf("generate document_id: %w", err)
    }

    attributes := map[string]string{
        "document_namespace": g.AppID,
        "document_type":      "{{ ping }}",
        "document_version":   "1",
        "document_id":        documentID.String(),
    }
    // Skip empty optional attributes; the decoder treats missing and empty
    // the same, and Pub/Sub charges for attribute bytes. The publisher does
    // not set submission_timestamp; the decoder stamps it from publishTime.
    if requestInfo.UserAgent != "" {
        attributes["user_agent"] = requestInfo.UserAgent
    }
    if requestInfo.IpAddress != "" {
        attributes["x_forwarded_for"] = requestInfo.IpAddress
    }

    return &pubsub.Message{
        Data:       compressed,
        Attributes: attributes,
    }, nil
}

// Build{{ ping|ping_type_name }}MessageWithoutUserInfo constructs a Pub/Sub
// message carrying the given `{{ ping }}` ping with no request-derived
// attributes.
func (g GleanEventsBuilder) Build{{ ping|ping_type_name }}MessageWithoutUserInfo(
    params {{ ping|ping_type_name }},
) (*pubsub.Message, error) {
    return g.Build{{ ping|ping_type_name }}Message(defaultRequestInfo, params)
}
{% else %}
// Record and submit `{{ ping }}` ping
func (g GleanEventsLogger) Record{{ ping|ping_type_name }}(
    requestInfo RequestInfo,
    params {{ ping|ping_type_name }},
) error {
    {% if metrics_by_type['string_list'] %}
    {% for metric in metrics_by_type['string_list'] %}
    // Ensure nil string_list metrics serialize as empty arrays, not null
    if params.{{ metric|metric_argument_name }} == nil {
        params.{{ metric|metric_argument_name }} = []string{}
    }
    {% endfor %}
    {% endif %}
    metrics := metrics{
    {% for metric_type, metrics in metrics_by_type.items() %}
    {% if metric_type != 'event' %}
        "{{ metric_type }}": {
        {% for metric in metrics %}
            {% if metric_type == 'datetime' %}
            "{{ metric|metric_name }}": params.{{ metric|metric_argument_name }}.Format("2006-01-02T15:04:05.000Z"),
            {% else %}
            "{{ metric|metric_name }}": params.{{ metric|metric_argument_name }},
            {% endif %}
        {% endfor %}
        },
    {% endif %}
    {% endfor %}
    }

    events := []gleanEvent{}
    {% if metrics_by_type['event'] %}
    if params.Event != nil {
        events = append(events, params.Event.gleanEvent())
    }
    {% endif %}
    return g.record("{{ ping }}", requestInfo, metrics, events)
}

// Record and submit `{{ ping }}` ping omitting user request info
func (g GleanEventsLogger) Record{{ ping|ping_type_name}}WithoutUserInfo(
    params {{ ping|ping_type_name}},
) error {
    return g.Record{{ ping|ping_type_name }}(defaultRequestInfo, params)
}
{% endif %}
{% endfor %}
