using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using PolyTrader.App.Avalonia.Shell;
using PolyTrader.Core.Configuration;
using PolyTrader.Core.DependencyInjection;
using PolyTrader.Core.Modularity;
using PolyTrader.Core.Notifications;
using PolyTrader.Core.Security;
using PolyTrader.Modules.Accounting;
using PolyTrader.Modules.CopyTrading;
using PolyTrader.Modules.ResolutionFarming;
using PolyTrader.Modules.Supervisor;
using PolyTraderSharp;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;

namespace PolyTrader.App.Avalonia
{
    /// <summary>
    /// Einstiegspunkt der plattformneutralen PolyTrader-Anwendung.
    ///
    /// <para>Der Aufbau ist bewusst zweigeteilt: <see cref="BuildHost"/> stellt den Generic Host mit
    /// Persistenz, Diensten und Modulen zusammen – ohne jeden Bezug zur Oberfläche. Erst
    /// <see cref="Main"/> hängt Avalonia daran. Damit lässt sich derselbe Host später ohne Umbau
    /// für den kopflosen Linux-Betrieb starten (Schalter <c>--headless</c>, Stufe L2).</para>
    /// </summary>
    internal static class Program
    {
        private const string ServerSettingsPath = "server_settings.xml";

        [STAThread]
        public static int Main(string[] args)
        {
            bool headless = args.Any(a => string.Equals(a, "--headless", StringComparison.OrdinalIgnoreCase));
            bool smoke = args.Any(a => string.Equals(a, "--smoke-ui", StringComparison.OrdinalIgnoreCase));

            // D-2: Lizenz-CLI-Schalter (--license-status/--license-set-key/--license-deactivate)
            // laufen VOR dem Host-Aufbau und beenden sich selbst - kein Sinn, dafür Trading-Dienste
            // hochzufahren.
            if (Licensing.LicenseCli.TryHandle(args, out int cliExitCode))
                return cliExitCode;

            var dcOptions = LoadDeploymentcenterOptions();

            // Konstruktionsprüfung (--smoke-ui) testet Fensteraufbau, keine Lizenzlogik - dort
            // bewusst ALLE Module registrieren, sonst würde die Prüfung auf einem Build-Server
            // (nie lizenziert) still einen Großteil ihrer Abdeckung verlieren.
            var host = BuildHost(dcOptions, enforceLicense: !smoke, out var bootLog);

            // Konstruktionspruefung der Fenster: Host NICHT starten. Sonst laufen Trading-Engine,
            // WSS-Clients und Jobs gegen die echten Boersen-Endpunkte an - fuer einen reinen
            // UI-Test unerwuenscht (und auf einem Build-Server schlicht falsch).
            if (smoke)
            {
                try { return RunSmokeUi(host, bootLog); }
                finally { host.Dispose(); }
            }

            host.Start();

            try
            {
                if (headless)
                {
                    bootLog.Info("Kopfloser Betrieb: keine Oberfläche, Trading-Dienste laufen. Beenden mit Strg+C.");
                    host.WaitForShutdown();
                    return 0;
                }

                App.Services = host.Services;
                BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
                return 0;
            }
            finally
            {
                // Geordnetes Herunterfahren der Module – erst nach dem Ende der Oberfläche.
                host.StopAsync(TimeSpan.FromSeconds(30)).GetAwaiter().GetResult();
                host.Dispose();
            }
        }

        /// <summary>Von Avalonia (auch vom XAML-Previewer) erwartete Fabrikmethode.</summary>
        public static AppBuilder BuildAvaloniaApp() =>
            AppBuilder.Configure<App>()
                      .UsePlatformDetect()
                      .WithInterFont()
                      .LogToTrace();

        /// <summary>
        /// Lädt NUR den Deploymentcenter-Konfigurationsblock, bevor der Generic Host existiert –
        /// die Lizenzprüfung (D-2) muss VOR der Modul-Liste laufen, also bevor
        /// <see cref="Host.CreateDefaultBuilder"/> seine eigene <see cref="IConfiguration"/> baut.
        /// Liest dieselben Dateien wie der Host später (appsettings.json + appsettings.Local.json),
        /// nur eigenständig und minimal. Von <see cref="Licensing.LicenseCli"/> mitbenutzt.
        /// </summary>
        internal static DeploymentcenterOptions LoadDeploymentcenterOptions()
        {
            var config = new ConfigurationBuilder()
                .SetBasePath(AppContext.BaseDirectory)
                .AddJsonFile("appsettings.json", optional: true)
                .AddJsonFile("appsettings.Local.json", optional: true)
                .Build();

            var options = new DeploymentcenterOptions();
            config.GetSection(DeploymentcenterOptions.SectionName).Bind(options);
            return options;
        }

        /// <summary>
        /// Baut den Anwendungs-Host: Konfiguration, Master-Key, Persistenz, Core-Dienste und die
        /// aktiven Module. Kennt die Oberfläche nicht.
        /// </summary>
        /// <param name="enforceLicense">
        /// D-2: Wenn <c>false</c> (nur <c>--smoke-ui</c>), werden immer alle Module registriert –
        /// die Konstruktionsprüfung testet Fensteraufbau, keine Lizenzlogik.
        /// </param>
        private static IHost BuildHost(DeploymentcenterOptions dcOptions, bool enforceLicense, out TerminalLogger bootLog)
        {
            var serverSettings = ServerSettings.Load(ServerSettingsPath);
            var logger = new TerminalLogger();
            bootLog = logger;

            // Anzeige-Kultur festnageln: Auf Linux richtet sich die Kultur sonst nach LANG/LC_ALL,
            // das unter systemd oft gar nicht gesetzt ist - dann faellt .NET auf Invariant zurueck
            // und aus "1.234,56 USDC" wird "1,234.56 USDC". Maschinen-I/O (API, Logs, CSV) laeuft
            // unabhaengig davon ohnehin invariant.
            var uiCulture = new System.Globalization.CultureInfo("de-DE");
            System.Globalization.CultureInfo.DefaultThreadCurrentCulture = uiCulture;
            System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = uiCulture;

            ConfigureSecretProtection(logger);

            // Anzeige-Zeitzone einmalig setzen (Logs, Auswertungen). Ein unbekannter Wert ist nicht
            // fatal - es wird auf die Systemzeitzone ausgewichen und gewarnt.
            AppTimeZone.Configure(serverSettings.ApplicationTimeZoneId, m => logger.Warning(m));
            logger.Info($"Anzeige-Zeitzone: {AppTimeZone.CurrentId}");

            var allModules = new List<IPolyTraderModule>
            {
                new CopyTradingModule(),
                new ResolutionFarmingModule(),
                new SupervisorModule(),
                new AccountingModule()
            };
            var disabled = new HashSet<string>(serverSettings.DisabledModules, StringComparer.OrdinalIgnoreCase);

            // D-2: ohne gültige Lizenz startet nur die Core-Shell (Terminal, Einstellungen) - alle
            // Trading-/Analyse-Module bleiben unregistriert. Kein Environment.Exit (Entscheidung
            // 31.07.2026): der Schlüssel lässt sich im Einstellungsfenster nachtragen.
            bool licensed = !enforceLicense || Licensing.LicenseGate.CheckUsable(dcOptions, serverSettings, logger);
            if (!licensed)
            {
                foreach (var m in allModules) disabled.Add(m.Name);
            }

            var modules = allModules.Where(m => !disabled.Contains(m.Name)).ToList();

            return Host.CreateDefaultBuilder()
                .UseContentRoot(AppContext.BaseDirectory)
                .ConfigureAppConfiguration((_, config) =>
                    config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
                .ConfigureServices((context, services) =>
                {
                    services.AddSingleton(dcOptions);
                    var databaseOptions = new DatabaseOptions
                    {
                        MySqlConnectionString = context.Configuration["Database:MySqlConnectionString"] ?? string.Empty
                    };
                    services.Configure<DatabaseOptions>(context.Configuration.GetSection(DatabaseOptions.SectionName));
                    services.AddCorePersistence(databaseOptions);

                    services.AddSingleton(serverSettings);
                    services.AddSingleton(logger);
                    services.AddSingleton<TradingState>();
                    services.AddSingleton<JobManager>();
                    services.AddSingleton<MullvadVpnService>();
                    services.AddHttpClient();

                    // Polymarket-Zugriffe teilen sich einen langlebigen Verbindungspool.
                    services.AddSingleton(sp => new PolymarketApiService(
                        sp.GetRequiredService<TerminalLogger>(), CreatePolymarketHttpClient()));
                    services.AddSingleton(sp => new PolymarketClobClient(
                        sp.GetRequiredService<TerminalLogger>(), CreatePolymarketHttpClient()));

                    // Benachrichtigungen: neutrale Senke, bis RocketChat/Telegram angebunden sind.
                    services.AddSingleton<INotificationSink>(
                        sp => new LogNotificationSink(sp.GetRequiredService<TerminalLogger>()));

                    services.AddSingleton(sp =>
                    {
                        var watchdog = new WatchdogHeartbeatService(sp.GetRequiredService<TerminalLogger>());

                        // D-1: erster "checks"-Eintrag - beweist die Mechanik mit einem real
                        // verfügbaren Signal. db/market_feed folgen, sobald die jeweiligen Dienste
                        // ein Gesundheitssignal nach außen geben (siehe
                        // docs/umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md, D-1).
                        var vpn = sp.GetRequiredService<MullvadVpnService>();
                        var serverSettings = sp.GetRequiredService<ServerSettings>();
                        watchdog.RegisterCheck("vpn", () => serverSettings.VpnEnabled
                            ? (vpn.IsConnected, vpn.IsConnected ? null : "Mullvad VPN nicht verbunden")
                            : (true, null));   // VPN bewusst aus -> kein Fehlalarm

                        return watchdog;
                    });

                    // MUSS vor den Modulen registriert werden: hydriert die Accounts, bevor die
                    // Trading-Dienste anlaufen (Reihenfolge = Registrierungsreihenfolge).
                    services.AddHostedService<CoreStateHydrationService>();

                    foreach (var module in modules)
                    {
                        services.AddSingleton(module);
                        module.RegisterServices(services, context.Configuration);
                    }

                    // D-3: Fehler-Reporting - rein ereignisgesteuert (AppDomain.UnhandledException,
                    // TaskScheduler.UnobservedTaskException, TerminalLogger-Fehler), kein eigener Takt.
                    services.AddSingleton(sp => new DeploymentcenterErrorReporter(
                        sp.GetRequiredService<TerminalLogger>(), sp.GetRequiredService<DeploymentcenterOptions>()));

                    services.AddHostedService<MarketSyncService>();
                    services.AddHostedService(sp => sp.GetRequiredService<MullvadVpnService>());
                    services.AddHostedService(sp => sp.GetRequiredService<WatchdogHeartbeatService>());
                    services.AddHostedService(sp => sp.GetRequiredService<DeploymentcenterErrorReporter>());

                    // Shell-Zustand (Fensterverwaltung). Auch im kopflosen Betrieb harmlos.
                    services.AddSingleton<AvaloniaUiHost>();
                })
                .Build();
        }

        /// <summary>
        /// Kopflose Konstruktionsprüfung aller registrierten Fenster – Nachfolger des
        /// <c>--smoke-ui</c>-Laufs der WinForms-App, der Konstruktionsfehler zuverlässig gefangen hat.
        /// Läuft ohne Fenster und ohne Message-Loop.
        /// </summary>
        private static int RunSmokeUi(IHost host, TerminalLogger logger)
        {
            // Avalonia muss initialisiert sein, damit Fenster konstruiert werden koennen.
            BuildAvaloniaApp().SetupWithoutStarting();
            App.Services = host.Services;
            ThemeManager.Initialize(host.Services.GetRequiredService<ServerSettings>(), ServerSettingsPath, logger);

            var uiHost = host.Services.GetRequiredService<AvaloniaUiHost>();
            CoreViews.Register(uiHost, host.Services);
            foreach (var module in host.Services.GetServices<IPolyTraderModule>())
                module.RegisterUi(uiHost, host.Services);
            ModuleViews.Register(uiHost, host.Services);
            ViewIcons.AssignDefaults(uiHost);

            int failures = 0;
            Console.WriteLine("=== Smoke-UI: Seiten-Konstruktion (Avalonia) ===");
            foreach (var view in uiHost.Views)
            {
                try
                {
                    _ = view.CreateView();
                    Console.WriteLine($"[OK] {view.Id}  ({view.Title})");
                }
                catch (Exception ex)
                {
                    failures++;
                    Console.WriteLine($"[FEHLER] {view.Id}: {ex.GetType().Name}: {ex.Message}");
                }
            }

            // Die Startseite steckt seit der Einfenster-Shell in der Schleife oben (core.start);
            // separat geprueft wird nur noch das Shell-Fenster selbst.
            try
            {
                _ = new Views.ShellWindow(uiHost, host.Services);
                Console.WriteLine("[OK] ShellWindow konstruiert");
            }
            catch (Exception ex)
            {
                failures++;
                Console.WriteLine($"[FEHLER] ShellWindow: {ex.GetType().Name}: {ex.Message}");
            }

            try
            {
                _ = new Views.ShutdownConfirmWindow();
                Console.WriteLine("[OK] ShutdownConfirmWindow konstruiert");
            }
            catch (Exception ex)
            {
                failures++;
                Console.WriteLine($"[FEHLER] ShutdownConfirmWindow: {ex.GetType().Name}: {ex.Message}");
            }

            // Der Einstellungs-Editor wird aus den Attributen des Modells aufgebaut. Ein Fenster kann
            // fehlerfrei konstruieren und trotzdem leer sein, wenn die Attribute verlorengehen -
            // deshalb hier gegen die tatsaechliche Feldzahl pruefen.
            try
            {
                var sections = ViewModels.SettingsModelBuilder.Build(new ServerSettings());
                int fieldCount = sections.Sum(x => x.Fields.Count);
                if (sections.Count == 0 || fieldCount == 0)
                {
                    failures++;
                    Console.WriteLine("[FEHLER] Einstellungs-Editor: keine Felder aus ServerSettings ermittelt " +
                                      "(Category-/DisplayName-Attribute verloren?).");
                }
                else
                {
                    Console.WriteLine($"[OK] Einstellungs-Editor: {sections.Count} Abschnitte, {fieldCount} Felder " +
                                      $"({string.Join(", ", sections.Select(x => x.Title))})");
                }
            }
            catch (Exception ex)
            {
                failures++;
                Console.WriteLine($"[FEHLER] Einstellungs-Editor: {ex.GetType().Name}: {ex.Message}");
            }

            // Farbschema: jeder Token muss in BEIDEN Varianten aufloesen. Ein Tippfehler im
            // Schluessel faellt sonst nicht auf - ThemeManager.Brush liefert dann still Grau.
            string[] themeKeys =
            {
                "AppBgBrush", "AppSurfaceBrush", "AppSurfaceAltBrush", "AppCardBrush",
                "AppBorderBrush", "AppDividerBrush",
                "AppTextBrush", "AppMutedTextBrush", "AppCaptionTextBrush", "AppReadOnlyTextBrush",
                "AppAccentBrush", "AppAccentAltBrush", "AppAccentStrongBrush", "AppAccentSoftBrush",
                "AppPositiveBrush", "AppNegativeBrush", "AppWarningBrush",
                "AppNotifyWarnBrush", "AppNotifyErrorBrush",
                "AppTradeLossBrush", "AppTradeSmallWinBrush", "AppTradeBigWinBrush",
                "AppToggleActiveBrush", "AppToggleSellOnlyBrush", "AppToggleInactiveBrush",
                "AppChatUserBrush", "AppChatAgentBrush"
            };
            foreach (var variant in new[] { global::Avalonia.Styling.ThemeVariant.Light,
                                            global::Avalonia.Styling.ThemeVariant.Dark })
            {
                var missing = themeKeys.Where(k =>
                    !(global::Avalonia.Application.Current!.TryFindResource(k, variant, out object? v) && v is global::Avalonia.Media.IBrush))
                    .ToList();
                if (missing.Count > 0)
                {
                    failures++;
                    Console.WriteLine($"[FEHLER] Farbschema „{variant}“: fehlende Farben: {string.Join(", ", missing)}");
                }
                else
                {
                    Console.WriteLine($"[OK] Farbschema „{variant}“: alle {themeKeys.Length} Farben vorhanden");
                }
            }

            Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ===");
            return failures == 0 ? 0 : 1;
        }

        /// <summary>
        /// HttpClient für die Polymarket-APIs: langlebiger Verbindungspool und die Header, die die
        /// Gegenstelle erwartet (identisch zur bisherigen WinForms-Zusammenstellung).
        /// </summary>
        private static System.Net.Http.HttpClient CreatePolymarketHttpClient() =>
            new(new System.Net.Http.SocketsHttpHandler
            {
                PooledConnectionLifetime = TimeSpan.FromMinutes(2),
                MaxConnectionsPerServer = 100
            })
            {
                DefaultRequestHeaders = { { "User-Agent", "py_clob_client" }, { "Accept", "*/*" } }
            };

        /// <summary>
        /// Lädt den Master-Key (Umgebungsvariable, sonst gitignorierte <c>master.key</c>, sonst wird
        /// einer erzeugt – D-5/D-10) und aktiviert die at-rest-Verschlüsselung. Muss VOR jeder
        /// Credential-Entschlüsselung laufen.
        /// </summary>
        private static void ConfigureSecretProtection(TerminalLogger logger)
        {
            string keyFile = Path.Combine(AppContext.BaseDirectory, "master.key");
            string? masterKey = MasterKeyResolver.Resolve(
                keyFile,
                () => Environment.GetEnvironmentVariable("POLYTRADER_MASTER_KEY"),
                onGenerated: _ => logger.Warning(
                    "🔐 Kein Master-Key gefunden – ein neuer wurde erzeugt und in \"master.key\" " +
                    "gespeichert (App-Ordner, gitignored). WICHTIG: Diese Datei SOFORT separat sichern " +
                    "(Passwort-Manager/Offline-Backup) – ihr Verlust macht jedes ab jetzt verschlüsselte " +
                    "Secret unwiederbringlich unlesbar."),
                onWarning: msg => logger.Warning($"⚠️ {msg}"));

            SecretProtection.Configure(masterKey);

            if (SecretProtection.IsConfigured)
                logger.Info("🔐 Secret-Verschlüsselung aktiv – Account-Credentials werden at-rest verschlüsselt (AES-256-GCM).");
            else
                logger.Warning("⚠️ SICHERHEIT: Kein POLYTRADER_MASTER_KEY gesetzt – Account-Credentials liegen UNVERSCHLÜSSELT in der DB.");
        }
    }
}
