// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Runtime.InteropServices;
using System.Threading;

using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Metrics;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.Extensibility.Implementation;

namespace Microsoft.PowerShell.Telemetry
{
    /// <summary>
    /// The category of telemetry.
    /// </summary>
    internal enum TelemetryType
    {
        /// <summary>
        /// Telemetry of the application type (cmdlet, script, etc).
        /// </summary>
        ApplicationType,

        /// <summary>
        /// Send telemetry when we load a module, only module names in the s_knownModules list
        /// will be reported, otherwise it will be "anonymous".
        /// </summary>
        ModuleLoad,

        /// <summary>
        /// Send telemetry when we load a module using Windows compatibility feature, only module names in the s_knownModules list
        /// will be reported, otherwise it will be "anonymous".
        /// </summary>
        WinCompatModuleLoad,

        /// <summary>
        /// Send telemetry for experimental module feature deactivation.
        /// All experimental engine features will be have telemetry.
        /// </summary>
        ExperimentalEngineFeatureDeactivation,

        /// <summary>
        /// Send telemetry for experimental module feature activation.
        /// All experimental engine features will be have telemetry.
        /// </summary>
        ExperimentalEngineFeatureActivation,

        /// <summary>
        /// Send telemetry for an experimental feature when use.
        /// </summary>
        ExperimentalFeatureUse,

        /// <summary>
        /// Send telemetry for experimental module feature deactivation.
        /// Experimental module features will send telemetry based on the module it is in.
        /// If we send telemetry for the module, we will also do so for any experimental feature
        /// in that module.
        /// </summary>
        ExperimentalModuleFeatureDeactivation,

        /// <summary>
        /// Send telemetry for experimental module feature activation.
        /// Experimental module features will send telemetry based on the module it is in.
        /// If we send telemetry for the module, we will also do so for any experimental feature
        /// in that module.
        /// </summary>
        ExperimentalModuleFeatureActivation,

        /// <summary>
        /// Send telemetry for each PowerShell.Create API.
        /// </summary>
        PowerShellCreate,

        /// <summary>
        /// Remote session creation.
        /// </summary>
        RemoteSessionOpen,

        /// <summary>
        /// Send telemetry for a stable feature when used.
        /// By making a distinction between this and experimental feature use, it will make
        /// queries much easier.
        /// </summary>
        FeatureUse,
    }

    /// <summary>
    /// Set up the telemetry initializer to mask the platform specific names.
    /// </summary>
    internal class NameObscurerTelemetryInitializer : ITelemetryInitializer
    {
        // Report the platform name information as "na".
        private const string _notavailable = "na";

        /// <summary>
        /// Initialize properties we are obscuring to "na".
        /// </summary>
        /// <param name="telemetry">The instance of our telemetry.</param>
        public void Initialize(ITelemetry telemetry)
        {
            telemetry.Context.Cloud.RoleName = _notavailable;
            telemetry.Context.GetInternalContext().NodeName = _notavailable;
            telemetry.Context.Cloud.RoleInstance = _notavailable;
        }
    }

    /// <summary>
    /// Send up telemetry for startup.
    /// </summary>
    public static class ApplicationInsightsTelemetry
    {
        // The string for SubsystermRegistration
        internal const string s_subsystemRegistration = "Subsystem.Registration";

        // If this env var is true, yes, or 1, telemetry will NOT be sent.
        private const string _telemetryOptoutEnvVar = "POWERSHELL_TELEMETRY_OPTOUT";

        // PSCoreInsight2 telemetry key
        // private const string _psCoreTelemetryKey = "ee4b2115-d347-47b0-adb6-b19c2c763808"; // Production
        private const string _psCoreTelemetryKey = "d26a5ef4-d608-452c-a6b8-a4a55935f70d"; // V7 Preview 3

        // In the event there is a problem in creating the node identifier file, use the default identifier.
        // This can happen if we are running in a system which has a read-only filesystem.
        private static readonly Guid _defaultNodeIdentifier = new Guid("2f998828-3f4a-4741-bf50-d11c6be42f50");

        // Use "anonymous" as the string to return when you can't report a name
        private const string Anonymous = "anonymous";

        // Use '0.0' as the string for an anonymous module version
        private const string AnonymousVersion = "0.0";

        // Use 'n/a' as the string when there's no tag to report
        private const string NoTag = "n/a";

        // the telemetry failure string
        private const string _telemetryFailure = "TELEMETRY_FAILURE";

        // Telemetry client to be reused when we start sending more telemetry
        private static TelemetryClient s_telemetryClient { get; }

        // the unique identifier for the user, when we start we
        private static string s_uniqueUserIdentifier { get; }

        // the session identifier
        private static string s_sessionId { get; }

        // private semaphore to determine whether we sent the startup telemetry event
        private static int s_startupEventSent = 0;

        /// Use a hashset for quick lookups.
        /// We send telemetry only a known set of modules and tags.
        /// If it's not in the list (initialized in the static constructor), then we report anonymous
        /// or don't report anything (in the case of tags).
        private static readonly HashSet<string> s_knownModules;
        private static readonly HashSet<string> s_knownModuleTags;

        private static readonly HashSet<string> s_knownSubsystemNames;

        private static readonly string s_uuidPath;

        /// <summary>Gets a value indicating whether telemetry can be sent.</summary>
        public static bool CanSendTelemetry { get; private set; }

        /// <summary>
        /// Initializes static members of the <see cref="ApplicationInsightsTelemetry"/> class.
        /// Static constructor determines whether telemetry is to be sent, and then
        /// sets the telemetry key and set the telemetry delivery mode.
        /// Creates the session ID and initializes the HashSet of known module names.
        /// Gets or constructs the unique identifier.
        /// </summary>
        static ApplicationInsightsTelemetry()
        {
            CanSendTelemetry = !Utils.GetEnvironmentVariableAsBool(name: _telemetryOptoutEnvVar, defaultValue: false)
                && Platform.TryDeriveFromCache("telemetry.uuid", out s_uuidPath);

#if !UNIX
            if (CanSendTelemetry)
            {
                // Respect the diagnostics and feedback setting in Windows.
                CanSendTelemetry = WindowsDataCollectionSetting.CanCollectDiagnostics(PlatformDataCollectionLevel.Enhanced);
            }
#endif

            if (!CanSendTelemetry)
            {
                // Avoid the initialization work if we can't send telemetry.
                return;
            }

            s_sessionId = Guid.NewGuid().ToString();
            TelemetryConfiguration configuration = TelemetryConfiguration.CreateDefault();
            configuration.ConnectionString = "InstrumentationKey=" + _psCoreTelemetryKey;

            // Set this to true to reduce latency during development
            configuration.TelemetryChannel.DeveloperMode = false;

            // Be sure to obscure any information about the client node name.
            configuration.TelemetryInitializers.Add(new NameObscurerTelemetryInitializer());

            s_telemetryClient = new TelemetryClient(configuration);

            // use a hashset when looking for module names, it should be quicker than a string comparison
            s_knownModules = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
            {
                "AADRM",
                "activedirectory",
                "adcsadministration",
                "adcsdeployment",
                "addsadministration",
                "addsdeployment",
                "adfs",
                "adrms",
                "adrmsadmin",
                "agpm",
                "AIShell",
                "appbackgroundtask",
                "applocker",
                "appv",
                "appvclient",
                "appvsequencer",
                "appvserver",
                "appx",
                "assignedaccess",
                "Az",
                "Az.Accounts",
                "Az.Advisor",
                "Az.Aks",
                "Az.AlertsManagement",
                "Az.AnalysisServices",
                "Az.ApiManagement",
                "Az.ApplicationInsights",
                "Az.Attestation",
                "Az.Automation",
                "Az.Batch",
                "Az.Billing",
                "Az.Blueprint",
                "Az.Cdn",
                "Az.CognitiveServices",
                "Az.Compute",
                "Az.ContainerInstance",
                "Az.ContainerRegistry",
                "Az.DataBox",
                "Az.DataFactory",
                "Az.DataLakeAnalytics",
                "Az.DataLakeStore",
                "Az.DataMigration",
                "Az.DataShare",
                "Az.DeploymentManager",
                "Az.DeviceProvisioningServices",
                "Az.DevSpaces",
                "Az.DevTestLabs",
                "Az.Dns",
                "Az.EventGrid",
                "Az.EventHub",
                "Az.FrontDoor",
                "Az.GuestConfiguration",
                "Az.HDInsight",
                "Az.HealthcareApis",
                "Az.IotCentral",
                "Az.IotHub",
                "Az.KeyVault",
                "Az.Kusto",
                "Az.LogicApp",
                "Az.MachineLearning",
                "Az.ManagedServiceIdentity",
                "Az.ManagedServices",
                "Az.ManagementPartner",
                "Az.Maps",
                "Az.MarketplaceOrdering",
                "Az.Media",
                "Az.MixedReality",
                "Az.Monitor",
                "Az.NetAppFiles",
                "Az.Network",
                "Az.NotificationHubs",
                "Az.OperationalInsights",
                "Az.Peering",
                "Az.PolicyInsights",
                "Az.PowerBIEmbedded",
                "Az.PrivateDns",
                "Az.RecoveryServices",
                "Az.RedisCache",
                "Az.Relay",
                "Az.Reservations",
                "Az.ResourceGraph",
                "Az.Resources",
                "Az.Search",
                "Az.Security",
                "Az.ServiceBus",
                "Az.ServiceFabric",
                "Az.SignalR",
                "Az.Sql",
                "Az.Storage",
                "Az.StorageSync",
                "Az.StorageTable",
                "Az.StreamAnalytics",
                "Az.Subscription",
                "Az.Tools.Predictor",
                "Az.TrafficManager",
                "Az.Websites",
                "Azs.Azurebridge.Admin",
                "Azs.Backup.Admin",
                "Azs.Commerce.Admin",
                "Azs.Compute.Admin",
                "Azs.Fabric.Admin",
                "Azs.Gallery.Admin",
                "Azs.Infrastructureinsights.Admin",
                "Azs.Keyvault.Admin",
                "Azs.Network.Admin",
                "Azs.Storage.Admin",
                "Azs.Subscriptions",
                "Azs.Subscriptions.Admin",
                "Azs.Update.Admin",
                "AzStorageTable",
                "Azure",
                "Azure.AnalysisServices",
                "Azure.Storage",
                "AzureAD",
                "AzureInformationProtection",
                "AzureRM.Aks",
                "AzureRM.AnalysisServices",
                "AzureRM.ApiManagement",
                "AzureRM.ApplicationInsights",
                "AzureRM.Automation",
                "AzureRM.Backup",
                "AzureRM.Batch",
                "AzureRM.Billing",
                "AzureRM.Cdn",
                "AzureRM.CognitiveServices",
                "AzureRm.Compute",
                "AzureRM.Compute.ManagedService",
                "AzureRM.Consumption",
                "AzureRM.ContainerInstance",
                "AzureRM.ContainerRegistry",
                "AzureRM.DataFactories",
                "AzureRM.DataFactoryV2",
                "AzureRM.DataLakeAnalytics",
                "AzureRM.DataLakeStore",
                "AzureRM.DataMigration",
                "AzureRM.DeploymentManager",
                "AzureRM.DeviceProvisioningServices",
                "AzureRM.DevSpaces",
                "AzureRM.DevTestLabs",
                "AzureRm.Dns",
                "AzureRM.EventGrid",
                "AzureRM.EventHub",
                "AzureRM.FrontDoor",
                "AzureRM.HDInsight",
                "AzureRm.Insights",
                "AzureRM.IotCentral",
                "AzureRM.IotHub",
                "AzureRm.Keyvault",
                "AzureRM.LocationBasedServices",
                "AzureRM.LogicApp",
                "AzureRM.MachineLearning",
                "AzureRM.MachineLearningCompute",
                "AzureRM.ManagedServiceIdentity",
                "AzureRM.ManagementPartner",
                "AzureRM.Maps",
                "AzureRM.MarketplaceOrdering",
                "AzureRM.Media",
                "AzureRM.Network",
                "AzureRM.NotificationHubs",
                "AzureRM.OperationalInsights",
                "AzureRM.PolicyInsights",
                "AzureRM.PowerBIEmbedded",
                "AzureRM.Profile",
                "AzureRM.RecoveryServices",
                "AzureRM.RecoveryServices.Backup",
                "AzureRM.RecoveryServices.SiteRecovery",
                "AzureRM.RedisCache",
                "AzureRM.Relay",
                "AzureRM.Reservations",
                "AzureRM.ResourceGraph",
                "AzureRM.Resources",
                "AzureRM.Scheduler",
                "AzureRM.Search",
                "AzureRM.Security",
                "AzureRM.ServerManagement",
                "AzureRM.ServiceBus",
                "AzureRM.ServiceFabric",
                "AzureRM.SignalR",
                "AzureRM.SiteRecovery",
                "AzureRM.Sql",
                "AzureRm.Storage",
                "AzureRM.StorageSync",
                "AzureRM.StreamAnalytics",
                "AzureRM.Subscription",
                "AzureRM.Subscription.Preview",
                "AzureRM.Tags",
                "AzureRM.TrafficManager",
                "AzureRm.UsageAggregates",
                "AzureRm.Websites",
                "AzureRmStorageTable",
                "bestpractices",
                "bitlocker",
                "bitstransfer",
                "booteventcollector",
                "branchcache",
                "CimCmdlets",
                "clusterawareupdating",
                "CompatPowerShellGet",
                "configci",
                "ConfigurationManager",
                "CompletionPredictor",
                "DataProtectionManager",
                "dcbqos",
                "deduplication",
                "defender",
                "devicehealthattestation",
                "dfsn",
                "dfsr",
                "dhcpserver",
                "directaccessclient",
                "directaccessclientcomponent",
                "directaccessclientcomponents",
                "dism",
                "dnsclient",
                "dnsserver",
                "ElasticDatabaseJobs",
                "EventTracingManagement",
                "failoverclusters",
                "fileserverresourcemanager",
                "FIMAutomation",
                "GPRegistryPolicy",
                "grouppolicy",
                "hardwarecertification",
                "hcs",
                "hgsattestation",
                "hgsclient",
                "hgsdiagnostics",
                "hgskeyprotection",
                "hgsserver",
                "hnvdiagnostics",
                "hostcomputeservice",
                "hpc",
                "HPC.ACM",
                "HPC.ACM.API.PS",
                "HPCPack2016",
                "hyper-v",
                "IISAdministration",
                "international",
                "ipamserver",
                "iscsi",
                "iscsitarget",
                "ISE",
                "kds",
                "Microsoft.MBAM",
                "Microsoft.MEDV",
                "MgmtSvcAdmin",
                "MgmtSvcConfig",
                "MgmtSvcMySql",
                "MgmtSvcSqlServer",
                "Microsoft.AzureStack.ReadinessChecker",
                "Microsoft.Crm.PowerShell",
                "Microsoft.DiagnosticDataViewer",
                "Microsoft.DirectoryServices.MetadirectoryServices.Config",
                "Microsoft.Dynamics.Nav.Apps.Management",
                "Microsoft.Dynamics.Nav.Apps.Tools",
                "Microsoft.Dynamics.Nav.Ide",
                "Microsoft.Dynamics.Nav.Management",
                "Microsoft.Dynamics.Nav.Model.Tools",
                "Microsoft.Dynamics.Nav.Model.Tools.Crm",
                "Microsoft.EnterpriseManagement.Warehouse.Cmdlets",
                "Microsoft.Medv.Administration.Commands.WorkspacePackager",
                "Microsoft.PowerApps.Checker.PowerShell",
                "Microsoft.PowerShell.Archive",
                "Microsoft.PowerShell.ConsoleGuiTools",
                "Microsoft.PowerShell.Core",
                "Microsoft.PowerShell.Crescendo",
                "Microsoft.PowerShell.Diagnostics",
                "Microsoft.PowerShell.Host",
                "Microsoft.PowerShell.LocalAccounts",
                "Microsoft.PowerShell.Management",
                "Microsoft.PowerShell.ODataUtils",
                "Microsoft.PowerShell.Operation.Validation",
                "Microsoft.PowerShell.PSAdapter",
                "Microsoft.PowerShell.PSResourceGet",
                "Microsoft.PowerShell.RemotingTools",
                "Microsoft.PowerShell.SecretManagement",
                "Microsoft.PowerShell.SecretStore",
                "Microsoft.PowerShell.Security",
                "Microsoft.PowerShell.TextUtility",
                "Microsoft.PowerShell.Utility",
                "Microsoft.SharePoint.Powershell",
                "Microsoft.SystemCenter.ServiceManagementAutomation",
                "Microsoft.Windows.ServerManager.Migration",
                "Microsoft.WSMan.Management",
                "Microsoft.Xrm.OnlineManagementAPI",
                "Microsoft.Xrm.Tooling.CrmConnector.PowerShell",
                "Microsoft.Xrm.Tooling.PackageDeployment",
                "Microsoft.Xrm.Tooling.PackageDeployment.Powershell",
                "Microsoft.Xrm.Tooling.Testing",
                "MicrosoftPowerBIMgmt",
                "MicrosoftPowerBIMgmt.Data",
                "MicrosoftPowerBIMgmt.Profile",
                "MicrosoftPowerBIMgmt.Reports",
                "MicrosoftPowerBIMgmt.Workspaces",
                "MicrosoftStaffHub",
                "MicrosoftTeams",
                "MIMPAM",
                "mlSqlPs",
                "MMAgent",
                "MPIO",
                "MsDtc",
                "MSMQ",
                "MSOnline",
                "MSOnlineBackup",
                "WmsCmdlets",
                "WmsCmdlets3",
                "NanoServerImageGenerator",
                "NAVWebClientManagement",
                "NetAdapter",
                "NetConnection",
                "NetEventPacketCapture",
                "Netlbfo",
                "Netldpagent",
                "NetNat",
                "Netqos",
                "NetSecurity",
                "NetSwitchtTeam",
                "Nettcpip",
                "Netwnv",
                "NetworkConnectivity",
                "NetworkConnectivityStatus",
                "NetworkController",
                "NetworkControllerDiagnostics",
                "NetworkloadBalancingClusters",
                "NetworkSwitchManager",
                "NetworkTransition",
                "NFS",
                "NPS",
                "OfficeWebapps",
                "OperationsManager",
                "PackageManagement",
                "PartnerCenter",
                "pcsvdevice",
                "pef",
                "Pester",
                "pkiclient",
                "platformidentifier",
                "pnpdevice",
                "PowerShellEditorServices",
                "PowerShellGet",
                "powershellwebaccess",
                "printmanagement",
                "ProcessMitigations",
                "provisioning",
                "PSDesiredStateConfiguration",
                "PSDiagnostics",
                "PSReadLine",
                "PSScheduledJob",
                "PSScriptAnalyzer",
                "PSWorkflow",
                "PSWorkflowUtility",
                "RemoteAccess",
                "RemoteDesktop",
                "RemoteDesktopServices",
                "ScheduledTasks",
                "Secureboot",
                "ServerCore",
                "ServerManager",
                "ServerManagerTasks",
                "ServerMigrationcmdlets",
                "ServiceFabric",
                "Microsoft.Online.SharePoint.PowerShell",
                "shieldedvmdatafile",
                "shieldedvmprovisioning",
                "shieldedvmtemplate",
                "SkypeOnlineConnector",
                "SkypeForBusinessHybridHealth",
                "smbshare",
                "smbwitness",
                "smisconfig",
                "softwareinventorylogging",
                "SPFAdmin",
                "Microsoft.SharePoint.MigrationTool.PowerShell",
                "sqlps",
                "SqlServer",
                "StartLayout",
                "StartScreen",
                "Storage",
                "StorageDsc",
                "storageqos",
                "Storagereplica",
                "Storagespaces",
                "Syncshare",
                "System.Center.Service.Manager",
                "TLS",
                "TroubleshootingPack",
                "TrustedPlatformModule",
                "UEV",
                "UpdateServices",
                "UserAccessLogging",
                "vamt",
                "VirtualMachineManager",
                "vpnclient",
                "WasPSExt",
                "WDAC",
                "WDS",
                "WebAdministration",
                "WebAdministrationDsc",
                "WebApplicationProxy",
                "WebSites",
                "Whea",
                "WhiteboardAdmin",
                "WindowsDefender",
                "WindowsDefenderDsc",
                "WindowsDeveloperLicense",
                "WindowsDiagnosticData",
                "WindowsErrorReporting",
                "WindowServerRackup",
                "WindowsSearch",
                "WindowsServerBackup",
                "WindowsUpdate",
                "WinGetCommandNotFound",
                "wsscmdlets",
                "wsssetup",
                "wsus",
                "xActiveDirectory",
                "xBitLocker",
                "xDefender",
                "xDhcpServer",
                "xDismFeature",
                "xDnsServer",
                "xHyper-V",
                "xHyper-VBackup",
                "xPSDesiredStateConfiguration",
                "xSmbShare",
                "xSqlPs",
                "xStorage",
                "xWebAdministration",
                "xWindowsUpdate",
            };

            // use a hashset when looking for module names, it should be quicker than a string comparison
            s_knownModuleTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
            {
                "CrescendoBuilt",
            };

            s_uniqueUserIdentifier = GetUniqueIdentifier().ToString();
            s_knownSubsystemNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
            {
                "Completion",
                "General Feedback",
                "Windows Package Manager - WinGet",
                "Az Predictor"
            };
        }

        /// <summary>
        /// Send module load telemetry as a metric.
        /// For modules we send the module name (if allowed), and the version.
        /// Some modules (CIM) will continue use the string alternative method.
        /// </summary>
        /// <param name="telemetryType">The type of telemetry that we'll be sending.</param>
        /// <param name="moduleInfo">The module to report. If it is not allowed, then it is set to 'anonymous'.</param>
        internal static void SendModuleTelemetryMetric(TelemetryType telemetryType, PSModuleInfo moduleInfo)
        {
            if (!CanSendTelemetry)
            {
                return;
            }

            // Package up the module name, version, and known tags as a metric.
            // Note that the allowed tags will be a comma separated list which will need to
            // be handled in the telemetry query.
            try
            {
                string allowedModuleName = GetModuleName(moduleInfo.Name);
                string allowedModuleVersion = allowedModuleName == Anonymous ? AnonymousVersion : moduleInfo.Version?.ToString();
                var allowedModuleTags = moduleInfo.Tags.Where(t => s_knownModuleTags.Contains(t)).Distinct();
                string allowedModuleTagString = allowedModuleTags.Any() ? string.Join(',', allowedModuleTags) : NoTag;

                s_telemetryClient.
                    GetMetric(new MetricIdentifier(string.Empty, telemetryType.ToString(), "uuid", "SessionId", "ModuleName", "Version", "Tag")).
                    TrackValue(metricValue: 1.0, s_uniqueUserIdentifier, s_sessionId, allowedModuleName, allowedModuleVersion, allowedModuleTagString);
            }
            catch
            {
                // Ignore errors.
            }

        }

        /// <summary>
        /// Send module load telemetry as a metric.
        /// For modules we send the module name (if allowed), and the version.
        /// Some modules (CIM) will continue use the string alternative method.
        /// </summary>
        /// <param name="telemetryType">The type of telemetry that we'll be sending.</param>
        /// <param name="moduleName">The module name to report. If it is not allowed, then it is set to 'anonymous'.</param>
        internal static void SendModuleTelemetryMetric(TelemetryType telemetryType, string moduleName)
        {
            if (!CanSendTelemetry)
            {
                return;
            }

            try
            {
                string allowedModuleName = GetModuleName(moduleName);
                s_telemetryClient.GetMetric(telemetryType.ToString(), "uuid", "SessionId", "ModuleName", "Version").TrackValue(metricValue: 1.0, s_uniqueUserIdentifier, s_sessionId, allowedModuleName, AnonymousVersion);
            }
            catch
            {
                // Ignore errors.
            }
        }

        /// <summary>
        /// Send telemetry as a metric.
        /// </summary>
        /// <param name="metricId">The type of telemetry that we'll be sending.</param>
        /// <param name="data">The specific details about the telemetry.</param>
        /// <param name="value">The count of instances for the telemetry payload.</param>
        internal static void SendTelemetryMetric(TelemetryType metricId, string data, double value = 1.0)
        {
            if (!CanSendTelemetry)
            {
                return;
            }

            // These should be handled by SendModuleTelemetryMetric.
            Debug.Assert(metricId != TelemetryType.ModuleLoad, "ModuleLoad should be handled by SendModuleTelemetryMetric.");
            Debug.Assert(metricId != TelemetryType.WinCompatModuleLoad, "WinCompatModuleLoad should be handled by SendModuleTelemetryMetric.");

            string metricName = metricId.ToString();
            try
            {
                switch (metricId)
                {
                    case TelemetryType.ApplicationType:
                    case TelemetryType.PowerShellCreate:
                    case TelemetryType.RemoteSessionOpen:
                    case TelemetryType.ExperimentalEngineFeatureActivation:
                    case TelemetryType.ExperimentalEngineFeatureDeactivation:
                    case TelemetryType.ExperimentalFeatureUse:
                    case TelemetryType.FeatureUse:
                        s_telemetryClient.GetMetric(metricName, "uuid", "SessionId", "Detail").TrackValue(metricValue: value, s_uniqueUserIdentifier, s_sessionId, data);
                        break;
                    case TelemetryType.ExperimentalModuleFeatureActivation:
                    case TelemetryType.ExperimentalModuleFeatureDeactivation:
                        string experimentalFeatureName = GetExperimentalFeatureName(data);
                        s_telemetryClient.GetMetric(metricName, "uuid", "SessionId", "Detail").TrackValue(metricValue: value, s_uniqueUserIdentifier, s_sessionId, experimentalFeatureName);
                        break;
                }
            }
            catch
            {
                // do nothing, telemetry can't be sent
                // don't send the panic telemetry as if we have failed above, it will likely fail here.
            }
        }

        /// <summary>
        /// Send additional information about an feature as it is used.
        /// </summary>
        /// <param name="featureName">The name of the feature.</param>
        /// <param name="detail">The details about the feature use.</param>
        /// <param name="value">The value to report when sending the payload.</param>
        internal static void SendUseTelemetry(string featureName, string detail, double value = 1.0)
        {
            if (!CanSendTelemetry)
            {
                return;
            }

            // keep payload small
            if (featureName is null || detail is null || featureName.Length > 33 || detail.Length > 33)
            {
                return;
            }

            if (string.Compare(featureName, s_subsystemRegistration, true) == 0)
            {
                SendTelemetryMetric(TelemetryType.FeatureUse, string.Join(":", featureName, GetSubsystemName(detail)), value);
            }
            else
            {
                SendTelemetryMetric(TelemetryType.FeatureUse, string.Join(":", featureName, detail), value);
            }
        }

        /// <summary>
        /// Send additional information about an experimental feature as it is used.
        /// </summary>
        /// <param name="featureName">The name of the experimental feature.</param>
        /// <param name="detail">The details about the experimental feature use.</param>
        internal static void SendExperimentalUseData(string featureName, string detail)
        {
            if (!CanSendTelemetry)
            {
                return;
            }

            SendTelemetryMetric(TelemetryType.ExperimentalFeatureUse, string.Join(":", featureName, detail));
        }

        // Get the experimental feature name. If we can report it, we'll return the name of the feature, otherwise, we'll return "anonymous"
        private static string GetExperimentalFeatureName(string featureNameToValidate)
        {
            // An experimental feature in a module is guaranteed to start with the module name
            // we can strip out the text past the last '.' as the text before that will be the ModuleName
            int lastDotIndex = featureNameToValidate.LastIndexOf('.');
            string moduleName = featureNameToValidate.Substring(0, lastDotIndex);
            if (s_knownModules.Contains(moduleName))
            {
                return featureNameToValidate;
            }

            return Anonymous;
        }

        // Get the module name. If we can report it, we'll return the name, otherwise, we'll return the string "anonymous"
        private static string GetSubsystemName(string subsystemNameToValidate)
        {
            if (s_knownSubsystemNames.Contains(subsystemNameToValidate))
            {
                return subsystemNameToValidate;
            }

            return Anonymous;
        }

        // Get the module name. If we can report it, we'll return the name, otherwise, we'll return anonymous.
        private static string GetModuleName(string moduleNameToValidate)
        {
            if (s_knownModules.Contains(moduleNameToValidate))
            {
                return moduleNameToValidate;
            }

            return Anonymous;
        }

        /// <summary>
        /// Create the startup payload and send it up.
        /// This is done only once during for the console host.
        /// </summary>
        /// <param name="mode">The "mode" of the startup.</param>
        /// <param name="parametersUsed">The parameter bitmap used when starting.</param>
        internal static void SendPSCoreStartupTelemetry(string mode, double parametersUsed)
        {
            // Check if we already sent startup telemetry
            if (Interlocked.CompareExchange(ref s_startupEventSent, 1, 0) == 1)
            {
                return;
            }

            if (!CanSendTelemetry)
            {
                return;
            }

            // This is the payload which reports the startup information of OS and shell details.
            var properties = new Dictionary<string, string>();

            // This is the payload for the parameter data which is sent as a metric.
            var parameters = new Dictionary<string, double>();

            // The variable POWERSHELL_DISTRIBUTION_CHANNEL is set in our docker images and
            // by various other environments. This allows us to track the actual docker OS as
            // OSDescription provides only "linuxkit" which has limited usefulness.
            var channel = Environment.GetEnvironmentVariable("POWERSHELL_DISTRIBUTION_CHANNEL");

            // Construct the payload for the OS and shell details.
            properties.Add("SessionId", s_sessionId);
            properties.Add("UUID", s_uniqueUserIdentifier);
            properties.Add("GitCommitID", PSVersionInfo.GitCommitId);
            properties.Add("OSDescription", RuntimeInformation.OSDescription);
            properties.Add("RuntimeIdentifier", RuntimeInformation.RuntimeIdentifier);
            properties.Add("OSChannel", string.IsNullOrEmpty(channel) ? "unknown" : channel);
            properties.Add("StartMode", string.IsNullOrEmpty(mode) ? "unknown" : mode);

            // Construct the payload for the parameters used.
            parameters.Add("Param", parametersUsed);
            try
            {
                s_telemetryClient.TrackEvent("ConsoleHostStartup", properties, parameters);
            }
            catch
            {
                // do nothing, telemetry cannot be sent
            }
        }

        /// <summary>
        /// Try to read the file and collect the guid.
        /// </summary>
        /// <param name="telemetryFilePath">The path to the telemetry file.</param>
        /// <param name="id">The newly created id.</param>
        /// <returns>
        /// The method returns a bool indicating success or failure of creating the id.
        /// </returns>
        private static bool TryGetIdentifier(string telemetryFilePath, out Guid id)
        {
            if (File.Exists(telemetryFilePath))
            {
                // attempt to read the persisted identifier
                const int GuidSize = 16;
                byte[] buffer = new byte[GuidSize];
                try
                {
                    using (FileStream fs = new FileStream(telemetryFilePath, FileMode.Open, FileAccess.Read))
                    {
                        // if the read is invalid, or wrong size, we return it
                        int n = fs.Read(buffer, 0, GuidSize);
                        if (n == GuidSize)
                        {
                            // it's possible this could through
                            id = new Guid(buffer);
                            if (id != Guid.Empty)
                            {
                                return true;
                            }
                        }
                    }
                }
                catch
                {
                    // something went wrong, the file may not exist or not have enough bytes, so return false
                }
            }

            id = Guid.Empty;
            return false;
        }

        /// <summary>
        /// Try to create a unique identifier and persist it to the telemetry.uuid file.
        /// </summary>
        /// <param name="telemetryFilePath">The path to the persisted telemetry.uuid file.</param>
        /// <returns>
        /// The method node id.
        /// </returns>
        private static Guid CreateUniqueIdentifierAndFile(string telemetryFilePath)
        {
            // one last attempt to retrieve before creating incase we have a lot of simultaneous entry into the mutex.
            Guid id = Guid.Empty;
            if (TryGetIdentifier(telemetryFilePath, out id))
            {
                return id;
            }

            // The directory may not exist, so attempt to create it
            // CreateDirectory will simply return the directory if exists
            bool attemptFileCreation = true;
            try
            {
                Directory.CreateDirectory(Path.GetDirectoryName(telemetryFilePath));
            }
            catch
            {
                // There was a problem in creating the directory for the file, do not attempt to create the file.
                // We don't send telemetry here because there are valid reasons for the directory to not exist
                // and not be able to be created.
                attemptFileCreation = false;
            }

            // If we were able to create the directory, try to create the file,
            // if this fails we will send telemetry to indicate this and then use the default identifier.
            if (attemptFileCreation)
            {
                try
                {
                    id = Guid.NewGuid();
                    File.WriteAllBytes(telemetryFilePath, id.ToByteArray());
                    return id;
                }
                catch
                {
                    // another bit of telemetry to notify us about a problem with saving the unique id.
                    s_telemetryClient.GetMetric(_telemetryFailure, "Detail").TrackValue(1, "saveuuid");
                }
            }

            // all attempts to create an identifier have failed, so use the default node id.
            id = _defaultNodeIdentifier;
            return id;
        }

        /// <summary>
        /// Retrieve the unique identifier from the persisted file, if it doesn't exist create it.
        /// Generate a guid which will be used as the UUID.
        /// </summary>
        /// <returns>A guid which represents the unique identifier.</returns>
        private static Guid GetUniqueIdentifier()
        {
            // Try to get the unique id. If this returns false, we'll
            // create/recreate the telemetry.uuid file to persist for next startup.
            Guid id = Guid.Empty;
            if (TryGetIdentifier(s_uuidPath, out id))
            {
                return id;
            }

            // Multiple processes may start simultaneously so we need a system wide
            // way to control access to the file in the case (although remote) when we have
            // simultaneous shell starts without the persisted file which attempt to create the file.
            try
            {
                // CreateUniqueIdentifierAndFile shouldn't throw, but the mutex might
                using var m = new Mutex(true, "CreateUniqueUserId");
                m.WaitOne();
                try
                {
                    return CreateUniqueIdentifierAndFile(s_uuidPath);
                }
                finally
                {
                    m.ReleaseMutex();
                }
            }
            catch (Exception)
            {
                // Any problem in generating a uuid will result in no telemetry being sent.
                // Try to send the failure in telemetry, but it will have no unique id.
                s_telemetryClient.GetMetric(_telemetryFailure, "Detail").TrackValue(1, "mutex");
            }

            // something bad happened, turn off telemetry since the unique id wasn't set.
            CanSendTelemetry = false;
            return id;
        }
    }
}
