using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace game.PlayerDataStorage
{
///
/// Private KV protocol for one fixed authenticated account/environment.
/// Caller owns the HttpClient, cookies and cancellation lifetime; never reuse
/// that transport for another account. This class neither retries nor logs payloads.
/// Production login/storage integration is deliberately outside this adapter.
///
public sealed class PlayerKVClient
{
readonly HttpClient http;
readonly Uri api;
public PlayerKVClient(HttpClient http, Uri apiBaseUri)
{
this.http = http ?? throw new ArgumentNullException(nameof(http));
if (apiBaseUri == null || !apiBaseUri.IsAbsoluteUri ||
(apiBaseUri.Scheme != "http" && apiBaseUri.Scheme != "https") ||
!string.IsNullOrEmpty(apiBaseUri.UserInfo) ||
!string.IsNullOrEmpty(apiBaseUri.Query) || !string.IsNullOrEmpty(apiBaseUri.Fragment))
throw new ArgumentException("An absolute HTTP(S) API base URI is required.", nameof(apiBaseUri));
api = new Uri(apiBaseUri.AbsoluteUri.TrimEnd('/') + "/");
}
public Task GetAsync(IEnumerable keys = null, CancellationToken token = default)
{
if (!CopyKeys(keys, true, out var snapshot)) return Invalid();
var body = new JObject();
if (snapshot.Count > 0) body["keys"] = new JArray(snapshot);
return SendAsync("Get", body, false, token);
}
public Task SetAsync(IDictionary data, CancellationToken token = default)
{
if (data == null) return Invalid();
var snapshot = new JObject();
foreach (var pair in data)
{
if (!ValidKey(pair.Key) || pair.Value == null) return Invalid();
snapshot.Add(pair.Key, pair.Value);
}
return SendAsync("Set", new JObject { ["data"] = snapshot }, true, token);
}
public Task DeleteAsync(IEnumerable keys, CancellationToken token = default)
{
if (!CopyKeys(keys, false, out var snapshot)) return Invalid();
return SendAsync("Delete", new JObject { ["keys"] = new JArray(snapshot) }, true, token);
}
static bool ValidKey(string key) => !string.IsNullOrEmpty(key) && key.Length <= 128;
static bool CopyKeys(IEnumerable keys, bool allowNull, out List snapshot)
{
snapshot = new List();
if (keys == null) return allowNull;
foreach (var key in keys)
{
if (!ValidKey(key)) return false;
snapshot.Add(key);
}
return true;
}
static Task Invalid() => Task.FromResult(new PlayerKVResult(PlayerKVError.Validation));
async Task SendAsync(string action, JObject body, bool mutation, CancellationToken token)
{
if (token.IsCancellationRequested) return new PlayerKVResult(PlayerKVError.Cancelled);
int? status = null;
bool enteredTransport = false;
try
{
using (var request = new HttpRequestMessage(HttpMethod.Post, new Uri(api, "playerdata/" + action)))
{
request.Content = new StringContent(body.ToString(Formatting.None), Encoding.UTF8, "application/json");
token.ThrowIfCancellationRequested();
enteredTransport = true;
using (var response = await http.SendAsync(request, token))
{
status = (int)response.StatusCode;
token.ThrowIfCancellationRequested();
if (response.StatusCode != HttpStatusCode.OK)
return new PlayerKVResult(status == 401 || status == 403
? PlayerKVError.Unauthorized : PlayerKVError.Http, status,
mayHaveCommitted: mutation);
if (mutation) return new PlayerKVResult(PlayerKVError.None, status);
var text = await response.Content.ReadAsStringAsync();
token.ThrowIfCancellationRequested();
return new PlayerKVResult(PlayerKVError.None, status, ParseData(text));
}
}
}
catch (OperationCanceledException)
{
return new PlayerKVResult(token.IsCancellationRequested ? PlayerKVError.Cancelled : PlayerKVError.Timeout,
status, mayHaveCommitted: mutation && enteredTransport);
}
catch (ObjectDisposedException)
{
return new PlayerKVResult(PlayerKVError.Cancelled, status, mayHaveCommitted: mutation && enteredTransport);
}
catch (HttpRequestException)
{
return new PlayerKVResult(PlayerKVError.Network, status, mayHaveCommitted: mutation && enteredTransport);
}
catch (IOException)
{
return new PlayerKVResult(PlayerKVError.Network, status, mayHaveCommitted: mutation && enteredTransport);
}
catch (JsonException)
{
return new PlayerKVResult(PlayerKVError.Protocol, status, mayHaveCommitted: mutation && enteredTransport);
}
}
static Dictionary ParseData(string text)
{
using (var reader = new JsonTextReader(new StringReader(text)) { DateParseHandling = DateParseHandling.None })
{
var body = JObject.Load(reader, new JsonLoadSettings { DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Error });
if (reader.Read() || !(body["data"] is JObject data)) throw new JsonReaderException("Invalid KV response.");
var result = new Dictionary(StringComparer.Ordinal);
foreach (var property in data.Properties())
{
if (property.Value.Type != JTokenType.String) throw new JsonReaderException("KV value must be a string.");
result.Add(property.Name, property.Value.Value());
}
return result;
}
}
}
}