// 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/.

using Mozilla.Glean.FFI;
using Mozilla.Glean.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;

namespace Mozilla.Glean.Private
{
    ///<summary>
    ///Deserialized event data.
    ///</summary> 
    public readonly struct RecordedEventData
    {
        /// <summary>
        /// The event's category, part of the full identifier
        /// </summary>
        public string Category { get; }

        /// <summary>
        /// The event's name, part of the full identifier
        /// </summary>
        public string Name { get; }

        /// <summary>
        /// The event's timestamp
        /// </summary>
        public UInt64 Timestamp { get; }

        /// <summary>
        /// Any extra data recorded for the event
        /// </summary>
        public Dictionary<string, string> Extra { get; }

        internal string Identifier { get => String.IsNullOrEmpty(Category) ? Name : $"{Category}.{Name}";  }

        public RecordedEventData(string category, string name, UInt64 timestamp, Dictionary<string, string> extra = null)
        {
            Category = category;
            Name = name;
            Timestamp = timestamp;
            Extra = extra;
        }
    }

    /// <summary>
    /// An enum with no values for convenient use as the default set of extra keys
    /// that an `EventMetricType` can accept.
    /// </summary>
    public enum NoExtraKeys : int
    {
        value
    }

    /// <summary>
    /// This implements the developer facing API for recording events.
    /// 
    /// Instances of this class type are automatically generated by the parsers at built time,
    /// allowing developers to record events that were previously registered in the metrics.yaml file.
    /// </summary>
    public sealed class EventMetricType<ExtraKeysEnum> where ExtraKeysEnum : struct, Enum
    {
        private bool disabled;
        private string[] sendInPings;
        private UInt64 handle;

        /// <summary>
        /// The public constructor used by automatically generated metrics.
        /// </summary>
        public EventMetricType(
            bool disabled,
            string category,
            Lifetime lifetime,
            string name,
            string[] sendInPings,
            string[] allowedExtraKeys = null
            ) : this(0, disabled, sendInPings)
        {
            handle = LibGleanFFI.glean_new_event_metric(
                        category: category,
                        name: name,
                        send_in_pings: sendInPings,
                        send_in_pings_len: sendInPings.Length,
                        lifetime: (int)lifetime,
                        disabled: disabled,
                        allowed_extra_keys: allowedExtraKeys,
                        allowed_extra_keys_len: allowedExtraKeys == null ? 0 : allowedExtraKeys.Length);
        }

        internal EventMetricType(
            UInt64 handle,
            bool disabled,
            string[] sendInPings
            )
        {
            this.disabled = disabled;
            this.sendInPings = sendInPings;
            this.handle = handle;
        }

        /// <summary>
        /// Record an event by using the information provided by the instance of this class.
        /// </summary>
        /// <param name="extra">optional. This is a map, both keys and values need to be strings, keys are
        /// identifiers.This is used for events where additional richer context is needed.
        /// The maximum length for values is 100 bytes.</param>
        public void Record(Dictionary<ExtraKeysEnum, string> extra = null)
        {
            if (disabled)
            {
                return;
            }

            ulong timestamp = HighPrecisionTimestamp.GetTimestamp(TimeUnit.Millisecond);

            Dispatchers.LaunchAPI(() => {
                // The Map is sent over FFI as a pair of arrays, one containing the
                // keys, and the other containing the values.
                Int32[] keys = null;
                string[] values = null;

                Int32 len = 0;
                if (extra != null)
                {
                    // While the `ToArray` functions below could throw `ArgumentNullException`, this would
                    // only happen if `extra` (and `extra.Keys|Values`) is null. Which is not the case, since
                    // we're specifically checking this.
                    // Note that the order of `extra.Keys` and `extra.Values` is unspecified, but guaranteed
                    // to be the same. See
                    // https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.values?view=netstandard-2.0#remarks
                    keys = extra.Keys.Cast<int>().ToArray();
                    values = extra.Values.ToArray();
                    len = extra.Count();
                }

                LibGleanFFI.glean_event_record(
                    handle,
                    timestamp,
                    keys,
                    values,
                    len
                );
            });
        }

        /// <summary>
        /// Tests whether a value is stored for the metric for testing purposes only. This function will
        /// attempt to await the last task (if any) writing to the the metric's storage engine before
        /// returning a value.
        /// </summary>
        /// <param name="pingName">represents the name of the ping to retrieve the metric for Defaults
        /// to the first value in `sendInPings`</param>
        /// <returns>true if metric value exists, otherwise false</returns>
        public bool TestHasValue(string pingName = null)
        {
            Dispatchers.AssertInTestingMode();

            string ping = pingName ?? sendInPings[0];
            return Convert.ToBoolean(LibGleanFFI.glean_event_test_has_value(handle, ping));
        }

        /// <summary>
        /// Returns the stored value for testing purposes only. This function will attempt to await the
        /// last task (if any) writing to the the metric's storage engine before returning a value.
        /// </summary>
        /// <param name="pingName">represents the name of the ping to retrieve the metric for.
        /// Defaults to the first value in `sendInPings`</param>
        /// <returns>value of the stored metric</returns>
        /// <exception cref="System.NullReferenceException">Thrown when the metric contains no value</exception>
        public RecordedEventData[] TestGetValue(string pingName = null)
        {
            Dispatchers.AssertInTestingMode();

            if (!TestHasValue(pingName))
            {
                throw new NullReferenceException();
            }

            string ping = pingName ?? sendInPings[0];

            string jsonValue = LibGleanFFI.glean_event_test_get_value_as_json_string(handle, ping).AsString();

            List<RecordedEventData> parsedEvents = new List<RecordedEventData>();
            try
            {
                JsonDocument data = JsonDocument.Parse(jsonValue);
                JsonElement root = data.RootElement;

                foreach (var entry in root.EnumerateArray())
                {
                    // Parse the 'extra' properties, if available.
                    Dictionary<string, string> parsedExtra = null;
                    JsonElement jsonExtra;
                    if (entry.TryGetProperty("extra", out jsonExtra))
                    {
                        parsedExtra = new Dictionary<string, string>();
                        foreach (var extraEntry in jsonExtra.EnumerateObject())
                        {
                            parsedExtra.Add(extraEntry.Name, extraEntry.Value.GetString());
                        }
                    }

                    parsedEvents.Add(new RecordedEventData(
                        category: entry.GetProperty("category").GetString(),
                        name: entry.GetProperty("name").GetString(),
                        timestamp: entry.GetProperty("timestamp").GetUInt64(),
                        extra: parsedExtra
                    ));
                }
            }
            catch (Exception)
            {
                // We're interested in catching anything that could have gone wrong
                // in the try block, and throw.
                throw new NullReferenceException();
            }

            return parsedEvents.ToArray();
        }

        /// <summary>
        /// Returns the number of errors recorded for the given metric.
        /// </summary>
        /// <param name="errorType">the type of the error recorded.</param>
        /// <param name="pingName">represents the name of the ping to retrieve the metric for.
        /// Defaults to the first value in `sendInPings`.</param>
        /// <returns>the number of errors recorded for the metric.</returns>
        public Int32 TestGetNumRecordedErrors(Testing.ErrorType errorType, string pingName = null)
        {
            Dispatchers.AssertInTestingMode();

            string ping = pingName ?? sendInPings[0];
            return LibGleanFFI.glean_event_test_get_num_recorded_errors(
                handle, (int)errorType, ping
            );
        }
    }
}
