using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Channels;
using PolyTrader.Core.Persistence;
using PolyTrader.Core.Security;
using System.Windows.Forms;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using PolyTrader.Core.Configuration;
using PolyTrader.Core.DependencyInjection;
using PolyTrader.Core.Modularity;
using PolyTrader.Modules.CopyTrading;
using PolyTrader.Modules.CopyTrading.Persistence;
using PolyTrader.Modules.ResolutionFarming;
using PolyTrader.Modules.Supervisor;
using PolyTrader.Modules.Accounting;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;

namespace PolyTraderSharp;

internal static class Program
{
    public static IHost? AppHost { get; private set; }

    /// <summary>Pfad der (gitignorierten) Server-Settings-Datei – Quelle u.a. für die Modul-Aktivierung.</summary>
    private const string ServerSettingsPath = "server_settings.xml";

	[STAThread]
	private static void Main(string[] args)
	{
		// Config-Migration aus mongoexport-JSON (Ordner, Default "MongoDB") -> MySQL.
		if (args.Length > 0 && string.Equals(args[0], "--migrate-json", StringComparison.OrdinalIgnoreCase))
		{
			var folder = args.Length > 1 ? args[1] : "MongoDB";
			RunConfigMigrationFromJson(folder);
			return;
		}

		// Readback-Verifikation der migrierten Config aus MySQL. Kein UI-Start.
		if (args.Length > 0 && string.Equals(args[0], "--verify-mysql", StringComparison.OrdinalIgnoreCase))
		{
			RunVerifyMySql();
			return;
		}

		// Headless-Smoke-Test der UI: konstruiert jede View + den Launcher (ohne Message-Loop
		// und ohne Trading-/WSS-Services). Fängt Laufzeit-Konstruktionsfehler ab. Kein Fenster.
		if (args.Length > 0 && string.Equals(args[0], "--smoke-ui", StringComparison.OrdinalIgnoreCase))
		{
			Environment.ExitCode = RunSmokeUi();
			return;
		}

		// Diagnose: gibt die MySQL-Serverversion aus (für das ServerVersion-Pinning). Kein UI.
		if (args.Length > 0 && string.Equals(args[0], "--db-version", StringComparison.OrdinalIgnoreCase))
		{
			RunDbVersion();
			return;
		}

		ApplicationConfiguration.Initialize();

			// Server-Settings früh laden (VOR der DI-Registrierung), damit deaktivierte Module gar nicht
			// erst geladen werden. Dieselbe Instanz wird als Singleton weitergereicht (eine Quelle).
			var serverSettings = PolyTraderSharp.Models.ServerSettings.Load(ServerSettingsPath);

			// Früher Logger: wird unten als DI-Singleton weitergereicht, damit die Startmeldungen
			// (Master-Key, Lizenz) später im Terminal-Fenster erscheinen (History-Replay).
			var bootLog = new TerminalLogger();

			// F1 (Sicherheit): Master-Key VOR jeder Entschlüsselung laden – wird sowohl für die
			// Account-Credentials als auch für den gespeicherten Lizenzschlüssel gebraucht.
			ConfigureSecretProtection(bootLog);

			// 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 => bootLog.Warning(m));
			bootLog.Info($"Anzeige-Zeitzone: {AppTimeZone.CurrentId}");

			// Alle bekannten Module; „modules" enthält nur die aktiven (nicht in DisabledModules).
			var allModules = new System.Collections.Generic.List<IPolyTraderModule>
			{
				new CopyTradingModule(),
				new ResolutionFarmingModule(),
				new SupervisorModule(),
				new AccountingModule()
			};
			var disabledModules = new System.Collections.Generic.HashSet<string>(
				serverSettings.DisabledModules, StringComparer.OrdinalIgnoreCase);

			// B.4: Lizenzprüfung beim Start. Bei ungültiger Lizenz zeigt der Gate einen Dialog und
			// gibt false zurück → es werden KEINE Module registriert (nur die Core-Shell startet,
			// damit Terminal und Einstellungen zum Eintragen einer Lizenz erreichbar bleiben).
			// Bewusst KEIN Environment.Exit – ein Trading-Bot darf nicht mitten im Lauf hart sterben.
			bool licensed = PolyTraderSharp.Licensing.LicenseGate.RunStartupGate(serverSettings, bootLog, ServerSettingsPath);
			var modules = licensed
				? allModules.Where(m => !disabledModules.Contains(m.Name)).ToList()
				: new System.Collections.Generic.List<IPolyTraderModule>();

			AppHost = Host.CreateDefaultBuilder()
				// Config immer neben der EXE suchen (nicht im Arbeitsverzeichnis), damit die App
				// auch beim Start aus bin/ oder per Doppelklick ihre appsettings findet.
				.UseContentRoot(AppContext.BaseDirectory)
				// Host.CreateDefaultBuilder lädt appsettings.Local.json NICHT (nur appsettings.json
				// + appsettings.{Environment}.json). Die gitignorierte Local-Datei hält aber die
				// MySQL-Connection – daher hier explizit ergänzen, sonst bleibt sie leer.
				.ConfigureAppConfiguration((context, config) =>
					config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
				.ConfigureServices(delegate(HostBuilderContext context, IServiceCollection services)
			{
				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<TradingState>();
				services.AddSingleton(delegate(IServiceProvider sp)
				{
					TerminalLogger requiredService2 = sp.GetRequiredService<TerminalLogger>();
					var httpHandler = new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(2), MaxConnectionsPerServer = 100 };
					return new PolymarketApiService(requiredService2, new HttpClient(httpHandler)
					{
						DefaultRequestHeaders = 
						{
							{ "User-Agent", "py_clob_client" },
							{ "Accept", "*/*" }
						}
					});
				});
				services.AddSingleton(delegate(IServiceProvider sp)
				{
					TerminalLogger requiredService2 = sp.GetRequiredService<TerminalLogger>();
					var httpHandler = new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(2), MaxConnectionsPerServer = 100 };
					return new PolymarketClobClient(requiredService2, new HttpClient(httpHandler)
					{
						DefaultRequestHeaders = 
						{
							{ "User-Agent", "py_clob_client" },
							{ "Accept", "*/*" }
						}
					});
				});
				// Denselben früh erzeugten Logger als Singleton weiterreichen, damit die
				// Master-Key-/Lizenz-Meldungen von oben im Terminal-Fenster auftauchen.
				services.AddSingleton(bootLog);
				services.AddSingleton<MullvadVpnService>();
				services.AddSingleton<JobManager>();

				// Benachrichtigungen laufen über eine neutrale Senke. Threema ist entfallen; bis RocketChat
				// und Telegram angebunden sind, landen Meldungen im Terminal-Log (LogNotificationSink).
				services.AddSingleton<PolyTrader.Core.Notifications.INotificationSink>(
					sp => new PolyTrader.Core.Notifications.LogNotificationSink(sp.GetRequiredService<TerminalLogger>()));

				// Watchdog-Heartbeat (Dead-Man's-Switch). Als Singleton registriert, damit das
				// Settings-Fenster denselben Dienst für ReloadSettings/Test-Heartbeat nutzt.
				services.AddSingleton(sp => new WatchdogHeartbeatService(sp.GetRequiredService<TerminalLogger>()));

				// MUSS als erster HostedService laufen: hydriert den State, bevor die
				// Trading-Services gegen einen leeren State anlaufen.
				services.AddHostedService<CoreStateHydrationService>();

				// Module registrieren ihre eigenen Services/Channels/State selbst.
				foreach (var module in modules)
				{
					services.AddSingleton(module);
					module.RegisterServices(services, context.Configuration);
				}

				services.AddHostedService<MarketSyncService>();
				services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService<MullvadVpnService>());
				services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService<WatchdogHeartbeatService>());
				services.AddSingleton<PolyTraderSharp.Ui.ShellUiHost>();
				services.AddTransient<PolyTraderSharp.Ui.LauncherForm>();
			}).Build();

            // (Master-Key wurde bereits VOR dem Host-Build geladen – siehe ConfigureSecretProtection(bootLog) oben.)

            // F5 (Sicherheit): warnen, wenn die (remote) DB-Verbindung keine TLS-Erzwingung hat.
            WarnIfDbTlsNotEnforced(AppHost.Services);

            try
            {
                // Trade-Nummerierung fortsetzen: höchste bestehende TradeId serverseitig lesen
                // (M3: kein Full-Table-Load mehr über Find(_=>true).Max()). Nur wenn CopyTrading aktiv ist –
                // sonst sind dessen Services (CopyTradingState/…) gar nicht registriert.
                if (modules.Any(m => m is CopyTradingModule))
                {
                    var copyState = AppHost.Services.GetRequiredService<CopyTradingState>();
                    var tradeLog = AppHost.Services.GetRequiredService<ICopyTradeLogRepository>();
                    copyState.TotalCopyTrades = tradeLog.GetMaxTradeId();
                }
            }
            catch (Exception ex)
            {
                // M3: NICHT still schlucken. Bliebe der Zähler bei 0, kollidierten neue TradeIds mit
                // historischen (PK ist ValueGeneratedNever) → stiller Verlust aller Session-Trades.
                var log = AppHost.Services.GetRequiredService<TerminalLogger>();
                log.Error($"❌ KRITISCH: TradeId-Fortsetzung konnte nicht initialisiert werden: {ex.Message}. " +
                          "Trade-Nummerierung ist NICHT sicher – neue Trades könnten mit bestehenden kollidieren. Bitte DB prüfen.");
            }

			AppHost.Start();

			// F1: einmalige, idempotente Verschlüsselung evtl. vorhandener Klartext-Credentials in der DB.
			ReencryptAccountCredentials(AppHost.Services);

			// Shell-Views registrieren (Core-App-Views; Module folgen via module.RegisterUi).
			var uiHost = AppHost.Services.GetRequiredService<PolyTraderSharp.Ui.ShellUiHost>();
			var viewServices = AppHost.Services;
			uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView
			{
				Id = "core.terminal",
				Title = "Terminal",
				Group = "Core",
				Order = 10,
				CreateView = () =>
				{
					var view = new PolyTraderSharp.Ui.Views.TerminalView();
					view.Initialize(viewServices.GetRequiredService<TerminalLogger>());
					return view;
				}
			});
			uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView
			{
				Id = "core.settings",
				Title = "Server Settings",
				Group = "Core",
				Order = 20,
				CreateView = () =>
				{
					var view = new PolyTraderSharp.Ui.Views.SettingsView();
					view.Initialize(
						viewServices.GetRequiredService<MullvadVpnService>(),
						viewServices.GetRequiredService<TerminalLogger>(),
						viewServices.GetRequiredService<PolyTrader.Core.Persistence.IAccountRepository>(),
						viewServices.GetRequiredService<TradingState>(),
						viewServices.GetRequiredService<WatchdogHeartbeatService>());
					return view;
				}
			});
			uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView
			{
				Id = "core.jobs",
				Title = "Server Jobs",
				Group = "Core",
				Order = 30,
				CreateView = () =>
				{
					var view = new PolyTraderSharp.Ui.Views.JobsView();
					view.Initialize(viewServices.GetRequiredService<JobManager>());
					return view;
				}
			});
			uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView
			{
				Id = "core.dashboard",
				Title = "Dashboard",
				Group = "Core",
				Order = 5,
				CreateView = () =>
				{
					var view = new PolyTraderSharp.Ui.Views.DashboardView();
					var config = viewServices.GetRequiredService<Microsoft.Extensions.Configuration.IConfiguration>();
					// Alle Module (auch deaktivierte) fürs Dashboard: läuft-Status = ist in dieser Session geladen.
					var moduleInfos = allModules
						.Select(m => new PolyTrader.Core.Modularity.ModuleActivationInfo(
							m.Name, modules.Contains(m), m.GetActivationBlocker(config)))
						.ToList();
					view.Initialize(
						viewServices.GetRequiredService<PolyTrader.Core.Persistence.ITradeLogRepository>(),
						viewServices.GetRequiredService<TradingState>(),
						moduleInfos,
						ServerSettingsPath);
					return view;
				}
			});
			// Modul-UI registrieren (Module steuern ihre Views selbst bei).
			foreach (var module in modules)
			{
				module.RegisterUi(uiHost, viewServices);
			}

			// Menü-Icons zentral zuweisen: Die App kennt alle Fenster-Ressourcen; Core und Module
			// referenzieren sie nicht. So erscheint jedes Fenster mit Icon in der obersten Menüleiste.
			AssignMenuIcons(uiHost);

			var launcher = AppHost.Services.GetRequiredService<PolyTraderSharp.Ui.LauncherForm>();
			Application.Run(launcher);
			AppHost.StopAsync().GetAwaiter().GetResult();
		}

	/// <summary>
	/// Weist den registrierten Views (Core + Module) ihren Standard-Symbolschlüssel zu.
	/// Die Auflösung Schlüssel → Bildressource liegt in <see cref="PolyTraderSharp.Ui.ViewIcons"/>;
	/// Core und Module kennen nur noch den Schlüssel (toolkit-neutral, siehe ModuleView.IconKey).
	/// </summary>
	private static void AssignMenuIcons(PolyTraderSharp.Ui.ShellUiHost uiHost)
		=> PolyTraderSharp.Ui.ViewIcons.AssignDefaults(uiHost);

	private static void RunConfigMigrationFromJson(string folder)
	{
		var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
			.SetBasePath(System.IO.Directory.GetCurrentDirectory())
			.AddJsonFile("appsettings.json", optional: true)
			.AddJsonFile("appsettings.Local.json", optional: true)
			.AddEnvironmentVariables()
			.Build();

		var mySql = config["Database:MySqlConnectionString"] ?? string.Empty;
		if (string.IsNullOrWhiteSpace(mySql))
		{
			Console.WriteLine("FEHLER: Database:MySqlConnectionString fehlt (appsettings.Local.json).");
			return;
		}

		if (!System.IO.Path.IsPathRooted(folder))
			folder = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), folder);

		Services.ConfigMigrator.RunFromJson(folder, mySql);
	}

	private static void RunVerifyMySql()
	{
		var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
			.SetBasePath(System.IO.Directory.GetCurrentDirectory())
			.AddJsonFile("appsettings.json", optional: true)
			.AddJsonFile("appsettings.Local.json", optional: true)
			.AddEnvironmentVariables()
			.Build();

		var mySql = config["Database:MySqlConnectionString"] ?? string.Empty;
		if (string.IsNullOrWhiteSpace(mySql))
		{
			Console.WriteLine("FEHLER: Database:MySqlConnectionString fehlt (appsettings.Local.json).");
			return;
		}

		Services.ConfigMigrator.VerifyMySql(mySql);
	}

	private static void RunDbVersion()
	{
		var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
			.SetBasePath(System.IO.Directory.GetCurrentDirectory())
			.AddJsonFile("appsettings.json", optional: true)
			.AddJsonFile("appsettings.Local.json", optional: true)
			.AddEnvironmentVariables()
			.Build();

		var conn = config["Database:MySqlConnectionString"] ?? string.Empty;
		if (string.IsNullOrWhiteSpace(conn))
		{
			Console.WriteLine("FEHLER: Database:MySqlConnectionString fehlt (appsettings.Local.json).");
			return;
		}

		try
		{
			using var c = new MySqlConnector.MySqlConnection(conn);
			c.Open();
			Console.WriteLine($"ServerVersion: {c.ServerVersion}");
		}
		catch (Exception ex)
		{
			Console.WriteLine($"FEHLER: {ex.GetType().Name}: {ex.Message}");
		}
	}

	/// <summary>
	/// F1: Lädt den Master-Key (POLYTRADER_MASTER_KEY, sonst gitignorierte master.key) und aktiviert die
	/// at-rest-Verschlüsselung. Ohne Key läuft die App wie bisher mit Klartext – mit deutlicher Warnung
	/// (kein stiller Sicherheitsverlust). Muss VOR jeder Credential-Entschlüsselung laufen.
	/// </summary>
	private static void ConfigureSecretProtection(TerminalLogger logger)
	{
		string? masterKey = Environment.GetEnvironmentVariable("POLYTRADER_MASTER_KEY");
		if (string.IsNullOrWhiteSpace(masterKey))
		{
			string keyFile = Path.Combine(AppContext.BaseDirectory, "master.key");
			if (File.Exists(keyFile)) masterKey = File.ReadAllText(keyFile).Trim();
		}
		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. " +
						   "Master-Key setzen (env POLYTRADER_MASTER_KEY oder master.key), siehe docs/sicherheit.");
	}

	/// <summary>
	/// F5: Warnt, wenn der DB-Connection-String keine TLS-Erzwingung (SslMode) enthält. Der String
	/// selbst wird NICHT geloggt (enthält das Passwort) – nur das Fehlen der TLS-Option.
	/// </summary>
	private static void WarnIfDbTlsNotEnforced(IServiceProvider services)
	{
		try
		{
			var config = services.GetService<Microsoft.Extensions.Configuration.IConfiguration>();
			string conn = config?["Database:MySqlConnectionString"] ?? string.Empty;
			if (string.IsNullOrEmpty(conn)) return;
			if (conn.IndexOf("sslmode", StringComparison.OrdinalIgnoreCase) < 0)
			{
				services.GetRequiredService<TerminalLogger>().Warning(
					"⚠️ SICHERHEIT: DB-Verbindung ohne SslMode – Transportverschlüsselung zur (remote) MySQL nicht erzwungen. " +
					"Im Connection-String 'SslMode=Required' (oder VerifyFull) setzen, siehe docs/sicherheit.");
			}
		}
		catch { /* Warnung ist best-effort; darf den Start nie stören */ }
	}

	/// <summary>
	/// F1: Verschlüsselt einmalig/idempotent evtl. vorhandene Klartext-Credentials in der DB (Alt-Bestand
	/// wird beim Re-Save durch den EF-Converter verschlüsselt). Nur wenn ein Master-Key gesetzt ist.
	/// Fehler blockieren den Start nicht (Log); künftige Saves verschlüsseln ohnehin.
	/// </summary>
	private static void ReencryptAccountCredentials(IServiceProvider services)
	{
		if (!SecretProtection.IsConfigured) return;
		var logger = services.GetRequiredService<TerminalLogger>();
		try
		{
			var accountRepo = services.GetRequiredService<IAccountRepository>();
			var accounts = accountRepo.GetAll();          // Converter entschlüsselt/passt Klartext durch
			foreach (var acc in accounts) accountRepo.Upsert(acc);   // Re-Save → Converter verschlüsselt
			logger.Info($"🔐 Account-Credentials at-rest gesichert ({accounts.Count} Account(s)).");
		}
		catch (Exception ex)
		{
			logger.Error($"Re-Encryption der Account-Credentials fehlgeschlagen: {ex.Message}. " +
						 "Stimmt der Master-Key mit den bereits verschlüsselten Daten überein?");
		}
	}

	/// <summary>
	/// Headless-Smoke-Test: baut einen minimalen Host (Persistenz + State + Modul-Registrierung
	/// + Shell/Launcher), hydriert den State aus MySQL und konstruiert jede registrierte View
	/// sowie den Launcher – ohne Message-Loop und ohne Trading-/WSS-Services zu starten.
	/// Gibt 0 zurück, wenn alles fehlerfrei konstruiert, sonst die Fehleranzahl.
	/// </summary>
	private static int RunSmokeUi()
	{
		ApplicationConfiguration.Initialize();

		var modules = new System.Collections.Generic.List<IPolyTraderModule> { new CopyTradingModule(), new ResolutionFarmingModule(), new SupervisorModule(), new AccountingModule() };

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

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

			services.AddSingleton<PolyTraderSharp.Ui.ShellUiHost>();
			services.AddTransient<PolyTraderSharp.Ui.LauncherForm>();
		}).Build();

		// F1: Master-Key laden, damit die Hydration verschlüsselte Credentials entschlüsseln kann.
		ConfigureSecretProtection(host.Services.GetRequiredService<TerminalLogger>());

		// State aus MySQL laden (Accounts/Trader/Settings), ohne die BackgroundServices zu starten.
		try
		{
			host.Services.GetRequiredService<CoreStateHydrationService>()
				.StartAsync(default).GetAwaiter().GetResult();
			host.Services.GetRequiredService<CopyTradingHydrationService>()
				.StartAsync(default).GetAwaiter().GetResult();
		}
		catch (Exception ex)
		{
			Console.WriteLine($"[WARN] Hydration übersprungen: {ex.Message}");
		}

		var uiHost = host.Services.GetRequiredService<PolyTraderSharp.Ui.ShellUiHost>();
		foreach (var module in modules)
			module.RegisterUi(uiHost, host.Services);

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

		try
		{
			using var launcher = host.Services.GetRequiredService<PolyTraderSharp.Ui.LauncherForm>();
			Console.WriteLine("[OK] LauncherForm konstruiert");
		}
		catch (Exception ex)
		{
			failures++;
			Console.WriteLine($"[FEHLER] LauncherForm: {ex.GetType().Name}: {ex.Message}");
		}

		// Core-Dashboard direkt konstruieren (im Smoke nicht via uiHost registriert) - prueft das ScottPlot-Rendering headless.
		try
		{
			using var dash = new PolyTraderSharp.Ui.Views.DashboardView();
			dash.Initialize(host.Services.GetRequiredService<ITradeLogRepository>(), host.Services.GetRequiredService<TradingState>());
			Console.WriteLine("[OK] core.dashboard konstruiert (inkl. Charts)");
		}
		catch (Exception ex)
		{
			failures++;
			Console.WriteLine($"[FEHLER] core.dashboard: {ex.GetType().Name}: {ex.Message}");
		}

		// Core-Views mit hand-erstellten Grid-Spalten direkt konstruieren (nicht im uiHost registriert):
		// faengt VS-Re-Serialisierungs-Regressionen (fallengelassene DataGridView-Spalten) ab.
		foreach (var (id, make) in new (string, Func<System.Windows.Forms.Form>)[]
		{
			("core.jobs", () => new PolyTraderSharp.Ui.Views.JobsView()),
			("core.terminal", () => new PolyTraderSharp.Ui.Views.TerminalView()),
			("core.settings", () => new PolyTraderSharp.Ui.Views.SettingsView()),
		})
		{
			try { using var f = make(); Console.WriteLine($"[OK] {id} konstruiert"); }
			catch (Exception ex) { failures++; Console.WriteLine($"[FEHLER] {id}: {ex.GetType().Name}: {ex.Message}"); }
		}

		// B.4: Lizenzdialog headless konstruieren (kein Netz, keine Message-Loop) – fängt
		// Designer-/Konstruktionsregressionen ab.
		try
		{
			var licClient = new LicenseLabrador.Client.LicenseClient(PolyTraderSharp.Licensing.LicenseGate.BuildConfig());
			var dummy = new LicenseLabrador.Client.LicenseResult(
				LicenseLabrador.Client.LicenseState.NoLicense, null, null, "Smoke-Test", string.Empty);
			using (var licDlg = new PolyTraderSharp.Ui.LicenseDialog(licClient, string.Empty, dummy, startupContext: true))
			using (var licDlgManage = new PolyTraderSharp.Ui.LicenseDialog(licClient, string.Empty, null, startupContext: false))
				Console.WriteLine("[OK] license.dialog konstruiert (Start- und Verwalten-Modus)");
		}
		catch (Exception ex) { failures++; Console.WriteLine($"[FEHLER] license.dialog: {ex.GetType().Name}: {ex.Message}"); }

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