Editor/Trust/TrustStore.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Sandbox.SecBox;

// JSON-backed trust store. Persists to <projectRoot>/.secbox/trust.json.
// Layout:
//   { "Policy": {...}, "Entries": [TrustEntry, ...] }
// Hand-editable (intentionally) so users can revoke or audit without UI.
public sealed class TrustStore
{
	const string Subfolder = ".secbox";
	const string FileName = "trust.json";

	static readonly JsonSerializerOptions JsonOpts = new()
	{
		WriteIndented = true,
		PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
		Converters = { new JsonStringEnumConverter() },
		DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
	};

	public Policy Policy { get; set; } = new();
	public List<TrustEntry> Entries { get; set; } = new();

	// Bumped on every Save() so listeners (status panel) can detect changes
	// without rereading the file or comparing entry contents.
	static int _version;
	public static int Version => System.Threading.Volatile.Read( ref _version );

	[JsonIgnore]
	public string FilePath { get; private set; }

	public static TrustStore Load( string projectRoot )
	{
		var path = Path.Combine( projectRoot, Subfolder, FileName );
		if ( !File.Exists( path ) )
			return new TrustStore { FilePath = path };

		try
		{
			var json = File.ReadAllText( path );
			var store = JsonSerializer.Deserialize<TrustStore>( json, JsonOpts ) ?? new TrustStore();
			store.FilePath = path;
			store.Policy ??= new Policy();
			store.Entries ??= new List<TrustEntry>();
			return store;
		}
		catch ( Exception ex )
		{
			// Corrupt store: preserve the file under a sidecar name so user
			// can recover, return a fresh store rather than crashing.
			try { File.Move( path, path + $".corrupt-{DateTime.UtcNow:yyyyMMddHHmmss}", overwrite: true ); }
			catch { }
			global::Sandbox.Internal.GlobalGameNamespace.Log.Warning( $"[secbox] trust store unreadable ({ex.Message}); starting fresh" );
			return new TrustStore { FilePath = path };
		}
	}

	public void Save()
	{
		if ( string.IsNullOrEmpty( FilePath ) )
			throw new InvalidOperationException( "TrustStore.FilePath not set; call Load first or construct via Create()." );

		var dir = Path.GetDirectoryName( FilePath );
		if ( !string.IsNullOrEmpty( dir ) && !Directory.Exists( dir ) )
			Directory.CreateDirectory( dir );

		var json = JsonSerializer.Serialize( this, JsonOpts );
		File.WriteAllText( FilePath, json );
		System.Threading.Interlocked.Increment( ref _version );
	}

	// Look up by hash. The hash is the authoritative key.
	public TrustEntry Find( string contentHash )
	{
		for ( int i = 0; i < Entries.Count; i++ )
		{
			if ( Entries[i].ContentHash == contentHash )
				return Entries[i];
		}
		return null;
	}

	public TrustEntry Upsert( TrustEntry entry )
	{
		for ( int i = 0; i < Entries.Count; i++ )
		{
			if ( Entries[i].ContentHash == entry.ContentHash )
			{
				Entries[i] = entry;
				return entry;
			}
		}
		Entries.Add( entry );
		return entry;
	}

	public bool Remove( string contentHash )
	{
		for ( int i = 0; i < Entries.Count; i++ )
		{
			if ( Entries[i].ContentHash == contentHash )
			{
				Entries.RemoveAt( i );
				return true;
			}
		}
		return false;
	}
}