using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Execution;
/// <summary>Preserves failed native receipts while recognizing a later, actual manual delivery.</summary>
public static class ExternalTradeSettlement
{
private static JsonElement Field(JsonElement row,string name)=>row.EnumerateObject().FirstOrDefault(p=>p.Name.Equals(name,StringComparison.OrdinalIgnoreCase)).Value;
private static string Text(JsonElement row,string name)=>Field(row,name) is {ValueKind:JsonValueKind.String} v?v.GetString()!:"";
private static long Number(JsonElement row,string name)=>Field(row,name) is {ValueKind:JsonValueKind.Number} v&&v.TryGetInt64(out var n)?n:0;
public static bool IsSettled(string pending,PlayerState player)
{
var marker=pending.Replace(".pending.json",".externally-settled.json",StringComparison.Ordinal);
if(!File.Exists(marker))return false;
using var document=JsonDocument.Parse(File.ReadAllText(marker));var row=document.RootElement;
return Text(row,"status")=="manual-sale-confirmed"&&Text(row,"commanderId")==player.CommanderId
&&Text(row,"pendingSha256")==Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(pending)));
}
public static async Task ReconcileAsync(string root,string journalDirectory,PlayerState player,JsonElement inventory,CancellationToken token)
{
if(!player.InGame||!player.CargoReliable||player.CargoCount!=0||!inventory.GetProperty("verified").GetBoolean()||inventory.GetProperty("count").GetInt32()!=0)return;
foreach(var pending in Directory.EnumerateFiles(root,"*.pending.json",SearchOption.AllDirectories))
{
if(File.Exists(pending.Replace(".pending.json",".confirmed.json",StringComparison.Ordinal))||IsSettled(pending,player))continue;
using var original=JsonDocument.Parse(await File.ReadAllTextAsync(pending,token));var row=original.RootElement;
var intent=Field(row,"intent");if(intent.ValueKind!=JsonValueKind.Object)continue;
var delta=Number(intent,"delta");var commodity=Text(intent,"commodity");var price=Number(intent,"unitPrice");
var commander=Text(row,"commanderId");var ship=Number(row,"currentShipId");
if(delta<=0||price<=0||commodity.Length==0||commander!=player.CommanderId||ship<=0||Number(Field(intent,"before"),"count")!=0)continue;
if(!DateTimeOffset.TryParse(Text(row,"created"),out var created))continue;
string? cargoProof=null,saleProof=null;var conflict=false;
foreach(var path in Directory.EnumerateFiles(journalDirectory,"Journal*.log").Where(p=>File.GetLastWriteTimeUtc(p)>=created.UtcDateTime).OrderBy(p=>p,StringComparer.Ordinal))
{
await using var stream=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);
using var reader=new StreamReader(stream);var fid="";long actualShip=0;
while(await reader.ReadLineAsync(token) is {} line)
{
if(!line.Contains("\"LoadGame\"",StringComparison.Ordinal)&&!line.Contains("\"Loadout\"",StringComparison.Ordinal)
&&!line.Contains("\"Cargo\"",StringComparison.Ordinal)&&!line.Contains("\"MarketSell\"",StringComparison.Ordinal)&&!line.Contains("\"MarketBuy\"",StringComparison.Ordinal))continue;
using var document=JsonDocument.Parse(line);var item=document.RootElement;var kind=Text(item,"event");
if(kind=="LoadGame"){fid=Text(item,"FID");actualShip=Number(item,"ShipID");}
if(kind=="Loadout")actualShip=Number(item,"ShipID");
if(fid!=commander||actualShip!=ship||!DateTimeOffset.TryParse(Text(item,"timestamp"),out var at)||at<created)continue;
if(kind=="MarketBuy"&&Text(item,"Type").Equals(commodity,StringComparison.OrdinalIgnoreCase)){conflict=true;break;}
if(kind=="Cargo"&&Number(item,"Count")==delta&&Field(item,"Inventory") is {ValueKind:JsonValueKind.Array} items
&&items.EnumerateArray().All(i=>Text(i,"Name").Equals(commodity,StringComparison.OrdinalIgnoreCase)&&Number(i,"Stolen")==0)
&&items.EnumerateArray().Sum(i=>Number(i,"Count"))==delta)cargoProof=line;
if(kind=="MarketSell"&&cargoProof is not null&&Text(item,"Type").Equals(commodity,StringComparison.OrdinalIgnoreCase)
&&Number(item,"Count")==delta&&Number(item,"AvgPricePaid")==price&&Number(item,"TotalSale")==(decimal)Number(item,"SellPrice")*delta)
{saleProof=line;break;}
}
if(conflict||saleProof is not null)break;
}
if(conflict||saleProof is null)continue;
var marker=pending.Replace(".pending.json",".externally-settled.json",StringComparison.Ordinal);
var data=new{status="manual-sale-confirmed",commanderId=commander,shipId=ship,quantity=delta,commodity,unitPurchasePrice=price,
operationKey=Text(row,"operationKey"),pendingSha256=Convert.ToHexStringLower(SHA256.HashData(await File.ReadAllBytesAsync(pending,token))),
cargoProof=JsonSerializer.Deserialize<JsonElement>(cargoProof!),saleProof=JsonSerializer.Deserialize<JsonElement>(saleProof),
currentEmptyInventory=inventory,at=DateTimeOffset.UtcNow,originalNativeReceiptRewritten=false,unknownCostsAssigned=false};
await File.WriteAllTextAsync(marker+".tmp",JsonSerializer.Serialize(data,ToolboxSettings.JsonOptions),token);File.Move(marker+".tmp",marker,true);
}
}
}
using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Execution;
/// <summary>Preserves failed native receipts while recognizing a later, actual manual delivery.</summary>
public static class ExternalTradeSettlement
{
private static JsonElement Field(JsonElement row,string name)=>row.EnumerateObject().FirstOrDefault(p=>p.Name.Equals(name,StringComparison.OrdinalIgnoreCase)).Value;
private static string Text(JsonElement row,string name)=>Field(row,name) is {ValueKind:JsonValueKind.String} v?v.GetString()!:"";
private static long Number(JsonElement row,string name)=>Field(row,name) is {ValueKind:JsonValueKind.Number} v&&v.TryGetInt64(out var n)?n:0;
public static bool IsSettled(string pending,PlayerState player)
{
var marker=pending.Replace(".pending.json",".externally-settled.json",StringComparison.Ordinal);
if(!File.Exists(marker))return false;
using var document=JsonDocument.Parse(File.ReadAllText(marker));var row=document.RootElement;
return Text(row,"status")=="manual-sale-confirmed"&&Text(row,"commanderId")==player.CommanderId
&&Text(row,"pendingSha256")==Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(pending)));
}
public static async Task ReconcileAsync(string root,string journalDirectory,PlayerState player,JsonElement inventory,CancellationToken token)
{
if(!player.InGame||!player.CargoReliable||player.CargoCount!=0||!inventory.GetProperty("verified").GetBoolean()||inventory.GetProperty("count").GetInt32()!=0)return;
foreach(var pending in Directory.EnumerateFiles(root,"*.pending.json",SearchOption.AllDirectories))
{
if(File.Exists(pending.Replace(".pending.json",".confirmed.json",StringComparison.Ordinal))||IsSettled(pending,player))continue;
using var original=JsonDocument.Parse(await File.ReadAllTextAsync(pending,token));var row=original.RootElement;
var intent=Field(row,"intent");if(intent.ValueKind!=JsonValueKind.Object)continue;
var delta=Number(intent,"delta");var commodity=Text(intent,"commodity");var price=Number(intent,"unitPrice");
var commander=Text(row,"commanderId");var ship=Number(row,"currentShipId");
if(delta<=0||price<=0||commodity.Length==0||commander!=player.CommanderId||ship<=0||Number(Field(intent,"before"),"count")!=0)continue;
if(!DateTimeOffset.TryParse(Text(row,"created"),out var created))continue;
string? cargoProof=null,saleProof=null;var conflict=false;
foreach(var path in Directory.EnumerateFiles(journalDirectory,"Journal*.log").Where(p=>File.GetLastWriteTimeUtc(p)>=created.UtcDateTime).OrderBy(p=>p,StringComparer.Ordinal))
{
await using var stream=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);
using var reader=new StreamReader(stream);var fid="";long actualShip=0;
while(await reader.ReadLineAsync(token) is {} line)
{
if(!line.Contains("\"LoadGame\"",StringComparison.Ordinal)&&!line.Contains("\"Loadout\"",StringComparison.Ordinal)
&&!line.Contains("\"Cargo\"",StringComparison.Ordinal)&&!line.Contains("\"MarketSell\"",StringComparison.Ordinal)&&!line.Contains("\"MarketBuy\"",StringComparison.Ordinal))continue;
using var document=JsonDocument.Parse(line);var item=document.RootElement;var kind=Text(item,"event");
if(kind=="LoadGame"){fid=Text(item,"FID");actualShip=Number(item,"ShipID");}
if(kind=="Loadout")actualShip=Number(item,"ShipID");
if(fid!=commander||actualShip!=ship||!DateTimeOffset.TryParse(Text(item,"timestamp"),out var at)||at<created)continue;
if(kind=="MarketBuy"&&Text(item,"Type").Equals(commodity,StringComparison.OrdinalIgnoreCase)){conflict=true;break;}
if(kind=="Cargo"&&Number(item,"Count")==delta&&Field(item,"Inventory") is {ValueKind:JsonValueKind.Array} items
&&items.EnumerateArray().All(i=>Text(i,"Name").Equals(commodity,StringComparison.OrdinalIgnoreCase)&&Number(i,"Stolen")==0)
&&items.EnumerateArray().Sum(i=>Number(i,"Count"))==delta)cargoProof=line;
if(kind=="MarketSell"&&cargoProof is not null&&Text(item,"Type").Equals(commodity,StringComparison.OrdinalIgnoreCase)
&&Number(item,"Count")==delta&&Number(item,"AvgPricePaid")==price&&Number(item,"TotalSale")==(decimal)Number(item,"SellPrice")*delta)
{saleProof=line;break;}
}
if(conflict||saleProof is not null)break;
}
if(conflict||saleProof is null)continue;
var marker=pending.Replace(".pending.json",".externally-settled.json",StringComparison.Ordinal);
var data=new{status="manual-sale-confirmed",commanderId=commander,shipId=ship,quantity=delta,commodity,unitPurchasePrice=price,
operationKey=Text(row,"operationKey"),pendingSha256=Convert.ToHexStringLower(SHA256.HashData(await File.ReadAllBytesAsync(pending,token))),
cargoProof=JsonSerializer.Deserialize<JsonElement>(cargoProof!),saleProof=JsonSerializer.Deserialize<JsonElement>(saleProof),
currentEmptyInventory=inventory,at=DateTimeOffset.UtcNow,originalNativeReceiptRewritten=false,unknownCostsAssigned=false};
await File.WriteAllTextAsync(marker+".tmp",JsonSerializer.Serialize(data,ToolboxSettings.JsonOptions),token);File.Move(marker+".tmp",marker,true);
}
}
}