// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using ManagedCommon;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Common.Services.Reports;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
using SystemUnhandledExceptionEventArgs = System.UnhandledExceptionEventArgs;
using XamlUnhandledExceptionEventArgs = Microsoft.UI.Xaml.UnhandledExceptionEventArgs;

namespace Microsoft.CmdPal.UI.Helpers;

/// <summary>
/// Global error handler for Command Palette.
/// </summary>
internal sealed partial class GlobalErrorHandler : IDisposable
{
    private ErrorReportBuilder? _errorReportBuilder;
    private Options? _options;
    private App? _app;

    // GlobalErrorHandler is designed to be self-contained; it can be registered and invoked before a service provider is available.
    internal void Register(App app, Options options, IApplicationInfoService? appInfoService = null)
    {
        ArgumentNullException.ThrowIfNull(app);
        ArgumentNullException.ThrowIfNull(options);

        _options = options;
        _app = app;
        _errorReportBuilder = new ErrorReportBuilder(appInfoService);

        _app.UnhandledException += App_UnhandledException;
        TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
        AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
    }

    private void App_UnhandledException(object sender, XamlUnhandledExceptionEventArgs e)
    {
        // Exceptions thrown on the main UI thread are handled here.
        if (e.Exception != null)
        {
            HandleException(e.Exception, Context.MainThreadException);
        }
    }

    private void CurrentDomain_UnhandledException(object sender, SystemUnhandledExceptionEventArgs e)
    {
        // Exceptions thrown on background threads are handled here.
        if (e.ExceptionObject is Exception ex)
        {
            HandleException(ex, Context.AppDomainUnhandledException);
        }
    }

    private void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
    {
        // This event is raised only when a faulted Task is garbage-collected
        // without its exception being observed. It is NOT raised immediately
        // when the Task faults; timing depends on GC finalization.
        e.SetObserved();
        HandleException(e.Exception, Context.UnobservedTaskException);
    }

    private void HandleException(Exception ex, Context context)
    {
        Logger.LogError($"Unhandled exception detected ({context})", ex);

        if (context == Context.MainThreadException)
        {
            var report = _errorReportBuilder!.BuildReport(ex, context.ToString(), _options?.RedactPii ?? true);

            StoreReport(report, storeOnDesktop: _options?.StoreReportOnUserDesktop == true);

            string message;
            string caption;
            try
            {
                message = ResourceLoaderInstance.GetString("GlobalErrorHandler_CrashMessageBox_Message");
                caption = ResourceLoaderInstance.GetString("GlobalErrorHandler_CrashMessageBox_Caption");
            }
            catch
            {
                // The resource loader may not be available if the exception occurred during startup.
                // Fall back to hardcoded strings in that case.
                message = "Command Palette has encountered a fatal error and must close.";
                caption = "Command Palette - Fatal error";
            }

            PInvoke.MessageBox(
                HWND.Null,
                message,
                caption,
                MESSAGEBOX_STYLE.MB_ICONERROR);
        }
    }

    private static string? StoreReport(string report, bool storeOnDesktop)
    {
        // Generate a unique name for the report file; include timestamp and a random zero-padded number to avoid collisions
        // in case of crash storm.
        var name = FormattableString.Invariant($"CmdPal_ErrorReport_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{Random.Shared.Next(100000):D5}.log");

        // Always store a copy in log directory, this way it is available for Bug Report Tool
        string? reportPath = null;
        if (Logger.CurrentVersionLogDirectoryPath != null)
        {
            reportPath = Save(report, name, static () => Logger.CurrentVersionLogDirectoryPath);
        }

        // Optionally store a copy on the desktop for user (in)convenience
        if (storeOnDesktop)
        {
            var path = Save(report, name, static () => Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory));

            // show the desktop copy if both succeeded
            if (path != null)
            {
                reportPath = path;
            }
        }

        return reportPath;

        static string? Save(string reportContent, string reportFileName, Func<string> directory)
        {
            try
            {
                var logDirectory = directory();
                Directory.CreateDirectory(logDirectory);
                var reportFilePath = Path.Combine(logDirectory, reportFileName);
                File.WriteAllText(reportFilePath, reportContent);
                return reportFilePath;
            }
            catch (Exception ex)
            {
                Logger.LogError("Failed to store exception report", ex);
                return null;
            }
        }
    }

    public void Dispose()
    {
        _app?.UnhandledException -= App_UnhandledException;
        TaskScheduler.UnobservedTaskException -= TaskScheduler_UnobservedTaskException;
        AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException;
    }

    private enum Context
    {
        Unknown = 0,
        MainThreadException,
        BackgroundThreadException,
        UnobservedTaskException,
        AppDomainUnhandledException,
    }

    /// <summary>
    /// Configuration options controlling how <see cref="GlobalErrorHandler"/> reacts to exceptions
    /// (what to log, what to show to the user, and where to store reports).
    /// </summary>
    internal sealed record Options
    {
        /// <summary>
        /// Gets the default configuration.
        /// </summary>
        public static Options Default { get; } = new();

        /// <summary>
        /// Gets a value indicating whether Personally Identifiable Information (PII) should be redacted in error reports.
        /// </summary>
        public bool RedactPii { get; init; } = true;

        /// <summary>
        /// Gets a value indicating whether to store the error report on the user's desktop in addition to the log directory.
        /// </summary>
        public bool StoreReportOnUserDesktop { get; init; }
    }
}
