/ GUNRPG.Infrastructure / Backend / GameBackendResolver.cs
GameBackendResolver.cs
  1  using System.Text.Json;
  2  using GUNRPG.Application.Backend;
  3  using GUNRPG.Infrastructure.Backend;
  4  using GUNRPG.Infrastructure.Persistence;
  5  using LiteDB;
  6  
  7  namespace GUNRPG.Infrastructure;
  8  
  9  /// <summary>
 10  /// Resolves the appropriate game backend (online or offline) based on current state.
 11  /// Mode is determined by operator state, not configuration.
 12  /// </summary>
 13  public sealed class GameBackendResolver
 14  {
 15      private readonly HttpClient _httpClient;
 16      private readonly OfflineStore _offlineStore;
 17      private readonly JsonSerializerOptions _jsonOptions;
 18  
 19      public GameBackendResolver(HttpClient httpClient, OfflineStore offlineStore, JsonSerializerOptions? jsonOptions = null)
 20      {
 21          _httpClient = httpClient;
 22          _offlineStore = offlineStore;
 23          _jsonOptions = jsonOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web);
 24      }
 25  
 26      /// <summary>
 27      /// The current mode of operation.
 28      /// </summary>
 29      public GameMode CurrentMode { get; private set; } = GameMode.Online;
 30  
 31      /// <summary>
 32      /// Resolves the appropriate backend based on server reachability and local state.
 33      /// When the server is reachable and an infiled operator exists, sync is run first.
 34      /// Gameplay is blocked if sync fails (chain-of-trust enforcement).
 35      /// </summary>
 36      public async Task<IGameBackend> ResolveAsync()
 37      {
 38          var serverReachable = await IsServerReachableAsync();
 39  
 40          if (serverReachable)
 41          {
 42              var onlineBackend = new OnlineGameBackend(_httpClient, _offlineStore, _jsonOptions);
 43  
 44              // If an operator was infiled offline, sync must succeed before allowing online play.
 45              var activeOp = _offlineStore.GetActiveInfiledOperator();
 46              if (activeOp != null)
 47              {
 48                  Console.WriteLine($"[SYNC] Infiled operator {activeOp.Id} found — synchronizing before returning online.");
 49                  IExfilSyncService syncService = new ExfilSyncService(_offlineStore, onlineBackend);
 50                  var syncResult = await syncService.SyncAsync(activeOp.Id);
 51                  if (!syncResult.Success)
 52                  {
 53                      Console.WriteLine($"[SYNC] Sync failed: {syncResult.FailureReason}. Gameplay blocked until operator is re-infiled.");
 54                      if (syncResult.IsIntegrityFailure)
 55                      {
 56                          // Integrity violation is permanent — remove the snapshot so subsequent
 57                          // calls to ResolveAsync don't loop on the same unresolvable failure.
 58                          // The operator must re-infil with a clean slate.
 59                          _offlineStore.RemoveInfiledOperator(activeOp.Id);
 60                          Console.WriteLine($"[SYNC] Infiled snapshot for operator {activeOp.Id} removed. Re-infil required.");
 61                      }
 62                      CurrentMode = GameMode.Blocked;
 63                      return onlineBackend;
 64                  }
 65  
 66                  Console.WriteLine($"[SYNC] Sync succeeded — {syncResult.EnvelopesSynced} envelope(s) uploaded.");
 67              }
 68              else
 69              {
 70                  // No infiled operator: log any residual unsynced results from other operators.
 71                  LogUnsyncedResults();
 72              }
 73  
 74              CurrentMode = GameMode.Online;
 75              Console.WriteLine("[MODE] Online mode — server is reachable.");
 76              return onlineBackend;
 77          }
 78  
 79          if (_offlineStore.HasActiveInfiledOperator())
 80          {
 81              CurrentMode = GameMode.Offline;
 82              var activeOp = _offlineStore.GetActiveInfiledOperator();
 83              Console.WriteLine($"[MODE] Offline mode — server unreachable, using infiled operator snapshot (ID: {activeOp?.Id}).");
 84              LogUnsyncedResults();
 85              return new OfflineGameBackend(_offlineStore);
 86          }
 87  
 88          CurrentMode = GameMode.Blocked;
 89          Console.WriteLine("[MODE] Blocked — server unreachable and no infiled operator available. Gameplay blocked.");
 90          return new OnlineGameBackend(_httpClient, _offlineStore, _jsonOptions);
 91      }
 92  
 93      /// <summary>
 94      /// Logs the count of unsynced offline mission results.
 95      /// </summary>
 96      private void LogUnsyncedResults()
 97      {
 98          var unsyncedResults = _offlineStore.GetAllUnsyncedResults();
 99          if (unsyncedResults.Count > 0)
100          {
101              Console.WriteLine($"[SYNC] {unsyncedResults.Count} unsynced offline mission result(s) pending.");
102          }
103      }
104  
105      /// <summary>
106      /// Checks if the API server is reachable.
107      /// </summary>
108      public async Task<bool> IsServerReachableAsync()
109      {
110          try
111          {
112              using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
113              using var response = await _httpClient.GetAsync(
114                  "health",
115                  HttpCompletionOption.ResponseHeadersRead,
116                  cts.Token);
117              // Any completed HTTP response (regardless of status code) means the server is reachable.
118              return true;
119          }
120          catch (Exception ex)
121          {
122              Console.WriteLine($"[MODE] Server connectivity check failed: {ex.Message}");
123              return false;
124          }
125      }
126  }
127  
128  /// <summary>
129  /// Represents the current game mode.
130  /// </summary>
131  public enum GameMode
132  {
133      /// <summary>Server reachable, full functionality available.</summary>
134      Online,
135      /// <summary>Server unreachable, using infiled operator snapshot.</summary>
136      Offline,
137      /// <summary>Server unreachable, no infiled operator — gameplay blocked.</summary>
138      Blocked
139  }