import 'backend_config.dart'; /// Eine auswählbare Server-Umgebung (z. B. Produktion, Test). /// /// Quelle ist ausschließlich `assets/hl_server_config.json` — die App /// kennt keine hartcodierten Server. Siehe [ServerCatalog]. class BackendEnvironment { const BackendEnvironment({ required this.id, required this.name, required this.apiBaseUrl, required this.keycloakIssuerUrl, }); /// Stabiler Schlüssel; wird als Auswahl persistiert und trennt lokale, /// umgebungsabhängige Daten (z. B. Fahrzeugauswahl). final String id; /// Anzeigename in der Umgebungsauswahl. final String name; /// Basis-URL der Rust-API (ohne abschließenden Slash). final String apiBaseUrl; /// Realm-Issuer der Keycloak-Instanz dieser Umgebung. final String keycloakIssuerUrl; /// „IP:Port“ der API für die Anzeige, z. B. `192.168.1.9:3001`. String get hostAndPort { final uri = Uri.parse(apiBaseUrl); return uri.hasPort ? '${uri.host}:${uri.port}' : uri.host; } factory BackendEnvironment.fromJson(Map json) { return BackendEnvironment( id: _requireString(json, 'id'), name: _requireString(json, 'name'), apiBaseUrl: _stripTrailingSlash(_requireString(json, 'apiBaseUrl')), keycloakIssuerUrl: _stripTrailingSlash(_requireString(json, 'keycloakIssuerUrl')), ); } } /// Alle in der Config-Datei hinterlegten Umgebungen plus die /// umgebungsübergreifenden Keycloak-Client-Daten. /// /// Client-Id und Redirect-URL sind global, weil sie an die native /// Plattform-Konfiguration gebunden sind (Redirect-Scheme in /// `build.gradle.kts` / `Info.plist`) und nicht pro Server variieren. class ServerCatalog { ServerCatalog({ required this.environments, required this.defaultEnvironmentId, required this.keycloakClientId, required this.keycloakRedirectUrl, }) { if (environments.isEmpty) { throw const FormatException('Config enthält keine Umgebungen.'); } final ids = environments.map((e) => e.id).toSet(); if (ids.length != environments.length) { throw const FormatException('Umgebungs-IDs in der Config sind doppelt.'); } if (!ids.contains(defaultEnvironmentId)) { throw FormatException( 'defaultEnvironment "$defaultEnvironmentId" existiert nicht.', ); } } final List environments; final String defaultEnvironmentId; final String keycloakClientId; final String keycloakRedirectUrl; BackendEnvironment get defaultEnvironment => byId(defaultEnvironmentId)!; BackendEnvironment? byId(String? id) { for (final env in environments) { if (env.id == id) return env; } return null; } /// Vollständige Netzwerk-Konfiguration für [environment]. BackendConfig configFor(BackendEnvironment environment) { return BackendConfig( apiBaseUrl: environment.apiBaseUrl, keycloakIssuerUrl: environment.keycloakIssuerUrl, keycloakClientId: keycloakClientId, keycloakRedirectUrl: keycloakRedirectUrl, ); } factory ServerCatalog.fromJson(Map json) { final keycloak = json['keycloak']; if (keycloak is! Map) { throw const FormatException('Abschnitt "keycloak" fehlt in der Config.'); } final rawEnvironments = json['environments']; if (rawEnvironments is! List) { throw const FormatException('Liste "environments" fehlt in der Config.'); } return ServerCatalog( environments: rawEnvironments .map((e) => BackendEnvironment.fromJson(e as Map)) .toList(growable: false), defaultEnvironmentId: _requireString(json, 'defaultEnvironment'), keycloakClientId: _requireString(keycloak, 'clientId'), keycloakRedirectUrl: _requireString(keycloak, 'redirectUrl'), ); } } String _requireString(Map json, String key) { final value = json[key]; if (value is String && value.trim().isNotEmpty) return value.trim(); throw FormatException('Feld "$key" fehlt oder ist leer in der Config.'); } String _stripTrailingSlash(String url) => url.endsWith('/') ? url.substring(0, url.length - 1) : url;