// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under    *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0          *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************

#include "application.h"
#include <memory>
#include <zen/file_access.h>
#include <zen/json.h>
#include <zen/shutdown.h>
#include <zen/process_exec.h>
#include <zen/resolve_path.h>
#include <zen/sys_info.h>
#include <wx/clipbrd.h>
#include <wx/tooltip.h>
#include <wx+/darkmode.h>
#include <wx+/popup_dlg.h>
#include <wx+/image_resources.h>
#include "afs/concrete.h"
#include "base/comparison.h"
#include "base/synchronization.h"
#include "ui/batch_status_handler.h"
#include "ui/main_dlg.h"
#include "ui/small_dlgs.h"
#include "base_tools.h"
#include "ffs_paths.h"
#include "return_codes.h"

    #include <gtk/gtk.h>

using namespace zen;
using namespace fff;


#ifdef __WXGTK3__
    /* Wayland backend used by GTK3 does not allow to move windows!

    "I'd really like to know if there is some deep technical reason for it or
    if this is really as bloody stupid as it seems?" - vadz  https://github.com/wxWidgets/wxWidgets/issues/18733#issuecomment-1011235902

    Show all available GTK backends: run FreeFileSync with env variable:    GDK_BACKEND=help

    => workaround: https://docs.gtk.org/gdk3/func.set_allowed_backends.html           */
    GLOBAL_RUN_ONCE(::gdk_set_allowed_backends("x11,*")); //call *before* gtk_init()
#endif

IMPLEMENT_APP(Application)


namespace
{
std::vector<Zstring> getCommandlineArgs(const wxApp& app)
{
    std::vector<Zstring> args;
    for (const wxString& arg : app.argv.GetArguments())
        args.push_back(utfTo<Zstring>(arg));
    //remove first argument which is exe path by convention: https://devblogs.microsoft.com/oldnewthing/20060515-07/?p=31203
    if (!args.empty())
        args.erase(args.begin());

    return args;
}


void showSyntaxHelp()
{
    showNotificationDialog(nullptr, DialogInfoType::info, PopupDialogCfg().
                           setTitle(_("Command line")).
                           setDetailInstructions(_("Syntax:") + L"\n\n" +
                                                 L"FreeFileSync" + L'\n' +
                                                 TAB_SPACE + L"[" + _("config files:") + L" *.ffs_gui/*.ffs_batch]" + L'\n' +
                                                 TAB_SPACE + L"[-DirPair " + _("directory") + L' ' + _("directory") + L"]" L"\n" +
                                                 TAB_SPACE + L"[-Edit]" + L'\n' +
                                                 TAB_SPACE + L"[" + _("global config file:") + L" GlobalSettings.xml]" + L"\n\n" +

                                                 _("config files:") + L'\n' +
                                                 _("Any number of FreeFileSync \"ffs_gui\" and/or \"ffs_batch\" configuration files.") + L"\n\n" +

                                                 L"-DirPair " + _("directory") + L' ' + _("directory") + L'\n' +
                                                 _("Any number of alternative directory pairs for at most one config file.") + L"\n\n" +

                                                 L"-Edit" + L'\n' +
                                                 _("Open the selected configuration for editing only, without executing it.") + L"\n\n" +

                                                 _("global config file:") + L'\n' +
                                                 _("Path to an alternate GlobalSettings.xml file.")));
}


void notifyAppError(const std::wstring& msg)
{
        std::cerr << utfTo<std::string>(_("Error") + L": " + msg) << '\n';
    //alternative0: std::wcerr: cannot display non-ASCII at all, so why does it exist???
    //alternative1: wxSafeShowMessage => NO console output on Debian x86, WTF!
    //alternative2: wxMessageBox() => works, but we probably shouldn't block during command line usage
}
}

//##################################################################################################################

bool Application::OnInit()
{
    //do not call wxApp::OnInit() to avoid using wxWidgets command line parser

    const auto now = std::chrono::system_clock::now(); //e.g. "ErrorLog 2023-07-05 105207.073.xml"
    initExtraLog([logFilePath = appendPath(getConfigDirPath(), Zstr("ErrorLog ") +
                                           formatTime(Zstr("%Y-%m-%d %H%M%S"), getLocalTime(std::chrono::system_clock::to_time_t(now))) + Zstr('.') +
                                           printNumber<Zstring>(Zstr("%03d"), //[ms] should yield a fairly unique name
                                                                static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count() % 1000)) +
                                           Zstr(".xml"))](const ErrorLog& log)
    {
        try //don't call functions depending on global state (which might be destroyed already!)
        {
            saveErrorLog(log, logFilePath); //throw FileError
        }
        catch (const FileError& e) { assert(false); notifyAppError(e.toString()); }
    });

    //tentatively set program language to OS default until GlobalSettings.xml is read later
    try { localizationInit(appendPath(getResourceDirPath(), Zstr("Languages.zip"))); } //throw FileError
    catch (const FileError& e) { logExtraError(e.toString()); }

    //parallel xBRZ-scaling! => run as early as possible
    try { imageResourcesInit(appendPath(getResourceDirPath(), Zstr("Icons.zip"))); }
    catch (const FileError& e) { logExtraError(e.toString()); } //not critical in this context

    //GTK should already have been initialized by wxWidgets (see \src\gtk\app.cpp:wxApp::Initialize)
    auto loadCSS = [&](const char* fileName)
    {
        GtkCssProvider* provider = ::gtk_css_provider_new();
        ZEN_ON_SCOPE_EXIT(::g_object_unref(provider));

        GError* error = nullptr;
        ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error));

        ::gtk_css_provider_load_from_path(provider, //GtkCssProvider* css_provider
                                          appendPath(getResourceDirPath(), fileName).c_str(), //const gchar* path
                                          &error); //GError** error
        if (error)
            throw SysError(formatGlibError("gtk_css_provider_load_from_path", error));

        ::gtk_style_context_add_provider_for_screen(::gdk_screen_get_default(),               //GdkScreen* screen
                                                    GTK_STYLE_PROVIDER(provider),             //GtkStyleProvider* provider
                                                    GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); //guint priority
    };
    try
    {
        loadCSS("Gtk3Styles.css"); //throw SysError
    }
    catch (const SysError& e)
    {
        std::cerr << "[FreeFileSync] " + utfTo<std::string>(e.toString()) + "\n" "Loading GTK3\'s old CSS format instead..." "\n";
        try
        {
            loadCSS("Gtk3Styles.old.css"); //throw SysError
        }
        catch (const SysError& e2) { logExtraError(_("Failed to update the color theme.") + L"\n\n" + e2.toString()); }
    }

    /* we're a GUI app: ignore SIGHUP when the parent terminal quits! (or process is killed!)
        => the FFS launcher will still be killed => fine
        => macOS: apparently not needed! interestingly the FFS launcher does receive SIGHUP and *is* killed  */
    if (sighandler_t oldHandler = ::signal(SIGHUP, SIG_IGN);
        oldHandler == SIG_ERR)
        logExtraError(_("Error during process initialization.") + L"\n\n" + formatSystemError("signal(SIGHUP)", getLastError()));
    else assert(!oldHandler);


    //Windows User Experience Interaction Guidelines: tool tips should have 5s timeout, info tips no timeout => compromise:
    wxToolTip::Enable(true); //wxWidgets screw-up: wxToolTip::SetAutoPop is no-op if global tooltip window is not yet constructed: wxToolTip::Enable creates it
    wxToolTip::SetAutoPop(15'000); //https://docs.microsoft.com/en-us/windows/win32/uxguide/ctrl-tooltips-and-infotips

    SetAppName(L"FreeFileSync"); //if not set, defaults to executable name


    initAfs({getResourceDirPath(), getConfigDirPath()}); //bonus: using FTP Gdrive implicitly inits OpenSSL (used in runSanityChecks() on Linux) already during globals init


    auto onSystemShutdown = [](int /*unused*/ = 0)
    {
        onSystemShutdownRunTasks();

        //- it's futile to try and clean up while the process is in full swing (CRASH!) => just terminate!
        //- system sends close events to all open dialogs: If one of these calls wxCloseEvent::Veto(),
        //  e.g. user clicking cancel on save prompt, this would cancel the shutdown
        terminateProcess(static_cast<int>(FfsExitCode::cancelled));
    };
    Bind(wxEVT_QUERY_END_SESSION, [onSystemShutdown](wxCloseEvent& event) { onSystemShutdown(); }); //can veto
    Bind(wxEVT_END_SESSION,       [onSystemShutdown](wxCloseEvent& event) { onSystemShutdown(); }); //can *not* veto
    //- log off: Windows/macOS generates wxEVT_QUERY_END_SESSION/wxEVT_END_SESSION
    //           Linux/macOS generates SIGTERM, which we handle below
    //- Windows sends WM_QUERYENDSESSION, WM_ENDSESSION during log off, *not* WM_CLOSE https://devblogs.microsoft.com/oldnewthing/20080421-00/?p=22663
    //   => "taskkill sending WM_CLOSE (without /f)" is a misguided app simulating a button-click on X
    //      -> should send WM_QUERYENDSESSION instead!
    if (auto /*sighandler_t n.a. on macOS*/ oldHandler = ::signal(SIGTERM, onSystemShutdown);//"graceful" exit requested, unlike SIGKILL
        oldHandler == SIG_ERR)
        logExtraError(_("Error during process initialization.") + L"\n\n" + formatSystemError("signal(SIGTERM)", getLastError()));
    else assert(!oldHandler);

    //Note: app start is deferred: batch mode requires the wxApp eventhandler to be established for UI update events. This is not the case at the time of OnInit()!
    CallAfter([&] { onEnterEventLoop(); });

    return true; //true: continue processing; false: exit immediately
}


int Application::OnExit()
{
    [[maybe_unused]] const bool rv = wxClipboard::Get()->Flush(); //see wx+/context_menu.h
    //assert(rv); -> fails if clipboard wasn't used
    localizationCleanup();
    imageResourcesCleanup();
    teardownAfs();
    colorThemeCleanup();
    return wxApp::OnExit();
}


wxLayoutDirection Application::GetLayoutDirection() const { return languageLayoutIsRtl() ? wxLayout_RightToLeft : wxLayout_LeftToRight; }


int Application::OnRun()
{
#if wxUSE_EXCEPTIONS
#error why is wxWidgets uncaught exception handling enabled!?
#endif

    //exception => Windows: let it crash and create mini dump!!! Linux/macOS: std::exception::what() logged to console
        [[maybe_unused]] const int rc = wxApp::OnRun();
    return static_cast<int>(exitCode_);
}




void Application::onEnterEventLoop()
{
    const std::vector<Zstring>& commandArgs = getCommandlineArgs(*this);

    //wxWidgets app exit handling is weird... we want to exit only if the logical main window is closed, not just *any* window!
    wxTheApp->SetExitOnFrameDelete(false); //prevent popup-windows from becoming temporary top windows leading to program exit after closure
    ZEN_ON_SCOPE_EXIT(if (!wxTheApp->GetExitOnFrameDelete()) wxTheApp->ExitMainLoop()); //quit application, if no main window was set (batch silent mode)

    try
    {
        //parse command line arguments
        std::vector<std::pair<Zstring, Zstring>> dirPathPhrasePairs;
        std::vector<Zstring> cfgFilePaths;
        Zstring globalCfgPathAlt;
        bool openForEdit = false;
        {
            const char* optionEdit    = "-edit";
            const char* optionDirPair = "-dirpair";
            const char* optionSendTo  = "-sendto"; //remaining arguments are unspecified number of folder paths; wonky syntax; let's keep it undocumented

            auto isHelpRequest = [](const Zstring& arg)
            {
                auto it = std::find_if(arg.begin(), arg.end(), [](Zchar c) { return c != Zstr('/') && c != Zstr('-'); });
                if (it == arg.begin()) return false; //require at least one prefix character

                const Zstring argTmp(it, arg.end());
                return equalAsciiNoCase(argTmp, "help") ||
                       equalAsciiNoCase(argTmp, "h")    ||
                       argTmp == Zstr("?");
            };

            auto isCommandLineOption = [&](const Zstring& arg)
            {
                return equalAsciiNoCase(arg, optionEdit   ) ||
                       equalAsciiNoCase(arg, optionDirPair) ||
                       equalAsciiNoCase(arg, optionSendTo ) ||
                       isHelpRequest(arg);
            };

            for (auto it = commandArgs.begin(); it != commandArgs.end(); ++it)
                if (isHelpRequest(*it))
                    return showSyntaxHelp();
                else if (equalAsciiNoCase(*it, optionEdit))
                    openForEdit = true;
                else if (equalAsciiNoCase(*it, optionDirPair))
                {
                    if (++it == commandArgs.end() || isCommandLineOption(*it))
                        throw FileError(replaceCpy(_("A left and a right directory path are expected after %x."), L"%x", utfTo<std::wstring>(optionDirPair)));
                    dirPathPhrasePairs.emplace_back(*it, Zstring());

                    if (++it == commandArgs.end() || isCommandLineOption(*it))
                        throw FileError(replaceCpy(_("A left and a right directory path are expected after %x."), L"%x", utfTo<std::wstring>(optionDirPair)));
                    dirPathPhrasePairs.back().second = *it;
                }
                else if (equalAsciiNoCase(*it, optionSendTo))
                {
                    for (size_t i = 0; ; ++i)
                    {
                        if (++it == commandArgs.end() || isCommandLineOption(*it))
                        {
                            --it;
                            break;
                        }

                        if (i < 2) //else: -SendTo with more than 2 paths? Doesn't make any sense, does it!?
                        {
                            //for -SendTo we expect a list of full native paths, not "phrases" that need to be resolved!
                            auto getFolderPath = [](Zstring itemPath)
                            {
                                try
                                {
                                    if (getItemType(itemPath) == ItemType::file) //throw FileError
                                        if (const std::optional<Zstring>& parentPath = getParentFolderPath(itemPath))
                                            return *parentPath;
                                }
                                catch (FileError&) {}

                                return itemPath;
                            };

                            if (i % 2 == 0)
                                dirPathPhrasePairs.emplace_back(getFolderPath(*it), Zstring());
                            else
                            {
                                const Zstring folderPath = getFolderPath(*it);
                                if (dirPathPhrasePairs.back().first != folderPath) //else: user accidentally sending to two files, which each time yield the same parent folder
                                    dirPathPhrasePairs.back().second = folderPath;
                            }
                        }
                    }
                }
                else
                {
                    const Zstring& filePath = getResolvedFilePath(*it);
#if 0
                    if (!fileAvailable(filePath)) //...be a little tolerant
                        for (const Zchar* ext : {Zstr(".ffs_gui"), Zstr(".ffs_batch"), Zstr(".xml")})
                            if (fileAvailable(filePath + ext))
                                filePath += ext;
#endif
                    if (endsWithAsciiNoCase(filePath, Zstr(".ffs_gui")) ||
                        endsWithAsciiNoCase(filePath, Zstr(".ffs_batch")))
                        cfgFilePaths.push_back(filePath);
                    else if (endsWithAsciiNoCase(filePath, Zstr(".xml")))
                        globalCfgPathAlt = filePath;
                    else
                        throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(filePath)),
                                        _("Unexpected file extension:") + L' ' + fmtPath(getFileExtension(filePath)) + L'\n' +
                                        _("Expected:") + L" ffs_gui, ffs_batch, xml");
                }
        }
        //----------------------------------------------------------------------------------------------------

        auto hasNonDefaultConfig = [](const LocalPairConfig& lpc)
        {
            return lpc != LocalPairConfig{lpc.folderPathPhraseLeft,
                                          lpc.folderPathPhraseRight,
                                          std::nullopt, std::nullopt, FilterConfig()};
        };

        auto replaceDirectories = [&](MainConfiguration& mainCfg) //throw FileError
        {
            if (!dirPathPhrasePairs.empty())
            {
                if (cfgFilePaths.size() > 1)
                    throw FileError(_("Directories cannot be set for more than one configuration file."));

                //check if config at folder-pair level is present: this probably doesn't make sense when replacing/adding the user-specified directories
                if (hasNonDefaultConfig(mainCfg.firstPair) || std::any_of(mainCfg.additionalPairs.begin(), mainCfg.additionalPairs.end(), hasNonDefaultConfig))
                    throw FileError(_("The config file must not contain settings at directory pair level when directories are set via command line."));

                mainCfg.additionalPairs.clear();
                for (size_t i = 0; i < dirPathPhrasePairs.size(); ++i)
                    if (i == 0)
                    {
                        mainCfg.firstPair.folderPathPhraseLeft  = dirPathPhrasePairs[0].first;
                        mainCfg.firstPair.folderPathPhraseRight = dirPathPhrasePairs[0].second;
                    }
                    else
                        mainCfg.additionalPairs.push_back({dirPathPhrasePairs[i].first, dirPathPhrasePairs[i].second,
                                                           std::nullopt, std::nullopt, FilterConfig()});
            }
        };

        const Zstring globalCfgFilePath = !globalCfgPathAlt.empty() ? globalCfgPathAlt : getGlobalConfigDefaultPath();

        GlobalConfig globalCfg;
        try
        {
            std::wstring warningMsg;
            std::tie(globalCfg, warningMsg) = readGlobalConfig(globalCfgFilePath); //throw FileError
            assert(warningMsg.empty()); //ignore parsing errors: should be migration problems only *cross-fingers*
        }
        catch (const FileError& e)
        {
            try
            {
                bool cfgFileExists = true;
                try { cfgFileExists = itemExists(globalCfgFilePath); /*throw FileError*/ } //=> unclear which exception is more relevant/useless:
                catch (const FileError& e2) { throw FileError(replaceCpy(e.toString(), L"\n\n", L'\n'), replaceCpy(e2.toString(), L"\n\n", L'\n')); }

                if (cfgFileExists)
                    throw;
            }
            catch (const FileError& e3) { logExtraError(e3.toString()); }
        }

        //late GlobalSettings.xml-dependent app initialization:
        try { setLanguage(globalCfg.programLanguage); } //throw FileError
        catch (const FileError& e) { logExtraError(e.toString()); }

        try { colorThemeInit(*this, globalCfg.appColorTheme); } //throw FileError
        catch (const FileError& e) { logExtraError(e.toString()); } //not critical in this context


        //-----------------------------------------------------------
        //distinguish sync scenarios:
        //-----------------------------------------------------------
        if (cfgFilePaths.empty())
        {
            //gui mode: default startup
            if (dirPathPhrasePairs.empty())
                MainDialog::create(globalCfg, globalCfgFilePath);
            //gui mode: default config with given directories
            else
            {
                FfsGuiConfig guiCfg;
                guiCfg.mainCfg.syncCfg.directionCfg = getDefaultSyncCfg(SyncVariant::mirror);

                replaceDirectories(guiCfg.mainCfg); //throw FileError

                MainDialog::create(guiCfg, {} /*cfgFilePaths*/, globalCfg, globalCfgFilePath, !openForEdit /*startComparison*/);
            }
        }
        else if (const Zstring filePath0 = cfgFilePaths[0];
                 //batch mode (single config)
                 cfgFilePaths.size() == 1 && endsWithAsciiNoCase(filePath0, Zstr(".ffs_batch")) && !openForEdit)
        {
            auto [batchCfg, warningMsg] = readBatchConfig(filePath0); //throw FileError
            if (!warningMsg.empty())
                throw FileError(warningMsg); //batch mode: break on errors AND even warnings!

            replaceDirectories(batchCfg.guiCfg.mainCfg); //throw FileError

            runBatchMode(batchCfg, filePath0, globalCfg, globalCfgFilePath);
        }
        else //GUI mode: (ffs_gui *or* ffs_batch)
        {
            auto [guiCfg, warningMsg] = readAnyConfig(cfgFilePaths); //throw FileError
            if (!warningMsg.empty())
                showNotificationDialog(nullptr, DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(warningMsg));
            //what about simulating changed config on parsing errors?

            replaceDirectories(guiCfg.mainCfg); //throw FileError
            //what about simulating changed config due to directory replacement?
            //-> propably fine to not show as changed on GUI and not ask user to save on exit!

            MainDialog::create(guiCfg, cfgFilePaths, globalCfg, globalCfgFilePath, !openForEdit /*startComparison*/);
        }
    }
    catch (const FileError& e)
    {
        raiseExitCode(exitCode_, FfsExitCode::exception);
        notifyAppError(e.toString());
    }
}


void Application::runBatchMode(const FfsBatchConfig& batchCfg, const Zstring& cfgFilePath, GlobalConfig globalCfg, const Zstring& globalCfgFilePath)
{
    const bool allowUserInteraction = !batchCfg.batchExCfg.autoCloseSummary ||
                                      (!batchCfg.guiCfg.mainCfg.ignoreErrors && batchCfg.batchExCfg.batchErrorHandling == BatchErrorHandling::showPopup);


    /* regular check for software updates -> disabled for batch
        if (batchCfg.showProgress && manualProgramUpdateRequired())
            checkForUpdatePeriodically(globalCfg.lastUpdateCheck);
        -> WinInet not working when FFS is running as a service!!! https://support.microsoft.com/en-us/help/238425/info-wininet-not-supported-for-use-in-services   */


    const std::chrono::system_clock::time_point syncStartTime = std::chrono::system_clock::now();

    const WindowLayout::Rect progDlgRect
    {
        globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.size,
        std::nullopt /*pos*/,
        globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized
    };

    //class handling status updates and error messages
    BatchStatusHandler statusHandler(!batchCfg.batchExCfg.runMinimized,
                                     extractJobName(cfgFilePath),
                                     syncStartTime,
                                     batchCfg.guiCfg.mainCfg.ignoreErrors,
                                     batchCfg.guiCfg.mainCfg.autoRetryCount,
                                     batchCfg.guiCfg.mainCfg.autoRetryDelay,
                                     globalCfg.soundFileSyncFinished,
                                     globalCfg.soundFileAlertPending,
                                     progDlgRect,
                                     batchCfg.batchExCfg.autoCloseSummary,
                                     batchCfg.batchExCfg.postBatchAction,
                                     batchCfg.batchExCfg.batchErrorHandling);

    AFS::RequestPasswordFun requestPassword; //throw CancelProcess
    if (allowUserInteraction)
        requestPassword = [&, password = Zstring()](const std::wstring& msg, const std::wstring& lastErrorMsg) mutable
    {
        assert(runningOnMainThread());
        if (showPasswordPrompt(statusHandler.getWindowIfVisible(), msg, lastErrorMsg, password) != ConfirmationButton::accept)
            statusHandler.cancelProcessNow(CancelReason::user); //throw CancelProcess

        return password;
    };

    try
    {
        //inform about (important) non-default global settings
        logNonDefaultSettings(globalCfg, statusHandler); //throw CancelProcess

        //batch mode: place directory locks on directories during both comparison AND synchronization
        std::unique_ptr<LockHolder> dirLocks;

        FolderComparison cmpResult = compare(globalCfg.warnDlgs,
                                             globalCfg.fileTimeTolerance,
                                             requestPassword,
                                             globalCfg.runWithBackgroundPriority,
                                             globalCfg.createLockFile,
                                             dirLocks,
                                             extractCompareCfg(batchCfg.guiCfg.mainCfg),
                                             statusHandler); //throw CancelProcess
        if (!cmpResult.empty())
            synchronize(syncStartTime,
                        globalCfg.verifyFileCopy,
                        globalCfg.copyLockedFiles,
                        globalCfg.copyFilePermissions,
                        globalCfg.failSafeFileCopy,
                        globalCfg.runWithBackgroundPriority,
                        extractSyncCfg(batchCfg.guiCfg.mainCfg),
                        cmpResult,
                        globalCfg.warnDlgs,
                        statusHandler); //throw CancelProcess
    }
    catch (CancelProcess&) {}

    //-------------------------------------------------------------------
    BatchStatusHandler::Result r = statusHandler.prepareResult();


    AbstractPath logFolderPath = createAbstractPath(batchCfg.guiCfg.mainCfg.altLogFolderPathPhrase); //optional
    if (AFS::isNullPath(logFolderPath))
        logFolderPath = createAbstractPath(globalCfg.logFolderPhrase);
    assert(!AFS::isNullPath(logFolderPath)); //mandatory! but still: let's include fall back
    if (AFS::isNullPath(logFolderPath))
        logFolderPath = createAbstractPath(getLogFolderDefaultPath());

    AbstractPath logFilePath = AFS::appendRelPath(logFolderPath, generateLogFileName(globalCfg.logFormat, r.summary));
    //e.g. %AppData%\FreeFileSync\Logs\Backup FreeFileSync 2013-09-15 015052.123 [Error].log

    auto notifyStatusNoThrow = [&](std::wstring&& msg) { try { statusHandler.updateStatus(std::move(msg)); /*throw CancelProcess*/ } catch (CancelProcess&) {} };


    if (statusHandler.taskCancelled() && *statusHandler.taskCancelled() == CancelReason::user)
        ; /* user cancelled => don't run post sync command
                            => don't run post sync action
                            => don't send email notification
                            => don't play sound notification  */
    else
    {
        //--------------------- post sync command ----------------------
        if (const Zstring cmdLine = trimCpy(expandMacros(batchCfg.guiCfg.mainCfg.postSyncCommand));
            !cmdLine.empty())
            if (batchCfg.guiCfg.mainCfg.postSyncCondition == PostSyncCondition::completion ||
                (batchCfg.guiCfg.mainCfg.postSyncCondition == PostSyncCondition::errors) == (r.summary.result == TaskResult::cancelled ||
                    r.summary.result == TaskResult::error))
                try
                {
                    //give consoleExecute() some "time to fail", but not too long to hang our process
                    const int DEFAULT_APP_TIMEOUT_MS = 100;

                    if (const auto& [exitCode, output] = consoleExecute(cmdLine, DEFAULT_APP_TIMEOUT_MS); //throw SysError, SysErrorTimeOut
                        exitCode != 0)
                        throw SysError(formatSystemError("", replaceCpy(_("Exit code %x"), L"%x", numberTo<std::wstring>(exitCode)), utfTo<std::wstring>(output)));

                    logMsg(r.errorLog.ref(), _("Executing command:") + L' ' + utfTo<std::wstring>(cmdLine) + L" [" + replaceCpy(_("Exit code %x"), L"%x", L"0") + L']', MSG_TYPE_INFO);
                }
                catch (SysErrorTimeOut&) //child process not failed yet => probably fine :>
                {
                    logMsg(r.errorLog.ref(), _("Executing command:") + L' ' + utfTo<std::wstring>(cmdLine), MSG_TYPE_INFO);
                }
                catch (const SysError& e)
                {
                    logMsg(r.errorLog.ref(), replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLine)) + L"\n\n" + e.toString(), MSG_TYPE_ERROR);
                }

        //--------------------- email notification ----------------------
        if (const std::string notifyEmail = trimCpy(batchCfg.guiCfg.mainCfg.emailNotifyAddress);
            !notifyEmail.empty())
            if (batchCfg.guiCfg.mainCfg.emailNotifyCondition == ResultsNotification::always ||
                (batchCfg.guiCfg.mainCfg.emailNotifyCondition == ResultsNotification::errorWarning && (r.summary.result == TaskResult::cancelled ||
                    r.summary.result == TaskResult::error ||
                    r.summary.result == TaskResult::warning)) ||
                (batchCfg.guiCfg.mainCfg.emailNotifyCondition == ResultsNotification::errorOnly && (r.summary.result == TaskResult::cancelled ||
                    r.summary.result == TaskResult::error)))
                try
                {
                    logMsg(r.errorLog.ref(), replaceCpy(_("Sending email notification to %x"), L"%x", utfTo<std::wstring>(notifyEmail)), MSG_TYPE_INFO);
                    sendLogAsEmail(notifyEmail, r.summary, r.errorLog.ref(), logFilePath, notifyStatusNoThrow); //throw FileError
                }
                catch (const FileError& e) { logMsg(r.errorLog.ref(), e.toString(), MSG_TYPE_ERROR); }
    }

    //--------------------- save log file ----------------------
    std::set<AbstractPath> logsToKeepPaths;
    for (const ConfigFileItem& cfi : globalCfg.mainDlg.config.fileHistory)
        if (!equalNativePath(cfi.cfgFilePath, cfgFilePath)) //exception: don't keep old log for the selected cfg file!
            logsToKeepPaths.insert(cfi.lastRunStats.logFilePath);

    try //create not before destruction: 1. avoid issues with FFS trying to sync open log file 2. include status in log file name without extra rename
    {
        //do NOT use tryReportingError()! saving log files should not be cancellable!
        saveLogFile(logFilePath, r.summary, r.errorLog.ref(), globalCfg.logfilesMaxAgeDays, globalCfg.logFormat, logsToKeepPaths, notifyStatusNoThrow); //throw FileError
    }
    catch (const FileError& e)
    {
        try //fallback: log file *must* be saved no matter what!
        {
            const AbstractPath logFileDefaultPath = AFS::appendRelPath(createAbstractPath(getLogFolderDefaultPath()), generateLogFileName(globalCfg.logFormat, r.summary));
            if (logFilePath == logFileDefaultPath)
                throw;

            logMsg(r.errorLog.ref(), e.toString(), MSG_TYPE_ERROR);

            logFilePath = logFileDefaultPath;
            saveLogFile(logFileDefaultPath, r.summary, r.errorLog.ref(), globalCfg.logfilesMaxAgeDays, globalCfg.logFormat, logsToKeepPaths, notifyStatusNoThrow); //throw FileError
        }
        catch (const FileError& e2) { logMsg(r.errorLog.ref(), e2.toString(), MSG_TYPE_ERROR); logExtraError(e2.toString()); } //should never happen!!!
    }

    //--------- update last sync stats for the selected cfg file ---------
    const ErrorLogStats& logStats = getStats(r.errorLog.ref());

    for (ConfigFileItem& cfi : globalCfg.mainDlg.config.fileHistory)
        if (equalNativePath(cfi.cfgFilePath, cfgFilePath))
        {
            assert(r.summary.startTime == syncStartTime);
            assert(!AFS::isNullPath(logFilePath));

            cfi.lastRunStats =
            {
                std::chrono::system_clock::to_time_t(r.summary.startTime),
                logFilePath,
                r.summary.result,
                r.summary.statsProcessed.items,
                r.summary.statsProcessed.bytes,
                r.summary.totalTime,
                logStats.errors,
                logStats.warnings,
            };
            break;
        }

    //---------------------------------------------------------------------------
    const BatchStatusHandler::DlgOptions dlgOpt = statusHandler.showResult();

    globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.size        = dlgOpt.dlgRect.size; //=> ignore dlgOpt.pos
    globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized = dlgOpt.dlgRect.isMaximized;

    //----------------------------------------------------------------------
    switch (r.summary.result)
    {
        case TaskResult::success:   raiseExitCode(exitCode_, FfsExitCode::success); break;
        case TaskResult::warning:   raiseExitCode(exitCode_, FfsExitCode::warning); break;
        case TaskResult::error:     raiseExitCode(exitCode_, FfsExitCode::error  ); break;
        case TaskResult::cancelled: raiseExitCode(exitCode_, FfsExitCode::cancelled); break;
    }

    //email sending, or saving log file failed? at least this should affect the exit code:
    if (logStats.errors > 0)
        raiseExitCode(exitCode_, FfsExitCode::error);
    else if (logStats.warnings > 0)
        raiseExitCode(exitCode_, FfsExitCode::warning);

    //---------------------------------------------------------------------------
    //stream sync stats to STDOUT as JSON
    JsonValue syncStats(JsonValue::Type::object);
    switch (r.summary.result)
    {
        case TaskResult::success:   syncStats.objectVal.set("syncResult", "success"); break;
        case TaskResult::warning:   syncStats.objectVal.set("syncResult", "warning"); break;
        case TaskResult::error:     syncStats.objectVal.set("syncResult", "error"); break;
        case TaskResult::cancelled: syncStats.objectVal.set("syncResult", "cancelled"); break;
    }

    std::string startTimeStr = utfTo<std::string>(formatTime(Zstr("%Y-%m-%dT%H:%M:%S%z"), getLocalTime(std::chrono::system_clock::to_time_t(r.summary.startTime))));
    syncStats.objectVal.set("startTime", std::move(startTimeStr.insert(startTimeStr.size() - 2, ":"))); //ISO 8601 date/time with offset e.g. 2001-08-23T14:55:02+02:00

    syncStats.objectVal.set("totalTimeSec", std::chrono::duration_cast<std::chrono::seconds>(r.summary.totalTime).count());

    syncStats.objectVal.set("errors",   logStats.errors);
    syncStats.objectVal.set("warnings", logStats.warnings);

    syncStats.objectVal.set("totalItems", r.summary.statsTotal.items);
    syncStats.objectVal.set("totalBytes", r.summary.statsTotal.bytes);

    syncStats.objectVal.set("processedItems", r.summary.statsProcessed.items);
    syncStats.objectVal.set("processedBytes", r.summary.statsProcessed.bytes);

    syncStats.objectVal.set("logFile", utfTo<std::string>(AFS::getDisplayPath(logFilePath)));

    std::cout << serializeJson(syncStats);

    //---------------------------------------------------------------------------
    try //save global settings to XML: e.g. ignored warnings, last sync stats
    {
        writeConfig(globalCfg, globalCfgFilePath); //FileError
    }
    catch (const FileError& e)
    {
        //raiseExitCode(exitCode_, FfsExitCode::error); -> sync successful
        if (allowUserInteraction)
            showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString()));
        else
            logExtraError(e.toString());
    }

    //---------------------------------------------------------------------------
    //run shutdown *after* saving global config! https://freefilesync.org/forum/viewtopic.php?t=5761
    using FinalRequest = BatchStatusHandler::FinalRequest;
    switch (dlgOpt.finalRequest)
    {
        case FinalRequest::none:
            break;

        case FinalRequest::switchGui: //open new top-level window *after* progress dialog is gone => run on main event loop
            MainDialog::create(batchCfg.guiCfg, {cfgFilePath}, globalCfg, globalCfgFilePath, true /*startComparison*/);
            break;

        case FinalRequest::shutdown:
            try
            {
                shutdownSystem(); //throw FileError
                terminateProcess(static_cast<int>(exitCode_)); //better exit in a controlled manner rather than letting the OS kill us any time!
            }
            catch (const FileError& e)
            {
                //raiseExitCode(exitCode_, FfsExitCode::error); -> no! sync was successful
                if (allowUserInteraction)
                    showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString()));
                else
                    logExtraError(e.toString());
            }
            break;
    }
}
