using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Execution;
/// <summary>Accounts for an actual official-game manual sale, preserving the failed automated receipt.</summary>
public static class ExternalNativeSaleSettlement
{
public static async Task ReconcileAsync(string root,string journals,PlayerState player,JsonElement inventory,IOriginalCommerceChannel channel,CancellationToken token)
{
if(!player.InGame||!player.Docked||!player.CargoReliable||!player.CreditsReliable||player.CargoCount!=0||player.MarketId is null||!inventory.GetProperty("verified").GetBoolean()||inventory.GetProperty("count").GetInt32()!=0)return;
foreach(var directory in Directory.EnumerateDirectories(root,"journey-*"))
{
var checkpointPath=Path.Combine(directory,"journey.json");var legPath=Path.Combine(directory,"leg.json");
if(!File.Exists(checkpointPath)||!File.Exists(legPath)||File.Exists(Path.Combine(directory,"settled.json")))continue;
var checkpoint=TradeJourneyCheckpoints.Load(checkpointPath);if(checkpoint.Bought is not{} bought||checkpoint.Sold is not null||checkpoint.Commander!=player.CommanderId||checkpoint.ShipId!=player.CurrentShipId)continue;
var leg=JsonSerializer.Deserialize<TradeLeg>(await File.ReadAllTextAsync(legPath,token),ToolboxSettings.JsonOptions)!;
var ops=Path.Combine(root,"operations");var pendingFiles=new List<string>();
foreach(var file in Directory.EnumerateFiles(ops,"*.pending.json"))
{
if(File.Exists(file.Replace(".pending.json",".confirmed.json",StringComparison.Ordinal)))continue;
var statusFile=file.Replace(".pending.json",".status.json",StringComparison.Ordinal);if(!File.Exists(statusFile))continue;
using var status=JsonDocument.Parse(await File.ReadAllTextAsync(statusFile,token));if(!NativeOrderReconciliation.IsServerFailure(status.RootElement))continue;
using var p=JsonDocument.Parse(await File.ReadAllTextAsync(file,token));var row=p.RootElement;var intent=row.GetProperty("intent");
if(row.GetProperty("CommanderId").GetString()==checkpoint.Commander&&row.GetProperty("CurrentShipId").GetInt64()==checkpoint.ShipId
&&intent.GetProperty("market").GetInt64()==leg.To.MarketId&&intent.GetProperty("delta").GetInt32()==-bought.QuantityDelta
&&intent.GetProperty("Commodity").GetString()==bought.Commodity)pendingFiles.Add(file);
}
if(pendingFiles.Count!=1)continue;var pending=pendingFiles[0];
JsonElement? sale=null;var conflict=false;decimal laterTradeCashChange=0;
var laterTrades=new List<JsonElement>();var laterCargo=new Dictionary<string,long>(StringComparer.Ordinal);
foreach(var file in Directory.EnumerateFiles(journals,"Journal*.log").Where(p=>File.GetLastWriteTimeUtc(p)>=bought.ConfirmedAt.UtcDateTime).Order(StringComparer.Ordinal))
{
await using var stream=new FileStream(file,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);using var reader=new StreamReader(stream);var fid="";long ship=0;
while(await reader.ReadLineAsync(token) is{} line)
{
JsonDocument d;try{d=JsonDocument.Parse(line);}catch(JsonException){continue;}
using var parsed=d;var r=d.RootElement;var kind=r.TryGetProperty("event",out var e)?e.GetString():null;
if(kind=="LoadGame"){fid=r.TryGetProperty("FID",out var f)?f.GetString()??"":"";ship=r.TryGetProperty("ShipID",out var s)&&s.TryGetInt64(out var id)?id:0;}
if(kind=="Loadout")ship=r.TryGetProperty("ShipID",out var s)&&s.TryGetInt64(out var id)?id:0;
if(fid!=checkpoint.Commander||ship!=checkpoint.ShipId||!r.TryGetProperty("timestamp",out var t)||t.GetDateTimeOffset()<bought.ConfirmedAt)continue;
if(kind=="MarketBuy")
{
if(sale is null){conflict=true;break;}
var commodity=r.GetProperty("Type").GetString()!;var quantity=r.GetProperty("Count").GetInt32();var cost=r.GetProperty("TotalCost").GetInt64();
if(quantity<=0||cost<=0||cost!=(decimal)quantity*r.GetProperty("BuyPrice").GetInt64()){conflict=true;break;}
laterCargo[commodity]=laterCargo.GetValueOrDefault(commodity)+quantity;laterTradeCashChange-=cost;laterTrades.Add(r.Clone());continue;
}
if(kind!="MarketSell")continue;
if(sale is not null)
{
var commodity=r.GetProperty("Type").GetString()!;var quantity=r.GetProperty("Count").GetInt32();var proceeds=r.GetProperty("TotalSale").GetInt64();
if(quantity<=0||proceeds<=0||proceeds!=(decimal)quantity*r.GetProperty("SellPrice").GetInt64()||laterCargo.GetValueOrDefault(commodity)<quantity){conflict=true;break;}
laterCargo[commodity]-=quantity;laterTradeCashChange+=proceeds;laterTrades.Add(r.Clone());continue;
}
if(r.GetProperty("MarketID").GetInt64()!=leg.To.MarketId||r.GetProperty("Type").GetString()!=bought.Commodity
||r.GetProperty("Count").GetInt32()!=bought.QuantityDelta||r.GetProperty("AvgPricePaid").GetInt64()!=bought.UnitPrice
||r.GetProperty("TotalSale").GetInt64()!=(decimal)r.GetProperty("SellPrice").GetInt64()*bought.QuantityDelta){conflict=true;break;}
sale=r.Clone();
}
if(conflict)break;
}
if(conflict||sale is not{} sold||laterCargo.Values.Any(quantity=>quantity!=0))continue;
var queued=await channel.CallAsync(new{op="commerce.market.read",marketId=player.MarketId.Value,refreshClient=false},token);var generation=queued.GetProperty("generation").GetUInt64();JsonElement market=default;
for(var attempt=0;attempt<350;attempt++)
{market=await channel.CallAsync(new{op="commerce.market.status",generation,maximum=50},token);if(market.GetProperty("state").GetInt32()>=3)break;await Task.Delay(100,token);}
if(market.GetProperty("state").GetInt32()!=3||!market.TryGetProperty("cargo",out var cargo)||cargo.ValueKind!=JsonValueKind.Object||cargo.EnumerateObject().Any(p=>p.Value.GetInt64()!=0))continue;
var actual=await channel.CallAsync(new{op="commerce.inventory"},token);if(!actual.GetProperty("verified").GetBoolean()||actual.GetProperty("count").GetInt32()!=0||actual.GetProperty("credits").GetInt64()!=player.Credits)continue;
if(market.TryGetProperty("serverCredits",out var serverCredits)&&serverCredits.GetInt64()!=player.Credits)continue;
using var ledger=new TradeProfitLedger(Path.Combine(directory,"ledger"));ledger.RecordTrade(bought);
using var openingDocument=JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(directory,"opening-wallet.json"),token));var opening=openingDocument.RootElement.GetProperty("credits").GetInt64();
var key=Convert.ToHexStringLower(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(sold.GetRawText())));var revenue=sold.GetProperty("TotalSale").GetInt64();
var soldAt=sold.GetProperty("timestamp").GetDateTimeOffset();
var allFees=await ExternalServiceCosts.ReadAsync(journals,checkpoint.Commander,checkpoint.ShipId,bought.ConfirmedAt,[leg.From.MarketId,leg.To.MarketId],token);
var fees=allFees.Where(p=>p.Entry.At<=soldAt&&!ledger.ContainsOperation(p.Entry.OperationKey)).ToArray();var laterFees=allFees.Where(p=>p.Entry.At>soldAt).ToArray();
var afterSettlementCashChange=laterTradeCashChange-laterFees.Sum(p=>p.Entry.Amount);
var expected=opening+ledger.Snapshot.CashChange+(ledger.ContainsOperation(key)?0:revenue)-fees.Sum(p=>p.Entry.Amount);
var observedWallet=actual.GetProperty("credits").GetInt64();var closing=observedWallet-afterSettlementCashChange;
var walletReconciled=expected==closing;
ledger.Record(new(key,TradeCashKind.Sale,revenue,leg.To.MarketId,sold.GetProperty("timestamp").GetDateTimeOffset(),bought.Commodity,bought.QuantityDelta));
var feeProofs=Path.Combine(directory,"external-service-proofs");Directory.CreateDirectory(feeProofs);
foreach(var fee in fees){await Save(Path.Combine(feeProofs,fee.Entry.OperationKey+".json"),fee,token);ledger.Record(fee.Entry);}
var proof=new{status="manual-sale-confirmed",proofKind="official-manual-sale-after-native-failure",commanderId=checkpoint.Commander,shipId=checkpoint.ShipId,
operationKey=Path.GetFileName(pending).Replace(".pending.json",""),purchaseOperationKey=bought.OperationKey,
pendingSha256=Convert.ToHexStringLower(SHA256.HashData(await File.ReadAllBytesAsync(pending,token))),saleProof=sold,actualInventory=actual,originalServerCargo=cargo,
observedWallet,afterSettlementCashChange,laterTrades,laterFees,historicalClosingWalletFromVerifiedJournalChain=closing,laterManualTradesIncludedInAutomationProfit=false,
walletReconciled,expectedClosingFromConfirmedReceipts=expected,unexplainedWalletDifference=closing-expected,
originalNativeReceiptRewritten=false,nativeJourneyPassed=false,at=DateTimeOffset.UtcNow};
var marker=pending.Replace(".pending.json",".externally-settled.json",StringComparison.Ordinal);
await Save(marker,proof,token);
await Save(Path.Combine(directory,"external-result.json"),new{passed=walletReconciled,tradeSettled=true,walletReconciled,manual=true,nativeJourneyPassed=false,profit=ledger.Snapshot,openingCredits=opening,closingCredits=closing,finished=DateTimeOffset.UtcNow,proof=marker},token);
await Save(Path.Combine(directory,"external-completion.json"),proof,token);
await Save(Path.Combine(directory,"settled.json"),new{manual=true,nativeJourneyPassed=false,at=DateTimeOffset.UtcNow},token);
}
}
private static async Task Save(string file,object value,CancellationToken token)
{await File.WriteAllTextAsync(file+".tmp",JsonSerializer.Serialize(value,ToolboxSettings.JsonOptions),token);File.Move(file+".tmp",file,true);}
}
using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Execution;
/// <summary>Accounts for an actual official-game manual sale, preserving the failed automated receipt.</summary>
public static class ExternalNativeSaleSettlement
{
public static async Task ReconcileAsync(string root,string journals,PlayerState player,JsonElement inventory,IOriginalCommerceChannel channel,CancellationToken token)
{
if(!player.InGame||!player.Docked||!player.CargoReliable||!player.CreditsReliable||player.CargoCount!=0||player.MarketId is null||!inventory.GetProperty("verified").GetBoolean()||inventory.GetProperty("count").GetInt32()!=0)return;
foreach(var directory in Directory.EnumerateDirectories(root,"journey-*"))
{
var checkpointPath=Path.Combine(directory,"journey.json");var legPath=Path.Combine(directory,"leg.json");
if(!File.Exists(checkpointPath)||!File.Exists(legPath)||File.Exists(Path.Combine(directory,"settled.json")))continue;
var checkpoint=TradeJourneyCheckpoints.Load(checkpointPath);if(checkpoint.Bought is not{} bought||checkpoint.Sold is not null||checkpoint.Commander!=player.CommanderId||checkpoint.ShipId!=player.CurrentShipId)continue;
var leg=JsonSerializer.Deserialize<TradeLeg>(await File.ReadAllTextAsync(legPath,token),ToolboxSettings.JsonOptions)!;
var ops=Path.Combine(root,"operations");var pendingFiles=new List<string>();
foreach(var file in Directory.EnumerateFiles(ops,"*.pending.json"))
{
if(File.Exists(file.Replace(".pending.json",".confirmed.json",StringComparison.Ordinal)))continue;
var statusFile=file.Replace(".pending.json",".status.json",StringComparison.Ordinal);if(!File.Exists(statusFile))continue;
using var status=JsonDocument.Parse(await File.ReadAllTextAsync(statusFile,token));if(!NativeOrderReconciliation.IsServerFailure(status.RootElement))continue;
using var p=JsonDocument.Parse(await File.ReadAllTextAsync(file,token));var row=p.RootElement;var intent=row.GetProperty("intent");
if(row.GetProperty("CommanderId").GetString()==checkpoint.Commander&&row.GetProperty("CurrentShipId").GetInt64()==checkpoint.ShipId
&&intent.GetProperty("market").GetInt64()==leg.To.MarketId&&intent.GetProperty("delta").GetInt32()==-bought.QuantityDelta
&&intent.GetProperty("Commodity").GetString()==bought.Commodity)pendingFiles.Add(file);
}
if(pendingFiles.Count!=1)continue;var pending=pendingFiles[0];
JsonElement? sale=null;var conflict=false;decimal laterTradeCashChange=0;
var laterTrades=new List<JsonElement>();var laterCargo=new Dictionary<string,long>(StringComparer.Ordinal);
foreach(var file in Directory.EnumerateFiles(journals,"Journal*.log").Where(p=>File.GetLastWriteTimeUtc(p)>=bought.ConfirmedAt.UtcDateTime).Order(StringComparer.Ordinal))
{
await using var stream=new FileStream(file,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);using var reader=new StreamReader(stream);var fid="";long ship=0;
while(await reader.ReadLineAsync(token) is{} line)
{
JsonDocument d;try{d=JsonDocument.Parse(line);}catch(JsonException){continue;}
using var parsed=d;var r=d.RootElement;var kind=r.TryGetProperty("event",out var e)?e.GetString():null;
if(kind=="LoadGame"){fid=r.TryGetProperty("FID",out var f)?f.GetString()??"":"";ship=r.TryGetProperty("ShipID",out var s)&&s.TryGetInt64(out var id)?id:0;}
if(kind=="Loadout")ship=r.TryGetProperty("ShipID",out var s)&&s.TryGetInt64(out var id)?id:0;
if(fid!=checkpoint.Commander||ship!=checkpoint.ShipId||!r.TryGetProperty("timestamp",out var t)||t.GetDateTimeOffset()<bought.ConfirmedAt)continue;
if(kind=="MarketBuy")
{
if(sale is null){conflict=true;break;}
var commodity=r.GetProperty("Type").GetString()!;var quantity=r.GetProperty("Count").GetInt32();var cost=r.GetProperty("TotalCost").GetInt64();
if(quantity<=0||cost<=0||cost!=(decimal)quantity*r.GetProperty("BuyPrice").GetInt64()){conflict=true;break;}
laterCargo[commodity]=laterCargo.GetValueOrDefault(commodity)+quantity;laterTradeCashChange-=cost;laterTrades.Add(r.Clone());continue;
}
if(kind!="MarketSell")continue;
if(sale is not null)
{
var commodity=r.GetProperty("Type").GetString()!;var quantity=r.GetProperty("Count").GetInt32();var proceeds=r.GetProperty("TotalSale").GetInt64();
if(quantity<=0||proceeds<=0||proceeds!=(decimal)quantity*r.GetProperty("SellPrice").GetInt64()||laterCargo.GetValueOrDefault(commodity)<quantity){conflict=true;break;}
laterCargo[commodity]-=quantity;laterTradeCashChange+=proceeds;laterTrades.Add(r.Clone());continue;
}
if(r.GetProperty("MarketID").GetInt64()!=leg.To.MarketId||r.GetProperty("Type").GetString()!=bought.Commodity
||r.GetProperty("Count").GetInt32()!=bought.QuantityDelta||r.GetProperty("AvgPricePaid").GetInt64()!=bought.UnitPrice
||r.GetProperty("TotalSale").GetInt64()!=(decimal)r.GetProperty("SellPrice").GetInt64()*bought.QuantityDelta){conflict=true;break;}
sale=r.Clone();
}
if(conflict)break;
}
if(conflict||sale is not{} sold||laterCargo.Values.Any(quantity=>quantity!=0))continue;
var queued=await channel.CallAsync(new{op="commerce.market.read",marketId=player.MarketId.Value,refreshClient=false},token);var generation=queued.GetProperty("generation").GetUInt64();JsonElement market=default;
for(var attempt=0;attempt<350;attempt++)
{market=await channel.CallAsync(new{op="commerce.market.status",generation,maximum=50},token);if(market.GetProperty("state").GetInt32()>=3)break;await Task.Delay(100,token);}
if(market.GetProperty("state").GetInt32()!=3||!market.TryGetProperty("cargo",out var cargo)||cargo.ValueKind!=JsonValueKind.Object||cargo.EnumerateObject().Any(p=>p.Value.GetInt64()!=0))continue;
var actual=await channel.CallAsync(new{op="commerce.inventory"},token);if(!actual.GetProperty("verified").GetBoolean()||actual.GetProperty("count").GetInt32()!=0||actual.GetProperty("credits").GetInt64()!=player.Credits)continue;
if(market.TryGetProperty("serverCredits",out var serverCredits)&&serverCredits.GetInt64()!=player.Credits)continue;
using var ledger=new TradeProfitLedger(Path.Combine(directory,"ledger"));ledger.RecordTrade(bought);
using var openingDocument=JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(directory,"opening-wallet.json"),token));var opening=openingDocument.RootElement.GetProperty("credits").GetInt64();
var key=Convert.ToHexStringLower(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(sold.GetRawText())));var revenue=sold.GetProperty("TotalSale").GetInt64();
var soldAt=sold.GetProperty("timestamp").GetDateTimeOffset();
var allFees=await ExternalServiceCosts.ReadAsync(journals,checkpoint.Commander,checkpoint.ShipId,bought.ConfirmedAt,[leg.From.MarketId,leg.To.MarketId],token);
var fees=allFees.Where(p=>p.Entry.At<=soldAt&&!ledger.ContainsOperation(p.Entry.OperationKey)).ToArray();var laterFees=allFees.Where(p=>p.Entry.At>soldAt).ToArray();
var afterSettlementCashChange=laterTradeCashChange-laterFees.Sum(p=>p.Entry.Amount);
var expected=opening+ledger.Snapshot.CashChange+(ledger.ContainsOperation(key)?0:revenue)-fees.Sum(p=>p.Entry.Amount);
var observedWallet=actual.GetProperty("credits").GetInt64();var closing=observedWallet-afterSettlementCashChange;
var walletReconciled=expected==closing;
ledger.Record(new(key,TradeCashKind.Sale,revenue,leg.To.MarketId,sold.GetProperty("timestamp").GetDateTimeOffset(),bought.Commodity,bought.QuantityDelta));
var feeProofs=Path.Combine(directory,"external-service-proofs");Directory.CreateDirectory(feeProofs);
foreach(var fee in fees){await Save(Path.Combine(feeProofs,fee.Entry.OperationKey+".json"),fee,token);ledger.Record(fee.Entry);}
var proof=new{status="manual-sale-confirmed",proofKind="official-manual-sale-after-native-failure",commanderId=checkpoint.Commander,shipId=checkpoint.ShipId,
operationKey=Path.GetFileName(pending).Replace(".pending.json",""),purchaseOperationKey=bought.OperationKey,
pendingSha256=Convert.ToHexStringLower(SHA256.HashData(await File.ReadAllBytesAsync(pending,token))),saleProof=sold,actualInventory=actual,originalServerCargo=cargo,
observedWallet,afterSettlementCashChange,laterTrades,laterFees,historicalClosingWalletFromVerifiedJournalChain=closing,laterManualTradesIncludedInAutomationProfit=false,
walletReconciled,expectedClosingFromConfirmedReceipts=expected,unexplainedWalletDifference=closing-expected,
originalNativeReceiptRewritten=false,nativeJourneyPassed=false,at=DateTimeOffset.UtcNow};
var marker=pending.Replace(".pending.json",".externally-settled.json",StringComparison.Ordinal);
await Save(marker,proof,token);
await Save(Path.Combine(directory,"external-result.json"),new{passed=walletReconciled,tradeSettled=true,walletReconciled,manual=true,nativeJourneyPassed=false,profit=ledger.Snapshot,openingCredits=opening,closingCredits=closing,finished=DateTimeOffset.UtcNow,proof=marker},token);
await Save(Path.Combine(directory,"external-completion.json"),proof,token);
await Save(Path.Combine(directory,"settled.json"),new{manual=true,nativeJourneyPassed=false,at=DateTimeOffset.UtcNow},token);
}
}
private static async Task Save(string file,object value,CancellationToken token)
{await File.WriteAllTextAsync(file+".tmp",JsonSerializer.Serialize(value,ToolboxSettings.JsonOptions),token);File.Move(file+".tmp",file,true);}
}