using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using PSLauncher.App.Services;
using PSLauncher.App.ViewModels;
using PSLauncher.App.Views;
using PSLauncher.Core.Configuration;
using PSLauncher.Core.Downloads;
using PSLauncher.Core.Installations;
using PSLauncher.Core.Integrity;
using PSLauncher.Core.Licensing;
using PSLauncher.Core.Localization;
using PSLauncher.Core.Manifests;
using PSLauncher.Core.Security;
using PSLauncher.Core.ApiTool;
using PSLauncher.Core.DocTool;
using PSLauncher.Core.Lan;
using PSLauncher.Core.Health;
using PSLauncher.Core.Health.OpenVRInterop;
using PSLauncher.Core.Migrations;
using PSLauncher.Core.Process;
using PSLauncher.Core.ReportTool;
using PSLauncher.Core.SteamVr;
using PSLauncher.Core.Updates;
using PSLauncher.Models;
using Serilog;

namespace PSLauncher.App;

public partial class App : Application
{
    private IHost? _host;

    /// <summary>
    /// IServiceProvider du host DI. Exposé pour les rares cas où une vue est
    /// instanciée par <c>new</c> hors du container (ex: SettingsDialog) et a
    /// besoin de résoudre un service.
    /// </summary>
    public IServiceProvider? HostServices => _host?.Services;
    private static System.Threading.Mutex? _singleInstanceMutex;

    private const string SingleInstanceMutexName = "Global\\PSLauncher-3F8E2C1A-9B47-4D5E-A0F8-2E9D1B6C7A3F";
    private const string BringToFrontMessage = "PSLAUNCHER_BRING_TO_FRONT";
    private static readonly int WM_PSLAUNCHER_BRING = RegisterWindowMessage(BringToFrontMessage);

    [DllImport("user32.dll")]
    private static extern int RegisterWindowMessage(string lpString);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool PostMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);

    private const int HWND_BROADCAST = 0xFFFF;

    public static string LogsDirectory { get; private set; } = string.Empty;

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        // Single-instance : si une autre instance tourne déjà, on lui demande de
        // se mettre au premier plan via un broadcast Windows et on quitte tout de suite.
        _singleInstanceMutex = new System.Threading.Mutex(initiallyOwned: true,
            name: SingleInstanceMutexName, out var createdNew);
        if (!createdNew)
        {
            PostMessage((IntPtr)HWND_BROADCAST, WM_PSLAUNCHER_BRING, IntPtr.Zero, IntPtr.Zero);
            Shutdown();
            return;
        }

        // Logs avant tout — pour ne rien perdre des erreurs au boot
        LogsDirectory = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "PSLauncher", "logs");
        Directory.CreateDirectory(LogsDirectory);

        // WebView2 a besoin d'écrire son UserDataFolder (cache, cookies, GPU shader cache,
        // etc.). Par défaut il essaie à côté du .exe — ce qui échoue silencieusement quand
        // on est installé en Program Files (read-only sans admin) → onglets Report/Doc
        // vides. On force le user-data-folder dans %LocalAppData%\PSLauncher\WebView2 via
        // la var d'environnement officielle, lue par WebView2 à la première initialisation.
        // À setter AVANT que tout contrôle WebView2 ne soit instancié → ici, dans OnStartup,
        // est largement assez tôt (la MainWindow n'est créée que plus bas).
        var webViewDataDir = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "PSLauncher", "WebView2");
        Directory.CreateDirectory(webViewDataDir);
        Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", webViewDataDir, EnvironmentVariableTarget.Process);

        // Bootstrap i18n : on lit d'abord la config (sans services DI) pour fixer la
        // langue avant que le moindre élément XAML soit instancié.
        try
        {
            var bootstrapStore = new ConfigStore(
                Microsoft.Extensions.Logging.Abstractions.NullLogger<ConfigStore>.Instance);
            var bootstrapCfg = bootstrapStore.Load();
            Strings.Init(bootstrapCfg.Language);
        }
        catch
        {
            Strings.Init("auto");
        }

        Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Debug()
            .Enrich.FromLogContext()
            .WriteTo.Debug()
            .WriteTo.File(
                path: Path.Combine(LogsDirectory, "app-.log"),
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: 10,
                outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}")
            .CreateLogger();

        Log.Information("PSLauncher starting (logs in {Path})", LogsDirectory);

        AppDomain.CurrentDomain.UnhandledException += (_, args) =>
        {
            // CRITIQUE : on flush AVANT que le process meure. Sans ça, sur les
            // exceptions de corruption d'état (AccessViolation depuis P/Invoke,
            // SEH natif…) le buffer Serilog n'est jamais écrit sur disque, et
            // le crash est totalement invisible dans les logs — exactement le
            // mode de panne qu'on a eu avec OpenVR pendant le démarrage SteamVR.
            try
            {
                Log.Fatal((Exception)args.ExceptionObject, "Unhandled exception (AppDomain) IsTerminating={IsTerminating}", args.IsTerminating);
                Log.CloseAndFlush();
            }
            catch { /* dernier recours — on ne peut plus rien faire */ }
        };
        DispatcherUnhandledException += (_, args) =>
        {
            Log.Error(args.Exception, "Unhandled UI exception");
            args.Handled = false;
        };

        _host = Host.CreateDefaultBuilder()
            .UseSerilog()
            .ConfigureServices((_, services) =>
            {
                services.AddSingleton<IConfigStore, ConfigStore>();
                services.AddSingleton<LocalConfig>(sp => sp.GetRequiredService<IConfigStore>().Load());

                services.AddSingleton(sp =>
                {
                    // Handler tuné pour les gros téléchargements parallèles :
                    // - MaxConnectionsPerServer=32 pour aligner avec le clamp DownloadManager (8-32).
                    //   Default WPF = 8 → trop bas pour notre multi-segment.
                    // - AutomaticDecompression=None : les builds sont des ZIPs déjà compressés ;
                    //   éviter la CPU+RAM gaspillée par un éventuel wrapping gzip côté serveur.
                    // - PooledConnectionLifetime court pour éviter qu'OVH ferme nos sockets sous nous.
                    var handler = new SocketsHttpHandler
                    {
                        PooledConnectionLifetime = TimeSpan.FromMinutes(5),
                        PooledConnectionIdleTimeout = TimeSpan.FromSeconds(60),
                        MaxConnectionsPerServer = 32,
                        EnableMultipleHttp2Connections = true,
                        AutomaticDecompression = System.Net.DecompressionMethods.None,
                    };
                    var http = new HttpClient(handler)
                    {
                        Timeout = Timeout.InfiniteTimeSpan,
                        DefaultRequestVersion = System.Net.HttpVersion.Version20,
                        DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower,
                    };
                    http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("PSLauncher", "0.5"));
                    return http;
                });

                services.AddSingleton<IInstallationRegistry>(sp =>
                    new InstallationRegistry(
                        sp.GetRequiredService<ILogger<InstallationRegistry>>(),
                        () => sp.GetRequiredService<LocalConfig>().InstallRoot));

                services.AddSingleton<IProcessLauncher, ProcessLauncher>();
                services.AddSingleton<IIntegrityService, IntegrityService>();
                services.AddSingleton<IZipInstaller, ZipInstaller>();
                services.AddSingleton<IDownloadStateStore, DownloadStateStore>();

                services.AddSingleton<IManifestCache, ManifestCache>();
                services.AddSingleton<ISettingsLockService, SettingsLockService>();
                services.AddSingleton<IManifestService>(sp =>
                    new ManifestService(
                        sp.GetRequiredService<HttpClient>(),
                        () => sp.GetRequiredService<LocalConfig>().ServerBaseUrl,
                        // Channel provider : lit le channel depuis la license cachée à
                        // chaque fetch (sp.GetRequiredService est lazy, donc pas de
                        // dépendance circulaire avec ILicenseService au démarrage).
                        // Si pas de license cachée → null → manifest default.
                        () => sp.GetRequiredService<ILicenseService>().GetCached()?.Channel,
                        sp.GetRequiredService<IManifestCache>(),
                        sp.GetRequiredService<IPeerManifestFetcher>(),
                        sp.GetRequiredService<ISettingsLockService>(),
                        sp.GetRequiredService<ILogger<ManifestService>>()));

                services.AddSingleton<IDownloadManager, DownloadManager>();
                services.AddSingleton<IUpdateChecker, UpdateChecker>();
                services.AddSingleton<ILauncherSelfUpdater, LauncherSelfUpdater>();

                services.AddSingleton<ILicenseService>(sp =>
                    new LicenseService(
                        sp.GetRequiredService<HttpClient>(),
                        () => sp.GetRequiredService<LocalConfig>().ServerBaseUrl,
                        sp.GetRequiredService<IConfigStore>(),
                        sp.GetRequiredService<LocalConfig>(),
                        sp.GetRequiredService<ISettingsLockService>(),
                        sp.GetRequiredService<ILogger<LicenseService>>()));

                services.AddSingleton<IToastService, ToastService>();

                // Migration DB MySQL : appliquée juste après extraction d'un build PROSERVE
                // si le ZIP contient un sous-répertoire _migrations/. La config DB et le
                // password (DPAPI) sont lus à la demande pour refléter les changements
                // utilisateur dans Settings sans recréer le service.
                services.AddSingleton<IDatabaseMigrationService>(sp =>
                    new DatabaseMigrationService(
                        configProvider: () => sp.GetRequiredService<LocalConfig>().Database,
                        passwordProvider: () => DatabasePasswordProtector.Unprotect(
                            sp.GetRequiredService<LocalConfig>().Database.EncryptedPassword),
                        sp.GetRequiredService<ILogger<DatabaseMigrationService>>()));

                // Déploiement de l'outil Report (page web XAMPP) à l'install d'une
                // nouvelle version : copie atomique de _report/ → C:\xampp\htdocs\ProserveReport\
                services.AddSingleton<IReportToolDeployer>(sp =>
                    new ReportToolDeployer(
                        configProvider: () => sp.GetRequiredService<LocalConfig>().ReportTool,
                        sp.GetRequiredService<ILogger<ReportToolDeployer>>()));

                // Déploiement de l'outil Documentation (parallèle à Report) :
                // copie atomique de _doc/ → C:\xampp\htdocs\ProserveDoc\
                services.AddSingleton<IDocToolDeployer>(sp =>
                    new DocToolDeployer(
                        configProvider: () => sp.GetRequiredService<LocalConfig>().DocTool,
                        sp.GetRequiredService<ILogger<DocToolDeployer>>()));

                // Déploiement de l'API PHP de stats (parallèle à Report/Doc) :
                // copie atomique de _api/ → C:\xampp\htdocs\ProserveApi\
                services.AddSingleton<IApiToolDeployer>(sp =>
                    new ApiToolDeployer(
                        configProvider: () => sp.GetRequiredService<LocalConfig>().ApiTool,
                        sp.GetRequiredService<ILogger<ApiToolDeployer>>()));

                // Merge des settings SteamVR : si _steamvr/steamvr.vrsettings est
                // présent dans le ZIP, tue SteamVR + Vive Business Streaming et
                // fusionne les blocs racine dans le fichier de l'utilisateur
                // (replace si existe, ajoute sinon). Voir SteamVrSettingsDeployer.
                services.AddSingleton<ISteamVrSettingsDeployer>(sp =>
                    new SteamVrSettingsDeployer(
                        configProvider: () => sp.GetRequiredService<LocalConfig>().SteamVr,
                        sp.GetRequiredService<ILogger<SteamVrSettingsDeployer>>()));

                // Cache LAN P2P : permet aux PCs du LAN de partager les ZIPs déjà
                // téléchargés au lieu de re-télécharger 14 Go depuis OVH.
                // - ZipCacheStore : couche de persistence (downloads + sidecar SHA)
                // - LanDiscoveryService : auto-découverte UDP des peers
                // - LanCacheServer : serveur HTTP local qui sert les ZIPs cachés
                // - PeerSourceResolver : avant chaque DL, probe les peers et choisit
                //   le premier qui matche le SHA-256 attendu
                services.AddSingleton<IZipCacheStore, ZipCacheStore>();
                services.AddSingleton<LanDiscoveryService>(sp => new LanDiscoveryService(
                    configProvider: () => sp.GetRequiredService<LocalConfig>().LanCache,
                    cache: sp.GetRequiredService<IZipCacheStore>(),
                    configStore: sp.GetRequiredService<IConfigStore>(),
                    http: sp.GetRequiredService<HttpClient>(),
                    logger: sp.GetRequiredService<ILogger<LanDiscoveryService>>()));
                services.AddSingleton<ILanDiscoveryService>(sp => sp.GetRequiredService<LanDiscoveryService>());
                services.AddHostedService(sp => sp.GetRequiredService<LanDiscoveryService>());
                services.AddSingleton<LanCacheServer>(sp => new LanCacheServer(
                    configProvider: () => sp.GetRequiredService<LocalConfig>().LanCache,
                    cache: sp.GetRequiredService<IZipCacheStore>(),
                    manifestCache: sp.GetRequiredService<IManifestCache>(),
                    logger: sp.GetRequiredService<ILogger<LanCacheServer>>()));
                services.AddSingleton<ILanCacheServer>(sp => sp.GetRequiredService<LanCacheServer>());
                services.AddHostedService(sp => sp.GetRequiredService<LanCacheServer>());
                services.AddSingleton<IPeerSourceResolver>(sp => new PeerSourceResolver(
                    configProvider: () => sp.GetRequiredService<LocalConfig>().LanCache,
                    discovery: sp.GetRequiredService<ILanDiscoveryService>(),
                    http: sp.GetRequiredService<HttpClient>(),
                    logger: sp.GetRequiredService<ILogger<PeerSourceResolver>>()));
                services.AddSingleton<IPeerManifestFetcher>(sp => new PeerManifestFetcher(
                    configProvider: () => sp.GetRequiredService<LocalConfig>().LanCache,
                    discovery: sp.GetRequiredService<ILanDiscoveryService>(),
                    http: sp.GetRequiredService<HttpClient>(),
                    logger: sp.GetRequiredService<ILogger<PeerManifestFetcher>>()));

                services.AddSingleton<IOpenVrService, OpenVrService>();
                services.AddSingleton<ISystemHealthService, SystemHealthService>();

                services.AddTransient<SettingsViewModel>();
                services.AddSingleton<MainViewModel>();
                services.AddSingleton<MainWindow>();
            })
            .Build();

        // Démarre les hosted services (LanCacheServer, LanDiscoveryService, …).
        // Sans ce StartAsync, les BackgroundService.ExecuteAsync ne seraient
        // jamais appelés — le DI les construit mais ne les "réveille" pas.
        // Fire-and-forget : on ne bloque pas le UI thread, mais on garde l'erreur
        // dans le log si un service plante au démarrage.
        _ = _host.StartAsync().ContinueWith(t =>
        {
            if (t.IsFaulted) Log.Error(t.Exception, "Host StartAsync failed");
        }, TaskScheduler.Default);

        var window = _host.Services.GetRequiredService<MainWindow>();
        window.DataContext = _host.Services.GetRequiredService<MainViewModel>();
        // Mise en page droite-à-gauche pour les langues RTL (arabe).
        if (Strings.IsRightToLeft)
            window.FlowDirection = FlowDirection.RightToLeft;
        MainWindow = window;
        window.Show();
    }

    protected override void OnExit(ExitEventArgs e)
    {
        Log.Information("PSLauncher shutting down");
        // Stop propre des hosted services (laisse 5s pour qu'ils ferment leurs
        // sockets HttpListener / UdpClient sans laisser le port en TIME_WAIT trop
        // longtemps). Best-effort : si timeout, on continue le shutdown quand même.
        try
        {
            using var stopCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
            _host?.StopAsync(stopCts.Token).GetAwaiter().GetResult();
        }
        catch (Exception ex) { Log.Warning(ex, "Host StopAsync failed (continuing shutdown)"); }
        Log.CloseAndFlush();
        _host?.Dispose();
        base.OnExit(e);
    }
}
