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

#include "pch.h"
#include "CascadiaSettings.h"

#include <fmt/chrono.h>
#include <shlobj.h>
#include <til/latch.h>
#include <til/io.h>

#include "resource.h"

#include "AzureCloudShellGenerator.h"
#include "PowershellCoreProfileGenerator.h"
#include "VisualStudioGenerator.h"
#include "WslDistroGenerator.h"
#include "SshHostGenerator.h"

#include "ApplicationState.h"
#include "DefaultTerminal.h"
#include "FileUtils.h"

#include "ProfileEntry.h"
#include "FolderEntry.h"
#include "MatchProfilesEntry.h"
#include "WtExeUtils.h"

using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Windows::ApplicationModel::AppExtensions;
using namespace winrt::Microsoft::Terminal::Settings;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;

static constexpr std::wstring_view SettingsFilename{ L"settings.json" };
static constexpr std::wstring_view DefaultsFilename{ L"defaults.json" };

static constexpr std::string_view ProfilesKey{ "profiles" };
static constexpr std::string_view DefaultSettingsKey{ "defaults" };
static constexpr std::string_view ProfilesListKey{ "list" };
static constexpr std::string_view SchemesKey{ "schemes" };
static constexpr std::string_view ThemesKey{ "themes" };

constexpr std::wstring_view systemThemeName{ L"system" };
constexpr std::wstring_view darkThemeName{ L"dark" };
constexpr std::wstring_view lightThemeName{ L"light" };
constexpr std::wstring_view legacySystemThemeName{ L"legacySystem" };
constexpr std::wstring_view legacyDarkThemeName{ L"legacyDark" };
constexpr std::wstring_view legacyLightThemeName{ L"legacyLight" };

static constexpr std::array builtinThemes{
    systemThemeName,
    lightThemeName,
    darkThemeName,
    legacySystemThemeName,
    legacyLightThemeName,
    legacyDarkThemeName,
};

static constexpr std::wstring_view jsonExtension{ L".json" };
static constexpr std::wstring_view FragmentsSubDirectory{ L"\\Fragments" };
static constexpr std::wstring_view FragmentsPath{ L"\\Microsoft\\Windows Terminal\\Fragments" };

static constexpr std::wstring_view AppExtensionHostName{ L"com.microsoft.windows.terminal.settings" };

// make sure this matches defaults.json.
static constexpr winrt::guid DEFAULT_WINDOWS_POWERSHELL_GUID{ 0x61c54bbd, 0xc2c6, 0x5271, { 0x96, 0xe7, 0x00, 0x9a, 0x87, 0xff, 0x44, 0xbf } };
static constexpr winrt::guid DEFAULT_COMMAND_PROMPT_GUID{ 0x0caa0dad, 0x35be, 0x5f56, { 0xa8, 0xff, 0xaf, 0xce, 0xee, 0xaa, 0x61, 0x01 } };

// Function Description:
// - Extracting the value from an async task (like talking to the app catalog) when we are on the
//   UI thread causes C++/WinRT to complain quite loudly (and halt execution!)
//   This templated function extracts the result from a task with chicanery.
template<typename TTask>
static auto extractValueFromTaskWithoutMainThreadAwait(TTask&& task) -> decltype(task.get())
{
    std::optional<decltype(task.get())> finalVal;
    til::latch latch{ 1 };

    const auto _ = [&]() -> safe_void_coroutine {
        const auto cleanup = wil::scope_exit([&]() {
            latch.count_down();
        });
        co_await winrt::resume_background();
        finalVal.emplace(co_await task);
    }();

    latch.wait();
    return finalVal.value();
}

// Concatenates the two given strings (!) and returns them as a path.
// You better make sure there's a path separator at the end of lhs or at the start of rhs.
static std::filesystem::path buildPath(const std::wstring_view& lhs, const std::wstring_view& rhs)
{
    std::wstring buffer;
    buffer.reserve(lhs.size() + rhs.size());
    buffer.append(lhs);
    buffer.append(rhs);
    return { std::move(buffer) };
}

void ParsedSettings::clear()
{
    globals = {};
    baseLayerProfile = {};
    profiles.clear();
    profilesByGuid.clear();
    colorSchemes.clear();
    fixupsAppliedDuringLoad = false;
    themesChangeLog.clear();
}

// This is a convenience method used by the CascadiaSettings constructor.
// It runs some basic settings layering without relying on external programs or files.
// This makes it suitable for most unit tests.
SettingsLoader SettingsLoader::Default(const std::string_view& userJSON, const std::string_view& inboxJSON)
{
    SettingsLoader loader{ userJSON, inboxJSON };
    loader.MergeInboxIntoUserSettings();
    loader.FinalizeLayering();
    loader.FixupUserSettings();
    return loader;
}

std::vector<Model::ExtensionPackage> SettingsLoader::LoadExtensionPackages()
{
    SettingsLoader loader{};
    loader.GenerateExtensionPackagesFromProfileGenerators();
    loader.FindFragmentsAndMergeIntoUserSettings(true /*generateExtensionPackages*/);

    std::vector<Model::ExtensionPackage> extensionPackages;
    for (auto [_, extPkg] : loader.extensionPackageMap)
    {
        extensionPackages.emplace_back(std::move(*extPkg));
    }
    return extensionPackages;
}

// The SettingsLoader class is an internal implementation detail of CascadiaSettings.
// Member methods aren't safe against misuse and you need to ensure to call them in a specific order.
// See CascadiaSettings::LoadAll() for a specific usage example.
//
// This constructor only handles parsing the two given JSON strings.
// At a minimum you should do at least everything that SettingsLoader::Default does.
SettingsLoader::SettingsLoader(const std::string_view& userJSON, const std::string_view& inboxJSON)
{
    _parse(OriginTag::InBox, {}, inboxJSON, inboxSettings);

    try
    {
        _parse(OriginTag::User, {}, userJSON, userSettings);
    }
    catch (const JsonUtils::DeserializationError& e)
    {
        _rethrowSerializationExceptionWithLocationInfo(e, userJSON);
    }

    if (const auto sources = userSettings.globals->DisabledProfileSources())
    {
        _ignoredNamespaces.reserve(sources.Size());
        for (auto&& id : sources)
        {
            _ignoredNamespaces.emplace(std::move(id));
        }
    }

    // Apply DisabledProfileSources policy setting. Pick whatever policy is set first.
    // In most cases HKCU settings take precedence over HKLM settings, but the inverse is true for policies.
    for (const auto key : { HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER })
    {
        wchar_t buffer[512]; // "640K ought to be enough for anyone"
        DWORD bufferSize = sizeof(buffer);
        if (RegGetValueW(key, LR"(Software\Policies\Microsoft\Windows Terminal)", L"DisabledProfileSources", RRF_RT_REG_MULTI_SZ, nullptr, buffer, &bufferSize) == 0)
        {
            for (auto p = buffer; *p;)
            {
                const auto len = wcslen(p);
                _ignoredNamespaces.emplace(p, gsl::narrow_cast<uint32_t>(len));
                p += len + 1;
            }
            break;
        }
    }

    // See member description of _userProfileCount.
    _userProfileCount = userSettings.profiles.size();
}

// This method is used to generate the JSON writer used for writing json in a styled format.
// We use it a few times throughout the loader, so we lazy load it and cache it here.
Json::StreamWriterBuilder SettingsLoader::_getJsonStyledWriter()
{
    static bool jsonWriterInitialized = false;
    static Json::StreamWriterBuilder styledWriter;
    if (!jsonWriterInitialized)
    {
        styledWriter["indentation"] = "    ";
        styledWriter["commentStyle"] = "All";
        styledWriter.settings_["enableYAMLCompatibility"] = true; // suppress spaces around colons
        styledWriter.settings_["precision"] = 6; // prevent values like 1.1000000000000001
        jsonWriterInitialized = true;
    }
    return styledWriter;
}

// Generate dynamic profiles and add them to the list of "inbox" profiles
// (meaning profiles specified by the application rather by the user).
void SettingsLoader::GenerateProfiles()
{
    auto generateProfiles = [&](const IDynamicProfileGenerator& generator) {
        if (!_ignoredNamespaces.contains(generator.GetNamespace()))
        {
            const auto oldProfileCount = inboxSettings.profiles.size();
            _executeGenerator(generator, inboxSettings.profiles);
            return oldProfileCount != inboxSettings.profiles.size();
        }
        return false;
    };

    // Generate profiles for each generator and add them to the inbox settings.
    // Be sure to update the same list below.
    generateProfiles(PowershellCoreProfileGenerator{});
    generateProfiles(WslDistroGenerator{});
    generateProfiles(AzureCloudShellGenerator{});
    generateProfiles(VisualStudioGenerator{});
    if constexpr (Feature_DynamicSSHProfiles::IsEnabled())
    {
        sshProfilesGenerated = generateProfiles(SshHostGenerator{});
    }
}

// Generate ExtensionPackage objects from the profile generators.
void SettingsLoader::GenerateExtensionPackagesFromProfileGenerators()
{
    auto generateExtensionPackages = [&](const IDynamicProfileGenerator& generator) {
        std::vector<winrt::com_ptr<implementation::Profile>> profilesList;
        _executeGenerator(generator, profilesList);

        // These are needed for the FragmentSettings object
        std::vector<Model::FragmentProfileEntry> profileEntries;
        Json::Value profilesListJson{ Json::ValueType::arrayValue };

        for (const auto& profile : profilesList)
        {
            const auto profileJson = profile->ToJson();
            profilesListJson.append(profileJson);
            profileEntries.push_back(winrt::make<FragmentProfileEntry>(profile->Guid(), hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), profileJson)) }));
        }

        // Manually construct the JSON for the FragmentSettings object
        Json::Value json{ Json::ValueType::objectValue };
        json[JsonKey(ProfilesKey)] = profilesListJson;

        auto generatorExtension = winrt::make_self<FragmentSettings>(hstring{ generator.GetNamespace() }, hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), json)) }, hstring{ L"settings.json" });
        generatorExtension->NewProfiles(winrt::single_threaded_vector<Model::FragmentProfileEntry>(std::move(profileEntries)));

        auto extPkg = _registerFragment(std::move(*generatorExtension), FragmentScope::Machine);
        extPkg->DisplayName(hstring{ generator.GetDisplayName() });
        extPkg->Icon(hstring{ generator.GetIcon() });
    };

    // Generate extension package objects for each generator.
    // Be sure to update the same list above.
    generateExtensionPackages(PowershellCoreProfileGenerator{});
    generateExtensionPackages(WslDistroGenerator{});
    generateExtensionPackages(AzureCloudShellGenerator{});
    generateExtensionPackages(VisualStudioGenerator{});
    if constexpr (Feature_DynamicSSHProfiles::IsEnabled())
    {
        generateExtensionPackages(SshHostGenerator{});
    }
}

// A new settings.json gets a special treatment:
// 1. The default profile is a PowerShell 7+ one, if one was generated,
//    and falls back to the standard PowerShell 5 profile otherwise.
// 2. cmd.exe gets a localized name.
void SettingsLoader::ApplyRuntimeInitialSettings()
{
    // 1.
    {
        const auto preferredPowershellProfile = PowershellCoreProfileGenerator::GetPreferredPowershellProfileName();
        auto guid = DEFAULT_WINDOWS_POWERSHELL_GUID;

        for (const auto& profile : inboxSettings.profiles)
        {
            if (profile->Name() == preferredPowershellProfile)
            {
                guid = profile->Guid();
                break;
            }
        }

        userSettings.globals->DefaultProfile(guid);
    }

    // 2.
    {
        for (const auto& profile : userSettings.profiles)
        {
            if (profile->Guid() == DEFAULT_COMMAND_PROMPT_GUID)
            {
                profile->Name(RS_(L"CommandPromptDisplayName"));
                break;
            }
        }
    }
}

// Adds profiles from .inboxSettings as parents of matching profiles in .userSettings.
// That way the user profiles will get appropriate defaults from the generators (like icons and such).
// If a matching profile doesn't exist yet in .userSettings, one will be created.
// Additionally, produces a final view of the color schemes from the inbox + user settings
void SettingsLoader::MergeInboxIntoUserSettings()
{
    for (const auto& profile : inboxSettings.profiles)
    {
        _addUserProfileParent(profile);
    }
}

// Searches AppData/ProgramData and app extension directories for settings JSON files.
// If such JSON files are found, they're read and their contents added to .userSettings.
//
// Of course it would be more elegant to add fragments to .inboxSettings first and then have MergeInboxIntoUserSettings
// merge them. Unfortunately however the "updates" key in fragment profiles make this impossible:
// The targeted profile might be one that got created as part of SettingsLoader::MergeInboxIntoUserSettings.
// Additionally the GUID in "updates" will conflict with existing GUIDs in .inboxSettings.
void SettingsLoader::FindFragmentsAndMergeIntoUserSettings(bool generateExtensionPackages)
{
    ParsedSettings fragmentSettings;

    const auto parseAndLayerFragmentFiles = [&](const std::filesystem::path& path, const winrt::hstring& source, FragmentScope scope) {
        const winrt::hstring sourceBasePath{ path.native() };
        for (const auto& fragmentExt : std::filesystem::directory_iterator{ path })
        {
            const auto fragExtPath = fragmentExt.path();
            if (fragExtPath.extension() == jsonExtension)
            {
                try
                {
                    const auto content = til::io::read_file_as_utf8_string_if_exists(fragExtPath);
                    if (!content.empty())
                    {
                        _parseFragment(source,
                                       sourceBasePath,
                                       content,
                                       fragmentSettings,
                                       generateExtensionPackages ?
                                           static_cast<std::optional<ParseFragmentMetadata>>(ParseFragmentMetadata{ fragExtPath.filename().wstring(), scope }) :
                                           std::nullopt);
                    }
                }
                CATCH_LOG();
            }
        }
    };

    for (const auto& rfid : std::array{ FOLDERID_LocalAppData, FOLDERID_ProgramData })
    {
        wil::unique_cotaskmem_string folder;
        THROW_IF_FAILED(SHGetKnownFolderPath(rfid, 0, nullptr, &folder));

        const auto fragmentPath = buildPath(folder.get(), FragmentsPath);

        if (std::filesystem::is_directory(fragmentPath))
        {
            for (const auto& fragmentExtFolder : std::filesystem::directory_iterator{ fragmentPath })
            {
                const auto filename = fragmentExtFolder.path().filename();
                const auto& source = filename.native();

                if (fragmentExtFolder.is_directory())
                {
                    parseAndLayerFragmentFiles(fragmentExtFolder.path(),
                                               winrt::hstring{ source },
                                               rfid == FOLDERID_LocalAppData ? FragmentScope::User : FragmentScope::Machine); // scope
                }
            }
        }
    }

    // Search through app extensions.
    // Gets the catalog of extensions with the name "com.microsoft.windows.terminal.settings".
    //
    // GH#12305: Open() can throw an 0x80070490 "Element not found.".
    // It's unclear to me under which circumstances this happens as no one on the team
    // was able to reproduce the user's issue, even if the application was run unpackaged.
    // The error originates from `CallerIdentity::GetCallingProcessAppId` which returns E_NOT_SET.
    // A comment can be found, reading:
    // > Gets the "strong" AppId from the process token. This works for UWAs and Centennial apps,
    // > strongly named processes where the AppId is stored securely in the process token. [...]
    // > E_NOT_SET is returned for processes without strong AppIds.
    IVectorView<AppExtension> extensions;
    try
    {
        const auto catalog = AppExtensionCatalog::Open(AppExtensionHostName);
        extensions = extractValueFromTaskWithoutMainThreadAwait(catalog.FindAllAsync());
    }
    CATCH_LOG();

    if (!extensions)
    {
        return;
    }

    for (const auto& ext : extensions)
    {
        const auto& package = ext.Package();
        const auto packageName = package.Id().FamilyName();

        // If the extension was explicitly disabled, skip over it early to avoid the async API!
        // NOTE: only do this if we're NOT generating extension packages. If we are, we need to get all the
        //       package metadata anyway to display in the settings UI later.
        if (!generateExtensionPackages && _ignoredNamespaces.contains(std::wstring_view{ packageName }))
        {
            continue;
        }

        // Likewise, getting the public folder from an extension is an async operation.
        auto foundFolder = extractValueFromTaskWithoutMainThreadAwait(ext.GetPublicFolderAsync());
        if (!foundFolder)
        {
            continue;
        }

        // the StorageFolder class has its own methods for obtaining the files within the folder
        // however, all those methods are Async methods
        // you may have noticed that we need to resort to clunky implementations for async operations
        // (they are in extractValueFromTaskWithoutMainThreadAwait)
        // so for now we will just take the folder path and access the files that way
        const auto path = buildPath(foundFolder.Path(), FragmentsSubDirectory);

        if (std::filesystem::is_directory(path))
        {
            // MSIX does not support machine-wide scope
            // See https://github.com/microsoft/winget-cli/discussions/1983
            parseAndLayerFragmentFiles(path,
                                       packageName,
                                       FragmentScope::User);

            if (generateExtensionPackages)
            {
                auto extPkg = extensionPackageMap[packageName];
                extPkg->Icon(package.Logo().AbsoluteUri());
                extPkg->DisplayName(package.DisplayName());
            }
        }
    }
}

// See FindFragmentsAndMergeIntoUserSettings.
// This function does the same, but for a single given JSON blob and source
// and at the time of writing is used for unit tests only.
void SettingsLoader::MergeFragmentIntoUserSettings(const winrt::hstring& source, const winrt::hstring& basePath, const std::string_view& content)
{
    ParsedSettings fragmentSettings;
    _parseFragment(source, basePath, content, fragmentSettings, std::nullopt);
}

// Call this method before passing SettingsLoader to the CascadiaSettings constructor.
// It layers all remaining objects onto each other (those that aren't covered
// by MergeInboxIntoUserSettings/FindFragmentsAndMergeIntoUserSettings).
void SettingsLoader::FinalizeLayering()
{
    for (const auto& colorScheme : inboxSettings.colorSchemes)
    {
        _addOrMergeUserColorScheme(colorScheme.second);
    }

    // Layer default globals -> user globals
    userSettings.globals->AddLeastImportantParent(inboxSettings.globals);

    // Actions are currently global, so if we want to conditionally light up a bunch of
    // actions, this is the time to do it.
    if (userSettings.globals->EnableColorSelection())
    {
        const auto json = _parseJson(LoadStringResource(IDR_ENABLE_COLOR_SELECTION));
        const auto globals = GlobalAppSettings::FromJson(json.root, OriginTag::InBox);
        userSettings.globals->AddLeastImportantParent(globals);
    }

    userSettings.globals->_FinalizeInheritance();
    // Layer default profile defaults -> user profile defaults
    userSettings.baseLayerProfile->AddLeastImportantParent(inboxSettings.baseLayerProfile);
    userSettings.baseLayerProfile->_FinalizeInheritance();
    // Layer user profile defaults -> user profiles
    for (const auto& profile : userSettings.profiles)
    {
        profile->AddMostImportantParent(userSettings.baseLayerProfile);

        // This completes the parenting process that was started in _addUserProfileParent().
        profile->_FinalizeInheritance();
        if (profile->Origin() == OriginTag::None)
        {
            // If you add more fields here, make sure to do the same in
            // implementation::CreateChild().
            profile->Origin(OriginTag::User);
            profile->Name(profile->Name());
            profile->Hidden(profile->Hidden());
        }
    }
}

// Let's say a user doesn't know that they need to write `"hidden": true` in
// order to prevent a profile from showing up (and a settings UI doesn't exist).
// Naturally they would open settings.json and try to remove the profile object.
// This section of code recognizes if a profile was seen before and marks it as
// `"hidden": true` by default and thus ensures the behavior the user expects:
// Profiles won't show up again after they've been removed from settings.json.
//
// Returns true if something got changed and
// the settings need to be saved to disk.
bool SettingsLoader::DisableDeletedProfiles()
{
    const auto& state = winrt::get_self<ApplicationState>(ApplicationState::SharedInstance());
    auto generatedProfileIds = state->GeneratedProfiles();
    auto newGeneratedProfiles = false;

    for (const auto& profile : _getNonUserOriginProfiles())
    {
        if (generatedProfileIds.emplace(profile->Guid()).second)
        {
            newGeneratedProfiles = true;
        }
        else
        {
            profile->Deleted(true);
            profile->Hidden(true);
        }
    }

    if (newGeneratedProfiles)
    {
        state->GeneratedProfiles(generatedProfileIds);
    }

    return newGeneratedProfiles;
}

// Returns true if something got changed and
// the settings need to be saved to disk.
bool SettingsLoader::AddDynamicProfileFolders()
{
    // Keep track of generated folders to avoid regenerating them
    const auto state = get_self<ApplicationState>(ApplicationState::SharedInstance());

    // If the SSH generator is enabled, try to create an "SSH" folder with all the generated profiles
    if (sshProfilesGenerated && !state->SSHFolderGenerated())
    {
        SshHostGenerator sshGenerator;
        auto matchProfilesEntry = make_self<implementation::MatchProfilesEntry>();
        matchProfilesEntry->Source(hstring{ sshGenerator.GetNamespace() });

        auto folderEntry = make_self<implementation::FolderEntry>();
        folderEntry->Name(L"SSH");
        folderEntry->Icon(MediaResource::FromString(hstring{ sshGenerator.GetIcon() }));
        folderEntry->Inlining(FolderEntryInlining::Auto);
        folderEntry->RawEntries(winrt::single_threaded_vector<Model::NewTabMenuEntry>({ *matchProfilesEntry }));

        // NewTabMenu is guaranteed to exist by FixupUserSettings, which runs before this fixup.
        userSettings.globals->NewTabMenu().Append(folderEntry.as<Model::NewTabMenuEntry>());
        state->SSHFolderGenerated(true);
        return true;
    }
    return false;
}

bool winrt::Microsoft::Terminal::Settings::Model::implementation::SettingsLoader::RemapColorSchemeForProfile(const winrt::com_ptr<winrt::Microsoft::Terminal::Settings::Model::implementation::Profile>& profile)
{
    bool modified{ false };

    const IAppearanceConfig appearances[] = {
        profile->DefaultAppearance(),
        profile->UnfocusedAppearance()
    };

    for (auto&& appearance : appearances)
    {
        if (appearance)
        {
            if (auto schemeName{ appearance.LightColorSchemeName() }; !schemeName.empty())
            {
                if (auto found{ userSettings.colorSchemeRemappings.find(schemeName) }; found != userSettings.colorSchemeRemappings.end())
                {
                    appearance.LightColorSchemeName(found->second);
                    modified = true;
                }
            }

            if (auto schemeName{ appearance.DarkColorSchemeName() }; !schemeName.empty())
            {
                if (auto found{ userSettings.colorSchemeRemappings.find(schemeName) }; found != userSettings.colorSchemeRemappings.end())
                {
                    appearance.DarkColorSchemeName(found->second);
                    modified = true;
                }
            }
        }
    }

    return modified;
}

// Runs migrations and fixups on user settings.
// Returns true if something got changed and
// the settings need to be saved to disk.
bool SettingsLoader::FixupUserSettings()
{
    struct CommandlinePatch
    {
        winrt::guid guid{};
        std::wstring_view before;
        std::wstring_view after;
    };

    static constexpr std::array commandlinePatches{
        CommandlinePatch{ DEFAULT_COMMAND_PROMPT_GUID, L"cmd.exe", L"%SystemRoot%\\System32\\cmd.exe" },
        CommandlinePatch{ DEFAULT_WINDOWS_POWERSHELL_GUID, L"powershell.exe", L"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" },
    };

    static constexpr std::array iconsToClearFromVisualStudioProfiles{
        std::wstring_view{ L"ms-appx:///ProfileIcons/{61c54bbd-c2c6-5271-96e7-009a87ff44bf}.png" },
        std::wstring_view{ L"ms-appx:///ProfileIcons/{0caa0dad-35be-5f56-a8ff-afceeeaa6101}.png" },
    };

    auto fixedUp = userSettings.fixupsAppliedDuringLoad;
    fixedUp = userSettings.globals->FixupsAppliedDuringLoad() || fixedUp;

    fixedUp = RemapColorSchemeForProfile(userSettings.baseLayerProfile) || fixedUp;
    for (const auto& profile : userSettings.profiles)
    {
        fixedUp = RemapColorSchemeForProfile(profile) || fixedUp;

        if (profile->HasCommandline())
        {
            for (const auto& patch : commandlinePatches)
            {
                if (profile->Guid() == patch.guid && til::equals_insensitive_ascii(profile->Commandline(), patch.before))
                {
                    profile->ClearCommandline();

                    // GH#12842:
                    // With the commandline field on the user profile gone, it's actually unknown what
                    // commandline it'll inherit, since a user profile can have multiple parents. We have to
                    // make sure we restore the correct commandline in case we don't inherit the expected one.
                    if (profile->Commandline() != patch.after)
                    {
                        profile->Commandline(winrt::hstring{ patch.after });
                    }

                    fixedUp = true;
                    break;
                }
            }
        }

        if (profile->HasIcon() && profile->HasSource() && profile->Source() == VisualStudioGenerator::Namespace)
        {
            for (auto&& icon : iconsToClearFromVisualStudioProfiles)
            {
                if (profile->Icon().Path() == icon)
                {
                    profile->ClearIcon();
                    fixedUp = true;
                    break;
                }
            }
        }
    }

    // Terminal 1.19: Migrate the global
    // `compatibility.reloadEnvironmentVariables` to being a per-profile
    // setting. If the user had it disabled in 1.18, then set the
    // profiles.defaults value to false to match.
    if (!userSettings.globals->LegacyReloadEnvironmentVariables())
    {
        // migrate the user's opt-out to the profiles.defaults
        userSettings.baseLayerProfile->ReloadEnvironmentVariables(false);
        fixedUp = true;
    }

    // Terminal 1.23: Migrate the global
    // `experimental.input.forceVT` to being a per-profile setting.
    if (userSettings.globals->LegacyForceVTInput())
    {
        // migrate the user's opt-out to the profiles.defaults
        userSettings.baseLayerProfile->ForceVTInput(true);
        fixedUp = true;
    }

    // Terminal 1.24
    // Ensure that the user always has a newTabMenu. We used to do this last, after
    // resolving all of the new tab menu entries, but there was no conceivable reason
    // that it should happen so late.
    if (!userSettings.globals->HasNewTabMenu())
    {
        userSettings.globals->NewTabMenu(winrt::single_threaded_vector<Model::NewTabMenuEntry>({ Model::RemainingProfilesEntry{} }));
        // This one does not need to be written back to the settings file immediately, it can wait until we write one for another reason.
    }

    return fixedUp;
}

// Give a string of length N and a position of [0,N) this function returns
// the line/column within the string, similar to how text editors do it.
// Newlines are considered part of the current line (as per POSIX).
std::pair<size_t, size_t> SettingsLoader::_lineAndColumnFromPosition(const std::string_view& string, const size_t position)
{
    size_t line = 1;
    size_t column = 0;

    for (;;)
    {
        const auto p = string.find('\n', column);
        if (p >= position)
        {
            break;
        }

        column = p + 1;
        line++;
    }

    return { line, position - column + 1 };
}

// Formats a JSON exception for humans to read and throws that.
void SettingsLoader::_rethrowSerializationExceptionWithLocationInfo(const JsonUtils::DeserializationError& e, const std::string_view& settingsString)
{
    std::string jsonValueAsString;
    try
    {
        jsonValueAsString = e.jsonValue.asString();
        if (e.jsonValue.isString())
        {
            jsonValueAsString = fmt::format("\"{}\"", jsonValueAsString);
        }
    }
    catch (...)
    {
        jsonValueAsString = "array or object";
    }

    const auto [line, column] = _lineAndColumnFromPosition(settingsString, static_cast<size_t>(e.jsonValue.getOffsetStart()));

    fmt::memory_buffer msg;
    fmt::format_to(std::back_inserter(msg), "* Line {}, Column {}", line, column);
    if (e.key)
    {
        fmt::format_to(std::back_inserter(msg), " ({})", *e.key);
    }
    fmt::format_to(std::back_inserter(msg), "\n  Have: {}\n  Expected: {}\0", jsonValueAsString, e.expectedType);

    throw SettingsTypedDeserializationException{ msg.data() };
}

// Simply parses the given content to a Json::Value.
Json::Value SettingsLoader::_parseJSON(const std::string_view& content)
{
    Json::Value json;
    std::string errs;
    const std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder{}.newCharReader() };

    if (!reader->parse(content.data(), content.data() + content.size(), &json, &errs))
    {
        throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs));
    }

    return json;
}

// A helper method similar to Json::Value::operator[], but compatible with std::string_view.
const Json::Value& SettingsLoader::_getJSONValue(const Json::Value& json, const std::string_view& key) noexcept
{
    if (json.isObject())
    {
        if (const auto val = json.find(key.data(), key.data() + key.size()))
        {
            return *val;
        }
    }

    return Json::Value::nullSingleton();
}

// We treat userSettings.profiles as an append-only array and will
// append profiles into the userSettings as necessary in this function.
// _userProfileCount stores the number of profiles that were in userJSON during construction.
//
// Thus no matter how many profiles are added later on, the following condition holds true:
// The userSettings.profiles in the range [0, _userProfileCount) contain all profiles specified by the user.
// In turn all profiles in the range [_userProfileCount, ∞) contain newly generated/added profiles.
// std::span{ userSettings.profiles }.subspan(_userProfileCount) gets us the latter range.
std::span<const winrt::com_ptr<Profile>> SettingsLoader::_getNonUserOriginProfiles() const
{
    return std::span{ userSettings.profiles }.subspan(_userProfileCount);
}

// Parses the given JSON string ("content") and fills a ParsedSettings instance with it.
// This function is to be used for user settings files.
void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings)
{
    const auto json = _parseJson(content);

    settings.clear();

    {
        settings.globals = GlobalAppSettings::FromJson(json.root, origin);

        for (const auto& schemeJson : json.colorSchemes)
        {
            if (const auto scheme = ColorScheme::FromJson(schemeJson))
            {
                scheme->Origin(origin);
                settings.colorSchemes.emplace(scheme->Name(), std::move(scheme));
            }
        }
    }

    {
        for (const auto& themeJson : json.themes)
        {
            if (const auto theme = Theme::FromJson(themeJson))
            {
                const auto& name{ theme->Name() };

                if (origin != OriginTag::InBox &&
                    (std::ranges::find(builtinThemes, name) != builtinThemes.end()))
                {
                    // If the theme didn't come from the in-box themes, and its
                    // name was one of the reserved names, then just ignore it.
                    // Themes don't support layering - we don't want the user
                    // versions of these themes overriding the built-in ones.
                    continue;
                }

                if (origin != OriginTag::InBox)
                {
                    static std::string themesContext{ "themes" };
                    theme->LogSettingChanges(settings.themesChangeLog, themesContext);
                }
                settings.globals->AddTheme(*theme);
            }
        }
    }

    {
        settings.baseLayerProfile = Profile::FromJson(json.profileDefaults);
        // Remove the `guid` member from the default settings.
        // That will hyper-explode, so just don't let them do that.
        // Also remove name, source, and commandline; those are not valid for the profiles defaults object.
        settings.baseLayerProfile->ClearGuid();
        settings.baseLayerProfile->ClearName();
        settings.baseLayerProfile->ClearSource();
        settings.baseLayerProfile->ClearCommandline();
        settings.baseLayerProfile->Origin(OriginTag::ProfilesDefaults);
    }

    {
        const auto size = json.profilesList.size();
        settings.profiles.reserve(size);
        settings.profilesByGuid.reserve(size);

        for (const auto& profileJson : json.profilesList)
        {
            auto profile = _parseProfile(origin, source, profileJson);
            // GH#9962: Discard Guid-less, Name-less profiles.
            if (profile->HasGuid())
            {
                _appendProfile(std::move(profile), profile->Guid(), settings);
            }
        }
    }
}

// Just like _parse, but is to be used for fragment files, which don't support anything but color
// schemes and profiles. Additionally this function supports profiles which specify an "updates" key.
// - fragmentMeta: If set, construct and register FragmentSettings objects. Provides metadata necessary for doing so.
//                 Otherwise, completely skip over that extra work and apply parsed settings to the user settings, if allowed by disabledProfileSources ("_ignoredNamespaces").
void SettingsLoader::_parseFragment(const winrt::hstring& source, const winrt::hstring& sourceBasePath, const std::string_view& content, ParsedSettings& settings, const std::optional<ParseFragmentMetadata>& fragmentMeta)
{
    auto json = _parseJson(content);

    const bool buildFragmentSettings = fragmentMeta.has_value();
    const bool applyToUserSettings = !buildFragmentSettings && !_ignoredNamespaces.contains(std::wstring_view{ source });
    winrt::com_ptr<implementation::FragmentSettings> fragmentSettings = buildFragmentSettings ?
                                                                            winrt::make_self<FragmentSettings>(source, hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), json.root)) }, hstring{ fragmentMeta->jsonFilename }) :
                                                                            nullptr;

    settings.clear();

    // Load GlobalAppSettings and ColorSchemes
    {
        std::vector<Model::FragmentColorSchemeEntry> fragmentColorSchemes;
        for (const auto& schemeJson : json.colorSchemes)
        {
            try
            {
                if (const auto scheme = ColorScheme::FromJson(schemeJson))
                {
                    scheme->Origin(OriginTag::Fragment);
                    if (buildFragmentSettings)
                    {
                        fragmentColorSchemes.emplace_back(winrt::make<FragmentColorSchemeEntry>(scheme->Name(), hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), schemeJson)) }));
                    }
                    if (applyToUserSettings)
                    {
                        // Don't add the color scheme to the Fragment's GlobalSettings; that will
                        // cause layering issues later. Add them to a staging area for later processing.
                        // (search for STAGED COLORS to find the next step)
                        settings.colorSchemes.emplace(scheme->Name(), std::move(scheme));
                    }
                }
            }
            CATCH_LOG()
        }

        if (buildFragmentSettings)
        {
            fragmentSettings->ColorSchemes(fragmentColorSchemes.empty() ? nullptr : single_threaded_vector<Model::FragmentColorSchemeEntry>(std::move(fragmentColorSchemes)));
        }
        if (applyToUserSettings)
        {
            // Parse out actions from the fragment. Manually opt-out of keybinding
            // parsing - fragments shouldn't be allowed to bind actions to keys
            // directly. We may want to revisit circa GH#2205
            settings.globals = winrt::make_self<GlobalAppSettings>();
            settings.globals->SourceBasePath = sourceBasePath;
            settings.globals->LayerActionsFrom(json.root, OriginTag::Fragment, false);
        }
    }

    // Load new and modified profiles
    {
        if (applyToUserSettings)
        {
            const auto size = json.profilesList.size();
            settings.profiles.reserve(size);
            settings.profilesByGuid.reserve(size);
        }

        std::vector<Model::FragmentProfileEntry> newProfiles;
        std::vector<Model::FragmentProfileEntry> modifiedProfiles;
        for (const auto& profileJson : json.profilesList)
        {
            try
            {
                // GH#9962: Discard Guid-less, Name-less profiles, but...
                // allow ones with an Updates field, as those are special for fragments.
                // We need to make sure to only call Guid() if HasGuid() is true,
                // as Guid() will dynamically generate a return value otherwise.
                auto profile = _parseProfile(OriginTag::Fragment, source, profileJson);
                const auto guid = profile->HasGuid() ? profile->Guid() : profile->Updates();
                auto destinationSet = profile->HasGuid() ? &newProfiles : &modifiedProfiles;
                if (guid != winrt::guid{})
                {
                    profile->SourceBasePath = sourceBasePath;
                    if (buildFragmentSettings)
                    {
                        destinationSet->emplace_back(winrt::make<FragmentProfileEntry>(guid, hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), profileJson)) }));
                    }
                    if (applyToUserSettings)
                    {
                        _appendProfile(std::move(profile), guid, settings);
                    }
                }
            }
            CATCH_LOG()
        }
        if (buildFragmentSettings)
        {
            fragmentSettings->NewProfiles(newProfiles.empty() ? nullptr : single_threaded_vector<Model::FragmentProfileEntry>(std::move(newProfiles)));
            fragmentSettings->ModifiedProfiles(modifiedProfiles.empty() ? nullptr : single_threaded_vector<Model::FragmentProfileEntry>(std::move(modifiedProfiles)));
            _registerFragment(std::move(*fragmentSettings), fragmentMeta->scope);
        }
    }

    // Merge profiles, color schemes, and globals into the user settings (aka inheritance)
    if (applyToUserSettings)
    {
        for (const auto& fragmentProfile : settings.profiles)
        {
            if (const auto updates = fragmentProfile->Updates(); updates != winrt::guid{})
            {
                if (const auto it = userSettings.profilesByGuid.find(updates); it != userSettings.profilesByGuid.end())
                {
                    it->second->AddMostImportantParent(fragmentProfile);
                }
            }
            else
            {
                _addUserProfileParent(fragmentProfile);
            }
        }

        // STAGED COLORS are processed here: we merge them into the partially-loaded
        // settings directly so that we can resolve conflicts between user-generated
        // color schemes and fragment-originated ones.
        for (const auto& [_, fragmentColorScheme] : settings.colorSchemes)
        {
            _addOrMergeUserColorScheme(fragmentColorScheme);
        }

        // Add the parsed fragment globals as a parent of the user's settings.
        // Later, in FinalizeInheritance, this will result in the action map from
        // the fragments being applied before the user's own settings.
        userSettings.globals->AddLeastImportantParent(settings.globals);
    }
}

SettingsLoader::JsonSettings SettingsLoader::_parseJson(const std::string_view& content)
{
    auto root = content.empty() ? Json::Value{ Json::ValueType::objectValue } : _parseJSON(content);
    const auto& colorSchemes = _getJSONValue(root, SchemesKey);
    const auto& themes = _getJSONValue(root, ThemesKey);
    const auto& profilesObject = _getJSONValue(root, ProfilesKey);
    const auto& profileDefaults = _getJSONValue(profilesObject, DefaultSettingsKey);
    const auto& profilesList = profilesObject.isArray() ? profilesObject : _getJSONValue(profilesObject, ProfilesListKey);
    return JsonSettings{ std::move(root), colorSchemes, profileDefaults, profilesList, themes };
}

// Just a common helper function between _parse and _parseFragment.
// Parses a profile and ensures it has a Guid if possible.
winrt::com_ptr<Profile> SettingsLoader::_parseProfile(const OriginTag origin, const winrt::hstring& source, const Json::Value& profileJson)
{
    auto profile = Profile::FromJson(profileJson);
    profile->Origin(origin);

    // The Guid() generation below depends on the value of Source().
    // --> Provide one if we got one.
    if (!source.empty())
    {
        profile->Source(source);
    }

    // If none exists. the Guid() getter generates one from Name() and optionally Source().
    // We want to ensure that every profile has a GUID no matter what, not just to
    // cache the value, but also to make them consistently identifiable later on.
    if (!profile->HasGuid() && profile->HasName())
    {
        profile->Guid(profile->Guid());
    }

    return profile;
}

// Adds a profile to the ParsedSettings instance. Takes ownership of the profile.
// It ensures no duplicate GUIDs are added to the ParsedSettings instance.
void SettingsLoader::_appendProfile(winrt::com_ptr<Profile>&& profile, const winrt::guid& guid, ParsedSettings& settings)
{
    // FYI: The static_cast ensures we don't move the profile into
    // `profilesByGuid`, even though we still need it later for `profiles`.
    if (settings.profilesByGuid.emplace(guid, static_cast<const winrt::com_ptr<Profile>&>(profile)).second)
    {
        settings.profiles.emplace_back(profile);
    }
    else
    {
        duplicateProfile = true;
    }
}

// If the given ParsedSettings instance contains a profile with the given profile's GUID,
// the profile is added as a parent. Otherwise, a new child profile is created.
void SettingsLoader::_addUserProfileParent(const winrt::com_ptr<implementation::Profile>& profile)
{
    if (const auto [it, inserted] = userSettings.profilesByGuid.emplace(profile->Guid(), nullptr); !inserted)
    {
        // If inserted is false, we got a matching user profile with identical GUID.
        // --> The generated profile is a parent of the existing user profile.
        it->second->AddLeastImportantParent(profile);
    }
    else
    {
        // If inserted is true, then this is a generated profile that doesn't exist
        // in the user's settings (which makes this branch somewhat unlikely).
        //
        // When a user modifies a profile they shouldn't modify the (static/constant)
        // inbox profile of course. That's why we need to create a child.
        // And since we previously added the (now) parent profile into profilesByGuid
        // we'll have to replace it->second with the (new) child profile.
        //
        // These additional things are required to complete a (user) profile:
        // * A call to _FinalizeInheritance()
        // * Every profile should at least have Origin(), Name() and Hidden() set
        // They're handled by SettingsLoader::FinalizeLayering() and detected by
        // the missing Origin(). Setting these fields as late as possible ensures
        // that we pick up the correct, inherited values of all of the child's parents.
        //
        // If you add more fields here, make sure to do the same in
        // implementation::CreateChild().
        auto child = winrt::make_self<Profile>();
        child->AddLeastImportantParent(profile);
        child->Guid(profile->Guid());

        // If profile is a dynamic/generated profile, a fragment's
        // Source() should have no effect on this user profile.
        if (profile->HasSource())
        {
            child->Source(profile->Source());
        }

        it->second = child;
        userSettings.profiles.emplace_back(std::move(child));
    }
}

// returns whether the scheme was successfully added
bool SettingsLoader::_addOrMergeUserColorScheme(const winrt::com_ptr<implementation::ColorScheme>& newScheme)
{
    // On entry, all the user color schemes have been loaded. Therefore, any insertions of inbox or fragment schemes
    // will fail; we can leverage this to detect when they are equivalent and delete the user's duplicate copies.
    // If the user has changed the otherwise "duplicate" scheme, though, we will move it aside.
    if (const auto [it, inserted] = userSettings.colorSchemes.emplace(newScheme->Name(), newScheme); !inserted)
    {
        // This scheme was not inserted because one already existed.
        auto existingScheme{ it->second };
        if (existingScheme->Origin() == OriginTag::User) // we only want to impose ordering on User schemes
        {
            it->second = newScheme; // Stomp the user's existing scheme with the one we just got (to make sure the right Origin is set)
            userSettings.fixupsAppliedDuringLoad = true; // Make sure we save the settings.
            if (!existingScheme->IsEquivalentForSettingsMergePurposes(newScheme))
            {
                auto newName = til::hstring_format(FMT_COMPILE(L"{} (modified)"), existingScheme->Name());
                int differentiator = 2;
                while (userSettings.colorSchemes.contains(newName))
                {
                    newName = til::hstring_format(FMT_COMPILE(L"{} (modified {})"), existingScheme->Name(), differentiator++);
                }
                // Rename the user's scheme.
                existingScheme->Name(newName);
                userSettings.colorSchemeRemappings.emplace(newScheme->Name(), newName);
                // And re-add it to the end.
                userSettings.colorSchemes.emplace(newName, std::move(existingScheme));
                return true;
            }
        }
        return false;
    }
    return true;
}

// As the name implies it executes a generator.
// Generated profiles are added to .inboxSettings. Used by GenerateProfiles().
void SettingsLoader::_executeGenerator(const IDynamicProfileGenerator& generator, std::vector<winrt::com_ptr<implementation::Profile>>& profilesList)
{
    const auto generatorNamespace = generator.GetNamespace();
    const auto previousSize = profilesList.size();
    try
    {
        generator.GenerateProfiles(profilesList);
    }
    CATCH_LOG_MSG("Dynamic Profile Namespace: \"%.*s\"", gsl::narrow<int>(generatorNamespace.size()), generatorNamespace.data())

    // If the generator produced some profiles we're going to give them default attributes.
    // By setting the Origin/Source/etc. here, we deduplicate some code and ensure they aren't missing accidentally.
    if (profilesList.size() > previousSize)
    {
        const winrt::hstring source{ generatorNamespace };

        for (const auto& profile : std::span(profilesList).subspan(previousSize))
        {
            profile->Origin(OriginTag::Generated);
            profile->Source(source);
        }
    }
}

winrt::com_ptr<ExtensionPackage> SettingsLoader::_registerFragment(const winrt::Microsoft::Terminal::Settings::Model::FragmentSettings& fragment, FragmentScope scope)
{
    winrt::com_ptr<ExtensionPackage> extPkg{ nullptr };
    const auto src = fragment.Source();
    const auto found = extensionPackageMap.find(src);
    if (found != extensionPackageMap.end())
    {
        // retrieve from extensionPackageMap
        extPkg = found->second;
    }
    else
    {
        // create a new entry in extensionPackageMap
        const auto em = extensionPackageMap.emplace(src, winrt::make_self<ExtensionPackage>(src, scope));
        extPkg = em.first->second;
    }
    extPkg->Fragments().Append(fragment);
    return extPkg;
}

// Method Description:
// - Creates a CascadiaSettings from whatever's saved on disk, or instantiates
//      a new one with the default values. If we're running as a packaged app,
//      it will load the settings from our packaged localappdata. If we're
//      running as an unpackaged application, it will read it from the path
//      we've set under localappdata.
// - Loads both the settings from the defaults.json and the user's settings.json
// - Also runs and dynamic profile generators. If any of those generators create
//   new profiles, we'll write the user settings back to the file, with the new
//   profiles inserted into their list of profiles.
// Return Value:
// - a unique_ptr containing a new CascadiaSettings object.
Model::CascadiaSettings CascadiaSettings::LoadAll()
try
{
    FILETIME lastWriteTime{};
    auto settingsString = til::io::read_file_as_utf8_string_if_exists(_settingsPath(), false, &lastWriteTime);
    auto firstTimeSetup = settingsString.empty();

    // If it's the firstTimeSetup and a preview build, then try to
    // read settings.json from the Release stable file path if it exists.
    // Otherwise use default settings file provided from original settings file
    bool releaseSettingExists = false;
    if (firstTimeSetup && !IsPortableMode())
    {
#if defined(WT_BRANDING_PREVIEW) || defined(WT_BRANDING_CANARY)
        {
            try
            {
                settingsString = til::io::read_file_as_utf8_string_if_exists(_releaseSettingsPath());
                releaseSettingExists = settingsString.empty() ? false : true;
            }
            catch (...)
            {
            }
        }
#endif
    }

    // GH#11119: If we find that the settings file doesn't exist, or is empty,
    // then let's quick delete the state file as well. If the user does have a
    // state file, and not a settings, then they probably tried to reset their
    // settings. It might have data in it that was only relevant for a previous
    // iteration of the settings file. If we don't, we'll load the old state and
    // ignore all dynamic profiles (for example)!
    if (firstTimeSetup)
    {
        ApplicationState::SharedInstance().Reset();
    }

    // Only uses default settings when firstTimeSetup is true and releaseSettingExists is false
    // Otherwise use existing settingsString
    const auto settingsStringView = (firstTimeSetup && !releaseSettingExists) ? LoadStringResource(IDR_USER_DEFAULTS) : settingsString;
    auto mustWriteToDisk = firstTimeSetup;

    SettingsLoader loader{ settingsStringView, LoadStringResource(IDR_DEFAULTS) };

    winrt::hstring baseUserSettingsPath{ GetBaseSettingsPath().native() };
    loader.userSettings.baseLayerProfile->SourceBasePath = baseUserSettingsPath;
    loader.userSettings.globals->SourceBasePath = baseUserSettingsPath;
    for (auto&& userProfile : loader.userSettings.profiles)
    {
        userProfile->SourceBasePath = baseUserSettingsPath;
    }

    // Generate dynamic profiles and add them as parents of user profiles.
    // That way the user profiles will get appropriate defaults from the generators (like icons and such).
    loader.GenerateProfiles();

    // ApplyRuntimeInitialSettings depends on generated profiles.
    // --> ApplyRuntimeInitialSettings must be called after GenerateProfiles.
    // Doesn't run when there is a Release settings.json that exists
    if (firstTimeSetup && !releaseSettingExists)
    {
        loader.ApplyRuntimeInitialSettings();
    }

    loader.MergeInboxIntoUserSettings();
    // Fragments might reference user profiles created by a generator.
    // --> FindFragmentsAndMergeIntoUserSettings must be called after MergeInboxIntoUserSettings.
    loader.FindFragmentsAndMergeIntoUserSettings(false /*generateExtensionPackages*/);
    loader.FinalizeLayering();

    // DisableDeletedProfiles returns true whenever we encountered any new generated/dynamic profiles.
    // Similarly FixupUserSettings returns true, when it encountered settings that were patched up.
    mustWriteToDisk |= loader.DisableDeletedProfiles();
    mustWriteToDisk |= loader.FixupUserSettings();
    mustWriteToDisk |= loader.AddDynamicProfileFolders();

    // If this throws, the app will catch it and use the default settings.
    const auto settings = winrt::make_self<CascadiaSettings>(std::move(loader));

    // If we created the file, or found new dynamic profiles, write the user
    // settings string back to the file.
    if (mustWriteToDisk)
    {
        settings->WriteSettingsToDisk();
    }
    else
    {
        // lastWriteTime is only valid if mustWriteToDisk is false.
        // Additionally WriteSettingsToDisk() updates the _hash for us already.
        settings->_hash = _calculateHash(settingsString, lastWriteTime);
    }

    settings->_researchOnLoad();

    return *settings;
}
catch (const SettingsException& ex)
{
    const auto settings{ winrt::make_self<CascadiaSettings>() };
    settings->_loadError = ex.Error();
    return *settings;
}
catch (const SettingsTypedDeserializationException& e)
{
    const auto settings{ winrt::make_self<CascadiaSettings>() };
    settings->_deserializationErrorMessage = til::u8u16(e.what());
    return *settings;
}

void CascadiaSettings::_researchOnLoad()
{
    // Only do this if we're actually being sampled
    if (TraceLoggingProviderEnabled(g_hSettingsModelProvider, 0, MICROSOFT_KEYWORD_MEASURES))
    {
        // ----------------------------- RE: Themes ----------------------------
        const auto numThemes = GlobalSettings().Themes().Size();
        const auto themeInUse = GlobalSettings().CurrentTheme().Name();
        const auto changedTheme = GlobalSettings().HasTheme();

        // system: 0
        // light: 1
        // dark: 2
        // a custom theme: 3
        // system (legacy): 4
        // light (legacy): 5
        // dark (legacy): 6
        const auto themeChoice = themeInUse == L"system"       ? 0 :
                                 themeInUse == L"light"        ? 1 :
                                 themeInUse == L"dark"         ? 2 :
                                 themeInUse == L"legacyDark"   ? 4 :
                                 themeInUse == L"legacyLight"  ? 5 :
                                 themeInUse == L"legacySystem" ? 6 :
                                                                 3;

        TraceLoggingWrite(
            g_hSettingsModelProvider,
            "ThemesInUse",
            TraceLoggingDescription("Data about the themes in use"),
            TraceLoggingInt32(themeChoice, "ThemeClass", "Identifier for the theme chosen. 0 is system (legacySystem = 6), 1 is light (legacyLight = 5), 2 is dark (legacyDark = 4), and 3 indicates any custom theme."),
            TraceLoggingBool(changedTheme, "ChangedTheme", "True if the user actually changed the theme from the default theme"),
            TraceLoggingInt32(numThemes, "NumberOfThemes", "Number of themes in the user's settings"),
            TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
            TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));

        // --------------------------- RE: sendInput ---------------------------
        auto collectSendInput = [&]() {
            auto totalSendInput = 0;
            const auto& allActions = GlobalSettings().ActionMap().AvailableActions();
            for (const auto&& [name, actionAndArgs] : allActions)
            {
                if (actionAndArgs.Action() == ShortcutAction::SendInput)
                {
                    totalSendInput++;
                }
            }
            return totalSendInput;
        };

        TraceLoggingWrite(
            g_hSettingsModelProvider,
            "SendInputUsage",
            TraceLoggingDescription("Event emitted upon settings load, containing the number of sendInput actions a user has"),
            TraceLoggingInt32(collectSendInput(), "NumberOfSendInputActions", "Number of sendInput actions in the user's settings"),
            TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
            TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));

        // ------------------------ RE: autoMarkPrompts ------------------------
        auto totalAutoMark = 0;
        auto totalShowMarks = 0;
        for (const auto&& p : AllProfiles())
        {
            totalAutoMark += p.AutoMarkPrompts() ? 1 : 0;
            totalShowMarks += p.ShowMarks() ? 1 : 0;
        }

        TraceLoggingWrite(
            g_hSettingsModelProvider,
            "MarksProfilesUsage",
            TraceLoggingDescription("Event emitted upon settings load, containing the number of profiles opted-in to scrollbar marks"),
            TraceLoggingInt32(totalAutoMark, "NumberOfAutoMarkPromptsProfiles", "Number of profiles for which AutoMarkPrompts is enabled"),
            TraceLoggingInt32(totalShowMarks, "NumberOfShowMarksProfiles", "Number of profiles for which ShowMarks is enabled"),
            TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
            TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
    }
}

// Function Description:
// - Creates a new CascadiaSettings object initialized with settings from the
//   hard-coded defaults.json.
// Arguments:
// - <none>
// Return Value:
// - a unique_ptr to a CascadiaSettings with the settings from defaults.json
Model::CascadiaSettings CascadiaSettings::LoadDefaults()
{
    return *winrt::make_self<CascadiaSettings>(std::string_view{}, LoadStringResource(IDR_DEFAULTS));
}

CascadiaSettings::CascadiaSettings(const winrt::hstring& userJSON, const winrt::hstring& inboxJSON) :
    CascadiaSettings{ SettingsLoader::Default(til::u16u8(userJSON), til::u16u8(inboxJSON)) }
{
}

CascadiaSettings::CascadiaSettings(const std::string_view& userJSON, const std::string_view& inboxJSON) :
    CascadiaSettings{ SettingsLoader::Default(userJSON, inboxJSON) }
{
}

CascadiaSettings::CascadiaSettings(SettingsLoader&& loader) :
    // The CascadiaSettings class declaration initializes these fields by default,
    // but we're going to set these fields in our constructor later on anyways.
    _globals{},
    _baseLayerProfile{},
    _allProfiles{},
    _activeProfiles{},
    _warnings{}
{
    std::vector<Model::Profile> allProfiles;
    std::vector<Model::Profile> activeProfiles;
    std::vector<Model::SettingsLoadWarnings> warnings;

    allProfiles.reserve(loader.userSettings.profiles.size());
    activeProfiles.reserve(loader.userSettings.profiles.size());

    for (const auto& colorScheme : loader.userSettings.colorSchemes)
    {
        loader.userSettings.globals->AddColorScheme(*colorScheme.second);
    }

    // SettingsLoader and ParsedSettings are supposed to always
    // create these two members. We don't want null-pointer exceptions.
    assert(loader.userSettings.globals != nullptr);
    assert(loader.userSettings.baseLayerProfile != nullptr);

    for (const auto& profile : loader.userSettings.profiles)
    {
        // If a generator stops producing a certain profile (e.g. WSL or PowerShell were removed) or
        // a profile from a fragment doesn't exist anymore, we should also stop including the
        // matching user's profile in _allProfiles (since they aren't functional anyways).
        //
        // A user profile has a valid, dynamic parent if it has a parent with identical source.
        if (const auto source = profile->Source(); !source.empty())
        {
            const auto& parents = profile->Parents();
            if (std::none_of(parents.begin(), parents.end(), [&](const auto& parent) { return parent->Source() == source; }))
            {
                profile->Orphaned(true);
            }
        }

        allProfiles.emplace_back(*profile);
        if (!profile->Hidden() && !profile->Orphaned())
        {
            activeProfiles.emplace_back(*profile);
        }
    }

    if (allProfiles.empty())
    {
        throw SettingsException(SettingsLoadErrors::NoProfiles);
    }
    if (activeProfiles.empty())
    {
        throw SettingsException(SettingsLoadErrors::AllProfilesHidden);
    }

    if (loader.duplicateProfile)
    {
        warnings.emplace_back(Model::SettingsLoadWarnings::DuplicateProfile);
    }

    _globals = loader.userSettings.globals;
    _baseLayerProfile = loader.userSettings.baseLayerProfile;
    _allProfiles = winrt::single_threaded_observable_vector(std::move(allProfiles));
    _activeProfiles = winrt::single_threaded_observable_vector(std::move(activeProfiles));
    _warnings = winrt::single_threaded_vector(std::move(warnings));
    _themesChangeLog = std::move(loader.userSettings.themesChangeLog);

    _resolveDefaultProfile();
    _resolveNewTabMenuProfiles();
    _validateSettings();

    ExpandCommands();
}

// Method Description:
// - Returns the path of the settings.json file.
// Arguments:
// - <none>
// Return Value:
// - Returns a path in 80% of cases. I measured!
const std::filesystem::path& CascadiaSettings::_settingsPath()
{
    static const auto path = GetBaseSettingsPath() / SettingsFilename;
    return path;
}

// Method Description:
// - Returns the path of the settings.json file from stable file path
// Arguments:
// - <none>
// Return Value:
// - Path to stable settings
const std::filesystem::path& CascadiaSettings::_releaseSettingsPath()
{
    static const auto path = GetReleaseSettingsPath() / SettingsFilename;
    return path;
}

// Returns a has (approximately) uniquely identifying the settings.json contents on disk.
winrt::hstring CascadiaSettings::_calculateHash(std::string_view settings, const FILETIME& lastWriteTime)
{
    const auto fileHash = til::hash(settings);
    const ULARGE_INTEGER fileTime{ lastWriteTime.dwLowDateTime, lastWriteTime.dwHighDateTime };
    const auto hash = fmt::format(FMT_COMPILE(L"{:016x}-{:016x}"), fileHash, fileTime.QuadPart);
    return winrt::hstring{ hash };
}

// This returns something akin to %LOCALAPPDATA%\Packages\WindowsTerminalDev_8wekyb3d8bbwe\LocalState
// just like SettingsPath(), but without the trailing \settings.json.
winrt::hstring CascadiaSettings::SettingsDirectory()
{
    return winrt::hstring{ GetBaseSettingsPath().native() };
}

// function Description:
// - Returns the full path to the settings file, either within the application
//   package, or in its unpackaged location. This path is under the "Local
//   AppData" folder, so it _doesn't_ roam to other machines.
// - If the application is unpackaged,
//   the file will end up under e.g. C:\Users\admin\AppData\Local\Microsoft\Windows Terminal\settings.json
// Arguments:
// - <none>
// Return Value:
// - the full path to the settings file
winrt::hstring CascadiaSettings::SettingsPath()
{
    return winrt::hstring{ _settingsPath().native() };
}

bool CascadiaSettings::IsPortableMode()
{
    return Model::IsPortableMode();
}

winrt::hstring CascadiaSettings::DefaultSettingsPath()
{
    // Both of these posts suggest getting the path to the exe, then removing
    // the exe's name to get the package root:
    // * https://blogs.msdn.microsoft.com/appconsult/2017/06/23/accessing-to-the-files-in-the-installation-folder-in-a-desktop-bridge-application/
    // * https://blogs.msdn.microsoft.com/appconsult/2017/03/06/handling-data-in-a-converted-desktop-app-with-the-desktop-bridge/
    //
    // This would break if we ever moved our exe out of the package root.
    // HOWEVER, if we try to look for a defaults.json that's simply in the same
    // directory as the exe, that will work for unpackaged scenarios as well. So
    // let's try that.

    const auto exePathString = wil::GetModuleFileNameW<std::wstring>(nullptr);

    std::filesystem::path path{ exePathString };
    path.replace_filename(DefaultsFilename);

    return winrt::hstring{ path.native() };
}

void CascadiaSettings::ResetApplicationState() const
{
    auto state = ApplicationState::SharedInstance();
    const auto hash = state.SettingsHash();
    state.Reset();
    state.SettingsHash(hash);
    state.Flush();
}

void CascadiaSettings::ResetToDefaultSettings()
{
    ApplicationState::SharedInstance().Reset();
    _writeSettingsToDisk(LoadStringResource(IDR_USER_DEFAULTS));
}

// Method Description:
// - Write the current state of CascadiaSettings to our settings file
// - Create a backup file with the current contents, if one does not exist
// - Persists the default terminal handler choice to the registry
// Arguments:
// - <none>
// Return Value:
// - <none>
bool CascadiaSettings::WriteSettingsToDisk()
{
    // write current settings to current settings file
    Json::StreamWriterBuilder wbuilder;
    wbuilder.settings_["enableYAMLCompatibility"] = true; // suppress spaces around colons
    wbuilder.settings_["indentation"] = "    ";
    wbuilder.settings_["precision"] = 6; // prevent values like 1.1000000000000001

    try
    {
        _writeSettingsToDisk(Json::writeString(wbuilder, ToJson()));
    }
    catch (...)
    {
        LOG_CAUGHT_EXCEPTION();
        _warnings.Append(SettingsLoadWarnings::FailedToWriteToSettings);
        return false;
    }
    return true;
}

void CascadiaSettings::_writeSettingsToDisk(std::string_view contents)
{
    FILETIME lastWriteTime{};
    til::io::write_utf8_string_to_file_atomic(_settingsPath(), contents, &lastWriteTime);

    _hash = _calculateHash(contents, lastWriteTime);

    // Persists the default terminal choice
    // GH#10003 - Only do this if _currentDefaultTerminal was actually initialized.
    if (_currentDefaultTerminal)
    {
        DefaultTerminal::Current(_currentDefaultTerminal);
    }
}

#ifndef NDEBUG
[[maybe_unused]] static std::string _getDevPathToSchema()
{
    std::filesystem::path filePath{ __FILE__ };
    auto schemaPath = filePath.parent_path().parent_path().parent_path().parent_path() / "doc" / "cascadia" / "profiles.schema.json";
    return "file:///" + schemaPath.generic_string();
}
#endif

// Method Description:
// - Create a new serialized JsonObject from an instance of this class
// Arguments:
// - <none>
// Return Value:
// the JsonObject representing this instance
Json::Value CascadiaSettings::ToJson() const
{
    // top-level json object
    auto json{ _globals->ToJson() };
    json["$help"] = "https://aka.ms/terminal-documentation";
    json["$schema"] =
#if defined(WT_BRANDING_RELEASE)
        "https://aka.ms/terminal-profiles-schema"
#elif defined(WT_BRANDING_PREVIEW)
        "https://aka.ms/terminal-profiles-schema-preview"
#elif !defined(NDEBUG) // DEBUG mode
        _getDevPathToSchema() // magic schema path that refers to the local source directory
#else // All other brandings
        "https://raw.githubusercontent.com/microsoft/terminal/main/doc/cascadia/profiles.schema.json"
#endif
        ;

    // "profiles" will always be serialized as an object
    Json::Value profiles{ Json::ValueType::objectValue };
    profiles[JsonKey(DefaultSettingsKey)] = _baseLayerProfile ? _baseLayerProfile->ToJson() : Json::ValueType::objectValue;
    Json::Value profilesList{ Json::ValueType::arrayValue };
    for (const auto& entry : _allProfiles)
    {
        if (!entry.Deleted())
        {
            const auto prof{ winrt::get_self<Profile>(entry) };
            profilesList.append(prof->ToJson());
        }
    }
    profiles[JsonKey(ProfilesListKey)] = profilesList;
    json[JsonKey(ProfilesKey)] = profiles;

    Json::Value schemes{ Json::ValueType::arrayValue };
    for (const auto& entry : _globals->ColorSchemes())
    {
        const auto scheme{ winrt::get_self<ColorScheme>(entry.Value()) };
        if (scheme->Origin() == OriginTag::User)
        {
            schemes.append(scheme->ToJson());
        }
    }
    json[JsonKey(SchemesKey)] = schemes;

    Json::Value themes{ Json::ValueType::arrayValue };
    for (const auto& entry : _globals->Themes())
    {
        // Ignore the built in themes, when serializing the themes back out. We
        // don't want to re-include them in the user settings file.
        const auto theme{ winrt::get_self<Theme>(entry.Value()) };
        const auto& name{ theme->Name() };
        if (std::ranges::find(builtinThemes, name) != builtinThemes.end())
        {
            continue;
        }
        themes.append(theme->ToJson());
    }
    json[JsonKey(ThemesKey)] = themes;

    return json;
}

// Method Description:
// - Resolves the "defaultProfile", which can be a profile name, to a GUID
//   and stores it back to the globals.
void CascadiaSettings::_resolveDefaultProfile() const
{
    if (const auto unparsedDefaultProfile = _globals->UnparsedDefaultProfile(); !unparsedDefaultProfile.empty())
    {
        if (const auto profile = GetProfileByName(unparsedDefaultProfile))
        {
            _globals->DefaultProfile(profile.Guid());
            return;
        }

        _warnings.Append(SettingsLoadWarnings::MissingDefaultProfile);
    }

    // Use the first profile as the new default.
    GlobalSettings().DefaultProfile(_allProfiles.GetAt(0).Guid());
}

// Method Description:
// - Iterates through the "newTabMenu" entries and for ProfileEntries resolves the "profile"
//   fields, which can be a profile name, to a GUID and stores it back.
// - It finds any "source" entries and finds all profiles generated by that source
// - Lastly, it finds any "remainingProfiles" entries and stores which profiles they
//   represent (those that were not resolved before). It adds a warning when
//   multiple of these entries are found.
void CascadiaSettings::_resolveNewTabMenuProfiles() const
{
    Model::RemainingProfilesEntry remainingProfilesEntry = nullptr;

    // The TerminalPage needs to know which profile has which profile ID. To prevent
    // continuous lookups in the _activeProfiles vector, we create a map <int, Profile>
    // to store these indices in-flight.
    auto remainingProfilesMap = std::map<int, Model::Profile>{};
    auto activeProfileCount = gsl::narrow_cast<int>(_activeProfiles.Size());
    for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++)
    {
        remainingProfilesMap.emplace(profileIndex, _activeProfiles.GetAt(profileIndex));
    }

    // We keep track of the "remaining profiles" - those that have not yet been resolved
    // in either a "profile" or "source" entry. They will possibly be assigned to a
    // "remainingProfiles" entry
    auto remainingProfiles = single_threaded_map(std::move(remainingProfilesMap));

    // We call a recursive helper function to process the entries
    auto entries = _globals->NewTabMenu();
    _resolveNewTabMenuProfilesSet(entries, remainingProfiles, remainingProfilesEntry);

    // If a "remainingProfiles" entry has been found, assign to it the remaining profiles
    if (remainingProfilesEntry != nullptr)
    {
        remainingProfilesEntry.Profiles(remainingProfiles);
    }
}

// Method Description:
// - Helper function that processes a set of tab menu entries and resolves any profile names
//   or source fields as necessary - see function above for a more detailed explanation.
void CascadiaSettings::_resolveNewTabMenuProfilesSet(const IVector<Model::NewTabMenuEntry> entries, IMap<int, Model::Profile>& remainingProfilesMap, Model::RemainingProfilesEntry& remainingProfilesEntry) const
{
    if (entries == nullptr || entries.Size() == 0)
    {
        return;
    }

    for (const auto& entry : entries)
    {
        if (entry == nullptr)
        {
            continue;
        }

        switch (entry.Type())
        {
        // For a simple profile entry, the "profile" field can either be a name or a GUID. We
        // use the GetProfileByName function to resolve this name to a profile instance, then
        // find the index of that profile, and store this information in the entry.
        case NewTabMenuEntryType::Profile:
        {
            // We need to access the unresolved profile name, a field that is not exposed
            // in the projected class. So, we need to first obtain our implementation struct
            // instance, to access this field.
            const auto profileEntry{ winrt::get_self<implementation::ProfileEntry>(entry.as<Model::ProfileEntry>()) };

            // Find the profile by name
            const auto profile = GetProfileByName(profileEntry->ProfileName());

            // If not found, or if the profile is hidden, skip it
            if (profile == nullptr || profile.Hidden())
            {
                profileEntry->Profile(nullptr); // override "default" profile
                break;
            }

            // Find the index of the resulting profile and store the result in the entry
            uint32_t profileIndex;
            _activeProfiles.IndexOf(profile, profileIndex);

            profileEntry->Profile(profile);
            profileEntry->ProfileIndex(profileIndex);

            // Remove from remaining profiles list (map)
            remainingProfilesMap.TryRemove(profileIndex);

            break;
        }

        // For a remainingProfiles entry, we store it in the variable that is passed back to our caller,
        // except when that one has already been set (so we found a second/third/...) instance, which will
        // trigger a warning. We then ignore this entry.
        case NewTabMenuEntryType::RemainingProfiles:
        {
            if (remainingProfilesEntry != nullptr)
            {
                _warnings.Append(SettingsLoadWarnings::DuplicateRemainingProfilesEntry);
            }
            else
            {
                remainingProfilesEntry = entry.as<Model::RemainingProfilesEntry>();
            }
            break;
        }

        // For a folder, we simply call this method recursively
        case NewTabMenuEntryType::Folder:
        {
            // We need to access the unfiltered entry list, a field that is not exposed
            // in the projected class. So, we need to first obtain our implementation struct
            // instance, to access this field.
            const auto folderEntry{ winrt::get_self<implementation::FolderEntry>(entry.as<Model::FolderEntry>()) };

            auto folderEntries = folderEntry->RawEntries();
            _resolveNewTabMenuProfilesSet(folderEntries, remainingProfilesMap, remainingProfilesEntry);
            break;
        }

        // For a "matchProfiles" entry, we iterate through the list of all profiles and
        // find all those matching: generated by the same source, having the same name, or
        // having the same commandline. This can be expanded with regex support in the future.
        // We make sure that none of the matches are included in the "remaining profiles" section.
        case NewTabMenuEntryType::MatchProfiles:
        {
            // We need to access the matching function, which is not exposed in the projected class.
            // So, we need to first obtain our implementation struct instance, to access this field.
            const auto matchEntry{ winrt::get_self<implementation::MatchProfilesEntry>(entry.as<Model::MatchProfilesEntry>()) };

            matchEntry->Profiles(single_threaded_map<int, Model::Profile>());

            auto activeProfileCount = gsl::narrow_cast<int>(_activeProfiles.Size());
            for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++)
            {
                const auto profile = _activeProfiles.GetAt(profileIndex);

                // On a match, we store it in the entry and remove it from the remaining list
                if (matchEntry->MatchesProfile(profile))
                {
                    matchEntry->Profiles().Insert(profileIndex, profile);
                    remainingProfilesMap.TryRemove(profileIndex);
                }
            }

            break;
        }
        }
    }
}

void CascadiaSettings::LogSettingChanges(bool isJsonLoad) const
{
#ifndef _DEBUG
    // Only do this if we're actually being sampled
    if (!TraceLoggingProviderEnabled(g_hSettingsModelProvider, 0, MICROSOFT_KEYWORD_MEASURES))
    {
        return;
    }
#endif // !_DEBUG

    // aggregate setting changes
    std::set<std::string> changes;
    static constexpr std::string_view globalContext{ "global" };
    _globals->LogSettingChanges(changes, globalContext);

    // Actions are not expected to change when loaded from the settings UI
    static constexpr std::string_view actionContext{ "action" };
    winrt::get_self<implementation::ActionMap>(_globals->ActionMap())->LogSettingChanges(changes, actionContext);

    static constexpr std::string_view profileContext{ "profile" };
    for (const auto& profile : _allProfiles)
    {
        winrt::get_self<Profile>(profile)->LogSettingChanges(changes, profileContext);
    }

    static constexpr std::string_view profileDefaultsContext{ "profileDefaults" };
    _baseLayerProfile->LogSettingChanges(changes, profileDefaultsContext);

    // Themes are not expected to change when loaded from the settings UI
    // DO NOT CALL Theme::LogSettingChanges!!
    // We already collected the changes when we loaded the JSON
    for (const auto& change : _themesChangeLog)
    {
        changes.insert(change);
    }

#if defined(WT_BRANDING_RELEASE)
    constexpr uint8_t branding = 3;
#elif defined(WT_BRANDING_PREVIEW)
    constexpr uint8_t branding = 2;
#elif defined(WT_BRANDING_CANARY)
    constexpr uint8_t branding = 1;
#else
    constexpr uint8_t branding = 0;
#endif
    const uint8_t distribution = IsPackaged()     ? 2 :
                                 IsPortableMode() ? 1 :
                                                    0;

    // report changes
    for (const auto& change : changes)
    {
        // A `isJsonLoad ? "JsonSettingsChanged" : "UISettingsChanged"`
        //   would be nice, but that apparently isn't allowed in the macro below.
        // Also, there's guidance to not send too much data all in one event,
        //   so we'll be sending a ton of events here.
        if (isJsonLoad)
        {
            TraceLoggingWrite(g_hSettingsModelProvider,
                              "JsonSettingsChanged",
                              TraceLoggingDescription("Event emitted when settings.json change"),
                              TraceLoggingValue(change.data(), "Setting"),
                              TraceLoggingValue(branding, "Branding"),
                              TraceLoggingValue(distribution, "Distribution"),
                              TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
                              TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
        }
        else
        {
            TraceLoggingWrite(g_hSettingsModelProvider,
                              "UISettingsChanged",
                              TraceLoggingDescription("Event emitted when settings change via the UI"),
                              TraceLoggingValue(change.data(), "Setting"),
                              TraceLoggingValue(branding, "Branding"),
                              TraceLoggingValue(distribution, "Distribution"),
                              TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
                              TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
        }
    }
}
